forked from Tadrisino/MathApp_frontend
give parents their own way in instead of the student setup form
A parent account has no grade, so the app sent the demo mother to the "which grade are you in?" form and she never reached the parents screen. Now an account with a name, no grade and a parent-side family link opens straight on the parents screen (with a logout button, since it has no tab bar), and the setup form offers "I'm a parent, not a student" for new ones. The header's back button only shows when there is somewhere to go back to. Found on the live site with the demo accounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<boolean> {
|
||||
try {
|
||||
const link = await api.family.link();
|
||||
if (link.status === 'pending') return true;
|
||||
await api.family.weeklyReport();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* اگر کاربر با لینکِ دعوت آمده بود و حالا با شماره وارد شده، معرفی را ثبت کن.
|
||||
* مهمان را سرور نمیپذیرد، پس کد تا ثبتِ شماره نگه داشته میشود.
|
||||
|
||||
@@ -31,15 +31,17 @@ interface Props {
|
||||
profile: UserProfile;
|
||||
grades: Grade[];
|
||||
payment: PaymentReturn | null;
|
||||
/** صفحهی شروع؛ برای والد بخشِ والدین است. */
|
||||
start?: Route;
|
||||
onSignedOut: () => void;
|
||||
onRegistered: (profile: UserProfile) => Promise<void>;
|
||||
}
|
||||
|
||||
/** اپِ اصلی بعد از ورود: تبها، صفحهها و دادهی مشترک. */
|
||||
export function MainApp({ profile, grades, payment, onSignedOut, onRegistered }: Props) {
|
||||
export function MainApp({ profile, grades, payment, start, onSignedOut, onRegistered }: Props) {
|
||||
return (
|
||||
<AppDataProvider initialProfile={profile} grades={grades}>
|
||||
<NavigatorProvider initial={{ name: payment ? 'subscription' : 'home' }}>
|
||||
<NavigatorProvider initial={start ?? { name: payment ? 'subscription' : 'home' }}>
|
||||
<Screens onSignedOut={onSignedOut} onRegistered={onRegistered} />
|
||||
{payment && <PaymentResult payment={payment} />}
|
||||
</NavigatorProvider>
|
||||
@@ -88,7 +90,7 @@ function RouteView({ route, onSignedOut, onRegistered }: { route: Route } & Pick
|
||||
case 'characters': return <CharactersScreen />;
|
||||
case 'streak': return <StreakScreen />;
|
||||
case 'support': return <SupportScreen />;
|
||||
case 'family': return <FamilyScreen />;
|
||||
case 'family': return <FamilyScreen onSignedOut={onSignedOut} />;
|
||||
case 'invite': return <InviteScreen />;
|
||||
case 'register': return <RegisterView onRegistered={onRegistered} />;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ScreenHeader({
|
||||
<div className="pointer-events-none absolute -left-12 -top-12 h-40 w-40 rounded-full bg-white/10" />
|
||||
<div className="pointer-events-none absolute -right-10 top-10 h-28 w-28 rounded-full bg-white/10" />
|
||||
<div className="relative z-10 pt-6">
|
||||
{back && (
|
||||
{back && (typeof back === 'function' || nav.canGoBack) && (
|
||||
<IconButton label="بازگشت" onClick={onBack} className="mb-3 bg-white/15">
|
||||
<ChevronRight size={18} strokeWidth={2.5} />
|
||||
</IconButton>
|
||||
|
||||
@@ -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 && <AcceptInvite onDone={state.reload} />}
|
||||
</>
|
||||
)}
|
||||
{/* والد بدونِ پایه نوارِ پایین را ندارد؛ خروج باید همینجا باشد. */}
|
||||
{!nav.canGoBack && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void api.auth.logout().catch(() => {}).finally(onSignedOut)}
|
||||
className="flex items-center justify-center gap-2 py-3 text-sm font-bold text-red-500"
|
||||
>
|
||||
<LogOut size={16} /> خروج از حساب
|
||||
</button>
|
||||
)}
|
||||
</ScreenBody>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
@@ -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<number | null>(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 (
|
||||
<div className="flex h-full flex-col bg-page">
|
||||
<header className="relative overflow-hidden rounded-b-[32px] bg-coral px-5 pb-7 pt-safe text-white">
|
||||
@@ -122,6 +139,9 @@ export function SetupScreen({
|
||||
<PrimaryButton onClick={() => void submit()} loading={saving} className="mt-auto">
|
||||
شروعِ یادگیری <ChevronLeft size={19} strokeWidth={3} />
|
||||
</PrimaryButton>
|
||||
<button type="button" onClick={() => void submitAsParent()} disabled={saving} className="-mt-2 py-2 text-sm font-bold text-muted disabled:opacity-40">
|
||||
دانشآموز نیستم؛ والدم
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user