From 348bfecea68cc1367123b60cd1a30b34e893b6a9 Mon Sep 17 00:00:00 2001 From: AliReza Date: Sat, 26 Sep 2026 23:35:48 +0330 Subject: [PATCH] rebuild the app on a typed, modular structure wired to the real API The whole app lived in one 3,071-line App.tsx on a Figma scaffold (60 unused packages, 45 unused UI components, no type-check). The live site could not reach the backend, Persian-keyboard digits were erased from the phone field, guests were signed out after an hour, "mark read" hit a method the server does not accept (405), and iOS Safari blocked the payment window. What changed: - src/ split into api/ (one typed client: timeouts, GET retry, single-flight refresh, SSE), screens/ (one file each), components/, state/, nav/, lib/. - The API address is configuration only: public/config.js at runtime, then VITE_API_BASE_URL at build time, then same-origin /api. No domain in code. - Responsive shell instead of a fixed 390x844 phone frame; self-hosted Vazirmatn instead of Google Fonts; strict TypeScript, `npm run build` type-checks first. - Everything on the handoff list: tutor answers keep their line breaks, have thumbs up/down, follow-ups, "from the book" badge, retry, history that survives tab switches and reloads, "why was I wrong" from a wrong answer with questionId; question reports; hearts with maxHearts/unlimited and a no-hearts screen with the refill time; result screen and a visible error when lesson completion fails; subscription status, badge, discount code before the gateway and the payment-return message; guest refresh token; 6-digit OTP with resend timer; age saved as a Jalali birthYear; PATCH read-all; leaderboard refetched on open with "you" highlighted; selected companion shown across the app; logout that revokes the refresh token. - New screens for endpoints that had none: profile, streak calendar, support tickets and bug reports, parents, invite-a-friend (with ?ref= tracking), guest-to-phone upgrade. - Browser/phone back button walks the screen stack. Checked with a headless-Chrome walkthrough against a local backend at 390x844 and 360x740 (fresh guest: onboarding, setup, a full lesson, map, island sheet, every tab and sub-screen, one real tutor question, reload, logout): no console errors, no failed API calls. Real SMS login and a real Zarinpal payment were not exercised. Co-Authored-By: Claude Opus 5.5 --- .env.development | 8 + .env.production | 8 + .gitignore | 5 +- ATTRIBUTIONS.md | 3 - README.md | 84 + default_shadcn_theme.css | 120 - guidelines/Guidelines.md | 61 - index.html | 40 +- package-lock.json | 3324 ++--------------- package.json | 88 +- pnpm-workspace.yaml | 10 - public/config.js | 9 + public/favicon.svg | 1 + src/api/ConnectionCheck.tsx | 87 - src/api/README.md | 91 - src/api/client.ts | 317 -- src/api/http.ts | 339 ++ src/api/index.ts | 589 +-- src/api/types.ts | 231 +- src/app/App.tsx | 3179 +--------------- src/app/MainApp.tsx | 149 + .../components/figma/ImageWithFallback.tsx | 27 - src/app/components/ui/accordion.tsx | 66 - src/app/components/ui/alert-dialog.tsx | 157 - src/app/components/ui/alert.tsx | 66 - src/app/components/ui/aspect-ratio.tsx | 11 - src/app/components/ui/avatar.tsx | 53 - src/app/components/ui/badge.tsx | 46 - src/app/components/ui/breadcrumb.tsx | 109 - src/app/components/ui/button.tsx | 58 - src/app/components/ui/calendar.tsx | 75 - src/app/components/ui/card.tsx | 92 - src/app/components/ui/carousel.tsx | 241 -- src/app/components/ui/chart.tsx | 353 -- src/app/components/ui/checkbox.tsx | 32 - src/app/components/ui/collapsible.tsx | 33 - src/app/components/ui/command.tsx | 177 - src/app/components/ui/context-menu.tsx | 252 -- src/app/components/ui/dialog.tsx | 135 - src/app/components/ui/drawer.tsx | 132 - src/app/components/ui/dropdown-menu.tsx | 257 -- src/app/components/ui/form.tsx | 168 - src/app/components/ui/hover-card.tsx | 44 - src/app/components/ui/input-otp.tsx | 77 - src/app/components/ui/input.tsx | 21 - src/app/components/ui/label.tsx | 24 - src/app/components/ui/menubar.tsx | 276 -- src/app/components/ui/navigation-menu.tsx | 168 - src/app/components/ui/pagination.tsx | 127 - src/app/components/ui/popover.tsx | 48 - src/app/components/ui/progress.tsx | 31 - src/app/components/ui/radio-group.tsx | 45 - src/app/components/ui/resizable.tsx | 56 - src/app/components/ui/scroll-area.tsx | 58 - src/app/components/ui/select.tsx | 189 - src/app/components/ui/separator.tsx | 28 - src/app/components/ui/sheet.tsx | 139 - src/app/components/ui/sidebar.tsx | 726 ---- src/app/components/ui/skeleton.tsx | 13 - src/app/components/ui/slider.tsx | 63 - src/app/components/ui/sonner.tsx | 25 - src/app/components/ui/switch.tsx | 31 - src/app/components/ui/table.tsx | 116 - src/app/components/ui/tabs.tsx | 66 - src/app/components/ui/textarea.tsx | 18 - src/app/components/ui/toggle-group.tsx | 73 - src/app/components/ui/toggle.tsx | 47 - src/app/components/ui/tooltip.tsx | 61 - src/app/components/ui/use-mobile.ts | 21 - src/app/components/ui/utils.ts | 6 - src/app/launchParams.ts | 29 + src/components/AgePicker.tsx | 66 + src/components/AppShell.tsx | 21 + src/components/BottomNav.tsx | 34 + src/components/NodeIcon.tsx | 18 + src/components/Sheet.tsx | 60 + src/components/Toast.tsx | 45 + src/components/ZigzagPath.tsx | 34 + src/components/ui.tsx | 198 + src/config.ts | 13 + src/lib/errors.ts | 11 + src/lib/format.ts | 70 + src/lib/sound.ts | 54 + src/lib/storage.ts | 45 + src/lib/useAsync.ts | 46 + src/main.tsx | 15 +- src/mascots/SeyarMascot.tsx | 36 + src/mascots/seyar.ts | 150 + src/nav/Navigator.tsx | 72 + src/nav/routes.ts | 26 + src/screens/AskScreen.tsx | 291 ++ src/screens/CharactersScreen.tsx | 105 + src/screens/FamilyScreen.tsx | 168 + src/screens/HomeScreen.tsx | 180 + src/screens/InviteScreen.tsx | 85 + src/screens/LeaderboardScreen.tsx | 87 + src/screens/LessonScreen.tsx | 102 + src/screens/LoginScreen.tsx | 209 ++ src/screens/MapScreen.tsx | 96 + src/screens/NotificationsScreen.tsx | 99 + src/screens/OnboardingScreen.tsx | 91 + src/screens/ProfileScreen.tsx | 283 ++ src/screens/QuizScreen.tsx | 466 +++ src/screens/SetupScreen.tsx | 128 + src/screens/SplashScreen.tsx | 27 + src/screens/StreakScreen.tsx | 92 + src/screens/SubscriptionScreen.tsx | 192 + src/screens/SupportScreen.tsx | 197 + src/screens/UnitScreen.tsx | 212 ++ src/state/AppData.tsx | 111 + src/state/askStore.ts | 86 + src/state/curriculum.ts | 57 + src/styles/fonts.css | 1 - src/styles/globals.css | 0 src/styles/index.css | 71 +- src/styles/tailwind.css | 4 - src/styles/theme.css | 181 - src/vite-env.d.ts | 10 + tsconfig.json | 21 + vite.config.ts | 48 +- 120 files changed, 5840 insertions(+), 12783 deletions(-) create mode 100644 .env.development create mode 100644 .env.production delete mode 100644 ATTRIBUTIONS.md create mode 100644 README.md delete mode 100644 default_shadcn_theme.css delete mode 100644 guidelines/Guidelines.md delete mode 100644 pnpm-workspace.yaml create mode 100644 public/config.js create mode 100644 public/favicon.svg delete mode 100644 src/api/ConnectionCheck.tsx delete mode 100644 src/api/README.md delete mode 100644 src/api/client.ts create mode 100644 src/api/http.ts create mode 100644 src/app/MainApp.tsx delete mode 100644 src/app/components/figma/ImageWithFallback.tsx delete mode 100644 src/app/components/ui/accordion.tsx delete mode 100644 src/app/components/ui/alert-dialog.tsx delete mode 100644 src/app/components/ui/alert.tsx delete mode 100644 src/app/components/ui/aspect-ratio.tsx delete mode 100644 src/app/components/ui/avatar.tsx delete mode 100644 src/app/components/ui/badge.tsx delete mode 100644 src/app/components/ui/breadcrumb.tsx delete mode 100644 src/app/components/ui/button.tsx delete mode 100644 src/app/components/ui/calendar.tsx delete mode 100644 src/app/components/ui/card.tsx delete mode 100644 src/app/components/ui/carousel.tsx delete mode 100644 src/app/components/ui/chart.tsx delete mode 100644 src/app/components/ui/checkbox.tsx delete mode 100644 src/app/components/ui/collapsible.tsx delete mode 100644 src/app/components/ui/command.tsx delete mode 100644 src/app/components/ui/context-menu.tsx delete mode 100644 src/app/components/ui/dialog.tsx delete mode 100644 src/app/components/ui/drawer.tsx delete mode 100644 src/app/components/ui/dropdown-menu.tsx delete mode 100644 src/app/components/ui/form.tsx delete mode 100644 src/app/components/ui/hover-card.tsx delete mode 100644 src/app/components/ui/input-otp.tsx delete mode 100644 src/app/components/ui/input.tsx delete mode 100644 src/app/components/ui/label.tsx delete mode 100644 src/app/components/ui/menubar.tsx delete mode 100644 src/app/components/ui/navigation-menu.tsx delete mode 100644 src/app/components/ui/pagination.tsx delete mode 100644 src/app/components/ui/popover.tsx delete mode 100644 src/app/components/ui/progress.tsx delete mode 100644 src/app/components/ui/radio-group.tsx delete mode 100644 src/app/components/ui/resizable.tsx delete mode 100644 src/app/components/ui/scroll-area.tsx delete mode 100644 src/app/components/ui/select.tsx delete mode 100644 src/app/components/ui/separator.tsx delete mode 100644 src/app/components/ui/sheet.tsx delete mode 100644 src/app/components/ui/sidebar.tsx delete mode 100644 src/app/components/ui/skeleton.tsx delete mode 100644 src/app/components/ui/slider.tsx delete mode 100644 src/app/components/ui/sonner.tsx delete mode 100644 src/app/components/ui/switch.tsx delete mode 100644 src/app/components/ui/table.tsx delete mode 100644 src/app/components/ui/tabs.tsx delete mode 100644 src/app/components/ui/textarea.tsx delete mode 100644 src/app/components/ui/toggle-group.tsx delete mode 100644 src/app/components/ui/toggle.tsx delete mode 100644 src/app/components/ui/tooltip.tsx delete mode 100644 src/app/components/ui/use-mobile.ts delete mode 100644 src/app/components/ui/utils.ts create mode 100644 src/app/launchParams.ts create mode 100644 src/components/AgePicker.tsx create mode 100644 src/components/AppShell.tsx create mode 100644 src/components/BottomNav.tsx create mode 100644 src/components/NodeIcon.tsx create mode 100644 src/components/Sheet.tsx create mode 100644 src/components/Toast.tsx create mode 100644 src/components/ZigzagPath.tsx create mode 100644 src/components/ui.tsx create mode 100644 src/config.ts create mode 100644 src/lib/errors.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/sound.ts create mode 100644 src/lib/storage.ts create mode 100644 src/lib/useAsync.ts create mode 100644 src/mascots/SeyarMascot.tsx create mode 100644 src/mascots/seyar.ts create mode 100644 src/nav/Navigator.tsx create mode 100644 src/nav/routes.ts create mode 100644 src/screens/AskScreen.tsx create mode 100644 src/screens/CharactersScreen.tsx create mode 100644 src/screens/FamilyScreen.tsx create mode 100644 src/screens/HomeScreen.tsx create mode 100644 src/screens/InviteScreen.tsx create mode 100644 src/screens/LeaderboardScreen.tsx create mode 100644 src/screens/LessonScreen.tsx create mode 100644 src/screens/LoginScreen.tsx create mode 100644 src/screens/MapScreen.tsx create mode 100644 src/screens/NotificationsScreen.tsx create mode 100644 src/screens/OnboardingScreen.tsx create mode 100644 src/screens/ProfileScreen.tsx create mode 100644 src/screens/QuizScreen.tsx create mode 100644 src/screens/SetupScreen.tsx create mode 100644 src/screens/SplashScreen.tsx create mode 100644 src/screens/StreakScreen.tsx create mode 100644 src/screens/SubscriptionScreen.tsx create mode 100644 src/screens/SupportScreen.tsx create mode 100644 src/screens/UnitScreen.tsx create mode 100644 src/state/AppData.tsx create mode 100644 src/state/askStore.ts create mode 100644 src/state/curriculum.ts delete mode 100644 src/styles/fonts.css delete mode 100644 src/styles/globals.css delete mode 100644 src/styles/tailwind.css delete mode 100644 src/styles/theme.css create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..46a2456 --- /dev/null +++ b/.env.development @@ -0,0 +1,8 @@ +# حالتِ توسعه (npm run dev). درخواست‌ها به /api می‌روند و Vite آن‌ها را به +# VITE_DEV_PROXY_TARGET پاس می‌دهد، پس CORS در توسعه اصلاً مطرح نیست. +# +# پیش‌فرض بک‌اندِ محلی است (python manage.py runserver). اگر بک‌اندِ محلی ندارید، +# در فایلِ .env.development.local این را به آدرسِ سرور عوض کنید — ولی یادتان +# باشد هرچه آنجا بزنید روی دیتابیسِ واقعی نوشته می‌شود. +VITE_API_BASE_URL=/api +VITE_DEV_PROXY_TARGET=http://127.0.0.1:8000 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..232afe0 --- /dev/null +++ b/.env.production @@ -0,0 +1,8 @@ +# آدرسِ API در buildِ پروداکشن. تنها جایی از ریپو که دامنه‌ی بک‌اند نوشته شده. +# +# برای عوض کردنِ دامنه دو راه هست: +# ۱) همین خط را عوض کنید و دوباره build بگیرید. +# ۲) بدونِ build: روی سرور، در dist/config.js مقدارِ apiBaseUrl را بنویسید. +# آن فایل در زمانِ اجرا خوانده می‌شود و بر این مقدار مقدم است. +# اگر nginxِ دامنه‌ی فرانت /api را به بک‌اند پراکسی کند، مقدارِ /api کافی است. +VITE_API_BASE_URL=https://api.math.tadrisino2.ir/api diff --git a/.gitignore b/.gitignore index b6eefa0..805d9ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules +dist -# متغیرهای محلی — هیچ‌وقت کامیت نشوند +# تنظیماتِ شخصیِ هر توسعه‌دهنده — کامیت نشوند .env .env.local -dist +.env.*.local diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md deleted file mode 100644 index 5df5c40..0000000 --- a/ATTRIBUTIONS.md +++ /dev/null @@ -1,3 +0,0 @@ -This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md). - -This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license). diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d890b1 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# تدریسینو — فرانتِ اپِ ریاضی + +اپِ وبِ موبایل‌محورِ تدریسینو (React + TypeScript + Vite + Tailwind). بک‌اندش ریپوی +جداست (`Tadrisino/MathApp`)؛ قراردادِ API در `/api/docs/` همان بک‌اند است. + +## راه‌اندازی + +```bash +npm install +npm run dev # http://localhost:5173 +``` + +در حالتِ توسعه، درخواست‌های `/api` از خودِ Vite به بک‌اند پاس داده می‌شوند، پس CORS +مطرح نیست. پیش‌فرض بک‌اندِ محلی است (`python manage.py runserver` روی پورتِ ۸۰۰۰). +بدونِ بک‌اندِ محلی، یک فایلِ `.env.development.local` بسازید: + +```bash +VITE_DEV_PROXY_TARGET=https://api.math.tadrisino2.ir +``` + +⚠️ آن‌وقت هر کاری در اپ بکنید روی دیتابیسِ واقعی نوشته می‌شود. + +| دستور | کار | +|---|---| +| `npm run dev` | سرورِ توسعه | +| `npm run typecheck` | بررسیِ نوع‌ها (strict) | +| `npm run build` | type-check و بعد build در `dist/` | +| `npm run preview` | اجرای همان build | + +## آدرسِ بک‌اند و عوض کردنِ دامنه + +دامنه هیچ‌جای کد نیامده. آدرسِ API به این ترتیب خوانده می‌شود +(`src/config.ts`): + +1. **`dist/config.js`** (از `public/config.js`) — در زمانِ اجرا. بعد از build هم روی + سرور قابلِ ویرایش است و build دوباره نمی‌خواهد. +2. **`VITE_API_BASE_URL`** — در زمانِ build، از `.env.production`. +3. **`/api`** — هم‌دامنه، وقتی nginxِ دامنه‌ی فرانت مسیرِ `/api` را به بک‌اند پراکسی کند. + +برای جابه‌جایی دامنه: + +- **سریع، بدونِ build:** روی سرور در `dist/config.js` بنویسید + `apiBaseUrl: "https://api.new-domain.ir/api"` و کشِ CDN را برای همین فایل پاک کنید. +- **ماندگار:** خطِ `VITE_API_BASE_URL` در `.env.production` را عوض کنید و build بگیرید. +- **سمتِ بک‌اند** هم دامنه‌ی تازه‌ی فرانت باید مجاز باشد (`FRONTEND_URL` در `.env` + بک‌اند). جزئیات در `docs/operations.md` همان ریپو. + +## دیپلوی + +```bash +npm ci && npm run build +``` + +محتوای `dist/` را سرو کنید. اپ مسیرِ جدا (route) ندارد و همه‌چیز از `/` بار می‌شود؛ +فقط `index.html` و `config.js` نباید طولانی کش شوند (باقیِ فایل‌ها نامِ hashدار دارند). +درگاهِ پرداخت کاربر را به `/?orderId=…&status=…` برمی‌گرداند و لینکِ دعوت +`/?ref=CODE` است؛ هر دو را خودِ اپ می‌خواند. + +## ساختار + +``` +src/ + config.ts آدرسِ API (بالا را ببینید) + api/ تنها درِ ارتباط با سرور — هیچ جای دیگر fetch نمی‌زند + http.ts timeout، تکرارِ GET، refreshِ تک‌نسخه‌ای، SSE + index.ts یک تابع برای هر اندپوینت + types.ts شکلِ پاسخ‌ها، از روی پاسخِ واقعیِ سرور + app/ App (جریانِ ورود) و MainApp (تب‌ها و صفحه‌ها) + nav/ ناوبریِ پشته‌ای، هماهنگ با دکمه‌ی back گوشی + state/ داده‌ی مشترک (پروفایل، نقشه، …) و گفتگوی دستیار + screens/ یک فایل برای هر صفحه + components/ اجزای مشترک (سربرگ، برگه‌ی پایین، …) + mascots/ سه‌یار: سیگما، دلتا، پی + lib/ اعدادِ فارسی، تاریخِ شمسی، صدا، localStorage +``` + +قاعده‌ها: + +- **اعداد:** برای نمایش `toFa`، برای ورودی `digitsOnly` (کیبوردِ فارسی ارقامِ + `۰-۹` می‌فرستد و `\d` در JS آن‌ها را نمی‌شناسد). +- **رنگ‌ها** در `src/styles/index.css` (`@theme`) تعریف شده‌اند: `bg-coral`، `text-ink`، … +- **خطا:** پیامِ کاربر همیشه از `friendlyError` می‌آید، نه متنِ خامِ سرور. +- **کلاینت را از حافظه ننویسید.** قرارداد را از `/api/docs/` بک‌اند بخوانید و تغییرِ + `types.ts` را با پاسخِ واقعیِ سرور بسنجید. diff --git a/default_shadcn_theme.css b/default_shadcn_theme.css deleted file mode 100644 index 39e1b44..0000000 --- a/default_shadcn_theme.css +++ /dev/null @@ -1,120 +0,0 @@ -/* KEEP_IN_SYNC(fullscreen/resources/figmake/shadcn/globals.css) */ - -:root { - --font-size: 16px; - --background: #ffffff; - --foreground: oklch(0.145 0 0); - --card: #ffffff; - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: #030213; - --primary-foreground: oklch(1 0 0); - --secondary: oklch(0.95 0.0058 264.53); - --secondary-foreground: #030213; - --muted: #ececf0; - --muted-foreground: #717182; - --accent: #e9ebef; - --accent-foreground: #030213; - --destructive: #d4183d; - --destructive-foreground: #ffffff; - --border: rgba(0, 0, 0, 0.1); - --input: transparent; - --input-background: #f3f3f5; - --switch-background: #cbced4; - --font-weight-medium: 500; - --font-weight-normal: 400; - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: #030213; - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --font-weight-medium: 500; - --font-weight-normal: 400; - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-input-background: var(--input-background); - --color-switch-background: var(--switch-background); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} diff --git a/guidelines/Guidelines.md b/guidelines/Guidelines.md deleted file mode 100644 index 110f117..0000000 --- a/guidelines/Guidelines.md +++ /dev/null @@ -1,61 +0,0 @@ -**Add your own guidelines here** - diff --git a/index.html b/index.html index be7eca1..738778e 100644 --- a/index.html +++ b/index.html @@ -1,22 +1,18 @@ - - - - - - - - Update Educational App Mascot - - - - - - - - -
- - - - - \ No newline at end of file + + + + + + + + + + تدریسینو + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json index 0a30838..e1c4aa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,92 +1,36 @@ { - "name": "@figma/my-make-file", - "version": "0.0.1", + "name": "tadrisino-math-app", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@figma/my-make-file", - "version": "0.0.1", + "name": "tadrisino-math-app", + "version": "2.0.0", "dependencies": { - "@emotion/react": "11.14.0", - "@emotion/styled": "11.14.1", - "@mui/icons-material": "7.3.5", - "@mui/material": "7.3.5", - "@popperjs/core": "2.11.8", - "@radix-ui/react-accordion": "1.2.3", - "@radix-ui/react-alert-dialog": "1.1.6", - "@radix-ui/react-aspect-ratio": "1.1.2", - "@radix-ui/react-avatar": "1.1.3", - "@radix-ui/react-checkbox": "1.1.4", - "@radix-ui/react-collapsible": "1.1.3", - "@radix-ui/react-context-menu": "2.2.6", - "@radix-ui/react-dialog": "1.1.6", - "@radix-ui/react-dropdown-menu": "2.1.6", - "@radix-ui/react-hover-card": "1.1.6", - "@radix-ui/react-label": "2.1.2", - "@radix-ui/react-menubar": "1.1.6", - "@radix-ui/react-navigation-menu": "1.2.5", - "@radix-ui/react-popover": "1.1.6", - "@radix-ui/react-progress": "1.1.2", - "@radix-ui/react-radio-group": "1.2.3", - "@radix-ui/react-scroll-area": "1.2.3", - "@radix-ui/react-select": "2.1.6", - "@radix-ui/react-separator": "1.1.2", - "@radix-ui/react-slider": "1.2.3", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-switch": "1.1.3", - "@radix-ui/react-tabs": "1.1.3", - "@radix-ui/react-toggle": "1.1.2", - "@radix-ui/react-toggle-group": "1.1.2", - "@radix-ui/react-tooltip": "1.1.8", "canvas-confetti": "1.9.4", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "cmdk": "1.1.1", - "date-fns": "3.6.0", - "embla-carousel-react": "8.6.0", - "input-otp": "1.4.2", "lucide-react": "0.487.0", "motion": "12.23.24", - "next-themes": "0.4.6", - "react-day-picker": "8.10.1", - "react-dnd": "16.0.1", - "react-dnd-html5-backend": "16.0.1", - "react-hook-form": "7.55.0", - "react-popper": "2.3.0", - "react-resizable-panels": "2.1.7", - "react-responsive-masonry": "2.7.1", - "react-router": "7.13.0", - "react-slick": "0.31.0", - "recharts": "2.15.2", - "sonner": "2.0.3", - "tailwind-merge": "3.2.0", - "tw-animate-css": "1.3.8", - "vaul": "1.1.2" + "react": "18.3.1", + "react-dom": "18.3.1", + "vazirmatn": "33.0.3" }, "devDependencies": { "@tailwindcss/vite": "4.1.12", + "@types/canvas-confetti": "1.9.0", + "@types/react": "18.3.31", + "@types/react-dom": "18.3.7", "@vitejs/plugin-react": "4.7.0", "tailwindcss": "4.1.12", + "typescript": "5.9.3", "vite": "6.3.5" - }, - "peerDependencies": { - "react": "18.3.1", - "react-dom": "18.3.1" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } } }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -138,17 +82,11 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/generator": { "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.8", @@ -182,6 +120,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -191,6 +130,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -232,6 +172,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -241,6 +182,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -271,9 +213,10 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.8" @@ -317,19 +260,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -344,6 +279,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -362,6 +298,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -371,152 +308,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -959,44 +750,6 @@ "node": ">=18" } }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", - "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.8.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", - "license": "MIT" - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1014,6 +767,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1035,260 +789,30 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.11", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz", - "integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.5.tgz", - "integrity": "sha512-LciL1GLMZ+VlzyHAALSVAR22t8IST4LCXmljcUSx2NOutgO2XnxdIp8ilFbeNf9wpo0iUFbAuoQcB7h+HHIf3A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^7.3.5", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", - "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/core-downloads-tracker": "^7.3.5", - "@mui/system": "^7.3.5", - "@mui/types": "^7.4.8", - "@mui/utils": "^7.3.5", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.2.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.5", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "7.3.11", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz", - "integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@mui/utils": "^7.3.11", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz", - "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "7.3.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz", - "integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@mui/private-theming": "^7.3.11", - "@mui/styled-engine": "^7.3.10", - "@mui/types": "^7.4.12", - "@mui/utils": "^7.3.11", - "clsx": "^2.1.1", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", - "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "7.3.11", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", - "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@mui/types": "^7.4.12", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -1309,1316 +833,6 @@ "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", - "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.3.tgz", - "integrity": "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collapsible": "1.1.3", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", - "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dialog": "1.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", - "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.2.tgz", - "integrity": "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz", - "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", - "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.3.tgz", - "integrity": "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", - "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz", - "integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-menu": "2.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", - "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", - "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-menu": "2.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz", - "integrity": "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", - "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", - "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.6.tgz", - "integrity": "sha512-FHq7+3DlXwh/7FOM4i0G4bC4vPjiq89VEEvNF4VMLchGnaUuUbE5uKXMUCjdKaOghEEMeiKa5XCa2Pk4kteWmg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-menu": "2.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.5.tgz", - "integrity": "sha512-myMHHQUZ3ZLTi8W381/Vu43Ia0NqakkQZ2vzynMmTUtQQ9kNkjzhOwkZC9TAM5R07OZUVIQyHC06f/9JZJpvvA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", - "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", - "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", - "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", - "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", - "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", - "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", - "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.0", - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", - "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.0", - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", - "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz", - "integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.0", - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", - "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", - "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", - "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.2.tgz", - "integrity": "sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-toggle": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", - "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", - "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", - "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", - "license": "MIT" - }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", - "license": "MIT" - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2627,9 +841,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", - "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz", + "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", "cpu": [ "arm" ], @@ -2641,9 +855,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", - "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz", + "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", "cpu": [ "arm64" ], @@ -2655,9 +869,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", - "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz", + "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", "cpu": [ "arm64" ], @@ -2669,9 +883,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", - "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz", + "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", "cpu": [ "x64" ], @@ -2683,9 +897,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", - "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz", + "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", "cpu": [ "arm64" ], @@ -2697,9 +911,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", - "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz", + "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", "cpu": [ "x64" ], @@ -2711,9 +925,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", - "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz", + "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", "cpu": [ "arm" ], @@ -2728,9 +942,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", - "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz", + "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", "cpu": [ "arm" ], @@ -2745,9 +959,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", - "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz", + "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", "cpu": [ "arm64" ], @@ -2762,9 +976,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", - "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz", + "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", "cpu": [ "arm64" ], @@ -2779,9 +993,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", - "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz", + "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", "cpu": [ "loong64" ], @@ -2796,9 +1010,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", - "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz", + "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", "cpu": [ "loong64" ], @@ -2813,9 +1027,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", - "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz", + "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", "cpu": [ "ppc64" ], @@ -2830,9 +1044,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", - "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz", + "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", "cpu": [ "ppc64" ], @@ -2847,9 +1061,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", - "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz", + "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", "cpu": [ "riscv64" ], @@ -2864,9 +1078,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", - "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz", + "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", "cpu": [ "riscv64" ], @@ -2881,9 +1095,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", - "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz", + "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", "cpu": [ "s390x" ], @@ -2898,9 +1112,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", - "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz", + "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", "cpu": [ "x64" ], @@ -2915,9 +1129,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", - "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz", + "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", "cpu": [ "x64" ], @@ -2932,9 +1146,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", - "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz", + "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", "cpu": [ "x64" ], @@ -2946,9 +1160,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", - "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz", + "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", "cpu": [ "arm64" ], @@ -2960,9 +1174,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", - "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz", + "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", "cpu": [ "arm64" ], @@ -2974,9 +1188,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", - "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz", + "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", "cpu": [ "ia32" ], @@ -2988,9 +1202,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", - "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz", + "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", "cpu": [ "x64" ], @@ -3002,9 +1216,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", - "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz", + "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", "cpu": [ "x64" ], @@ -3349,67 +1563,11 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree": { @@ -3419,35 +1577,32 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { + "@types/prop-types": "*", "csstype": "^3.2.2" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "*" + "@types/react": "^18.0.0" } }, "node_modules/@vitejs/plugin-react": { @@ -3471,37 +1626,10 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, "node_modules/baseline-browser-mapping": { - "version": "2.11.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", - "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "version": "2.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3512,9 +1640,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.1.tgz", + "integrity": "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==", "dev": true, "funding": [ { @@ -3532,11 +1660,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" + "baseline-browser-mapping": "^2.11.25", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.438", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -3545,19 +1673,10 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001809", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", - "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "version": "1.0.30001812", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001812.tgz", + "integrity": "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==", "dev": true, "funding": [ { @@ -3595,234 +1714,25 @@ "node": ">=18" } }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3836,12 +1746,6 @@ } } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3852,72 +1756,17 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", - "license": "MIT", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.407", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", - "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", + "version": "1.5.439", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.439.tgz", + "integrity": "sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==", "dev": true, "license": "ISC" }, - "node_modules/embla-carousel": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", - "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" - }, - "node_modules/embla-carousel-react": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", - "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", - "license": "MIT", - "dependencies": { - "embla-carousel": "8.6.0", - "embla-carousel-reactive-utils": "8.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/embla-carousel-reactive-utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", - "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", - "license": "MIT", - "peerDependencies": { - "embla-carousel": "8.6.0" - } - }, "node_modules/enhanced-resolve": { - "version": "5.24.5", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", - "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", "dev": true, "license": "MIT", "dependencies": { @@ -3928,24 +1777,6 @@ "node": ">=10.13.0" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -3998,39 +1829,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-equals": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", - "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4049,12 +1847,6 @@ } } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, "node_modules/framer-motion": { "version": "12.43.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", @@ -4097,15 +1889,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4116,15 +1899,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4132,89 +1906,6 @@ "dev": true, "license": "ISC" }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/input-otp": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", - "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4235,6 +1926,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -4243,21 +1935,6 @@ "node": ">=6" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json2mq": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", - "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", - "license": "MIT", - "dependencies": { - "string-convert": "^0.2.0" - } - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4522,24 +2199,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4649,12 +2308,13 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -4670,90 +2330,27 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" - } - }, "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -4764,9 +2361,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -4784,7 +2381,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4792,29 +2389,11 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4822,65 +2401,11 @@ "node": ">=0.10.0" } }, - "node_modules/react-day-picker": { - "version": "8.10.1", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", - "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", - "license": "MIT", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/gpbl" - }, - "peerDependencies": { - "date-fns": "^2.28.0 || ^3.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-dnd": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", - "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", - "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", - "license": "MIT", - "dependencies": { - "dnd-core": "^16.0.1" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4889,49 +2414,6 @@ "react": "^18.3.1" } }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", - "license": "MIT" - }, - "node_modules/react-hook-form": { - "version": "7.55.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz", - "integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-is": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "license": "MIT" - }, - "node_modules/react-popper": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", - "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", - "license": "MIT", - "dependencies": { - "react-fast-compare": "^3.0.1", - "warning": "^4.0.2" - }, - "peerDependencies": { - "@popperjs/core": "^2.0.0", - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" - } - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4942,248 +2424,10 @@ "node": ">=0.10.0" } }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-resizable-panels": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", - "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/react-responsive-masonry": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-responsive-masonry/-/react-responsive-masonry-2.7.1.tgz", - "integrity": "sha512-Q+u+nOH87PzjqGFd2PgTcmLpHPZnCmUPREHYoNBc8dwJv6fi51p9U6hqwG8g/T8MN86HrFjrU+uQU6yvETU7cA==", - "license": "MIT" - }, - "node_modules/react-router": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", - "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-slick": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.31.0.tgz", - "integrity": "sha512-zo6VLT8wuSBJffg/TFPbzrw2dEnfZ/cUKmYsKByh3AgatRv29m2LoFbq5vRMa3R3A4wp4d8gwbJKO2fWZFaI3g==", - "license": "MIT", - "dependencies": { - "classnames": "^2.2.5", - "json2mq": "^0.2.0", - "lodash.debounce": "^4.0.8", - "resize-observer-polyfill": "^1.5.0" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/recharts": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", - "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", - "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", - "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/rollup": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", - "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", + "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", "dev": true, "license": "MIT", "dependencies": { @@ -5198,31 +2442,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.62.4", - "@rollup/rollup-android-arm64": "4.62.4", - "@rollup/rollup-darwin-arm64": "4.62.4", - "@rollup/rollup-darwin-x64": "4.62.4", - "@rollup/rollup-freebsd-arm64": "4.62.4", - "@rollup/rollup-freebsd-x64": "4.62.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", - "@rollup/rollup-linux-arm-musleabihf": "4.62.4", - "@rollup/rollup-linux-arm64-gnu": "4.62.4", - "@rollup/rollup-linux-arm64-musl": "4.62.4", - "@rollup/rollup-linux-loong64-gnu": "4.62.4", - "@rollup/rollup-linux-loong64-musl": "4.62.4", - "@rollup/rollup-linux-ppc64-gnu": "4.62.4", - "@rollup/rollup-linux-ppc64-musl": "4.62.4", - "@rollup/rollup-linux-riscv64-gnu": "4.62.4", - "@rollup/rollup-linux-riscv64-musl": "4.62.4", - "@rollup/rollup-linux-s390x-gnu": "4.62.4", - "@rollup/rollup-linux-x64-gnu": "4.62.4", - "@rollup/rollup-linux-x64-musl": "4.62.4", - "@rollup/rollup-openbsd-x64": "4.62.4", - "@rollup/rollup-openharmony-arm64": "4.62.4", - "@rollup/rollup-win32-arm64-msvc": "4.62.4", - "@rollup/rollup-win32-ia32-msvc": "4.62.4", - "@rollup/rollup-win32-x64-gnu": "4.62.4", - "@rollup/rollup-win32-x64-msvc": "4.62.4", + "@rollup/rollup-android-arm-eabi": "4.63.5", + "@rollup/rollup-android-arm64": "4.63.5", + "@rollup/rollup-darwin-arm64": "4.63.5", + "@rollup/rollup-darwin-x64": "4.63.5", + "@rollup/rollup-freebsd-arm64": "4.63.5", + "@rollup/rollup-freebsd-x64": "4.63.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", + "@rollup/rollup-linux-arm-musleabihf": "4.63.5", + "@rollup/rollup-linux-arm64-gnu": "4.63.5", + "@rollup/rollup-linux-arm64-musl": "4.63.5", + "@rollup/rollup-linux-loong64-gnu": "4.63.5", + "@rollup/rollup-linux-loong64-musl": "4.63.5", + "@rollup/rollup-linux-ppc64-gnu": "4.63.5", + "@rollup/rollup-linux-ppc64-musl": "4.63.5", + "@rollup/rollup-linux-riscv64-gnu": "4.63.5", + "@rollup/rollup-linux-riscv64-musl": "4.63.5", + "@rollup/rollup-linux-s390x-gnu": "4.63.5", + "@rollup/rollup-linux-x64-gnu": "4.63.5", + "@rollup/rollup-linux-x64-musl": "4.63.5", + "@rollup/rollup-openbsd-x64": "4.63.5", + "@rollup/rollup-openharmony-arm64": "4.63.5", + "@rollup/rollup-win32-arm64-msvc": "4.63.5", + "@rollup/rollup-win32-ia32-msvc": "4.63.5", + "@rollup/rollup-win32-x64-gnu": "4.63.5", + "@rollup/rollup-win32-x64-msvc": "4.63.5", "fsevents": "~2.3.2" } }, @@ -5231,7 +2475,6 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -5246,31 +2489,6 @@ "semver": "bin/semver.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "node_modules/sonner": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", - "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5281,40 +2499,6 @@ "node": ">=0.10.0" } }, - "node_modules/string-convert": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", - "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", - "license": "MIT" - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", - "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", @@ -5363,12 +2547,6 @@ "node": ">=18" } }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -5392,19 +2570,24 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tw-animate-css": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", - "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Wombosvideo" + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, "node_modules/update-browserslist-db": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", - "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -5432,83 +2615,11 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/vaul": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", - "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-dialog": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } + "node_modules/vazirmatn": { + "version": "33.0.3", + "resolved": "https://registry.npmjs.org/vazirmatn/-/vazirmatn-33.0.3.tgz", + "integrity": "sha512-fbjNc0CMjazZpIegWzz9OHGzI1APFboT+x7ZecXlUCtDe/nkyx7gtCTZnWS/+eeXG7fbfXymCPKJI8EsnM3iqw==", + "license": "OFL" }, "node_modules/vite": { "version": "6.3.5", @@ -5585,15 +2696,6 @@ } } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index fad7042..e7fe8ea 100644 --- a/package.json +++ b/package.json @@ -1,90 +1,30 @@ { - "name": "@figma/my-make-file", + "name": "tadrisino-math-app", "private": true, - "version": "0.0.1", + "version": "2.0.0", "type": "module", "scripts": { - "build": "vite build", - "dev": "vite" + "dev": "vite", + "build": "tsc --noEmit && vite build", + "typecheck": "tsc --noEmit", + "preview": "vite preview" }, "dependencies": { - "@emotion/react": "11.14.0", - "@emotion/styled": "11.14.1", - "@mui/icons-material": "7.3.5", - "@mui/material": "7.3.5", - "@popperjs/core": "2.11.8", - "@radix-ui/react-accordion": "1.2.3", - "@radix-ui/react-alert-dialog": "1.1.6", - "@radix-ui/react-aspect-ratio": "1.1.2", - "@radix-ui/react-avatar": "1.1.3", - "@radix-ui/react-checkbox": "1.1.4", - "@radix-ui/react-collapsible": "1.1.3", - "@radix-ui/react-context-menu": "2.2.6", - "@radix-ui/react-dialog": "1.1.6", - "@radix-ui/react-dropdown-menu": "2.1.6", - "@radix-ui/react-hover-card": "1.1.6", - "@radix-ui/react-label": "2.1.2", - "@radix-ui/react-menubar": "1.1.6", - "@radix-ui/react-navigation-menu": "1.2.5", - "@radix-ui/react-popover": "1.1.6", - "@radix-ui/react-progress": "1.1.2", - "@radix-ui/react-radio-group": "1.2.3", - "@radix-ui/react-scroll-area": "1.2.3", - "@radix-ui/react-select": "2.1.6", - "@radix-ui/react-separator": "1.1.2", - "@radix-ui/react-slider": "1.2.3", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-switch": "1.1.3", - "@radix-ui/react-tabs": "1.1.3", - "@radix-ui/react-toggle-group": "1.1.2", - "@radix-ui/react-toggle": "1.1.2", - "@radix-ui/react-tooltip": "1.1.8", "canvas-confetti": "1.9.4", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "cmdk": "1.1.1", - "date-fns": "3.6.0", - "embla-carousel-react": "8.6.0", - "input-otp": "1.4.2", "lucide-react": "0.487.0", "motion": "12.23.24", - "next-themes": "0.4.6", - "react-day-picker": "8.10.1", - "react-dnd": "16.0.1", - "react-dnd-html5-backend": "16.0.1", - "react-hook-form": "7.55.0", - "react-popper": "2.3.0", - "react-resizable-panels": "2.1.7", - "react-responsive-masonry": "2.7.1", - "react-router": "7.13.0", - "react-slick": "0.31.0", - "recharts": "2.15.2", - "sonner": "2.0.3", - "tailwind-merge": "3.2.0", - "tw-animate-css": "1.3.8", - "vaul": "1.1.2" + "react": "18.3.1", + "react-dom": "18.3.1", + "vazirmatn": "33.0.3" }, "devDependencies": { "@tailwindcss/vite": "4.1.12", + "@types/canvas-confetti": "1.9.0", + "@types/react": "18.3.31", + "@types/react-dom": "18.3.7", "@vitejs/plugin-react": "4.7.0", "tailwindcss": "4.1.12", + "typescript": "5.9.3", "vite": "6.3.5" - }, - "peerDependencies": { - "react": "18.3.1", - "react-dom": "18.3.1" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - }, - "pnpm": { - "overrides": { - "vite": "6.3.5" - } } -} \ No newline at end of file +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index d7bfa8a..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,10 +0,0 @@ -packages: - - '.' -supportedArchitectures: - os: - - linux - cpu: - - x64 - - arm64 - libc: - - glibc diff --git a/public/config.js b/public/config.js new file mode 100644 index 0000000..91378a6 --- /dev/null +++ b/public/config.js @@ -0,0 +1,9 @@ +// تنظیماتِ زمانِ اجرای اپ — بعد از build هم روی سرور قابلِ ویرایش است. +// +// apiBaseUrl خالی یعنی همان آدرسی که موقعِ build در .env.production بوده. +// برای جابه‌جا کردنِ بک‌اند بدونِ build دوباره، آدرسِ کامل را اینجا بنویسید: +// apiBaseUrl: "https://api.new-domain.ir/api", +// اگر CDN (آروان) جلوی سایت است، بعد از ویرایش کشِ این فایل را پاک کنید. +window.__TADRISINO_CONFIG__ = { + apiBaseUrl: "", +}; diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..fadea4c --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ +Σ diff --git a/src/api/ConnectionCheck.tsx b/src/api/ConnectionCheck.tsx deleted file mode 100644 index c78e40e..0000000 --- a/src/api/ConnectionCheck.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/** - * بررسیِ اتصال به بک‌اند — ابزارِ توسعه، نه بخشی از محصول. - * - * برای امتحان کردن، یک خط در App.tsx (یا هر جای دیگر) کافی‌ست: - * - * import { ConnectionCheck } from '@/api/ConnectionCheck'; - * - * - * هیچ فایلِ دیگری را عوض نمی‌کند و اگر بک‌اند بالا نباشد هم اپ را نمی‌خواباند. - */ -import { useEffect, useState } from 'react'; -import type { CSSProperties } from 'react'; -import { api, ApiError } from '@/api'; -import type { Grade } from '@/api'; - -type State = - | { kind: 'loading' } - | { kind: 'ok'; grades: Grade[] } - | { kind: 'error'; message: string; hint: string }; - -function hintFor(error: unknown): string { - if (!(error instanceof ApiError)) return 'خطای پیش‌بینی‌نشده — کنسول را ببینید.'; - if (error.isNetwork) { - return 'سرور جواب نداد. VITE_API_BASE_URL درست است؟ بک‌اند بالاست؟'; - } - if (error.status === 502) { - return 'nginx بالاست ولی جنگو نه — سرویسِ بک‌اند روی سرور اجرا نشده.'; - } - if (error.status === 0) return 'ارتباط برقرار نشد.'; - if (error.isRateLimited) return 'سقفِ نرخ خورد؛ کلاینت خودش دوباره تلاش می‌کند.'; - if (error.isUnauthorized) return 'نیاز به ورود دارد (این اندپوینت نباید داشته باشد).'; - return `سرور ${error.status} داد.`; -} - -export function ConnectionCheck() { - const [state, setState] = useState({ kind: 'loading' }); - - useEffect(() => { - const controller = new AbortController(); - api - .grades({ signal: controller.signal }) - .then((grades) => setState({ kind: 'ok', grades })) - .catch((error: unknown) => { - if (controller.signal.aborted) return; - setState({ - kind: 'error', - message: error instanceof Error ? error.message : String(error), - hint: hintFor(error), - }); - }); - return () => controller.abort(); - }, []); - - const box: CSSProperties = { - position: 'fixed', bottom: 12, right: 12, zIndex: 9999, - maxWidth: 320, padding: '10px 12px', borderRadius: 10, - font: '12px/1.6 system-ui, sans-serif', direction: 'rtl', - background: '#fff', border: '1px solid #e5e7eb', - boxShadow: '0 4px 16px rgba(0,0,0,.08)', - }; - - if (state.kind === 'loading') { - return
در حالِ بررسیِ اتصال به بک‌اند…
; - } - - if (state.kind === 'error') { - return ( -
- اتصال برقرار نشد -
{state.message}
-
{state.hint}
-
{api.http.baseUrl}
-
- ); - } - - return ( -
- اتصال برقرار است -
- {state.grades.length} پایه از سرور آمد - {state.grades.length > 0 && `: ${state.grades.map((g) => g.title).join('، ')}`} -
-
{api.http.baseUrl}
-
- ); -} diff --git a/src/api/README.md b/src/api/README.md deleted file mode 100644 index a678b23..0000000 --- a/src/api/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# اتصال به بک‌اند - -کلِ ارتباط با سرور از همین پوشه رد می‌شود. **هیچ‌جای اپ مستقیم `fetch` نزنید** — -مدیریتِ توکن، تکرارِ درخواست و رفتارِ درستِ خطا اینجا یک‌جا حل شده‌اند. - -## راه‌اندازی - -**هیچ تنظیماتی لازم نیست.** به‌طورِ پیش‌فرض به بک‌اندِ مشترک -(`https://api.math.tadrisino2.ir/api`) وصل می‌شود: - -```bash -npm install -npm run dev -``` - -فقط اگر بک‌اند را روی ماشینِ خودتان بالا آورده‌اید: - -```bash -cp .env.example .env # و خطِ VITE_API_BASE_URL را باز کنید -``` - -برای اینکه ببینید اتصال برقرار است، یک خط کافی‌ست (بعداً پاکش کنید): - -```tsx -import { ConnectionCheck } from '@/api/ConnectionCheck'; -// داخلِ خروجیِ App: - -``` - -گوشه‌ی صفحه سبز می‌شود و فهرستِ پایه‌ها را از سرور نشان می‌دهد، یا قرمز -می‌شود و می‌گوید دقیقاً کجای کار می‌لنگد. - -## استفاده - -```tsx -import { api, ApiError } from '@/api'; - -const grades = await api.grades(); -const home = await api.home(); // پایه‌ی خودِ کاربر -const intro = await api.lessons.intro(lessonId); -const answer = await api.ai.ask('این رو نفهمیدم', { lessonId }); -``` - -### ورود - -```tsx -await api.auth.guest(deviceId); // «شروع سریع»، بدونِ شماره -// یا -const { devOtp } = await api.auth.requestOtp('09120000000'); -await api.auth.login('09120000000', otp); // نامِ فیلد در سرور otp است -``` - -توکن خودش در `localStorage` ذخیره می‌شود و به هر درخواست اضافه می‌گردد. - -### خطاها - -```tsx -try { - await api.ai.ask(text, { lessonId }); -} catch (error) { - if (error instanceof ApiError) { - if (error.isRateLimited) { /* ۴۲۹ — خیلی سریع پرسیده */ } - if (error.isProviderDown) { /* ۵۰۳ — سرویسِ AI موقتاً قطع */ } - if (error.isNetwork) { /* اصلاً نرسید یا timeout */ } - } -} -``` - -## چرا این‌قدر ملاحظه در `client.ts` - -هر تکه‌اش برای حالتی‌ست که فقط زیرِ فشار پیش می‌آید: - -| مسئله | اگر نباشد چه می‌شود | -|---|---| -| `timeout` روی هر درخواست | یک بک‌اندِ کند سوکت‌ها را باز نگه می‌دارد؛ مرورگر به سقفِ ۶ درخواستِ همزمان می‌خورد و کلِ اپ قفل می‌شود | -| backoff با **jitter** | هزار کلاینت بعد از یک خطا *همزمان* دوباره می‌زنند و موجِ دوم سرور را می‌خواباند | -| تکرارِ خودکار فقط روی `GET` | تکرارِ `POST` یعنی سفارش یا پاسخِ تکراری | -| `Idempotency-Key` روی ثبتِ پاسخ و سفارش | دابل‌کلیکِ کاربر دو بار XP یا دو سفارش می‌سازد | -| `refresh` تک‌نسخه‌ای | ۲۰ درخواستِ همزمانِ ۴۰۱ ⇒ ۲۰ بار refresh ⇒ سرور همه را throttle می‌کند | -| ادغامِ `GET`های یکسان | دو کامپوننت که با هم یک چیز می‌خواهند، دو برابر بار می‌سازند | -| احترام به هدرِ `Retry-After` | بک‌اند صریح می‌گوید چقدر صبر کن؛ نادیده گرفتنش throttle را طولانی‌تر می‌کند | - -`credentials: 'omit'` هم عمدی‌ست: احرازِ هویت توکنِ Bearer در هدر است و سمتِ -سرور `CORS_ALLOW_CREDENTIALS=False` تنظیم شده. عوضش نکنید. - -## چیزی که هنوز نیست - -- صفحه‌ی ورود در اپ وجود ندارد؛ `api.auth` آماده است ولی UI ندارد. -- `api.home()` ساختارِ واقعیِ درس‌ها را می‌دهد، ولی وصل کردنش به صفحه‌ها - هنوز انجام نشده — این PR عمداً `App.tsx` را دست نزده تا با کارِ در جریان - تصادف نکند. diff --git a/src/api/client.ts b/src/api/client.ts deleted file mode 100644 index 584a1bc..0000000 --- a/src/api/client.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** - * هسته‌ی ارتباط با بک‌اندِ تدریسینو. - * - * این فایل عمداً هیچ پکیجی اضافه نمی‌کند — فقط fetch استاندارد. هر چیزی که - * اینجاست برای یک حالتِ خراب است که در فشار پیش می‌آید، نه برای حالتِ خوب: - * - * • timeout — بدونِ آن، یک بک‌اندِ کند سوکت‌ها را باز نگه می‌دارد تا مرورگر - * به سقفِ ۶ درخواستِ همزمان بخورد و کلِ اپ قفل شود. - * • backoff با jitter — وقتی سرور ۴۲۹/۵۰۳ می‌دهد، تکرارِ فوری فقط بار را - * بیشتر می‌کند. jitter لازم است وگرنه هزار کلاینت *همزمان* دوباره می‌زنند - * و موجِ بعدی از موجِ اول بدتر است. - * • فقط GET خودکار تکرار می‌شود — تکرارِ POST یعنی سفارشِ تکراری. - * • refresh تک‌نسخه‌ای — اگر ۲۰ درخواست با هم ۴۰۱ بگیرند، بدونِ این ۲۰ بار - * refresh صدا زده می‌شود و سرور همه را throttle می‌کند. - * • حذفِ درخواستِ تکراری — دو کامپوننت که همزمان یک GET را می‌خواهند، یک - * درخواست می‌فرستند نه دو تا. - */ - -export interface Tokens { - token: string; - refreshToken: string; -} - -export class ApiError extends Error { - constructor( - readonly status: number, - message: string, - readonly payload: Record = {}, - ) { - super(message); - this.name = 'ApiError'; - } - /** ۴۰۱ — توکن نامعتبر یا منقضی. کاربر باید دوباره وارد شود. */ - get isUnauthorized() { return this.status === 401; } - /** ۴۲۹ — سقفِ نرخ. `retryAfterMs` می‌گوید چقدر صبر کنید. */ - get isRateLimited() { return this.status === 429; } - /** ۵۰۳ — سرویسِ بیرونی (هوش مصنوعی/پیامک) موقتاً در دسترس نیست. */ - get isProviderDown() { return this.status === 503; } - /** درخواست اصلاً به سرور نرسید یا timeout شد. */ - get isNetwork() { return this.status === 0; } - retryAfterMs?: number; -} - -/* ─── تنظیمات ───────────────────────────────────────────────────────────── */ - -export interface ClientOptions { - /** پیش‌فرض از VITE_API_BASE_URL خوانده می‌شود. */ - baseUrl?: string; - /** سقفِ زمانِ هر درخواست. پیش‌فرض ۱۵ ثانیه. */ - timeoutMs?: number; - /** بیشترین تعدادِ تلاشِ دوباره برای GET. پیش‌فرض ۳. */ - maxRetries?: number; - storage?: TokenStorage; - /** وقتی نشست به‌کل باطل شد (refresh هم شکست خورد) صدا زده می‌شود. */ - onSignedOut?: () => void; -} - -export interface TokenStorage { - get(): Tokens | null; - set(t: Tokens | null): void; -} - -const STORAGE_KEY = 'tadrisino.tokens'; - -/** localStorage در حالتِ خصوصیِ مرورگر throw می‌کند؛ هیچ‌کدام نباید اپ را بخواباند. */ -export function localTokenStorage(): TokenStorage { - return { - get() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - return raw ? (JSON.parse(raw) as Tokens) : null; - } catch { - return null; - } - }, - set(t) { - try { - if (t) localStorage.setItem(STORAGE_KEY, JSON.stringify(t)); - else localStorage.removeItem(STORAGE_KEY); - } catch { - /* نادیده */ - } - }, - }; -} - -/** - * بک‌اندِ مشترک. پیش‌فرض است تا کسی که تازه ریپو را clone می‌کند بدونِ هیچ - * تنظیماتی بتواند اپ را اجرا کند و داده‌ی واقعی ببیند. - * برای بک‌اندِ محلی، در فایلِ .env مقدارِ VITE_API_BASE_URL را عوض کنید. - */ -const DEFAULT_API_BASE_URL = 'https://api.math.tadrisino2.ir/api'; - -function resolveBaseUrl(explicit?: string): string { - if (explicit) return explicit.replace(/\/+$/, ''); - const fromEnv = (import.meta as { env?: Record }).env?.VITE_API_BASE_URL; - if (fromEnv) return fromEnv.replace(/\/+$/, ''); - return DEFAULT_API_BASE_URL; -} - -/* ─── کمکی‌ها ───────────────────────────────────────────────────────────── */ - -const RETRIABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]); - -/** full jitter — هزار کلاینت نباید همزمان بیدار شوند. */ -function backoffMs(attempt: number, retryAfterMs?: number): number { - if (retryAfterMs != null) return Math.min(retryAfterMs, 30_000); - const ceiling = Math.min(1000 * 2 ** attempt, 8000); - return Math.random() * ceiling; -} - -function parseRetryAfter(header: string | null): number | undefined { - if (!header) return undefined; - const seconds = Number(header); - if (Number.isFinite(seconds)) return seconds * 1000; - const at = Date.parse(header); - return Number.isNaN(at) ? undefined : Math.max(0, at - Date.now()); -} - -const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -/** برای POSTهایی که چیزی می‌سازند؛ سرور هدرِ Idempotency-Key را می‌پذیرد. */ -function newIdempotencyKey(): string { - const c = globalThis.crypto; - if (c && 'randomUUID' in c) return c.randomUUID(); - return `${Date.now()}-${Math.random().toString(36).slice(2)}`; -} - -export interface RequestOptions { - /** هدرهای اضافه. */ - headers?: Record; - /** برای درخواست‌های کند مثل پرسش از دستیار. */ - timeoutMs?: number; - /** لغو از بیرون (مثلاً کاربر از صفحه رفت). */ - signal?: AbortSignal; - /** POST را هم قابلِ تکرار می‌کند — فقط وقتی Idempotency-Key می‌فرستید. */ - retry?: boolean; - /** کلیدِ یکتا تا تکرارِ درخواست سفارشِ تکراری نسازد. */ - idempotencyKey?: string; -} - -/* ─── کلاینت ────────────────────────────────────────────────────────────── */ - -export function createHttpClient(options: ClientOptions = {}) { - const baseUrl = resolveBaseUrl(options.baseUrl); - const defaultTimeout = options.timeoutMs ?? 15_000; - const maxRetries = options.maxRetries ?? 3; - const storage = options.storage ?? localTokenStorage(); - - /** درخواست‌های GETِ در جریان، تا دو کامپوننت یک چیز را دوبار نگیرند. */ - const inFlight = new Map>(); - /** فقط یک refresh در هر لحظه. */ - let refreshing: Promise | null = null; - - async function doRefresh(): Promise { - const current = storage.get(); - if (!current?.refreshToken) return null; - if (refreshing) return refreshing; - - refreshing = (async () => { - try { - const response = await fetch(`${baseUrl}/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken: current.refreshToken }), - signal: AbortSignal.timeout(defaultTimeout), - }); - if (!response.ok) return null; - const data = (await response.json()) as Tokens; - storage.set({ token: data.token, refreshToken: data.refreshToken }); - return data; - } catch { - return null; - } finally { - // در میکروتسکِ بعدی آزاد می‌شود تا هم‌زمان‌ها همین نتیجه را ببینند. - queueMicrotask(() => { refreshing = null; }); - } - })(); - return refreshing; - } - - async function once( - method: string, - path: string, - body: unknown, - opts: RequestOptions, - allowRefresh: boolean, - ): Promise { - const tokens = storage.get(); - const headers: Record = { Accept: 'application/json', ...opts.headers }; - if (body !== undefined) headers['Content-Type'] = 'application/json'; - if (tokens?.token) headers.Authorization = `Bearer ${tokens.token}`; - if (opts.idempotencyKey) headers['Idempotency-Key'] = opts.idempotencyKey; - - // timeoutِ خودمان + لغوِ بیرونی، هر کدام زودتر رسید. - const timer = AbortSignal.timeout(opts.timeoutMs ?? defaultTimeout); - const signal = opts.signal ? anySignal([timer, opts.signal]) : timer; - - let response: Response; - try { - response = await fetch(`${baseUrl}${path}`, { - method, - headers, - body: body === undefined ? undefined : JSON.stringify(body), - signal, - // احرازِ هویت هدرِ Bearer است، نه کوکی — سمتِ سرور هم عمداً - // CORS_ALLOW_CREDENTIALS=False است. اینجا هم نباید عوض شود. - credentials: 'omit', - }); - } catch (err) { - const aborted = err instanceof DOMException && err.name === 'AbortError'; - throw new ApiError(0, aborted ? 'زمانِ درخواست تمام شد' : 'ارتباط با سرور برقرار نشد'); - } - - if (response.status === 401 && allowRefresh) { - const refreshed = await doRefresh(); - if (refreshed) return once(method, path, body, opts, false); - storage.set(null); - options.onSignedOut?.(); - } - - const text = await response.text(); - let parsed: Record = {}; - if (text) { - try { - parsed = JSON.parse(text) as Record; - } catch { - // سرور HTML داد (۵۰۲ از nginx مثلاً) — پیامِ خام گمراه‌کننده است. - parsed = {}; - } - } - - if (!response.ok) { - const detail = - (parsed.detail as string) ?? - (parsed.error as string) ?? - (response.status === 502 ? 'سرور در دسترس نیست' : `خطای ${response.status}`); - const error = new ApiError(response.status, detail, parsed); - error.retryAfterMs = parseRetryAfter(response.headers.get('Retry-After')); - throw error; - } - return parsed as T; - } - - async function request( - method: string, - path: string, - body?: unknown, - opts: RequestOptions = {}, - ): Promise { - const retriable = opts.retry ?? method === 'GET'; - - let lastError: ApiError | undefined; - for (let attempt = 0; attempt <= (retriable ? maxRetries : 0); attempt++) { - try { - return await once(method, path, body, opts, true); - } catch (err) { - if (!(err instanceof ApiError)) throw err; - lastError = err; - const worthRetrying = err.isNetwork || RETRIABLE_STATUS.has(err.status); - if (!retriable || !worthRetrying || attempt === maxRetries) throw err; - if (opts.signal?.aborted) throw err; - await sleep(backoffMs(attempt, err.retryAfterMs)); - } - } - throw lastError ?? new ApiError(0, 'خطای نامشخص'); - } - - /** GETها با هم ادغام می‌شوند تا رندرِ همزمانِ چند کامپوننت یک درخواست بدهد. */ - function get(path: string, opts: RequestOptions = {}): Promise { - // درخواستِ لغوشدنی را ادغام نمی‌کنیم؛ لغوِ یکی نباید دیگری را بکشد. - if (opts.signal) return request('GET', path, undefined, opts); - - const existing = inFlight.get(path); - if (existing) return existing as Promise; - - const promise = request('GET', path, undefined, opts).finally(() => { - inFlight.delete(path); - }); - inFlight.set(path, promise); - return promise; - } - - return { - baseUrl, - storage, - get, - post: (path: string, body?: unknown, opts?: RequestOptions) => - request('POST', path, body, opts), - patch: (path: string, body?: unknown, opts?: RequestOptions) => - request('PATCH', path, body, opts), - /** POSTی که چیزی می‌سازد: با کلیدِ یکتا، پس تکرارش سفارشِ دوم نمی‌سازد. */ - create: (path: string, body?: unknown, opts: RequestOptions = {}) => - request('POST', path, body, { - ...opts, - retry: true, - idempotencyKey: opts.idempotencyKey ?? newIdempotencyKey(), - }), - }; -} - -export type HttpClient = ReturnType; - -/** AbortSignal.any هنوز همه‌جا نیست؛ این جایگزینِ کوچکش است. */ -function anySignal(signals: AbortSignal[]): AbortSignal { - const anyFn = (AbortSignal as { any?: (s: AbortSignal[]) => AbortSignal }).any; - if (anyFn) return anyFn(signals); - const controller = new AbortController(); - for (const s of signals) { - if (s.aborted) { - controller.abort(s.reason); - break; - } - s.addEventListener('abort', () => controller.abort(s.reason), { once: true }); - } - return controller.signal; -} diff --git a/src/api/http.ts b/src/api/http.ts new file mode 100644 index 0000000..c2be150 --- /dev/null +++ b/src/api/http.ts @@ -0,0 +1,339 @@ +/** + * هسته‌ی ارتباط با بک‌اند. فقط fetch استاندارد، بدونِ پکیج. + * + * هر چیزی که اینجاست برای یک حالتِ خراب است، نه حالتِ خوب: + * • timeout — بک‌اندِ کند نباید سوکت‌ها را تا ابد باز نگه دارد. + * • تکرارِ خودکار فقط برای GET، با backoff و jitter؛ تکرارِ POST یعنی ثبتِ دوباره. + * • refreshِ تک‌نسخه‌ای — ده درخواستِ هم‌زمان با ۴۰۱ یک refresh می‌زنند، نه ده تا. + * • ادغامِ GETهای هم‌زمانِ یکسان. + * • وقتی نشست به‌کل باطل شد، onSignedOut خبر می‌دهد تا اپ به صفحه‌ی ورود برگردد. + */ + +export interface Tokens { + token: string; + refreshToken: string; +} + +export class ApiError extends Error { + retryAfterMs?: number; + + constructor( + readonly status: number, + readonly detail: string, + readonly body: Record = {}, + ) { + super(detail); + this.name = 'ApiError'; + } + + /** کدِ ماشینیِ خطا، اگر سرور داده باشد (مثلاً "no_hearts"). */ + get code(): string | undefined { + return typeof this.body.code === 'string' ? this.body.code : undefined; + } + get isNetwork() { return this.status === 0; } + get isUnauthorized() { return this.status === 401; } + get isRateLimited() { return this.status === 429; } + get isProviderDown() { return this.status === 503; } +} + +export interface TokenStorage { + get(): Tokens | null; + set(tokens: Tokens | null): void; +} + +const STORAGE_KEY = 'tadrisino.tokens'; + +/** localStorage در حالتِ خصوصیِ بعضی مرورگرها throw می‌کند؛ اپ نباید بخوابد. */ +export const localTokenStorage: TokenStorage = { + get() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? (JSON.parse(raw) as Tokens) : null; + } catch { + return null; + } + }, + set(tokens) { + try { + if (tokens) localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens)); + else localStorage.removeItem(STORAGE_KEY); + } catch { + /* ذخیره نشد؛ نشست فقط تا بستنِ صفحه می‌ماند */ + } + }, +}; + +export interface RequestOptions { + timeoutMs?: number; + signal?: AbortSignal; + /** POST را هم تکرارپذیر می‌کند؛ فقط همراهِ idempotencyKey معنا دارد. */ + retry?: boolean; + idempotencyKey?: string; +} + +export interface HttpClientOptions { + baseUrl: string; + storage?: TokenStorage; + timeoutMs?: number; + maxRetries?: number; +} + +const RETRIABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]); + +function backoffMs(attempt: number, retryAfterMs?: number): number { + if (retryAfterMs != null) return Math.min(retryAfterMs, 30_000); + return Math.random() * Math.min(1000 * 2 ** attempt, 8000); +} + +function parseRetryAfter(header: string | null): number | undefined { + if (!header) return undefined; + const seconds = Number(header); + if (Number.isFinite(seconds)) return seconds * 1000; + const at = Date.parse(header); + return Number.isNaN(at) ? undefined : Math.max(0, at - Date.now()); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function newIdempotencyKey(): string { + const c = globalThis.crypto; + if (c && 'randomUUID' in c) return c.randomUUID(); + return `${Date.now()}-${Math.random().toString(36).slice(2)}`; +} + +/** AbortSignal.any هنوز همه‌جا نیست؛ جایگزینِ کوچکش. */ +function anySignal(signals: AbortSignal[]): AbortSignal { + const native = (AbortSignal as { any?: (s: AbortSignal[]) => AbortSignal }).any; + if (native) return native(signals); + const controller = new AbortController(); + for (const s of signals) { + if (s.aborted) { + controller.abort(s.reason); + break; + } + s.addEventListener('abort', () => controller.abort(s.reason), { once: true }); + } + return controller.signal; +} + +function timeoutSignal(ms: number): AbortSignal { + if ('timeout' in AbortSignal) return AbortSignal.timeout(ms); + const controller = new AbortController(); + setTimeout(() => controller.abort(), ms); + return controller.signal; +} + +async function parseBody(response: Response): Promise> { + const text = await response.text(); + if (!text) return {}; + try { + const parsed: unknown = JSON.parse(text); + return parsed && typeof parsed === 'object' ? (parsed as Record) : {}; + } catch { + // HTML به‌جای JSON: معمولاً ۵۰۲ِ nginx، یا آدرسِ API اشتباه است و + // سرورِ فرانت index.html را برگردانده. + return {}; + } +} + +function errorFrom(response: Response, body: Record): ApiError { + const detail = + (typeof body.detail === 'string' && body.detail) || + (typeof body.error === 'string' && body.error) || + firstFieldError(body) || + (response.status >= 500 ? 'سرور موقتاً در دسترس نیست' : `خطای ${response.status}`); + const error = new ApiError(response.status, detail, body); + error.retryAfterMs = parseRetryAfter(response.headers.get('Retry-After')); + return error; +} + +/** خطای اعتبارسنجیِ DRF به شکلِ {field: ["پیام"]} است. */ +function firstFieldError(body: Record): string | undefined { + for (const value of Object.values(body)) { + if (Array.isArray(value) && typeof value[0] === 'string') return value[0]; + } + return undefined; +} + +export function createHttpClient(options: HttpClientOptions) { + const baseUrl = options.baseUrl.replace(/\/+$/, ''); + const storage = options.storage ?? localTokenStorage; + const defaultTimeout = options.timeoutMs ?? 15_000; + const maxRetries = options.maxRetries ?? 2; + + const inFlight = new Map>(); + const signedOutListeners = new Set<() => void>(); + let refreshing: Promise | null = null; + + function authHeaders(): Record { + const token = storage.get()?.token; + return token ? { Authorization: `Bearer ${token}` } : {}; + } + + function signOut() { + storage.set(null); + signedOutListeners.forEach((listener) => listener()); + } + + async function refreshTokens(): Promise { + const current = storage.get(); + if (!current?.refreshToken) return null; + if (refreshing) return refreshing; + + refreshing = (async () => { + try { + const response = await fetch(`${baseUrl}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: current.refreshToken }), + signal: timeoutSignal(defaultTimeout), + credentials: 'omit', + }); + if (!response.ok) return null; + const data = (await response.json()) as Tokens; + const next = { token: data.token, refreshToken: data.refreshToken || current.refreshToken }; + storage.set(next); + return next; + } catch { + return null; + } finally { + queueMicrotask(() => { + refreshing = null; + }); + } + })(); + return refreshing; + } + + async function send( + method: string, + path: string, + body: unknown, + opts: RequestOptions, + ): Promise { + const headers: Record = { Accept: 'application/json', ...authHeaders() }; + if (body !== undefined) headers['Content-Type'] = 'application/json'; + if (opts.idempotencyKey) headers['Idempotency-Key'] = opts.idempotencyKey; + + const timer = timeoutSignal(opts.timeoutMs ?? defaultTimeout); + try { + return await fetch(`${baseUrl}${path}`, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + signal: opts.signal ? anySignal([timer, opts.signal]) : timer, + // احرازِ هویت هدرِ Bearer است، نه کوکی؛ سرور هم CORS را بدونِ credentials دارد. + credentials: 'omit', + }); + } catch (err) { + if (opts.signal?.aborted) throw err; + const timedOut = err instanceof DOMException && (err.name === 'TimeoutError' || err.name === 'AbortError'); + throw new ApiError(0, timedOut ? 'زمانِ درخواست تمام شد' : 'ارتباط با سرور برقرار نشد'); + } + } + + /** یک درخواست، با یک‌بار refresh در صورتِ ۴۰۱. */ + async function sendWithRefresh( + method: string, + path: string, + body: unknown, + opts: RequestOptions, + ): Promise { + const hadToken = Boolean(storage.get()?.token); + let response = await send(method, path, body, opts); + if (response.status === 401 && hadToken) { + const refreshed = await refreshTokens(); + if (refreshed) response = await send(method, path, body, opts); + else signOut(); + } + return response; + } + + async function request(method: string, path: string, body?: unknown, opts: RequestOptions = {}): Promise { + const retriable = opts.retry ?? method === 'GET'; + for (let attempt = 0; ; attempt++) { + try { + const response = await sendWithRefresh(method, path, body, opts); + const parsed = await parseBody(response); + if (!response.ok) throw errorFrom(response, parsed); + return parsed as T; + } catch (err) { + if (!(err instanceof ApiError)) throw err; + const worthRetrying = err.isNetwork || RETRIABLE_STATUS.has(err.status); + if (!retriable || !worthRetrying || attempt >= maxRetries || opts.signal?.aborted) throw err; + await sleep(backoffMs(attempt, err.retryAfterMs)); + } + } + } + + function get(path: string, opts: RequestOptions = {}): Promise { + // درخواستِ لغوشدنی ادغام نمی‌شود؛ لغوِ یکی نباید دیگری را بکشد. + if (opts.signal) return request('GET', path, undefined, opts); + const existing = inFlight.get(path); + if (existing) return existing as Promise; + const promise = request('GET', path, undefined, opts).finally(() => inFlight.delete(path)); + inFlight.set(path, promise); + return promise; + } + + /** + * درخواستِ Server-Sent Events (پاسخِ تکه‌تکه‌ی دستیار). + * رویدادها با خطِ خالی جدا می‌شوند: `event: <نام>\ndata: `. + */ + async function stream( + path: string, + body: unknown, + onEvent: (event: string, data: Record) => void, + opts: RequestOptions = {}, + ): Promise { + const response = await sendWithRefresh('POST', path, body, { timeoutMs: 120_000, ...opts }); + if (!response.ok || !response.body) { + throw errorFrom(response, await parseBody(response)); + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split('\n\n'); + buffer = parts.pop() ?? ''; + for (const part of parts) { + const event = /^event: (.+)$/m.exec(part)?.[1]; + const data = /^data: (.+)$/m.exec(part)?.[1]; + if (!event || !data) continue; + try { + onEvent(event, JSON.parse(data) as Record); + } catch (err) { + if (err instanceof ApiError) throw err; + /* تکه‌ی ناقص؛ نادیده */ + } + } + } + } + + return { + baseUrl, + storage, + get, + post: (path: string, body?: unknown, opts?: RequestOptions) => request('POST', path, body, opts), + patch: (path: string, body?: unknown, opts?: RequestOptions) => request('PATCH', path, body, opts), + /** POSTی که چیزی ثبت می‌کند: با کلیدِ یکتا، پس تکرارش دوبار ثبت نمی‌کند. */ + create: (path: string, body?: unknown, opts: RequestOptions = {}) => + request('POST', path, body, { + ...opts, + retry: true, + idempotencyKey: opts.idempotencyKey ?? newIdempotencyKey(), + }), + stream, + onSignedOut(listener: () => void) { + signedOutListeners.add(listener); + return () => { + signedOutListeners.delete(listener); + }; + }, + }; +} + +export type HttpClient = ReturnType; diff --git a/src/api/index.ts b/src/api/index.ts index f1b7da8..3904512 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,459 +1,184 @@ /** - * کلاینتِ API تدریسینو — برای فرانتِ React/TypeScript (MathApp_frontend). + * تنها درِ ارتباط با بک‌اند. هیچ جای دیگرِ اپ fetch نمی‌زند. * - * از روی OpenAPI واقعیِ همین بک‌اند نوشته شده، نه از روی حدس. نامِ فیلدها - * دقیقاً همان چیزی‌ست که سرور برمی‌گرداند (camelCase). - * - * نکته‌های مهمِ اتصال: - * - احرازِ هویت توکنِ Bearer است، نه کوکی. پس `credentials: 'include'` لازم - * نیست و CORS سمتِ سرور هم عمداً بدونِ credentials تنظیم شده. - * - توکنِ دسترسی امضاشده و کوتاه‌عمر است؛ با ۴۰۱ باید `refresh` صدا زد. - * این کلاینت خودش یک‌بار refresh می‌کند و درخواست را تکرار. - * - برای پرسش از دستیار کافی‌ست `lessonId` بدهید؛ سرور خودش می‌فهمد آن درس - * مالِ کدام پایه و فصل است و پاسخ را به کتاب مستند می‌کند. + * هر تابع یک اندپوینت است و نامِ فیلدها عیناً همان نامِ سرور است. */ +import { resolveApiBaseUrl } from '@/config'; +import { ApiError, createHttpClient, newIdempotencyKey } from './http'; +import type * as T from './types'; -export type Json = Record; +export { ApiError }; +export type * from './types'; -export interface Tokens { - token: string; - refreshToken: string; +export const http = createHttpClient({ baseUrl: resolveApiBaseUrl() }); + +const q = (params: Record) => { + const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== ''); + return entries.length ? `?${new URLSearchParams(entries.map(([k, v]) => [k, String(v)]))}` : ''; +}; + +function saveTokens(data: T.AuthResponse) { + http.storage.set({ token: data.token, refreshToken: data.refreshToken ?? '' }); + return data; } -/* ─── انواعِ پاسخ ───────────────────────────────────────────────────────── */ +export const api = { + auth: { + isSignedIn: () => Boolean(http.storage.get()?.token), -export type NodeStatus = 'locked' | 'unlocked' | 'in_progress' | 'completed'; + /** مهمان: فقط با شناسه‌ی دستگاه. refreshToken هم می‌گیرد، پس بعد از یک ساعت نمی‌افتد. */ + guest: (deviceId: string) => + http.post('/auth/session', { type: 'guest', deviceId }).then(saveTokens), -export interface HomeNode { - id: string; - title: string; - status: NodeStatus; - lessonIds: number[]; -} -export interface HomeUnit { - id: string; - title: string; - nodes: HomeNode[]; -} -export interface HomeResponse { - units: HomeUnit[]; - stats: { progressPercent: number; xp: number; coins: number }; -} + /** گامِ ۱: پیامکِ کد. `devOtp` فقط وقتی سرور در حالتِ توسعه است برمی‌گردد. */ + requestOtp: (phone: string) => + http.post<{ success: boolean; devOtp?: string }>('/auth/otp/request', { phone }), -export interface Grade { - id: number; - title: string; - icon: string; - color: string; -} + /** + * گامِ ۲: تأییدِ کد. اگر الان مهمان باشد، سرور همان کاربر را نگه می‌دارد و + * پیشرفتش از دست نمی‌رود (توکنِ مهمان همراهِ همین درخواست می‌رود). + */ + login: (phone: string, otp: string) => + http.post('/auth/login', { phone, otp }).then(saveTokens), -export interface UserSummary { - userId: string; - name: string; - avatar: string; - gradeId: number | null; - xp: number; - coins: number; - streak: { current: number; longest: number }; - subscription: string; -} - -export interface UserProfile { - userId: string; - phone: string | null; - name: string; - avatar: string; - isGuest: boolean; - gradeId: number | null; - birthYear: number | null; - parentPhone: string; - bio: string; -} - -export interface LessonIntro { - lessonId: string; - title: string; - introText: string; - /** برای انتخابِ حالتِ ماسکوت در صفحه‌ی درس */ - mascotAnimation: string; - /** - * صفر یعنی متنِ درس هست ولی آزمونش هنوز ساخته نشده — «به‌زودی» نشان دهید، - * نه کویزِ خالی. `complete` هم `totalQuestions: 0` را نمی‌پذیرد. - */ - questionCount: number; -} - -export interface QuestionOption { - id: string; - value: string; -} -export interface Question { - id: string; - type: 'multiple_choice' | 'count_objects'; - prompt: string; - options: QuestionOption[]; -} - -export interface AnswerResult { - correct: boolean; - correctOptionId: string; - xpEarned: number; - heartsRemaining: number; - /** حالتِ ماسکوت: درست → happy، غلط → encourage */ - mascotMood: string; -} - -export interface LessonComplete { - stars: number; - xpEarned: number; - coinsEarned: number; - stats: { progressPercent: number; xp: number; coins: number }; -} - -export interface Hearts { - currentHearts: number; - maxHearts: number; - nextRefillAt: string | null; - 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; - answer: string; - suggestedFollowUps: string[]; - remainingQuota: number; - /** تکه‌های کتاب که پاسخ رویشان بنا شده؛ خالی یعنی پاسخِ عمومی */ - usedChunks: { title: string; summary: string }[]; -} - -export interface Plan { - id: string; - title: string; - price: number; - durationDays: number; - features: string[]; -} - -/* ─── خطا ───────────────────────────────────────────────────────────────── */ - -export class ApiError extends Error { - constructor( - readonly status: number, - readonly detail: string, - readonly body: Json, - ) { - super(detail); - this.name = 'ApiError'; - } - - /** ۴۲۹: throttle یا اتمامِ سهمیه‌ی روزانه‌ی دستیار */ - get isRateLimited() { - return this.status === 429; - } - /** ۵۰۳: سرویسِ هوش مصنوعی/پیامک موقتاً در دسترس نیست */ - get isProviderDown() { - return this.status === 503; - } -} - -/* ─── کلاینت ────────────────────────────────────────────────────────────── */ - -export interface ClientOptions { - baseUrl?: string; - /** خواندن/نوشتنِ توکن؛ پیش‌فرض localStorage */ - storage?: { - get(): Tokens | null; - set(t: Tokens | null): void; - }; -} - -const STORAGE_KEY = 'tadrisino.tokens'; - -function defaultStorage() { - return { - get(): Tokens | null { + /** خروج در سرور (باطل کردنِ refresh token) و بعد پاک کردنِ حافظه. */ + async logout() { + const refreshToken = http.storage.get()?.refreshToken; try { - const raw = localStorage.getItem(STORAGE_KEY); - return raw ? (JSON.parse(raw) as Tokens) : null; - } catch { - return null; + await http.post('/auth/logout', refreshToken ? { refreshToken } : {}); + } finally { + http.storage.set(null); } }, - set(t: Tokens | null) { - try { - if (t) localStorage.setItem(STORAGE_KEY, JSON.stringify(t)); - else localStorage.removeItem(STORAGE_KEY); - } catch { - /* حالتِ خصوصیِ مرورگر — نادیده */ - } - }, - }; -} + }, -export function createClient(options: ClientOptions = {}) { - const baseUrl = (options.baseUrl ?? '/api').replace(/\/$/, ''); - const storage = options.storage ?? defaultStorage(); + grades: () => http.get<{ grades: T.Grade[] }>('/grades').then((r) => r.grades), - async function raw( - method: string, - path: string, - body?: unknown, - extraHeaders: Record = {}, - retryOn401 = true, - ): Promise { - const tokens = storage.get(); - const headers: Record = { ...extraHeaders }; - if (body !== undefined) headers['Content-Type'] = 'application/json'; - if (tokens?.token) headers['Authorization'] = `Bearer ${tokens.token}`; + profile: { + get: () => http.get('/user/profile'), + update: (patch: Partial>) => + http.patch('/user/profile', patch), + setGrade: (gradeId: number) => + http.post<{ success: boolean; gradeId: number }>('/user/grade', { gradeId }), + }, - const response = await fetch(`${baseUrl}${path}`, { - method, - headers, - body: body === undefined ? undefined : JSON.stringify(body), - }); + summary: () => http.get('/user/summary'), + hearts: () => http.get('/user/hearts'), + streakCalendar: () => http.get('/user/streak/calendar'), - if (response.status === 401 && retryOn401 && tokens?.refreshToken) { - const refreshed = await refresh(tokens.refreshToken).catch(() => null); - if (refreshed) return raw(method, path, body, extraHeaders, false); - storage.set(null); - } + /** بدونِ grade، پایه‌ی ذخیره‌شده‌ی خودِ کاربر ملاک است. */ + home: () => http.get('/home'), + nodeLessons: (nodeId: string) => http.get(`/nodes/${nodeId}/lessons`), - const text = await response.text(); - const parsed: Json = text ? (JSON.parse(text) as Json) : {}; - if (!response.ok) { - const detail = - (parsed.detail as string) ?? - (parsed.error as string) ?? - `خطای ${response.status}`; - throw new ApiError(response.status, detail, parsed); - } - return parsed as T; - } - - async function refresh(refreshToken: string) { - const response = await fetch(`${baseUrl}/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken }), - }); - if (!response.ok) return null; - const data = (await response.json()) as Tokens; - storage.set({ token: data.token, refreshToken: data.refreshToken }); - return data; - } - - return { - /* ── ورود (صفحه‌ای که فعلاً در فرانت نیست و باید ساخته شود) ────────── */ - auth: { - /** مهمان: بدونِ شماره، فقط با شناسه‌ی دستگاه. برای «شروع سریع». */ - async guest(deviceId: string) { - const data = await raw<{ userId: string; token: string; isNewUser: boolean; isGuest: boolean }>( - 'POST', '/auth/session', { type: 'guest', deviceId }, - ); - storage.set({ token: data.token, refreshToken: '' }); - return data; - }, - /** گامِ ۱: ارسالِ پیامکِ کد. در حالتِ توسعه `devOtp` هم برمی‌گردد. */ - requestOtp(phone: string) { - return raw<{ success: boolean; devOtp?: string; note?: string }>( - 'POST', '/auth/otp/request', { phone }, - ); - }, - /** گامِ ۲: تأییدِ کد و گرفتنِ توکن. نامِ فیلد در سرور `otp` است. */ - async login(phone: string, otp: string) { - const data = await raw<{ userId: string } & Tokens>( - 'POST', '/auth/login', { phone, otp }, - ); - storage.set({ token: data.token, refreshToken: data.refreshToken }); - return data; - }, - logout() { - storage.set(null); - }, - isSignedIn() { - return Boolean(storage.get()?.token); - }, - }, - - /* ── صفحه‌ی «بیا باهم آشنا بشیم» ──────────────────────────────────── */ - profile: { - get: () => raw('GET', '/user/profile'), - /** اسم/آواتار/… . فقط فیلدهایی که می‌فرستید عوض می‌شوند. */ - update: (patch: Partial>) => - raw('PATCH', '/user/profile', patch), - setGrade: (gradeId: number) => - raw<{ success: boolean; gradeId: number }>('POST', '/user/grade', { gradeId }), - }, - - /** فهرستِ پایه‌ها را از سرور بگیرید، هاردکد نکنید — پایه‌ها اضافه می‌شوند. */ - grades: () => raw<{ grades: Grade[] }>('GET', '/grades').then((r) => r.grades), - - /* ── صفحه‌ی خانه و نقشه‌ی مهارت ────────────────────────────────────── */ - /** بدونِ gradeId، پایه‌ی ذخیره‌شده‌ی خودِ کاربر ملاک است. */ - home: (gradeId?: number) => - raw('GET', gradeId ? `/home?grade=${gradeId}` : '/home'), - summary: () => raw('GET', '/user/summary'), - hearts: () => raw('GET', '/user/hearts'), - streakCalendar: () => - raw<{ currentStreak: number; longestStreak: number; activeDays: string[] }>( - 'GET', '/user/streak/calendar', + lesson: { + intro: (lessonId: number) => http.get(`/lessons/${lessonId}/intro`), + questions: (lessonId: number) => + http.get<{ questions: T.Question[] }>(`/lessons/${lessonId}/questions`).then((r) => r.questions), + /** + * کلیدِ یکتا را یک‌بار برای هر جواب بسازید و در تکرار همان را بفرستید تا + * امتیاز و قلب دوبار حساب نشود. + */ + answer: (questionId: string, selectedOptionId: string, idempotencyKey = newIdempotencyKey()) => + http.post( + `/questions/${questionId}/answer`, + { selectedOptionId: Number(selectedOptionId) }, + { idempotencyKey, retry: true }, ), - leaderboard: (period: 'weekly' | 'all_time' = 'weekly') => - raw<{ period: string; entries: unknown[] }>('GET', `/leaderboard?period=${period}`), + /** مجموعِ XP و سکه‌ی کاربر اینجا به‌روز می‌شود، نه سرِ هر جواب. */ + complete: (lessonId: number, result: { correctCount: number; totalQuestions: number }, idempotencyKey?: string) => + http.create(`/lessons/${lessonId}/complete`, result, { idempotencyKey }), + report: (questionId: string, reason: T.ReportReason, note?: string) => + http.post<{ success: boolean; reportId: number; alreadyReported: boolean }>( + `/questions/${questionId}/report`, + { reason, ...(note ? { note } : {}) }, + ), + }, - /** کاتالوگِ کاراکترهای گیمیفیکیشن — مثلِ ماسکات‌های 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 }), + leaderboard: (period: T.LeaderboardPeriod) => + http.get<{ period: string; entries: T.LeaderboardEntry[] }>(`/leaderboard${q({ period })}`).then((r) => r.entries), + + characters: { + list: () => http.get<{ characters: T.Character[] }>('/characters').then((r) => r.characters), + select: (characterId: string) => + http.post<{ characterId: string; selected: boolean }>('/characters/select', { characterId }), + }, + + ai: { + /** + * پرسش با پاسخِ تکه‌تکه. `lessonId` پاسخ را به کتابِ همان درس مستند می‌کند؛ + * `questionId` به دستیار می‌گوید دانش‌آموز کدام سوال را غلط زده. + */ + async ask( + input: { userQuestion: string; conversationId?: string; lessonId?: number; questionId?: string }, + onDelta: (text: string) => void, + signal?: AbortSignal, + ): Promise { + let done: T.AskDone | null = null; + await http.stream( + '/ai/ask', + { ...input, lessonId: input.lessonId?.toString(), stream: true }, + (event, data) => { + if (event === 'chunk') onDelta(String(data.delta ?? '')); + else if (event === 'done') done = data as unknown as T.AskDone; + else if (event === 'error') throw new ApiError(503, String(data.detail ?? 'دستیار در دسترس نیست'), data); + }, + { signal }, + ); + if (!done) throw new ApiError(0, 'پاسخ کامل نرسید'); + return done; }, + feedback: (messageId: string, isHelpful: boolean, reason?: string) => + http.post<{ success: boolean }>(`/ai/messages/${messageId}/feedback`, { + isHelpful, + ...(reason ? { reason } : {}), + }), + history: (conversationId: string) => + http.get<{ conversationId: string; lessonId: string | null; messages: T.ConversationMessage[] }>( + `/ai/conversations/${conversationId}/messages`, + ), + }, - /* ── صفحه‌ی درس و سوال ─────────────────────────────────────────────── */ - lesson: { - intro: (lessonId: number) => raw('GET', `/lessons/${lessonId}/intro`), - questions: (lessonId: number) => - raw<{ questions: Question[] }>('GET', `/lessons/${lessonId}/questions`).then((r) => r.questions), - /** - * `idempotencyKey` را بدهید تا اگر شبکه قطع شد و کاربر دوباره زد، - * امتیاز/قلب دوبار حساب نشود. - */ - answer: (questionId: number, selectedOptionId: number, idempotencyKey?: string) => - raw( - 'POST', `/questions/${questionId}/answer`, { selectedOptionId }, - idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}, - ), - /** - * پایانِ درس. `correctCount` را سرور با تعدادِ واقعیِ سوال‌ها سقف می‌زند. - * مجموعِ XPِ کاربر **اینجا** به‌روز می‌شود، نه سرِ هر پاسخ. - */ - complete: ( - lessonId: number, - result: { correctCount: number; totalQuestions: number }, - idempotencyKey?: string, - ) => - raw( - 'POST', `/lessons/${lessonId}/complete`, result, - idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}, - ), - }, + notifications: { + list: () => http.get<{ items: T.NotificationItem[]; unreadCount: number }>('/notifications'), + markRead: (id: number) => http.patch(`/notifications/${id}/read`), + markAllRead: () => http.patch('/notifications/read-all'), + }, - /* ── صفحه‌ی «از رُبو بپرس» ─────────────────────────────────────────── */ - ai: { - /** - * `lessonId` را بدهید تا پاسخ به کتابِ همان درس مستند شود؛ لازم نیست - * subject/grade/chapter را خودتان حساب کنید. - * `conversationId` را از پاسخِ قبلی پاس بدهید تا گفتگو ادامه پیدا کند. - */ - ask: (input: { - userQuestion: string; - lessonId?: number | string; - conversationId?: string; - questionId?: number | string; - idempotencyKey?: string; - }) => { - const { idempotencyKey, ...body } = input; - return raw( - 'POST', '/ai/ask', body, - idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}, - ); - }, + subscription: { + plans: () => http.get<{ plans: T.Plan[] }>('/subscription/plans').then((r) => r.plans), + status: () => http.get('/subscription/status'), + /** سفارش می‌سازد؛ paymentUrl خودش سراغِ درگاه می‌رود. */ + purchase: (planId: string) => http.create('/subscription/purchase', { planId }), + /** کدِ تخفیف باید پیش از رفتن به درگاه ثبت شود؛ هر سفارش فقط یک کد. */ + redeem: (code: string, orderId: string) => + http.post('/discount/redeem', { code, orderId }), + order: (orderId: string) => http.get(`/payment/orders/${orderId}`), + }, - /** - * همان ask ولی توکن‌به‌توکن (SSE). اولین کلمه ~۳.۵ ثانیه به‌جای ~۱۰. - * برای حبابِ چتِ «در حالِ تایپ» همین را استفاده کنید. - */ - async askStream( - input: { userQuestion: string; lessonId?: number | string; conversationId?: string }, - onDelta: (text: string) => void, - ): Promise<{ conversationId?: string; messageId?: string }> { - const tokens = storage.get(); - const response = await fetch(`${baseUrl}/ai/ask`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(tokens?.token ? { Authorization: `Bearer ${tokens.token}` } : {}), - }, - body: JSON.stringify({ ...input, stream: true }), - }); - if (!response.ok || !response.body) { - throw new ApiError(response.status, 'استریم شروع نشد', {}); - } + support: { + createTicket: (subject: string, category: string) => + http.create<{ ticketId: string; status: string }>('/support/tickets', { subject, category }), + ticket: (ticketId: string) => http.get(`/support/tickets/${ticketId}`), + reply: (ticketId: string, text: string) => + http.create<{ messageId: string; success: boolean }>(`/support/tickets/${ticketId}/messages`, { text }), + bugReport: (title: string, description: string) => + http.create<{ bugReportId: string; status: string }>('/support/bug-reports', { title, description }), + }, - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - let done: { conversationId?: string; messageId?: string } = {}; + family: { + link: () => http.get('/family/link'), + invite: (childPhone: string) => + http.post<{ familyLinkId: string; inviteCode: string; status: string }>('/family/link/invite', { childPhone }), + accept: (inviteCode: string) => + http.post<{ familyLinkId: string; status: string }>('/family/link/accept', { inviteCode }), + weeklyReport: () => http.get('/family/weekly-report'), + settings: () => http.get('/family/settings'), + updateSettings: (patch: Partial) => http.patch('/family/settings', patch), + }, - for (;;) { - const { value, done: finished } = await reader.read(); - if (finished) break; - buffer += decoder.decode(value, { stream: true }); - - // رویدادها با یک خطِ خالی از هم جدا می‌شوند. - const parts = buffer.split('\n\n'); - buffer = parts.pop() ?? ''; - for (const part of parts) { - const event = /^event: (.+)$/m.exec(part)?.[1]; - const dataLine = /^data: (.+)$/m.exec(part)?.[1]; - if (!event || !dataLine) continue; - const payload = JSON.parse(dataLine) as Json; - if (event === 'chunk') onDelta(String(payload.delta ?? '')); - else if (event === 'done') done = payload as typeof done; - else if (event === 'error') { - throw new ApiError(503, String(payload.detail ?? 'خطای دستیار'), payload); - } - } - } - return done; - }, - - /** 👍/👎 روی یک پاسخ. `reason` اختیاری ولی خیلی مفید است. */ - feedback: (messageId: string | number, isHelpful: boolean, reason?: string) => - raw<{ success: boolean }>('POST', `/ai/messages/${messageId}/feedback`, { - isHelpful, - ...(reason ? { reason } : {}), - }), - - history: (conversationId: string | number) => - raw<{ conversationId: string; lessonId: string | null; messages: unknown[] }>( - 'GET', `/ai/conversations/${conversationId}/messages`, - ), - }, - - /* ── صفحه‌ی اشتراک ─────────────────────────────────────────────────── */ - subscription: { - plans: () => raw<{ plans: Plan[] }>('GET', '/subscription/plans').then((r) => r.plans), - status: () => - raw<{ active: boolean; planId: string | null; expiresAt: string | null }>( - 'GET', '/subscription/status', - ), - /** - * `paymentUrl` را در مرورگر باز کنید. آن آدرس خودش سراغِ زرین‌پال - * می‌رود؛ اپ نباید مستقیم با درگاه حرف بزند. - */ - purchase: (planId: string, idempotencyKey?: string) => - raw<{ orderId: string; paymentUrl: string }>( - 'POST', '/subscription/purchase', { planId }, - idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}, - ), - }, - - notifications: { - list: () => raw<{ items: unknown[]; unreadCount: number }>('GET', '/notifications'), - markRead: (id: number) => raw('POST', `/notifications/${id}/read`), - }, - }; -} - -export type TadrisinoClient = ReturnType; + affiliate: { + me: () => http.get('/affiliate/me'), + /** کاربرِ تازه (با شماره، نه مهمان) را به صاحبِ کدِ معرف وصل می‌کند. */ + track: (referralCode: string) => http.post<{ success: boolean }>('/affiliate/track', { referralCode }), + }, +}; diff --git a/src/api/types.ts b/src/api/types.ts index 904355d..ab4c75a 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,24 +1,72 @@ /** - * تایپ‌های پاسخِ بک‌اند — مستقیم از OpenAPI واقعیِ سرویس، نه از حدس. + * شکلِ پاسخ‌های بک‌اند. * - * دو نکته که موقعِ نوشتن غافلگیرکننده بودند و اگر رعایت نشوند درخواست‌ها - * بی‌سروصدا شکست می‌خورند: - * • شناسه‌ها در پاسخ‌ها اغلب رشته‌اند، نه عدد. - * • فیلدِ پرسش در /ai/ask اسمش userQuestion است، نه question. + * از روی پاسخِ واقعیِ سرور ثبت شده (۲۰۲۶-۰۹-۲۶)، نه از حدس. قراردادِ کامل در + * /api/docs/ همان بک‌اند است؛ اگر فیلدی آنجا عوض شد، اینجا هم عوض کنید. + * + * دو نکته که غافلگیر می‌کنند: + * • شناسه‌ها در پاسخ‌ها رشته‌اند، ولی در بدنه‌ی درخواست گاهی عدد خواسته می‌شوند. + * • birthYear سالِ شمسی است (سرور بیشتر از ۱۵۰۰ را رد می‌کند). */ -export type NodeStatusValue = 'locked' | 'unlocked' | 'in_progress' | 'completed'; +export interface AuthResponse { + userId: string; + token: string; + refreshToken: string; + isNewUser?: boolean; + isGuest?: boolean; +} + +export interface Grade { + id: number; + number: number | null; + title: string; + icon: string; + color: string; + lessonCount: number; +} + +export interface UserProfile { + userId: string; + phone: string | null; + name: string; + avatar: string; + isGuest: boolean; + gradeId: number | null; + birthYear: number | null; + parentPhone: string; + bio: string; +} + +export interface UserSummary { + userId: string; + name: string; + avatar: string; + gradeId: number | null; + xp: number; + coins: number; + streak: { current: number; longest: number }; + subscription: string | null; +} + +export type NodeStatus = 'locked' | 'unlocked' | 'in_progress' | 'completed'; export interface HomeNode { id: string; title: string; - status: NodeStatusValue; + /** نامِ آیکنِ lucide (مثلاً "Grid3x3")؛ ممکن است خالی باشد. */ + icon: string; + color: string; + status: NodeStatus; + /** درس‌های سوال‌دارِ این جزیره؛ اولی همیشه درسِ بعدی است. */ lessonIds: number[]; + nextLessonId: number | null; } export interface HomeUnit { id: string; title: string; + blurb: string; nodes: HomeNode[]; } @@ -33,24 +81,18 @@ export interface HomeResponse { stats: HomeStats; } -export interface Grade { - id: number; +export interface NodeLesson { + id: string; + kind: 'quiz' | 'video'; title: string; - icon: string; - color: string; + meta: string; + done: boolean; } -/** آمارِ بازی (xp/سکه/استریک) اینجا نیست — از /user/summary می‌آید. */ -export interface UserProfile { - userId: string; - phone: string; - name: string; - avatar: string; - isGuest: boolean; - gradeId: number | null; - birthYear: number | null; - parentPhone: string; - bio: string; +export interface NodeLessons { + nodeId: string; + title: string; + lessons: NodeLesson[]; } export interface LessonIntro { @@ -58,10 +100,16 @@ export interface LessonIntro { title: string; introText: string; mascotAnimation: string; + /** صفر یعنی متنِ درس هست ولی سوالش هنوز ساخته نشده. */ questionCount: number; + kind: 'quiz' | 'video'; + example: string; + steps: string[]; + videoUrl: string; + durationMinutes: number | null; } -export interface ChoiceOption { +export interface QuestionOption { id: string; value: string; } @@ -70,11 +118,10 @@ export interface Question { id: string; type: string; prompt: string; - options: ChoiceOption[]; + options: QuestionOption[]; } -/** `mascotMood`: پاسخِ درست → happy، غلط → encourage. */ -export interface AnswerResponse { +export interface AnswerResult { correct: boolean; correctOptionId: string; xpEarned: number; @@ -82,41 +129,147 @@ export interface AnswerResponse { mascotMood: string; } +export interface LessonComplete { + stars: number; + xpEarned: number; + coinsEarned: number; + stats: HomeStats; +} + +export type ReportReason = 'wrong_answer' | 'unclear' | 'typo' | 'too_hard' | 'other'; + +export interface Hearts { + currentHearts: number; + maxHearts: number; + nextRefillAt: string | null; + fullRefillAt: string | null; + isUnlimited: boolean; +} + +export interface StreakCalendar { + currentStreak: number; + longestStreak: number; + /** تاریخ‌های میلادی به شکلِ YYYY-MM-DD */ + activeDays: string[]; +} + +export type LeaderboardPeriod = 'weekly' | 'all_time'; + +export interface LeaderboardEntry { + rank: number; + userId: string; + name: string; + xpInPeriod: number; +} + +export interface Character { + characterId: string; + slug: string; + name: string; + icon: string; + unlockXp: number; + unlocked: boolean; + selected: boolean; +} + export interface UsedChunk { title: string; summary: string; } -/** - * `usedChunks` خالی یعنی پاسخ از کتاب پیدا نشده و عمومی است — برای نشان دادنِ - * برچسبِ «بر اساسِ کتاب» در برابرِ «پاسخِ عمومی» همین کافی‌ست. - */ -export interface AskResponse { +/** رویدادِ پایانیِ استریمِ دستیار. */ +export interface AskDone { conversationId: string; messageId: string; - answer: string; suggestedFollowUps: string[]; remainingQuota: number; usedChunks: UsedChunk[]; - retrievalDebug?: Record; +} + +export interface ConversationMessage { + messageId: string; + role: 'user' | 'assistant' | string; + text: string; +} + +export interface NotificationItem { + id: number; + type: string; + title: string; + body: string; + isRead: boolean; + createdAt: string; } export interface Plan { id: string; title: string; + /** تومان (currency = "IRT") */ price: number; currency: string; popular: boolean; discountPercent: number; durationDays: number; + features: string[]; + badge: string; } -export type ReportReason = - | 'wrong_answer' | 'unclear' | 'typo' | 'too_hard' | 'other'; +export interface SubscriptionStatus { + active: boolean; + planId: string | null; + expiresAt: string | null; +} -export interface ReportResponse { +export interface PurchaseResponse { + orderId: string; + paymentUrl: string; +} + +export interface RedeemResponse { success: boolean; - reportId: number; - alreadyReported: boolean; + discountAmount: number; + /** "paid" یعنی کد کلِ مبلغ را پوشاند و نباید به درگاه رفت. */ + orderStatus: string; + amount: number; +} + +export interface OrderStatus { + orderId: string; + status: string; + amount: number; +} + +export interface Ticket { + ticketId: string; + subject: string; + status: string; + messages: { id: string; sender: string; text: string }[]; +} + +export interface FamilyLink { + familyLinkId: string; + status: string; + childName: string; +} + +export interface FamilyWeeklyReport { + weekStart: string; + xpEarned: number; + lessonsCompleted: number; + islandsCompleted: number; + currentStreak: number; +} + +export interface FamilySettings { + dailyLimitMinutes: number; + quietHoursStart: string; + quietHoursEnd: string; +} + +export interface AffiliateMe { + referralCode: string; + commissionRate: number; + referredCount: number; + paidCount: number; + totalCommission: number; } - */ \ No newline at end of file diff --git a/src/app/App.tsx b/src/app/App.tsx index d89123e..04b2676 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,3071 +1,148 @@ -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, ChevronRight, Zap, ChevronDown, AlertCircle, - Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat, - Flame, Star, Trophy, Heart, Users, FileText, -} from "lucide-react"; -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"; +import { useCallback, useEffect, useState } from 'react'; +import { AnimatePresence, motion } from 'motion/react'; +import { api, ApiError, http, type Grade, type UserProfile } from '@/api'; +import { AppShell } from '@/components/AppShell'; +import { ToastProvider } from '@/components/Toast'; +import { ErrorState } from '@/components/ui'; +import { KEYS, store } from '@/lib/storage'; +import { askStore } from '@/state/askStore'; +import { LoginScreen } from '@/screens/LoginScreen'; +import { OnboardingScreen } from '@/screens/OnboardingScreen'; +import { SetupScreen } from '@/screens/SetupScreen'; +import { SplashScreen } from '@/screens/SplashScreen'; +import { MainApp } from './MainApp'; +import { launchPayment } from './launchParams'; -// ─── کلاینتِ API — تنها جایی که با بک‌اند حرف می‌زنیم ───────────────────────── -const api = createClient({ - baseUrl: (import.meta as { env?: Record }).env?.VITE_API_BASE_URL ?? "https://api.math.tadrisino2.ir/api", -}); +type Flow = + | { kind: 'boot' } + | { kind: 'bootError'; error: unknown } + | { kind: 'onboarding' } + | { kind: 'login' } + | { kind: 'setup'; profile: UserProfile; grades: Grade[] } + | { kind: 'main'; profile: UserProfile; grades: Grade[] }; -function getOrCreateDeviceId(): string { - let id = localStorage.getItem("deviceId"); - if (!id) { id = crypto.randomUUID(); localStorage.setItem("deviceId", id); } - return id; -} +/** کمترین زمانِ نمایشِ اسپلش، تا صفحه چشمک نزند. */ +const SPLASH_MIN_MS = 700; -// ─── Robot Mascot ───────────────────────────────────────────────────────────── +/** + * جریانِ ورود: اسپلش → (معرفی، فقط بارِ اول) → ورود → تکمیلِ پروفایل → اپ. + * خودِ اپ (تب‌ها و صفحه‌ها) در MainApp است. + */ +export function App() { + const [flow, setFlow] = useState({ kind: 'boot' }); -type RobotMood = "wave" | "point" | "explain" | "think" | "neutral" | "happy" | "encourage"; + /** پروفایل و پایه‌ها را می‌گیرد و تصمیم می‌گیرد کاربر کجا برود. */ + const enter = useCallback(async () => { + const [profile, grades] = await Promise.all([api.profile.get(), api.grades()]); + await trackReferral(profile); + setFlow(profile.name && profile.gradeId ? { kind: 'main', profile, grades } : { kind: 'setup', profile, grades }); + }, []); -function RobotMascot({ mood, size = 120 }: { mood: RobotMood; size?: number }) { - const eyes: Record = { - wave: { lw: 10, lh: 10, rw: 10, rh: 10, mouth: "smile" }, - point: { lw: 10, lh: 10, rw: 10, rh: 10, mouth: "smirk" }, - explain: { lw: 9, lh: 12, rw: 12, rh: 12, mouth: "open" }, - think: { lw: 10, lh: 7, rw: 10, rh: 7, mouth: "hmm" }, - neutral: { lw: 9, lh: 9, rw: 9, rh: 9, mouth: "line" }, - happy: { lw: 12, lh: 7, rw: 12, rh: 7, mouth: "grin" }, - encourage:{ lw: 10, lh: 10, rw: 10, rh: 10, mouth: "soft" }, - }; - const e = eyes[mood]; - const lx = 38, ly = 45, rx = 62, ry = 45; + const boot = useCallback(async () => { + setFlow({ kind: 'boot' }); + const started = Date.now(); + let next: Flow | null = null; + if (api.auth.isSignedIn()) { + try { + await enter(); + } catch (error) { + // ۴۰۱ یعنی نشست باطل شده و onSignedOut خودش به ورود می‌برد؛ + // خطای شبکه نباید کاربر را بی‌دلیل از حساب بیرون بیندازد. + if (!(error instanceof ApiError && error.isUnauthorized)) next = { kind: 'bootError', error }; + else next = { kind: 'login' }; + } + } else { + next = store.get(KEYS.onboarded) ? { kind: 'login' } : { kind: 'onboarding' }; + } + const wait = SPLASH_MIN_MS - (Date.now() - started); + if (wait > 0) await new Promise((r) => setTimeout(r, wait)); + if (next) setFlow(next); + }, [enter]); - const mouth = () => { - switch (e.mouth) { - case "smile": return ; - case "smirk": return ; - case "open": return ; - case "hmm": return ; - case "line": return ; - case "grin": return ; - case "soft": return ; - default: return null; + useEffect(() => { + void boot(); + }, [boot]); + + // هر جا نشست باطل شد (refresh هم شکست خورد)، برگرد به صفحه‌ی ورود. + useEffect( + () => + http.onSignedOut(() => { + askStore.clear(); + setFlow({ kind: 'login' }); + }), + [], + ); + + const afterLogin = async () => { + try { + await enter(); + } catch (error) { + setFlow({ kind: 'bootError', error }); } }; - const arms = () => { - if (mood === "wave") return ( - - - - - - - - ); - if (mood === "point") return ( - - - - - - - ); - return ( - - - - - ); - }; - return ( - - {/* Antenna */} - - - - {/* Legs */} - - - - {/* Body */} - - - - - - {/* Arms */} - {arms()} - - {/* Head shell */} - - - {/* Screen bezel */} - - - - {/* Glare */} - - - {/* Eyes — left */} - - - - {/* Eyes — right */} - - - - {/* Blush */} - {(mood === "happy" || mood === "encourage") && ( - <> - - - - )} - - {mouth()} - - {/* Happy sparkles */} - {mood === "happy" && ( - <> - ⭐ - ✨ - - )} - - ); -} - -// ─── سه‌یار: مسکات‌های سیگما / دلتا / پی ───────────────────────────────────── -// موتور تولید 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: "همراهِ دلگرمی" }, -}; - -/** اگه slugِ کاراکترِ سرور با یکی از سه‌یار یکی باشه، از طرحِ خودمون (انیمیشن‌دار) استفاده می‌کنیم؛ - * وگرنه یه کارتِ عمومی نشون می‌دیم (پیشنهاد: به بک‌اند بگید slugها رو sigma/delta/pi بذاره). */ -function seyarSlugMatch(slug: string): SeyarChar | null { - const s = slug.toLowerCase(); - if (s.includes("sigma") || s.includes("سیگما")) return "sigma"; - if (s.includes("delta") || s.includes("دلتا")) return "delta"; - if (s.includes("pi") || s.includes("پی")) return "pi"; - return null; -} - -function SeyarMascot({ char, mood = "idle", size = 100, bob = false }: { char: SeyarChar; mood?: SeyarMood; size?: number; bob?: boolean }) { - return ( - - ); -} - -// ─── Types ──────────────────────────────────────────────────────────────────── - -type AppFlow = "splash" | "onboarding" | "login" | "profile" | "main"; -type MainScreen = "home" | "topics" | "skillmap" | "lessons" | "lesson" | "question" | "askai" | "subscription" | "notifications" | "leaderboard" | "characters"; -interface Profile { - name: string; - age: number; - grade: string; - gradeName?: string; -} -// ─── فصل‌ها و درس‌ها — کاملاً پویا از روی api.home() ──────────────────────── -// دیگه چیزی هاردکد نیست: هر پایه هر چند فصل و هر فصل هر چند ایستگاه که سرور -// برگردونه همونه. رنگ و آیکون رو چون سرور نمی‌ده، خودمون چرخشی می‌دیم. - -const UNIT_PALETTE = ["#ED6956", "#91ADC6", "#97C0C0", "#9B7BFF", "#FF7A3D", "#F3A9B0", "#4C5FD5", "#5FBF8F", "#E06BAE"]; -const UNIT_ICONS: React.ElementType[] = [Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat, BookOpen, Sparkles, Brain]; - -const unitColor = (i: number) => UNIT_PALETTE[i % UNIT_PALETTE.length]; -const unitIcon = (i: number) => UNIT_ICONS[i % UNIT_ICONS.length]; - -function unitProgress(unit: HomeUnit) { - const total = unit.nodes.length; - const done = unit.nodes.filter((n) => n.status === "completed").length; - return { done, total, percent: total ? Math.round((done / total) * 100) : 0 }; -} - -/** اولین ایستگاهِ ناتمام رو تو کلِ نقشه پیدا می‌کنه (برای کارتِ «ادامه بده») */ -function findContinueNode(home: HomeResponse | null): { unit: HomeUnit; node: HomeNode; unitIndex: number } | null { - if (!home) return null; - for (let i = 0; i < home.units.length; i++) { - const node = home.units[i].nodes.find((n) => n.status !== "completed"); - if (node) return { unit: home.units[i], node, unitIndex: i }; - } - return null; -} - -/** چیدمانِ مارپیچِ نقشه، برای هر تعداد ایستگاه که لازم باشه */ -function zigzagPositions(count: number, spacingY = 148, startY = 70): { x: number; y: number }[] { - return Array.from({ length: count }, (_, i) => ({ x: i % 2 === 0 ? 30 : 74, y: startY + i * spacingY })); -} - -/** حالتِ ماسکوت که سرور با متن می‌ده (مثلاً «happy»/«encourage») رو به حالتِ سه‌یار نگاشت می‌کنه */ -function mapMascotMood(server: string | undefined | null): SeyarMood { - const s = (server ?? "").toLowerCase(); - if (s.includes("excited") || s.includes("celebrat")) return "excited"; - if (s.includes("happy")) return "happy"; - if (s.includes("sad") || s.includes("encourage")) return "sad"; - if (s.includes("angry")) return "angry"; - if (s.includes("depress") || s.includes("tired")) return "depressed"; - return "idle"; -} - -// ─── اعلان‌ها ───────────────────────────────────────────────────────────────── -interface NotificationItem { id: number; type: string; title: string; isRead: boolean } - -const NOTIF_STYLE: Record = { - xp_earned: { icon: Star, color: "#E8A400", bg: "#FFF8E1" }, - lesson_completed: { icon: Check, color: "#5FBF8F", bg: "#EAF8F0" }, - order_paid: { icon: Crown, color: "#9B7BFF", bg: "#F5F1FF" }, - ticket_replied: { icon: MessageCircle, color: "#4C5FD5", bg: "#EEF1FF" }, - family_link_accepted: { icon: Users, color: "#FF7A3D", bg: "#FFF1E6" }, - weekly_report_ready: { icon: FileText, color: "#91ADC6", bg: "#EAF2F6" }, -}; -const notifStyle = (type: string) => NOTIF_STYLE[type] ?? { icon: Bell, color: "#9e9ec0", bg: "#f0eeff" }; - -// ─── 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: "topics", icon: Map, label: "مهارت‌ها" }, - { id: "lessons", icon: BookOpen, label: "درس‌ها" }, - { id: "askai", icon: Brain, label: "دستیار" }, - { id: "subscription", icon: Crown, label: "اشتراک" }, - ]; - return ( -
- {items.map(({ id, icon: Icon, label }) => ( - - ))} -
- ); -} - -// ─── Splash Screen ──────────────────────────────────────────────────────────── - -function SplashScreen({ onNew, onReturning }: { onNew: () => void; onReturning: () => void }) { - useEffect(() => { - // Auto-advance after 2.2 s; returning users press button - const t = setTimeout(onNew, 2200); - return () => clearTimeout(t); - }, [onNew]); - - return ( -
- -
- -
-

