forked from Tadrisino/MathApp_frontend
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:
@@ -29,7 +29,7 @@ function recentWeeks(today = new Date()): Date[][] {
|
||||
const dayOfMonth = (d: Date) => new Intl.DateTimeFormat('fa-IR-u-ca-persian', { day: 'numeric' }).format(d);
|
||||
|
||||
export function StreakScreen() {
|
||||
const calendar = useAsync(() => api.streakCalendar(), []);
|
||||
const calendar = useAsync(() => api.streakCalendar(), [], { key: 'streak-calendar' });
|
||||
const active = new Set(calendar.data?.activeDays ?? []);
|
||||
const today = isoDay(new Date());
|
||||
const weeks = recentWeeks();
|
||||
@@ -57,7 +57,7 @@ export function StreakScreen() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<StreakGoal current={calendar.data.currentStreak} />
|
||||
<StreakGoal current={calendar.data.currentStreak} doneToday={active.has(today)} />
|
||||
|
||||
<div className="rounded-3xl border border-line bg-white p-4">
|
||||
<div className="mb-2 grid grid-cols-7 gap-1.5 text-center text-[10px] font-bold text-muted">
|
||||
@@ -96,11 +96,19 @@ export function StreakScreen() {
|
||||
/** نقطهعطفهای زنجیره؛ فقط پیام و نوار، هیچ پاداشِ واقعیای پشتش نیست (پاداش تصمیمِ محصولی است). */
|
||||
const MILESTONES = [3, 7, 14, 30, 60, 100];
|
||||
|
||||
function StreakGoal({ current }: { current: number }) {
|
||||
function StreakGoal({ current, doneToday }: { current: number; doneToday: boolean }) {
|
||||
const next = MILESTONES.find((m) => m > current);
|
||||
const reached = MILESTONES.filter((m) => m <= current).pop();
|
||||
// زنجیرهی صفر: «۳ روز دیگه میشه زنجیرهی ۳ روزه» هدفی دور بود؛ قدمِ امروز کوچک و روشن گفته میشود.
|
||||
// زنجیرهی روشن و امروزِ خالی: یادآوریِ اینکه امروز چیزی مانده تا زنجیره نسوزد.
|
||||
const today = current === 0
|
||||
? 'امروز فقط یه درس یا یه جزیره تموم کن تا زنجیرهت روشن بشه.'
|
||||
: !doneToday
|
||||
? `امروز هنوز تمرین نکردی؛ یه درس بزن تا زنجیرهی ${toFa(current)} روزهت نسوزه.`
|
||||
: null;
|
||||
return (
|
||||
<div className="rounded-3xl border border-line bg-[#FFF3E8] px-4 py-3">
|
||||
{today && <p className="mb-1 text-xs font-bold leading-6 text-[#B4501C]">{today}</p>}
|
||||
<p className="text-sm font-black leading-7 text-ink">
|
||||
{reached ? <><Flame size={16} fill="#FF7A3D" className="mb-0.5 ml-1 inline text-orange" />{toFa(current)} روزهای! </> : null}
|
||||
{next ? `${toFa(next - current)} روز دیگه میشه زنجیرهی ${toFa(next)} روزه.` : 'از همهی نقطهعطفها رد شدی، افسانهای!'}
|
||||
|
||||
Reference in New Issue
Block a user