diff --git a/src/app/App.tsx b/src/app/App.tsx index 10e1830..1a59f21 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,13 +1,78 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { motion } from "motion/react"; import { AnimatePresence } from "motion/react"; import { Home, Map, BookOpen, Brain, Crown, Bell, Play, Lock, Sparkles, MessageCircle, Check, X, Mic, Send, - ChevronLeft, Zap, ChevronDown, AlertCircle, - Flame, Star, Trophy, + ChevronLeft, ChevronRight, Zap, ChevronDown, AlertCircle, + Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat, + Flame, Star, Trophy, Heart, } from "lucide-react"; +// ─── Auth Helpers ─────────────────────────────────────────────────────────── + +let onAuthExpired: (() => void) | null = null; +function setAuthExpiredHandler(fn: (() => void) | null) { onAuthExpired = fn; } + +async function refreshAuthToken(): Promise { + const refreshToken = localStorage.getItem("refreshToken"); + if (!refreshToken) return null; // مهمان‌ها refreshToken ندارن + try { + const res = await fetch("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refreshToken }), + }); + if (!res.ok) throw new Error(); + const data: { token: string; expiresIn: number } = await res.json(); + localStorage.setItem("token", data.token); + localStorage.setItem("tokenExpiresAt", String(Date.now() + data.expiresIn * 1000)); + return data.token; + } catch { + return null; + } +}async function authFetch(path: string, options: RequestInit = {}): Promise { + const token = localStorage.getItem("token"); + const res = await fetch(path, { + ...options, + headers: { + ...options.headers, + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}) // ✅ اصلاح شد + }, + }); + + if (res.status !== 401) return res; + + const newToken = await refreshAuthToken(); + if (!newToken) { + onAuthExpired?.(); + return res; + } + + return fetch(path, { + ...options, + headers: { + ...options.headers, + "Content-Type": "application/json", + Authorization: `Bearer ${newToken}` + }, + }); +} + +function getOrCreateDeviceId(): string { + let id = localStorage.getItem("deviceId"); + if (!id) { id = crypto.randomUUID(); localStorage.setItem("deviceId", id); } + return id; +} + +function storeAuth(data: AuthData) { + localStorage.setItem("token", data.token); + if (data.refreshToken) localStorage.setItem("refreshToken", data.refreshToken); + localStorage.setItem("userId", data.userId); + localStorage.setItem("isGuest", String(!!data.isGuest)); +} + // ─── Robot Mascot ───────────────────────────────────────────────────────────── type RobotMood = "wave" | "point" | "explain" | "think" | "neutral" | "happy" | "encourage"; @@ -136,19 +201,243 @@ function RobotMascot({ mood, size = 120 }: { mood: RobotMood; size?: number }) { ); } +// ─── سه‌یار: مسکات‌های سیگما / دلتا / پی ───────────────────────────────────── +// موتور تولید SVG سه کاراکتر، هر کدوم با ۵ حالت احساسی + یک حالت عادی. +type SeyarChar = "sigma" | "delta" | "pi"; +type SeyarMood = "idle" | "happy" | "excited" | "sad" | "angry" | "depressed"; + +const SEYAR_PAL: Record> = { + sigma: { body: "#4C5FD5", inner: "#7A8CFF", belly: "#EAF0FF", ink: "#232355", cheek: "#FFB2C0", mouth: "#FF6B6B", tear: "#8EC9FF", badge: "#FFD23F" }, + delta: { body: "#FF7A3D", inner: "#FFB25E", belly: "#FFE7C7", ink: "#6E2A12", cheek: "#FF8FA3", mouth: "#D6402A", tear: "#8EC9FF", spike: "#FFEB3B" }, + pi: { body: "#9B7BFF", inner: "#C6B3FF", belly: "#F1ECFF", ink: "#3B2B66", cheek: "#FFC2D1", mouth: "#FF6F91", tear: "#5EC8E8" }, +}; +const SEYAR_YSHIFT: Record = { idle: 0, happy: 0, excited: -4, sad: 3, angry: 0, depressed: 9 }; +const SEYAR_ARMDELTA: Record = { idle: 0, happy: 2, excited: 46, sad: -4, angry: -8, depressed: -10 }; + +function seyarFace(mood: SeyarMood, ex: number, ey: number, mx: number, my: number, ink: string, mouthC: string, tearC: string, cheekC: string, spread: number) { + const lx = ex, rx = ex + spread; + let eyes = "", mouth = "", cheeks = "", extra = ""; + if (mood === "idle") { + eyes = `` + + ``; + mouth = ``; + } else if (mood === "happy") { + eyes = `` + + ``; + mouth = ``; + cheeks = ``; + } else if (mood === "excited") { + eyes = `` + + `` + + ``; + mouth = ``; + cheeks = ``; + extra = ``; + } else if (mood === "sad") { + eyes = `` + + `` + + ``; + mouth = ``; + extra = ``; + } else if (mood === "angry") { + eyes = `` + + `` + + ``; + mouth = ``; + cheeks = ``; + extra = ``; + } else if (mood === "depressed") { + eyes = `` + + ``; + mouth = ``; + extra = `` + + `` + + `` + + `` + + `` + + `` + + ``; + } + return { eyes, mouth, cheeks, extra }; +} + +function seyarSigmaSVG(mood: SeyarMood): string { + const C = SEYAR_PAL.sigma; + const f = seyarFace(mood, 80, 86, 100, 114, C.ink, C.mouth, C.tear, C.cheek, 40); + const a = 12 + SEYAR_ARMDELTA[mood], dy = SEYAR_YSHIFT[mood]; + return ` + + + + + + + + + + + ${f.extra}${f.eyes}${f.cheeks}${f.mouth} + + Σ + `; +} + +function seyarDeltaSVG(mood: SeyarMood): string { + const C = SEYAR_PAL.delta; + const f = seyarFace(mood, 84, 108, 100, 128, C.ink, C.mouth, C.tear, C.cheek, 32); + const a = 16 + SEYAR_ARMDELTA[mood], dy = SEYAR_YSHIFT[mood]; + return ` + + + + + + + + + ${f.extra}${f.eyes}${f.cheeks}${f.mouth} + `; +} + +function seyarPiSVG(mood: SeyarMood): string { + const C = SEYAR_PAL.pi; + const f = seyarFace(mood, 80, 112, 100, 138, C.ink, C.mouth, C.tear, C.cheek, 40); + const a = 18 + SEYAR_ARMDELTA[mood], dy = SEYAR_YSHIFT[mood]; + return ` + + + + + + + + + + ${f.extra}${f.eyes}${f.cheeks}${f.mouth} + `; +} + +const SEYAR_SVG: Record string> = { + sigma: seyarSigmaSVG, delta: seyarDeltaSVG, pi: seyarPiSVG, +}; +const SEYAR_INFO: Record = { + sigma: { name: "سیگما", symbol: "Σ", role: "راهنمای آزمون و کوییز" }, + delta: { name: "دلتا", symbol: "Δ", role: "هم‌تیمیِ تمرین‌های سریع" }, + pi: { name: "پی", symbol: "π", role: "همراهِ دلگرمی" }, +}; + +function SeyarMascot({ char, mood = "idle", size = 100, bob = false }: { char: SeyarChar; mood?: SeyarMood; size?: number; bob?: boolean }) { + return ( + + ); +} + // ─── Types ──────────────────────────────────────────────────────────────────── -type AppFlow = "splash" | "onboarding" | "profile" | "main"; -type MainScreen = "home" | "skillmap" | "lesson" | "question" | "askai" | "subscription"; -interface Profile { name: string; grade: string } +type AppFlow = "splash" | "onboarding" | "login" | "profile" | "main"; +type MainScreen = "home" | "topics" | "skillmap" | "lessons" | "lesson" | "question" | "askai" | "subscription"; +interface Profile { + name: string; + age: number; + grade: string; +} +// ─── Math Topics (مباحث) ──────────────────────────────────────────────────── +interface Topic { id: string; name: string; icon: React.ElementType; color: string } +interface LessonNode { id: number; type: "video" | "quiz"; title: string; meta: string; done: boolean } + +const TOPICS: Topic[] = [ + { id: "linear-algebra", name: "جبر خطی", icon: Grid3x3, color: "#ED6956" }, + { id: "limits", name: "حد", icon: TrendingUp, color: "#91ADC6" }, + { id: "derivatives", name: "مشتق", icon: LineChart, color: "#97C0C0" }, + { id: "integrals", name: "انتگرال", icon: Sigma, color: "#9B7BFF" }, + { id: "geometry", name: "هندسه تحلیلی", icon: Compass, color: "#F3A9B0" }, + { id: "sequences", name: "دنباله و سری", icon: Repeat, color: "#FF7A3D" }, +]; + +const LESSONS_BY_TOPIC: Record = { + "linear-algebra": [ + { id: 1, type: "video", title: "بردار و فضای برداری", meta: "۱۲ دقیقه", done: true }, + { id: 2, type: "quiz", title: "آزمون بردارها", meta: "۵ سوال", done: true }, + { id: 3, type: "video", title: "ماتریس و عملیات آن", meta: "۱۴ دقیقه", done: true }, + { id: 4, type: "quiz", title: "آزمون ماتریس", meta: "۶ سوال", done: false }, + { id: 5, type: "video", title: "دترمینان", meta: "۱۰ دقیقه", done: false }, + { id: 6, type: "quiz", title: "آزمون دترمینان", meta: "۵ سوال", done: false }, + { id: 7, type: "video", title: "دستگاه معادلات خطی", meta: "۱۵ دقیقه", done: false }, + { id: 8, type: "quiz", title: "آزمون پایانی", meta: "۸ سوال", done: false }, + ], + "limits": [ + { id: 1, type: "video", title: "مفهوم حد", meta: "۱۰ دقیقه", done: true }, + { id: 2, type: "quiz", title: "آزمون مفهوم حد", meta: "۵ سوال", done: true }, + { id: 3, type: "video", title: "حد در بی‌نهایت", meta: "۹ دقیقه", done: false }, + { id: 4, type: "quiz", title: "آزمون حد در بی‌نهایت", meta: "۵ سوال", done: false }, + { id: 5, type: "video", title: "پیوستگی توابع", meta: "۱۱ دقیقه", done: false }, + { id: 6, type: "quiz", title: "آزمون پایانی", meta: "۷ سوال", done: false }, + ], + "derivatives": [ + { id: 1, type: "video", title: "مفهوم مشتق", meta: "۱۳ دقیقه", done: false }, + { id: 2, type: "quiz", title: "آزمون مفهوم مشتق", meta: "۵ سوال", done: false }, + { id: 3, type: "video", title: "قواعد مشتق‌گیری", meta: "۱۲ دقیقه", done: false }, + { id: 4, type: "quiz", title: "آزمون قواعد مشتق", meta: "۶ سوال", done: false }, + { id: 5, type: "video", title: "کاربرد مشتق", meta: "۱۴ دقیقه", done: false }, + { id: 6, type: "quiz", title: "آزمون پایانی", meta: "۸ سوال", done: false }, + ], + "integrals": [ + { id: 1, type: "video", title: "انتگرال نامعین", meta: "۱۲ دقیقه", done: false }, + { id: 2, type: "quiz", title: "آزمون انتگرال نامعین", meta: "۵ سوال", done: false }, + { id: 3, type: "video", title: "انتگرال معین", meta: "۱۳ دقیقه", done: false }, + { id: 4, type: "quiz", title: "آزمون انتگرال معین", meta: "۶ سوال", done: false }, + { id: 5, type: "video", title: "کاربرد انتگرال", meta: "۱۵ دقیقه", done: false }, + { id: 6, type: "quiz", title: "آزمون پایانی", meta: "۸ سوال", done: false }, + ], + "geometry": [ + { id: 1, type: "video", title: "معادله خط و دایره", meta: "۱۱ دقیقه", done: false }, + { id: 2, type: "quiz", title: "آزمون خط و دایره", meta: "۵ سوال", done: false }, + { id: 3, type: "video", title: "مقاطع مخروطی", meta: "۱۴ دقیقه", done: false }, + { id: 4, type: "quiz", title: "آزمون پایانی", meta: "۷ سوال", done: false }, + ], + "sequences": [ + { id: 1, type: "video", title: "دنباله‌های عددی", meta: "۱۰ دقیقه", done: false },{ id: 2, type: "quiz", title: "آزمون دنباله‌ها", meta: "۵ سوال", done: false }, + { id: 3, type: "video", title: "سری‌های همگرا", meta: "۱۳ دقیقه", done: false }, + { id: 4, type: "quiz", title: "آزمون پایانی", meta: "۷ سوال", done: false }, + ], +} + +// ─── دسته‌بندیِ درس‌ها (مناسبِ دانش‌آموزِ دبیرستانی) ────────────────────────── +// مباحث بالا رو زیرِ سه گروهِ آشنا برای دانش‌آموزِ ریاضی/تجربیِ دبیرستان جمع می‌کنیم +interface LessonCategory { id: string; title: string; blurb: string; topicIds: string[] } + +const LESSON_CATEGORIES: LessonCategory[] = [ + { id: "algebra", title: "جبر و معادلات", blurb: "پایه‌ی همه‌چیز", topicIds: ["linear-algebra"] }, + { id: "calculus", title: "حسابان", blurb: "حد، مشتق، انتگرال", topicIds: ["limits", "derivatives", "integrals"] }, + { id: "geo-seq", title: "هندسه و دنباله", blurb: "دید فضایی و الگو", topicIds: ["geometry", "sequences"] }, +]; + +// اولین درسِ ناتمومِ دانش‌آموز رو پیدا می‌کنه (برای کارتِ «ادامه بده» تو خانه و درس‌ها) +function findContinueLesson(): { topic: Topic; lesson: LessonNode } { + for (const topic of TOPICS) { + const items = LESSONS_BY_TOPIC[topic.id] ?? []; + const next = items.find((item) => !item.done); + if (next) return { topic, lesson: next }; + } + return { topic: TOPICS[0], lesson: LESSONS_BY_TOPIC[TOPICS[0].id][0] }; +} // ─── Bottom Nav ─────────────────────────────────────────────────────────────── function BottomNav({ active, onChange }: { active: MainScreen; onChange: (s: MainScreen) => void }) { const items: { id: MainScreen; icon: React.ElementType; label: string }[] = [ { id: "home", icon: Home, label: "خانه" }, - { id: "skillmap", icon: Map, label: "مهارت‌ها" }, - { id: "lesson", icon: BookOpen, label: "درس‌ها" }, + { id: "topics", icon: Map, label: "مهارت‌ها" }, + { id: "lessons", icon: BookOpen, label: "درس‌ها" }, { id: "askai", icon: Brain, label: "دستیار" }, { id: "subscription", icon: Crown, label: "اشتراک" }, ]; @@ -184,9 +473,9 @@ function SplashScreen({ onNew, onReturning }: { onNew: () => void; onReturning: transition={{ type: "spring", stiffness: 220, damping: 18 }} className="flex flex-col items-center gap-4">
- +
-