تدریسینو

-

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

-
- - - قبلاً وارد شدم - - - -
- ); -} - -// ─── Onboarding ─────────────────────────────────────────────────────────────── - -function OnboardingScreen({ onDone }: { onDone: () => void }) { - const [step, setStep] = useState(0); - const slides = [ - { - mood: "wave" as RobotMood, bg: "#ED6956", - title: "به تدریسینو خوش آمدی! 🤖", - sub: "دستیار ریاضی هوشمند", - desc: "با رُبو ریاضی یاد بگیر و هر روز باهوش‌تر بشو. یادگیری می‌تونه هم آسون هم جالب باشه!", - }, - { - mood: "point" as RobotMood, bg: "#91ADC6", - title: "مسیر ریاضیت رو بساز", - sub: "گام به گام پیشرفت کن", - desc: "از جمع و تفریق شروع کن تا به ضرب، تقسیم و کسرها برسی. رُبو همه‌جا کنارته!", - }, - { - mood: "explain" as RobotMood, bg: "#97C0C0", - title: "هر چیزی نفهمیدی بپرس!", - sub: "توضیح ساده و صبورانه", - desc: "رُبو هر مسئله‌ای رو با مثال‌های جالب برات توضیح می‌ده تا مطمئن بشی خوب یاد گرفتی.", - }, - ]; - const slide = slides[step]; - return ( -
- {step > 0 && ( - - )} -
+ + - -
- + {flow.kind === 'boot' && } + {flow.kind === 'bootError' && ( +
+ void boot()} /> +
+ )} + {flow.kind === 'onboarding' && ( + { + store.set(KEYS.onboarded, '1'); + setFlow({ kind: 'login' }); + }} /> -
-
-

{slide.sub}

-

{slide.title}

-

{slide.desc}

-
+ )} + {flow.kind === 'login' && void afterLogin()} />} + {flow.kind === 'setup' && ( + setFlow({ kind: 'main', profile, grades: flow.grades })} + /> + )} + {flow.kind === 'main' && ( + setFlow({ kind: 'login' })} + onRegistered={trackReferral} + /> + )}
-
- {/* Dots */} -
- {slides.map((_, i) => ( -
- ))} -
-
- {step < slides.length - 1 ? ( - <> - - - - ) : ( - - )} -
-
+ + ); } -//-----login screen -interface AuthData { - userId: string; - isGuest: 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(""); - - const phoneValid = /^09\d{9}$/.test(phone); - const otpValid = otp.trim().length >= 4; - - const friendlyError = (err: unknown, fallback: string) => { - if (err instanceof ApiError) { - if (err.isRateLimited) return "تعداد تلاش زیاد بوده، کمی صبر کن و دوباره امتحان کن"; - if (err.isProviderDown) return "سرویسِ پیامک موقتاً در دسترس نیست"; - return err.detail || fallback; - } - return fallback; - }; - - const requestOtp = async () => { - if (!phoneValid || loading) return; - setError(""); - setLoading(true); - try { - const data = await api.auth.requestOtp(phone); - setDevOtp(data.devOtp ?? null); - setStep("otp"); - } catch (err) { - setError(friendlyError(err, "ارسال کد با خطا مواجه شد، دوباره امتحان کن")); - } finally { - setLoading(false); - } - }; - - const verifyOtp = async () => { - if (!otpValid || loading) return; - setError(""); - setLoading(true); - try { - const data = await api.auth.login(phone, otp.trim()); - onDone({ userId: data.userId, isGuest: false }); - } catch (err) { - setError(friendlyError(err, "کد وارد شده اشتباهه، دوباره امتحان کن")); - } finally { - setLoading(false); - } - }; - - const continueAsGuest = async () => { - setError(""); - setLoading(true); +/** + * اگر کاربر با لینکِ دعوت آمده بود و حالا با شماره وارد شده، معرفی را ثبت کن. + * مهمان را سرور نمی‌پذیرد، پس کد تا ثبتِ شماره نگه داشته می‌شود. + */ +async function trackReferral(profile: UserProfile) { + const code = store.get(KEYS.referralCode); + if (!code || profile.isGuest) return; try { - const deviceId = getOrCreateDeviceId(); - - try { - const data = await api.auth.guest(deviceId); - onDone({ - userId: data.userId, - isGuest: true, - }); - } catch { - // Backend guest endpoint unavailable → local guest - onDone({ - userId: `guest-${deviceId}`, - isGuest: true, - }); - } - } finally { - setLoading(false); + await api.affiliate.track(code); + store.remove(KEYS.referralCode); + } catch (err) { + // کدِ نامعتبر یا معرفیِ خود: دوباره امتحان کردن فایده ندارد. + if (err instanceof ApiError && err.status >= 400 && err.status < 500) store.remove(KEYS.referralCode); } -}; - - return ( -
-
-
-
-

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

