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:
@@ -26,9 +26,9 @@ type View = 'subscription' | 'pack';
|
||||
* یکجا کاربر را گیج نکند. از فصلِ قفل یکراست به فهرستِ اشتراک میآید (`initialView`).
|
||||
*/
|
||||
export function SubscriptionScreen({ initialView }: { initialView?: View }) {
|
||||
const plans = useAsync(() => api.subscription.plans(), []);
|
||||
const status = useAsync(() => api.subscription.status(), []);
|
||||
const quota = useAsync(() => api.ai.quota(), []);
|
||||
const plans = useAsync(() => api.subscription.plans(), [], { key: 'plans' });
|
||||
const status = useAsync(() => api.subscription.status(), [], { key: 'subscription-status' });
|
||||
const quota = useAsync(() => api.ai.quota(), [], { key: 'ai-quota' });
|
||||
const [view, setView] = useState<View | null>(initialView ?? null);
|
||||
const [buying, setBuying] = useState<Plan | null>(null);
|
||||
// داخلِ اپِ بازار، طبقِ قانونِ بازار، خرید فقط با پرداختِ درونبرنامهایِ خودش است؛ محصولی که
|
||||
@@ -53,7 +53,7 @@ export function SubscriptionScreen({ initialView }: { initialView?: View }) {
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4 px-4 py-5">
|
||||
<StatusCard status={status.data ?? null} credits={credits} heartsUntil={heartsUntil} quota={quota.data ?? null} />
|
||||
<StatusCard status={status.data ?? null} statusLoading={status.loading && !status.data} credits={credits} heartsUntil={heartsUntil} quota={quota.data ?? null} quotaLoading={quota.loading && !quota.data} />
|
||||
|
||||
{view === null ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -132,8 +132,11 @@ function ChoiceCard({ icon, tone, title, text, hint, onClick }: { icon: ReactNod
|
||||
}
|
||||
|
||||
/** وضعیتِ فعلیِ کاربر بالای فروشگاه: اشتراک و روزهای مانده، اعتبارِ سوال، قلبِ نامحدود. */
|
||||
function StatusCard({ status, credits, heartsUntil, quota }: {
|
||||
status: SubscriptionStatus | null; credits: number; heartsUntil: string | null; quota: TutorQuota | null;
|
||||
/** تا وضعیت نرسیده جای خالیِ هماندازه نشان داده میشود؛ قبلاً «نداری» میآمد و برای مشترک غلط بود. */
|
||||
const Placeholder = ({ className }: { className: string }) => <span className={`inline-block animate-pulse rounded-lg bg-soft align-middle ${className}`} />;
|
||||
|
||||
function StatusCard({ status, statusLoading, credits, heartsUntil, quota, quotaLoading }: {
|
||||
status: SubscriptionStatus | null; statusLoading: boolean; credits: number; heartsUntil: string | null; quota: TutorQuota | null; quotaLoading: boolean;
|
||||
}) {
|
||||
const days = status?.active ? daysLeft(status.expiresAt) : null;
|
||||
return (
|
||||
@@ -143,9 +146,10 @@ function StatusCard({ status, credits, heartsUntil, quota }: {
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-bold text-muted">اشتراکِ تو</div>
|
||||
<div className="text-base font-black text-ink">
|
||||
{status?.active ? <>{status.planTitle ?? 'فعال'}{days !== null && <span className="text-sm font-bold text-body"> · {toFa(days)} روز مونده</span>}</> : 'نداری'}
|
||||
{statusLoading ? <Placeholder className="h-5 w-20" /> : status?.active ? <>{status.planTitle ?? 'فعال'}{days !== null && <span className="text-sm font-bold text-body"> · {toFa(days)} روز مونده</span>}</> : 'نداری'}
|
||||
</div>
|
||||
</div>
|
||||
{quotaLoading && <Placeholder className="h-10 w-14" />}
|
||||
{quota && (
|
||||
<div className="text-left text-xs leading-6 text-muted">
|
||||
سوالِ امروز<br />
|
||||
@@ -154,7 +158,7 @@ function StatusCard({ status, credits, heartsUntil, quota }: {
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 border-t border-line pt-3 text-xs font-bold">
|
||||
<span className="flex items-center gap-1.5 text-violet"><Sparkles size={13} /> اعتبار: {toFa(credits)} سوال</span>
|
||||
<span className="flex items-center gap-1.5 text-violet"><Sparkles size={13} /> اعتبار: {statusLoading && quotaLoading ? <Placeholder className="h-3.5 w-10" /> : <>{toFa(credits)} سوال</>}</span>
|
||||
{(status?.active || heartsUntil) && (
|
||||
<span className="flex items-center gap-1.5 text-coral">
|
||||
<Heart size={13} fill="#ED6956" /> قلبِ نامحدود{!status?.active && heartsUntil && <> تا {formatJalaliDate(heartsUntil)}</>}
|
||||
|
||||
Reference in New Issue
Block a user