From a92d61db8a583d451dbb75ea1f11f20dfebefcc6 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sat, 20 Jun 2026 17:53:56 -0700 Subject: [PATCH] identity: redesign e-ID + passport to match the Digital Kurdistan brand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuild the /identity ID card and passport as flip cards rendered fully in code (HTML/CSS/SVG) with each citizen's real data: - e-ID: holographic light card, front (BÊ KURDISTAN JÎYANE NÎNE, photo, gold chip, bilingual fields NAV/NAME · PASNAV/SURNAME · DATE OF BIRTH · NATIONALITY KURDISTANÎ/KURDISH · ID NUMBER, faux-QR, e-ID: KOMARA KURDISTAN footer) + back (DIGITAL KURDISTAN STATE: ID number, digital wallet ID, biometric status, valid-until, gov services, support; authority seal). - Passport: navy cover with the gold ram (mouflon) emblem + Sorani/English titles, flips to a holographic data page (flag stripe, bilingual fields, photo, ICAO-style MRZ). - Adds a Surname field and pulls the connected wallet address for the e-ID back. Document labels are the official bilingual set (not UI-translated); data stays device-local. Tap ↻ to flip. --- web/src/pages/Identity.tsx | 636 ++++++++++++++++++++----------------- 1 file changed, 353 insertions(+), 283 deletions(-) diff --git a/web/src/pages/Identity.tsx b/web/src/pages/Identity.tsx index 7473ee62..21de58ff 100644 --- a/web/src/pages/Identity.tsx +++ b/web/src/pages/Identity.tsx @@ -4,11 +4,16 @@ import { useTranslation } from 'react-i18next'; import { useIsMobile } from '@/hooks/use-mobile'; import MobileShell from '@/components/MobileShell'; import { useDashboard } from '@/contexts/DashboardContext'; -import { Save, CreditCard, BookOpen, Camera } from 'lucide-react'; +import { usePezkuwi } from '@/contexts/PezkuwiContext'; +import { + Save, CreditCard, BookOpen, Camera, RotateCw, + Hash, Wallet, ScanFace, CalendarClock, Landmark, Phone, +} from 'lucide-react'; // ── Types ── interface IdentityData { - fullName: string; + fullName: string; // given name(s) + surname: string; fatherName: string; motherName: string; dateOfBirth: string; @@ -21,19 +26,13 @@ interface IdentityData { } const DEFAULT_DATA: IdentityData = { - fullName: '', - fatherName: '', - motherName: '', - dateOfBirth: '', - placeOfBirth: '', - gender: '', - bloodType: '', - citizenNumber: '', - passportNumber: '', - photo: '', + fullName: '', surname: '', fatherName: '', motherName: '', + dateOfBirth: '', placeOfBirth: '', gender: '', bloodType: '', + citizenNumber: '', passportNumber: '', photo: '', }; const STORAGE_KEY = 'pezkuwi_identity_data'; +const SUN = '/kurdistan_sun_light.svg'; // ── Helpers ── function generatePassportNo(citizenNo: string): string { @@ -42,12 +41,17 @@ function generatePassportNo(citizenNo: string): string { } function formatMRZ(data: IdentityData): [string, string] { - const name = data.fullName.toUpperCase().replace(/[^A-Z ]/g, '').replace(/ /g, '<') || 'SURNAME< { const d = new Date(); d.setFullYear(d.getFullYear() + 10); return d.toISOString().slice(2, 10).replace(/-/g, ''); })(); + const sex = data.gender || '<'; + const pno = (data.passportNumber.replace(/[^A-Z0-9]/g, '') || 'A0000000').padEnd(9, '<').slice(0, 9); + const line2 = `${pno}1KUD${dob}1${sex}${exp}${'<'.repeat(14)}00`.slice(0, 44); return [line1, line2]; } @@ -56,32 +60,30 @@ const issueDate = today.toLocaleDateString('en-GB', { day: '2-digit', month: '2- const expiryDate = new Date(today.getFullYear() + 10, today.getMonth(), today.getDate()) .toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }); +function fmtDOB(v: string) { return v ? new Date(v).toLocaleDateString('en-GB') : '—'; } +function shortAddr(a?: string | null) { return a ? `${a.slice(0, 8)}…${a.slice(-6)}` : '—'; } + // ── Main Component ── export default function Identity() { const { t } = useTranslation(); const navigate = useNavigate(); const isMobile = useIsMobile(); const { citizenNumber: nftCitizenNumber } = useDashboard(); + const { selectedAccount } = usePezkuwi(); + const walletAddr = selectedAccount?.address || ''; const [tab, setTab] = useState<'id' | 'passport'>('id'); const [data, setData] = useState(DEFAULT_DATA); const [saved, setSaved] = useState(false); + const [idBack, setIdBack] = useState(false); + const [passData, setPassData] = useState(false); - // Load from localStorage useEffect(() => { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (raw) setData(JSON.parse(raw)); - } catch { /* ignore */ } + try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) setData({ ...DEFAULT_DATA, ...JSON.parse(raw) }); } catch { /* ignore */ } }, []); - // Sync citizen number from role card NFT useEffect(() => { if (nftCitizenNumber && nftCitizenNumber !== 'N/A') { - setData(prev => ({ - ...prev, - citizenNumber: nftCitizenNumber, - passportNumber: generatePassportNo(nftCitizenNumber), - })); + setData(prev => ({ ...prev, citizenNumber: nftCitizenNumber, passportNumber: generatePassportNo(nftCitizenNumber) })); } }, [nftCitizenNumber]); @@ -90,22 +92,17 @@ export default function Identity() { const handlePhotoSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; - // Resize to max 300px and compress const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); - const maxSize = 300; + const maxSize = 320; let w = img.width, h = img.height; - if (w > h) { h = (h / w) * maxSize; w = maxSize; } - else { w = (w / h) * maxSize; h = maxSize; } - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext('2d')!; - ctx.drawImage(img, 0, 0, w, h); - const dataUrl = canvas.toDataURL('image/jpeg', 0.8); - setData(prev => ({ ...prev, photo: dataUrl })); + if (w > h) { h = (h / w) * maxSize; w = maxSize; } else { w = (w / h) * maxSize; h = maxSize; } + canvas.width = w; canvas.height = h; + canvas.getContext('2d')!.drawImage(img, 0, 0, w, h); + setData(prev => ({ ...prev, photo: canvas.toDataURL('image/jpeg', 0.82) })); setSaved(false); }; img.src = reader.result as string; @@ -116,9 +113,7 @@ export default function Identity() { const handleChange = (field: keyof IdentityData, value: string) => { setData(prev => { const next = { ...prev, [field]: value }; - if (field === 'citizenNumber') { - next.passportNumber = generatePassportNo(value); - } + if (field === 'citizenNumber') next.passportNumber = generatePassportNo(value); return next; }); setSaved(false); @@ -131,75 +126,61 @@ export default function Identity() { }; const [mrzLine1, mrzLine2] = formatMRZ(data); + const openPhoto = () => photoInputRef.current?.click(); const content = (
{/* Tab switcher */}
- -
- {/* Hidden file input for photo */} - + - {/* ── CARD PREVIEW ── */} {tab === 'id' ? ( - photoInputRef.current?.click()} /> + setIdBack(v => !v)} + front={} + back={} /> ) : ( - photoInputRef.current?.click()} /> + setPassData(v => !v)} + front={} + back={} /> )} +

