From dc1fb15a55ed556e3461c8b1120c63df0b667d4c Mon Sep 17 00:00:00 2001 From: AliReza Date: Thu, 8 Oct 2026 16:00:49 +0330 Subject: [PATCH] Lessons tab: chapters closed by default, no quiz-button flash while lessons load, state kept on back, continue card and progress Co-Authored-By: Claude Sonnet 5.5 --- src/screens/LessonsScreen.tsx | 200 +++++++++++++++++++++++++--------- 1 file changed, 147 insertions(+), 53 deletions(-) diff --git a/src/screens/LessonsScreen.tsx b/src/screens/LessonsScreen.tsx index 5f766ca..2de71f7 100644 --- a/src/screens/LessonsScreen.tsx +++ b/src/screens/LessonsScreen.tsx @@ -1,35 +1,54 @@ import { useState } from 'react'; -import { BookOpen, Brain, CheckCircle2, ChevronDown, Circle, Lock, Play } from 'lucide-react'; +import { motion } from 'motion/react'; +import { Brain, Check, ChevronDown, Lock, Play, Sparkles } from 'lucide-react'; import { api, type HomeNode, type HomeUnit } from '@/api'; import { useAppData } from '@/state/AppData'; import { findContinue, unitColor, unitProgress } from '@/state/curriculum'; import { useNav } from '@/nav/Navigator'; import { useAsync } from '@/lib/useAsync'; import { EmptyState, ErrorState, LoadingState, ProgressBar, Screen, ScreenHeader, Sep } from '@/components/ui'; +import { iconFor } from '@/components/NodeIcon'; import { toFa } from '@/lib/format'; import { MathText } from '@/lib/MathText'; /** - * تبِ «درس‌ها»: فصل ← جزیره ← درس، برای کسی که می‌خواهد اول بخواند. - * آزمونِ هر جزیره همین‌جا کنارِ درس‌هایش است؛ نقشه‌ی بازی‌وار همان خانه است. - * فقط درس‌های سوال‌دار (lessonIds) فهرست می‌شوند، مثلِ برگه‌ی جزیره. + * تبِ «درس‌ها»: فصل ← جزیره ← درس. فصل‌ها اول بسته‌اند و بالای صفحه یک کارتِ «ادامه بده» هست. + * بازبودنِ فصل/جزیره در متغیرِ ماژول نگه داشته می‌شود، چون با رفتن به درس و برگشتن صفحه از نو ساخته + * می‌شود (کلیدِ مسیر در MainApp) و کاربر جایش را گم می‌کرد. با بستنِ اپ پاک می‌شود، عمداً. */ +const remembered: { unit: string | null; node: string | null } = { unit: null, node: null }; + export function LessonsScreen() { const { home, homeError, refreshHome, gradeTitle } = useAppData(); const nav = useNav(); const units = home?.units ?? []; const current = findContinue(home); - const [openUnit, setOpenUnit] = useState(current?.unit.id ?? null); - const [openNode, setOpenNode] = useState(current?.node.id ?? null); + const [openUnit, setOpenUnitState] = useState(remembered.unit); + const [openNode, setOpenNodeState] = useState(remembered.node); + const setOpenUnit = (id: string | null) => { remembered.unit = id; setOpenUnitState(id); }; + const setOpenNode = (id: string | null) => { remembered.node = id; setOpenNodeState(id); }; + + const totalNodes = units.reduce((n, u) => n + u.nodes.length, 0); + const doneNodes = units.reduce((n, u) => n + unitProgress(u).done, 0); return ( - + {gradeTitle && <>{gradeTitle}}اول بخون، بعد آزمونِ جزیره رو بده} color="#2FB36B" mascot={{ char: 'sigma', mood: 'happy' }} - /> + > + {home && totalNodes > 0 && ( +
+
+ جزیره‌هایی که تموم کردی + {toFa(doneNodes)} از {toFa(totalNodes)} +
+ +
+ )} +
{!home ? ( @@ -38,23 +57,43 @@ export function LessonsScreen() { ) : (
+ {current && current.node.nextLessonId != null && ( + + )} + {units.map((unit, i) => ( setOpenUnit(openUnit === unit.id ? null : unit.id)} openNode={openNode} onToggleNode={(id) => setOpenNode(openNode === id ? null : id)} /> ))} +
@@ -65,45 +104,74 @@ export function LessonsScreen() { } function UnitCard({ - unit, color, open, onToggle, openNode, onToggleNode, + index, unit, color, currentNodeId, open, onToggle, openNode, onToggleNode, }: { - unit: HomeUnit; color: string; open: boolean; onToggle: () => void; openNode: string | null; onToggleNode: (id: string) => void; + index: number; unit: HomeUnit; color: string; currentNodeId: string | null; open: boolean; onToggle: () => void; + openNode: string | null; onToggleNode: (id: string) => void; }) { const p = unitProgress(unit); + const complete = p.total > 0 && p.done === p.total; + const untouched = unit.nodes.every((n) => n.status === 'locked'); + const Icon = iconFor(unit.nodes[0]?.icon, index); return ( -
- {open && ( -
    - {unit.nodes.map((node) => ( - onToggleNode(node.id)} /> +
      + {unit.nodes.map((node, ni) => ( + onToggleNode(node.id)} + /> ))}
    )} -
+ ); } -function NodeRow({ node, color, open, onToggle }: { node: HomeNode; color: string; open: boolean; onToggle: () => void }) { +function NodeRow({ + num, node, color, isCurrent, open, onToggle, +}: { num: number; node: HomeNode; color: string; isCurrent: boolean; open: boolean; onToggle: () => void }) { const nav = useNav(); const locked = node.status === 'locked'; const done = node.status === 'completed'; const lessons = useAsync(() => (open && !locked ? api.nodeLessons(node.id) : Promise.resolve(null)), [node.id, open, locked]); const playable = new Set(node.lessonIds); const list = lessons.data?.lessons.filter((l) => playable.has(Number(l.id))) ?? []; + const loaded = !lessons.loading && !lessons.error && lessons.data !== null && lessons.data !== undefined; return ( -
  • +
  • + {open && !locked && (
    - {lessons.loading ? ( -
    دارم درس‌ها رو میارم…
    - ) : lessons.error ? ( + {lessons.error ? ( + ) : !loaded ? ( +
    + {[0, 1, 2].map((k) =>
    )} +
    ) : ( -
      - {list.map((l) => ( -
    • - -
    • - ))} -
    + <> +
      + {list.map((l, li) => ( +
    • + +
    • + ))} +
    + + )} -
    )}