forked from Tadrisino/MathApp_frontend
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:
68
src/state/subject.ts
Normal file
68
src/state/subject.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import { api, ApiError, type HomeResponse } from '@/api';
|
||||
import { KEYS, store } from '@/lib/storage';
|
||||
|
||||
/**
|
||||
* کتابی که دانشآموز در هر پایه انتخاب کرده («هندسه»، «حسابان»، …). یک منبعِ حقیقت برای هر دو تبِ «آزمونها» و
|
||||
* «درسها»: هر دو از `home` در AppData میخوانند و `home` همیشه نقشهی همین کتاب است، پس جابهجایی در یکی در
|
||||
* دیگری هم دیده میشود.
|
||||
*
|
||||
* هر پایه جدا نگه داشته میشود (با عوضکردنِ پایه، انتخابِ پایهی قبلی نباید به دوازدهم بپرد) و در
|
||||
* localStorage میماند تا بعد از بستنِ اپ همان کتاب باز شود. منبعِ حقیقتِ فهرستِ کتابها سرور است
|
||||
* (`home.subjects`)؛ این فقط «کدام» را میگوید.
|
||||
*/
|
||||
type SubjectMap = Record<string, string>;
|
||||
|
||||
function read(): SubjectMap {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(store.get(KEYS.subjects) ?? '{}');
|
||||
return parsed && typeof parsed === 'object' ? (parsed as SubjectMap) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
let current: SubjectMap = read();
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
export const subjectStore = {
|
||||
get: (gradeId: number): string | null => current[String(gradeId)] ?? null,
|
||||
set(gradeId: number, key: string | null) {
|
||||
const id = String(gradeId);
|
||||
if ((current[id] ?? null) === key) return;
|
||||
const next = { ...current };
|
||||
if (key) next[id] = key;
|
||||
else delete next[id];
|
||||
current = next;
|
||||
store.set(KEYS.subjects, JSON.stringify(next));
|
||||
listeners.forEach((l) => l());
|
||||
},
|
||||
subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
snapshot: () => current,
|
||||
};
|
||||
|
||||
/** کتابِ انتخابشدهی این پایه؛ null یعنی هنوز انتخابی نشده (سرور اولین کتاب را میدهد). */
|
||||
export function useSelectedSubject(gradeId: number | null | undefined): string | null {
|
||||
const map = useSyncExternalStore(subjectStore.subscribe, subjectStore.snapshot, subjectStore.snapshot);
|
||||
return gradeId == null ? null : map[String(gradeId)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* نقشهی کتابِ انتخابشده. کلیدِ ذخیرهشدهای که سرور دیگر نمیشناسد (کتاب برداشته شده یا پایه عوض شده)
|
||||
* ۴۰۰ میگیرد؛ آن را پاک میکنیم و نقشهی پیشفرض را میگیریم تا کاربر صفحهی خالی نبیند.
|
||||
*/
|
||||
export async function fetchHome(gradeId: number | null | undefined): Promise<HomeResponse> {
|
||||
const wanted = gradeId == null ? null : subjectStore.get(gradeId);
|
||||
try {
|
||||
return await api.home(wanted ?? undefined);
|
||||
} catch (err) {
|
||||
if (wanted && gradeId != null && err instanceof ApiError && err.status === 400) {
|
||||
subjectStore.set(gradeId, null);
|
||||
return api.home();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user