-

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

-
-
- -
-
-
- -
- {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} -
- )} -
- ) : ( -
- 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 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 [grades, setGrades] = useState([]); - const [gradesError, setGradesError] = useState(false); - const [saving, setSaving] = useState(false); - const [saveError, setSaveError] = useState(""); - - useEffect(() => { - api.grades().then(setGrades).catch(() => setGradesError(true)); - }, []); - - 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 = async () => { - - setTouched(true); - - if (!name.trim() || !grade || saving) return; - - setSaving(true); - setSaveError(""); - try { - await api.profile.update({ name: name.trim() }); - await api.profile.setGrade(Number(grade)); - const g = grades.find((x) => String(x.id) === grade); - onDone({ - name: name.trim(), - age, - grade, - gradeName: g?.title ?? "", - }); - } catch (err) { - setSaveError(err instanceof ApiError ? err.detail : "ذخیره نشد، دوباره امتحان کن"); - } finally { - setSaving(false); - } - }; - - - 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.length === 0 && !gradesError && ( -
- {[0, 1, 2, 3].map((i) => ( -
- ))} -
- )} - - {gradesError && ( -
- فهرستِ پایه‌ها نیومد؛ اتصالت رو چک کن و دوباره باز کن. -
- )} - -
- - {grades.map((g) => { - - const selected = grade === String(g.id); - - return ( - - - - ); - - })} - -
- - - {gradeErr && ( -
- - پایه تحصیلیت رو انتخاب کن -
- )} - -
- {saveError && ( -
- {saveError} -
- )} - - {/* ───────── START ───────── */} - - -
- -
- -
- ); -} -// ─── Home Screen ────────────────────────────────────────────────────────────── - -function HomeScreen({ - profile, - home, - summary, - unreadCount, - onNavigate, - onContinue, -}: { - profile: Profile; - home: HomeResponse | null; - summary: { xp: number; coins: number; streak: { current: number; longest: number } } | null; - unreadCount: number; - onNavigate: (s: MainScreen) => void; - onContinue: (unitId: string) => void; -}) { - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - const cont = findContinueNode(home); - const cUnit = cont?.unit; - const cProg = cUnit ? unitProgress(cUnit) : { done: 0, total: 0, percent: 0 }; - const cColor = cont ? unitColor(cont.unitIndex) : "#ED6956"; - const xp = summary?.xp ?? 0; - const level = Math.floor(xp / 100) + 1; - return ( -
- - {/* ── Header ─────────────────────────────────────────────────────────── */} -
- {/* decorative blobs */} -
-
- - {/* Greeting row */} -
-
-

