forked from Tadrisino/MathApp_frontend
Calmer shop with payment choice, step-by-step phone back, sound/haptics that reach the phone, live streak flame
- Shop: subscription perks shown once (each card repeated them, «۳ سوال در روز» twice), plans as selectable rows with one big buy button, prices as «۷۹۸ هزار تومان», a single clear status card for subscribers. Checkout inside the Bazaar app offers «کارتِ بانکی» (default) or «کافهبازار» for packs with a SKU; «بهزودی در بازار» is gone. The gateway opens in the phone browser; status refreshes on return. - Back button: every back-able level (open sheet, pushed screen, non-home tab, shop list) owns exactly one history entry created right after the tap; back closes the sheet, then the screen, then returns to Home, then leaves the app. (A re-pushed trap is skipped by Chrome's history intervention.) - Sound: AudioContext unlocked on the first touch (it was created after a network wait, which WebView/Safari keep muted); softer notes, option tick, finish fanfare, streak whoosh. Haptics through the new TadrisinoAndroid.vibrate bridge when the APK has it, navigator.vibrate otherwise. - Streak: animated flame with lit / at-risk (grey) / frozen (ice) states on Home and the streak screen, a companion reminder when the streak is at risk or frozen, and a full-screen «روزِ پیاپی» celebration after the first lesson of the day (needs backend 9024c6b: streak.activeToday / lost). - Lessons: intro/example text with inline «۱. … ۲. …» renders as a numbered list (2,250 of 4,469 texts). - Quiz: duplicate React key on the answer bubble fixed. - android/MainActivity.kt: vibrate bridge and mediaPlaybackRequiresUserGesture=false. Not built; needs the next APK from the signing owner. Checked in headless Chrome against the local backend (shop as a subscriber with the Bazaar bridge stubbed, back sequences, quiz → streak celebration, frozen state). Not checked on a real phone or in the APK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@ 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 = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'];
|
||||
@@ -30,6 +32,8 @@ const dayOfMonth = (d: Date) => new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
|
||||
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();
|
||||
@@ -48,16 +52,16 @@ export function StreakScreen() {
|
||||
{[
|
||||
{ label: 'زنجیرهی فعلی', value: calendar.data.currentStreak },
|
||||
{ label: 'بیشترین زنجیره', value: calendar.data.longestStreak },
|
||||
].map((s) => (
|
||||
].map((s, i) => (
|
||||
<div key={s.label} className="flex flex-col items-center rounded-3xl border border-line bg-white py-4">
|
||||
<Flame size={24} className="text-orange" fill="#FF7A3D" />
|
||||
{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)} />
|
||||
<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">
|
||||
@@ -96,12 +100,14 @@ export function StreakScreen() {
|
||||
/** نقطهعطفهای زنجیره؛ فقط پیام و نوار، هیچ پاداشِ واقعیای پشتش نیست (پاداش تصمیمِ محصولی است). */
|
||||
const MILESTONES = [3, 7, 14, 30, 60, 100];
|
||||
|
||||
function StreakGoal({ current, doneToday }: { current: number; doneToday: boolean }) {
|
||||
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
|
||||
const today = current === 0 && lost > 0
|
||||
? `زنجیرهی ${toFa(lost)} روزهت یخ زد! امروز یه درس بزن تا دوباره آتیشش روشن بشه.`
|
||||
: current === 0
|
||||
? 'امروز فقط یه درس یا یه جزیره تموم کن تا زنجیرهت روشن بشه.'
|
||||
: !doneToday
|
||||
? `امروز هنوز تمرین نکردی؛ یه درس بزن تا زنجیرهی ${toFa(current)} روزهت نسوزه.`
|
||||
|
||||
Reference in New Issue
Block a user