رُبوماتیک

+

تدریسینو

یادگیری ریاضی با هوش مصنوعی

@@ -211,27 +500,33 @@ function OnboardingScreen({ onDone }: { onDone: () => void }) { const [step, setStep] = useState(0); const slides = [ { - mood: "wave" as RobotMood, bg: "#ED6956", - title: "به رُبوماتیک خوش آمدی! 🤖", + mood: "happy" as SeyarMood, bg: "#ED6956", + title: "به تدریسینو خوش آمدی! 👋", sub: "دستیار ریاضی هوشمند", - desc: "با رُبو ریاضی یاد بگیر و هر روز باهوش‌تر بشو. یادگیری می‌تونه هم آسون هم جالب باشه!", + desc: "با سیگما ریاضی یاد بگیر و هر روز باهوش‌تر بشو. یادگیری می‌تونه هم آسون هم جالب باشه!", }, { - mood: "point" as RobotMood, bg: "#91ADC6", + mood: "idle" as SeyarMood, bg: "#91ADC6", title: "مسیر ریاضیت رو بساز", sub: "گام به گام پیشرفت کن", - desc: "از جمع و تفریق شروع کن تا به ضرب، تقسیم و کسرها برسی. رُبو همه‌جا کنارته!", + desc: "از جمع و تفریق شروع کن تا به ضرب، تقسیم و کسرها برسی. سیگما همه‌جا کنارته!", }, { - mood: "explain" as RobotMood, bg: "#97C0C0", + mood: "happy" as SeyarMood, bg: "#97C0C0", title: "هر چیزی نفهمیدی بپرس!", sub: "توضیح ساده و صبورانه", - desc: "رُبو هر مسئله‌ای رو با مثال‌های جالب برات توضیح می‌ده تا مطمئن بشی خوب یاد گرفتی.", + desc: "سیگما هر مسئله‌ای رو با مثال‌های جالب برات توضیح می‌ده تا مطمئن بشی خوب یاد گرفتی.", }, ]; const slide = slides[step]; return ( -
+
+ {step > 0 && ( + + )}
void }) { transition={{ duration: 0.38 }} className="flex flex-col items-center gap-6">
- -
+ +

{slide.sub}

{slide.title}

@@ -278,127 +573,647 @@ function OnboardingScreen({ onDone }: { onDone: () => void }) {
); } +//-----login screen +interface AuthData { + userId: string; + token: string; + refreshToken?: string; // مهمان‌ها refreshToken ندارن + isGuest?: boolean; + isNewUser?: boolean; +} +function LoginScreen({ onDone }: { onDone: (auth: AuthData) => void }) { + const [step, setStep] = useState<"phone" | "otp">("phone"); + const [phone, setPhone] = useState(""); + const [otp, setOtp] = useState(""); + const [devOtp, setDevOtp] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + // API تا وصل بشه، بعداً حذف + const devBypass = () => { + const fakeAuth: AuthData = { userId: "dev", token: "dev-token", refreshToken: "dev-refresh" }; + storeAuth(fakeAuth); + onDone(fakeAuth); + }; -// ─── Profile Setup ──────────────────────────────────────────────────────────── + const phoneValid = /^09\d{9}$/.test(phone); + const otpValid = otp.trim().length >= 4; -const GRADES = [ - { id: "1", label: "پایه اول", emoji: "1️⃣" }, - { id: "2", label: "پایه دوم", emoji: "2️⃣" }, - { id: "3", label: "پایه سوم", emoji: "3️⃣" }, - { id: "4", label: "پایه چهارم", emoji: "4️⃣" }, - { id: "5", label: "پایه پنجم", emoji: "5️⃣" }, - { id: "6", label: "پایه ششم", emoji: "6️⃣" }, -]; + const requestOtp = async () => { + if (!phoneValid || loading) return; + setError(""); + setLoading(true); + try { + const res = await fetch("/api/auth/otp/request", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone }), + }); + if (!res.ok) throw new Error(); + const data: { success: boolean; devOtp: string; note: string } = await res.json(); + setDevOtp(data.devOtp ?? null); + setStep("otp"); + } catch { + setError("ارسال کد با خطا مواجه شد، دوباره امتحان کن"); + } finally { + setLoading(false); + } + }; -function ProfileSetupScreen({ onDone }: { onDone: (p: Profile) => void }) { - const [name, setName] = useState(""); - const [grade, setGrade] = useState(""); - const [touched, setTouched] = useState(false); + const verifyOtp = async () => { + if (!otpValid || loading) return; + setError(""); + setLoading(true); + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone, otp: otp.trim() }), + }); + if (!res.ok) throw new Error(); + const data: AuthData = await res.json(); + storeAuth(data); + onDone(data); + } catch { + setError("کد وارد شده اشتباهه، دوباره امتحان کن"); + } finally { + setLoading(false); + } + }; - const nameErr = touched && !name.trim(); - const gradeErr = touched && !grade; - - const handleSubmit = () => { - setTouched(true); - if (!name.trim() || !grade) return; - onDone({ name: name.trim(), grade }); + const continueAsGuest = async () => { + setError(""); + setLoading(true); + try { + const res = await fetch("/api/auth/session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "guest", deviceId: getOrCreateDeviceId() }), + }); + if (!res.ok) throw new Error(); + const data: AuthData = await res.json(); + storeAuth(data); + onDone(data); + } catch { + setError("اتصال مهمان با خطا مواجه شد، دوباره امتحان کن"); + } finally { + setLoading(false); + } }; return (
- {/* Header */}
-

