import { useState, type ReactNode } from 'react'; import { BarChart3, Bell, CalendarDays, Check, ChevronLeft, Coins, Crown, Heart, Flame, Gift, GraduationCap, LifeBuoy, LogOut, FileText, Pencil, Sparkles, Star, Trash2, Trophy, UserRound, Volume2, VolumeX, } from 'lucide-react'; import { api } from '@/api'; import { useAppData } from '@/state/AppData'; import { askStore } from '@/state/askStore'; import { useNav } from '@/nav/Navigator'; import type { Route } from '@/nav/routes'; import { SeyarMascot } from '@/mascots/SeyarMascot'; import { Sheet } from '@/components/Sheet'; import { useToast } from '@/components/Toast'; import { BirthDatePicker, ageOf, formatBirthDate } from '@/components/BirthDatePicker'; import { InlineError, PrimaryButton, Screen, SecondaryButton } from '@/components/ui'; import { friendlyError } from '@/lib/errors'; import { setSoundEnabled, soundEnabled } from '@/lib/sound'; import { digitsOnly, formatJalaliDate, toFa } from '@/lib/format'; import { useAsync } from '@/lib/useAsync'; import { daysLeft } from '@/lib/subscription'; import { BrandFooter } from '@/components/BrandFooter'; type Editing = 'name' | 'grade' | 'birth' | 'account' | 'logout' | 'delete' | null; /** «من»: پروفایل، تنظیمات و میان‌بُر به بخش‌های فرعی. */ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) { const { profile, gradeTitle, summary, companion } = useAppData(); const nav = useNav(); const [editing, setEditing] = useState(null); const [sound, setSound] = useState(soundEnabled()); const age = ageOf(profile.birthDate, profile.birthYear); const go = (route: Route) => () => nav.push(route); return (
شناسه کاربری: {profile.publicId}
{gradeTitle && {gradeTitle}} {age && {toFa(age)} ساله} {profile.phone && {toFa(profile.phone)}}
{[ { icon: Star, value: summary?.xp ?? 0, label: 'امتیاز' }, { icon: Coins, value: summary?.coins ?? 0, label: 'سکه' }, { icon: Flame, value: summary?.streak.longest ?? 0, label: 'بیشترین زنجیره' }, ].map(({ icon: Icon, value, label }) => (
{toFa(value)} {label}
))}
nav.switchTab('subscription')} /> setEditing('grade')} /> setEditing('birth')} /> { setSoundEnabled(!sound); setSound(!sound); }} /> {/* حذفِ حساب یک لایه پایین‌تر، در «حساب کاربری»، مثلِ اپ‌های بزرگ (دیجی‌کالا، اسنپ، دولینگو): پیدا می‌شود (Google Play و App Store «در دسترس و پیداشدنی» می‌خواهند) ولی با یک لمسِ اشتباه زیرِ انگشت نیست. */} setEditing('account')} /> setEditing('logout')} />
setEditing(null)} /> setEditing(null)} /> setEditing(null)} /> setEditing(null)} onDelete={() => setEditing('delete')} /> setEditing(null)} onSignedOut={onSignedOut} /> setEditing(null)} onSignedOut={onSignedOut} />
); } /** * اشتراک به‌صورتِ نوار: چند روز از کلِ دوره مانده. مخرج طولِ پلن است (planDurationDays)؛ تمدیدِ زودتر از * موعد روزها را روی هم می‌گذارد، پس نوار در ۱۰۰٪ بسته می‌شود. زدنِ کارت فروشگاه را باز می‌کند. */ function SubscriptionCard({ onOpen }: { onOpen: () => void }) { const status = useAsync(() => api.subscription.status(), [], { key: 'subscription-status' }); const s = status.data; if (!s) return
; const days = s.active ? daysLeft(s.expiresAt) : 0; const total = s.planDurationDays ?? 30; // اشتراکِ بی‌تاریخ (هدیه) پر نشان داده می‌شود. const percent = s.active ? (days === null ? 100 : Math.min(100, Math.max(4, Math.round((days / total) * 100)))) : 0; const low = s.active && days !== null && days <= 5; return ( ); } function Menu({ children }: { children: ReactNode }) { return
{children}
; } function Row({ icon: Icon, label, value, danger = false, onClick }: { icon: typeof Bell; label: string; value?: string; danger?: boolean; onClick: () => void }) { return ( ); } function NameSheet({ open, onClose }: { open: boolean; onClose: () => void }) { const { profile, setProfile } = useAppData(); const toast = useToast(); const [name, setName] = useState(profile.name); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); async function save() { if (!name.trim()) return; setSaving(true); setError(''); try { setProfile(await api.profile.update({ name: name.trim() })); toast('اسمت عوض شد', 'success'); onClose(); } catch (err) { setError(friendlyError(err)); } finally { setSaving(false); } } return ( setName(e.target.value.slice(0, 40))} className="mb-4 w-full rounded-2xl border-2 border-line px-4 py-3.5 text-sm outline-none focus:border-coral" /> {error &&
{error}
} void save()}>ذخیره
); } function GradeSheet({ open, onClose }: { open: boolean; onClose: () => void }) { const { profile, grades, setProfile, refreshHome, refreshSummary } = useAppData(); const toast = useToast(); const [saving, setSaving] = useState(null); async function choose(gradeId: number) { if (gradeId === profile.gradeId) return onClose(); setSaving(gradeId); try { await api.profile.setGrade(gradeId); setProfile({ ...profile, gradeId }); askStore.reset(null); await Promise.all([refreshHome(gradeId), refreshSummary()]); toast('پایه‌ت عوض شد', 'success'); onClose(); } catch (err) { toast(friendlyError(err), 'error'); } finally { setSaving(null); } } return (

پیشرفت هر پایه جدا نگه داشته می‌شه؛ با برگشتن، سر جاشه.

{grades.map((g) => { const on = g.id === profile.gradeId; return ( ); })}
); } function BirthDateSheet({ open, onClose }: { open: boolean; onClose: () => void }) { const { profile, setProfile } = useAppData(); const toast = useToast(); const [birthDate, setBirthDate] = useState(profile.birthDate); const [saving, setSaving] = useState(false); async function save() { if (!birthDate) return; setSaving(true); try { setProfile(await api.profile.update({ birthDate })); toast('ذخیره شد', 'success'); onClose(); } catch (err) { toast(friendlyError(err), 'error'); } finally { setSaving(false); } } return (

روز، ماه و سال تولدت به شمسی.

void save()}>ذخیره
); } /** * خروج. «ورود با حسابِ دیگر» (۲۰۲۶-۱۰-۱۴) برداشته شد: همان خروج بود با متنِ دیگر. عمداً چند حسابِ واردشده روی یک * گوشی نگه داشته نمی‌شود: گوشیِ خانوادگی دست‌به‌دست می‌شود و هر کس بی‌کد واردِ حسابِ دیگری می‌شد، و چند refresh * tokenِ ماندگار در مرورگر خطرِ دزدیده شدن را چند برابر می‌کرد. خروج و ورودِ دوباره با پیامک همان کار را می‌کند. */ function LogoutSheet({ open, onClose, onSignedOut }: { open: boolean; onClose: () => void; onSignedOut: () => void }) { const [busy, setBusy] = useState(false); async function logout() { setBusy(true); try { await api.auth.logout(); } catch { /* حافظه در هر صورت پاک شده؛ کاربر بیرون است */ } askStore.clear(); onSignedOut(); } return (

هر وقت خواستی با همین شماره دوباره وارد شو؛ پیشرفتت سر جاشه.

نه، می‌مونم void logout()}>خروج
); } /** «حساب کاربری»: شماره و شناسه، و پایینش حذفِ حساب (جای استانداردش؛ نه دمِ دستِ صفحه‌ی اصلی). */ function AccountSheet({ open, onClose, onDelete }: { open: boolean; onClose: () => void; onDelete: () => void }) { const { profile } = useAppData(); return (
شماره موبایل {profile.phone ? toFa(profile.phone) : '—'}
شناسه کاربری {profile.publicId}

با حذف حساب، همه پیشرفت و اطلاعاتت برای همیشه پاک می‌شه.

); } /** * حذفِ همیشگیِ حساب (POST /user/delete). شماره‌دار با کدِ تازه‌ی پیامکی تأیید می‌کند تا گوشیِ * بازمانده یا توکنِ دزدیده نتواند حساب را پاک کند. */ function DeleteAccountSheet({ open, onClose, onSignedOut }: { open: boolean; onClose: () => void; onSignedOut: () => void }) { const { profile } = useAppData(); const [sent, setSent] = useState(false); const [value, setValue] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); async function sendCode() { if (!profile.phone) return; setBusy(true); setError(''); try { await api.auth.requestOtp(profile.phone); setSent(true); } catch (err) { setError(friendlyError(err)); } finally { setBusy(false); } } async function remove() { setBusy(true); setError(''); try { await api.auth.deleteAccount({ otp: digitsOnly(value, 6) }); askStore.clear(); onSignedOut(); } catch (err) { setError(friendlyError(err)); } finally { setBusy(false); } } return (

امتیاز، زنجیره، پیشرفت جزیره‌ها، گفتگوها با دستیار و اشتراکت پاک می‌شن و برنمی‌گردن. سوابق پرداخت فقط برای حسابداری می‌مونه. اگه اشتراکت رو از کافه‌بازار خریدی، تمدید خودکارش رو از بخش «اشتراک‌ها»ی بازار لغو کن.

{sent ? ( setValue(digitsOnly(e.target.value, 6))} placeholder="کد پیامک‌شده" inputMode="numeric" dir="ltr" className="mb-3 w-full rounded-2xl border-2 border-line px-4 py-3 text-center font-bold tracking-widest outline-none focus:border-red-400" /> ) : (

برای اطمینان، یک کد به شماره خودت پیامک می‌شه.

)} {error &&
{error}
}
نه، پشیمون شدم {!sent ? ( void sendCode()}>فرستادن کد ) : ( void remove()} > حذف همیشگی )}
); }