From af11d96fd03bf02fe33bed90f9d2dae1c01d47ea Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sun, 2 Aug 2026 17:23:00 +0330 Subject: [PATCH] Initial commit of Balout Pastry admin dashboards. Co-authored-by: Cursor --- .env.example | 1 + .gitignore | 29 + .oxlintrc.json | 8 + CONTEXT.md | 135 ++ index.html | 13 + package-lock.json | 1453 +++++++++++++++++ package.json | 28 + public/favicon.svg | 6 + src/App.tsx | 105 ++ src/components/AmountPriceModal.module.css | 4 + src/components/AmountPriceModal.tsx | 151 ++ src/components/AnimatedTiles.tsx | 74 + src/components/CategoryItem.module.css | 163 ++ src/components/CategoryItem.tsx | 138 ++ src/components/CategoryModal.module.css | 303 ++++ src/components/CategoryModal.tsx | 267 +++ .../CategoryOptionsModal.module.css | 403 +++++ src/components/CategoryOptionsModal.tsx | 327 ++++ src/components/ChangePasswordModal.module.css | 9 + src/components/ChangePasswordModal.tsx | 137 ++ src/components/CreateOrderModal.module.css | 342 ++++ src/components/CreateOrderModal.tsx | 816 +++++++++ src/components/Header.module.css | 219 +++ src/components/Header.tsx | 146 ++ src/components/ImageCropper.module.css | 163 ++ src/components/ImageCropper.tsx | 143 ++ src/components/ImageGallery.module.css | 101 ++ src/components/ImageGallery.tsx | 68 + src/components/OrderDetailsModal.module.css | 212 +++ src/components/OrderDetailsModal.tsx | 173 ++ src/components/OrderLineItemModal.module.css | 137 ++ src/components/OrderLineItemModal.tsx | 226 +++ src/components/OrderStatusModal.module.css | 46 + src/components/OrderStatusModal.tsx | 93 ++ src/components/PriceInput.module.css | 30 + src/components/PriceInput.tsx | 33 + src/components/ProductCard.module.css | 199 +++ src/components/ProductCard.tsx | 87 + src/components/ProductOptionsModal.module.css | 56 + src/components/ProductOptionsModal.tsx | 347 ++++ src/components/RichTextArea.module.css | 73 + src/components/RichTextArea.tsx | 79 + src/components/RoleModal.module.css | 43 + src/components/RoleModal.tsx | 88 + src/components/TagInput.module.css | 60 + src/components/TagInput.tsx | 67 + src/components/UserModal.module.css | 66 + src/components/UserModal.tsx | 325 ++++ src/data/categories.ts | 177 ++ src/data/districts.ts | 25 + src/data/flavors.ts | 56 + src/data/orders.ts | 267 +++ src/data/products.ts | 149 ++ src/data/users.ts | 367 +++++ src/fonts/CAILYNE.TTF | Bin 0 -> 20592 bytes src/fonts/Doran-Bold.woff | Bin 0 -> 59924 bytes src/fonts/Doran-Medium.woff | Bin 0 -> 59868 bytes src/fonts/Doran-Regular.woff | Bin 0 -> 59064 bytes src/fonts/Doran-Thin.woff | Bin 0 -> 56708 bytes src/fonts/fonts.css | 31 + src/images/Logo-balout-256.png | Bin 0 -> 15473 bytes src/images/Logo_balout-1280.png | Bin 0 -> 132725 bytes src/images/products/IMG_1010.jpeg | Bin 0 -> 48144 bytes src/images/products/IMG_1031.jpeg | Bin 0 -> 37006 bytes src/images/products/IMG_1035.jpeg | Bin 0 -> 41628 bytes src/images/products/IMG_1039.jpeg | Bin 0 -> 29316 bytes src/images/products/IMG_1040.jpeg | Bin 0 -> 60688 bytes src/images/products/IMG_1043.jpeg | Bin 0 -> 39153 bytes src/index.css | 142 ++ src/lib/api.ts | 119 ++ src/lib/auth.ts | 67 + src/lib/authApi.ts | 40 + src/lib/branchesStore.ts | 55 + src/lib/categoryOptionsStore.ts | 93 ++ src/lib/nav.ts | 105 ++ src/lib/orderSearch.ts | 91 ++ src/lib/userAddressesStore.ts | 85 + src/lib/usersApi.ts | 92 ++ src/main.tsx | 14 + src/pages/AddProductPage.module.css | 291 ++++ src/pages/AddProductPage.tsx | 314 ++++ src/pages/CategoriesPage.module.css | 82 + src/pages/CategoriesPage.tsx | 173 ++ src/pages/HomePage.module.css | 254 +++ src/pages/HomePage.tsx | 36 + src/pages/LoginPage.module.css | 224 +++ src/pages/LoginPage.tsx | 132 ++ src/pages/OrdersPage.module.css | 396 +++++ src/pages/OrdersPage.tsx | 249 +++ src/pages/PlaceholderPage.tsx | 36 + src/pages/ProductsListPage.module.css | 208 +++ src/pages/ProductsListPage.tsx | 216 +++ src/pages/ProductsPage.tsx | 38 + src/pages/SettingsPage.module.css | 328 ++++ src/pages/SettingsPage.tsx | 349 ++++ src/pages/UsersPage.module.css | 489 ++++++ src/pages/UsersPage.tsx | 525 ++++++ src/utils/cropImage.ts | 46 + src/utils/price.ts | 35 + tsconfig.app.json | 26 + tsconfig.json | 7 + tsconfig.node.json | 23 + vite.config.ts | 7 + 103 files changed, 14651 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .oxlintrc.json create mode 100644 CONTEXT.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 src/App.tsx create mode 100644 src/components/AmountPriceModal.module.css create mode 100644 src/components/AmountPriceModal.tsx create mode 100644 src/components/AnimatedTiles.tsx create mode 100644 src/components/CategoryItem.module.css create mode 100644 src/components/CategoryItem.tsx create mode 100644 src/components/CategoryModal.module.css create mode 100644 src/components/CategoryModal.tsx create mode 100644 src/components/CategoryOptionsModal.module.css create mode 100644 src/components/CategoryOptionsModal.tsx create mode 100644 src/components/ChangePasswordModal.module.css create mode 100644 src/components/ChangePasswordModal.tsx create mode 100644 src/components/CreateOrderModal.module.css create mode 100644 src/components/CreateOrderModal.tsx create mode 100644 src/components/Header.module.css create mode 100644 src/components/Header.tsx create mode 100644 src/components/ImageCropper.module.css create mode 100644 src/components/ImageCropper.tsx create mode 100644 src/components/ImageGallery.module.css create mode 100644 src/components/ImageGallery.tsx create mode 100644 src/components/OrderDetailsModal.module.css create mode 100644 src/components/OrderDetailsModal.tsx create mode 100644 src/components/OrderLineItemModal.module.css create mode 100644 src/components/OrderLineItemModal.tsx create mode 100644 src/components/OrderStatusModal.module.css create mode 100644 src/components/OrderStatusModal.tsx create mode 100644 src/components/PriceInput.module.css create mode 100644 src/components/PriceInput.tsx create mode 100644 src/components/ProductCard.module.css create mode 100644 src/components/ProductCard.tsx create mode 100644 src/components/ProductOptionsModal.module.css create mode 100644 src/components/ProductOptionsModal.tsx create mode 100644 src/components/RichTextArea.module.css create mode 100644 src/components/RichTextArea.tsx create mode 100644 src/components/RoleModal.module.css create mode 100644 src/components/RoleModal.tsx create mode 100644 src/components/TagInput.module.css create mode 100644 src/components/TagInput.tsx create mode 100644 src/components/UserModal.module.css create mode 100644 src/components/UserModal.tsx create mode 100644 src/data/categories.ts create mode 100644 src/data/districts.ts create mode 100644 src/data/flavors.ts create mode 100644 src/data/orders.ts create mode 100644 src/data/products.ts create mode 100644 src/data/users.ts create mode 100644 src/fonts/CAILYNE.TTF create mode 100644 src/fonts/Doran-Bold.woff create mode 100644 src/fonts/Doran-Medium.woff create mode 100644 src/fonts/Doran-Regular.woff create mode 100644 src/fonts/Doran-Thin.woff create mode 100644 src/fonts/fonts.css create mode 100644 src/images/Logo-balout-256.png create mode 100644 src/images/Logo_balout-1280.png create mode 100644 src/images/products/IMG_1010.jpeg create mode 100644 src/images/products/IMG_1031.jpeg create mode 100644 src/images/products/IMG_1035.jpeg create mode 100644 src/images/products/IMG_1039.jpeg create mode 100644 src/images/products/IMG_1040.jpeg create mode 100644 src/images/products/IMG_1043.jpeg create mode 100644 src/index.css create mode 100644 src/lib/api.ts create mode 100644 src/lib/auth.ts create mode 100644 src/lib/authApi.ts create mode 100644 src/lib/branchesStore.ts create mode 100644 src/lib/categoryOptionsStore.ts create mode 100644 src/lib/nav.ts create mode 100644 src/lib/orderSearch.ts create mode 100644 src/lib/userAddressesStore.ts create mode 100644 src/lib/usersApi.ts create mode 100644 src/main.tsx create mode 100644 src/pages/AddProductPage.module.css create mode 100644 src/pages/AddProductPage.tsx create mode 100644 src/pages/CategoriesPage.module.css create mode 100644 src/pages/CategoriesPage.tsx create mode 100644 src/pages/HomePage.module.css create mode 100644 src/pages/HomePage.tsx create mode 100644 src/pages/LoginPage.module.css create mode 100644 src/pages/LoginPage.tsx create mode 100644 src/pages/OrdersPage.module.css create mode 100644 src/pages/OrdersPage.tsx create mode 100644 src/pages/PlaceholderPage.tsx create mode 100644 src/pages/ProductsListPage.module.css create mode 100644 src/pages/ProductsListPage.tsx create mode 100644 src/pages/ProductsPage.tsx create mode 100644 src/pages/SettingsPage.module.css create mode 100644 src/pages/SettingsPage.tsx create mode 100644 src/pages/UsersPage.module.css create mode 100644 src/pages/UsersPage.tsx create mode 100644 src/utils/cropImage.ts create mode 100644 src/utils/price.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..edcf870 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:3100/api/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d679a59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# env +.env +.env.local +.env.*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..2fe4245 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,135 @@ +# Balout Pastry — Dashboards (setup context) + +Admin dashboard for شیرینی‌فروشی بلوط. React + Vite SPA that talks to the NestJS API. + +Repo: `https://git.meshkee.com/BaloutPastry/dashboards.git` +API repo: `https://git.meshkee.com/BaloutPastry/backend.git` + +## Stack + +- React 19 + TypeScript + Vite 8 +- React Router 7 +- Lucide icons +- Auth via JWT (access + refresh) stored in `localStorage` +- API client: `src/lib/api.ts` → `VITE_API_BASE_URL` + +## Prerequisites + +- Node.js **20+** (LTS recommended) +- npm +- Running **Backend** API on port **3100** (see backend `README.md`) +- A `superAdmin` / `admin` user in the API database + +## Setup on a new device + +```bash +git clone https://git.meshkee.com/BaloutPastry/dashboards.git +cd dashboards + +cp .env.example .env +# Edit if API is not on localhost:3100 +# VITE_API_BASE_URL=http://localhost:3100/api/v1 + +npm install +npm run dev +``` + +Open **http://localhost:5173** + +### Backend must be up first + +From the backend repo (separate clone): + +```bash +cp .env.example .env +npm install +npm run db:up +npx prisma migrate deploy +npm run create-super-admin -- --phone 09120000000 --password secret123 +npm run start:dev # http://localhost:3100/api/v1 +``` + +Then log into the dashboard with that phone/password. + +CORS: backend `CORS_ORIGIN` should include `http://localhost:5173` (default in `.env.example`). + +## Scripts + +| Command | What it does | +|---------|----------------| +| `npm run dev` | Vite dev server → `http://localhost:5173` | +| `npm run build` | Typecheck + production build → `dist/` | +| `npm run preview` | Serve production build locally | +| `npm run lint` | Oxlint | + +## Environment + +| Variable | Required | Default | Notes | +|----------|----------|---------|-------| +| `VITE_API_BASE_URL` | no | `http://localhost:3100/api/v1` | Must match backend base URL including `/api/v1` | + +Copy from `.env.example`. Do **not** commit `.env`. + +After changing env vars, restart `npm run dev` (Vite only reads them at startup). + +## App routes + +| Path | Page | +|------|------| +| `/login` | Admin login (mobile + password) | +| `/` | Home | +| `/products` | Products hub | +| `/products/list` | Product list | +| `/products/new` | Add product | +| `/products/categories` | Categories | +| `/users` | Users | +| `/orders` | Orders | +| `/settings` | Settings (shipping, branches) | + +Protected routes redirect to `/login` when there is no session. + +## Auth / session notes + +- Login: `POST /auth/login` with `cellNumber` + `password` +- Only `admin` and `superAdmin` can use this dashboard +- Tokens live in `localStorage` (`src/lib/auth.ts`) +- `apiRequest` refreshes access token on `401` via `/auth/refresh` +- Logout clears the session + +## Project layout (important paths) + +``` +src/ + lib/api.ts HTTP client + ApiError + lib/auth.ts Session / tokens + lib/authApi.ts Login / refresh / logout / me + lib/*Api.ts Domain API helpers + pages/ Route pages + components/ Modals, header, shared UI + data/ Shared types & constants (titles, roles, …) + utils/ Helpers (price, digits, …) +``` + +## Production build + +```bash +npm run build +# Serve dist/ behind nginx/Caddy, or: +npm run preview +``` + +Set `VITE_API_BASE_URL` to the real API URL **before** `npm run build` (it is baked into the bundle). + +## Common issues + +| Symptom | Likely cause | +|---------|----------------| +| «اتصال به سرور» / save failed | Backend not running, or wrong `VITE_API_BASE_URL` | +| Login works then immediate logout | Refresh token rejected; check JWT secrets / DB | +| CORS errors in browser | Backend `CORS_ORIGIN` missing dashboard origin | +| Empty users/products | API up but empty DB, or not logged in as admin | + +## Related + +- Backend docs: clone `BaloutPastry/backend` and read its `README.md` +- Postgres for local API: Docker Compose on host port **5434** diff --git a/index.html b/index.html new file mode 100644 index 0000000..adcd8dd --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + بلوط | پنل مدیریت + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..de4d521 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1453 @@ +{ + "name": "balout-pastry", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "balout-pastry", + "version": "0.0.0", + "dependencies": { + "lucide-react": "^1.28.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-easy-crop": "^6.2.3", + "react-router-dom": "^7.18.2" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } + }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.76.0.tgz", + "integrity": "sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.76.0.tgz", + "integrity": "sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.76.0.tgz", + "integrity": "sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.76.0.tgz", + "integrity": "sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.76.0.tgz", + "integrity": "sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.76.0.tgz", + "integrity": "sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.76.0.tgz", + "integrity": "sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.76.0.tgz", + "integrity": "sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.76.0.tgz", + "integrity": "sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.76.0.tgz", + "integrity": "sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.76.0.tgz", + "integrity": "sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.76.0.tgz", + "integrity": "sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.76.0.tgz", + "integrity": "sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.76.0.tgz", + "integrity": "sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.76.0.tgz", + "integrity": "sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.76.0.tgz", + "integrity": "sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.76.0.tgz", + "integrity": "sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.76.0.tgz", + "integrity": "sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.76.0.tgz", + "integrity": "sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.28.0.tgz", + "integrity": "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, + "node_modules/oxlint": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.76.0.tgz", + "integrity": "sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.76.0", + "@oxlint/binding-android-arm64": "1.76.0", + "@oxlint/binding-darwin-arm64": "1.76.0", + "@oxlint/binding-darwin-x64": "1.76.0", + "@oxlint/binding-freebsd-x64": "1.76.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.76.0", + "@oxlint/binding-linux-arm-musleabihf": "1.76.0", + "@oxlint/binding-linux-arm64-gnu": "1.76.0", + "@oxlint/binding-linux-arm64-musl": "1.76.0", + "@oxlint/binding-linux-ppc64-gnu": "1.76.0", + "@oxlint/binding-linux-riscv64-gnu": "1.76.0", + "@oxlint/binding-linux-riscv64-musl": "1.76.0", + "@oxlint/binding-linux-s390x-gnu": "1.76.0", + "@oxlint/binding-linux-x64-gnu": "1.76.0", + "@oxlint/binding-linux-x64-musl": "1.76.0", + "@oxlint/binding-openharmony-arm64": "1.76.0", + "@oxlint/binding-win32-arm64-msvc": "1.76.0", + "@oxlint/binding-win32-ia32-msvc": "1.76.0", + "@oxlint/binding-win32-x64-msvc": "1.76.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-easy-crop": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-6.2.3.tgz", + "integrity": "sha512-ebimG3OGlzizjxEZ77Cj9CVLcg5vDZ6QVz8ud1rx4WmsCDWHIROEhgMi0GpJ/jKAuwHrH0M+QZUK4hyvxbYhOA==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "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-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.142.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e3602b --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "balout-pastry", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^1.28.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-easy-crop": "^6.2.3", + "react-router-dom": "^7.18.2" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..92ff205 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..84ff083 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,105 @@ +import { Navigate, Route, Routes } from 'react-router-dom' +import { LoginPage } from './pages/LoginPage' +import { HomePage } from './pages/HomePage' +import { ProductsPage } from './pages/ProductsPage' +import { ProductsListPage } from './pages/ProductsListPage' +import { CategoriesPage } from './pages/CategoriesPage' +import { AddProductPage } from './pages/AddProductPage' +import { UsersPage } from './pages/UsersPage' +import { OrdersPage } from './pages/OrdersPage' +import { SettingsPage } from './pages/SettingsPage' +import { isAuthenticated } from './lib/auth' + +function RequireAuth({ children }: { children: React.ReactNode }) { + if (!isAuthenticated()) { + return + } + return children +} + +function RedirectIfAuth({ children }: { children: React.ReactNode }) { + if (isAuthenticated()) { + return + } + return children +} + +export default function App() { + return ( + + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + ) +} diff --git a/src/components/AmountPriceModal.module.css b/src/components/AmountPriceModal.module.css new file mode 100644 index 0000000..d377e3f --- /dev/null +++ b/src/components/AmountPriceModal.module.css @@ -0,0 +1,4 @@ +.overlay { + z-index: 90; + background: rgba(42, 24, 16, 0.35); +} diff --git a/src/components/AmountPriceModal.tsx b/src/components/AmountPriceModal.tsx new file mode 100644 index 0000000..db614ec --- /dev/null +++ b/src/components/AmountPriceModal.tsx @@ -0,0 +1,151 @@ +import { useEffect, useId, useState } from 'react' +import { X } from 'lucide-react' +import { PriceInput } from './PriceInput' +import { parsePriceNumber } from '../utils/price' +import styles from './CategoryModal.module.css' +import nestedStyles from './AmountPriceModal.module.css' + +export type AmountPriceValues = { + amount: string + price: string +} + +type AmountPriceModalProps = { + open: boolean + title?: string + initialValues?: AmountPriceValues | null + onClose: () => void + onSubmit: (values: AmountPriceValues) => void +} + +export function AmountPriceModal({ + open, + title = 'مقدار و قیمت', + initialValues = null, + onClose, + onSubmit, +}: AmountPriceModalProps) { + const titleId = useId() + const [amount, setAmount] = useState('') + const [price, setPrice] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + if (!open) return + setAmount(initialValues?.amount ?? '') + setPrice(initialValues?.price ?? '') + setError('') + }, [open, initialValues]) + + useEffect(() => { + if (!open) return + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + event.stopPropagation() + onClose() + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [open, onClose]) + + if (!open) return null + + function handleSubmit(event: React.FormEvent) { + event.preventDefault() + if (!amount.trim()) { + setError('مقدار الزامی است') + return + } + const numericPrice = parsePriceNumber(price) + if (numericPrice === null || numericPrice < 0) { + setError('قیمت معتبر وارد کنید') + return + } + + onSubmit({ + amount: amount.trim(), + price: String(numericPrice), + }) + } + + return ( +
+
event.stopPropagation()} + > +
+ +
+
+

