diff --git a/web/src/App.tsx b/web/src/App.tsx index 1119da75..355c2523 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -33,6 +33,7 @@ const AdminPanel = lazy(() => import('@/pages/AdminPanel')); const WalletDashboard = lazy(() => import('./pages/WalletDashboard')); const ReservesDashboardPage = lazy(() => import('./pages/ReservesDashboardPage')); const BeCitizen = lazy(() => import('./pages/BeCitizen')); +const Identity = lazy(() => import('./pages/Identity')); const Citizens = lazy(() => import('./pages/Citizens')); const CitizensIssues = lazy(() => import('./pages/citizens/CitizensIssues')); const GovernmentEntrance = lazy(() => import('./pages/citizens/GovernmentEntrance')); @@ -145,6 +146,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/web/src/components/AppLayout.tsx b/web/src/components/AppLayout.tsx index c0f7bb8f..397fb34e 100644 --- a/web/src/components/AppLayout.tsx +++ b/web/src/components/AppLayout.tsx @@ -31,6 +31,8 @@ import { PezkuwiWalletButton } from './PezkuwiWalletButton'; import { DEXDashboard } from './dex/DEXDashboard'; import { P2PDashboard } from './p2p/P2PDashboard'; import EducationPlatform from '../pages/EducationPlatform'; +import { useIsMobile } from '@/hooks/use-mobile'; +import MobileHomeLayout from './MobileHomeLayout'; const AppLayout: React.FC = () => { const navigate = useNavigate(); @@ -52,6 +54,7 @@ const AppLayout: React.FC = () => { const { t } = useTranslation(); const { isConnected } = useWebSocket(); useWallet(); + const isMobile = useIsMobile(); const [, _setIsAdmin] = useState(false); // Close dropdown on outside click @@ -72,6 +75,13 @@ const AppLayout: React.FC = () => { React.useEffect(() => { _setIsAdmin(false); // Admin status managed by AuthContext }, [user]); + // On mobile, when no feature panel is active, show the mobile home layout + const isFeaturePanelOpen = showDEX || showProposalWizard || showDelegation || showForum || showModeration || showTreasury || showStaking || showMultiSig || showEducation || showP2P; + + if (isMobile && !isFeaturePanelOpen) { + return ; + } + return (
{/* Navigation */} diff --git a/web/src/components/MobileHomeLayout.tsx b/web/src/components/MobileHomeLayout.tsx new file mode 100644 index 00000000..e41a1e19 --- /dev/null +++ b/web/src/components/MobileHomeLayout.tsx @@ -0,0 +1,403 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '@/contexts/AuthContext'; +import { usePezkuwi } from '@/contexts/PezkuwiContext'; +import { supabase } from '@/lib/supabase'; +import { LanguageSwitcher } from './LanguageSwitcher'; +import { PezkuwiWalletButton } from './PezkuwiWalletButton'; +import NotificationBell from './notifications/NotificationBell'; +import { fetchUserTikis, getPrimaryRole, getTikiDisplayName, getTikiEmoji } from '@pezkuwi/lib/tiki'; +import { getAllScores, type UserScores } from '@pezkuwi/lib/scores'; +import { getKycStatus } from '@pezkuwi/lib/kyc'; + +// Avatar pool (same as mobile) +const AVATAR_POOL = [ + { id: 'avatar1', emoji: '👨🏻' }, { id: 'avatar2', emoji: '👨🏼' }, + { id: 'avatar3', emoji: '👨🏽' }, { id: 'avatar4', emoji: '👨🏾' }, + { id: 'avatar5', emoji: '👩🏻' }, { id: 'avatar6', emoji: '👩🏼' }, + { id: 'avatar7', emoji: '👩🏽' }, { id: 'avatar8', emoji: '👩🏾' }, + { id: 'avatar9', emoji: '🧔🏻' }, { id: 'avatar10', emoji: '🧔🏼' }, + { id: 'avatar11', emoji: '🧔🏽' }, { id: 'avatar12', emoji: '🧔🏾' }, + { id: 'avatar13', emoji: '👳🏻‍♂️' }, { id: 'avatar14', emoji: '👳🏼‍♂️' }, + { id: 'avatar15', emoji: '👳🏽‍♂️' }, { id: 'avatar16', emoji: '🧕🏻' }, + { id: 'avatar17', emoji: '🧕🏼' }, { id: 'avatar18', emoji: '🧕🏽' }, +]; + +const getEmojiFromAvatarId = (avatarId: string): string => { + const avatar = AVATAR_POOL.find(a => a.id === avatarId); + return avatar ? avatar.emoji : '👤'; +}; + +// App icon definition +interface AppItem { + title: string; + icon: string; + route: string; + comingSoon?: boolean; + requiresAuth?: boolean; +} + +// Section definition +interface AppSection { + titleKey: string; + emoji: string; + borderColor: string; + apps: AppItem[]; +} + +const APP_SECTIONS: AppSection[] = [ + { + titleKey: 'FINANCE', + emoji: '💰', + borderColor: 'border-l-green-500', + apps: [ + { title: 'Wallet', icon: '👛', route: '/wallet' }, + { title: 'Bank', icon: '🏦', route: '/wallet', comingSoon: true }, + { title: 'Exchange', icon: '💱', route: '/dex', requiresAuth: true }, + { title: 'P2P', icon: '🤝', route: '/p2p', requiresAuth: true }, + { title: 'B2B', icon: '🤖', route: '/wallet', comingSoon: true }, + { title: 'Bac/Zekat', icon: '💰', route: '/wallet', comingSoon: true }, + { title: 'Launchpad', icon: '🚀', route: '/launchpad' }, + ], + }, + { + titleKey: 'GOVERNANCE', + emoji: '🏛️', + borderColor: 'border-l-red-500', + apps: [ + { title: 'President', icon: '👑', route: '/elections', requiresAuth: true }, + { title: 'Assembly', icon: '🏛️', route: '/citizens/government', comingSoon: true }, + { title: 'Vote', icon: '🗳️', route: '/elections', requiresAuth: true }, + { title: 'Validators', icon: '🛡️', route: '/wallet' }, + { title: 'Justice', icon: '⚖️', route: '/citizens/government', comingSoon: true }, + { title: 'Proposals', icon: '📜', route: '/citizens/government' }, + { title: 'Polls', icon: '📊', route: '/citizens/government', comingSoon: true }, + { title: 'Identity', icon: '🆔', route: '/identity' }, + ], + }, + { + titleKey: 'SOCIAL', + emoji: '💬', + borderColor: 'border-l-blue-500', + apps: [ + { title: 'whatsKURD', icon: '💬', route: '/message', comingSoon: true }, + { title: 'Forum', icon: '📰', route: '/forum' }, + { title: 'KurdMedia', icon: '📺', route: '/forum', comingSoon: true }, + { title: 'Events', icon: '📅', route: '/forum', comingSoon: true }, + { title: 'Help', icon: '❓', route: '/docs' }, + { title: 'Music', icon: '🎵', route: '/forum', comingSoon: true }, + { title: 'VPN', icon: '🛡️', route: '/forum', comingSoon: true }, + { title: 'Referral', icon: '👥', route: '/dashboard', requiresAuth: true }, + ], + }, + { + titleKey: 'EDUCATION', + emoji: '📚', + borderColor: 'border-l-yellow-500', + apps: [ + { title: 'University', icon: '🎓', route: '/education', comingSoon: true }, + { title: 'Perwerde', icon: '📖', route: '/education', requiresAuth: true }, + { title: 'Certificates', icon: '🏆', route: '/education', comingSoon: true }, + { title: 'Research', icon: '🔬', route: '/education', comingSoon: true }, + ], + }, +]; + +const MobileHomeLayout: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { t } = useTranslation(); + const { user } = useAuth(); + const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi(); + + // Profile state + const [profileData, setProfileData] = useState<{ + full_name?: string | null; + avatar_url?: string | null; + created_at?: string; + } | null>(null); + + // Blockchain state + const [tikis, setTikis] = useState([]); + const [scores, setScores] = useState({ + trustScore: 0, referralScore: 0, stakingScore: 0, tikiScore: 0, totalScore: 0, + }); + const [kycStatus, setKycStatus] = useState('NotStarted'); + const [loadingScores, setLoadingScores] = useState(false); + + const fetchProfile = useCallback(async () => { + if (!user) return; + try { + const { data } = await supabase + .from('profiles') + .select('*') + .eq('id', user.id) + .maybeSingle(); + if (data) setProfileData(data); + } catch { /* profile fetch is best-effort */ } + }, [user]); + + const fetchBlockchainData = useCallback(async () => { + if (!selectedAccount || !peopleApi || !isPeopleReady) return; + setLoadingScores(true); + try { + const [userTikis, allScores, status] = await Promise.all([ + fetchUserTikis(peopleApi, selectedAccount.address), + getAllScores(peopleApi, selectedAccount.address), + getKycStatus(peopleApi, selectedAccount.address), + ]); + setTikis(userTikis); + setScores(allScores); + setKycStatus(status); + } catch { /* blockchain fetch is best-effort */ } + finally { setLoadingScores(false); } + }, [selectedAccount, peopleApi, isPeopleReady]); + + useEffect(() => { fetchProfile(); }, [fetchProfile]); + useEffect(() => { + if (selectedAccount && peopleApi && isPeopleReady) fetchBlockchainData(); + }, [fetchBlockchainData, selectedAccount, peopleApi, isPeopleReady]); + + const primaryRole = tikis.length > 0 ? getPrimaryRole(tikis) : 'Visitor'; + const displayName = profileData?.full_name || user?.email?.split('@')[0] || 'Heval'; + const memberSince = profileData?.created_at + ? new Date(profileData.created_at).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }) + : user?.created_at + ? new Date(user.created_at).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }) + : 'N/A'; + + const avatarEmoji = profileData?.avatar_url && !profileData.avatar_url.startsWith('http') + ? getEmojiFromAvatarId(profileData.avatar_url) + : '👤'; + + const currentTab = location.pathname === '/be-citizen' ? 'citizen' + : location.pathname === '/dashboard' ? 'referral' + : 'home'; + + return ( +
+ {/* ── HEADER ── */} +
+
+ {/* Left: Avatar + Greeting */} +
+
+ {profileData?.avatar_url?.startsWith('http') ? ( + avatar + ) : ( +
+ {avatarEmoji} +
+ )} + {/* Online dot */} +
+
+
+

+ {t('mobile.greeting', 'Rojbaş')}, {displayName} +

+ + {getTikiEmoji(primaryRole)} {getTikiDisplayName(primaryRole)} + +
+
+ + {/* Right: Actions */} +
+ + + +
+
+
+ + {/* ── SCROLLABLE CONTENT ── */} +
+ + {/* ── SCORE CARDS (horizontal scroll) ── */} +
+
+ {/* Card 1: Member Since OR Login/Sign Up */} + {user ? ( + + ) : ( + navigate('/login') }} + /> + )} + {/* Role - always visible, shows Visitor for guests */} + + {/* Total Score */} + navigate('/login') } : undefined} /> + {/* Trust Score */} + navigate('/login') } : undefined} /> + {/* Referral Score */} + navigate('/login') } : undefined} /> + {/* Staking Score */} + navigate('/login') } : undefined} /> + {/* Tiki Score */} + navigate('/login') } : undefined} /> + {/* KYC Status */} + navigate('/login') } + : kycStatus === 'NotStarted' + ? { label: t('mobile.apply', 'Apply'), onClick: () => navigate('/be-citizen') } + : undefined} + /> +
+
+ + {/* ── APP SECTIONS ── */} + {APP_SECTIONS.map((section) => ( +
+ {/* Section header */} +
+

+ {section.titleKey} {section.emoji} +

+
+ {/* App grid - 4 per row */} +
+ {section.apps.map((app) => { + const needsLogin = app.requiresAuth && !user; + return ( + + ); + })} +
+
+ ))} + + {/* Bottom spacing for tab bar */} +
+
+ + {/* ── BOTTOM TAB BAR ── */} +
+
+ navigate('/')} + /> + navigate('/be-citizen')} + accent + /> + navigate(user ? '/dashboard' : '/login')} + /> +
+
+
+ ); +}; + +// ── Sub-components ── + +function ScoreCard({ icon, label, value, sub, color, action }: { + icon: string; label: string; value: string; sub?: string; color: string; + action?: { label: string; onClick: () => void }; +}) { + return ( +
+ {icon} +

