Files
MathApp_frontend/src/components/StreakFlame.tsx
AliReza ca96a91dee No diacritics or written ezafe ی in the UI; map without the speech banner, companions cheering along the path; familiar payment sheet with gateway logos and per-channel methods; account deletion moved under «حساب کاربری»
- Text: kasra/fatha/damma/shadda/sukun/hamza-above and «ه‌ی» ezafe removed from every user-visible string (comments untouched); tanwin kept. Stale copy fixed (question packs are no longer sold), awkward onboarding sentences rewritten, سؤال → سوال. plainPersian() in MathText cleans the live tutor stream.
- Home: the mascot + speech bubble header is gone; the selected companion hops beside the current island and sits at every third bend (CSS transform animations only).
- Checkout: order summary, payment-method rows with Zarinpal/Bazaar icons and radio, amount on the pay button, lock trust line, darker overlay. paymentMethods in config.js per channel (web: zarinpal, app: zarinpal+bazaar by default).
- Profile: «ورود با حساب دیگر» removed (it was logout); new «حساب کاربری» sheet shows phone/ID and holds «حذف حساب کاربری»; logout is a red row. Delete sheet tells Bazaar subscribers to cancel renewal in Bazaar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:55:17 +03:30

128 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 { useEffect, useState } from 'react';
import { motion } from 'motion/react';
import { PrimaryButton } from '@/components/ui';
import { toFa } from '@/lib/format';
import { sounds } from '@/lib/sound';
/**
* سه حالتِ زنجیره، مثلِ دولینگو:
* lit امروز تمرین شده؛ آتشِ نارنجیِ زنده (لرزشِ آرامِ شعله‌ها).
* risk زنجیره هست ولی امروز هنوز نه؛ همان شعله خاکستری، تا کاربر ببیند چیزی منتظرش است.
* frozen زنجیره دیروز شکست؛ شعله‌ی یخ‌زده‌ی آبی، با برق.
* none هیچ‌وقت زنجیره نداشته؛ مثلِ risk.
* فقط SVG و CSS (transform/opacity)، بی‌تصویر.
*/
export type FlameState = 'lit' | 'risk' | 'frozen' | 'none';
export function flameState(streak: { current: number; activeToday?: boolean; lost?: number } | undefined | null): FlameState {
if (!streak) return 'none';
if (streak.activeToday) return 'lit';
if (streak.current > 0) return 'risk';
if ((streak.lost ?? 0) > 0) return 'frozen';
return 'none';
}
const PALETTE: Record<FlameState, [string, string, string]> = {
lit: ['#FF7A3D', '#FFB23F', '#FFE58A'],
risk: ['#C9C6DA', '#DCDAE8', '#EEEDF5'],
none: ['#C9C6DA', '#DCDAE8', '#EEEDF5'],
frozen: ['#5BB8F0', '#9ED8FA', '#E6F6FF'],
};
export function StreakFlame({ state, size = 20, className = '' }: { state: FlameState; size?: number; className?: string }) {
const [outer, middle, core] = PALETTE[state];
const alive = state === 'lit';
return (
<svg viewBox="0 0 64 80" width={size} height={size * 1.25} className={className} aria-hidden>
<g className={alive ? 'flame-outer' : undefined}>
<path d="M32 2C36 18 54 26 54 50c0 15-10 28-22 28S10 65 10 50c0-11 6-18 10-22 1 7 4 11 8 12-2-14 0-28 4-38z" fill={outer} />
</g>
<g className={alive ? 'flame-inner' : undefined}>
<path d="M33 30c3 9 13 13 13 26 0 9-6 16-14 16s-14-7-14-16c0-6 3-10 6-12 0 4 2 7 5 7-1-8 1-15 4-21z" fill={middle} />
</g>
<path d="M32 52c2 5 7 7 7 13 0 4-3 7-7 7s-7-3-7-7c0-3 2-5 3-6 0 2 1 3 2 3 0-4 1-7 2-10z" fill={core} />
{state === 'frozen' && (
<>
{/* برقِ یخ و ترک‌های ریز */}
<path d="M22 40l6 4M40 34l-5 6M30 60l4-5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" opacity=".85" />
<circle cx="44" cy="22" r="2.4" fill="#fff" className="ice-glint" />
</>
)}
</svg>
);
}
const WEEKDAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'];
/**
* صفحه‌ی «زنجیره‌ت روشن ماند!» بعد از اولین درس/جزیره‌ی امروز. عدد از مقدارِ قبلی به تازه می‌رود و روزهای این
* هفته که در زنجیره‌اند روشن می‌شوند (از طولِ زنجیره حساب می‌شود، بی‌درخواستِ دیگر).
*/
export function StreakCelebration({ from, to, onDone }: { from: number; to: number; onDone: () => void }) {
const [shown, setShown] = useState(from);
useEffect(() => {
sounds.streak();
const t = setTimeout(() => setShown(to), 650);
return () => clearTimeout(t);
}, [to]);
const today = (new Date().getDay() + 1) % 7; // شنبه = ۰
const message = to === 1
? 'اولین روز زنجیره‌ت! فردا هم بیا تا آتیشش بزرگ‌تر بشه.'
: `${toFa(to)} روزه که هر روز ریاضی تمرین می‌کنی. فردا هم بیا تا خاموش نشه!`;
return (
<div className="relative flex h-full flex-col items-center justify-center gap-6 overflow-hidden bg-gradient-to-b from-[#FFF4EC] to-white px-8 text-center">
<div aria-hidden className="pointer-events-none absolute inset-x-0 bottom-1/3 h-48">
{[14, 30, 48, 66, 82].map((x, i) => (
<span key={x} className="spark-rise absolute bottom-0 h-2 w-2 rounded-full bg-[#FFB23F]" style={{ left: `${x}%`, animationDelay: `${i * 0.35}s` }} />
))}
</div>
<motion.div
initial={{ scale: 0.3, y: 40, opacity: 0 }}
animate={{ scale: 1, y: 0, opacity: 1 }}
transition={{ type: 'spring', stiffness: 180, damping: 12 }}
className="relative"
>
<div className="absolute inset-0 -z-10 scale-150 rounded-full bg-[#FFB23F]/30 blur-2xl" />
<StreakFlame state="lit" size={130} />
</motion.div>
<div>
<motion.div
key={shown}
initial={{ scale: 1.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 14 }}
className="text-6xl font-black text-orange"
>
{toFa(shown)}
</motion.div>
<h2 className="mt-1 text-2xl font-black text-ink">روز پیاپی!</h2>
<p className="mt-2 text-sm leading-7 text-body">{message}</p>
</div>
<div className="flex gap-2" aria-label="روزهای این هفته">
{WEEKDAYS.map((d, i) => {
const inStreak = i <= today && today - i < to;
return (
<motion.div
key={d}
initial={{ scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.5 + i * 0.06 }}
className="flex flex-col items-center gap-1"
>
<span className={`flex h-9 w-9 items-center justify-center rounded-full ${inStreak ? 'bg-orange' : 'bg-soft'} ${i === today ? 'ring-2 ring-orange ring-offset-2' : ''}`}>
{inStreak ? <StreakFlame state="lit" size={16} className="[&_path]:fill-white" /> : null}
</span>
<span className={`text-xs font-bold ${i === today ? 'text-orange' : 'text-muted'}`}>{d}</span>
</motion.div>
);
})}
</div>
<div className="w-full">
<PrimaryButton color="#FF7A3D" onClick={onDone}>ادامه</PrimaryButton>
</div>
</div>
);
}