سلام، {profile.name}!

-

- آماده‌ای ریاضی رو شروع کنیم؟ -

- {profile.gradeName && ( - - {profile.gradeName} - - )} -
-
- - - -
-
-
- - {/* ── Bold stat bar — overlaps the header for more visual weight ─────── */} -
-
{[ - { icon: Flame, val: toFa(summary?.streak.current ?? 0), label: "روز پیاپی", fg: "#FF7A3D", bg: "#FFF1E6" }, - { icon: Star, val: toFa(xp), label: "امتیاز", fg: "#E8A400", bg: "#FFF8E1" }, - { icon: Trophy, val: toFa(level), label: "سطح", fg: "#9B7BFF", bg: "#F5F1FF" }, - ].map(({ icon: Icon, val, label, fg, bg }) => ( -
-
- -
-
{val}
-
{label}
-
- ))} -
-
- - {/* ── Scrollable content ──────────────────────────────────────────────── */} -
- - {/* Continue card */} - {cont && cUnit ? ( -
-
-
-

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

-

{cont.node.title}

-

{cUnit.title} · {toFa(cProg.done)} از {toFa(cProg.total)} · {toFa(cProg.percent)}٪ تکمیل

-
-
- -
-
- {/* Progress bar */} -
-
-
- -
- ) : ( -
- -

- {home ? "همه‌ی فصل‌های این پایه رو تموم کردی! 🎉" : "در حال آوردنِ مسیر یادگیری..."} -

-
- )} - - {/* Quick access grid */} -
- - -
-
-
- ); -} - -// // ─── 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 }, -// ]; - -// 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 ─────────────────────────────────────────────────────────── - -// چیدمانِ فصل‌ها روی نقشه — به‌شکلِ مارپیچ، مثلِ نقشه‌ی مهارت ولی برای کلِ فصل‌ها -function TopicSelectScreen({ home, onSelect }: { home: HomeResponse | null; onSelect: (unitId: string) => void }) { - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - const units = home?.units ?? []; - const positions = zigzagPositions(units.length); - const mapHeight = positions.length ? positions[positions.length - 1].y + 190 : 400; - - const overallPct = home?.stats.progressPercent ?? 0; - - return ( -
- {/* Header */} -
-
-
-
-

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

