From 267cf5fca02164b0aa69ced782645a83b149eb6e Mon Sep 17 00:00:00 2001 From: zrabg Date: Thu, 24 Sep 2026 12:30:58 +0330 Subject: [PATCH] fixed --- .env.example | 11 ---- src/api/index.ts | 19 +++++- src/app/App.tsx | 150 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 164 insertions(+), 16 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 8867d77..0000000 --- a/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -# این فایل فقط نمونه است. اگر بک‌اندِ محلی دارید، آن را به .env کپی کنید: -# cp .env.example .env -# -# اگر هیچ کاری نکنید هم اپ کار می‌کند و به بک‌اندِ مشترک وصل می‌شود: -# https://api.math.tadrisino2.ir/api -# -# فقط وقتی بک‌اند را روی ماشینِ خودتان بالا آورده‌اید این را باز کنید: -# VITE_API_BASE_URL=http://127.0.0.1:8000/api - -# هیچ کلید یا رمزی اینجا نگذارید: هر چیزی که با VITE_ شروع شود داخلِ باندلِ -# نهایی می‌رود و کاربر می‌تواند ببیندش. diff --git a/src/api/index.ts b/src/api/index.ts index 15bbfca..f1b7da8 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -117,6 +117,16 @@ export interface Hearts { isUnlimited: boolean; } +export interface Character { + characterId: string; + slug: string; + name: string; + icon: string; + unlockXp: number; + unlocked: boolean; + selected: boolean; +} + export interface AskResponse { conversationId: string; messageId: string; @@ -299,9 +309,16 @@ export function createClient(options: ClientOptions = {}) { raw<{ currentStreak: number; longestStreak: number; activeDays: string[] }>( 'GET', '/user/streak/calendar', ), - leaderboard: (period: 'weekly' | 'monthly' = 'weekly') => + leaderboard: (period: 'weekly' | 'all_time' = 'weekly') => raw<{ period: string; entries: unknown[] }>('GET', `/leaderboard?period=${period}`), + /** کاتالوگِ کاراکترهای گیمیفیکیشن — مثلِ ماسکات‌های Duolingo، بازشدن بر اساسِ XP. */ + characters: { + list: () => raw<{ characters: Character[] }>('GET', '/characters').then((r) => r.characters), + select: (characterId: string) => + raw<{ characterId: string; selected: boolean }>('POST', '/characters/select', { characterId }), + }, + /* ── صفحه‌ی درس و سوال ─────────────────────────────────────────────── */ lesson: { intro: (lessonId: number) => raw('GET', `/lessons/${lessonId}/intro`), diff --git a/src/app/App.tsx b/src/app/App.tsx index cc21679..d89123e 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -12,11 +12,12 @@ import { createClient, ApiError } from "../api"; import type { Grade as ApiGrade, HomeResponse, HomeUnit, HomeNode, NodeStatus, LessonIntro, Question as ApiQuestion, AnswerResult, Plan as ApiPlan, + Character as ApiCharacter, } from "../api"; // ─── کلاینتِ API — تنها جایی که با بک‌اند حرف می‌زنیم ───────────────────────── const api = createClient({ - baseUrl: (import.meta as { env?: Record }).env?.VITE_API_URL ?? "/api", + baseUrl: (import.meta as { env?: Record }).env?.VITE_API_BASE_URL ?? "https://api.math.tadrisino2.ir/api", }); function getOrCreateDeviceId(): string { @@ -282,6 +283,16 @@ const SEYAR_INFO: Record
+ +
+

همراهت رو انتخاب کن

+

با کسبِ امتیاز، کاراکترهای بیشتری باز می‌شن

+
+
+ + +
+ {loading ? ( +
+ +

در حال آوردنِ کاراکترها...

+
+ ) : loadErr ? ( +
+ +

لیست الان نیومد، بعداً دوباره امتحان کن.

+
+ ) : !characters || characters.length === 0 ? ( +
+ +

فعلاً کاراکتری تعریف نشده

+
+ ) : ( +
+ {characters.map((c) => { + const seyar = seyarSlugMatch(c.slug); + const isBusy = selecting === c.characterId; + return ( + + ); + })} +
+ )} +
+ + ); +} + // ─── Subscription ───────────────────────────────────────────────────────────── const PLAN_STYLES = [ @@ -2796,12 +2911,34 @@ export default function App() { const refreshLeaderboard = (period: LeaderboardPeriod) => { setLbLoading(true); setLbErr(false); - api.leaderboard(period as any) + api.leaderboard(period) .then((r) => setLbEntries(r.entries as unknown as LeaderboardEntry[])) .catch(() => setLbErr(true)) .finally(() => setLbLoading(false)); }; + const [characters, setCharacters] = useState(null); + const [charLoading, setCharLoading] = useState(false); + const [charErr, setCharErr] = useState(false); + const [charSelecting, setCharSelecting] = useState(null); + + const refreshCharacters = () => { + setCharLoading(true); + setCharErr(false); + api.characters.list() + .then(setCharacters) + .catch(() => setCharErr(true)) + .finally(() => setCharLoading(false)); + }; + + const selectCharacter = (characterId: string) => { + setCharSelecting(characterId); + api.characters.select(characterId) + .then(() => setCharacters((list) => list?.map((c) => ({ ...c, selected: c.characterId === characterId })) ?? list)) + .catch(() => {}) + .finally(() => setCharSelecting(null)); + }; + useEffect(() => { if (flow === "main") { refreshHome(); refreshNotifications(); } }, [flow]); @@ -2810,6 +2947,10 @@ export default function App() { if (flow === "main" && screen === "leaderboard" && !lbEntries) refreshLeaderboard(lbPeriod); }, [flow, screen]); + useEffect(() => { + if (flow === "main" && screen === "characters" && !characters) refreshCharacters(); + }, [flow, screen]); + // ─── تاریخچه‌ی صفحات، برای دکمه‌ی «بازگشت» ──────────────────────────────── // navigate: رفتن به جلو (صفحه‌ی فعلی رو تو تاریخچه نگه می‌داره) // goBack: برگشت به صفحه‌ی قبلی (یا خانه، اگه چیزی تو تاریخچه نبود) @@ -2915,6 +3056,7 @@ export default function App() { {flow === "main" && screen === "home" && { setUnitId(id); navigate("skillmap"); }}/>} {flow === "main" && screen === "notifications" && } {flow === "main" && screen === "leaderboard" && { setLbPeriod(p); refreshLeaderboard(p); }} onBack={goBack}/>} + {flow === "main" && screen === "characters" && } {flow === "main" && screen === "topics" && { setUnitId(id); navigate("skillmap"); }}/>} {flow === "main" && screen === "lessons" && { setUnitId(id); navigate("skillmap"); }} onNavigate={navigate}/>} {flow === "main" && screen === "skillmap" && { setLessonId(id); navigate("lesson"); }} onBack={goBack}/>} @@ -2926,4 +3068,4 @@ export default function App() { ); -} \ No newline at end of file +} -- 2.49.1