بیا باهم آشنا بشیم!

-

اطلاعاتت رو وارد کن

+

ورود به تدریسینو

+

+ {step === "phone" ? "شماره موبایلت رو وارد کن" : "کد پیامک‌شده رو وارد کن"} +

- +
- {/* Form */}
- {/* Name field */} -
- - setName(e.target.value)} - placeholder="مثلاً: سارا یا علی" - dir="rtl" - className={`w-full px-4 py-4 rounded-2xl text-sm bg-white border-2 outline-none text-[#1a1a2e] placeholder:text-[#b0b0c8] transition-all font-medium ${ - nameErr ? "border-red-400 bg-red-50" : name ? "border-[#ED6956]" : "border-[#e8e4ff]" - }`} - /> - {nameErr && ( -
- اسمت رو وارد کن -
- )} -
- - {/* Grade selection */} -
- -
- {GRADES.map(g => ( - - ))} + {step === "phone" ? ( +
+ + setPhone(e.target.value.replace(/\D/g, "").slice(0, 11))} + placeholder="09xxxxxxxxx" + dir="ltr" + inputMode="numeric" + className={`w-full px-4 py-4 rounded-2xl text-sm bg-white border-2 outline-none text-[#1a1a2e] placeholder:text-[#b0b0c8] transition-all font-medium text-left ${ + error ? "border-red-400 bg-red-50" : phone ? "border-[#ED6956]" : "border-[#e8e4ff]" + }`} + /> + {error && ( +
+ {error} +
+ )}
- {gradeErr && ( -
- پایه تحصیلیت رو انتخاب کن -
- )} -
- - {/* Fun message */} -
-
- + ) : ( +
+ setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))} + placeholder="----" + dir="ltr" + inputMode="numeric" + className={`w-full px-4 py-4 rounded-2xl text-lg tracking-[0.5em] text-center bg-white border-2 outline-none text-[#1a1a2e] placeholder:text-[#b0b0c8] transition-all font-bold ${ + error ? "border-red-400 bg-red-50" : otp ? "border-[#ED6956]" : "border-[#e8e4ff]" + }`} + /> + {error && ( +
+ {error} +
+ )} + {devOtp && ( +

+ کد تست محیط توسعه: {devOtp} (یا ۰۰۰۰) +

+ )} +
-