+ {tab === 'id' + ? t('identity.tapFlipId', 'Tap ↻ to see the back of your e-ID') + : t('identity.tapFlipPass', 'Tap ↻ to open the passport data page')} +

+ {/* ── FORM ── */}
-

- {t('identity.personalInfo', 'Personal Information')} -

+

{t('identity.personalInfo', 'Personal Information')}

- handleChange('fullName', v)} placeholder="Azad Kurdistanî" /> - - handleChange('fatherName', v)} placeholder="Rêber" /> - - handleChange('motherName', v)} placeholder="Jîn" /> +
+ handleChange('fullName', v)} placeholder="Azad" /> + handleChange('surname', v)} placeholder="Kurdistanî" /> +
handleChange('dateOfBirth', v)} type="date" /> -
- - handleChange('gender', e.target.value as 'M' | 'F' | '')} + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white"> @@ -207,62 +188,30 @@ export default function Identity() {
-
- handleChange('placeOfBirth', v)} placeholder="Hewlêr" /> + handleChange('placeOfBirth', v)} placeholder="Hewlêr" /> -
- - -
-
- - handleChange('citizenNumber', v)} placeholder="KRD-000000" readOnly={!!(nftCitizenNumber && nftCitizenNumber !== 'N/A')} /> - {/* Save button */} -