-

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

-
- 0 ? "happy" : "idle"} size={64}/> -
-
-
-
-
- - {/* Map */} -
- {!home ? ( -
- -

در حال آوردنِ فصل‌ها...

-
- ) : units.length === 0 ? ( -
- -

هنوز فصلی برای این پایه ثبت نشده

-
- ) : ( -
- - {/* مسیرِ اتصال بینِ خونه‌های فصل‌ها */} - - {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 started = units[i]?.nodes.some((n) => n.status !== "locked"); - return ( - - ); - })} - - - {/* خونه‌های فصل‌ها */} - {units.map((unit, i) => { - const pos = positions[i]; - if (!pos) return null; - const prog = unitProgress(unit); - const complete = prog.percent === 100; - const color = unitColor(i); - const Icon = unitIcon(i); - return ( - - - - -
-
{unit.title}
-
{toFa(prog.done)} از {toFa(prog.total)} ایستگاه
-
-
- ); - })} - - {/* پی، منتظرِ انتهای مسیر */} -
- -
- {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({ - unitId, - home, - onOpenLesson, - onBack, -}: { - unitId: string; - home: HomeResponse | null; - onOpenLesson: (lessonId: number) => void; - onBack: () => void; -}) { - const unitIndex = home?.units.findIndex((u) => u.id === unitId) ?? -1; - const unit = unitIndex >= 0 ? home!.units[unitIndex] : undefined; - const items = unit?.nodes ?? []; - const color = unitIndex >= 0 ? unitColor(unitIndex) : "#91ADC6"; - - const doneCount = items.filter((it) => it.status === "completed").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 */} - -
- -
- -
- - - -
- -
-
- مسیر یادگیری -
- -

- {unit?.title ?? "..."} -

- -

- {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]?.status === "completed"; - - return ( - - ); - - })} - - - - - {/* ───────── Nodes ───────── */} - - {items.map((item, i) => { - - const position = positions[i]; - - const locked = item.status === "locked"; - - const current = item.status === "unlocked" || item.status === "in_progress"; - - const completed = item.status === "completed"; - - const Icon = Play; - - return ( - - - - {/* Current lesson label */} - - {current && ( - - - -
- - {item.title} - - {/* little arrow */} - -
- -
- - - - )} - - - {/* Current glow */} - - {current && ( - - - - )} - - - {/* Node */} - - - - - - - ); - - })} - - - {/* Finish decoration */} - - {items.length > 0 && ( - -
- -
- ادامه بده! -
- -
- -
- -
- - )} - -
- -
- -
- ); -} -// ─── Lessons Hub (تب «درس‌ها») ────────────────────────────────────────────── -// دسته‌بندی‌شده و مناسبِ دانش‌آموزِ دبیرستانی: کارتِ ادامه، سه دسته‌ی درسی، -// و یه دعوتِ کوچیک از دلتا برای یه چالشِ سریع. - -function LessonsHubScreen({ - home, - onOpenUnit, - onNavigate, -}: { - home: HomeResponse | null; - onOpenUnit: (unitId: string) => void; - onNavigate: (s: MainScreen) => void; -}) { - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - - // اولین ایستگاهِ ناتمام رو برای کارتِ «ادامه بده» پیدا می‌کنیم - const cont = findContinueNode(home); - const cUnit = cont?.unit; - const cProg = cUnit ? unitProgress(cUnit) : { done: 0, total: 0, percent: 0 }; - const cColor = cont ? unitColor(cont.unitIndex) : "#97C0C0"; - const units = home?.units ?? []; - - return ( -
- {/* Header */} -
-
-
-
-

