feat: add Be Citizen page with 6-language support

This commit is contained in:
2026-02-14 20:44:17 +03:00
parent c4282f5870
commit b8ab86028f
16 changed files with 1653 additions and 44 deletions
+362
View File
@@ -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>
);
}
+64
View File
@@ -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>
);
}