Files
MathApp_frontend/src/components/BirthDatePicker.tsx
AliReza de7cd0bfa8 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>
2026-10-02 02:38:43 +03:30

120 lines
5.8 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, useRef, useState } from 'react';
import { toEnDigits, toFa } from '@/lib/format';
/**
* تاریخِ تولدِ شمسی با سه فهرستِ روز/ماه/سال (از ۱۴۰۵-۰۷ به جای «چند سالته؟»).
* <select> عمداً بومی است: روی گوشی همان چرخِ آشنای خودِ گوشی باز می‌شود و تقویمِ
* ماهانه‌ای که برای تاریخِ سی سال پیش باید ده‌ها بار ورق بخورد لازم نیست.
*
* سرور تاریخ را شمسی می‌گیرد («1385-04-12») و خودش به میلادی تبدیل می‌کند
* (accounts/jalali.py)؛ این‌جا هیچ تبدیلی انجام نمی‌شود، فقط تعدادِ روزهای ماه.
*/
export const JALALI_MONTHS = [
'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور',
'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند',
];
/** همان بازه‌ی سرور (accounts/serializers.py: MIN_AGE / MAX_AGE). */
const MIN_AGE = 5;
const MAX_AGE = 99;
/** از این سن به بعد فرمِ آشنایی به‌جای «کدوم پایه‌ای؟» می‌پرسد «ریاضیِ کدوم پایه؟». */
export const ADULT_AGE = 19;
type Parts = { y: number; m: number; d: number };
function jalaliParts(date: Date): Parts {
const parts = new Intl.DateTimeFormat('en-US-u-ca-persian', { year: 'numeric', month: 'numeric', day: 'numeric' }).formatToParts(date);
const get = (type: string) => parseInt(toEnDigits(parts.find((p) => p.type === type)?.value ?? '0'), 10);
return { y: get('year'), m: get('month'), d: get('day') };
}
export const todayJalali = () => jalaliParts(new Date());
/** کبیسه یعنی سی‌امِ اسفند وجود دارد؛ آن روز همیشه یکی از ۱۹ تا ۲۱ مارسِ سالِ بعدِ میلادی است. */
function isLeap(y: number): boolean {
return [19, 20, 21].some((day) => {
const p = jalaliParts(new Date(y + 622, 2, day, 12));
return p.y === y && p.m === 12 && p.d === 30;
});
}
function monthLength(y: number, m: number): number {
if (m <= 6) return 31;
if (m <= 11) return 30;
return isLeap(y) ? 30 : 29;
}
function parse(value: string | null | undefined): Parts | null {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? '');
return match ? { y: +match[1], m: +match[2], d: +match[3] } : null;
}
const pad = (n: number) => String(n).padStart(2, '0');
/** «1385-04-12» → «۱۲ تیر ۱۳۸۵». */
export function formatBirthDate(value: string | null): string {
const p = parse(value);
return p ? `${toFa(p.d)} ${JALALI_MONTHS[p.m - 1]} ${toFa(p.y)}` : '';
}
/** سنِ کامل از تاریخِ تولد؛ برای کاربرِ قدیمی که فقط سال داده، تقریبی از سال. */
export function ageOf(birthDate: string | null, birthYear: number | null): number | null {
const today = todayJalali();
const born = parse(birthDate);
if (born) return today.y - born.y - (today.m < born.m || (today.m === born.m && today.d < born.d) ? 1 : 0);
return birthYear ? today.y - birthYear : null;
}
export function BirthDatePicker({
value,
fallbackYear = null,
onChange,
}: {
value: string | null;
/** سالِ تولدِ کاربرِ قدیمی (بی‌تاریخ) تا فهرستِ سال خالی باز نشود. */
fallbackYear?: number | null;
/** فقط وقتی هر سه انتخاب شده‌اند تاریخ می‌دهد؛ تا آن موقع null. */
onChange: (value: string | null) => void;
}) {
const initial = parse(value);
const [y, setY] = useState<number | ''>(initial?.y ?? fallbackYear ?? '');
const [m, setM] = useState<number | ''>(initial?.m ?? '');
const [d, setD] = useState<number | ''>(initial?.d ?? '');
// والد هر بار تابعِ تازه می‌سازد؛ با ref، تغییرِ تابع خودش onChange را دوباره صدا نمی‌زند.
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const thisYear = todayJalali().y;
const years = Array.from({ length: MAX_AGE - MIN_AGE + 1 }, (_, i) => thisYear - MIN_AGE - i);
const days = monthLength(typeof y === 'number' ? y : thisYear, typeof m === 'number' ? m : 1);
// سی‌ویکِ مهر یا سی‌امِ اسفندِ غیرکبیسه با عوض شدنِ ماه/سال معتبر نمی‌ماند.
useEffect(() => {
if (typeof d === 'number' && d > days) setD(days);
}, [d, days]);
useEffect(() => {
const complete = y !== '' && m !== '' && d !== '' && d <= days;
onChangeRef.current(complete ? `${y}-${pad(m)}-${pad(d)}` : null);
}, [y, m, d, days]);
const select = 'w-full appearance-none rounded-2xl border-2 bg-white px-3 py-3.5 text-center text-sm font-bold outline-none focus:border-coral';
const tone = (v: number | '') => (v === '' ? 'border-line text-muted' : 'border-coral text-ink');
return (
<div className="grid grid-cols-[1fr_1.5fr_1.3fr] gap-2">
<select aria-label="روزِ تولد" value={d} onChange={(e) => setD(e.target.value ? +e.target.value : '')} className={`${select} ${tone(d)}`}>
<option value="">روز</option>
{Array.from({ length: days }, (_, i) => i + 1).map((n) => <option key={n} value={n}>{toFa(n)}</option>)}
</select>
<select aria-label="ماهِ تولد" value={m} onChange={(e) => setM(e.target.value ? +e.target.value : '')} className={`${select} ${tone(m)}`}>
<option value="">ماه</option>
{JALALI_MONTHS.map((name, i) => <option key={name} value={i + 1}>{name}</option>)}
</select>
<select aria-label="سالِ تولد" value={y} onChange={(e) => setY(e.target.value ? +e.target.value : '')} className={`${select} ${tone(y)}`}>
<option value="">سال</option>
{years.map((n) => <option key={n} value={n}>{toFa(n)}</option>)}
</select>
</div>
);
}