{label}

+

{value}

+ {sub &&

{sub}

} + {action && ( + + )} +
+ ); +} + +function TabButton({ icon, label, active, onClick, accent }: { + icon: string; label: string; active: boolean; onClick: () => void; accent?: boolean; +}) { + return ( + + ); +} + +export default MobileHomeLayout; diff --git a/web/src/components/MobileShell.tsx b/web/src/components/MobileShell.tsx new file mode 100644 index 00000000..384ef509 --- /dev/null +++ b/web/src/components/MobileShell.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ArrowLeft } from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; + +interface MobileShellProps { + title: string; + children: React.ReactNode; + /** Hide header gradient bar (e.g. when page has its own header) */ + hideHeader?: boolean; +} + +/** + * Shared mobile wrapper: compact header with back button + sticky bottom tab bar. + * Used by all mobile-specific pages (Citizen, Referral, etc.) + */ +const MobileShell: React.FC = ({ title, children, hideHeader }) => { + const navigate = useNavigate(); + const location = useLocation(); + const { t } = useTranslation(); + const { user } = useAuth(); + + const currentTab = location.pathname === '/be-citizen' ? 'citizen' + : location.pathname === '/dashboard' ? 'referral' + : 'home'; + + return ( +
+ {/* ── HEADER ── */} + {!hideHeader && ( +
+ +

{title}

+
+ )} + + {/* ── CONTENT ── */} +
+ {children} +
+ + {/* ── BOTTOM TAB BAR ── */} +
+
+ navigate('/')} /> + navigate('/be-citizen')} accent /> + navigate(user ? '/dashboard' : '/login')} /> +
+
+
+ ); +}; + +function TabBtn({ icon, label, active, onClick, accent }: { + icon: string; label: string; active: boolean; onClick: () => void; accent?: boolean; +}) { + return ( + + ); +} + +export default MobileShell; diff --git a/web/src/i18n/locales/ar.ts b/web/src/i18n/locales/ar.ts index eaad4cd5..e0c8608c 100644 --- a/web/src/i18n/locales/ar.ts +++ b/web/src/i18n/locales/ar.ts @@ -3757,4 +3757,23 @@ export default { 'messaging.checkingKey': 'جاري التحقق من مفتاح التشفير...', 'messaging.send': 'إرسال', 'messaging.sending': 'جاري الإرسال...', + + // Mobile Home Layout + 'mobile.greeting': 'مرحباً', + 'mobile.home': 'الرئيسية', + 'mobile.citizen': 'مواطن', + 'mobile.referral': 'إحالة', + 'mobile.memberSince': 'عضو منذ', + 'mobile.role': 'الدور', + 'mobile.totalScore': 'النقاط الإجمالية', + 'mobile.trustScore': 'نقاط الثقة', + 'mobile.referralScore': 'نقاط الإحالة', + 'mobile.stakingScore': 'نقاط الستاكينغ', + 'mobile.tikiScore': 'نقاط التيكي', + 'mobile.kycStatus': 'حالة KYC', + 'mobile.connectWallet': 'ربط المحفظة', + 'mobile.apply': 'تقديم', + 'mobile.joinUs': 'انضم إلينا', + 'mobile.signInUp': 'تسجيل الدخول / التسجيل', + 'mobile.loginToSeeRoles': 'سجل الدخول لرؤية الأدوار', }; diff --git a/web/src/i18n/locales/ckb.ts b/web/src/i18n/locales/ckb.ts index 75c65ecb..4ea14fa0 100644 --- a/web/src/i18n/locales/ckb.ts +++ b/web/src/i18n/locales/ckb.ts @@ -3747,4 +3747,23 @@ export default { 'messaging.checkingKey': 'کلیلی شفرکردن پشکنین دەکرێت...', 'messaging.send': 'بنێرە', 'messaging.sending': 'دەنێردرێت...', + + // Mobile Home Layout + 'mobile.greeting': 'ڕۆژباش', + 'mobile.home': 'ماڵەوە', + 'mobile.citizen': 'هاوڵاتی', + 'mobile.referral': 'ئاماژە', + 'mobile.memberSince': 'ئەندام لە', + 'mobile.role': 'ڕۆڵ', + 'mobile.totalScore': 'کۆی خاڵ', + 'mobile.trustScore': 'خاڵی متمانە', + 'mobile.referralScore': 'خاڵی ئاماژە', + 'mobile.stakingScore': 'خاڵی ستەیکینگ', + 'mobile.tikiScore': 'خاڵی تیکی', + 'mobile.kycStatus': 'بارودۆخی KYC', + 'mobile.connectWallet': 'جزدان ببەستە', + 'mobile.apply': 'داواکاری', + 'mobile.joinUs': 'پەیوەست ببە', + 'mobile.signInUp': 'چوونەژوورەوە / تۆمارکردن', + 'mobile.loginToSeeRoles': 'بۆ بینینی ڕۆڵەکان بچۆ ژوورەوە', }; diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 926d1d76..f1e54898 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -3795,4 +3795,23 @@ export default { 'messaging.checkingKey': 'Checking encryption key...', 'messaging.send': 'Send', 'messaging.sending': 'Sending...', + + // Mobile Home Layout + 'mobile.greeting': 'Rojbaş', + 'mobile.home': 'Home', + 'mobile.citizen': 'Citizen', + 'mobile.referral': 'Referral', + 'mobile.memberSince': 'Member Since', + 'mobile.role': 'Role', + 'mobile.totalScore': 'Total Score', + 'mobile.trustScore': 'Trust Score', + 'mobile.referralScore': 'Referral Score', + 'mobile.stakingScore': 'Staking Score', + 'mobile.tikiScore': 'Tiki Score', + 'mobile.kycStatus': 'KYC Status', + 'mobile.connectWallet': 'Connect wallet', + 'mobile.apply': 'Apply', + 'mobile.joinUs': 'Join Us', + 'mobile.signInUp': 'Sign In / Up', + 'mobile.loginToSeeRoles': 'Login to see roles', } diff --git a/web/src/i18n/locales/fa.ts b/web/src/i18n/locales/fa.ts index be5b3a74..11540126 100644 --- a/web/src/i18n/locales/fa.ts +++ b/web/src/i18n/locales/fa.ts @@ -3791,4 +3791,23 @@ export default { 'messaging.checkingKey': 'بررسی کلید رمزنگاری...', 'messaging.send': 'ارسال', 'messaging.sending': 'در حال ارسال...', + + // Mobile Home Layout + 'mobile.greeting': 'سلام', + 'mobile.home': 'خانه', + 'mobile.citizen': 'شهروند', + 'mobile.referral': 'ارجاع', + 'mobile.memberSince': 'عضو از', + 'mobile.role': 'نقش', + 'mobile.totalScore': 'امتیاز کل', + 'mobile.trustScore': 'امتیاز اعتماد', + 'mobile.referralScore': 'امتیاز ارجاع', + 'mobile.stakingScore': 'امتیاز استیکینگ', + 'mobile.tikiScore': 'امتیاز تیکی', + 'mobile.kycStatus': 'وضعیت KYC', + 'mobile.connectWallet': 'اتصال کیف پول', + 'mobile.apply': 'درخواست', + 'mobile.joinUs': 'به ما بپیوندید', + 'mobile.signInUp': 'ورود / ثبت نام', + 'mobile.loginToSeeRoles': 'برای دیدن نقش‌ها وارد شوید', }; diff --git a/web/src/i18n/locales/kmr.ts b/web/src/i18n/locales/kmr.ts index d299ebdc..2995d7ae 100644 --- a/web/src/i18n/locales/kmr.ts +++ b/web/src/i18n/locales/kmr.ts @@ -3774,4 +3774,23 @@ export default { 'messaging.checkingKey': 'Mifteya şîfrekirinê tê kontrol kirin...', 'messaging.send': 'Bişîne', 'messaging.sending': 'Tê şandin...', + + // Mobile Home Layout + 'mobile.greeting': 'Rojbaş', + 'mobile.home': 'Mal', + 'mobile.citizen': 'Welatî', + 'mobile.referral': 'Referans', + 'mobile.memberSince': 'Endam ji', + 'mobile.role': 'Rol', + 'mobile.totalScore': 'Pûana Giştî', + 'mobile.trustScore': 'Pûana Pêbaweriyê', + 'mobile.referralScore': 'Pûana Referansê', + 'mobile.stakingScore': 'Pûana Stakingê', + 'mobile.tikiScore': 'Pûana Tikiyê', + 'mobile.kycStatus': 'Rewşa KYC', + 'mobile.connectWallet': 'Cîzdanê girêde', + 'mobile.apply': 'Serlêdan', + 'mobile.joinUs': 'Tevlî me bibe', + 'mobile.signInUp': 'Têkeve / Tomar bibe', + 'mobile.loginToSeeRoles': 'Ji bo rolan têkeve', }; diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index 30a015dd..9d509d71 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -3777,4 +3777,23 @@ export default { 'messaging.checkingKey': 'Şifreleme anahtarı kontrol ediliyor...', 'messaging.send': 'Gönder', 'messaging.sending': 'Gönderiliyor...', + + // Mobile Home Layout + 'mobile.greeting': 'Rojbaş', + 'mobile.home': 'Ana Sayfa', + 'mobile.citizen': 'Vatandaş', + 'mobile.referral': 'Referans', + 'mobile.memberSince': 'Üyelik Tarihi', + 'mobile.role': 'Rol', + 'mobile.totalScore': 'Toplam Puan', + 'mobile.trustScore': 'Güven Puanı', + 'mobile.referralScore': 'Referans Puanı', + 'mobile.stakingScore': 'Staking Puanı', + 'mobile.tikiScore': 'Tiki Puanı', + 'mobile.kycStatus': 'KYC Durumu', + 'mobile.connectWallet': 'Cüzdan bağla', + 'mobile.apply': 'Başvur', + 'mobile.joinUs': 'Bize Katil', + 'mobile.signInUp': 'Giris / Kayit', + 'mobile.loginToSeeRoles': 'Rolleri gormek icin giris yap', }; diff --git a/web/src/pages/BeCitizen.tsx b/web/src/pages/BeCitizen.tsx index 81e9b979..b634d353 100644 --- a/web/src/pages/BeCitizen.tsx +++ b/web/src/pages/BeCitizen.tsx @@ -6,6 +6,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { CitizenshipModal } from '@/components/citizenship/CitizenshipModal'; import { InviteUserModal } from '@/components/referral/InviteUserModal'; import { Shield, Users, Award, Globe, ChevronRight, ArrowLeft, UserPlus } from 'lucide-react'; +import { useIsMobile } from '@/hooks/use-mobile'; +import MobileShell from '@/components/MobileShell'; const BeCitizen: React.FC = () => { const navigate = useNavigate(); @@ -25,6 +27,209 @@ const BeCitizen: React.FC = () => { } }, [searchParams]); + const isMobile = useIsMobile(); + + // ── MOBILE VERSION ── + if (isMobile) { + return ( + + {/* HERO - fills viewport minus shell tab bar (64px) */} +
+ {/* Back button */} +
+ +
+ + {/* Decorative circles */} +
+
+ + {/* Content centered */} +
+ {/* Kurdistan flag stripe */} +
+
+
+
+
+ + 🏛️ + +

+ {t('beCitizen.heroTitle', 'Digital Kurdistan')} +

+

+ {t('beCitizen.heroSubtitle', 'Be a Citizen')} +

+ +

+ {t('beCitizen.heroDesc')} +

+ + {/* CTA Buttons */} +
+ + +
+ + {/* Feature badges */} +
+ + {t('beCitizen.zkAuth', 'ZK Authentication')} + + + {t('beCitizen.soulboundNft', 'Soulbound NFT')} + + + {t('beCitizen.decentralizedId', 'Decentralized ID')} + +
+
+ + {/* Scroll indicator */} +
+ {t('mobile.scrollDown', 'Scroll')} + +
+
+ + {/* SCROLLABLE CONTENT - Ready to Join and below */} +
+ {/* Ready to Join CTA */} + + +
+

{t('beCitizen.readyToJoin')}

+

{t('beCitizen.readyToJoinDesc')}

+
+
+
+ + {/* Benefits Grid - 2 col */} +
+ + + + {t('beCitizen.privacyTitle')} + {t('beCitizen.privacyDesc')} + + + + + + {t('beCitizen.nftTitle')} + {t('beCitizen.nftDesc')} + + + + + + {t('beCitizen.trustTitle')} + {t('beCitizen.trustDesc')} + + + + + + {t('beCitizen.govTitle')} + {t('beCitizen.govDesc')} + + +
+ + {/* How It Works */} +
+

{t('beCitizen.howItWorks')}

+
+ + +
+
+ 1 +
+ {t('beCitizen.existingTitle')} +
+
+ +

✓ {t('beCitizen.existing1')}

+

✓ {t('beCitizen.existing2')}

+

✓ {t('beCitizen.existing3')}

+

✓ {t('beCitizen.existing4')}

+
+
+ + +
+
+ 2 +
+ {t('beCitizen.newTitle')} +
+
+ +

✓ {t('beCitizen.new1')}

+

✓ {t('beCitizen.new2')}

+

✓ {t('beCitizen.new3')}

+

✓ {t('beCitizen.new4')}

+
+
+ + +
+
+ 3 +
+ {t('beCitizen.benefitsTitle')} +
+
+ +

✓ {t('beCitizen.benefit1')}

+

✓ {t('beCitizen.benefit2')}

+

✓ {t('beCitizen.benefit3')}

+

✓ {t('beCitizen.benefit4')}

+
+
+
+
+ + {/* Security Notice */} + + +
+ +
+

{t('beCitizen.securityTitle')}

+

{t('beCitizen.securityDesc')}

+
+
+
+
+ +
+
+ + {/* Modals */} + setIsModalOpen(false)} referrerAddress={referrerAddress} /> + setIsInviteModalOpen(false)} /> + + ); + } + + // ── DESKTOP VERSION (unchanged) ── return (
@@ -150,7 +355,6 @@ const BeCitizen: React.FC = () => {

{t('beCitizen.howItWorks')}

- {/* Existing Citizens */}
@@ -166,7 +370,6 @@ const BeCitizen: React.FC = () => { - {/* New Citizens */}
@@ -182,7 +385,6 @@ const BeCitizen: React.FC = () => { - {/* After Citizenship */}
@@ -218,14 +420,7 @@ const BeCitizen: React.FC = () => {
- {/* Citizenship Modal */} - setIsModalOpen(false)} - referrerAddress={referrerAddress} - /> - - {/* Invite Friend Modal */} + setIsModalOpen(false)} referrerAddress={referrerAddress} /> setIsInviteModalOpen(false)} />
); diff --git a/web/src/pages/Identity.tsx b/web/src/pages/Identity.tsx new file mode 100644 index 00000000..70bd0c3e --- /dev/null +++ b/web/src/pages/Identity.tsx @@ -0,0 +1,483 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useIsMobile } from '@/hooks/use-mobile'; +import MobileShell from '@/components/MobileShell'; +import { useAuth } from '@/contexts/AuthContext'; +import { usePezkuwi } from '@/contexts/PezkuwiContext'; +import { Save, CreditCard, BookOpen, Camera } from 'lucide-react'; + +// ── Types ── +interface IdentityData { + fullName: string; + fatherName: string; + motherName: string; + dateOfBirth: string; + placeOfBirth: string; + gender: 'M' | 'F' | ''; + bloodType: string; + citizenNumber: string; + passportNumber: string; + photo: string; // base64 data URL +} + +const DEFAULT_DATA: IdentityData = { + fullName: '', + fatherName: '', + motherName: '', + dateOfBirth: '', + placeOfBirth: '', + gender: '', + bloodType: '', + citizenNumber: '', + passportNumber: '', + photo: '', +}; + +const STORAGE_KEY = 'pezkuwi_identity_data'; + +// ── Helpers ── +function generatePassportNo(citizenNo: string): string { + if (!citizenNo) return 'KRD-000000'; + return `KRD-${citizenNo.replace(/\D/g, '').slice(0, 6).padStart(6, '0')}`; +} + +function formatMRZ(data: IdentityData): [string, string] { + const name = data.fullName.toUpperCase().replace(/[^A-Z ]/g, '').replace(/ /g, '<') || 'SURNAME< = { + avatar1: '👨🏻', avatar2: '👨🏼', avatar3: '👨🏽', avatar4: '👨🏾', + avatar5: '👩🏻', avatar6: '👩🏼', avatar7: '👩🏽', avatar8: '👩🏾', + avatar9: '🧔🏻', avatar10: '🧔🏼', avatar11: '🧔🏽', avatar12: '🧔🏾', + avatar13: '👳🏻‍♂️', avatar14: '👳🏼‍♂️', avatar15: '👳🏽‍♂️', avatar16: '🧕🏻', + avatar17: '🧕🏼', avatar18: '🧕🏽', +}; + +// ── Main Component ── +export default function Identity() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const isMobile = useIsMobile(); + const { user } = useAuth(); + const { selectedAccount } = usePezkuwi(); + const [tab, setTab] = useState<'id' | 'passport'>('id'); + const [data, setData] = useState(DEFAULT_DATA); + const [saved, setSaved] = useState(false); + + // Load from localStorage + useEffect(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) setData(JSON.parse(raw)); + } catch { /* ignore */ } + }, []); + + // Auto-fill citizen number from wallet + useEffect(() => { + if (selectedAccount && !data.citizenNumber) { + const short = selectedAccount.address.slice(-8).toUpperCase(); + setData(prev => ({ + ...prev, + citizenNumber: short, + passportNumber: generatePassportNo(short), + })); + } + }, [selectedAccount, data.citizenNumber]); + + const photoInputRef = React.useRef(null); + + 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; + 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 })); + setSaved(false); + }; + img.src = reader.result as string; + }; + reader.readAsDataURL(file); + }; + + const handleChange = (field: keyof IdentityData, value: string) => { + setData(prev => { + const next = { ...prev, [field]: value }; + if (field === 'citizenNumber') { + next.passportNumber = generatePassportNo(value); + } + return next; + }); + setSaved(false); + }; + + const handleSave = () => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }; + + const [mrzLine1, mrzLine2] = formatMRZ(data); + + const content = ( +
+ {/* Tab switcher */} +
+ + +
+ +
+ {/* Hidden file input for photo */} + + + {/* ── CARD PREVIEW ── */} + {tab === 'id' ? ( + photoInputRef.current?.click()} /> + ) : ( + photoInputRef.current?.click()} /> + )} + + {/* ── FORM ── */} +
+

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

