forked from Tadrisino/MathApp_frontend
- 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>
130 lines
6.5 KiB
TypeScript
130 lines
6.5 KiB
TypeScript
import { Flame } from 'lucide-react';
|
||
import { api } from '@/api';
|
||
import { useAsync } from '@/lib/useAsync';
|
||
import { ErrorState, LoadingState, Screen, ScreenBody, ScreenHeader } from '@/components/ui';
|
||
import { toFa } from '@/lib/format';
|
||
import { useAppData } from '@/state/AppData';
|
||
import { StreakFlame, flameState } from '@/components/StreakFlame';
|
||
|
||
const WEEKS = 6;
|
||
const WEEKDAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'];
|
||
|
||
/** YYYY-MM-DD به وقتِ محلی (سرور روزها را همینطور میدهد). */
|
||
const isoDay = (d: Date) =>
|
||
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
|
||
/** شش هفتهی اخیر، هفته از شنبه شروع میشود. */
|
||
function recentWeeks(today = new Date()): Date[][] {
|
||
const end = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||
const offsetFromSaturday = (end.getDay() + 1) % 7;
|
||
const start = new Date(end);
|
||
start.setDate(end.getDate() - offsetFromSaturday - (WEEKS - 1) * 7);
|
||
return Array.from({ length: WEEKS }, (_, w) =>
|
||
Array.from({ length: 7 }, (_, d) => {
|
||
const day = new Date(start);
|
||
day.setDate(start.getDate() + w * 7 + d);
|
||
return day;
|
||
}),
|
||
);
|
||
}
|
||
|
||
const dayOfMonth = (d: Date) => new Intl.DateTimeFormat('fa-IR-u-ca-persian', { day: 'numeric' }).format(d);
|
||
|
||
export function StreakScreen() {
|
||
const calendar = useAsync(() => api.streakCalendar(), [], { key: 'streak-calendar' });
|
||
const { summary } = useAppData();
|
||
const flame = flameState(summary?.streak);
|
||
const active = new Set(calendar.data?.activeDays ?? []);
|
||
const today = isoDay(new Date());
|
||
const weeks = recentWeeks();
|
||
|
||
return (
|
||
<Screen>
|
||
<ScreenHeader title="زنجیره روزها" subtitle="هر روز حداقل یه درس، زنجیرهت رو زنده نگه میداره" color="#FF7A3D" back mascot={{ mood: 'excited', size: 56 }} />
|
||
<ScreenBody className="flex flex-col gap-4">
|
||
{calendar.loading ? (
|
||
<LoadingState />
|
||
) : calendar.error || !calendar.data ? (
|
||
<ErrorState error={calendar.error} onRetry={calendar.reload} />
|
||
) : (
|
||
<>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{[
|
||
{ label: 'زنجیره فعلی', value: calendar.data.currentStreak },
|
||
{ label: 'بیشترین زنجیره', value: calendar.data.longestStreak },
|
||
].map((s, i) => (
|
||
<div key={s.label} className="flex flex-col items-center rounded-3xl border border-line bg-white py-4">
|
||
{i === 0 ? <StreakFlame state={flame} size={26} /> : <Flame size={24} className="text-orange" fill="#FF7A3D" />}
|
||
<span className="mt-1 text-2xl font-black text-ink">{toFa(s.value)}</span>
|
||
<span className="text-xs text-muted">{s.label} (روز)</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<StreakGoal current={calendar.data.currentStreak} doneToday={active.has(today)} lost={summary?.streak.lost ?? 0} />
|
||
|
||
<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">
|
||
{WEEKDAYS.map((d) => <span key={d}>{d}</span>)}
|
||
</div>
|
||
<div className="flex flex-col gap-1.5">
|
||
{weeks.map((week, w) => (
|
||
<div key={w} className="grid grid-cols-7 gap-1.5">
|
||
{week.map((day) => {
|
||
const key = isoDay(day);
|
||
const on = active.has(key);
|
||
const future = key > today;
|
||
return (
|
||
<span
|
||
key={key}
|
||
title={key}
|
||
className={`flex aspect-square items-center justify-center rounded-xl text-[11px] font-bold ${
|
||
on ? 'bg-orange text-white' : future ? 'text-faint/50' : 'bg-soft text-muted'
|
||
} ${key === today ? 'ring-2 ring-orange ring-offset-1' : ''}`}
|
||
>
|
||
{on ? <Flame size={14} fill="white" /> : dayOfMonth(day)}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</ScreenBody>
|
||
</Screen>
|
||
);
|
||
}
|
||
|
||
/** نقطهعطفهای زنجیره؛ فقط پیام و نوار، هیچ پاداشِ واقعیای پشتش نیست (پاداش تصمیمِ محصولی است). */
|
||
const MILESTONES = [3, 7, 14, 30, 60, 100];
|
||
|
||
function StreakGoal({ current, doneToday, lost }: { current: number; doneToday: boolean; lost: number }) {
|
||
const next = MILESTONES.find((m) => m > current);
|
||
const reached = MILESTONES.filter((m) => m <= current).pop();
|
||
// زنجیرهی صفر: «۳ روز دیگه میشه زنجیرهی ۳ روزه» هدفی دور بود؛ قدمِ امروز کوچک و روشن گفته میشود.
|
||
// زنجیرهی روشن و امروزِ خالی: یادآوریِ اینکه امروز چیزی مانده تا زنجیره نسوزد.
|
||
const today = current === 0 && lost > 0
|
||
? `زنجیره ${toFa(lost)} روزهت یخ زد! امروز یه درس بزن تا دوباره آتیشش روشن بشه.`
|
||
: 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)} روزه.` : 'از همه نقطهعطفها رد شدی، افسانهای!'}
|
||
</p>
|
||
{next && (
|
||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white">
|
||
<div className="h-full rounded-full bg-orange transition-[width]" style={{ width: `${Math.round((current / next) * 100)}%` }} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|