Smooth screen transitions: new screen waits hidden until its data arrives, no half-built flash
- Navigator stage: the next screen mounts hidden under the current one and is revealed only when its first data has arrived (ScreenGate held by useAsync/useGateHold), at most 450 ms; a thin top bar shows after 120 ms of waiting. Push slides from the left, back from the right, tabs only fade. - Session cache (stale-while-revalidate) for screen GETs: revisited screens draw instantly. - Home map and stats are fetched during the splash, so the first screen arrives complete. - Entrance pop-ins (mascot, islands, lesson cards) run on first view only, not on every revisit. - Shop status card shows placeholders instead of a wrong «نداری» while loading. - Re-tapping the open tab scrolls to top instead of reloading; sheets close with a swipe down. - Quiz: «۱ از ۴» instead of «۱/۴» (read as a fraction), varied praise, «N تا پشتِ هم درست», growth-mindset copy after a weak round, zero-reward tiles hidden. - Streak: a concrete next step when the streak is 0 or today is still empty. Checked in headless Chrome against the local backend (frame captures at 150 ms and 700 ms latency, rapid tab tapping, full quiz run). Not checked on a real phone or iOS Safari. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { AnimatePresence, MotionConfig, motion } from 'motion/react';
|
||||
import { api, ApiError, http, type Grade, type UserProfile } from '@/api';
|
||||
import type { Prefetch } from '@/state/AppData';
|
||||
import { AppShell } from '@/components/AppShell';
|
||||
import { ToastProvider } from '@/components/Toast';
|
||||
import { ErrorState } from '@/components/ui';
|
||||
import { KEYS, store } from '@/lib/storage';
|
||||
import { askStore } from '@/state/askStore';
|
||||
import { clearAsyncCache } from '@/lib/useAsync';
|
||||
import { LoginScreen } from '@/screens/LoginScreen';
|
||||
import { OnboardingScreen } from '@/screens/OnboardingScreen';
|
||||
import { SetupScreen } from '@/screens/SetupScreen';
|
||||
@@ -19,7 +21,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[]; prefetch?: Prefetch };
|
||||
|
||||
/** کمترین زمانِ نمایشِ اسپلش، تا صفحه چشمک نزند. */
|
||||
const SPLASH_MIN_MS = 700;
|
||||
@@ -33,9 +35,11 @@ export function App() {
|
||||
|
||||
/** پروفایل و پایهها را میگیرد و تصمیم میگیرد کاربر کجا برود. */
|
||||
const enter = useCallback(async () => {
|
||||
const [profile, grades] = await Promise.all([api.profile.get(), api.grades()]);
|
||||
// نقشه و آمار همزمان با پروفایل گرفته میشوند تا خانه کامل بیاید، نه اول «دارم مسیرت رو میارم» و آمارِ صفر.
|
||||
// شکستشان ورود را نمیخواباند: AppData همانها را دوباره میگیرد و خطای نقشه را روی خانه نشان میدهد.
|
||||
const [profile, grades, prefetch] = await Promise.all([api.profile.get(), api.grades(), prefetchMain()]);
|
||||
await trackReferral();
|
||||
if (profile.name && profile.gradeId) setFlow({ kind: 'main', profile, grades });
|
||||
if (profile.name && profile.gradeId) setFlow({ kind: 'main', profile, grades, prefetch });
|
||||
else setFlow({ kind: 'setup', profile, grades });
|
||||
}, []);
|
||||
|
||||
@@ -69,6 +73,7 @@ export function App() {
|
||||
() =>
|
||||
http.onSignedOut(() => {
|
||||
askStore.clear();
|
||||
clearAsyncCache();
|
||||
setFlow({ kind: 'login' });
|
||||
}),
|
||||
[],
|
||||
@@ -122,8 +127,12 @@ export function App() {
|
||||
<MainApp
|
||||
profile={flow.profile}
|
||||
grades={flow.grades}
|
||||
prefetch={flow.prefetch}
|
||||
payment={launchPayment}
|
||||
onSignedOut={() => setFlow({ kind: 'login' })}
|
||||
onSignedOut={() => {
|
||||
clearAsyncCache();
|
||||
setFlow({ kind: 'login' });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
@@ -134,6 +143,15 @@ export function App() {
|
||||
);
|
||||
}
|
||||
|
||||
/** دادهی صفحهی اول؛ هر کدام که نرسید null میماند و AppData خودش دوباره میگیرد. */
|
||||
async function prefetchMain(): Promise<Prefetch> {
|
||||
const [home, summary] = await Promise.allSettled([api.home(), api.summary()]);
|
||||
return {
|
||||
home: home.status === 'fulfilled' ? home.value : null,
|
||||
summary: summary.status === 'fulfilled' ? summary.value : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** اگر کاربر با لینکِ دعوت آمده بود و حالا وارد شده، معرفی را ثبت کن. */
|
||||
async function trackReferral() {
|
||||
const code = store.get(KEYS.referralCode);
|
||||
|
||||
Reference in New Issue
Block a user