Files
MathApp_frontend/src/screens/InviteScreen.tsx
AliReza eafee76a93 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>
2026-10-10 17:55:47 +03:30

86 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Copy, Gift, Share2, Users } from 'lucide-react';
import { api } from '@/api';
import { useAsync } from '@/lib/useAsync';
import { useToast } from '@/components/Toast';
import { ErrorState, LoadingState, PrimaryButton, Screen, ScreenBody, ScreenHeader } from '@/components/ui';
import { formatToman, toFa } from '@/lib/format';
/** لینکِ دعوت: هر کس با آن بیاید و شماره ثبت کند، معرفیِ تو حساب می‌شود. */
export function inviteLink(code: string): string {
return `${window.location.origin}/?ref=${encodeURIComponent(code)}`;
}
export function InviteScreen() {
const toast = useToast();
const me = useAsync(() => api.affiliate.me(), [], { key: 'affiliate' });
async function share(code: string) {
const url = inviteLink(code);
const text = 'با تدریسینو ریاضی رو قدم‌به‌قدم یاد بگیر:';
if (navigator.share) {
try {
await navigator.share({ title: 'تدریسینو', text, url });
return;
} catch {
/* کاربر لغو کرد؛ می‌رویم سراغِ کپی */
}
}
try {
await navigator.clipboard.writeText(`${text} ${url}`);
toast('لینک کپی شد', 'success');
} catch {
toast(url);
}
}
return (
<Screen>
<ScreenHeader title="معرفی به دوستان" subtitle="دوستات رو بیار، با هم ریاضی بخونید" color="#5FBF8F" back mascot={{ char: 'delta', mood: 'happy', size: 56 }} />
<ScreenBody className="flex flex-col gap-4">
{me.loading ? (
<LoadingState />
) : me.error || !me.data ? (
<ErrorState error={me.error} onRetry={me.reload} />
) : (
<>
<div className="flex flex-col items-center gap-2 rounded-3xl border border-line bg-white p-5 text-center">
<Gift size={28} className="text-mint" />
<span className="text-xs text-muted">کدِ معرفیِ تو</span>
<button
type="button"
onClick={() => void navigator.clipboard?.writeText(me.data!.referralCode).then(() => toast('کد کپی شد', 'success'))}
className="flex items-center gap-2 rounded-2xl bg-soft px-5 py-2.5 text-xl font-black tracking-widest text-ink"
dir="ltr"
>
{me.data.referralCode} <Copy size={16} className="text-muted" />
</button>
</div>
<div className="grid grid-cols-3 gap-2">
{[
{ label: 'معرفی‌شده', value: toFa(me.data.referredCount) },
{ label: 'خرید کرده', value: toFa(me.data.paidCount) },
{ label: 'پورسانت', value: formatToman(me.data.totalCommission) },
].map((s) => (
<div key={s.label} className="flex flex-col items-center rounded-2xl border border-line bg-white px-1 py-3 text-center">
<span className="text-sm font-black text-ink">{s.value}</span>
<span className="mt-0.5 text-[10px] text-muted">{s.label}</span>
</div>
))}
</div>
<p className="flex items-start gap-2 rounded-2xl bg-[#EAF8F0] p-3.5 text-xs leading-6 text-body">
<Users size={16} className="mt-0.5 flex-shrink-0 text-mint" />
دوستت باید با لینکِ تو وارد بشه و شماره‌ش رو ثبت کنه. اگه اشتراک خرید، {toFa(Math.round(me.data.commissionRate * 100))}٪ مبلغ پورسانتِ توئه.
</p>
<PrimaryButton color="#5FBF8F" onClick={() => void share(me.data!.referralCode)}>
<Share2 size={17} /> فرستادنِ لینکِ دعوت
</PrimaryButton>
</>
)}
</ScreenBody>
</Screen>
);
}