درس‌ها

-

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

-
- -
-
- -
- - {/* Continue card */} - {cont && cUnit ? ( -
-
-
-

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

-

{cUnit.title}

-

{cont.node.title} · {toFa(cProg.done)} از {toFa(cProg.total)} درس

-
-
- -
-
-
-
-
- -
- ) : ( -
- -

- {home ? "همه‌ی فصل‌ها تموم شدن! 🎉" : "در حال آوردنِ درس‌ها..."} -

-
- )} - - {/* همه‌ی فصل‌ها */} - {units.length > 0 && ( -
-
-

فصل‌های این پایه

- {toFa(units.length)} فصل -
-
- {units.map((unit, i) => { - const prog = unitProgress(unit); - const color = unitColor(i); - const Icon = unitIcon(i); - return ( - - ); - })} -
-
- )} - - {/* دعوتِ دلتا به یه چالشِ سریع */} - - -
-
- ); -} - -// ─── Lesson Screen ──────────────────────────────────────────────────────────── - -function LessonScreen({ - unitId, - lessonId, - home, - onNavigate, - onBack, -}: { - unitId: string; - lessonId: number; - home: HomeResponse | null; - onNavigate: (s: MainScreen) => void; - onBack: () => void; -}) { - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - const unitIndex = home?.units.findIndex((u) => u.id === unitId) ?? -1; - const unit = unitIndex >= 0 ? home!.units[unitIndex] : undefined; - const color = unitIndex >= 0 ? unitColor(unitIndex) : "#ED6956"; - - const [intro, setIntro] = useState(null); - const [loadErr, setLoadErr] = useState(false); - - useEffect(() => { - setIntro(null); - setLoadErr(false); - api.lesson.intro(lessonId).then(setIntro).catch(() => setLoadErr(true)); - }, [lessonId]); - - const idx = unit ? Math.max(0, unit.nodes.findIndex((n) => n.lessonIds.includes(lessonId))) : 0; - - return ( -
-
- -
-
- {unit && ( -
- ایستگاه {toFa(idx + 1)} از {toFa(unit.nodes.length)} · {unit.title} -
- )} -

