Subject bar under the header in Quizzes and Lessons, synced between tabs; bidi fix for Latin math in RTL text

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 05:35:30 +03:30
parent 56dcc93202
commit f569ece55b
14 changed files with 217 additions and 21 deletions

View File

@@ -1,7 +1,8 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import { api, type Character, type Grade, type HomeResponse, type UserProfile, type UserSummary } from '@/api';
import { seyarFromSlug, type SeyarChar } from '@/mascots/seyar';
import { companionStore, useCompanion } from '@/state/companion';
import { fetchHome, subjectStore } from '@/state/subject';
/**
* داده‌هایی که چند صفحه با هم لازم دارند: پروفایل، آمار، نقشه، همراهِ انتخابی
@@ -18,7 +19,12 @@ interface AppData {
/** همراهی که کاربر در صفحه‌ی کاراکترها انتخاب کرده؛ پیش‌فرض سیگما. */
companion: SeyarChar;
unreadCount: number;
refreshHome: () => Promise<void>;
/** `gradeId` را فقط وقتی بدهید که پایه همین الان عوض شده و state هنوز رندر نشده. */
refreshHome: (gradeId?: number) => Promise<void>;
/** عوضِ کتابِ نقشه (هندسه، حسابان، …)؛ آزمون‌ها و درس‌ها هر دو همین را می‌خوانند. */
selectSubject: (key: string) => void;
/** کتابی که روی `home` است؛ بی‌نقشه (هنوز نرسیده) null. */
subjectKey: string | null;
refreshSummary: () => Promise<void>;
refreshUnread: () => Promise<void>;
setProfile: (profile: UserProfile) => void;
@@ -54,14 +60,35 @@ export function AppDataProvider({
const setCompanionChar = companionStore.set;
const [unreadCount, setUnreadCount] = useState(0);
const refreshHome = useCallback(async () => {
const gradeId = profile.gradeId;
// نقشه‌ی کتاب‌هایی که دیده شده؛ جابه‌جایی به کتابِ دیده‌شده فوری است و پشت‌صحنه تازه می‌شود.
const homeCache = useRef(new Map<string, HomeResponse>());
const refreshHome = useCallback(async (forGrade?: number) => {
const target = forGrade ?? gradeId;
try {
setHome(await api.home());
const fresh = await fetchHome(target);
if (fresh.subject) homeCache.current.set(`${target}:${fresh.subject.key}`, fresh);
setHome(fresh);
setHomeError(null);
} catch (err) {
setHomeError(err);
}
}, []);
}, [gradeId]);
const selectSubject = useCallback(
(key: string) => {
if (gradeId == null) return;
subjectStore.set(gradeId, key);
const cached = homeCache.current.get(`${gradeId}:${key}`);
// کتابِ دیده‌شده فوری می‌آید؛ دیده‌نشده یک لحظه «در حال آوردن» است تا نقشه‌ی کتابِ قبلی زیرِ نامِ
// کتابِ تازه نماند.
setHome(cached ?? null);
setHomeError(null);
void refreshHome();
},
[gradeId, refreshHome],
);
const refreshSummary = useCallback(async () => {
try {
@@ -106,13 +133,15 @@ export function AppDataProvider({
companion,
unreadCount,
refreshHome,
selectSubject,
subjectKey: home?.subject?.key ?? null,
refreshSummary,
refreshUnread,
setProfile,
setCompanion: (character) => setCompanionChar(seyarFromSlug(character.slug) ?? 'sigma'),
setUnreadCount,
}),
[profile, grades, summary, home, homeError, companion, unreadCount, refreshHome, refreshSummary, refreshUnread],
[profile, grades, summary, home, homeError, companion, unreadCount, refreshHome, selectSubject, refreshSummary, refreshUnread],
);
return <AppDataContext.Provider value={value}>{children}</AppDataContext.Provider>;