diff --git a/src/app/App.tsx b/src/app/App.tsx index 04b2676..6073195 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { api, ApiError, http, type Grade, type UserProfile } from '@/api'; +import type { Route } from '@/nav/routes'; import { AppShell } from '@/components/AppShell'; import { ToastProvider } from '@/components/Toast'; import { ErrorState } from '@/components/ui'; @@ -19,7 +20,7 @@ type Flow = | { kind: 'onboarding' } | { kind: 'login' } | { kind: 'setup'; profile: UserProfile; grades: Grade[] } - | { kind: 'main'; profile: UserProfile; grades: Grade[] }; + | { kind: 'main'; profile: UserProfile; grades: Grade[]; start?: Route }; /** کمترین زمانِ نمایشِ اسپلش، تا صفحه چشمک نزند. */ const SPLASH_MIN_MS = 700; @@ -35,7 +36,10 @@ export function App() { const enter = useCallback(async () => { const [profile, grades] = await Promise.all([api.profile.get(), api.grades()]); await trackReferral(profile); - setFlow(profile.name && profile.gradeId ? { kind: 'main', profile, grades } : { kind: 'setup', profile, grades }); + if (profile.name && profile.gradeId) setFlow({ kind: 'main', profile, grades }); + // والد پایه ندارد؛ به‌جای فرمِ دانش‌آموز، مستقیم به بخشِ والدین. + else if (profile.name && (await isParent())) setFlow({ kind: 'main', profile, grades, start: { name: 'family' } }); + else setFlow({ kind: 'setup', profile, grades }); }, []); const boot = useCallback(async () => { @@ -113,6 +117,7 @@ export function App() { profile={flow.profile} grades={flow.grades} onDone={(profile) => setFlow({ kind: 'main', profile, grades: flow.grades })} + onParent={(profile) => setFlow({ kind: 'main', profile, grades: flow.grades, start: { name: 'family' } })} /> )} {flow.kind === 'main' && ( @@ -120,6 +125,7 @@ export function App() { profile={flow.profile} grades={flow.grades} payment={launchPayment} + start={flow.start} onSignedOut={() => setFlow({ kind: 'login' })} onRegistered={trackReferral} /> @@ -131,6 +137,21 @@ export function App() { ); } +/** + * آیا این حساب والد است؟ سرور نقش را صریح نمی‌دهد: پیوندِ «در انتظار» فقط برای + * والدی که دعوت ساخته دیده می‌شود، و گزارشِ هفتگی فقط به والد جواب می‌دهد. + */ +async function isParent(): Promise { + try { + const link = await api.family.link(); + if (link.status === 'pending') return true; + await api.family.weeklyReport(); + return true; + } catch { + return false; + } +} + /** * اگر کاربر با لینکِ دعوت آمده بود و حالا با شماره وارد شده، معرفی را ثبت کن. * مهمان را سرور نمی‌پذیرد، پس کد تا ثبتِ شماره نگه داشته می‌شود. diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index 5d4a071..da25127 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -31,15 +31,17 @@ interface Props { profile: UserProfile; grades: Grade[]; payment: PaymentReturn | null; + /** صفحه‌ی شروع؛ برای والد بخشِ والدین است. */ + start?: Route; onSignedOut: () => void; onRegistered: (profile: UserProfile) => Promise; } /** اپِ اصلی بعد از ورود: تب‌ها، صفحه‌ها و داده‌ی مشترک. */ -export function MainApp({ profile, grades, payment, onSignedOut, onRegistered }: Props) { +export function MainApp({ profile, grades, payment, start, onSignedOut, onRegistered }: Props) { return ( - + {payment && } @@ -88,7 +90,7 @@ function RouteView({ route, onSignedOut, onRegistered }: { route: Route } & Pick case 'characters': return ; case 'streak': return ; case 'support': return ; - case 'family': return ; + case 'family': return ; case 'invite': return ; case 'register': return ; } diff --git a/src/components/ui.tsx b/src/components/ui.tsx index 3570077..10b1edf 100644 --- a/src/components/ui.tsx +++ b/src/components/ui.tsx @@ -33,7 +33,7 @@ export function ScreenHeader({
- {back && ( + {back && (typeof back === 'function' || nav.canGoBack) && ( diff --git a/src/screens/FamilyScreen.tsx b/src/screens/FamilyScreen.tsx index 4ca31ad..9fce3a3 100644 --- a/src/screens/FamilyScreen.tsx +++ b/src/screens/FamilyScreen.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; -import { BookOpen, Flame, Link2, Star, Trophy, UserPlus } from 'lucide-react'; +import { BookOpen, Flame, Link2, LogOut, Star, Trophy, UserPlus } from 'lucide-react'; +import { useNav } from '@/nav/Navigator'; import { api, ApiError, type FamilyLink, type FamilyWeeklyReport } from '@/api'; import { useAsync } from '@/lib/useAsync'; import { useToast } from '@/components/Toast'; @@ -12,7 +13,8 @@ import { digitsOnly, formatJalaliDate, toFa } from '@/lib/format'; * (با همان شماره وارد شده) کد را اینجا وارد می‌کند. بعد از اتصال، والد * گزارشِ هفتگی را می‌بیند. */ -export function FamilyScreen() { +export function FamilyScreen({ onSignedOut }: { onSignedOut: () => void }) { + const nav = useNav(); // GET /family/link نمی‌گوید کاربر در این اتصال والد است یا فرزند؛ گزارشِ // هفتگی فقط برای والد جواب می‌دهد و برای فرزند ۴۰۴ است، پس نقش از همان درمی‌آید. const state = useAsync(async () => { @@ -46,6 +48,16 @@ export function FamilyScreen() { {!data?.role && } )} + {/* والد بدونِ پایه نوارِ پایین را ندارد؛ خروج باید همین‌جا باشد. */} + {!nav.canGoBack && ( + + )} ); diff --git a/src/screens/SetupScreen.tsx b/src/screens/SetupScreen.tsx index 30c3469..4f583a0 100644 --- a/src/screens/SetupScreen.tsx +++ b/src/screens/SetupScreen.tsx @@ -12,10 +12,13 @@ export function SetupScreen({ profile, grades, onDone, + onParent, }: { profile: UserProfile; grades: Grade[]; onDone: (profile: UserProfile) => void; + /** «من والدم»: بدونِ پایه، مستقیم به بخشِ والدین. */ + onParent: (profile: UserProfile) => void; }) { const [name, setName] = useState(profile.name); const [age, setAge] = useState(ageFromBirthYear(profile.birthYear)); @@ -46,6 +49,20 @@ export function SetupScreen({ } } + async function submitAsParent() { + setTouched(true); + if (!name.trim() || saving) return; + setSaving(true); + setError(''); + try { + onParent(await api.profile.update({ name: name.trim() })); + } catch (err) { + setError(friendlyError(err, 'ذخیره نشد، دوباره امتحان کن')); + } finally { + setSaving(false); + } + } + return (
@@ -122,6 +139,9 @@ export function SetupScreen({ void submit()} loading={saving} className="mt-auto"> شروعِ یادگیری +
);