{t('identity.localOnly', 'Data is stored only on your device. Never uploaded.')}

-
); - if (isMobile) { - return ( - - {content} - - ); - } + if (isMobile) return {content}; - // Desktop: simple centered layout return (
@@ -275,181 +224,308 @@ export default function Identity() { ); } -// ── ID Card Preview ── -function IDCardPreview({ data, onPhotoClick }: { data: IdentityData; onPhotoClick: () => void }) { - return ( -
- - {/* Top stripe - Kurdistan flag colors */} -
-
-
-
-
- - {/* Header */} -
-
-

KOMARA KURDISTANÊ

-

KURDISTAN REPUBLIC

-
- {/* Sun emblem */} -
- Kurdistan Sun -
-
-

کۆماری کوردستان

-

NASNAMA / ID CARD

-
-
- - {/* Divider */} -
- - {/* Body */} -
- {/* Photo area - clickable */} - - - {/* Info */} -
- - - -
- - -
- -
-
- - {/* Bottom bar */} -
-
-

JIM / NO

-

- {data.citizenNumber || 'KRD-000000'} -

-
-
-

XWÎNê / Blood

-

{data.bloodType || '—'}

-
-
-

DERBASDAR / Expiry

-

{expiryDate}

-
-
-
- ); -} - -// ── Passport Preview ── -function PassportPreview({ data, mrzLine1, mrzLine2, onPhotoClick }: { - data: IdentityData; mrzLine1: string; mrzLine2: string; onPhotoClick: () => void; +// ════════════════════════════════════════════════════════════ +// Flip card shell +// ════════════════════════════════════════════════════════════ +function FlipCard({ flipped, aspectClass, front, back, onFlip }: { + flipped: boolean; aspectClass: string; front: React.ReactNode; back: React.ReactNode; onFlip: () => void; }) { return ( -
- - {/* Top ornament */} -
- - {/* Header */} -
-

KOMARA KURDISTANÊ

-

KURDISTAN REPUBLIC

- - {/* Emblem */} -
- Kurdistan Sun -
- -

پاسپۆرت

-

PASSPORT

+
+
+
{front}
+
{back}
+ +
+ ); +} - {/* Divider */} -
+// Holographic light surface (e-ID) +const HOLO_BG: React.CSSProperties = { + background: + 'linear-gradient(135deg,#eef3f8 0%,#f3ecf6 22%,#e9f5f0 46%,#f5f0e6 68%,#eaf0f8 100%)', +}; +function HoloSheen() { + return ( +
+ ); +} - {/* Data page */} -
+// Newroz-sun emblem in a soft disc +function SunEmblem({ size = 34, className = '' }: { size?: number; className?: string }) { + return ( + + + + ); +} -
- {/* Photo - clickable */} - +// Stylized gold ram (mouflon) emblem — passport signature +function RamEmblem({ size = 96, className = '' }: { size?: number; className?: string }) { + return ( + + + + + + + + + + + + + + + + + + ); +} -
- - - +// Decorative QR-style block (deterministic from the ID, not a scannable code) +function FauxQR({ seed, size = 46 }: { seed: string; size?: number }) { + const n = 11; + let h = 2166136261; for (const c of (seed || 'KRD')) { h ^= c.charCodeAt(0); h = (h * 16777619) >>> 0; } + const rnd = () => { h = (h * 1103515245 + 12345) & 0x7fffffff; return h / 0x7fffffff; }; + const cells: React.ReactNode[] = []; + const finder = (x: number, y: number) => (x < 3 && y < 3) || (x > n - 4 && y < 3) || (x < 3 && y > n - 4); + for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { + if (finder(x, y)) continue; + if (rnd() > 0.52) cells.push(); + } + const Finder = ({ x, y }: { x: number; y: number }) => ( + + ); + return ( + + + {cells} + + + ); +} + +function PhotoBox({ photo, onClick, className = '' }: { photo: string; onClick: () => void; className?: string }) { + return ( + + ); +} + +// ════════════════════════════════════════════════════════════ +// e-ID — FRONT +// ════════════════════════════════════════════════════════════ +function IDFront({ data, walletAddr, onPhotoClick }: { data: IdentityData; walletAddr: string; onPhotoClick: () => void }) { + return ( +
+ + {/* faint sun watermark */} + + +
+ {/* header */} +
+
+

