commit 35939e6ee4aa48a6caa0379c6578c991a740baca Author: Alireza Hassani Date: Wed Aug 5 15:15:01 2026 +0330 Initial commit of Balout Pastry public website. Co-authored-by: Cursor diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..61bcbaa --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=http://localhost:3100/api/v1 +VITE_CUSTOMER_APP_URL=http://customer.baloutpastry.com:5173 +VITE_COOKIE_DOMAIN=.baloutpastry.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4513a15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# 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.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..259fbde --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,155 @@ +# Balout Pastry — Website (setup context) + +Public storefront for شیرینی‌فروشی بلوط. React + Vite SPA that browses products and places orders via the NestJS API. Design matched to [shirinibalout.com](https://www.shirinibalout.com/). + +Repo: `https://git.meshkee.com/BaloutPastry/website.git` +API repo: `https://git.meshkee.com/BaloutPastry/backend.git` +Dashboards (customer login / account): `https://git.meshkee.com/BaloutPastry/dashboards.git` + +## Stack + +- React 19 + TypeScript + Vite 8 +- React Router 7 +- Lucide icons +- Cart in `localStorage` (`balout.cart`) +- Auth session cookie `balout.auth` (shared with customer dashboard on `*.baloutpastry.com`) +- API client: `src/lib/api.ts` → `VITE_API_BASE_URL` + +## Prerequisites + +- Node.js **20+** (LTS recommended) +- npm +- Running **Backend** API on port **3100** +- Optional: customer dashboard on **5173** for login / account (SSO via cookie or hash handoff) + +## Setup on a new device + +```bash +git clone https://git.meshkee.com/BaloutPastry/website.git +cd website + +cp .env.example .env +# Edit if API / customer app URLs differ + +npm install +npm run dev +``` + +Open **http://localhost:5174** (or **http://baloutpastry.com:5174** with local DNS). + +### Local hosts (recommended) + +Add to `/etc/hosts`: + +``` +127.0.0.1 baloutpastry.com www.baloutpastry.com +127.0.0.1 admin.baloutpastry.com customer.baloutpastry.com +``` + +Then: + +| App | URL | +|-----|-----| +| Website | `http://baloutpastry.com:5174` | +| Customer dashboard | `http://customer.baloutpastry.com:5173` | +| Admin dashboard | `http://admin.baloutpastry.com:5173` | +| API | `http://localhost:3100/api/v1` | + +Backend `CORS_ORIGIN` must include the website origin(s), e.g. +`http://baloutpastry.com:5174,http://www.baloutpastry.com:5174,http://localhost:5174` + +### Backend must be up + +From the backend repo: + +```bash +cp .env.example .env +npm install +npm run db:up +npx prisma migrate deploy +npm run start:dev # http://localhost:3100/api/v1 +``` + +## Scripts + +| Command | What it does | +|---------|----------------| +| `npm run dev` | Vite dev server → **http://localhost:5174** | +| `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 include `/api/v1` | +| `VITE_CUSTOMER_APP_URL` | no | `http://customer.baloutpastry.com:5173` | Login / profile redirects | +| `VITE_COOKIE_DOMAIN` | no | `.baloutpastry.com` (when on that host) | Shared auth cookie domain | + +Copy from `.env.example`. Do **not** commit `.env`. + +After changing env vars, restart `npm run dev`. + +## App routes + +| Path | Page | +|------|------| +| `/` | Home (hero, categories, featured products) | +| `/products` | Product list + filters | +| `/products/:slugOrId` | Category by slug **or** product detail by id | +| `/quick-info` | Quick info | +| `/about-us` | About | +| `/contact-us` | Contact | + +Cart drawer and toasts are global (not separate routes). Checkout / login hand off to the customer dashboard when needed. + +## Auth / cart notes + +- Guest browsing and cart work without login +- Session cookie: `balout.auth` (also accepts `#balout_auth=…` hash handoff from customer app) +- Access token sent as `Authorization: Bearer …` on authenticated API calls +- Cart key: `balout.cart` in `localStorage`; change event `balout:cart-change` +- Prices are integer **تومان** + +## Project layout + +``` +src/ + App.tsx Routes + shell (Header / Footer / Cart / Toast) + pages/ Home, products, about, contact, quick-info + components/ Header, Footer, CartDrawer, ProductCard, … + lib/ + api.ts HTTP client + catalog / checkout helpers + auth.ts Cookie session + customer-app redirects + cart.ts localStorage cart + types.ts Shared types + formatPriceFa + ids.ts cuid vs category-slug detection +``` + +## Production build + +```bash +cp .env.example .env # set real API + customer app URLs +npm ci +npm run build +# Serve dist/ behind nginx/Caddy, or: +npm run preview +``` + +Set `VITE_*` **before** `npm run build` (they are baked into the bundle). + +## Common issues + +| Symptom | Likely cause | +|---------|----------------| +| Empty catalog / fetch errors | Backend not running, or wrong `VITE_API_BASE_URL` | +| CORS errors in browser | Backend `CORS_ORIGIN` missing website origin (port **5174**) | +| Port already in use | Another process on **5174** (`strictPort: true`) | +| Login redirect fails | Customer dashboard down, or wrong `VITE_CUSTOMER_APP_URL` | +| Auth not shared across apps | Missing `/etc/hosts` + `VITE_COOKIE_DOMAIN=.baloutpastry.com` | + +## Related + +- Backend setup: clone `BaloutPastry/backend` and read `CONTEXT.md` +- Customer / admin UI: clone `BaloutPastry/dashboards` and read `CONTEXT.md` diff --git a/README.md b/README.md new file mode 100644 index 0000000..501c98e --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Balout Website + +Public storefront for شیرینی بلوط — design matched to [shirinibalout.com](https://www.shirinibalout.com/). + +```bash +cd Website +npm install +npm run dev +``` + +Opens at http://localhost:5173 diff --git a/index.html b/index.html new file mode 100644 index 0000000..f2b351b --- /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..605b104 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1341 @@ +{ + "name": "balout-website", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "balout-website", + "version": "0.0.0", + "dependencies": { + "lucide-react": "^0.511.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "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/@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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.77.0.tgz", + "integrity": "sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.77.0.tgz", + "integrity": "sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.77.0.tgz", + "integrity": "sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.77.0.tgz", + "integrity": "sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.77.0.tgz", + "integrity": "sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.77.0.tgz", + "integrity": "sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.77.0.tgz", + "integrity": "sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.77.0.tgz", + "integrity": "sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.77.0.tgz", + "integrity": "sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.77.0.tgz", + "integrity": "sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.77.0.tgz", + "integrity": "sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.77.0.tgz", + "integrity": "sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.77.0.tgz", + "integrity": "sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.77.0.tgz", + "integrity": "sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.77.0.tgz", + "integrity": "sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.77.0.tgz", + "integrity": "sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.77.0.tgz", + "integrity": "sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.77.0.tgz", + "integrity": "sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==", + "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.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.77.0.tgz", + "integrity": "sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz", + "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz", + "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz", + "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz", + "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz", + "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==", + "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.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==", + "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/@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": "0.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "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/oxlint": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.77.0.tgz", + "integrity": "sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==", + "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.77.0", + "@oxlint/binding-android-arm64": "1.77.0", + "@oxlint/binding-darwin-arm64": "1.77.0", + "@oxlint/binding-darwin-x64": "1.77.0", + "@oxlint/binding-freebsd-x64": "1.77.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.77.0", + "@oxlint/binding-linux-arm-musleabihf": "1.77.0", + "@oxlint/binding-linux-arm64-gnu": "1.77.0", + "@oxlint/binding-linux-arm64-musl": "1.77.0", + "@oxlint/binding-linux-ppc64-gnu": "1.77.0", + "@oxlint/binding-linux-riscv64-gnu": "1.77.0", + "@oxlint/binding-linux-riscv64-musl": "1.77.0", + "@oxlint/binding-linux-s390x-gnu": "1.77.0", + "@oxlint/binding-linux-x64-gnu": "1.77.0", + "@oxlint/binding-linux-x64-musl": "1.77.0", + "@oxlint/binding-openharmony-arm64": "1.77.0", + "@oxlint/binding-win32-arm64-msvc": "1.77.0", + "@oxlint/binding-win32-ia32-msvc": "1.77.0", + "@oxlint/binding-win32-x64-msvc": "1.77.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-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.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz", + "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==", + "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.2", + "@rolldown/binding-darwin-arm64": "1.2.2", + "@rolldown/binding-darwin-x64": "1.2.2", + "@rolldown/binding-freebsd-x64": "1.2.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.2", + "@rolldown/binding-linux-arm64-gnu": "1.2.2", + "@rolldown/binding-linux-arm64-musl": "1.2.2", + "@rolldown/binding-linux-ppc64-gnu": "1.2.2", + "@rolldown/binding-linux-s390x-gnu": "1.2.2", + "@rolldown/binding-linux-x64-gnu": "1.2.2", + "@rolldown/binding-linux-x64-musl": "1.2.2", + "@rolldown/binding-openharmony-arm64": "1.2.2", + "@rolldown/binding-win32-arm64-msvc": "1.2.2", + "@rolldown/binding-win32-x64-msvc": "1.2.2" + } + }, + "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/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..7710624 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "balout-website", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8", + "lucide-react": "^0.511.0", + "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" + } +} \ No newline at end of file diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..6371726 Binary files /dev/null and b/public/favicon.png differ diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..401d963 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,33 @@ +import { Route, Routes } from 'react-router-dom' +import Header from './components/Header' +import Footer from './components/Footer' +import ToastHost from './components/ToastHost' +import CartDrawer from './components/CartDrawer' +import HomePage from './pages/HomePage' +import QuickInfoPage from './pages/QuickInfoPage' +import ContactUsPage from './pages/ContactUsPage' +import AboutUsPage from './pages/AboutUsPage' +import ProductsListPage from './pages/ProductsListPage' +import ProductsSlugOrDetailPage from './pages/ProductsSlugOrDetailPage' + +export default function App() { + return ( +
+
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ + +
+ ) +} diff --git a/src/assets/fonts/Cailyne.ttf b/src/assets/fonts/Cailyne.ttf new file mode 100644 index 0000000..a61ccd9 Binary files /dev/null and b/src/assets/fonts/Cailyne.ttf differ diff --git a/src/assets/fonts/Doran-Bold.woff2 b/src/assets/fonts/Doran-Bold.woff2 new file mode 100644 index 0000000..604a910 Binary files /dev/null and b/src/assets/fonts/Doran-Bold.woff2 differ diff --git a/src/assets/fonts/Doran-Light.woff2 b/src/assets/fonts/Doran-Light.woff2 new file mode 100644 index 0000000..7add446 Binary files /dev/null and b/src/assets/fonts/Doran-Light.woff2 differ diff --git a/src/assets/fonts/Doran-Medium.woff2 b/src/assets/fonts/Doran-Medium.woff2 new file mode 100644 index 0000000..3e29426 Binary files /dev/null and b/src/assets/fonts/Doran-Medium.woff2 differ diff --git a/src/assets/fonts/Doran-Regular.woff2 b/src/assets/fonts/Doran-Regular.woff2 new file mode 100644 index 0000000..81fbe9a Binary files /dev/null and b/src/assets/fonts/Doran-Regular.woff2 differ diff --git a/src/assets/images/Box.svg b/src/assets/images/Box.svg new file mode 100644 index 0000000..518551a --- /dev/null +++ b/src/assets/images/Box.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/Cake.svg b/src/assets/images/Cake.svg new file mode 100644 index 0000000..09dc3de --- /dev/null +++ b/src/assets/images/Cake.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/images/ChatTeardropDots.svg b/src/assets/images/ChatTeardropDots.svg new file mode 100644 index 0000000..d042917 --- /dev/null +++ b/src/assets/images/ChatTeardropDots.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/images/DefaultPhoto.png b/src/assets/images/DefaultPhoto.png new file mode 100644 index 0000000..b2b63cf Binary files /dev/null and b/src/assets/images/DefaultPhoto.png differ diff --git a/src/assets/images/FlatLay.png b/src/assets/images/FlatLay.png new file mode 100644 index 0000000..7f9c2ed Binary files /dev/null and b/src/assets/images/FlatLay.png differ diff --git a/src/assets/images/Handbag.svg b/src/assets/images/Handbag.svg new file mode 100644 index 0000000..1ba1205 --- /dev/null +++ b/src/assets/images/Handbag.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/Info.svg b/src/assets/images/Info.svg new file mode 100644 index 0000000..0cf24ba --- /dev/null +++ b/src/assets/images/Info.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/images/Waves.svg b/src/assets/images/Waves.svg new file mode 100644 index 0000000..1d2c476 --- /dev/null +++ b/src/assets/images/Waves.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/images/backImage1.png b/src/assets/images/backImage1.png new file mode 100644 index 0000000..ba2c080 Binary files /dev/null and b/src/assets/images/backImage1.png differ diff --git a/src/assets/images/brown-macaron-left.png b/src/assets/images/brown-macaron-left.png new file mode 100644 index 0000000..a71c0a0 Binary files /dev/null and b/src/assets/images/brown-macaron-left.png differ diff --git a/src/assets/images/brown-macaron-right.png b/src/assets/images/brown-macaron-right.png new file mode 100644 index 0000000..ad439db Binary files /dev/null and b/src/assets/images/brown-macaron-right.png differ diff --git a/src/assets/images/chocolate-cake.png b/src/assets/images/chocolate-cake.png new file mode 100644 index 0000000..b8753af Binary files /dev/null and b/src/assets/images/chocolate-cake.png differ diff --git a/src/assets/images/cookies.png b/src/assets/images/cookies.png new file mode 100644 index 0000000..34cf179 Binary files /dev/null and b/src/assets/images/cookies.png differ diff --git a/src/assets/images/delicious-decorate.png b/src/assets/images/delicious-decorate.png new file mode 100644 index 0000000..faba412 Binary files /dev/null and b/src/assets/images/delicious-decorate.png differ diff --git a/src/assets/images/derakht-balout.png b/src/assets/images/derakht-balout.png new file mode 100644 index 0000000..31ccd6d Binary files /dev/null and b/src/assets/images/derakht-balout.png differ diff --git a/src/assets/images/gradiantCircle.png b/src/assets/images/gradiantCircle.png new file mode 100644 index 0000000..cb856b5 Binary files /dev/null and b/src/assets/images/gradiantCircle.png differ diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000..6371726 Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/assets/images/muffins-decorate.png b/src/assets/images/muffins-decorate.png new file mode 100644 index 0000000..af356d5 Binary files /dev/null and b/src/assets/images/muffins-decorate.png differ diff --git a/src/assets/images/peste.png b/src/assets/images/peste.png new file mode 100644 index 0000000..ad2d310 Binary files /dev/null and b/src/assets/images/peste.png differ diff --git a/src/assets/images/sohan4.png b/src/assets/images/sohan4.png new file mode 100644 index 0000000..e7a0215 Binary files /dev/null and b/src/assets/images/sohan4.png differ diff --git a/src/assets/images/strawberry-cake-cream.png b/src/assets/images/strawberry-cake-cream.png new file mode 100644 index 0000000..b2c9b4a Binary files /dev/null and b/src/assets/images/strawberry-cake-cream.png differ diff --git a/src/assets/images/strawberry-decorate.png b/src/assets/images/strawberry-decorate.png new file mode 100644 index 0000000..d60e270 Binary files /dev/null and b/src/assets/images/strawberry-decorate.png differ diff --git a/src/assets/images/tiramisu-cake.png b/src/assets/images/tiramisu-cake.png new file mode 100644 index 0000000..f86bdcc Binary files /dev/null and b/src/assets/images/tiramisu-cake.png differ diff --git a/src/components/CartDrawer.css b/src/components/CartDrawer.css new file mode 100644 index 0000000..5c45a7c --- /dev/null +++ b/src/components/CartDrawer.css @@ -0,0 +1,568 @@ +.cart-drawer-overlay { + position: fixed; + inset: 0; + z-index: 950; + background: rgba(47, 24, 10, 0.35); + backdrop-filter: blur(2px); +} + +.cart-drawer { + position: absolute; + inset-block: 0; + left: 0; + width: min(440px, 100%); + background: #fffaf5; + box-shadow: 20px 0 50px rgba(79, 35, 8, 0.18); + display: flex; + flex-direction: column; + animation: cart-slide 0.25s ease; +} + +@keyframes cart-slide { + from { + transform: translateX(-16px); + opacity: 0.6; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.cart-drawer__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 22px 22px 12px; +} + +.cart-drawer__eyebrow { + margin: 0; + color: rgba(143, 65, 12, 0.55); + font-family: Cailyne, Georgia, serif; + font-size: 13px; +} + +.cart-drawer__title { + margin: 4px 0 0; + color: var(--color-primary); + font-size: 24px; +} + +.cart-drawer__close { + width: 36px; + height: 36px; + border: none; + border-radius: 10px; + background: rgba(143, 65, 12, 0.06); + color: var(--color-primary); + display: grid; + place-items: center; +} + +.cart-drawer__steps { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + padding: 0 22px 14px; +} + +.cart-drawer__step { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + color: var(--color-gray-300); + font-size: 12px; +} + +.cart-drawer__step span { + width: 28px; + height: 28px; + border-radius: 50%; + display: grid; + place-items: center; + border: 1px solid rgba(143, 65, 12, 0.2); + font-weight: 700; +} + +.cart-drawer__step em { + font-style: normal; +} + +.cart-drawer__step.is-active, +.cart-drawer__step.is-done { + color: var(--color-primary); +} + +.cart-drawer__step.is-active span, +.cart-drawer__step.is-done span { + background: var(--color-primary); + color: #fff; + border-color: var(--color-primary); +} + +.cart-drawer__body { + flex: 1; + overflow: auto; + padding: 8px 22px 16px; +} + +.cart-drawer__section { + display: flex; + flex-direction: column; + gap: 14px; +} + +.cart-drawer__error { + background: #fde8e8; + color: #9b2c2c; + border-radius: 12px; + padding: 10px 12px; + font-size: 13px; + margin-bottom: 12px; +} + +.cart-drawer__empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + color: var(--color-secondary); + padding: 48px 12px; + text-align: center; +} + +.cart-drawer__muted { + color: var(--color-gray-300); + font-size: 13px; + margin: 0; +} + +.cart-lines { + display: flex; + flex-direction: column; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; +} + +.cart-line { + display: grid; + grid-template-columns: 72px 1fr; + gap: 12px; + padding: 12px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(143, 65, 12, 0.08); +} + +.cart-line__image { + width: 72px; + height: 72px; + object-fit: cover; + border-radius: 12px; +} + +.cart-line__name { + color: var(--color-gray-400); + font-size: 14px; + font-weight: 700; +} + +.cart-line__top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; +} + +.cart-line__calc-row { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 6px 10px; + margin-top: 6px; + font-size: 12px; +} + +.cart-line__calc-row--option { + padding-inline-start: 4px; +} + +.cart-line__option-name { + width: 100%; + color: var(--color-secondary); + font-size: 12px; +} + +.cart-line__calc { + color: var(--color-secondary); + font-variant-numeric: tabular-nums; +} + +.cart-line__result { + color: var(--color-primary); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.cart-line__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 10px; +} + +.cart-line__line-total { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + color: var(--color-secondary); + font-size: 11px; +} + +.cart-line__line-total strong { + color: var(--color-primary); + font-size: 13px; +} + +.cart-line__actions { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; +} + +.cart-line__actions strong { + margin-inline-start: auto; + color: var(--color-primary); + font-size: 13px; +} + +.cart-line__remove { + border: none; + background: transparent; + color: #9b2c2c; + padding: 4px; + flex-shrink: 0; +} + +.qty-stepper--sm { + border-radius: 10px; +} + +.qty-stepper--sm .qty-stepper__btn { + width: 30px; + height: 30px; +} + +.qty-stepper__value { + min-width: 36px; + text-align: center; + font-size: 13px; + font-weight: 700; + color: var(--color-primary); +} + +.cart-delivery-toggle { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.cart-delivery-toggle button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + border: 1px solid rgba(143, 65, 12, 0.15); + background: #fff; + border-radius: 12px; + padding: 12px 8px; + font-family: inherit; + color: var(--color-secondary); + font-size: 13px; +} + +.cart-delivery-toggle button.is-active { + background: rgba(143, 65, 12, 0.08); + border-color: var(--color-primary); + color: var(--color-primary); + font-weight: 700; +} + +.cart-choice-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.cart-choice { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 12px; + border-radius: 14px; + border: 1px solid rgba(143, 65, 12, 0.12); + background: #fff; + cursor: pointer; +} + +.cart-choice.is-active { + border-color: var(--color-primary); + background: rgba(143, 65, 12, 0.05); +} + +.cart-choice input { + margin-top: 3px; +} + +.cart-choice strong { + display: block; + color: var(--color-primary); + font-size: 14px; +} + +.cart-choice em { + display: block; + margin-top: 4px; + font-style: normal; + color: var(--color-secondary); + font-size: 12px; + line-height: 1.5; +} + +.cart-drawer__link-btn { + border: none; + background: none; + color: var(--color-primary); + font-family: inherit; + font-size: 13px; + font-weight: 700; + text-align: start; + padding: 0; +} + +.cart-address-form { + display: flex; + flex-direction: column; + gap: 8px; +} + +.cart-address-form input, +.cart-address-form select, +.cart-address-form textarea, +.cart-field input, +.cart-field textarea { + border: 1px solid rgba(143, 65, 12, 0.15); + border-radius: 12px; + background: #fff; + padding: 10px 12px; + font-family: inherit; + font-size: 13px; +} + +.cart-pay-summary { + display: flex; + flex-direction: column; + gap: 10px; + padding: 14px; + border-radius: 16px; + background: rgba(143, 65, 12, 0.05); +} + +.cart-pay-summary > div { + display: flex; + justify-content: space-between; + gap: 12px; + color: var(--color-secondary); + font-size: 13px; +} + +.cart-pay-summary__total { + padding-top: 8px; + border-top: 1px solid rgba(143, 65, 12, 0.12); + color: var(--color-primary) !important; + font-size: 15px !important; + font-weight: 700; +} + +.cart-field { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + color: var(--color-secondary); +} + +.cart-discount-row { + display: flex; + gap: 8px; +} + +.cart-discount-input { + text-transform: uppercase; +} + +.cart-discount-row button { + border: none; + border-radius: 12px; + background: rgba(143, 65, 12, 0.1); + color: var(--color-primary); + padding: 0 14px; + font-family: inherit; + font-weight: 700; +} + +.cart-discount-row button:disabled, +.cart-discount-input:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.cart-discount-active { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 8px; + padding: 10px 12px; + border-radius: 12px; + background: rgba(34, 140, 78, 0.12); + border: 1px solid rgba(34, 140, 78, 0.28); + color: #1b6b3a; + font-size: 13px; + font-weight: 600; +} + +.cart-discount-active__cancel { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: 8px; + background: rgba(27, 107, 58, 0.12); + color: #1b6b3a; + cursor: pointer; + padding: 0; +} + +.cart-discount-active__cancel:hover { + background: rgba(27, 107, 58, 0.22); +} + +.cart-pay-method { + padding: 14px; + border-radius: 14px; + border: 1px dashed rgba(143, 65, 12, 0.25); + color: var(--color-secondary); +} + +.cart-pay-method strong { + display: block; + color: var(--color-primary); + margin-bottom: 4px; +} + +.cart-pay-method p, +.cart-pay-method em { + margin: 0; + font-size: 13px; + font-style: normal; +} + +.cart-pay-method em { + display: block; + margin-top: 8px; + color: var(--color-gray-300); +} + +.cart-drawer__footer { + display: flex; + gap: 10px; + padding: 16px 22px 22px; + border-top: 1px solid rgba(143, 65, 12, 0.08); +} + +.cart-drawer__ghost { + border: 1px solid rgba(143, 65, 12, 0.15); + background: transparent; + color: var(--color-secondary); + border-radius: 12px; + padding: 10px 16px; + font-family: inherit; +} + +.cart-drawer__primary { + flex: 1; + justify-content: center; +} + +@media (max-width: 480px) { + .cart-drawer { + width: 100%; + } + + .cart-drawer__header, + .cart-drawer__steps, + .cart-drawer__body { + padding-inline: 16px; + } + + .cart-drawer__header { + padding-top: 16px; + } + + .cart-drawer__title { + font-size: 20px; + } + + .cart-drawer__step em { + font-size: 11px; + } + + .cart-line { + grid-template-columns: 64px 1fr; + padding: 10px; + gap: 10px; + } + + .cart-line__image { + width: 64px; + height: 64px; + } + + .cart-line__footer { + flex-wrap: wrap; + } + + .cart-drawer__footer { + flex-wrap: wrap; + padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px)); + } + + .cart-drawer__ghost { + min-height: 44px; + } + + .cart-drawer__primary { + flex: 1 1 100%; + min-height: 48px; + } + + .cart-discount-row { + flex-direction: column; + } + + .cart-discount-row button { + min-height: 44px; + } +} diff --git a/src/components/CartDrawer.tsx b/src/components/CartDrawer.tsx new file mode 100644 index 0000000..802bd9e --- /dev/null +++ b/src/components/CartDrawer.tsx @@ -0,0 +1,677 @@ +import { useEffect, useMemo, useState } from 'react' +import { + ChevronLeft, + MapPin, + Minus, + Plus, + ShoppingBag, + Store, + Trash2, + X, +} from 'lucide-react' +import { + createMyAddress, + createMyOrder, + listBranches, + listDistricts, + listMyAddresses, + listMyDiscounts, + listShipping, + type Branch, + type Discount, + type ShippingException, + type UserAddress, +} from '../lib/api' +import { + CART_OPEN_EVENT, + clearCart, + getCartCheckoutReturnUrl, + getCartItems, + getCartSubtotal, + openCartDrawer, + removeCartItem, + setCartItemQuantity, + showToast, + subscribeCart, + type CartItem, + type CartOpenDetail, +} from '../lib/cart' +import { + consumeAuthHashFromUrl, + getAuthUser, + getCustomerLoginUrl, + isAuthenticated, +} from '../lib/auth' +import { formatPriceFa } from '../lib/types' +import defaultPhoto from '../assets/images/DefaultPhoto.png' +import './CartDrawer.css' + +type Step = 1 | 2 | 3 +type DeliveryType = 'pickup' | 'shipping' + +function applyDiscount(subtotal: number, discount: Discount | null) { + if (!discount) return 0 + if (subtotal < discount.minOrderAmount) return 0 + const raw = Math.round((subtotal * discount.percent) / 100) + return Math.min(raw, discount.maxValue) +} + +export default function CartDrawer() { + const [open, setOpen] = useState(false) + const [step, setStep] = useState(1) + const [items, setItems] = useState(() => getCartItems()) + const [deliveryType, setDeliveryType] = useState('pickup') + const [branches, setBranches] = useState([]) + const [addresses, setAddresses] = useState([]) + const [districts, setDistricts] = useState([]) + const [shipping, setShipping] = useState([]) + const [discounts, setDiscounts] = useState([]) + const [branchId, setBranchId] = useState('') + const [addressId, setAddressId] = useState('') + const [discountCode, setDiscountCode] = useState('') + const [appliedDiscount, setAppliedDiscount] = useState(null) + const [note, setNote] = useState('') + const [loadingMeta, setLoadingMeta] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState('') + const [showNewAddress, setShowNewAddress] = useState(false) + const [newAddress, setNewAddress] = useState({ + name: '', + district: '', + address: '', + landline: '', + }) + + const subtotal = useMemo(() => getCartSubtotal(), [items]) + const selectedAddress = addresses.find((row) => row.id === addressId) + const shippingFee = + deliveryType === 'shipping' && selectedAddress + ? shipping.find((row) => row.district === selectedAddress.district)?.price ?? 0 + : 0 + const discountValue = applyDiscount(subtotal, appliedDiscount) + const payable = Math.max(0, subtotal + shippingFee - discountValue) + + useEffect(() => { + return subscribeCart(() => setItems(getCartItems())) + }, []) + + useEffect(() => { + consumeAuthHashFromUrl() + + function onOpen(event: Event) { + const detail = (event as CustomEvent).detail + const nextStep = detail?.step ?? 1 + setItems(getCartItems()) + setError('') + // Guests may only stay on the items step. + if (nextStep > 1 && !isAuthenticated()) { + setStep(1) + setOpen(true) + return + } + setStep(nextStep) + setOpen(true) + } + + window.addEventListener(CART_OPEN_EVENT, onOpen) + + const params = new URLSearchParams(window.location.search) + const cartParam = params.get('cart') + if (cartParam === 'checkout' || cartParam === '1' || cartParam === 'open') { + if (cartParam === 'checkout' && isAuthenticated()) { + openCartDrawer(2) + } else { + openCartDrawer(1) + } + params.delete('cart') + const clean = `${window.location.pathname}${params.toString() ? `?${params}` : ''}${window.location.hash}` + window.history.replaceState(null, '', clean) + } + + return () => window.removeEventListener(CART_OPEN_EVENT, onOpen) + }, []) + + useEffect(() => { + if (!open || step === 1) return + if (!isAuthenticated()) { + setStep(1) + return + } + let cancelled = false + setLoadingMeta(true) + setError('') + Promise.all([ + listBranches(), + listMyAddresses(), + listDistricts(), + listShipping(), + listMyDiscounts(), + ]) + .then(([branchRows, addressRows, districtRows, shippingRows, discountRows]) => { + if (cancelled) return + setBranches(branchRows) + setAddresses(addressRows) + setDistricts(districtRows) + setShipping(shippingRows) + setDiscounts(discountRows.items.filter((d) => d.active && !d.expired)) + if (!branchId && branchRows[0]) setBranchId(branchRows[0].id) + if (!addressId && addressRows[0]) setAddressId(addressRows[0].id) + if (!newAddress.district && districtRows[0]) { + setNewAddress((current) => ({ ...current, district: districtRows[0] })) + } + }) + .catch((err: Error) => { + if (!cancelled) setError(err.message || 'خطا در دریافت اطلاعات') + }) + .finally(() => { + if (!cancelled) setLoadingMeta(false) + }) + return () => { + cancelled = true + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, step]) + + function close() { + setOpen(false) + setError('') + } + + function goNext() { + setError('') + if (step === 1) { + if (items.length === 0) { + setError('سبد خرید خالی است') + return + } + // Auth is required only after reviewing cart items. + if (!isAuthenticated()) { + window.location.assign(getCustomerLoginUrl(getCartCheckoutReturnUrl())) + return + } + setStep(2) + return + } + if (step === 2) { + if (deliveryType === 'pickup' && !branchId) { + setError('شعبه را انتخاب کنید') + return + } + if (deliveryType === 'shipping' && !addressId) { + setError('آدرس را انتخاب کنید') + return + } + setStep(3) + } + } + + function applyCode() { + const code = discountCode.trim() + if (!code) { + setAppliedDiscount(null) + return + } + const found = discounts.find( + (row) => row.code.toLowerCase() === code.toLowerCase(), + ) + if (!found) { + setError('کد تخفیف معتبر نیست') + setAppliedDiscount(null) + return + } + if (subtotal < found.minOrderAmount) { + setError( + `حداقل مبلغ سفارش برای این کد ${formatPriceFa(found.minOrderAmount)} تومان است`, + ) + setAppliedDiscount(null) + return + } + setError('') + setAppliedDiscount(found) + showToast({ message: 'کد تخفیف اعمال شد' }) + } + + async function saveAddress() { + try { + setSubmitting(true) + setError('') + const created = await createMyAddress(newAddress) + setAddresses((current) => [created, ...current]) + setAddressId(created.id) + setShowNewAddress(false) + setNewAddress({ name: '', district: districts[0] || '', address: '', landline: '' }) + showToast({ message: 'آدرس جدید ذخیره شد' }) + } catch (err) { + setError(err instanceof Error ? err.message : 'ثبت آدرس ناموفق بود') + } finally { + setSubmitting(false) + } + } + + async function submitOrder() { + try { + setSubmitting(true) + setError('') + const order = await createMyOrder({ + deliveryType, + branchId: deliveryType === 'pickup' ? branchId : undefined, + shippingAddressId: deliveryType === 'shipping' ? addressId : undefined, + discountCode: appliedDiscount?.code, + note: note.trim() || undefined, + items: items.map((item) => ({ + productId: item.productId, + quantity: item.quantity, + optionIds: item.options.map((option) => option.id), + })), + }) + clearCart() + setItems([]) + setStep(1) + setOpen(false) + showToast({ + message: 'سفارش با موفقیت ثبت شد', + detail: `کد سفارش ${order.code} — مبلغ ${formatPriceFa(order.totalPrice)} تومان`, + }) + } catch (err) { + setError(err instanceof Error ? err.message : 'ثبت سفارش ناموفق بود') + } finally { + setSubmitting(false) + } + } + + if (!open) return null + + const user = getAuthUser() + + return ( +
+