parent report page with two charts, Jalali birth date picker, user ID and invoices in profile
- the parents section is gone: no parent login, invite codes or "I'm a parent" button; "گزارشِ والدین" in the profile shows the week against the previous one (praise line, six tiles, minutes-per-day and right/wrong-per-day charts, a table of the numbers) - birth date as day / month / year selects (Jalali, leap Esfand handled) instead of age; adults (19+) are asked which grade's math they want to learn - user ID under the name, copyable - "پرداختها و فاکتورها": every order with invoice number, discount, amount and reference code - tutor: when the free daily quota runs out, a button to the subscription page instead of "retry" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import {
|
||||
Bell, CalendarDays, Check, ChevronLeft, Coins, Flame, Gift, GraduationCap, LifeBuoy, LogOut,
|
||||
Pencil, Smartphone, Sparkles, Star, Trophy, Users, Volume2, VolumeX,
|
||||
BarChart3, Bell, CalendarDays, Check, ChevronLeft, Coins, Copy, Flame, Gift, GraduationCap, LifeBuoy, LogOut,
|
||||
FileText, Pencil, Smartphone, Sparkles, Star, Trophy, Volume2, VolumeX,
|
||||
} from 'lucide-react';
|
||||
import { api } from '@/api';
|
||||
import { useAppData } from '@/state/AppData';
|
||||
@@ -11,13 +11,13 @@ import type { Route } from '@/nav/routes';
|
||||
import { SeyarMascot } from '@/mascots/SeyarMascot';
|
||||
import { Sheet } from '@/components/Sheet';
|
||||
import { useToast } from '@/components/Toast';
|
||||
import { AgePicker, ageFromBirthYear, birthYearFromAge } from '@/components/AgePicker';
|
||||
import { BirthDatePicker, ageOf, formatBirthDate } from '@/components/BirthDatePicker';
|
||||
import { InlineError, PrimaryButton, Screen, SecondaryButton } from '@/components/ui';
|
||||
import { friendlyError } from '@/lib/errors';
|
||||
import { setSoundEnabled, soundEnabled } from '@/lib/sound';
|
||||
import { toFa } from '@/lib/format';
|
||||
|
||||
type Editing = 'name' | 'grade' | 'age' | 'logout' | null;
|
||||
type Editing = 'name' | 'grade' | 'birth' | 'logout' | null;
|
||||
|
||||
/** «من»: پروفایل، تنظیمات و میانبُر به بخشهای فرعی. */
|
||||
export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
@@ -25,7 +25,18 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
const nav = useNav();
|
||||
const [editing, setEditing] = useState<Editing>(null);
|
||||
const [sound, setSound] = useState(soundEnabled());
|
||||
const age = ageFromBirthYear(profile.birthYear);
|
||||
const toast = useToast();
|
||||
const age = ageOf(profile.birthDate, profile.birthYear);
|
||||
|
||||
// شناسه همان شمارهی ثبتِ کاربر است؛ برای پشتیبانی، بیآنکه شمارهی تلفن گفته شود.
|
||||
async function copyId() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(profile.userId);
|
||||
toast('شناسه کپی شد', 'success');
|
||||
} catch {
|
||||
toast(`شناسهی تو: ${toFa(profile.userId)}`, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
const go = (route: Route) => () => nav.push(route);
|
||||
|
||||
@@ -42,6 +53,9 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
<button type="button" onClick={() => setEditing('name')} className="flex items-center gap-2 text-xl font-black">
|
||||
{profile.name || 'بینام'} <Pencil size={14} className="text-white/70" />
|
||||
</button>
|
||||
<button type="button" onClick={() => void copyId()} className="mt-0.5 flex items-center gap-1 text-xs text-white/75" aria-label="کپیِ شناسهی کاربری">
|
||||
شناسهی کاربری: <span className="font-bold text-white">{toFa(profile.userId)}</span> <Copy size={11} />
|
||||
</button>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-white/80">
|
||||
{gradeTitle && <span className="rounded-full bg-white/15 px-2.5 py-0.5">{gradeTitle}</span>}
|
||||
{age && <span className="rounded-full bg-white/15 px-2.5 py-0.5">{toFa(age)} ساله</span>}
|
||||
@@ -86,11 +100,17 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
|
||||
<Menu>
|
||||
<Row icon={GraduationCap} label="پایهی تحصیلی" value={gradeTitle} onClick={() => setEditing('grade')} />
|
||||
<Row icon={CalendarDays} label="سن" value={age ? `${toFa(age)} سال` : 'وارد نشده'} onClick={() => setEditing('age')} />
|
||||
<Row
|
||||
icon={CalendarDays}
|
||||
label="تاریخِ تولد"
|
||||
value={formatBirthDate(profile.birthDate) || (age ? `${toFa(age)} سال` : 'وارد نشده')}
|
||||
onClick={() => setEditing('birth')}
|
||||
/>
|
||||
<Row icon={Sparkles} label="همراهِ من" onClick={go({ name: 'characters' })} />
|
||||
</Menu>
|
||||
|
||||
<Menu>
|
||||
<Row icon={BarChart3} label="گزارشِ والدین" onClick={go({ name: 'report' })} />
|
||||
<Row icon={Flame} label="تقویمِ زنجیره" onClick={go({ name: 'streak' })} />
|
||||
<Row icon={Trophy} label="جدولِ امتیازها" onClick={go({ name: 'leaderboard' })} />
|
||||
<Row icon={Bell} label="اعلانها" onClick={go({ name: 'notifications' })} />
|
||||
@@ -98,7 +118,7 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
|
||||
<Menu>
|
||||
{!profile.isGuest && <Row icon={Gift} label="معرفی به دوستان" onClick={go({ name: 'invite' })} />}
|
||||
{!profile.isGuest && <Row icon={Users} label="بخشِ والدین" onClick={go({ name: 'family' })} />}
|
||||
<Row icon={FileText} label="پرداختها و فاکتورها" onClick={go({ name: 'invoices' })} />
|
||||
<Row icon={LifeBuoy} label="پشتیبانی و گزارشِ مشکل" onClick={go({ name: 'support' })} />
|
||||
<Row
|
||||
icon={sound ? Volume2 : VolumeX}
|
||||
@@ -119,7 +139,7 @@ export function ProfileScreen({ onSignedOut }: { onSignedOut: () => void }) {
|
||||
|
||||
<NameSheet open={editing === 'name'} onClose={() => setEditing(null)} />
|
||||
<GradeSheet open={editing === 'grade'} onClose={() => setEditing(null)} />
|
||||
<AgeSheet open={editing === 'age'} onClose={() => setEditing(null)} />
|
||||
<BirthDateSheet open={editing === 'birth'} onClose={() => setEditing(null)} />
|
||||
<LogoutSheet open={editing === 'logout'} onClose={() => setEditing(null)} onSignedOut={onSignedOut} />
|
||||
</Screen>
|
||||
);
|
||||
@@ -222,17 +242,17 @@ function GradeSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AgeSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
function BirthDateSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const { profile, setProfile } = useAppData();
|
||||
const toast = useToast();
|
||||
const [age, setAge] = useState<number | null>(ageFromBirthYear(profile.birthYear));
|
||||
const [birthDate, setBirthDate] = useState<string | null>(profile.birthDate);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
async function save() {
|
||||
if (!age) return;
|
||||
if (!birthDate) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
setProfile(await api.profile.update({ birthYear: birthYearFromAge(age) }));
|
||||
setProfile(await api.profile.update({ birthDate }));
|
||||
toast('ذخیره شد', 'success');
|
||||
onClose();
|
||||
} catch (err) {
|
||||
@@ -243,11 +263,12 @@ function AgeSheet({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onClose={onClose} title="چند سالته؟">
|
||||
<Sheet open={open} onClose={onClose} title="تاریخِ تولد">
|
||||
<p className="mb-3 text-xs leading-6 text-muted">روز، ماه و سالِ تولدت به شمسی.</p>
|
||||
<div className="mb-5">
|
||||
<AgePicker value={age} onChange={setAge} />
|
||||
<BirthDatePicker value={profile.birthDate} fallbackYear={profile.birthYear} onChange={setBirthDate} />
|
||||
</div>
|
||||
<PrimaryButton loading={saving} disabled={!age} onClick={() => void save()}>ذخیره</PrimaryButton>
|
||||
<PrimaryButton loading={saving} disabled={!birthDate} onClick={() => void save()}>ذخیره</PrimaryButton>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user