{intro?.title ?? "..."}

-
-
- -
-
-
- -
- {/* توضیحِ سیگما، به‌جای پنلِ متنیِ خشک */} -
-
- -
-
- {loadErr ? ( -

این درس الان بارگذاری نشد. یه بار دیگه امتحان کن.

- ) : !intro ? ( -

در حال آوردنِ توضیح درس...

- ) : ( -

{intro.introText}

- )} -
-
- - {intro && intro.questionCount === 0 ? ( -
- آزمونِ این درس به‌زودی اضافه می‌شه -
- ) : ( - - )} -
-
- ); -} - -// ─── Question Screen ────────────────────────────────────────────────────────── - -function QuestionScreen({ lessonId, onNavigate, onBack, onComplete }: { lessonId: number; onNavigate: (s: MainScreen) => void; onBack: () => void; onComplete?: () => void }) { - const [questions, setQuestions] = useState(null); - const [loadErr, setLoadErr] = useState(false); - const [qIdx, setQIdx] = useState(0); - const [sel, setSel] = useState(null); - const [result, setResult] = useState(null); - const [answering, setAnswering] = useState(false); - const [lives, setLives] = useState(5); - const [correctCount, setCorrectCount] = useState(0); - const outOfLives = lives <= 0; - - useEffect(() => { - api.hearts().then((h) => setLives(h.currentHearts)).catch(() => {}); - }, []); - - useEffect(() => { - setQuestions(null); - setLoadErr(false); - api.lesson.questions(lessonId).then(setQuestions).catch(() => setLoadErr(true)); - }, [lessonId]); - - const q = questions?.[qIdx]; - const correct = result?.correct ?? false; - // سه‌یار: قبل از جواب سیگما راهنماست، جوابِ درست یعنی دلتا، جوابِ غلط یعنی پی - const seyarChar: SeyarChar = sel === null ? "sigma" : correct ? "delta" : "pi"; - const seyarMood: SeyarMood = sel === null ? "idle" : correct ? "excited" : outOfLives ? "depressed" : "sad"; - - const optCls = (optId: string) => { - if (!result) return "bg-white border-2 border-[#e8e4ff] text-[#1a1a2e]"; - if (optId === result.correctOptionId) return "bg-[#e5fdf0] border-2 border-green-400 text-green-700"; - if (optId === sel) return "bg-[#fff0f0] border-2 border-red-300 text-red-500"; - return "bg-white border-2 border-[#e8e4ff] text-[#b0b0c8]"; - }; - - const selectAnswer = async (optId: string) => { - if (sel !== null || !q || answering) return; - setSel(optId); - setAnswering(true); - try { - const r = await api.lesson.answer(Number(q.id), Number(optId), crypto.randomUUID()); - setResult(r); - setLives(r.heartsRemaining); - if (r.correct) setCorrectCount((c) => c + 1); - } catch { - setLoadErr(true); - } finally { - setAnswering(false); - } - }; - - const next = async () => { - if (!questions) return; - if (outOfLives) { - setQIdx(0); setSel(null); setResult(null); setCorrectCount(0); - api.hearts().then((h) => setLives(h.currentHearts)).catch(() => {}); - return; - } - if (qIdx < questions.length - 1) { - setQIdx((i) => i + 1); setSel(null); setResult(null); - } else { - try { - await api.lesson.complete(lessonId, { correctCount, totalQuestions: questions.length }, crypto.randomUUID()); - onComplete?.(); - } catch { /* اگه ثبتِ پایان شکست خورد، بازم برگرد خونه — بعداً دوباره می‌شه امتحان کرد */ } - onNavigate("home"); - } - }; - - if (loadErr) { - return ( -
- -

این سوال‌ها الان بارگذاری نشدن.

- -
- ); - } - - if (!questions || !q) { - return ( -
- -

در حال آوردنِ سوال‌ها...

-
- ); - } - - return ( -
- {/* Top bar */} -
-
- -
-
-
- {qIdx + 1}/{questions.length} -
- - {/* جون‌ها (قلب‌ها) */} -
- {Array.from({ length: 5 }).map((_, i) => { - const filled = i < lives; - return ( -
- - - {filled && ( - - - - )} - -
- ); - })} -
-
- -
- {/* سه‌یار bubble */} -
- - - - - -
- {sel === null ? "جواب درست رو انتخاب کن! " : - correct ? "آفرین! درست بود ⚡ ادامه بده!" : - outOfLives ? "جونات تموم شد؛ نگران نباش، از اول با هم شروع می‌کنیم." : - "اشکالی نداره، بیا با هم دوباره نگاهش کنیم."} -
-
- - {/* Problem card */} -
-

سوال {qIdx + 1}

-

{q.prompt}

-
- - {/* Options grid */} -
- {q.options.map((opt) => ( - selectAnswer(opt.id)} - whileTap={{ scale: sel === null ? 0.93 : 1 }} - className={`py-4 rounded-2xl text-xl font-black flex items-center justify-center gap-1.5 transition-all ${optCls(opt.id)}`}> - {opt.value} - {result && opt.id === result.correctOptionId && } - {sel === opt.id && result && !result.correct && } - - ))} -
- - {result && ( - - {outOfLives ? <>دوباره تلاش کن : - qIdx < questions.length - 1 ? <>سوال بعدی : - <>پایان تمرین } - - )} -
-
- ); -} - -// ─── Ask AI ─────────────────────────────────────────────────────────────────── - -function AskAIScreen({ profile }: { profile: Profile }) { - const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); - const [streamingText, setStreamingText] = useState(null); - const [conversationId, setConversationId] = useState(undefined); - const [messages, setMessages] = useState([ - { from: "bot", text: `سلام ${profile.name}! من پی‌ام 🙂 هر سوالی داری بپرس، قضاوتی هم در کار نیست!` }, - ]); - - const send = async () => { - if (!input.trim() || loading) return; - const txt = input; - setInput(""); - setMessages(m => [...m, { from: "user", text: txt }]); - setLoading(true); - setStreamingText(""); - let finalText = ""; - try { - const res = await api.ai.askStream({ userQuestion: txt, conversationId }, (delta) => { - finalText += delta; - setStreamingText(finalText); - }); - if (res.conversationId) setConversationId(res.conversationId); - setMessages(m => [...m, { from: "bot", text: finalText }]); - } catch (err) { - const msg = err instanceof ApiError - ? (err.isRateLimited ? "یه‌کم زیاد سوال پرسیدی؛ چند لحظه صبر کن." : - err.isProviderDown ? "دستیار الان در دسترس نیست، بعداً دوباره امتحان کن." : - err.detail || "پاسخ نیومد، دوباره امتحان کن.") - : "پاسخ نیومد، دوباره امتحان کن."; - setMessages(m => [...m, { from: "bot", text: msg }]); - } finally { - setStreamingText(null); - setLoading(false); - } - }; - - return ( -
-
-
-
-

از پی بپرس

-

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

-
-
- -
-
-
- -
- {messages.map((msg, i) => ( -
- {msg.from === "bot" && ( -
- -
- )} -
{msg.text}
-
- ))} - {loading && ( -
-
- -
- {streamingText ? ( -
- {streamingText} -
- ) : ( -
- {[0, 1, 2].map(i => ( - - ))} -
- )} -
- )} -
- -
-
- - 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]"/> - -
-
-
- ); -} - -// ─── Notifications Screen ────────────────────────────────────────────────────── - -function NotificationsScreen({ - items, - loading, - loadErr, - onBack, - onMarkRead, - onMarkAllRead, -}: { - items: NotificationItem[] | null; - loading: boolean; - loadErr: boolean; - onBack: () => void; - onMarkRead: (id: number) => void; - onMarkAllRead: () => void; -}) { - const unreadCount = items?.filter((n) => !n.isRead).length ?? 0; - - return ( -
-
-
- -

