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>
This commit is contained in:
2026-10-14 16:55:17 +03:30
parent 4a661f504f
commit ca96a91dee
48 changed files with 492 additions and 337 deletions

View File

@@ -257,7 +257,7 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
<p className="text-sm text-muted">دارم نتیجه‌ت رو ثبت می‌کنم…</p>
) : (
<>
<InlineError>نتیجه‌ی این تمرین ثبت نشد: {friendlyError(phase.error)}</InlineError>
<InlineError>نتیجه این تمرین ثبت نشد: {friendlyError(phase.error)}</InlineError>
<PrimaryButton color={color} onClick={() => void finish()}>دوباره ثبت کن</PrimaryButton>
<SecondaryButton onClick={nav.back}>بی‌خیال، برگرد</SecondaryButton>
</>
@@ -281,9 +281,9 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
const mood: SeyarMood = !answer ? 'idle' : answer.correct ? 'excited' : outOfHearts ? 'depressed' : 'sad';
const bubble = !answer
? selected === null ? 'جوابِ درست رو انتخاب کن!' : 'مطمئنی؟ «بررسی» رو بزن.'
? selected === null ? 'جواب درست رو انتخاب کن!' : 'مطمئنی؟ «بررسی» رو بزن.'
: answer.correct
? combo >= 2 ? `${toFa(combo)} تا پشتِ هم درست! داری عالی پیش می‌ری` : PRAISE[index % PRAISE.length]
? combo >= 2 ? `${toFa(combo)} تا پشت هم درست! داری عالی پیش می‌ری` : PRAISE[index % PRAISE.length]
: outOfHearts
? 'قلب‌هات تموم شد؛ یه کم استراحت کنیم.'
: NUDGE[index % NUDGE.length];
@@ -343,7 +343,7 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
<div className="rounded-3xl border border-line bg-white p-5 shadow-sm">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-muted">سوالِ {toFa(index + 1)}</span>
<span className="text-xs font-medium text-muted">سوال {toFa(index + 1)}</span>
</div>
<h2 className="whitespace-pre-line text-base font-bold leading-8 text-ink"><MathText text={question.prompt} /></h2>
{question.figure ? <div className="mt-3"><Diagram spec={question.figure} /></div> : null}
@@ -389,7 +389,7 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
</button>
)}
<PrimaryButton color={answer.correct ? '#5FBF8F' : color} onClick={() => void next()}>
{isLast ? 'پایانِ تمرین' : 'ادامه'} <ChevronLeft size={18} />
{isLast ? 'پایان تمرین' : 'ادامه'} <ChevronLeft size={18} />
</PrimaryButton>
</>
) : (
@@ -417,7 +417,7 @@ function HeartsRow({ hearts }: { hearts: Hearts | null }) {
if (hearts?.isUnlimited) {
return (
<div className="mt-3 flex items-center justify-center gap-1.5 text-xs font-bold text-coral">
<Heart size={18} fill="#ED6956" /> <InfinityIcon size={18} /> قلبِ نامحدود
<Heart size={18} fill="#ED6956" /> <InfinityIcon size={18} /> قلب نامحدود
</div>
);
}
@@ -495,7 +495,7 @@ function Result({
<div>
<h2 className="text-2xl font-black text-ink">{result.stars === 3 ? 'عالی بود!' : result.stars === 2 ? 'آفرین!' : 'تموم شد!'}</h2>
{nodeMode && <p className="mt-1 text-sm font-bold" style={{ color }}>این جزیره رو تموم کردی</p>}
<p className="mt-1 text-sm text-muted">{toFa(correct)} جوابِ درست از {toFa(total)} سوال</p>
<p className="mt-1 text-sm text-muted">{toFa(correct)} جواب درست از {toFa(total)} سوال</p>
{/* نمره‌ی کم: تلاش و راه را ستایش کن، نه «باهوشی» را (پژوهش‌های ذهنیتِ رشد)؛ و راهِ بعدی را نشان بده. */}
{result.stars <= 1 && (
<p className="mt-2 text-xs leading-6 text-body">
@@ -517,7 +517,7 @@ function Result({
</div>
</div>}
<PrimaryButton color={color} loading={loadingNext} onClick={() => then(() => void goNext())}>
{nodeMode && nextNode ? 'بریم جزیره‌ی بعدی' : 'ادامه'} <ChevronLeft size={18} />
{nodeMode && nextNode ? 'بریم جزیره بعدی' : 'ادامه'} <ChevronLeft size={18} />
</PrimaryButton>
{nodeMode && nextNode && (
<button type="button" onClick={() => then(nav.back)} className="-mt-2 py-2 text-sm font-bold text-muted">
@@ -534,13 +534,13 @@ function Result({
* تلاش و راه ستایش می‌شود و قدمِ بعدی گفته می‌شود، نه این‌که «اشکالی نداره» تنها بماند.
*/
const PRAISE = ['آفرین! درست بود', 'دقیقاً همینه!', 'عالی! خوب فکر کردی', 'درسته! داری راه میفتی'];
const NUDGE = ['نزدیک بودی! جوابِ درست رو ببین.', 'اشکالی نداره؛ از همین اشتباه یاد می‌گیریم.', 'این یکی سخت بود؛ جوابِ درست سبز شده.'];
const NUDGE = ['نزدیک بودی! جواب درست رو ببین.', 'اشکالی نداره؛ از همین اشتباه یاد می‌گیریم.', 'این یکی سخت بود؛ جواب درست سبز شده.'];
/** «راهنمای مهربون»ِ پی: نکته‌ی عمومیِ کوتاه بعد از جوابِ غلط؛ نه جوابِ سوال، فقط عادتِ خوبِ حل کردن. */
const PI_TIPS = [
'صورتِ سوال رو یه بار دیگه آروم بخون؛ کلمه‌ی «نیست» یا «بیشترین» زیاد جا می‌مونه.',
'علامتِ منفی‌ها رو یه بار دیگه چک کن.',
'اول داخلِ پرانتز، بعد ضرب و تقسیم، آخرش جمع و تفریق.',
'صورت سوال رو یه بار دیگه آروم بخون؛ کلمه «نیست» یا «بیشترین» زیاد جا می‌مونه.',
'علامت منفی‌ها رو یه بار دیگه چک کن.',
'اول داخل پرانتز، بعد ضرب و تقسیم، آخرش جمع و تفریق.',
'جوابت رو بذار توی سوال؛ اگه درست درنیومد یه جا اشتباه شده.',
];