Files
MathApp_frontend/src/components/BirthDatePicker.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

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>
);
}