Amount & Price

+

+ {title} +

+
+ +
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setAmount(e.target.value)} + autoFocus + /> +
+ +
+ + +
+ +
+ + +
+
+
+
+ ) +} diff --git a/src/components/AnimatedTiles.tsx b/src/components/AnimatedTiles.tsx new file mode 100644 index 0000000..8f46af5 --- /dev/null +++ b/src/components/AnimatedTiles.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { ChevronLeft, type LucideIcon } from 'lucide-react' +import styles from '../pages/HomePage.module.css' + +export type AnimatedTile = { + id: string + title: string + titleEn: string + description: string + href: string + icon: LucideIcon +} + +type AnimatedTilesProps = { + tiles: AnimatedTile[] + label: string +} + +export function AnimatedTiles({ tiles, label }: AnimatedTilesProps) { + const location = useLocation() + const [visible, setVisible] = useState(false) + const [entered, setEntered] = useState(false) + + useEffect(() => { + setVisible(false) + setEntered(false) + const frame = window.requestAnimationFrame(() => { + setVisible(true) + }) + const enterMs = 700 + Math.max(0, tiles.length - 1) * 100 + const clearDelay = window.setTimeout(() => { + setEntered(true) + }, enterMs) + return () => { + window.cancelAnimationFrame(frame) + window.clearTimeout(clearDelay) + } + }, [location.key, tiles.length]) + + return ( +
+ {tiles.map((tile, index) => { + const Icon = tile.icon + return ( + + + + + + + {tile.titleEn} + + {tile.title} + {tile.description} + + + + + + + ) + })} +
+ ) +} diff --git a/src/components/CategoryItem.module.css b/src/components/CategoryItem.module.css new file mode 100644 index 0000000..89b3558 --- /dev/null +++ b/src/components/CategoryItem.module.css @@ -0,0 +1,163 @@ +.item { + list-style: none; +} + +.row { + display: flex; + align-items: center; + gap: 10px; + min-height: 56px; + padding: 10px 14px; + border-radius: var(--radius-sm); + background: rgba(255, 250, 250, 0.55); + transition: background 0.2s; +} + +.rowClickable { + cursor: pointer; +} + +.rowClickable:hover { + background: rgba(255, 250, 250, 0.9); +} + +.row:hover { + background: rgba(255, 250, 250, 0.9); +} + +.childRow { + background: transparent; +} + +.childRow:hover { + background: rgba(143, 65, 12, 0.04); +} + +.collapseIcon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + color: var(--text-primary); +} + +.collapseIconEmpty { + visibility: hidden; +} + +.chevron { + transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); +} + +.chevronOpen { + transform: rotate(-90deg); +} + +.titles { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + +.nameFa { + font-size: 0.98rem; + font-weight: 500; + color: var(--brown); + line-height: 1.35; +} + +.nameEn { + font-family: var(--font-en); + font-size: 0.8rem; + font-weight: 400; + letter-spacing: 0.03em; + color: var(--text-muted); + direction: ltr; + text-align: end; + line-height: 1.3; +} + +.controls { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.iconBtn { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 10px; + color: var(--text-primary); + transition: + color 0.2s, + background 0.2s; +} + +.iconBtn:hover { + color: var(--brown); + background: rgba(143, 65, 12, 0.07); +} + +.iconBtn::after { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + z-index: 6; + padding: 5px 8px; + border-radius: 8px; + background: var(--brown-deeper); + color: var(--text-on-dark); + font-size: 0.7rem; + font-weight: 400; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transform: translate(-50%, 4px); + transition: + opacity 0.18s, + transform 0.18s; +} + +.iconBtn:hover::after, +.iconBtn:focus-visible::after { + opacity: 1; + transform: translate(-50%, 0); +} + +.removeBtn:hover { + color: #9b2c2c; + background: rgba(155, 44, 44, 0.08); +} + +.removeBtn:hover::after { + background: #9b2c2c; +} + +.children { + list-style: none; + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 4px; + margin-inline-start: 32px; + padding: 0; + padding-inline-start: 14px; + border-inline-start: 2px solid rgba(143, 65, 12, 0.18); +} + +@media (prefers-reduced-motion: reduce) { + .chevron, + .iconBtn::after { + transition: none; + } +} diff --git a/src/components/CategoryItem.tsx b/src/components/CategoryItem.tsx new file mode 100644 index 0000000..9ed4213 --- /dev/null +++ b/src/components/CategoryItem.tsx @@ -0,0 +1,138 @@ +import { useState } from 'react' +import { + ChevronLeft, + FolderPlus, + Pencil, + SlidersHorizontal, + Trash2, +} from 'lucide-react' +import type { Category } from '../data/categories' +import styles from './CategoryItem.module.css' + +type CategoryItemProps = { + category: Category + depth?: number + onAddChild: (parentId: string) => void + onEdit: (category: Category) => void + onOpenOptions: (category: Category) => void + onRemove: (id: string) => void +} + +export function CategoryItem({ + category, + depth = 0, + onAddChild, + onEdit, + onOpenOptions, + onRemove, +}: CategoryItemProps) { + const hasChildren = category.children.length > 0 + const [open, setOpen] = useState(false) + + function toggleOpen() { + if (!hasChildren) return + setOpen((value) => !value) + } + + return ( +
  • +
    0 ? styles.childRow : ''} ${hasChildren ? styles.rowClickable : ''}`} + role={hasChildren ? 'button' : undefined} + tabIndex={hasChildren ? 0 : undefined} + aria-expanded={hasChildren ? open : undefined} + onClick={toggleOpen} + onKeyDown={(event) => { + if (!hasChildren) return + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + toggleOpen() + } + }} + > + + {hasChildren && ( + + )} + + +
    + {category.nameFa} + {category.nameEn} +
    + +
    event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + > + + + + + + + +
    +
    + + {hasChildren && open && ( +
      + {category.children.map((child) => ( + + ))} +
    + )} +
  • + ) +} diff --git a/src/components/CategoryModal.module.css b/src/components/CategoryModal.module.css new file mode 100644 index 0000000..06b424c --- /dev/null +++ b/src/components/CategoryModal.module.css @@ -0,0 +1,303 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 80; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(42, 24, 16, 0.28); + backdrop-filter: blur(14px) saturate(1.15); + -webkit-backdrop-filter: blur(14px) saturate(1.15); + animation: fadeOverlay 0.25s ease both; +} + +.modal { + position: relative; + width: 100%; + max-width: 460px; + padding: 28px 26px 24px; + border-radius: var(--radius); + background: color-mix(in srgb, var(--bg) 92%, transparent); + backdrop-filter: blur(28px) saturate(1.35); + -webkit-backdrop-filter: blur(28px) saturate(1.35); + border: 1px solid rgba(255, 255, 255, 0.75); + box-shadow: + 0 24px 60px rgba(143, 65, 12, 0.18), + inset 0 1px 0 rgba(255, 255, 255, 0.85); + overflow: hidden; + animation: riseIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.shine { + position: absolute; + inset: 0; + background: linear-gradient( + 145deg, + rgba(255, 255, 255, 0.55) 0%, + rgba(255, 255, 255, 0.12) 40%, + transparent 70% + ); + pointer-events: none; +} + +.header { + position: relative; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 22px; +} + +.eyebrow { + font-family: var(--font-en); + font-size: 0.78rem; + letter-spacing: 0.06em; + color: var(--brown); + opacity: 0.45; + margin-bottom: 4px; + direction: ltr; + text-align: end; +} + +.title { + font-size: 1.25rem; + font-weight: 700; + color: var(--brown); +} + +.closeBtn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 10px; + color: var(--text-primary); + transition: + color 0.2s, + background 0.2s; +} + +.closeBtn:hover { + color: var(--brown); + background: rgba(143, 65, 12, 0.08); +} + +.form { + position: relative; + display: flex; + flex-direction: column; + gap: 14px; +} + +.field { + position: relative; + display: flex; + flex-direction: column; + gap: 6px; +} + +.field label { + font-size: 0.8rem; + font-weight: 500; + color: var(--text-secondary); +} + +.field input { + width: 100%; + height: var(--field-height); + padding: 0 12px; + border-radius: var(--radius-sm); + border: 1px solid rgba(143, 65, 12, 0.14); + background: rgba(255, 250, 250, 0.88); + color: var(--text-primary); + font-family: inherit; + font-size: 0.92rem; + font-weight: 400; + outline: none; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +.field input::placeholder { + color: var(--text-muted); +} + +.field input:focus { + border-color: var(--brown); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 0 0 3px rgba(var(--primary-rgb) / 0.12); +} + +.field input:disabled { + opacity: 0.72; + cursor: not-allowed; +} + +.searchWrap { + position: relative; + display: flex; + align-items: center; +} + +.searchIcon { + position: absolute; + inset-inline-start: 12px; + color: var(--text-muted); + pointer-events: none; +} + +.chevron { + position: absolute; + inset-inline-end: 12px; + color: var(--text-muted); + pointer-events: none; +} + +.searchWrap input { + padding-inline: 38px 36px; +} + +.dropdown { + position: absolute; + top: calc(100% + 6px); + inset-inline: 0; + z-index: 20; + max-height: 220px; + overflow: auto; + padding: 6px; + list-style: none; + border-radius: var(--radius-sm); + background: #fffdfc; + backdrop-filter: none; + border: 1px solid var(--glass-border); + box-shadow: 0 14px 34px rgba(143, 65, 12, 0.14); +} + +.option { + width: 100%; + display: flex; + flex-direction: column; + gap: 2px; + padding: 10px 12px; + border-radius: 10px; + text-align: start; + transition: background 0.2s; +} + +.option:hover, +.optionActive { + background: rgba(143, 65, 12, 0.07); +} + +.optionFa { + font-size: 0.9rem; + font-weight: 500; + color: var(--text-primary); +} + +.optionEn { + font-family: var(--font-en); + font-size: 0.75rem; + color: var(--text-muted); + direction: ltr; + text-align: end; +} + +.emptyOption { + padding: 12px; + text-align: center; + font-size: 0.85rem; + color: var(--text-muted); +} + +.error { + font-size: 0.85rem; + color: #8b2e2e; + background: rgba(180, 60, 60, 0.08); + border: 1px solid rgba(180, 60, 60, 0.22); + border-radius: var(--radius-xs); + padding: 10px 12px; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 8px; +} + +.cancelBtn, +.submitBtn { + height: 42px; + padding: 0 18px; + border-radius: var(--radius-sm); + font-size: 0.9rem; + font-weight: 500; + transition: + background 0.2s, + transform 0.2s, + color 0.2s; +} + +.cancelBtn { + color: var(--text-primary); + background: rgba(255, 250, 250, 0.7); + border: 1px solid rgba(143, 65, 12, 0.12); +} + +.cancelBtn:hover { + color: var(--brown); + background: rgba(255, 255, 255, 0.9); +} + +.submitBtn { + color: var(--text-on-dark); + background: var(--brown); + box-shadow: 0 8px 18px rgba(143, 65, 12, 0.22); +} + +.submitBtn:hover { + background: var(--brown-dark); + transform: translateY(-1px); +} + +@keyframes fadeOverlay { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes riseIn { + from { + opacity: 0; + transform: translateY(18px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@media (max-width: 520px) { + .modal { + padding: 24px 18px 20px; + } + + .actions { + flex-direction: column-reverse; + } + + .cancelBtn, + .submitBtn { + width: 100%; + } +} diff --git a/src/components/CategoryModal.tsx b/src/components/CategoryModal.tsx new file mode 100644 index 0000000..aa4b1a5 --- /dev/null +++ b/src/components/CategoryModal.tsx @@ -0,0 +1,267 @@ +import { useEffect, useId, useMemo, useRef, useState } from 'react' +import { ChevronDown, Search, X } from 'lucide-react' +import { + findCategory, + findParentId, + flattenCategories, + type Category, +} from '../data/categories' +import styles from './CategoryModal.module.css' + +export type CategoryFormValues = { + id?: string + parentId: string | null + nameFa: string + nameEn: string +} + +type CategoryModalProps = { + open: boolean + categories: Category[] + initialParentId?: string | null + editingCategory?: Category | null + onClose: () => void + onSubmit: (values: CategoryFormValues) => void +} + +export function CategoryModal({ + open, + categories, + initialParentId = null, + editingCategory = null, + onClose, + onSubmit, +}: CategoryModalProps) { + const titleId = useId() + const isEditing = Boolean(editingCategory) + const [parentId, setParentId] = useState(initialParentId) + const [nameFa, setNameFa] = useState('') + const [nameEn, setNameEn] = useState('') + const [query, setQuery] = useState('') + const [dropdownOpen, setDropdownOpen] = useState(false) + const [error, setError] = useState('') + const searchRef = useRef(null) + + const options = useMemo(() => { + const flat = flattenCategories(categories) + if (!editingCategory) return flat + return flat.filter((option) => option.id !== editingCategory.id) + }, [categories, editingCategory]) + + const filteredOptions = useMemo(() => { + const q = query.trim().toLowerCase() + if (!q) return options + return options.filter( + (option) => + option.labelFa.includes(query.trim()) || + option.labelEn.toLowerCase().includes(q), + ) + }, [options, query]) + + const selectedParent = parentId ? findCategory(categories, parentId) : null + + useEffect(() => { + if (!open) return + if (editingCategory) { + const currentParent = findParentId(categories, editingCategory.id) + setParentId(currentParent ?? null) + setNameFa(editingCategory.nameFa) + setNameEn(editingCategory.nameEn) + } else { + setParentId(initialParentId) + setNameFa('') + setNameEn('') + } + setQuery('') + setDropdownOpen(false) + setError('') + }, [open, initialParentId, editingCategory, categories]) + + useEffect(() => { + if (!open) return + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') onClose() + } + + function handlePointerDown(event: MouseEvent) { + if (!searchRef.current?.contains(event.target as Node)) { + setDropdownOpen(false) + } + } + + document.addEventListener('keydown', handleKeyDown) + document.addEventListener('mousedown', handlePointerDown) + return () => { + document.removeEventListener('keydown', handleKeyDown) + document.removeEventListener('mousedown', handlePointerDown) + } + }, [open, onClose]) + + if (!open) return null + + function handleSubmit(event: React.FormEvent) { + event.preventDefault() + if (!nameFa.trim()) { + setError('نام فارسی الزامی است') + return + } + if (!nameEn.trim()) { + setError('نام انگلیسی الزامی است') + return + } + + onSubmit({ + id: editingCategory?.id, + parentId, + nameFa: nameFa.trim(), + nameEn: nameEn.trim(), + }) + } + + return ( +
    +
    event.stopPropagation()} + > +
    + +
    +
    +

    Categories

    +

    + {isEditing ? 'ویرایش دسته‌بندی' : 'افزودن دسته‌بندی'} +

    +
    + +
    + +
    + {error && ( +
    + {error} +
    + )} + +
    + +
    + + { + setQuery(e.target.value) + setDropdownOpen(true) + if (parentId) setParentId(null) + }} + onFocus={() => { + setDropdownOpen(true) + setQuery('') + }} + autoComplete="off" + disabled={isEditing} + /> + +
    + + {dropdownOpen && !isEditing && ( +
      +
    • + +
    • + {filteredOptions.length === 0 ? ( +
    • موردی یافت نشد
    • + ) : ( + filteredOptions.map((option) => ( +
    • + +
    • + )) + )} +
    + )} +
    + +
    + + setNameFa(e.target.value)} + autoFocus + /> +
    + +
    + + setNameEn(e.target.value)} + /> +
    + +
    + + +
    +
    +
    +
    + ) +} diff --git a/src/components/CategoryOptionsModal.module.css b/src/components/CategoryOptionsModal.module.css new file mode 100644 index 0000000..88a508d --- /dev/null +++ b/src/components/CategoryOptionsModal.module.css @@ -0,0 +1,403 @@ +.modal { + max-width: min(720px, 100%); + overflow: hidden; +} + +.categoryName { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-primary); +} + +.categoryEn { + font-family: var(--font-en); + font-size: 0.78rem; + font-weight: 400; + color: var(--text-muted); + direction: ltr; +} + +.body { + position: relative; + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 8px; + max-height: min(62vh, 560px); + overflow-x: hidden; + overflow-y: auto; + padding-inline-end: 2px; + min-width: 0; +} + +.select { + width: 100%; + height: var(--field-height); + padding: 0 12px; + border-radius: var(--radius-sm); + border: 1px solid rgba(143, 65, 12, 0.14); + background-color: rgba(255, 250, 250, 0.88); + color: var(--text-primary); + font-family: inherit; + font-size: 0.92rem; + font-weight: 400; + outline: none; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); + transition: + border-color 0.2s, + box-shadow 0.2s; + appearance: none; + background-image: linear-gradient(45deg, transparent 50%, var(--text-muted) 50%), + linear-gradient(135deg, var(--text-muted) 50%, transparent 50%); + background-position: + calc(100% - 22px) calc(50% - 3px), + calc(100% - 16px) calc(50% - 3px); + background-size: 6px 6px; + background-repeat: no-repeat; + padding-inline-end: 36px; +} + +.select:focus { + border-color: var(--brown); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 0 0 3px rgba(var(--primary-rgb) / 0.12); +} + +:dir(rtl) .select { + background-position: + 16px calc(50% - 3px), + 22px calc(50% - 3px); +} + +.listHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.listTitle { + font-size: 0.95rem; + font-weight: 500; + color: var(--brown); +} + +.subTitle { + font-size: 0.88rem; + font-weight: 500; + color: var(--brown); +} + +.addBtn { + display: inline-flex; + align-items: center; + gap: 6px; + height: 36px; + padding: 0 12px; + border-radius: 10px; + font-size: 0.82rem; + font-weight: 500; + color: var(--text-primary); + border: 1px solid rgba(143, 65, 12, 0.22); + transition: + color 0.2s, + background 0.2s; +} + +.addBtn:hover { + color: var(--brown); + background: rgba(143, 65, 12, 0.06); +} + +.addOptionBtn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + height: 48px; + border-radius: var(--radius-sm); + border: 1.5px solid rgba(143, 65, 12, 0.22); + background: transparent; + color: var(--brown); + font-size: 0.92rem; + font-weight: 500; + transition: + background 0.2s, + border-color 0.2s; +} + +.addOptionBtn:hover { + border-color: rgba(143, 65, 12, 0.4); + background: rgba(143, 65, 12, 0.05); +} + +.addAmountCard { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + height: 100%; + min-height: 48px; + padding: 12px 14px; + border-radius: var(--radius-sm); + border: 1.5px solid rgba(143, 65, 12, 0.22); + background: transparent; + color: var(--brown); + font-size: 0.85rem; + font-weight: 500; + transition: + background 0.2s, + border-color 0.2s; +} + +.addAmountCard:hover { + border-color: rgba(143, 65, 12, 0.4); + background: rgba(143, 65, 12, 0.05); +} + +.flavorList { + list-style: none; + display: flex; + flex-direction: column; + gap: 14px; +} + +.flavorBlock { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + padding: 16px; + border-radius: var(--radius-sm); + border: 1.5px solid rgba(143, 65, 12, 0.22); + background: rgba(255, 250, 250, 0.72); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); +} + +.flavorTop { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; +} + +.flavorTop .field { + margin: 0; + min-width: 0; +} + +.flavorActions { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.srOnly { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.empty { + padding: 18px 14px; + text-align: center; + font-size: 0.85rem; + color: var(--text-muted); + border-radius: var(--radius-sm); + border: 1px dashed rgba(143, 65, 12, 0.2); + background: rgba(255, 250, 250, 0.45); +} + +.list { + list-style: none; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(168px, 100%), 1fr)); + gap: 10px; + min-width: 0; + align-items: stretch; +} + +.addAmountItem { + display: flex; + min-width: 0; +} + +.row { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-width: 0; + height: 100%; + min-height: 48px; + padding: 12px 14px; + border-radius: var(--radius-sm); + border: 1.5px solid rgba(143, 65, 12, 0.22); + background: rgba(255, 252, 252, 0.95); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); + transition: + border-color 0.2s, + background 0.2s; +} + +.row:hover, +.row:focus-within { + border-color: rgba(143, 65, 12, 0.4); + background: #fff; +} + +.rowText { + display: flex; + flex-wrap: nowrap; + align-items: baseline; + justify-content: center; + gap: 8px; + min-width: 0; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.amount { + font-size: 0.85rem; + font-weight: 500; + color: var(--text-primary); + line-height: 1.35; + flex-shrink: 0; +} + +.price { + font-size: 0.78rem; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; + line-height: 1.35; + flex-shrink: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.rowActions { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + border-radius: inherit; + background: rgba(251, 243, 243, 0.88); + backdrop-filter: blur(4px); + opacity: 0; + pointer-events: none; + transition: opacity 0.18s; +} + +.row:hover .rowActions, +.row:focus-within .rowActions { + opacity: 1; + pointer-events: auto; +} + +.iconBtn { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 10px; + color: var(--text-primary); + transition: + color 0.2s, + background 0.2s; +} + +.iconBtn:hover { + color: var(--brown); + background: rgba(143, 65, 12, 0.07); +} + +.rowActions .iconBtn { + background: rgba(255, 250, 250, 0.95); + border: 1px solid rgba(143, 65, 12, 0.14); +} + +.rowActions .iconBtn:hover { + background: #fff; +} + +.removeBtn:hover { + color: #9b2c2c; + background: rgba(155, 44, 44, 0.08); +} + +.rowActions .removeBtn:hover { + background: rgba(255, 250, 250, 0.95); + border-color: rgba(155, 44, 44, 0.28); +} + +.iconBtn[data-tooltip]::after { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + right: auto; + z-index: 3; + padding: 4px 8px; + border-radius: 8px; + background: rgba(42, 24, 16, 0.9); + color: #fffafa; + font-size: 0.7rem; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transform: translateX(-50%); + transition: opacity 0.15s; +} + +.rowActions .iconBtn[data-tooltip]::after { + top: calc(100% + 6px); + bottom: auto; +} + +.iconBtn[data-tooltip]:hover::after, +.iconBtn[data-tooltip]:focus-visible::after { + opacity: 1; +} + +@media (max-width: 520px) { + .flavorTop { + grid-template-columns: 1fr; + } + + .flavorActions { + justify-content: flex-end; + } + + .list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (prefers-reduced-motion: reduce) { + .row, + .rowActions, + .iconBtn { + transition: none; + } +} diff --git a/src/components/CategoryOptionsModal.tsx b/src/components/CategoryOptionsModal.tsx new file mode 100644 index 0000000..3f516a8 --- /dev/null +++ b/src/components/CategoryOptionsModal.tsx @@ -0,0 +1,327 @@ +import { useEffect, useId, useState } from 'react' +import { Copy, Pencil, Plus, Trash2, X } from 'lucide-react' +import type { Category } from '../data/categories' +import { + createFlavorBlock, + createFlavorEntry, + flavors, + formatPrice, + type CategoryFlavorOptions, + type FlavorBlock, +} from '../data/flavors' +import { + AmountPriceModal, + type AmountPriceValues, +} from './AmountPriceModal' +import styles from './CategoryModal.module.css' +import optionStyles from './CategoryOptionsModal.module.css' + +type CategoryOptionsModalProps = { + open: boolean + category: Category | null + initialOptions?: CategoryFlavorOptions + onClose: () => void + onSave: (categoryId: string, options: CategoryFlavorOptions) => void +} + +type EntryDraft = { + blockId: string + mode: 'create' | 'edit' + entryId?: string + values: AmountPriceValues | null +} + +export function CategoryOptionsModal({ + open, + category, + initialOptions = [], + onClose, + onSave, +}: CategoryOptionsModalProps) { + const titleId = useId() + const [blocks, setBlocks] = useState([]) + const [draft, setDraft] = useState(null) + + useEffect(() => { + if (!open || !category) return + const cloned = structuredClone(initialOptions) + setBlocks(cloned.length > 0 ? cloned : [createFlavorBlock()]) + setDraft(null) + // Reset only when the dialog opens for a category + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, category?.id]) + + useEffect(() => { + if (!open) return + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape' && !draft) onClose() + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [open, draft, onClose]) + + if (!open || !category) return null + + function updateBlock( + blockId: string, + updater: (block: FlavorBlock) => FlavorBlock, + ) { + setBlocks((current) => + current.map((block) => (block.id === blockId ? updater(block) : block)), + ) + } + + function handleAddFlavor() { + setBlocks((current) => [...current, createFlavorBlock()]) + } + + function handleDuplicateFlavor(block: FlavorBlock) { + setBlocks((current) => [ + ...current, + { + ...structuredClone(block), + id: createFlavorBlock().id, + }, + ]) + } + + function handleRemoveFlavor(blockId: string) { + setBlocks((current) => { + const next = current.filter((block) => block.id !== blockId) + return next.length > 0 ? next : [createFlavorBlock()] + }) + } + + function handleEntrySubmit(values: AmountPriceValues) { + if (!draft) return + const price = Number(values.price) + + updateBlock(draft.blockId, (block) => { + if (draft.mode === 'edit' && draft.entryId) { + return { + ...block, + entries: block.entries.map((entry) => + entry.id === draft.entryId + ? { ...entry, amount: values.amount, price } + : entry, + ), + } + } + + return { + ...block, + entries: [...block.entries, createFlavorEntry(values.amount, price)], + } + }) + + setDraft(null) + } + + function handleRemoveEntry(blockId: string, entryId: string) { + updateBlock(blockId, (block) => ({ + ...block, + entries: block.entries.filter((entry) => entry.id !== entryId), + })) + } + + function handleSave() { + onSave(category.id, blocks) + } + + return ( + <> +
    +
    event.stopPropagation()} + > +
    + +
    +
    +

    Flavor Duplicator

    +

    + آپشن‌ها +

    +

    + {category.nameFa} + + {category.nameEn} + +

    +
    + +
    + +
    +
      + {blocks.map((block) => ( +
    • +
      +
      + + +
      + +
      + + +
      +
      + +
        + {block.entries.map((entry) => ( +
      • +
        + + {entry.amount} + + + {formatPrice(entry.price)} تومان + +
        +
        + + +
        +
      • + ))} +
      • + +
      • +
      +
    • + ))} +
    + + +
    + +
    + + +
    +
    +
    + + setDraft(null)} + onSubmit={handleEntrySubmit} + /> + + ) +} diff --git a/src/components/ChangePasswordModal.module.css b/src/components/ChangePasswordModal.module.css new file mode 100644 index 0000000..814e718 --- /dev/null +++ b/src/components/ChangePasswordModal.module.css @@ -0,0 +1,9 @@ +.modal { + max-width: 420px; +} + +.userName { + margin-top: 8px; + font-size: 0.9rem; + color: var(--text-secondary); +} diff --git a/src/components/ChangePasswordModal.tsx b/src/components/ChangePasswordModal.tsx new file mode 100644 index 0000000..0163cd8 --- /dev/null +++ b/src/components/ChangePasswordModal.tsx @@ -0,0 +1,137 @@ +import { useEffect, useId, useState } from 'react' +import { X } from 'lucide-react' +import styles from './CategoryModal.module.css' +import localStyles from './ChangePasswordModal.module.css' + +type ChangePasswordModalProps = { + open: boolean + userName: string + onClose: () => void + onSubmit: (password: string) => void +} + +export function ChangePasswordModal({ + open, + userName, + onClose, + onSubmit, +}: ChangePasswordModalProps) { + const titleId = useId() + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + if (!open) return + setPassword('') + setConfirmPassword('') + setError('') + }, [open]) + + useEffect(() => { + if (!open) return + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') onClose() + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [open, onClose]) + + if (!open) return null + + function handleSubmit(event: React.FormEvent) { + event.preventDefault() + if (!password.trim()) { + setError('رمز عبور جدید الزامی است') + return + } + if (password !== confirmPassword) { + setError('تکرار رمز عبور مطابقت ندارد') + return + } + + onSubmit(password) + } + + return ( +
    +
    event.stopPropagation()} + > +
    + +
    +
    +

    Password

    +

    + تغییر رمز عبور +

    + {userName &&

    {userName}

    } +
    + +
    + +
    + {error && ( +
    + {error} +
    + )} + +
    + + setPassword(e.target.value)} + autoFocus + /> +
    + +
    + + setConfirmPassword(e.target.value)} + /> +
    + +
    + + +
    +
    +
    +
    + ) +} diff --git a/src/components/CreateOrderModal.module.css b/src/components/CreateOrderModal.module.css new file mode 100644 index 0000000..02befc9 --- /dev/null +++ b/src/components/CreateOrderModal.module.css @@ -0,0 +1,342 @@ +.modal { + max-width: 720px; + max-height: min(90vh, 760px); + overflow: auto; +} + +.stepLabel { + margin-top: 8px; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.searchField { + position: relative; + z-index: 1; +} + +.searchFieldOpen { + z-index: 30; +} + +.lines { + position: relative; + list-style: none; + display: flex; + flex-direction: column; + gap: 8px; + margin: 0 0 14px; + padding: 0; +} + +.line { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + border-radius: var(--radius-sm); + background: rgba(255, 250, 250, 0.72); + border: 1px solid rgba(143, 65, 12, 0.1); +} + +.lineBody { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} + +.lineName { + font-size: 0.9rem; + font-weight: 500; + color: var(--text-primary); +} + +.lineOption { + font-size: 0.78rem; + color: var(--brown); +} + +.lineMeta { + font-size: 0.8rem; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.removeBtn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + flex-shrink: 0; + border-radius: 10px; + color: var(--text-muted); + transition: + color 0.2s, + background 0.2s; +} + +.removeBtn:hover { + color: #9b2c2c; + background: rgba(155, 44, 44, 0.08); +} + +.note { + width: 100%; + padding: 10px 12px; + border-radius: var(--radius-sm); + border: 1px solid rgba(143, 65, 12, 0.14); + background: rgba(255, 250, 250, 0.88); + color: var(--text-primary); + font-family: inherit; + font-size: 0.92rem; + font-weight: 400; + line-height: 1.7; + resize: vertical; + outline: none; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +.note::placeholder { + color: var(--text-muted); +} + +.note:focus { + border-color: var(--brown); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 0 0 3px rgba(var(--primary-rgb) / 0.12); +} + +.summary { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; + padding: 12px 14px; + border-radius: var(--radius-sm); + background: rgba(143, 65, 12, 0.06); + border: 1px solid rgba(143, 65, 12, 0.14); + font-size: 0.9rem; + font-weight: 500; + color: var(--brown); + font-variant-numeric: tabular-nums; +} + +.deliveryType { + position: relative; + margin: 0 0 16px; + padding: 0; + border: 0; +} + +.deliveryType legend { + margin-bottom: 10px; + font-size: 0.8rem; + font-weight: 500; + color: var(--text-secondary); +} + +.typeRow { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.typeCard { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 14px 12px; + border-radius: var(--radius-sm); + border: 1.5px solid rgba(143, 65, 12, 0.18); + background: rgba(255, 250, 250, 0.72); + font-size: 0.92rem; + font-weight: 500; + color: var(--text-primary); + cursor: pointer; + transition: + border-color 0.2s, + background 0.2s, + color 0.2s; +} + +.typeCard input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.typeCard:hover { + border-color: rgba(143, 65, 12, 0.35); +} + +.typeCardActive { + border-color: var(--brown); + background: rgba(143, 65, 12, 0.08); + color: var(--brown); +} + +.select { + width: 100%; + height: var(--field-height); + padding: 0 12px; + padding-inline-start: 36px; + border-radius: var(--radius-sm); + border: 1px solid rgba(143, 65, 12, 0.14); + background-color: rgba(255, 250, 250, 0.88); + color: var(--text-primary); + font-family: inherit; + font-size: 0.92rem; + font-weight: 400; + outline: none; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%8f410c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: left 12px center; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +.select:focus { + border-color: var(--brown); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 0 0 3px rgba(var(--primary-rgb) / 0.12); +} + +.addressSection { + position: relative; + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 8px; +} + +.addressHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + font-size: 0.8rem; + font-weight: 500; + color: var(--text-secondary); +} + +.addAddressBtn { + color: var(--brown); + font-size: 0.85rem; + font-weight: 500; + padding: 4px 2px; + transition: opacity 0.2s; +} + +.addAddressBtn:hover { + opacity: 0.75; +} + +.addressList { + list-style: none; + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; + padding: 0; +} + +.addressCard { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; + gap: 0 8px; + padding: 12px 14px; + border-radius: var(--radius-sm); + border: 1.5px solid rgba(143, 65, 12, 0.16); + background: rgba(255, 250, 250, 0.72); + cursor: pointer; + transition: + border-color 0.2s, + background 0.2s; +} + +.addressCard input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.addressCard:hover { + border-color: rgba(143, 65, 12, 0.32); +} + +.addressCardActive { + border-color: var(--brown); + background: rgba(143, 65, 12, 0.08); +} + +.addressName { + font-size: 0.9rem; + font-weight: 500; + color: var(--brown); + white-space: nowrap; +} + +.addressMeta, +.addressLine, +.addressPhone { + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.5; +} + +.addressLine { + color: var(--text-primary); + min-width: 0; +} + +.addressPhone { + color: var(--text-muted); + font-variant-numeric: tabular-nums; + white-space: nowrap; + direction: ltr; +} + +.addressSep { + color: var(--text-muted); + font-size: 0.85rem; + user-select: none; +} + +.newAddress { + display: flex; + flex-direction: column; + gap: 14px; + padding: 12px; + border-radius: var(--radius-sm); + border: 1px dashed rgba(143, 65, 12, 0.22); + background: rgba(255, 250, 250, 0.5); +} + +.addressActions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +@media (max-width: 520px) { + .typeRow { + grid-template-columns: 1fr; + } +} diff --git a/src/components/CreateOrderModal.tsx b/src/components/CreateOrderModal.tsx new file mode 100644 index 0000000..52adfa1 --- /dev/null +++ b/src/components/CreateOrderModal.tsx @@ -0,0 +1,816 @@ +import { useEffect, useId, useRef, useState } from 'react' +import { Search, Trash2, X } from 'lucide-react' +import { + searchProducts, + searchUsers, + type SearchUserResult, +} from '../lib/orderSearch' +import { getBranches } from '../lib/branchesStore' +import { + addUserAddress, + getUserAddresses, +} from '../lib/userAddressesStore' +import { districts } from '../data/districts' +import { + getItemLineTotal, + orderSellUnitLabel, + type DeliveryType, + type Order, + type OrderDelivery, + type OrderItem, + type ShippingAddress, +} from '../data/orders' +import type { Product } from '../data/products' +import { formatPrice } from '../utils/price' +import { + OrderLineItemModal, + type OrderLineDraft, +} from './OrderLineItemModal' +import styles from './CategoryModal.module.css' +import localStyles from './CreateOrderModal.module.css' + +export type CreateOrderValues = { + customer: SearchUserResult + items: OrderItem[] + note: string + delivery: OrderDelivery +} + +type CreateOrderModalProps = { + open: boolean + onClose: () => void + onSubmit: (values: CreateOrderValues) => void +} + +type SelectedLine = OrderLineDraft & { key: string } +type Step = 1 | 2 + +function lineKey() { + return `${Date.now()}-${Math.random().toString(36).slice(2, 7)}` +} + +const emptyAddressForm = { + name: '', + district: '', + address: '', + landline: '', +} + +export function CreateOrderModal({ + open, + onClose, + onSubmit, +}: CreateOrderModalProps) { + const titleId = useId() + const userRef = useRef(null) + const productRef = useRef(null) + + const [step, setStep] = useState(1) + + const [customer, setCustomer] = useState(null) + const [userQuery, setUserQuery] = useState('') + const [userOpen, setUserOpen] = useState(false) + const [userResults, setUserResults] = useState([]) + const [userLoading, setUserLoading] = useState(false) + + const [productQuery, setProductQuery] = useState('') + const [productOpen, setProductOpen] = useState(false) + const [productResults, setProductResults] = useState([]) + const [productLoading, setProductLoading] = useState(false) + + const [lines, setLines] = useState([]) + const [pickingProduct, setPickingProduct] = useState(null) + const [note, setNote] = useState('') + const [error, setError] = useState('') + + const [deliveryType, setDeliveryType] = useState('pickup') + const [branchId, setBranchId] = useState('') + const [addresses, setAddresses] = useState([]) + const [selectedAddressId, setSelectedAddressId] = useState('') + const [addingAddress, setAddingAddress] = useState(false) + const [addressForm, setAddressForm] = useState(emptyAddressForm) + + const branches = getBranches() + + useEffect(() => { + if (!open) return + setStep(1) + setCustomer(null) + setUserQuery('') + setUserOpen(false) + setUserResults([]) + setProductQuery('') + setProductOpen(false) + setProductResults([]) + setLines([]) + setPickingProduct(null) + setNote('') + setError('') + setDeliveryType('pickup') + setBranchId(getBranches()[0]?.id ?? '') + setAddresses([]) + setSelectedAddressId('') + setAddingAddress(false) + setAddressForm(emptyAddressForm) + }, [open]) + + useEffect(() => { + if (!open || !customer) return + const list = getUserAddresses(customer.id) + setAddresses(list) + setSelectedAddressId(list[0]?.id ?? '') + setAddingAddress(false) + setAddressForm(emptyAddressForm) + }, [open, customer]) + + useEffect(() => { + if (!open) return + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape' && !pickingProduct) onClose() + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [open, pickingProduct, onClose]) + + useEffect(() => { + function handlePointerDown(event: MouseEvent) { + const target = event.target as Node + if (!userRef.current?.contains(target)) setUserOpen(false) + if (!productRef.current?.contains(target)) setProductOpen(false) + } + document.addEventListener('mousedown', handlePointerDown) + return () => document.removeEventListener('mousedown', handlePointerDown) + }, []) + + useEffect(() => { + if (!open || !userOpen || step !== 1) return + let cancelled = false + setUserLoading(true) + const timer = window.setTimeout(() => { + void searchUsers(userQuery).then((results) => { + if (cancelled) return + setUserResults(results) + setUserLoading(false) + }) + }, 180) + return () => { + cancelled = true + window.clearTimeout(timer) + } + }, [open, userOpen, userQuery, step]) + + useEffect(() => { + if (!open || !productOpen || step !== 1) return + let cancelled = false + setProductLoading(true) + const timer = window.setTimeout(() => { + void searchProducts(productQuery).then((results) => { + if (cancelled) return + setProductResults(results) + setProductLoading(false) + }) + }, 180) + return () => { + cancelled = true + window.clearTimeout(timer) + } + }, [open, productOpen, productQuery, step]) + + if (!open) return null + + const itemCount = lines.reduce((sum, line) => sum + line.quantity, 0) + const totalPrice = lines.reduce((sum, line) => { + const item: OrderItem = { + id: line.key, + nameFa: line.product.nameFa, + quantity: line.quantity, + unitPrice: line.product.price, + sellUnit: line.product.sellUnit, + options: line.options.map((option) => ({ + name: option.name, + price: option.price, + })), + } + return sum + getItemLineTotal(item) + }, 0) + + function buildItems(): OrderItem[] { + return lines.map((line) => ({ + id: line.key, + nameFa: line.product.nameFa, + quantity: line.quantity, + unitPrice: line.product.price, + sellUnit: line.product.sellUnit, + options: + line.options.length > 0 + ? line.options.map((option) => ({ + name: option.name, + price: option.price, + })) + : undefined, + })) + } + + function handleLineSubmit(draft: OrderLineDraft) { + setLines((current) => [...current, { ...draft, key: lineKey() }]) + setPickingProduct(null) + setProductQuery('') + setProductOpen(false) + } + + function handleContinue(event: React.FormEvent) { + event.preventDefault() + if (!customer) { + setError('انتخاب کاربر الزامی است') + return + } + if (lines.length === 0) { + setError('حداقل یک محصول اضافه کنید') + return + } + setError('') + setStep(2) + } + + function handleAddAddress(event: React.FormEvent) { + event.preventDefault() + if (!customer) return + if (!addressForm.name.trim()) { + setError('نام آدرس الزامی است') + return + } + if (!addressForm.district) { + setError('انتخاب منطقه الزامی است') + return + } + if (!addressForm.address.trim()) { + setError('آدرس کامل الزامی است') + return + } + + const created = addUserAddress(customer.id, { + name: addressForm.name.trim(), + district: addressForm.district, + address: addressForm.address.trim(), + landline: addressForm.landline.trim(), + }) + setAddresses(getUserAddresses(customer.id)) + setSelectedAddressId(created.id) + setAddingAddress(false) + setAddressForm(emptyAddressForm) + setError('') + } + + function handleFinalSubmit(event: React.FormEvent) { + event.preventDefault() + if (!customer) { + setError('انتخاب کاربر الزامی است') + return + } + + if (deliveryType === 'pickup') { + const branch = branches.find((item) => item.id === branchId) + if (!branch) { + setError('انتخاب شعبه الزامی است') + return + } + onSubmit({ + customer, + note: note.trim(), + items: buildItems(), + delivery: { type: 'pickup', branch: branch.name }, + }) + return + } + + const selected = addresses.find((item) => item.id === selectedAddressId) + if (!selected) { + setError('انتخاب یا افزودن آدرس الزامی است') + return + } + + onSubmit({ + customer, + note: note.trim(), + items: buildItems(), + delivery: { type: 'shipping', shippingAddress: selected }, + }) + } + + return ( + <> +
    +
    event.stopPropagation()} + > +
    + +
    +
    +

    New Order

    +

    + ثبت سفارش جدید +

    +

    + مرحله {step === 1 ? '۱ از ۲ — اقلام سفارش' : '۲ از ۲ — نحوه تحویل'} +

    +
    + +
    + + {step === 1 ? ( +
    + {error && ( +
    + {error} +
    + )} + +
    + +
    + + { + setUserQuery(e.target.value) + setUserOpen(true) + if (customer) setCustomer(null) + }} + onFocus={() => { + setUserOpen(true) + setUserQuery('') + }} + autoComplete="off" + /> +
    + + {userOpen && ( +
      + {userLoading ? ( +
    • در حال جستجو...
    • + ) : userResults.length === 0 ? ( +
    • موردی یافت نشد
    • + ) : ( + userResults.map((user) => ( +
    • + +
    • + )) + )} +
    + )} +
    + +
    + +
    + + { + setProductQuery(e.target.value) + setProductOpen(true) + }} + onFocus={() => setProductOpen(true)} + autoComplete="off" + /> +
    + + {productOpen && ( +
      + {productLoading ? ( +
    • در حال جستجو...
    • + ) : productResults.length === 0 ? ( +
    • موردی یافت نشد
    • + ) : ( + productResults.map((product) => ( +
    • + +
    • + )) + )} +
    + )} +
    + + {lines.length > 0 && ( +
      + {lines.map((line) => { + const optionsTotal = line.options.reduce( + (sum, option) => sum + option.price, + 0, + ) + const lineTotal = + line.quantity * (line.product.price + optionsTotal) + return ( +
    • +
      + + {line.product.nameFa} + + {line.options.map((option) => ( + + {option.name} + + ))} + + {formatPrice(line.quantity)}{' '} + {orderSellUnitLabel[line.product.sellUnit]} —{' '} + {formatPrice(lineTotal)} تومان + +
      + +
    • + ) + })} +
    + )} + +
    + +