- رُبو از اطلاعات تو برای شخصی‌سازی درس‌ها استفاده می‌کنه تا یادگیری برات راحت‌تر باشه! -

-
+ )} - + + +
); } +// ─── Profile Setup ──────────────────────────────────────────────────────────── +const GRADES = [ + { id: "7", label: "پایه هفتم", emoji: "7" }, + { id: "8", label: "پایه هشتم", emoji: "8" }, + { id: "9", label: "پایه نهم", emoji: "9" }, + { id: "10", label: "پایه دهم", emoji: "۱۰" }, + { id: "11", label: "پایه یازدهم", emoji: "۱۱" }, + { id: "12", label: "پایه دوازدهم", emoji: "۱۲" }, +]; + +const AGES = Array.from({ length: 7 }, (_, i) => i + 12); + +const toPersianNumber = (value: number | string) => + String(value).replace( + /\d/g, + (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)] + ); + +function ProfileSetupScreen({ + onDone, +}: { + onDone: (p: Profile) => void; +}) { + + const [name, setName] = useState(""); + const [age, setAge] = useState(15); + const [grade, setGrade] = useState(""); + const [touched, setTouched] = useState(false); + + const nameErr = touched && !name.trim(); + const gradeErr = touched && !grade; + + + // ───────────────────────────────────────────────────────────────────── + // Scroll sound + // ───────────────────────────────────────────────────────────────────── + + const playTick = () => { + + try { + + const AudioCtx = + window.AudioContext || + (window as any).webkitAudioContext; + + if (!AudioCtx) return; + + const ctx = new AudioCtx(); + + if (ctx.state === "suspended") { + ctx.resume(); + } + + const oscillator = ctx.createOscillator(); + const gain = ctx.createGain(); + + oscillator.type = "sine"; + oscillator.frequency.setValueAtTime(720, ctx.currentTime); + + gain.gain.setValueAtTime(0.0001, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime( + 0.045, + ctx.currentTime + 0.005 + ); + gain.gain.exponentialRampToValueAtTime( + 0.0001, + ctx.currentTime + 0.045 + ); + + oscillator.connect(gain); + gain.connect(ctx.destination); + + oscillator.start(); + oscillator.stop(ctx.currentTime + 0.05); + + setTimeout(() => { + ctx.close(); + }, 100); + + } catch { + // Audio is optional; don't break the form if unavailable. + } + }; + + + // ───────────────────────────────────────────────────────────────────── + // Age wheel + // ───────────────────────────────────────────────────────────────────── +const changeAge = (direction: number) => { + setAge((current) => { + const next = Math.max( + AGES[0], + Math.min(AGES[AGES.length - 1], current + direction) + ); + + if (next !== current) { + playTick(); + } + + return next; + }); +}; + const handleSubmit = () => { + + setTouched(true); + + if (!name.trim() || !grade) return; + + onDone({ + name: name.trim(), + age, + grade, + }); + }; + + + return ( +
+ + {/* ───────────────── HEADER ───────────────── */} + +
+ + {/* decorative circles */} + +
+ +
+ +
+ +
+ +
+ فقط چندتا سؤال کوچیک +
+ +

+ بیا باهم آشنا بشیم! +

+ + +
+ +
+ +
+ +
+ +
+ + + {/* ───────────────── FORM ───────────────── */} + +
+ +
+ + + {/* ───────── NAME ───────── */} + +
+ + + + setName(e.target.value)} + placeholder="مثلاً سارا یا علی" + dir="rtl" + className={` + w-full px-4 py-4 rounded-2xl text-sm + bg-white border-2 outline-none + text-[#1a1a2e] + placeholder:text-[#b0b0c8] + transition-all font-medium + ${ + nameErr + ? "border-red-400 bg-red-50" + : name + ? "border-[#ED6956]" + : "border-[#e8e4ff]" + } + `} + /> + + {nameErr && ( +
+ + اسمت رو وارد کن +
+ )} + +
+ {/* ───────── AGE ───────── */} +
+ + +
+ + {/* selection highlight - ثابت */} +
+ + {/* fade بالا */} +
+ + {/* fade پایین */} +
+ + {/* اسکرول واقعی */} +
{ + const el = e.currentTarget; + + const itemHeight = 36; + const center = el.clientHeight / 2; + + const index = Math.round( + (el.scrollTop + center - 67) / itemHeight + ); + + const newAge = AGES[Math.max(0, Math.min(AGES.length - 1, index))]; + + if (newAge !== age) { + setAge(newAge); + playTick(); + } + }} + > +
+ + {AGES.map((value) => { + const distance = Math.abs(value - age); + + return ( + + ); + })} + + {/* فضای خالی پایین */} +
+
+
+
+ {/* ───────── GRADE ───────── */} + +
+ + + +
+ + {GRADES.map((g) => { + + const selected = grade === g.id; + + return ( + + + + ); + + })} + +
+ + + {gradeErr && ( +
+ + پایه تحصیلیت رو انتخاب کن +
+ )} + +
+ + + {/* ───────── ROBOT MESSAGE ───────── */} + +
+ +
+ +
+ +

+ رُبو بر اساس سن و پایه‌ات مسیر یادگیری مناسب خودت رو می‌سازه! +

+ +
+ + + {/* ───────── START ───────── */} + + + +
+ +
+ +
+ ); +} // ─── Home Screen ────────────────────────────────────────────────────────────── -function HomeScreen({ profile, onNavigate }: { profile: Profile; onNavigate: (s: MainScreen) => void }) { +function HomeScreen({ + profile, + onNavigate, + onContinue, +}: { + profile: Profile; + onNavigate: (s: MainScreen) => void; + onContinue: (topicId: string) => void; +}) { const grade = GRADES.find(g => g.id === profile.grade); + const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); + const { topic: cTopic, lesson: cLesson } = findContinueLesson(); + const cItems = LESSONS_BY_TOPIC[cTopic.id] ?? []; + const cDone = cItems.filter((i) => i.done).length; + const cIdx = Math.max(0, cItems.findIndex((i) => i.id === cLesson.id)); + const cPct = cItems.length ? Math.round((cDone / cItems.length) * 100) : 0; return (
- {/* ── Purple header — self-contained, no negative-margin overlap ─────── */} -
+ {/* ── Header ─────────────────────────────────────────────────────────── */} +
{/* decorative blobs */}
{/* Greeting row */} -
+

سلام، {profile.name}!