+ + handleChange('fullName', v)} placeholder="Azad Kurdistanî" /> + + handleChange('fatherName', v)} placeholder="Rêber" /> + + handleChange('motherName', v)} placeholder="Jîn" /> + +
+ handleChange('dateOfBirth', v)} type="date" /> + +
+ + +
+
+ +
+ handleChange('placeOfBirth', v)} placeholder="Hewlêr" /> + +
+ + +
+
+ + handleChange('citizenNumber', v)} placeholder="KRD-000000" /> + + {/* Save button */} + + +

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

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

KOMARA KURDISTANÊ

+

KURDISTAN REPUBLIC

+
+ {/* Sun emblem */} +
+ ☀️ +
+
+

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

+

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; +}) { + return ( +
+ + {/* Top ornament */} +
+ + {/* Header */} +
+

KOMARA KURDISTANÊ

+

KURDISTAN REPUBLIC

+ + {/* Emblem */} +
+ ☀️ +
+ +

پاسپۆرت

+

PASSPORT

+
+ + {/* Divider */} +
+ + {/* Data page */} +
+ +
+ {/* Photo - clickable */} + + +
+ + + +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + {/* MRZ Zone */} +
+

{mrzLine1}

+

{mrzLine2}

+
+
+ ); +} + +// ── Shared sub-components ── +function IDField({ label, value, bold }: { label: string; value: string; bold?: boolean }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function PassField({ label, value, bold, mono }: { label: string; value: string; bold?: boolean; mono?: boolean }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function FormField({ label, value, onChange, placeholder, type = 'text' }: { + label: string; value: string; onChange: (v: string) => void; placeholder?: string; type?: string; +}) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:border-green-500 focus:outline-none transition-colors" + /> +
+ ); +}