+ BÊ KURDISTAN JÎYANE NÎNE +

+

+ DEMOCRACY · FEDERALISM · PEACE · DIGITAL INNOVATION +

+
- -
- - -
-
- - -
-
- - + {/* body */} +
+
+ + {/* gold chip + DIGITAL ID */} +
+

DIGITAL ID

+
+ +
+ + + + + +
+ +
- {/* MRZ Zone */} -
-

{mrzLine1}

-

{mrzLine2}

+ {/* footer bar */} +
+ e-ID: KOMARA KURDISTAN + کۆماری کوردستان
); } -// ── Shared sub-components ── -function IDField({ label, value, bold }: { label: string; value: string; bold?: boolean }) { +// ════════════════════════════════════════════════════════════ +// e-ID — BACK +// ════════════════════════════════════════════════════════════ +function IDBack({ data, walletAddr }: { data: IdentityData; walletAddr: string }) { + const rows = [ + { Icon: Hash, label: 'ID NUMBER', val: data.citizenNumber || 'KRD-000000' }, + { Icon: Wallet, label: 'DIGITAL WALLET ID', val: shortAddr(walletAddr) }, + { Icon: ScanFace, label: 'BIOMETRIC ENROLMENT', val: 'YES — Face / Fingerprint' }, + { Icon: CalendarClock, label: 'VALID UNTIL', val: expiryDate }, + { Icon: Landmark, label: 'ACCESS GOVERNMENT SERVICES', val: 'apps.pezkuwichain.io' }, + { Icon: Phone, label: 'SUPPORT & INFORMATION', val: 'pezkuwichain.io' }, + ]; return ( -
-

{label}

-

{value}

+
+ + + +
+

+ DIGITAL KURDISTAN STATE +

+
+ +
+ {rows.map(({ Icon, label, val }) => ( +
+ + + +
+

{label}

+

{val}

+
+
+ ))} +
+ +
+
+

DIGITAL IDENTITY COMMISSION

+

Digital Identity Authority · Kurdistan State

+
+ +
+
); } -function PassField({ label, value, bold, mono }: { label: string; value: string; bold?: boolean; mono?: boolean }) { +// ════════════════════════════════════════════════════════════ +// Passport — COVER (navy + gold ram) +// ════════════════════════════════════════════════════════════ +function PassportCover() { return ( -
-

{label}

-

{value}

+
+
+ +
+

پاسپۆرتی کۆماری کوردستان

+

REPUBLIC OF DIJITAL KURDISTAN

+
+ + + +
+

PASSPORT

+

PASAPORTA KOMARA KURDISTAN

+ +
+
+ ); +} + +// ════════════════════════════════════════════════════════════ +// Passport — DATA PAGE +// ════════════════════════════════════════════════════════════ +function PassportDataPage({ data, mrzLine1, mrzLine2, onPhotoClick }: { + data: IdentityData; mrzLine1: string; mrzLine2: string; onPhotoClick: () => void; +}) { + const sex = data.gender === 'M' ? 'NÊR / MALE' : data.gender === 'F' ? 'JIN / FEMALE' : '—'; + return ( +
+ + + +
+ {/* header with flag stripe */} +
+ + + + +
+

KOMARA KURDISTAN

+

REPUBLIC OF DIJITAL KURDISTAN

+
+

کۆماری کوردستان

+
+ +
+ +
+
+
+ + +
+ + + + +
+ + +
+
+ +
+ + {/* MRZ */} +
+

{mrzLine1}

+

{mrzLine2}

+
+
+
+ ); +} + +// ── small field renderer (document style) ── +function Field({ k, v, strong, mono }: { k: string; v: string; strong?: boolean; mono?: boolean }) { + return ( +
+

{k}

+

{v}

); } @@ -460,15 +536,9 @@ function FormField({ label, value, onChange, placeholder, type = 'text', readOnl return (
- onChange(e.target.value)} - placeholder={placeholder} - readOnly={readOnly} + onChange(e.target.value)} placeholder={placeholder} readOnly={readOnly} className={`w-full bg-gray-800 border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-none transition-colors - ${readOnly ? 'border-gray-600 text-gray-400 cursor-default' : 'border-gray-700 focus:border-green-500'}`} - /> + ${readOnly ? 'border-gray-600 text-gray-400 cursor-default' : 'border-gray-700 focus:border-amber-500'}`} />
); }