اعلان‌ها

- {unreadCount > 0 && ( - - )} -
-
- -
- {loading ? ( -
- -

در حال آوردنِ اعلان‌ها...

-
- ) : loadErr ? ( -
- -

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

-
- ) : !items || items.length === 0 ? ( -
- -

فعلاً خبری نیست

-
- ) : ( -
- {items.map((n) => { - const style = notifStyle(n.type); - const Icon = style.icon; - return ( - - ); - })} -
- )} -
-
- ); -} - -// ─── Leaderboard Screen ───────────────────────────────────────────────────────── -// توجه: کلاینتِ api.ts پارامترِ period رو "weekly" | "monthly" تایپ کرده، ولی -// بک‌اندِ واقعی فقط "weekly" | "all_time" رو قبول می‌کنه (سریالایزرِ خودشون رو -// دیدم). این‌جا از مقادیرِ واقعیِ بک‌اند استفاده می‌کنیم. - -interface LeaderboardEntry { rank: number; userId: string; name: string; xpInPeriod: number } -type LeaderboardPeriod = "weekly" | "all_time"; - -const RANK_MEDAL: Record = { - 1: { bg: "#FFD23F", fg: "#8a6d00" }, - 2: { bg: "#D9DEE8", fg: "#5a6270" }, - 3: { bg: "#E8B27A", fg: "#7a4a1a" }, -}; - -function LeaderboardScreen({ - entries, - period, - loading, - loadErr, - myUserId, - onChangePeriod, - onBack, -}: { - entries: LeaderboardEntry[] | null; - period: LeaderboardPeriod; - loading: boolean; - loadErr: boolean; - myUserId?: string; - onChangePeriod: (p: LeaderboardPeriod) => void; - onBack: () => void; -}) { - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - const top3 = entries?.slice(0, 3) ?? []; - const rest = entries?.slice(3) ?? []; - - return ( -
- {/* Header */} -
-
-
-
- -

جدولِ امتیازها

- -
- - {/* Period toggle */} -
- {([["weekly", "این هفته"], ["all_time", "کل زمان"]] as [LeaderboardPeriod, string][]).map(([p, label]) => ( - - ))} -
-
-
- -
- {loading ? ( -
- -

در حال آوردنِ جدول...

-
- ) : loadErr ? ( -
- -

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

-
- ) : !entries || entries.length === 0 ? ( -
- -

هنوز کسی تو این جدول نیست

-
- ) : ( - <> - {/* سکوی سه نفرِ برتر */} - {top3.length > 0 && ( -
- {[top3[1], top3[0], top3[2]].filter(Boolean).map((e) => { - const isMe = e.userId === myUserId; - const medal = RANK_MEDAL[e.rank]; - const podiumH = e.rank === 1 ? 92 : e.rank === 2 ? 72 : 56; - return ( -
-
- {toFa(e.rank)} -
-

{isMe ? "تو" : e.name}

-

{toFa(e.xpInPeriod)} XP

-
-
- ); - })} -
- )} - - {/* بقیه‌ی رتبه‌ها */} -
- {rest.map((e) => { - const isMe = e.userId === myUserId; - return ( -
- {toFa(e.rank)} -
- {e.name ? e.name[0] : "؟"} -
-

{isMe ? "تو" : e.name}

- {toFa(e.xpInPeriod)} XP -
- ); - })} -
- - )} -
-
- ); -} - - - -// ─── Characters Screen (کاتالوگِ گیمیفیکیشنِ بک‌اند) ───────────────────────── -// کاراکترها رو خودِ بک‌اند مدیریت می‌کنه (مثلِ subscription.Plan)، پس تعدادشون -// می‌تونه هر چقدر باشه. اگه slug با سیگما/دلتا/پی جور دربیاد، طرحِ سه‌یارِ -// خودمون (انیمیشن‌دار) نشون داده می‌شه؛ وگرنه یه کارتِ عمومی. - -function CharactersScreen({ - characters, - loading, - loadErr, - selecting, - onSelect, - onBack, -}: { - characters: ApiCharacter[] | null; - loading: boolean; - loadErr: boolean; - selecting: string | null; - onSelect: (characterId: string) => void; - onBack: () => void; -}) { - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - - return ( -
-
-
-
- -
-

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

-

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

-
-
-
- -
- {loading ? ( -
- -

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

-
- ) : loadErr ? ( -
- -

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

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

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

-
- ) : ( -
- {characters.map((c) => { - const seyar = seyarSlugMatch(c.slug); - const isBusy = selecting === c.characterId; - return ( - - ); - })} -
- )} -
-
- ); -} - -// ─── Subscription ───────────────────────────────────────────────────────────── - -const PLAN_STYLES = [ - { card: "bg-white border border-[#ede9ff]", titleCls: "text-[#1a1a2e]", subCls: "text-[#9e9ec0]", fCls: "text-[#3a3a5c]", checkCls: "text-[#ED6956]", btnCls: "border-2 border-[#ED6956] text-[#ED6956]" }, - { 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" }, - { card: "bg-[#91ADC6] border-2 border-white/30", titleCls: "text-white", subCls: "text-white/60", fCls: "text-white/88", checkCls: "text-white", btnCls: "bg-white text-[#91ADC6] font-bold" }, -]; - -function durationLabel(days: number): string { - if (days === 0) return ""; - if (days === 30) return "ماهانه"; - if (days >= 360 && days <= 370) return "سالانه"; - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - return `${toFa(days)} روزه`; -} - -function SubscriptionScreen() { - const [plans, setPlans] = useState(null); - const [loadErr, setLoadErr] = useState(false); - const [purchasingId, setPurchasingId] = useState(null); - const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]); - - useEffect(() => { - api.subscription.plans().then(setPlans).catch(() => setLoadErr(true)); - }, []); - - const buy = async (planId: string) => { - if (purchasingId) return; - setPurchasingId(planId); - try { - const res = await api.subscription.purchase(planId, crypto.randomUUID()); - window.open(res.paymentUrl, "_blank"); - } catch { - /* اگه پرداخت شروع نشد، همینجا می‌مونه تا دوباره امتحان کنه */ - } finally { - setPurchasingId(null); - } - }; - - return ( -
-
- -

اشتراک ویژه

-

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

-
- -
- {loadErr && ( -
- پلن‌ها الان نیومدن؛ اتصالت رو چک کن. -
- )} - - {!plans && !loadErr && ( -
- -

در حال آوردنِ پلن‌ها...

-
- )} - - {/* Plan cards */} - {plans?.map((plan, i) => { - const style = PLAN_STYLES[i % PLAN_STYLES.length]; - const isFree = plan.price === 0; - const dur = durationLabel(plan.durationDays); - return ( -
-
-
-
-

{plan.title}

- {dur &&

{dur}

} -
-
-

- {isFree ? "رایگان" : `${toFa(plan.price.toLocaleString())} تومان`} -

-
-
-
- {plan.features.map(f => ( -
- - {f} -
- ))} -
- -
-
- ); - })} - - {plans &&

هر زمان می‌توانید لغو کنید · بدون تعهد

} -
-
- ); -} - -// ─── Root ───────────────────────────────────────────────────────────────────── - -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 [unitId, setUnitId] = useState(""); - const [lessonId, setLessonId] = useState(1); - const [home, setHome] = useState(null); - const [summary, setSummary] = useState<{ xp: number; coins: number; streak: { current: number; longest: number } } | null>(null); - const [notifications, setNotifications] = useState(null); - const [notifLoading, setNotifLoading] = useState(false); - const [notifErr, setNotifErr] = useState(false); - const unreadCount = notifications?.filter((n) => !n.isRead).length ?? 0; - - const refreshHome = () => { - api.home().then((h) => { - setHome(h); - if (!unitId && h.units[0]) setUnitId(h.units[0].id); - }).catch(() => {}); - api.summary().then((s: any) => setSummary(s)).catch(() => {}); - }; - - const refreshNotifications = () => { - setNotifLoading(true); - setNotifErr(false); - api.notifications.list() - .then((r) => setNotifications(r.items as unknown as NotificationItem[])) - .catch(() => setNotifErr(true)) - .finally(() => setNotifLoading(false)); - }; - - const markNotifRead = (id: number) => { - setNotifications((list) => list?.map((n) => (n.id === id ? { ...n, isRead: true } : n)) ?? list); - api.notifications.markRead(id).catch(() => {}); - }; - const markAllNotifRead = () => { - const unread = notifications?.filter((n) => !n.isRead) ?? []; - setNotifications((list) => list?.map((n) => ({ ...n, isRead: true })) ?? list); - unread.forEach((n) => api.notifications.markRead(n.id).catch(() => {})); - }; - - const [lbEntries, setLbEntries] = useState(null); - const [lbPeriod, setLbPeriod] = useState("weekly"); - const [lbLoading, setLbLoading] = useState(false); - const [lbErr, setLbErr] = useState(false); - - const refreshLeaderboard = (period: LeaderboardPeriod) => { - setLbLoading(true); - setLbErr(false); - 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]); - - useEffect(() => { - if (flow === "main" && screen === "leaderboard" && !lbEntries) refreshLeaderboard(lbPeriod); - }, [flow, screen]); - - useEffect(() => { - if (flow === "main" && screen === "characters" && !characters) refreshCharacters(); - }, [flow, screen]); - - // ─── تاریخچه‌ی صفحات، برای دکمه‌ی «بازگشت» ──────────────────────────────── - // 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); - }; - - useEffect(() => { - // اگه قبلاً واردِ اپ شده بودیم (توکن هنوز معتبره)، مستقیم بریم تو خانه - if (api.auth.isSignedIn()) { - Promise.all([api.profile.get(), api.grades().catch(() => [])]) - .then(([p, gs]) => { - const gName = gs.find((g) => g.id === p.gradeId)?.title ?? ""; - setProfile({ name: p.name || "", age: 15, grade: p.gradeId ? String(p.gradeId) : "", gradeName: gName }); - setFlow(p.name && p.gradeId ? "main" : "profile"); - }) - .catch(() => { /* توکن خراب بود؛ همون اسپلش بمونه، کاربر از اول وارد می‌شه */ }); - } - }, []); - - const handleReturning = () => setFlow("login"); - const handleOnboardingDone = () => setFlow("login"); - - const handleLoginDone = async (a: AuthData) => { - setAuth(a); - try { - const [p, gs] = await Promise.all([api.profile.get(), api.grades().catch(() => [])]); - if (p.name && p.gradeId) { - const gName = gs.find((g) => g.id === p.gradeId)?.title ?? ""; - setProfile({ name: p.name, age: 15, grade: String(p.gradeId), gradeName: gName }); - setFlow("main"); setScreen("home"); - return; - } - } catch { - /* پروفایلِ تازه یا خطای موقت — بریم مرحله‌ی تکمیلِ پروفایل */ - } - setFlow("profile"); - }; - - const handleProfileDone = (p: Profile) => { - setProfile(p); - setFlow("main"); - setScreen("home"); - }; - - const showNav = flow === "main"; - - const isLightHeader = flow === "main" && screen === "question"; - - return ( -
- {/* Phone frame */} -
- - {/* Status bar */} -
- ۹:۴۱ -
-
- {[3,4,5].map(h =>
)} -
-
-
-
-
-
- - {/* Screen area */} -
- - - {flow === "splash" && setFlow("onboarding")} onReturning={handleReturning}/>} - {flow === "onboarding"&& } - {flow === "profile" && } - {flow === "main" && screen === "lesson" && } - {flow === "main" && screen === "question" && } - {flow === "main" && screen === "askai" && } - {flow === "main" && screen === "subscription" && } - {flow === "login" && } - {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}/>} - - -
- - {showNav && } -
-
- ); } diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx new file mode 100644 index 0000000..5d4a071 --- /dev/null +++ b/src/app/MainApp.tsx @@ -0,0 +1,149 @@ +import { useEffect, useState } from 'react'; +import { AnimatePresence, motion } from 'motion/react'; +import { CheckCircle2, XCircle } from 'lucide-react'; +import { api, type Grade, type UserProfile } from '@/api'; +import { AppDataProvider, useAppData } from '@/state/AppData'; +import { NavigatorProvider, useNav } from '@/nav/Navigator'; +import { isTab, type Route } from '@/nav/routes'; +import { BottomNav } from '@/components/BottomNav'; +import { Sheet } from '@/components/Sheet'; +import { useToast } from '@/components/Toast'; +import { PrimaryButton } from '@/components/ui'; +import { AskScreen } from '@/screens/AskScreen'; +import { CharactersScreen } from '@/screens/CharactersScreen'; +import { FamilyScreen } from '@/screens/FamilyScreen'; +import { HomeScreen } from '@/screens/HomeScreen'; +import { InviteScreen } from '@/screens/InviteScreen'; +import { LeaderboardScreen } from '@/screens/LeaderboardScreen'; +import { LessonScreen } from '@/screens/LessonScreen'; +import { LoginScreen } from '@/screens/LoginScreen'; +import { MapScreen } from '@/screens/MapScreen'; +import { NotificationsScreen } from '@/screens/NotificationsScreen'; +import { ProfileScreen } from '@/screens/ProfileScreen'; +import { QuizScreen } from '@/screens/QuizScreen'; +import { StreakScreen } from '@/screens/StreakScreen'; +import { SubscriptionScreen } from '@/screens/SubscriptionScreen'; +import { SupportScreen } from '@/screens/SupportScreen'; +import { UnitScreen } from '@/screens/UnitScreen'; +import type { PaymentReturn } from './launchParams'; + +interface Props { + profile: UserProfile; + grades: Grade[]; + payment: PaymentReturn | null; + onSignedOut: () => void; + onRegistered: (profile: UserProfile) => Promise; +} + +/** اپِ اصلی بعد از ورود: تب‌ها، صفحه‌ها و داده‌ی مشترک. */ +export function MainApp({ profile, grades, payment, onSignedOut, onRegistered }: Props) { + return ( + + + + {payment && } + + + ); +} + +function Screens({ onSignedOut, onRegistered }: Pick) { + const nav = useNav(); + const { route } = nav; + const showNav = isTab(route.name) && nav.tab === route.name; + + return ( + <> +
+ + + + + +
+ {showNav && } + + ); +} + +function RouteView({ route, onSignedOut, onRegistered }: { route: Route } & Pick) { + switch (route.name) { + case 'home': return ; + case 'map': return ; + case 'ask': return ; + case 'subscription': return ; + case 'profile': return ; + case 'unit': return ; + case 'lesson': return ; + case 'quiz': return ; + case 'notifications': return ; + case 'leaderboard': return ; + case 'characters': return ; + case 'streak': return ; + case 'support': return ; + case 'family': return ; + case 'invite': return ; + case 'register': return ; + } +} + +/** مهمانی که شماره ثبت می‌کند؛ همان کاربر می‌ماند و فقط پروفایل تازه می‌شود. */ +function RegisterView({ onRegistered }: Pick) { + const nav = useNav(); + const { setProfile } = useAppData(); + const toast = useToast(); + return ( + { + try { + const profile = await api.profile.get(); + setProfile(profile); + await onRegistered(profile); + } catch { + /* ورود انجام شده؛ پروفایل با بازگشت به اپ تازه می‌شود */ + } + toast('شماره‌ت ثبت شد! پیشرفتت امنه.', 'success'); + nav.back(); + }} + /> + ); +} + +/** نتیجه‌ی برگشت از درگاه؛ وضعیتِ سفارش از خودِ سرور هم دوباره خوانده می‌شود. */ +function PaymentResult({ payment }: { payment: PaymentReturn }) { + const { refreshSummary } = useAppData(); + const [open, setOpen] = useState(true); + const [paid, setPaid] = useState(payment.status === 'paid'); + + useEffect(() => { + api.subscription + .order(payment.orderId) + .then((order) => setPaid(order.status === 'paid')) + .catch(() => {}); + void refreshSummary(); + }, [payment.orderId, refreshSummary]); + + return ( + setOpen(false)}> +
+ {paid ? : } +

{paid ? 'پرداخت موفق بود! 🎉' : 'پرداخت انجام نشد'}

+

+ {paid + ? 'اشتراکت فعال شد؛ از الان قلب‌هات نامحدوده.' + : 'اگه مبلغی از حسابت کم شده، نگران نباش؛ طیِ چند ساعت خودکار برمی‌گرده یا اشتراکت فعال می‌شه.'} +

+ setOpen(false)}>باشه +
+
+ ); +} diff --git a/src/app/components/figma/ImageWithFallback.tsx b/src/app/components/figma/ImageWithFallback.tsx deleted file mode 100644 index 0e26139..0000000 --- a/src/app/components/figma/ImageWithFallback.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useState } from 'react' - -const ERROR_IMG_SRC = - 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg==' - -export function ImageWithFallback(props: React.ImgHTMLAttributes) { - const [didError, setDidError] = useState(false) - - const handleError = () => { - setDidError(true) - } - - const { src, alt, style, className, ...rest } = props - - return didError ? ( -
-
- Error loading image -
-
- ) : ( - {alt} - ) -} diff --git a/src/app/components/ui/accordion.tsx b/src/app/components/ui/accordion.tsx deleted file mode 100644 index bd6b1e3..0000000 --- a/src/app/components/ui/accordion.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDownIcon } from "lucide-react"; - -import { cn } from "./utils"; - -function Accordion({ - ...props -}: React.ComponentProps) { - return ; -} - -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AccordionTrigger({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - svg]:rotate-180", - className, - )} - {...props} - > - {children} - - - - ); -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - -
{children}
-
- ); -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/app/components/ui/alert-dialog.tsx b/src/app/components/ui/alert-dialog.tsx deleted file mode 100644 index 875b8df..0000000 --- a/src/app/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; - -import { cn } from "./utils"; -import { buttonVariants } from "./button"; - -function AlertDialog({ - ...props -}: React.ComponentProps) { - return ; -} - -function AlertDialogTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - - ); -} - -function AlertDialogHeader({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ); -} - -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ); -} - -function AlertDialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogAction({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AlertDialogCancel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -}; diff --git a/src/app/components/ui/alert.tsx b/src/app/components/ui/alert.tsx deleted file mode 100644 index 9c35976..0000000 --- a/src/app/components/ui/alert.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "./utils"; - -const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", - { - variants: { - variant: { - default: "bg-card text-card-foreground", - destructive: - "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function Alert({ - className, - variant, - ...props -}: React.ComponentProps<"div"> & VariantProps) { - return ( -
- ); -} - -function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function AlertDescription({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ); -} - -export { Alert, AlertTitle, AlertDescription }; diff --git a/src/app/components/ui/aspect-ratio.tsx b/src/app/components/ui/aspect-ratio.tsx deleted file mode 100644 index c16d6bc..0000000 --- a/src/app/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; - -function AspectRatio({ - ...props -}: React.ComponentProps) { - return ; -} - -export { AspectRatio }; diff --git a/src/app/components/ui/avatar.tsx b/src/app/components/ui/avatar.tsx deleted file mode 100644 index c990451..0000000 --- a/src/app/components/ui/avatar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AvatarPrimitive from "@radix-ui/react-avatar"; - -import { cn } from "./utils"; - -function Avatar({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AvatarFallback({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/app/components/ui/badge.tsx b/src/app/components/ui/badge.tsx deleted file mode 100644 index 2ccc2c4..0000000 --- a/src/app/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "./utils"; - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span"; - - return ( - - ); -} - -export { Badge, badgeVariants }; diff --git a/src/app/components/ui/breadcrumb.tsx b/src/app/components/ui/breadcrumb.tsx deleted file mode 100644 index 8f84d7e..0000000 --- a/src/app/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { ChevronRight, MoreHorizontal } from "lucide-react"; - -import { cn } from "./utils"; - -function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return