mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-06-15 11:31:15 +00:00
feat: add Be Citizen page with 6-language support
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* Citizen Application Form
|
||||
* Collects citizenship data from the user
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { useTranslation } from '@/i18n';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import type { CitizenshipData, Region, MaritalStatus, ChildInfo } from '@/lib/citizenship';
|
||||
|
||||
interface Props {
|
||||
walletAddress: string;
|
||||
onSubmit: (data: CitizenshipData) => void;
|
||||
}
|
||||
|
||||
const REGIONS: { value: Region; labelKey: string }[] = [
|
||||
{ value: 'bakur', labelKey: 'citizen.regionBakur' },
|
||||
{ value: 'basur', labelKey: 'citizen.regionBasur' },
|
||||
{ value: 'rojava', labelKey: 'citizen.regionRojava' },
|
||||
{ value: 'rojhelat', labelKey: 'citizen.regionRojhelat' },
|
||||
{ value: 'kurdistan_a_sor', labelKey: 'citizen.regionKurdistanASor' },
|
||||
{ value: 'diaspora', labelKey: 'citizen.regionDiaspora' },
|
||||
];
|
||||
|
||||
export function CitizenForm({ walletAddress, onSubmit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { hapticImpact, hapticNotification } = useTelegram();
|
||||
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [fatherName, setFatherName] = useState('');
|
||||
const [grandfatherName, setGrandfatherName] = useState('');
|
||||
const [motherName, setMotherName] = useState('');
|
||||
const [tribe, setTribe] = useState('');
|
||||
const [maritalStatus, setMaritalStatus] = useState<MaritalStatus>('nezewici');
|
||||
const [childrenCount, setChildrenCount] = useState(0);
|
||||
const [children, setChildren] = useState<ChildInfo[]>([]);
|
||||
const [region, setRegion] = useState<Region | ''>('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [profession, setProfession] = useState('');
|
||||
const [referralCode, setReferralCode] = useState('');
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleMaritalChange = (status: MaritalStatus) => {
|
||||
hapticImpact('light');
|
||||
setMaritalStatus(status);
|
||||
if (status === 'nezewici') {
|
||||
setChildrenCount(0);
|
||||
setChildren([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChildrenCountChange = (count: number) => {
|
||||
const c = Math.max(0, Math.min(20, count));
|
||||
setChildrenCount(c);
|
||||
setChildren((prev) => {
|
||||
if (c > prev.length) {
|
||||
return [
|
||||
...prev,
|
||||
...Array.from({ length: c - prev.length }, () => ({ name: '', birthYear: 2000 })),
|
||||
];
|
||||
}
|
||||
return prev.slice(0, c);
|
||||
});
|
||||
};
|
||||
|
||||
const updateChild = (index: number, field: keyof ChildInfo, value: string | number) => {
|
||||
setChildren((prev) =>
|
||||
prev.map((child, i) => (i === index ? { ...child, [field]: value } : child))
|
||||
);
|
||||
};
|
||||
|
||||
const addChild = () => {
|
||||
hapticImpact('light');
|
||||
handleChildrenCountChange(childrenCount + 1);
|
||||
};
|
||||
|
||||
const removeChild = (index: number) => {
|
||||
hapticImpact('light');
|
||||
setChildren((prev) => prev.filter((_, i) => i !== index));
|
||||
setChildrenCount((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setError('');
|
||||
|
||||
if (
|
||||
!fullName ||
|
||||
!fatherName ||
|
||||
!grandfatherName ||
|
||||
!motherName ||
|
||||
!tribe ||
|
||||
!region ||
|
||||
!email ||
|
||||
!profession
|
||||
) {
|
||||
setError(t('citizen.fillAllFields'));
|
||||
hapticNotification('error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consent) {
|
||||
setError(t('citizen.acceptConsent'));
|
||||
hapticNotification('error');
|
||||
return;
|
||||
}
|
||||
|
||||
hapticImpact('medium');
|
||||
|
||||
const data: CitizenshipData = {
|
||||
fullName,
|
||||
fatherName,
|
||||
grandfatherName,
|
||||
motherName,
|
||||
tribe,
|
||||
maritalStatus,
|
||||
childrenCount: maritalStatus === 'zewici' ? childrenCount : undefined,
|
||||
children: maritalStatus === 'zewici' && children.length > 0 ? children : undefined,
|
||||
region: region as Region,
|
||||
email,
|
||||
profession,
|
||||
referralCode: referralCode || undefined,
|
||||
walletAddress,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
const inputClass = 'w-full px-4 py-3 bg-muted rounded-xl text-sm';
|
||||
const labelClass = 'text-sm text-muted-foreground mb-1 block';
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4 pb-24">
|
||||
{/* Full Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.fullName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.fullNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Father's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.fatherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fatherName}
|
||||
onChange={(e) => setFatherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.fatherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Grandfather's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.grandfatherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={grandfatherName}
|
||||
onChange={(e) => setGrandfatherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.grandfatherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mother's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.motherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={motherName}
|
||||
onChange={(e) => setMotherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.motherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tribe */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.tribe')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tribe}
|
||||
onChange={(e) => setTribe(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.tribePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Marital Status */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.maritalStatus')}</label>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMaritalChange('nezewici')}
|
||||
className={`flex-1 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
maritalStatus === 'nezewici'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('citizen.single')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMaritalChange('zewici')}
|
||||
className={`flex-1 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
maritalStatus === 'zewici'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('citizen.married')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Children (if married) */}
|
||||
{maritalStatus === 'zewici' && (
|
||||
<div className="space-y-3">
|
||||
<label className={labelClass}>{t('citizen.childrenCount')}</label>
|
||||
{children.map((child, index) => (
|
||||
<div key={index} className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t('citizen.childName', { index: String(index + 1) })}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={child.name}
|
||||
onChange={(e) => updateChild(index, 'name', e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.childNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t('citizen.childBirthYear')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={child.birthYear}
|
||||
onChange={(e) =>
|
||||
updateChild(index, 'birthYear', parseInt(e.target.value) || 2000)
|
||||
}
|
||||
className={inputClass}
|
||||
min={1950}
|
||||
max={2026}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeChild(index)}
|
||||
className="p-3 text-red-400 hover:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addChild}
|
||||
className="flex items-center gap-2 text-sm text-primary"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('citizen.addChild')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Region */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.region')}</label>
|
||||
<select
|
||||
value={region}
|
||||
onChange={(e) => {
|
||||
setRegion(e.target.value as Region);
|
||||
hapticImpact('light');
|
||||
}}
|
||||
className={`${inputClass} appearance-none`}
|
||||
>
|
||||
<option value="">{t('citizen.regionPlaceholder')}</option>
|
||||
{REGIONS.map((r) => (
|
||||
<option key={r.value} value={r.value}>
|
||||
{t(r.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.email')}</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.emailPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profession */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.profession')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={profession}
|
||||
onChange={(e) => setProfession(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.professionPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Referral Code */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.referralCode')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={referralCode}
|
||||
onChange={(e) => setReferralCode(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.referralCodePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Consent */}
|
||||
<label className="flex items-start gap-3 p-3 bg-muted/50 rounded-xl cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={consent}
|
||||
onChange={(e) => setConsent(e.target.checked)}
|
||||
className="mt-1 w-5 h-5 rounded accent-primary flex-shrink-0"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">{t('citizen.consentCheckbox')}</span>
|
||||
</label>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!consent || !fullName || !region || !email}
|
||||
className="w-full py-3 bg-primary text-primary-foreground rounded-xl font-semibold disabled:opacity-50"
|
||||
>
|
||||
{t('citizen.submit')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Citizen Processing Component
|
||||
* Shows KurdistanSun animation while preparing data,
|
||||
* then enables sign button when ready
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { KurdistanSun } from '@/components/KurdistanSun';
|
||||
import { useTranslation } from '@/i18n';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import type { CitizenshipData } from '@/lib/citizenship';
|
||||
import {
|
||||
generateCommitmentHash,
|
||||
generateNullifierHash,
|
||||
saveCitizenshipLocally,
|
||||
uploadToIPFS,
|
||||
submitCitizenshipApplication,
|
||||
} from '@/lib/citizenship';
|
||||
|
||||
interface Props {
|
||||
citizenshipData: CitizenshipData;
|
||||
onSuccess: (blockHash?: string) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
type ProcessingState = 'preparing' | 'ready' | 'signing';
|
||||
|
||||
export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { hapticImpact, hapticNotification } = useTelegram();
|
||||
const { peopleApi, keypair } = useWallet();
|
||||
|
||||
const [state, setState] = useState<ProcessingState>('preparing');
|
||||
const [ipfsCid, setIpfsCid] = useState<string>('');
|
||||
|
||||
// Prepare data on mount
|
||||
useEffect(() => {
|
||||
const prepare = async () => {
|
||||
try {
|
||||
// Generate commitment hash
|
||||
generateCommitmentHash(citizenshipData);
|
||||
generateNullifierHash(citizenshipData.walletAddress, citizenshipData.timestamp);
|
||||
|
||||
// Save encrypted data locally
|
||||
saveCitizenshipLocally(citizenshipData);
|
||||
|
||||
// Mock IPFS upload
|
||||
const cid = await uploadToIPFS(citizenshipData);
|
||||
setIpfsCid(cid);
|
||||
|
||||
// Small delay to show animation
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
setState('ready');
|
||||
hapticNotification('success');
|
||||
} catch (err) {
|
||||
onError(err instanceof Error ? err.message : 'Preparation failed');
|
||||
}
|
||||
};
|
||||
|
||||
prepare();
|
||||
}, [citizenshipData, hapticNotification, onError]);
|
||||
|
||||
const handleSign = useCallback(async () => {
|
||||
if (!peopleApi || !keypair) {
|
||||
onError(t('citizen.walletNotConnected'));
|
||||
return;
|
||||
}
|
||||
|
||||
setState('signing');
|
||||
hapticImpact('medium');
|
||||
|
||||
try {
|
||||
const result = await submitCitizenshipApplication(
|
||||
peopleApi,
|
||||
keypair,
|
||||
citizenshipData.fullName,
|
||||
citizenshipData.email,
|
||||
ipfsCid,
|
||||
`Citizenship application - ${citizenshipData.region}`
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
hapticNotification('success');
|
||||
onSuccess(result.blockHash);
|
||||
} else {
|
||||
hapticNotification('error');
|
||||
onError(result.error || t('citizen.submissionFailed'));
|
||||
}
|
||||
} catch (err) {
|
||||
hapticNotification('error');
|
||||
onError(err instanceof Error ? err.message : t('citizen.submissionFailed'));
|
||||
}
|
||||
}, [
|
||||
peopleApi,
|
||||
keypair,
|
||||
citizenshipData,
|
||||
ipfsCid,
|
||||
hapticImpact,
|
||||
hapticNotification,
|
||||
onSuccess,
|
||||
onError,
|
||||
t,
|
||||
]);
|
||||
|
||||
const isReady = state === 'ready';
|
||||
const isSigning = state === 'signing';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] p-6 space-y-8">
|
||||
{/* Kurdistan Sun Animation */}
|
||||
<div className={state === 'ready' ? 'opacity-80' : ''}>
|
||||
<KurdistanSun size={100} />
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-lg font-medium">
|
||||
{state === 'preparing' && t('citizen.preparingData')}
|
||||
{state === 'ready' && t('citizen.readyToSign')}
|
||||
{state === 'signing' && t('citizen.signingTx')}
|
||||
</p>
|
||||
{state === 'preparing' && (
|
||||
<p className="text-sm text-muted-foreground">{citizenshipData.fullName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sign Button */}
|
||||
<button
|
||||
onClick={handleSign}
|
||||
disabled={!isReady || isSigning}
|
||||
className={`w-full max-w-xs py-4 rounded-xl font-bold text-lg transition-all ${
|
||||
isReady
|
||||
? 'bg-green-600 text-white hover:bg-green-500 active:scale-95'
|
||||
: 'bg-muted text-muted-foreground cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isSigning ? t('citizen.signingTx') : t('citizen.sign')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Citizen Success Screen
|
||||
* Shows after successful citizenship application submission
|
||||
*/
|
||||
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { useTranslation } from '@/i18n';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import { formatAddress } from '@/lib/wallet-service';
|
||||
import { generateCitizenNumber } from '@/lib/citizenship';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
onOpenApp: () => void;
|
||||
}
|
||||
|
||||
export function CitizenSuccess({ address, onOpenApp }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { hapticImpact } = useTelegram();
|
||||
|
||||
// Generate a citizen number based on address
|
||||
const citizenNumber = generateCitizenNumber(address, 42, 0);
|
||||
const citizenId = `#42-0-${citizenNumber}`;
|
||||
|
||||
const handleOpenApp = () => {
|
||||
hapticImpact('medium');
|
||||
onOpenApp();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[70vh] p-6 space-y-6">
|
||||
{/* Success Icon */}
|
||||
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-500" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-2xl font-bold">{t('citizen.successTitle')}</h1>
|
||||
<p className="text-muted-foreground">{t('citizen.successSubtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Citizen ID Card */}
|
||||
<div className="w-full max-w-sm bg-muted/50 rounded-2xl p-5 space-y-4 border border-border">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t('citizen.citizenId')}</p>
|
||||
<p className="text-xl font-mono font-bold text-primary">{citizenId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t('citizen.walletAddress')}</p>
|
||||
<p className="text-sm font-mono">{formatAddress(address)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open App Button */}
|
||||
<button
|
||||
onClick={handleOpenApp}
|
||||
className="w-full max-w-sm py-3 bg-primary text-primary-foreground rounded-xl font-semibold"
|
||||
>
|
||||
{t('citizen.openApp')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user