mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-21 23:47:56 +00:00
feat: add mobile layout with native app UX, identity page with ID card & passport
Mobile users (<768px) now see a native app-style home page with: - Green gradient header with avatar, greeting, language/wallet/notification - Horizontal scrollable score cards (auth-aware: login prompt for guests) - App grid sections (Finance, Governance, Social, Education) with 4-col layout - Bottom tab bar (Home / Citizen / Referral) - MobileShell wrapper for consistent mobile navigation across pages BeCitizen page redesigned for mobile with full-viewport hero screen, scroll-to-reveal content, and compact benefits/process cards. New Identity page (/identity) with realistic Kurdistan Republic ID card and passport design. Users can fill personal info, upload photo from camera/gallery, and save to device (localStorage only). Desktop layout completely untouched. i18n keys added for all 6 languages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
<Route path="/development" element={<Development />} />
|
||||
<Route path="/local" element={<Local />} />
|
||||
<Route path="/be-citizen" element={<BeCitizen />} />
|
||||
<Route path="/identity" element={<Identity />} />
|
||||
<Route path="/citizens" element={<Citizens />} />
|
||||
<Route path="/citizens/issues" element={<CitizensIssues />} />
|
||||
<Route path="/citizens/government" element={<GovernmentEntrance />} />
|
||||
|
||||
@@ -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 <MobileHomeLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-white">
|
||||
{/* Navigation */}
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
const [scores, setScores] = useState<UserScores>({
|
||||
trustScore: 0, referralScore: 0, stakingScore: 0, tikiScore: 0, totalScore: 0,
|
||||
});
|
||||
const [kycStatus, setKycStatus] = useState<string>('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 (
|
||||
<div className="min-h-screen bg-gray-950 text-white flex flex-col pb-16">
|
||||
{/* ── HEADER ── */}
|
||||
<div className="bg-gradient-to-r from-green-700 to-green-600 px-4 pt-3 pb-4 rounded-b-2xl shadow-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left: Avatar + Greeting */}
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="relative flex-shrink-0">
|
||||
{profileData?.avatar_url?.startsWith('http') ? (
|
||||
<img
|
||||
src={profileData.avatar_url}
|
||||
alt="avatar"
|
||||
className="w-11 h-11 rounded-full border-2 border-white/80 object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-11 h-11 rounded-full border-2 border-white/80 bg-green-800 flex items-center justify-center text-2xl">
|
||||
{avatarEmoji}
|
||||
</div>
|
||||
)}
|
||||
{/* Online dot */}
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-green-400 border-2 border-green-700" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-white font-bold text-sm truncate">
|
||||
{t('mobile.greeting', 'Rojbaş')}, {displayName}
|
||||
</p>
|
||||
<span className="text-[10px] bg-white/20 px-2 py-0.5 rounded-full text-white/90 font-medium">
|
||||
{getTikiEmoji(primaryRole)} {getTikiDisplayName(primaryRole)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Actions */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<LanguageSwitcher />
|
||||
<PezkuwiWalletButton />
|
||||
<NotificationBell />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── SCROLLABLE CONTENT ── */}
|
||||
<div className="flex-1 overflow-y-auto px-3 pt-3 space-y-4">
|
||||
|
||||
{/* ── SCORE CARDS (horizontal scroll) ── */}
|
||||
<div className="-mx-3 px-3">
|
||||
<div className="flex gap-2.5 overflow-x-auto pb-2 scrollbar-hide">
|
||||
{/* Card 1: Member Since OR Login/Sign Up */}
|
||||
{user ? (
|
||||
<ScoreCard icon="📅" label={t('mobile.memberSince', 'Member Since')} value={memberSince} color="border-l-green-500" />
|
||||
) : (
|
||||
<ScoreCard
|
||||
icon="🔑"
|
||||
label={t('mobile.joinUs', 'Join Us')}
|
||||
value={t('nav.login', 'Login')}
|
||||
color="border-l-green-500"
|
||||
action={{ label: t('mobile.signInUp', 'Sign In / Up'), onClick: () => navigate('/login') }}
|
||||
/>
|
||||
)}
|
||||
{/* Role - always visible, shows Visitor for guests */}
|
||||
<ScoreCard icon={getTikiEmoji(primaryRole)} label={t('mobile.role', 'Role')} value={getTikiDisplayName(primaryRole)}
|
||||
sub={!user ? t('mobile.loginToSeeRoles', 'Login to see roles') : selectedAccount ? `${tikis.length} ${tikis.length === 1 ? 'role' : 'roles'}` : t('mobile.connectWallet', 'Connect wallet')}
|
||||
color="border-l-orange-500" />
|
||||
{/* Total Score */}
|
||||
<ScoreCard icon="🏆" label={t('mobile.totalScore', 'Total Score')}
|
||||
value={!user ? '—' : loadingScores ? '...' : String(scores.totalScore)}
|
||||
color="border-l-purple-500"
|
||||
action={!user ? { label: t('nav.login', 'Login'), onClick: () => navigate('/login') } : undefined} />
|
||||
{/* Trust Score */}
|
||||
<ScoreCard icon="🛡️" label={t('mobile.trustScore', 'Trust Score')}
|
||||
value={!user ? '—' : loadingScores ? '...' : String(scores.trustScore)}
|
||||
color="border-l-purple-500"
|
||||
action={!user ? { label: t('nav.login', 'Login'), onClick: () => navigate('/login') } : undefined} />
|
||||
{/* Referral Score */}
|
||||
<ScoreCard icon="👥" label={t('mobile.referralScore', 'Referral Score')}
|
||||
value={!user ? '—' : loadingScores ? '...' : String(scores.referralScore)}
|
||||
color="border-l-cyan-500"
|
||||
action={!user ? { label: t('nav.login', 'Login'), onClick: () => navigate('/login') } : undefined} />
|
||||
{/* Staking Score */}
|
||||
<ScoreCard icon="📈" label={t('mobile.stakingScore', 'Staking Score')}
|
||||
value={!user ? '—' : loadingScores ? '...' : String(scores.stakingScore)}
|
||||
color="border-l-green-500"
|
||||
action={!user ? { label: t('nav.login', 'Login'), onClick: () => navigate('/login') } : undefined} />
|
||||
{/* Tiki Score */}
|
||||
<ScoreCard icon="⭐" label={t('mobile.tikiScore', 'Tiki Score')}
|
||||
value={!user ? '—' : loadingScores ? '...' : String(scores.tikiScore)}
|
||||
color="border-l-pink-500"
|
||||
action={!user ? { label: t('nav.login', 'Login'), onClick: () => navigate('/login') } : undefined} />
|
||||
{/* KYC Status */}
|
||||
<ScoreCard
|
||||
icon={!user ? '📝' : kycStatus === 'Approved' ? '✅' : kycStatus === 'Pending' ? '⏳' : '📝'}
|
||||
label={t('mobile.kycStatus', 'KYC Status')}
|
||||
value={!user ? '—' : kycStatus}
|
||||
color={kycStatus === 'Approved' ? 'border-l-green-500' : 'border-l-yellow-500'}
|
||||
action={!user
|
||||
? { label: t('nav.login', 'Login'), onClick: () => navigate('/login') }
|
||||
: kycStatus === 'NotStarted'
|
||||
? { label: t('mobile.apply', 'Apply'), onClick: () => navigate('/be-citizen') }
|
||||
: undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── APP SECTIONS ── */}
|
||||
{APP_SECTIONS.map((section) => (
|
||||
<div key={section.titleKey} className="bg-gray-900/60 rounded-xl border border-gray-800/60 overflow-hidden">
|
||||
{/* Section header */}
|
||||
<div className={`flex items-center justify-between px-4 py-2.5 border-l-4 ${section.borderColor}`}>
|
||||
<h3 className="text-sm font-bold text-white tracking-wide">
|
||||
{section.titleKey} {section.emoji}
|
||||
</h3>
|
||||
</div>
|
||||
{/* App grid - 4 per row */}
|
||||
<div className="grid grid-cols-4 gap-1 px-3 py-3">
|
||||
{section.apps.map((app) => {
|
||||
const needsLogin = app.requiresAuth && !user;
|
||||
return (
|
||||
<button
|
||||
key={app.title}
|
||||
onClick={() => {
|
||||
if (app.comingSoon) return;
|
||||
if (needsLogin) { navigate('/login'); return; }
|
||||
navigate(app.route);
|
||||
}}
|
||||
className={`flex flex-col items-center gap-1 py-2 px-1 rounded-xl transition-all active:scale-95
|
||||
${app.comingSoon ? 'opacity-50' : 'hover:bg-gray-800/60'}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<span className="text-2xl">{app.icon}</span>
|
||||
{app.comingSoon && (
|
||||
<span className="absolute -top-1 -right-2 text-[10px]">🔒</span>
|
||||
)}
|
||||
{needsLogin && !app.comingSoon && (
|
||||
<span className="absolute -top-1 -right-2 text-[10px]">🔑</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-300 font-medium text-center leading-tight">
|
||||
{app.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Bottom spacing for tab bar */}
|
||||
<div className="h-4" />
|
||||
</div>
|
||||
|
||||
{/* ── BOTTOM TAB BAR ── */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-gray-950/95 backdrop-blur-md border-t border-gray-800">
|
||||
<div className="flex items-center justify-around h-16 max-w-md mx-auto">
|
||||
<TabButton
|
||||
icon="🏠"
|
||||
label={t('mobile.home', 'Home')}
|
||||
active={currentTab === 'home'}
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
<TabButton
|
||||
icon="🏛️"
|
||||
label={t('mobile.citizen', 'Citizen')}
|
||||
active={currentTab === 'citizen'}
|
||||
onClick={() => navigate('/be-citizen')}
|
||||
accent
|
||||
/>
|
||||
<TabButton
|
||||
icon="👥"
|
||||
label={t('mobile.referral', 'Referral')}
|
||||
active={currentTab === 'referral'}
|
||||
onClick={() => navigate(user ? '/dashboard' : '/login')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── 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 (
|
||||
<div className={`flex-shrink-0 w-28 bg-gray-900/80 rounded-xl border border-gray-800/60 border-l-4 ${color} p-3 space-y-1`}>
|
||||
<span className="text-lg">{icon}</span>
|
||||
<p className="text-[10px] text-gray-400 font-medium">{label}</p>
|
||||
<p className="text-sm font-bold text-white truncate">{value}</p>
|
||||
{sub && <p className="text-[9px] text-gray-500">{sub}</p>}
|
||||
{action && (
|
||||
<button
|
||||
onClick={action.onClick}
|
||||
className="mt-1 text-[10px] bg-green-600 hover:bg-green-700 text-white px-2 py-0.5 rounded-full font-medium"
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButton({ icon, label, active, onClick, accent }: {
|
||||
icon: string; label: string; active: boolean; onClick: () => void; accent?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex flex-col items-center gap-0.5 px-4 py-1 rounded-xl transition-all active:scale-95
|
||||
${active ? 'text-green-400' : 'text-gray-500'}
|
||||
${accent ? 'relative' : ''}`}
|
||||
>
|
||||
{accent ? (
|
||||
<div className={`w-12 h-12 -mt-6 rounded-full flex items-center justify-center shadow-lg
|
||||
${active ? 'bg-green-600' : 'bg-gray-800 border border-gray-700'}`}>
|
||||
<span className="text-xl">{icon}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xl">{icon}</span>
|
||||
)}
|
||||
<span className="text-[10px] font-medium">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileHomeLayout;
|
||||
@@ -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<MobileShellProps> = ({ 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 (
|
||||
<div className="min-h-screen bg-gray-950 text-white flex flex-col pb-16">
|
||||
{/* ── HEADER ── */}
|
||||
{!hideHeader && (
|
||||
<div className="bg-gradient-to-r from-green-700 to-green-600 px-4 py-3 flex items-center gap-3 shadow-md">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="p-1.5 rounded-lg bg-white/15 active:bg-white/25 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
<h1 className="text-base font-bold text-white truncate">{title}</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── CONTENT ── */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* ── BOTTOM TAB BAR ── */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-gray-950/95 backdrop-blur-md border-t border-gray-800">
|
||||
<div className="flex items-center justify-around h-16 max-w-md mx-auto">
|
||||
<TabBtn icon="🏠" label={t('mobile.home', 'Home')} active={currentTab === 'home'} onClick={() => navigate('/')} />
|
||||
<TabBtn icon="🏛️" label={t('mobile.citizen', 'Citizen')} active={currentTab === 'citizen'} onClick={() => navigate('/be-citizen')} accent />
|
||||
<TabBtn icon="👥" label={t('mobile.referral', 'Referral')} active={currentTab === 'referral'}
|
||||
onClick={() => navigate(user ? '/dashboard' : '/login')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function TabBtn({ icon, label, active, onClick, accent }: {
|
||||
icon: string; label: string; active: boolean; onClick: () => void; accent?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex flex-col items-center gap-0.5 px-4 py-1 rounded-xl transition-all active:scale-95
|
||||
${active ? 'text-green-400' : 'text-gray-500'}
|
||||
${accent ? 'relative' : ''}`}
|
||||
>
|
||||
{accent ? (
|
||||
<div className={`w-12 h-12 -mt-6 rounded-full flex items-center justify-center shadow-lg
|
||||
${active ? 'bg-green-600' : 'bg-gray-800 border border-gray-700'}`}>
|
||||
<span className="text-xl">{icon}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xl">{icon}</span>
|
||||
)}
|
||||
<span className="text-[10px] font-medium">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileShell;
|
||||
@@ -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': 'سجل الدخول لرؤية الأدوار',
|
||||
};
|
||||
|
||||
@@ -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': 'بۆ بینینی ڕۆڵەکان بچۆ ژوورەوە',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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': 'برای دیدن نقشها وارد شوید',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
+206
-11
@@ -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 (
|
||||
<MobileShell title={`🏛️ ${t('beCitizen.heroTitle', 'Be a Citizen')}`} hideHeader>
|
||||
{/* HERO - fills viewport minus shell tab bar (64px) */}
|
||||
<div className="relative flex flex-col min-h-[calc(100vh-64px)] bg-gradient-to-b from-green-700 via-green-600 to-green-800 overflow-hidden">
|
||||
{/* Back button */}
|
||||
<div className="px-4 pt-3 pb-1 flex items-center">
|
||||
<button onClick={() => navigate('/')} className="p-1.5 rounded-lg bg-white/15 active:bg-white/25">
|
||||
<ArrowLeft className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Decorative circles */}
|
||||
<div className="absolute top-10 right-0 w-40 h-40 bg-yellow-400/10 rounded-full -mr-16" />
|
||||
<div className="absolute bottom-20 left-0 w-32 h-32 bg-red-500/10 rounded-full -ml-12" />
|
||||
|
||||
{/* Content centered */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-6 text-center -mt-8">
|
||||
{/* Kurdistan flag stripe */}
|
||||
<div className="flex gap-1 mb-4">
|
||||
<div className="w-12 h-1.5 rounded-full bg-red-500" />
|
||||
<div className="w-12 h-1.5 rounded-full bg-white" />
|
||||
<div className="w-12 h-1.5 rounded-full bg-green-400" />
|
||||
</div>
|
||||
|
||||
<span className="text-5xl mb-3">🏛️</span>
|
||||
|
||||
<h1 className="text-2xl font-bold text-white mb-1 leading-tight">
|
||||
{t('beCitizen.heroTitle', 'Digital Kurdistan')}
|
||||
</h1>
|
||||
<h2 className="text-base font-semibold text-yellow-300 mb-4">
|
||||
{t('beCitizen.heroSubtitle', 'Be a Citizen')}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-white/80 leading-relaxed mb-6 max-w-[280px]">
|
||||
{t('beCitizen.heroDesc')}
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="w-full max-w-[280px] space-y-2.5">
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="w-full py-3 rounded-xl bg-gradient-to-r from-red-500 to-yellow-500 text-white font-bold text-sm shadow-lg active:scale-95 transition-transform flex items-center justify-center gap-2"
|
||||
>
|
||||
{t('beCitizen.startProcess', 'Start Citizenship Process')}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsInviteModalOpen(true)}
|
||||
className="w-full py-2.5 rounded-xl bg-white/15 border border-white/30 text-white font-medium text-sm active:scale-95 transition-transform flex items-center justify-center gap-2"
|
||||
>
|
||||
<UserPlus className="w-4 h-4" />
|
||||
{t('beCitizen.inviteFriend', 'Invite a Friend')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Feature badges */}
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-5">
|
||||
<span className="text-[10px] bg-white/15 text-white/90 px-2.5 py-1 rounded-full flex items-center gap-1">
|
||||
<Shield className="w-3 h-3" /> {t('beCitizen.zkAuth', 'ZK Authentication')}
|
||||
</span>
|
||||
<span className="text-[10px] bg-white/15 text-white/90 px-2.5 py-1 rounded-full flex items-center gap-1">
|
||||
<Award className="w-3 h-3" /> {t('beCitizen.soulboundNft', 'Soulbound NFT')}
|
||||
</span>
|
||||
<span className="text-[10px] bg-white/15 text-white/90 px-2.5 py-1 rounded-full flex items-center gap-1">
|
||||
<Globe className="w-3 h-3" /> {t('beCitizen.decentralizedId', 'Decentralized ID')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="flex flex-col items-center pb-4 animate-bounce">
|
||||
<span className="text-white/50 text-[10px] mb-1">{t('mobile.scrollDown', 'Scroll')}</span>
|
||||
<ChevronRight className="w-4 h-4 text-white/50 rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SCROLLABLE CONTENT - Ready to Join and below */}
|
||||
<div className="bg-gradient-to-b from-green-800 via-white to-red-50 px-4 py-6 space-y-6">
|
||||
{/* Ready to Join CTA */}
|
||||
<Card className="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-400 border-red-600 border-2 shadow-xl">
|
||||
<CardContent className="pt-6 pb-6">
|
||||
<div className="text-center space-y-3">
|
||||
<h3 className="text-lg font-bold text-red-700">{t('beCitizen.readyToJoin')}</h3>
|
||||
<p className="text-sm text-gray-800 font-medium">{t('beCitizen.readyToJoinDesc')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Benefits Grid - 2 col */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Card className="bg-red-50/90 border-red-600/50 shadow-md">
|
||||
<CardHeader className="p-4">
|
||||
<Shield className="h-8 w-8 text-red-600 mb-2" />
|
||||
<CardTitle className="text-sm text-red-700 font-bold">{t('beCitizen.privacyTitle')}</CardTitle>
|
||||
<CardDescription className="text-xs text-gray-700">{t('beCitizen.privacyDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="bg-yellow-50/90 border-yellow-600/50 shadow-md">
|
||||
<CardHeader className="p-4">
|
||||
<Award className="h-8 w-8 text-yellow-700 mb-2" />
|
||||
<CardTitle className="text-sm text-yellow-800 font-bold">{t('beCitizen.nftTitle')}</CardTitle>
|
||||
<CardDescription className="text-xs text-gray-700">{t('beCitizen.nftDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="bg-green-50/90 border-green-600/50 shadow-md">
|
||||
<CardHeader className="p-4">
|
||||
<Users className="h-8 w-8 text-green-600 mb-2" />
|
||||
<CardTitle className="text-sm text-green-700 font-bold">{t('beCitizen.trustTitle')}</CardTitle>
|
||||
<CardDescription className="text-xs text-gray-700">{t('beCitizen.trustDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="bg-red-50/90 border-red-600/50 shadow-md">
|
||||
<CardHeader className="p-4">
|
||||
<Globe className="h-8 w-8 text-red-600 mb-2" />
|
||||
<CardTitle className="text-sm text-red-700 font-bold">{t('beCitizen.govTitle')}</CardTitle>
|
||||
<CardDescription className="text-xs text-gray-700">{t('beCitizen.govDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* How It Works */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-red-700 text-center mb-4">{t('beCitizen.howItWorks')}</h3>
|
||||
<div className="space-y-3">
|
||||
<Card className="bg-red-50/90 border-red-600/50 shadow-md">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-red-600 w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-sm font-bold text-white">1</span>
|
||||
</div>
|
||||
<CardTitle className="text-sm text-red-700 font-bold">{t('beCitizen.existingTitle')}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 text-gray-700 font-medium space-y-1 text-xs">
|
||||
<p>✓ {t('beCitizen.existing1')}</p>
|
||||
<p>✓ {t('beCitizen.existing2')}</p>
|
||||
<p>✓ {t('beCitizen.existing3')}</p>
|
||||
<p>✓ {t('beCitizen.existing4')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-yellow-50/90 border-yellow-600/50 shadow-md">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-yellow-600 w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-sm font-bold text-white">2</span>
|
||||
</div>
|
||||
<CardTitle className="text-sm text-yellow-800 font-bold">{t('beCitizen.newTitle')}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 text-gray-700 font-medium space-y-1 text-xs">
|
||||
<p>✓ {t('beCitizen.new1')}</p>
|
||||
<p>✓ {t('beCitizen.new2')}</p>
|
||||
<p>✓ {t('beCitizen.new3')}</p>
|
||||
<p>✓ {t('beCitizen.new4')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-green-50/90 border-green-600/50 shadow-md">
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-green-600 w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-sm font-bold text-white">3</span>
|
||||
</div>
|
||||
<CardTitle className="text-sm text-green-700 font-bold">{t('beCitizen.benefitsTitle')}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 text-gray-700 font-medium space-y-1 text-xs">
|
||||
<p>✓ {t('beCitizen.benefit1')}</p>
|
||||
<p>✓ {t('beCitizen.benefit2')}</p>
|
||||
<p>✓ {t('beCitizen.benefit3')}</p>
|
||||
<p>✓ {t('beCitizen.benefit4')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security Notice */}
|
||||
<Card className="bg-yellow-50/90 border-yellow-600/50 shadow-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<Shield className="h-5 w-5 text-yellow-700 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-gray-700">
|
||||
<p className="font-bold text-yellow-800 mb-1">{t('beCitizen.securityTitle')}</p>
|
||||
<p className="font-medium">{t('beCitizen.securityDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="h-4" />
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<CitizenshipModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} referrerAddress={referrerAddress} />
|
||||
<InviteUserModal isOpen={isInviteModalOpen} onClose={() => setIsInviteModalOpen(false)} />
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
// ── DESKTOP VERSION (unchanged) ──
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-700 via-white to-red-600">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
@@ -150,7 +355,6 @@ const BeCitizen: React.FC = () => {
|
||||
<h3 className="text-3xl font-bold text-red-700 text-center mb-8 drop-shadow-lg">{t('beCitizen.howItWorks')}</h3>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{/* Existing Citizens */}
|
||||
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<div className="bg-red-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
@@ -166,7 +370,6 @@ const BeCitizen: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* New Citizens */}
|
||||
<Card className="bg-yellow-50/90 backdrop-blur-md border-yellow-600/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<div className="bg-yellow-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
@@ -182,7 +385,6 @@ const BeCitizen: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* After Citizenship */}
|
||||
<Card className="bg-green-50/90 backdrop-blur-md border-green-600/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<div className="bg-green-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
@@ -218,14 +420,7 @@ const BeCitizen: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Citizenship Modal */}
|
||||
<CitizenshipModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
referrerAddress={referrerAddress}
|
||||
/>
|
||||
|
||||
{/* Invite Friend Modal */}
|
||||
<CitizenshipModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} referrerAddress={referrerAddress} />
|
||||
<InviteUserModal isOpen={isInviteModalOpen} onClose={() => setIsInviteModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<<NAME';
|
||||
const dob = data.dateOfBirth.replace(/-/g, '').slice(2) || '000000';
|
||||
const gender = data.gender || '<';
|
||||
const pno = data.passportNumber.replace(/[^A-Z0-9]/g, '').padEnd(9, '<');
|
||||
const line1 = `P<KRD${name}${'<'.repeat(Math.max(0, 44 - 5 - name.length))}`.slice(0, 44);
|
||||
const line2 = `${pno}0KRD${dob}0${gender}${'<'.repeat(14)}00`.slice(0, 44);
|
||||
return [line1, line2];
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const issueDate = today.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
const expiryDate = new Date(today.getFullYear() + 10, today.getMonth(), today.getDate())
|
||||
.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
|
||||
// Avatar from profile
|
||||
const AVATAR_POOL: Record<string, string> = {
|
||||
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<IdentityData>(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<HTMLInputElement>(null);
|
||||
|
||||
const handlePhotoSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 = (
|
||||
<div className="bg-gray-950 min-h-full">
|
||||
{/* Tab switcher */}
|
||||
<div className="flex bg-gray-900 border-b border-gray-800">
|
||||
<button
|
||||
onClick={() => setTab('id')}
|
||||
className={`flex-1 py-3 text-sm font-semibold flex items-center justify-center gap-2 transition-colors
|
||||
${tab === 'id' ? 'text-green-400 border-b-2 border-green-400 bg-gray-900' : 'text-gray-500'}`}
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
{t('identity.idCard', 'ID Card')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('passport')}
|
||||
className={`flex-1 py-3 text-sm font-semibold flex items-center justify-center gap-2 transition-colors
|
||||
${tab === 'passport' ? 'text-green-400 border-b-2 border-green-400 bg-gray-900' : 'text-gray-500'}`}
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
{t('identity.passport', 'Passport')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-4 space-y-4">
|
||||
{/* Hidden file input for photo */}
|
||||
<input
|
||||
ref={photoInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="user"
|
||||
className="hidden"
|
||||
onChange={handlePhotoSelect}
|
||||
/>
|
||||
|
||||
{/* ── CARD PREVIEW ── */}
|
||||
{tab === 'id' ? (
|
||||
<IDCardPreview data={data} onPhotoClick={() => photoInputRef.current?.click()} />
|
||||
) : (
|
||||
<PassportPreview data={data} mrzLine1={mrzLine1} mrzLine2={mrzLine2} onPhotoClick={() => photoInputRef.current?.click()} />
|
||||
)}
|
||||
|
||||
{/* ── FORM ── */}
|
||||
<div className="bg-gray-900/80 rounded-xl border border-gray-800 p-4 space-y-3">
|
||||
<h3 className="text-sm font-bold text-white mb-2">
|
||||
{t('identity.personalInfo', 'Personal Information')}
|
||||
</h3>
|
||||
|
||||
<FormField label={t('identity.fullName', 'Full Name')} value={data.fullName}
|
||||
onChange={v => handleChange('fullName', v)} placeholder="Azad Kurdistanî" />
|
||||
|
||||
<FormField label={t('identity.fatherName', "Father's Name")} value={data.fatherName}
|
||||
onChange={v => handleChange('fatherName', v)} placeholder="Rêber" />
|
||||
|
||||
<FormField label={t('identity.motherName', "Mother's Name")} value={data.motherName}
|
||||
onChange={v => handleChange('motherName', v)} placeholder="Jîn" />
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField label={t('identity.dateOfBirth', 'Date of Birth')} value={data.dateOfBirth}
|
||||
onChange={v => handleChange('dateOfBirth', v)} type="date" />
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-400 font-medium block mb-1">
|
||||
{t('identity.gender', 'Gender')}
|
||||
</label>
|
||||
<select
|
||||
value={data.gender}
|
||||
onChange={e => 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"
|
||||
>
|
||||
<option value="">—</option>
|
||||
<option value="M">{t('identity.male', 'Male')}</option>
|
||||
<option value="F">{t('identity.female', 'Female')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField label={t('identity.placeOfBirth', 'Place of Birth')} value={data.placeOfBirth}
|
||||
onChange={v => handleChange('placeOfBirth', v)} placeholder="Hewlêr" />
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-400 font-medium block mb-1">
|
||||
{t('identity.bloodType', 'Blood Type')}
|
||||
</label>
|
||||
<select
|
||||
value={data.bloodType}
|
||||
onChange={e => handleChange('bloodType', e.target.value)}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'].map(bt => (
|
||||
<option key={bt} value={bt}>{bt}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField label={t('identity.citizenNo', 'Citizen Number')} value={data.citizenNumber}
|
||||
onChange={v => handleChange('citizenNumber', v)} placeholder="KRD-000000" />
|
||||
|
||||
{/* Save button */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className={`w-full py-3 rounded-xl font-bold text-sm flex items-center justify-center gap-2 transition-all active:scale-95
|
||||
${saved
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gradient-to-r from-green-600 to-yellow-500 text-white shadow-lg'}`}
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{saved ? t('identity.saved', 'Saved!') : t('identity.save', 'Save to Device')}
|
||||
</button>
|
||||
|
||||
<p className="text-[10px] text-gray-500 text-center">
|
||||
{t('identity.localOnly', 'Data is stored only on your device. Never uploaded.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="h-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<MobileShell title={`🆔 ${t('identity.title', 'Identity')}`}>
|
||||
{content}
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: simple centered layout
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-white">
|
||||
<div className="max-w-lg mx-auto py-8">
|
||||
<button onClick={() => navigate('/')} className="mb-4 text-sm text-gray-400 hover:text-white">
|
||||
← {t('common.backToHome', 'Back to Home')}
|
||||
</button>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── ID Card Preview ──
|
||||
function IDCardPreview({ data, onPhotoClick }: { data: IdentityData; onPhotoClick: () => void }) {
|
||||
return (
|
||||
<div className="relative w-full aspect-[1.586/1] rounded-xl overflow-hidden shadow-2xl border-2 border-yellow-600/50"
|
||||
style={{ background: 'linear-gradient(135deg, #f5f0e8 0%, #e8dcc8 50%, #f0e8d8 100%)' }}>
|
||||
|
||||
{/* Top stripe - Kurdistan flag colors */}
|
||||
<div className="absolute top-0 left-0 right-0 flex h-2">
|
||||
<div className="flex-1 bg-red-600" />
|
||||
<div className="flex-1 bg-white" />
|
||||
<div className="flex-1 bg-green-600" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-4 pt-4 pb-1 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[8px] font-bold text-gray-600 tracking-wider">KOMARA KURDISTANÊ</p>
|
||||
<p className="text-[7px] text-gray-500">KURDISTAN REPUBLIC</p>
|
||||
</div>
|
||||
{/* Sun emblem */}
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-yellow-400 to-yellow-600 flex items-center justify-center shadow-md">
|
||||
<span className="text-sm">☀️</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[8px] font-bold text-gray-600 tracking-wider">کۆماری کوردستان</p>
|
||||
<p className="text-[7px] text-gray-500">NASNAMA / ID CARD</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mx-4 h-px bg-gradient-to-r from-red-400 via-yellow-400 to-green-400" />
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-4 pt-2 flex gap-3">
|
||||
{/* Photo area - clickable */}
|
||||
<button onClick={onPhotoClick}
|
||||
className="w-16 h-20 rounded-md bg-gray-200 border border-gray-300 flex items-center justify-center flex-shrink-0 overflow-hidden relative group">
|
||||
{data.photo ? (
|
||||
<img src={data.photo} alt="photo" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-3xl">👤</span>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-active:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<Camera className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0 space-y-0.5">
|
||||
<IDField label="Nav / Name" value={data.fullName || '—'} bold />
|
||||
<IDField label="Navê bav / Father" value={data.fatherName || '—'} />
|
||||
<IDField label="Navê dayik / Mother" value={data.motherName || '—'} />
|
||||
<div className="flex gap-3">
|
||||
<IDField label="Zayîn / DOB" value={data.dateOfBirth ? new Date(data.dateOfBirth).toLocaleDateString('en-GB') : '—'} />
|
||||
<IDField label="Zayî / Sex" value={data.gender || '—'} />
|
||||
</div>
|
||||
<IDField label="Cih / Place" value={data.placeOfBirth || '—'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-r from-green-800 via-green-700 to-green-800 px-4 py-1.5 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[7px] text-green-200">JIM / NO</p>
|
||||
<p className="text-[9px] font-mono font-bold text-white tracking-widest">
|
||||
{data.citizenNumber || 'KRD-000000'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[7px] text-green-200">XWÎNê / Blood</p>
|
||||
<p className="text-[9px] font-bold text-white">{data.bloodType || '—'}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[7px] text-green-200">DERBASDAR / Expiry</p>
|
||||
<p className="text-[9px] font-mono text-white">{expiryDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Passport Preview ──
|
||||
function PassportPreview({ data, mrzLine1, mrzLine2, onPhotoClick }: {
|
||||
data: IdentityData; mrzLine1: string; mrzLine2: string; onPhotoClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative w-full aspect-[0.71/1] rounded-xl overflow-hidden shadow-2xl border-2 border-green-800/70"
|
||||
style={{ background: 'linear-gradient(180deg, #1a472a 0%, #0d2818 100%)' }}>
|
||||
|
||||
{/* Top ornament */}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-red-500 via-yellow-400 to-green-500" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center pt-4 pb-2">
|
||||
<p className="text-[9px] font-bold text-yellow-400 tracking-[0.2em]">KOMARA KURDISTANÊ</p>
|
||||
<p className="text-[8px] text-yellow-400/70 tracking-wider">KURDISTAN REPUBLIC</p>
|
||||
|
||||
{/* Emblem */}
|
||||
<div className="w-14 h-14 mx-auto my-2 rounded-full bg-gradient-to-br from-yellow-300 to-yellow-600 flex items-center justify-center shadow-lg border-2 border-yellow-300/50">
|
||||
<span className="text-2xl">☀️</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[9px] font-bold text-yellow-400/80 tracking-[0.3em]">پاسپۆرت</p>
|
||||
<p className="text-[8px] text-yellow-400/60 tracking-widest">PASSPORT</p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mx-6 h-px bg-yellow-400/30" />
|
||||
|
||||
{/* Data page */}
|
||||
<div className="mx-4 mt-2 bg-white/95 rounded-lg p-3 space-y-1.5"
|
||||
style={{ background: 'linear-gradient(135deg, #fefdf8 0%, #f5f0e0 100%)' }}>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{/* Photo - clickable */}
|
||||
<button onClick={onPhotoClick}
|
||||
className="w-14 h-18 rounded bg-gray-200 border border-gray-300 flex items-center justify-center flex-shrink-0 overflow-hidden relative group">
|
||||
{data.photo ? (
|
||||
<img src={data.photo} alt="photo" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-2xl">👤</span>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-active:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<Camera className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex-1 space-y-0.5">
|
||||
<PassField label="Type / Cure" value="P" />
|
||||
<PassField label="Code / Kod" value="KRD" />
|
||||
<PassField label="Passport No" value={data.passportNumber || 'KRD-000000'} mono />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PassField label="Surname, Name / Nav û paşnav" value={data.fullName?.toUpperCase() || '—'} bold />
|
||||
<div className="flex gap-3">
|
||||
<PassField label="Nationality / Netewe" value="KURDISTANÎ" />
|
||||
<PassField label="Sex / Zayî" value={data.gender || '—'} />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<PassField label="Date of Birth / Zayîn" value={data.dateOfBirth ? new Date(data.dateOfBirth).toLocaleDateString('en-GB') : '—'} />
|
||||
<PassField label="Place / Cih" value={data.placeOfBirth?.toUpperCase() || '—'} />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<PassField label="Issue / Dest pê" value={issueDate} />
|
||||
<PassField label="Expiry / Dawî" value={expiryDate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MRZ Zone */}
|
||||
<div className="mx-4 mt-2 bg-white/90 rounded-b-lg px-2 py-1.5 font-mono">
|
||||
<p className="text-[7px] text-gray-700 tracking-[0.15em] leading-relaxed break-all">{mrzLine1}</p>
|
||||
<p className="text-[7px] text-gray-700 tracking-[0.15em] leading-relaxed break-all">{mrzLine2}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Shared sub-components ──
|
||||
function IDField({ label, value, bold }: { label: string; value: string; bold?: boolean }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-[6px] text-gray-500 leading-none">{label}</p>
|
||||
<p className={`text-[9px] text-gray-900 leading-tight truncate ${bold ? 'font-bold' : 'font-medium'}`}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PassField({ label, value, bold, mono }: { label: string; value: string; bold?: boolean; mono?: boolean }) {
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[5px] text-gray-500 leading-none">{label}</p>
|
||||
<p className={`text-[8px] text-gray-900 leading-tight truncate
|
||||
${bold ? 'font-bold' : 'font-medium'}
|
||||
${mono ? 'font-mono tracking-wider' : ''}`}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormField({ label, value, onChange, placeholder, type = 'text' }: {
|
||||
label: string; value: string; onChange: (v: string) => void; placeholder?: string; type?: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-400 font-medium block mb-1">{label}</label>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user