@@ -414,25 +1229,28 @@ function HomeScreen({ profile, onNavigate }: { profile: Profile; onNavigate: (s:

+
- {/* Stats row */} - {/* Stats row */} -
+ {/* ── Bold stat bar — overlaps the header for more visual weight ─────── */} +
+
{[ - { icon: Flame, label: "روز پیاپی", val: "۵" }, - { icon: Star, label: "امتیاز", val: "۸۴۰" }, - { icon: Trophy, label: "سطح", val: "۷" }, - ].map(({ icon: Icon, label, val }) => ( -
- -
{val}
-
{label}
+ { icon: Flame, val: "۵", label: "روز پیاپی", fg: "#FF7A3D", bg: "#FFF1E6" }, + { icon: Star, val: "۸۴۰", label: "امتیاز", fg: "#E8A400", bg: "#FFF8E1" }, + { icon: Trophy, val: "۷", label: "سطح", fg: "#9B7BFF", bg: "#F5F1FF" }, + ].map(({ icon: Icon, val, label, fg, bg }) => ( +
+
+ +
+
{val}
+
{label}
))}
- {/* ── Scrollable content — starts flush below header ─────────────────── */} + {/* ── Scrollable content ──────────────────────────────────────────────── */}
{/* Continue card */} @@ -440,65 +1258,40 @@ function HomeScreen({ profile, onNavigate }: { profile: Profile; onNavigate: (s:

ادامه از جایی که موندی

-

ضرب دو رقمی

-

درس ۳ از ۸ · ۴۰٪ تکمیل

+

{cLesson.title}

+

{cTopic.name} · درس {toFa(cIdx + 1)} از {toFa(cItems.length)} · {toFa(cPct)}٪ تکمیل

- +
{/* Progress bar */}
-
+
-
- {/* Daily exercises */} -
-
-

تمرین‌های امروز

- -
-
- {[ - { emoji: "➕", title: "جمع", sub: "۵ مسئله", done: true }, - { emoji: "✖️", title: "ضرب", sub: "۵ مسئله", done: false }, - { emoji: "➗", title: "تقسیم", sub: "۵ مسئله", done: false }, - { emoji: "½", title: "کسر", sub: "۳ مسئله", done: false }, - ].map(({ emoji, title, sub, done }) => ( - - ))} -
-
- {/* Quick access grid */}
-
@@ -507,146 +1300,940 @@ function HomeScreen({ profile, onNavigate }: { profile: Profile; onNavigate: (s: ); } -// ─── Skill Map ──────────────────────────────────────────────────────────────── +// // ─── Skill Map ──────────────────────────────────────────────────────────────── -function SkillMapScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) { - // هر آیتم یا ویدیوی درس است یا آزمون آن؛ ترتیب آرایه همان ترتیب واقعی خواندن دانش‌آموز است - const items: { id: number; type: "video" | "quiz"; title: string; meta: string; done: boolean }[] = [ - { id: 1, type: "video", title: "آشنایی با اعداد", meta: "۸ دقیقه", done: true }, - { id: 2, type: "quiz", title: "آزمون اعداد", meta: "۵ سوال", done: true }, - { id: 3, type: "video", title: "جمع اعداد", meta: "۱۰ دقیقه", done: true }, - { id: 4, type: "quiz", title: "آزمون جمع", meta: "۵ سوال", done: false }, - { id: 5, type: "video", title: "تفریق اعداد", meta: "۹ دقیقه", done: false }, - { id: 6, type: "quiz", title: "آزمون تفریق", meta: "۵ سوال", done: false }, - { id: 7, type: "video", title: "ضرب مقدماتی", meta: "۱۱ دقیقه", done: false }, - { id: 8, type: "quiz", title: "آزمون پایانی", meta: "۱۰ سوال", done: false }, - ]; +// function SkillMapScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) { +// // هر آیتم یا ویدیوی درس است یا آزمون آن؛ ترتیب آرایه همان ترتیب واقعی خواندن دانش‌آموز است +// const items: { id: number; type: "video" | "quiz"; title: string; meta: string; done: boolean }[] = [ +// { id: 1, type: "video", title: "آشنایی با اعداد", meta: "۸ دقیقه", done: true }, +// { id: 2, type: "quiz", title: "آزمون اعداد", meta: "۵ سوال", done: true }, +// { id: 3, type: "video", title: "جمع اعداد", meta: "۱۰ دقیقه", done: true }, +// { id: 4, type: "quiz", title: "آزمون جمع", meta: "۵ سوال", done: false }, +// { id: 5, type: "video", title: "تفریق اعداد", meta: "۹ دقیقه", done: false }, +// { id: 6, type: "quiz", title: "آزمون تفریق", meta: "۵ سوال", done: false }, +// { id: 7, type: "video", title: "ضرب مقدماتی", meta: "۱۱ دقیقه", done: false }, +// { id: 8, type: "quiz", title: "آزمون پایانی", meta: "۱۰ سوال", done: false }, +// ]; - const doneCount = items.filter((it) => it.done).length; - const percent = Math.round((doneCount / items.length) * 100); - const percentFa = String(percent).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); +// const doneCount = items.filter((it) => it.done).length; +// const percent = Math.round((doneCount / items.length) * 100); +// const percentFa = String(percent).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); + +// return ( +//
+//
+//
+//
+//

مسیر ریاضی

+//

{percentFa}٪ مسیر طی شده

+//
+// +//
+//
+//
+//
+//
+//
+//
+// {/* خط تایم‌لاین که از وسط آیکن‌ها رد می‌شود */} +//
+//
+// {items.map((item, i) => { +// const locked = i > 0 && !items[i - 1].done; +// const isCurrent = !locked && !item.done; +// const Icon = item.type === "video" ? Play : Brain; +// return ( +// +// ); +// })} +//
+//
+//
+//
+// ); +// } + +// ─── Topic Select ─────────────────────────────────────────────────────────── + +// چیدمانِ فصل‌ها روی نقشه — به‌شکلِ مارپیچ، مثلِ نقشه‌ی مهارت ولی برای کلِ فصل‌ها +const CHAPTER_POSITIONS = [ + { x: 30, y: 66 }, + { x: 74, y: 214 }, + { x: 26, y: 362 }, + { x: 74, y: 510 }, + { x: 30, y: 658 }, + { x: 70, y: 806 }, +]; + +function TopicSelectScreen({ onSelect }: { onSelect: (topic: Topic) => void }) { + const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); + const mapHeight = CHAPTER_POSITIONS[CHAPTER_POSITIONS.length - 1].y + 190; + + const overallDone = TOPICS.reduce((a, t) => a + (LESSONS_BY_TOPIC[t.id]?.filter((l) => l.done).length ?? 0), 0); + const overallTotal = TOPICS.reduce((a, t) => a + (LESSONS_BY_TOPIC[t.id]?.length ?? 0), 0); + const overallPct = overallTotal ? Math.round((overallDone / overallTotal) * 100) : 0; return ( -
-
-
+
+ {/* Header */} +
+
+
-

مسیر ریاضی

-

{percentFa}٪ مسیر طی شده

+

نقشه‌ی فصل‌ها

+

{toFa(overallPct)}٪ از کل مسیر رو رفتی

- + 0 ? "happy" : "idle"} size={64}/>
-
-
+
+
-
-
- {/* خط تایم‌لاین که از وسط آیکن‌ها رد می‌شود */} -
-
- {items.map((item, i) => { - const locked = i > 0 && !items[i - 1].done; - const isCurrent = !locked && !item.done; - const Icon = item.type === "video" ? Play : Brain; + + {/* Map */} +
+
+ + {/* مسیرِ اتصال بینِ خونه‌های فصل‌ها */} + + {CHAPTER_POSITIONS.slice(0, -1).map((p1, i) => { + const p2 = CHAPTER_POSITIONS[i + 1]; + const midY = (p1.y + p2.y) / 2; + const d = `M ${p1.x} ${p1.y} C ${p1.x} ${midY}, ${p2.x} ${midY}, ${p2.x} ${p2.y}`; + const started = (LESSONS_BY_TOPIC[TOPICS[i]?.id] ?? []).some((l) => l.done); return ( -
- {locked ? : - item.done ? : - } -
-
-
-
{item.title}
-
- - {item.type === "video" ? "ویدیو" : "آزمون"} · {item.meta} -
-
- {isCurrent && ( - ادامه - )} -
- + ); })} + + + {/* خونه‌های فصل‌ها */} + {TOPICS.map((topic, i) => { + const pos = CHAPTER_POSITIONS[i]; + if (!pos) return null; + const items = LESSONS_BY_TOPIC[topic.id] ?? []; + const done = items.filter((l) => l.done).length; + const percent = items.length ? Math.round((done / items.length) * 100) : 0; + const complete = percent === 100; + const Icon = topic.icon; + return ( + + + + +
+
{topic.name}
+
{toFa(done)} از {toFa(items.length)} درس
+
+
+ ); + })} + + {/* پی، منتظرِ انتهای مسیر */} +
+ +
+ {overallPct === 100 ? "همه رو تموم کردی! 🎉" : "همین‌جا منتظرتم"} +
+
); } +// ─── Skill Map (مسیر مبحث، شبیه دولینگو) ──────────────────────────────────── + +// ─── Beautiful Skill Map ───────────────────────────────────────────────────── + +const MAP_NODE = 76; + +const MAP_POSITIONS = [ + { x: 50, y: 70 }, + { x: 72, y: 190 }, + { x: 43, y: 310 }, + { x: 25, y: 430 }, + { x: 55, y: 550 }, + { x: 76, y: 670 }, + { x: 48, y: 790 }, + { x: 28, y: 910 }, + { x: 58, y: 1030 }, + { x: 75, y: 1150 }, +]; + +function SkillMapScreen({ + topicId, + onOpenLesson, + onBack, +}: { + topicId: string; + onOpenLesson: (lessonId: number) => void; + onBack: () => void; +}) { + const topic = TOPICS.find((t) => t.id === topicId) ?? TOPICS[0]; + const items = LESSONS_BY_TOPIC[topic.id] ?? []; + + const doneCount = items.filter((it) => it.done).length; + const percent = items.length + ? Math.round((doneCount / items.length) * 100) + : 0; + + const percentFa = String(percent).replace( + /\d/g, + (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)] + ); + + const positions = items.map((_, i) => { + if (MAP_POSITIONS[i]) return MAP_POSITIONS[i]; + + const pattern = [ + { x: 50, y: 70 }, + { x: 72, y: 190 }, + { x: 43, y: 310 }, + { x: 25, y: 430 }, + ]; + + const p = pattern[i % pattern.length]; + + return { + x: p.x, + y: p.y + Math.floor(i / pattern.length) * 480, + }; + }); + + const mapHeight = + positions.length > 0 + ? positions[positions.length - 1].y + 150 + : 500; + + return ( +
+ + {/* ───────── Header ───────── */} + +
+ + {/* Decorative circles */} + +
+ +
+ +
+ + + +
+ +
+
+ مسیر یادگیری +
+ +

+ {topic.name} +

+ +

+ {percentFa}٪ مسیر رو رفتی +

+
+ + = 50 ? "happy" : "idle"} + size={76} + /> + +
+ + {/* Progress */} + +
+ +
+ پیشرفت تو + {percentFa}٪ +
+ +
+ + + +
+ +
+ +
+
+ + + {/* ───────── Map ───────── */} + +
+ +
+ + {/* Background decorative dots */} + +
+ + {[...Array(18)].map((_, i) => ( + +
+ + ))} + +
+ + + {/* ───────── Curved Path ───────── */} + + + + {/* Shadow path */} + + {positions.slice(0, -1).map((p1, i) => { + + const p2 = positions[i + 1]; + + const midY = (p1.y + p2.y) / 2; + + const d = ` + M ${p1.x} ${p1.y} + C ${p1.x} ${midY}, + ${p2.x} ${midY}, + ${p2.x} ${p2.y} + `; + + return ( + + ); + + })} + + + {/* Main path */} + + {positions.slice(0, -1).map((p1, i) => { + + const p2 = positions[i + 1]; + + const midY = (p1.y + p2.y) / 2; + + const d = ` + M ${p1.x} ${p1.y} + C ${p1.x} ${midY}, + ${p2.x} ${midY}, + ${p2.x} ${p2.y} + `; + + const completed = items[i]?.done; + + return ( + + ); + + })} + + + + + {/* ───────── Nodes ───────── */} + + {items.map((item, i) => { + + const position = positions[i]; + + const locked = + i > 0 && !items[i - 1].done; + + const current = + !locked && !item.done; + + const completed = + item.done; + + const Icon = + item.type === "video" + ? Play + : Brain; + + return ( + + + + {/* Current lesson label */} + + {current && ( + + + +
+ + {item.title} + + {/* little arrow */} + +
+ +
+ + + + )} + + + {/* Current glow */} + + {current && ( + + + + )} + + + {/* Node */} + + + + + {/* Lesson type */} + + {!locked && !current && ( + +
+ {item.type === "video" + ? "ویدیو" + : "آزمون"} +
+ + )} + +
+ + ); + + })} + + + {/* Finish decoration */} + + {items.length > 0 && ( + +
+ +
+ ادامه بده! +
+ +
+ +
+ +
+ + )} + +
+ +
+ +
+ ); +} +// ─── Lessons Hub (تب «درس‌ها») ────────────────────────────────────────────── +// دسته‌بندی‌شده و مناسبِ دانش‌آموزِ دبیرستانی: کارتِ ادامه، سه دسته‌ی درسی، +// و یه دعوتِ کوچیک از دلتا برای یه چالشِ سریع. + +function LessonsHubScreen({ + onOpenTopic, + onNavigate, +}: { + onOpenTopic: (topicId: string) => void; + onNavigate: (s: MainScreen) => void; +}) { + const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); + + // اولین درسِ ناتمام رو برای کارتِ «ادامه بده» پیدا می‌کنیم + const { topic: continueTopic, lesson: continueLesson } = findContinueLesson(); + const continueItems = LESSONS_BY_TOPIC[continueTopic.id] ?? []; + const continueDone = continueItems.filter((i) => i.done).length; + + return ( +
+ {/* Header */} +
+
+
+
+

درس‌ها

+

با سه‌یار، قدم‌به‌قدم جلو برو

+
+ +
+
+ +
+ + {/* Continue card */} +
+
+
+

ادامه از جایی که موندی

+

{continueTopic.name}

+

{continueLesson.title} · {toFa(continueDone)} از {toFa(continueItems.length)} درس

+
+
+ +
+
+
+
+
+ +
+ + {/* Categories */} + {LESSON_CATEGORIES.map((cat) => ( +
+
+

{cat.title}

+ {cat.blurb} +
+
+ {cat.topicIds.map((tid) => { + const topic = TOPICS.find((t) => t.id === tid); + if (!topic) return null; + const items = LESSONS_BY_TOPIC[topic.id] ?? []; + const done = items.filter((i) => i.done).length; + const percent = items.length ? Math.round((done / items.length) * 100) : 0; + const Icon = topic.icon; + return ( + + ); + })} +
+
+ ))} + + {/* دعوتِ دلتا به یه چالشِ سریع */} + + +
+
+ ); +} + // ─── Lesson Screen ──────────────────────────────────────────────────────────── -function LessonScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) { +// ─── محتوای واقعیِ درس‌ها (برای اولین درسِ هر فصل) ────────────────────────── +// کلید: `${topicId}-${lessonId}` — بقیه‌ی درس‌ها یه توضیحِ کوتاهِ عمومی می‌گیرن +const LESSON_CONTENT: Record = { + "linear-algebra-1": { + explain: "یه بردار در واقع یه پیکانه که هم اندازه داره هم جهت؛ برخلافِ عددهای معمولی که فقط بزرگی دارن. توی فضای دو‌بعدی هر بردار رو با دو مؤلفه (x, y) نشون می‌دیم.", + example: "a = (۳, ۴) → |a| = ۵", + steps: [ + "بردار رو با مؤلفه‌هاش بنویس: (x, y)", + "اندازه‌ش رو با قضیه‌ی فیثاغورس حساب کن: √(x²+y²)", + "جهتش رو با زاویه‌ای که با محورِ x می‌سازه مشخص کن", + ], + }, + "limits-1": { + explain: "حدِ یه تابع تو یه نقطه یعنی: وقتی x به اون نقطه نزدیک می‌شه (نه لزوماً بهش می‌رسه)، مقدارِ تابع به چه عددی نزدیک می‌شه؟", + example: "lim(x→۲) x² = ۴", + steps: [ + "ببین تابع تو خودِ نقطه تعریف‌شده یا نه", + "اگه تعریف‌شده و پیوسته بود، فقط عدد رو جایگذاری کن", + "اگه نامعین شد (مثلِ ۰/۰)، باید اول ساده‌سازی کنی", + ], + }, + "derivatives-1": { + explain: "مشتق نشون می‌ده یه تابع تو هر نقطه با چه شیبی داره تغییر می‌کنه؛ یعنی نرخِ لحظه‌ایِ تغییرات.", + example: "f(x) = x² → f′(x) = ۲x", + steps: [ + "توانِ x رو بیار جلو و ضرب کن", + "توان رو یکی کم کن", + "برای چندجمله‌ای‌ها، هر جمله رو جدا مشتق بگیر", + ], + }, + "integrals-1": { + explain: "انتگرالِ نامعین برعکسِ مشتق‌گیریه؛ یعنی دنبالِ تابعی می‌گردیم که مشتقش، همون تابعِ داده‌شده باشه.", + example: "∫x dx = x²/۲ + C", + steps: [ + "یک واحد به توانِ x اضافه کن", + "بر توانِ جدید تقسیم کن", + "یادت نره C (ثابتِ انتگرال‌گیری) رو بذاری", + ], + }, + "geometry-1": { + explain: "معادله‌ی خط نشون می‌ده هر نقطه‌ی روی اون خط چه رابطه‌ای با x و y داره؛ معادله‌ی دایره هم فاصله‌ی ثابتِ هر نقطه از مرکز رو نشون می‌ده.", + example: "(x−a)² + (y−b)² = r²", + steps: [ + "مرکزِ دایره (a, b) و شعاعِ r رو مشخص کن", + "تو فرمول جاگذاری کن", + "برای خط، از شیب و یه نقطه معادله رو بساز: y−y₁ = m(x−x₁)", + ], + }, + "sequences-1": { + explain: "دنباله یعنی یه لیستِ مرتب از عددها که هرکدوم بر اساسِ یه قاعده‌ی مشخص میاد بعدِ قبلی.", + example: "aₙ = ۲n − ۱ → ۱، ۳، ۵، ۷، …", + steps: [ + "قاعده‌ی دنباله (جمله‌ی عمومی) رو پیدا کن", + "با جایگذاریِ n=۱،۲،۳،… جمله‌ها رو بساز", + "برای دنباله‌ی حسابی، اختلافِ ثابت بینِ جمله‌ها رو چک کن", + ], + }, +}; + +function LessonScreen({ + topicId, + lessonId, + onNavigate, + onBack, +}: { + topicId: string; + lessonId: number; + onNavigate: (s: MainScreen) => void; + onBack: () => void; +}) { + const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); + const topic = TOPICS.find((t) => t.id === topicId) ?? TOPICS[0]; + const items = LESSONS_BY_TOPIC[topic.id] ?? []; + const idx = Math.max(0, items.findIndex((l) => l.id === lessonId)); + const lesson = items[idx] ?? items[0]; + const content = LESSON_CONTENT[`${topic.id}-${lesson.id}`]; + return (
-
+
+
- درس ۳ از ۸ + درس {toFa(idx + 1)} از {toFa(items.length)} · {topic.name}
-

ضرب دو رقمی

-

Multiplication

+

{lesson.title}

+

{topic.name}

- +
- {["⏱ ۱۰ دقیقه", " ۸ مسئله"].map(t => ( + {[`⏱ ${lesson.meta}`, lesson.type === "quiz" ? "آزمون" : "ویدیو"].map(t => ( {t} ))}
-
-
-
- -
- توضیح رُبو + {/* توضیحِ سیگما، به‌جای پنلِ متنیِ خشک */} +
+
+
-

- ضرب دو رقمی رو می‌شه با روش ستون‌نویسی حل کرد. اول رقم یکان رو در کل عدد ضرب می‌کنیم، بعد رقم دهگان رو. -

-
-

مثال:

-

۲۳ × ۱۴ = ۳۲۲

+
+

+ {content + ? content.explain + : `این درس بخشی از فصلِ «${topic.name}»‌ه. تو ویدیو، «${lesson.title}» رو قدم‌به‌قدم با هم جلو می‌ریم.`} +

+ {content && ( +
+

مثال:

+

{content.example}

+
+ )}
-
-

مرحله به مرحله

- {[ - { n: "۱", t: "۲۳ × ۴ = ۹۲ (ضرب در رقم یکان)" }, - { n: "۲", t: "۲۳ × ۱۰ = ۲۳۰ (ضرب در رقم دهگان)" }, - { n: "۳", t: "۹۲ + ۲۳۰ = ۳۲۲ (جمع دو نتیجه)" }, - ].map(({ n, t }) => ( -
-
- {n} + {content && ( +
+

مرحله به مرحله

+ {content.steps.map((t, i) => ( +
+
+ {toFa(i + 1)} +
+

{t}

-

{t}

-
- ))} -
+ ))} +
+ )}
@@ -656,9 +2243,11 @@ function LessonScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) { // ─── Question Screen ────────────────────────────────────────────────────────── -function QuestionScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) { - const [sel, setSel] = useState(null); - const [qIdx, setQIdx] = useState(0); +function QuestionScreen({ onNavigate, onBack }: { onNavigate: (s: MainScreen) => void; onBack: () => void }) { + const [sel, setSel] = useState(null); + const [qIdx, setQIdx] = useState(0); + const [lives, setLives] = useState(5); + const outOfLives = lives <= 0; const Qs = [ { q: "جواب این مسئله چیه؟", p: "۱۲ × ۳ = ?", opts: ["۳۴","۳۶","۳۸","۳۰"], c: 1 }, @@ -668,7 +2257,9 @@ function QuestionScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) ]; const q = Qs[qIdx]; const correct = sel === q.c; - const mood: RobotMood = sel === null ? "neutral" : correct ? "happy" : "encourage"; + // سه‌یار: قبل از جواب سیگما راهنماست، جوابِ درست یعنی دلتا، جوابِ غلط یعنی پی + const seyarChar: SeyarChar = sel === null ? "sigma" : correct ? "delta" : "pi"; + const seyarMood: SeyarMood = sel === null ? "idle" : correct ? "excited" : outOfLives ? "depressed" : "sad"; const optCls = (i: number) => { if (sel === null) return "bg-white border-2 border-[#e8e4ff] text-[#1a1a2e]"; @@ -677,7 +2268,14 @@ function QuestionScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) return "bg-white border-2 border-[#e8e4ff] text-[#b0b0c8]"; }; + const selectAnswer = (i: number) => { + if (sel !== null) return; + setSel(i); + if (i !== q.c) setLives(l => Math.max(0, l - 1)); + }; + const next = () => { + if (outOfLives) { setLives(5); setQIdx(0); setSel(null); return; } if (qIdx < Qs.length - 1) { setQIdx(i => i + 1); setSel(null); } else onNavigate("home"); }; @@ -687,7 +2285,7 @@ function QuestionScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) {/* Top bar */}
- @@ -697,32 +2295,55 @@ function QuestionScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void })
{qIdx + 1}/{Qs.length}
+ + {/* جون‌ها (قلب‌ها) */} +
+ {Array.from({ length: 5 }).map((_, i) => { + const filled = i < lives; + return ( +
+ + + {filled && ( + + + + )} + +
+ ); + })} +
- {/* Robot bubble */} + {/* سه‌یار bubble */}
- - +
- {mood === "neutral" ? "جواب درست رو انتخاب کن! " : - mood === "happy" ? "آفرین عالی بودی! ادامه بده!" : - "اشکال نداره! دوباره فکر کن تو می‌تونی!"} + {sel === null ? "جواب درست رو انتخاب کن! " : + correct ? "آفرین! درست بود ⚡ ادامه بده!" : + outOfLives ? "جونات تموم شد؛ نگران نباش، از اول با هم شروع می‌کنیم." : + "اشکالی نداره، بیا با هم دوباره نگاهش کنیم."}
@@ -739,7 +2360,7 @@ function QuestionScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void })
{q.opts.map((opt, i) => ( sel === null && setSel(i)} + onClick={() => selectAnswer(i)} whileTap={{ scale: sel === null ? 0.93 : 1 }} className={`py-4 rounded-2xl text-2xl font-black flex items-center justify-center gap-1.5 transition-all ${optCls(i)}`}> {opt} @@ -752,9 +2373,12 @@ function QuestionScreen({ onNavigate }: { onNavigate: (s: MainScreen) => void }) {sel !== null && ( - {qIdx < Qs.length - 1 ? "سوال بعدی" : "پایان تمرین"} - + className={`w-full py-4 text-white rounded-2xl font-black flex items-center justify-center gap-2 shadow-lg active:scale-[0.98] transition-transform ${ + outOfLives ? "bg-[#9B7BFF] shadow-[#9B7BFF]/30" : "bg-[#ED6956] shadow-[#ED6956]/30" + }`}> + {outOfLives ? <>دوباره تلاش کن : + qIdx < Qs.length - 1 ? <>سوال بعدی : + <>پایان تمرین } )}
@@ -768,7 +2392,7 @@ function AskAIScreen({ profile }: { profile: Profile }) { const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [messages, setMessages] = useState([ - { from: "bot", text: `سلام ${profile.name}! هر سوالی داری بپرس!` }, + { from: "bot", text: `سلام ${profile.name}! من پی‌ام 🙂 هر سوالی داری بپرس، قضاوتی هم در کار نیست!` }, ]); const send = () => { @@ -788,14 +2412,14 @@ function AskAIScreen({ profile }: { profile: Profile }) { return (
-
+
-

از رُبو بپرس

+

از پی بپرس

دستیار هوشمند ریاضی

- +
@@ -804,8 +2428,8 @@ function AskAIScreen({ profile }: { profile: Profile }) { {messages.map((msg, i) => (
{msg.from === "bot" && ( -
- +
+
)}
-
- +
+
{[0, 1, 2].map(i => ( - ))}
@@ -832,15 +2456,15 @@ function AskAIScreen({ profile }: { profile: Profile }) {
- setInput(e.target.value)} onKeyDown={e => e.key === "Enter" && send()} placeholder="سوالت رو بنویس..." dir="rtl" className="flex-1 h-10 bg-[#f0eeff] rounded-full px-4 text-sm outline-none text-[#1a1a2e] placeholder:text-[#b0b0c8]"/>
@@ -869,10 +2493,10 @@ function SubscriptionScreen() { name: "طلایی", en: "Gold", price: { monthly: "۴۹٬۰۰۰ تومان", yearly: "۳۹۰٬۰۰۰ تومان" }, badge: "محبوب‌ترین", badgeColor: "#ED6956", badgeFg: "white", - card: "bg-[#FBBD4C] border-2 border-white/40", - titleCls: "text-[#1a1a2e]", subCls: "text-[#1a1a2e]/55", - fCls: "text-[#1a1a2e]/80", checkCls: "text-[#ED6956]", - btnCls: "bg-[#ED6956] text-white font-bold", + card: "bg-[#FF7A3D] border-2 border-white/40", + titleCls: "text-white", subCls: "text-white/60", + fCls: "text-white/85", checkCls: "text-white", + btnCls: "bg-white text-[#FF7A3D] font-bold", features: ["درس‌های نامحدود", "همه تمرین‌ها", "دستیار هوشمند پایه", "بدون تبلیغ"], }, { @@ -889,10 +2513,10 @@ function SubscriptionScreen() { return (
-
+

اشتراک ویژه

-

یادگیری ریاضی رو جدی بگیر

+

یادگیری ریاضی رو جدی بگیر

@@ -901,7 +2525,7 @@ function SubscriptionScreen() { {(["monthly","yearly"] as const).map(b => ( ))}
@@ -959,14 +2583,48 @@ export default function App() { const [flow, setFlow] = useState("splash"); const [screen, setScreen] = useState("home"); const [profile, setProfile] = useState(null); + const [auth, setAuth] = useState(null); + const [topicId, setTopicId] = useState(TOPICS[0].id); + const [lessonId, setLessonId] = useState(1); - // "Returning user" simulation: if profile exists, skip splash+onboarding+profile - const handleReturning = () => { - if (profile) { setFlow("main"); } - else { setFlow("profile"); } + // ─── تاریخچه‌ی صفحات، برای دکمه‌ی «بازگشت» ──────────────────────────────── + // navigate: رفتن به جلو (صفحه‌ی فعلی رو تو تاریخچه نگه می‌داره) + // goBack: برگشت به صفحه‌ی قبلی (یا خانه، اگه چیزی تو تاریخچه نبود) + // switchTab: تعویضِ تبِ پایین؛ تاریخچه رو پاک می‌کنه چون یه مسیرِ تازه‌ست + const [history, setHistory] = useState([]); + + const navigate = (s: MainScreen) => { + setHistory(h => [...h, screen]); + setScreen(s); + }; + const goBack = () => { + setHistory(h => { + if (h.length === 0) { setScreen("home"); return h; } + setScreen(h[h.length - 1]); + return h.slice(0, -1); + }); + }; + const switchTab = (s: MainScreen) => { + setHistory([]); + setScreen(s); }; - const handleOnboardingDone = () => setFlow("profile"); + useEffect(() => { + setAuthExpiredHandler(() => { + setAuth(null); + setFlow("login"); + }); + return () => setAuthExpiredHandler(null); + }, []); + + const handleReturning = () => setFlow("login"); + const handleOnboardingDone = () => setFlow("login"); + + const handleLoginDone = (a: AuthData) => { + setAuth(a); + if (profile && !a.isNewUser) { setFlow("main"); setScreen("home"); } + else { setFlow("profile"); } + }; const handleProfileDone = (p: Profile) => { setProfile(p); @@ -1011,17 +2669,20 @@ export default function App() { {flow === "splash" && setFlow("onboarding")} onReturning={handleReturning}/>} {flow === "onboarding"&& } {flow === "profile" && } - {flow === "main" && screen === "home" && } - {flow === "main" && screen === "skillmap" && } - {flow === "main" && screen === "lesson" && } - {flow === "main" && screen === "question" && } + {flow === "main" && screen === "lesson" && } + {flow === "main" && screen === "question" && } {flow === "main" && screen === "askai" && } {flow === "main" && screen === "subscription" && } + {flow === "login" && } + {flow === "main" && screen === "home" && { setTopicId(id); navigate("skillmap"); }}/>} + {flow === "main" && screen === "topics" && { setTopicId(t.id); navigate("skillmap"); }}/>} + {flow === "main" && screen === "lessons" && { setTopicId(id); navigate("skillmap"); }} onNavigate={navigate}/>} + {flow === "main" && screen === "skillmap" && { setLessonId(id); navigate("lesson"); }} onBack={goBack}/>}
- {showNav && } + {showNav && }
);