diff --git a/README.md b/README.md
index f9a55ca..8457218 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,113 @@
-# booking_webapp
+# WaterTrekk Webapp
+Customer-facing booking storefront and vendor dashboard for **WaterTrekk** — tours, boat rentals, and equipment — built with React and Vite. The app talks to a Django-style REST API (`/api/v1`) via Axios, with JWT access/refresh tokens stored in `localStorage`.
+
+## Tech stack
+
+- **React 19** and **TypeScript**
+- **Vite 6** for dev server and production builds
+- **React Router 7** for routing and layouts
+- **Axios** for HTTP, including automatic refresh on `401` responses
+
+## Features
+
+- **Storefront** (public): home, boat rentals and search, tour listings, equipment detail and booking flows (booking routes require sign-in).
+- **Authentication**: sign in, register, forgot password; tokens persisted for authenticated API calls.
+- **Vendor area** (`/dashboard/*`): dashboard, bookings, listings, analytics, and settings — gated for vendor accounts.
+
+## Prerequisites
+
+- **Node.js** (LTS recommended) and npm
+- A running **backend** that serves `/api/v1` (see configuration below)
+
+## Getting started
+
+```bash
+npm install
+npm run dev
+```
+
+Open the URL Vite prints (typically `http://localhost:5173`).
+
+### Production build
+
+```bash
+npm run build
+npm run preview
+```
+
+### Lint
+
+```bash
+npm run lint
+```
+
+## Configuration
+
+| Variable | Description |
+| -------- | ----------- |
+| `VITE_API_BASE_URL` | Optional. Base URL for the API **without** trailing slash. When unset, requests use relative `/api/v1` (see dev proxy). |
+
+Example:
+
+```bash
+VITE_API_BASE_URL=https://api.example.com npm run build
+```
+
+### Local development proxy
+
+With `VITE_API_BASE_URL` unset, `npm run dev` proxies `/api` to `http://127.0.0.1:8003` (see `vite.config.ts`). Start your API on that host/port, or change the proxy target to match your setup.
+
+## Scripts
+
+| Script | Purpose |
+| ------ | ------- |
+| `npm run dev` | Start Vite dev server with HMR |
+| `npm run build` | Typecheck and emit production assets to `dist/` |
+| `npm run preview` | Serve the production build locally |
+| `npm run lint` | Run ESLint |
+
+## Repository layout (high level)
+
+- `src/pages/` — route-level screens (storefront, auth, vendor admin)
+- `src/components/` — shared layout and UI pieces
+- `src/auth/` — auth context, route guards (`RequireSignIn`, `RequireVendor`)
+- `src/api/` — Axios client, token handling, service calls, types
+
+## Screenshots
+
+Captured from a local production preview at 1400×900 viewport (external images may depend on network).
+
+### Home
+
+
+
+### Boat rentals
+
+
+
+### Boat search
+
+
+
+### Tours
+
+
+
+### Sign in
+
+
+
+### For vendors
+
+
+
+### Regenerating screenshots
+
+With a preview server running on port **4173** (for example `npm run preview` after `npm run build`):
+
+```bash
+npx playwright@1.49.1 screenshot http://127.0.0.1:4173/ docs/screenshots/home.png --viewport-size=1400,900 --wait-for-timeout=4000
+```
+
+Repeat for other paths, or adjust viewport and output paths as needed. On first use, Playwright may download Chromium (`npx playwright install chromium`).
diff --git a/docs/screenshots/boat-rentals.png b/docs/screenshots/boat-rentals.png
new file mode 100644
index 0000000..6d03b4f
Binary files /dev/null and b/docs/screenshots/boat-rentals.png differ
diff --git a/docs/screenshots/boat-search.png b/docs/screenshots/boat-search.png
new file mode 100644
index 0000000..fd2da36
Binary files /dev/null and b/docs/screenshots/boat-search.png differ
diff --git a/docs/screenshots/home.png b/docs/screenshots/home.png
new file mode 100644
index 0000000..df68ae9
Binary files /dev/null and b/docs/screenshots/home.png differ
diff --git a/docs/screenshots/sign-in.png b/docs/screenshots/sign-in.png
new file mode 100644
index 0000000..aba37ff
Binary files /dev/null and b/docs/screenshots/sign-in.png differ
diff --git a/docs/screenshots/tours.png b/docs/screenshots/tours.png
new file mode 100644
index 0000000..7be70e9
Binary files /dev/null and b/docs/screenshots/tours.png differ
diff --git a/docs/screenshots/vendors.png b/docs/screenshots/vendors.png
new file mode 100644
index 0000000..02ef672
Binary files /dev/null and b/docs/screenshots/vendors.png differ
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..79a552e
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,28 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": [
+ "warn",
+ { allowConstantExport: true },
+ ],
+ },
+ },
+);
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f0faee8
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ WaterTrekk — Bookings
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..3502623
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3562 @@
+{
+ "name": "watertrekk-webapp",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "watertrekk-webapp",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.15.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.6.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.21.0",
+ "@types/react": "^19.0.10",
+ "@types/react-dom": "^19.0.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "eslint": "^9.21.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.15.0",
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.24.1",
+ "vite": "^6.2.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
+ "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/type-utils": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
+ "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
+ "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.58.1",
+ "@typescript-eslint/types": "^8.58.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
+ "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
+ "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
+ "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
+ "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
+ "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.58.1",
+ "@typescript-eslint/tsconfig-utils": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
+ "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
+ "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+ "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.17",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz",
+ "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001787",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "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/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.335",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
+ "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "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/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
+ "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
+ "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.14.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
+ "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.14.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.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/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "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/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz",
+ "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.58.1",
+ "@typescript-eslint/parser": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..561ace3
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "watertrekk-webapp",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.15.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.6.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.21.0",
+ "@types/react": "^19.0.10",
+ "@types/react-dom": "^19.0.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "eslint": "^9.21.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.15.0",
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.24.1",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 0000000..2fddd93
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..796813a
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,77 @@
+import { Navigate, Route, Routes } from "react-router-dom";
+import { AdminLayout } from "./components/AdminLayout";
+import { Layout } from "./components/Layout";
+import { RequireSignIn } from "./auth/RequireSignIn";
+import { RequireVendor } from "./auth/RequireVendor";
+import { AdventureBookPage } from "./pages/AdventureBookPage";
+import { AdventureDetailPage } from "./pages/AdventureDetailPage";
+import { BoatRentalsPage } from "./pages/BoatRentalsPage";
+import { BoatSearchPage } from "./pages/BoatSearchPage";
+import { DashboardPage } from "./pages/DashboardPage";
+import { EquipmentBookPage } from "./pages/EquipmentBookPage";
+import { EquipmentDetailPage } from "./pages/EquipmentDetailPage";
+import { HomePage } from "./pages/HomePage";
+import { ToursPage } from "./pages/ToursPage";
+import { VendorAnalyticsPage } from "./pages/VendorAnalyticsPage";
+import { VendorBookingsPage } from "./pages/VendorBookingsPage";
+import { VendorListingsPage } from "./pages/VendorListingsPage";
+import { VendorSettingsPage } from "./pages/VendorSettingsPage";
+import { VendorWhyPage } from "./pages/VendorWhyPage";
+import { ForgotPasswordPage } from "./pages/auth/ForgotPasswordPage";
+import { RegisterPage } from "./pages/auth/RegisterPage";
+import { SignInPage } from "./pages/auth/SignInPage";
+
+function App() {
+ return (
+
+ } />
+ } />
+ } />
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ }
+ />
+ } />
+ } />
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ >
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ } />
+
+ );
+}
+
+export default App;
diff --git a/src/api/client.ts b/src/api/client.ts
new file mode 100644
index 0000000..d853c68
--- /dev/null
+++ b/src/api/client.ts
@@ -0,0 +1,97 @@
+import axios, { type AxiosError, type InternalAxiosRequestConfig } from "axios";
+
+const ACCESS_KEY = "watertrek_access";
+const REFRESH_KEY = "watertrek_refresh";
+
+function apiBaseUrl(): string {
+ const env = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.replace(/\/$/, "") ?? "";
+ return env ? `${env}/api/v1` : "/api/v1";
+}
+
+export const api = axios.create({
+ baseURL: apiBaseUrl(),
+ headers: { "Content-Type": "application/json" },
+});
+
+/** Storefront reads only — no Authorization header so invalid/expired tokens do not break public browsing. */
+export const publicApi = axios.create({
+ baseURL: apiBaseUrl(),
+ headers: { "Content-Type": "application/json" },
+});
+
+export function getAccessToken(): string | null {
+ return localStorage.getItem(ACCESS_KEY);
+}
+
+export function getRefreshToken(): string | null {
+ return localStorage.getItem(REFRESH_KEY);
+}
+
+export function setTokens(access: string, refresh: string): void {
+ localStorage.setItem(ACCESS_KEY, access);
+ localStorage.setItem(REFRESH_KEY, refresh);
+}
+
+export function setAccessToken(access: string): void {
+ localStorage.setItem(ACCESS_KEY, access);
+}
+
+export function clearTokens(): void {
+ localStorage.removeItem(ACCESS_KEY);
+ localStorage.removeItem(REFRESH_KEY);
+}
+
+api.interceptors.request.use((config) => {
+ const token = getAccessToken();
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+});
+
+type RetryConfig = InternalAxiosRequestConfig & { _retry?: boolean };
+
+api.interceptors.response.use(
+ (res) => res,
+ async (error: AxiosError) => {
+ const original = error.config as RetryConfig | undefined;
+ const status = error.response?.status;
+ const url = original?.url ?? "";
+ if (
+ status !== 401 ||
+ !original ||
+ original._retry ||
+ url.includes("/accounts/token/") ||
+ !getRefreshToken()
+ ) {
+ return Promise.reject(error);
+ }
+ original._retry = true;
+ try {
+ const { data } = await axios.post<{ access: string }>(
+ `${apiBaseUrl()}/accounts/token/refresh/`,
+ { refresh: getRefreshToken() },
+ { headers: { "Content-Type": "application/json" } },
+ );
+ setAccessToken(data.access);
+ original.headers.Authorization = `Bearer ${data.access}`;
+ return api(original);
+ } catch {
+ clearTokens();
+ return Promise.reject(error);
+ }
+ },
+);
+
+export function formatApiMessage(err: unknown): string {
+ if (axios.isAxiosError(err)) {
+ const data = err.response?.data as Record | undefined;
+ if (data && typeof data.detail === "string") return data.detail;
+ if (data && Array.isArray(data.non_field_errors) && typeof data.non_field_errors[0] === "string") {
+ return data.non_field_errors[0];
+ }
+ if (err.message) return err.message;
+ }
+ if (err instanceof Error) return err.message;
+ return "Something went wrong.";
+}
diff --git a/src/api/services.ts b/src/api/services.ts
new file mode 100644
index 0000000..1fa65bd
--- /dev/null
+++ b/src/api/services.ts
@@ -0,0 +1,174 @@
+import { api, publicApi } from "./client";
+import type {
+ AdventureOffering,
+ AdventureOfferingDetail,
+ AdventureStorefrontResponse,
+ AvailabilityResponse,
+ Booking,
+ CreateVendorAdventureRequest,
+ CreateVendorEquipmentRequest,
+ CustomerRegisterRequest,
+ EquipmentCategory,
+ EquipmentItem,
+ EquipmentItemDetail,
+ EquipmentStorefrontResponse,
+ ListingClick,
+ LoginRequest,
+ MarketingSummary,
+ TokenPair,
+ User,
+ VendorProfile,
+ VendorRegisterRequest,
+} from "./types";
+
+export async function login(body: LoginRequest): Promise {
+ const { data } = await api.post("/accounts/token/", body);
+ return data;
+}
+
+export async function fetchMe(): Promise {
+ const { data } = await api.get("/accounts/me/");
+ return data;
+}
+
+export async function registerVendor(body: VendorRegisterRequest): Promise {
+ const { data } = await api.post("/accounts/register/vendor/", body);
+ return data;
+}
+
+export async function registerCustomer(body: CustomerRegisterRequest): Promise {
+ const { data } = await api.post("/accounts/register/customer/", body);
+ return data;
+}
+
+export async function requestPasswordReset(email?: string): Promise<{ detail: string }> {
+ const { data } = await api.post<{ detail: string }>("/accounts/password/reset/request/", { email: email ?? "" });
+ return data;
+}
+
+export async function fetchVendorProfile(): Promise {
+ const { data } = await api.get("/accounts/vendor-profile/me/");
+ return data;
+}
+
+export async function patchVendorProfile(body: Partial): Promise {
+ const { data } = await api.patch("/accounts/vendor-profile/me/", body);
+ return data;
+}
+
+export type EquipmentListParams = {
+ category?: string;
+ location?: string;
+ vendor_slug?: string;
+ min_price?: string;
+ max_price?: string;
+ available_from?: string;
+ available_to?: string;
+};
+
+export async function listEquipmentItems(params?: EquipmentListParams): Promise {
+ const { data } = await publicApi.get("/equipment/items/", { params });
+ return data;
+}
+
+export async function getEquipmentItem(
+ publicId: string,
+ params?: Record,
+): Promise {
+ const { data } = await publicApi.get(`/equipment/items/${publicId}/`, { params });
+ return data;
+}
+
+export async function listVendorEquipment(): Promise {
+ const { data } = await api.get("/equipment/vendor/items/");
+ return data;
+}
+
+export async function listEquipmentCategories(): Promise {
+ const { data } = await publicApi.get("/equipment/categories/");
+ return data;
+}
+
+export async function createVendorEquipment(body: CreateVendorEquipmentRequest): Promise {
+ const { data } = await api.post("/equipment/vendor/items/", body);
+ return data;
+}
+
+export type AdventureListParams = EquipmentListParams;
+
+export async function listAdventureOfferings(params?: AdventureListParams): Promise {
+ const { data } = await publicApi.get("/adventrues/offerings/", { params });
+ return data;
+}
+
+export async function getAdventureOffering(
+ publicId: string,
+ params?: Record,
+): Promise {
+ const { data } = await publicApi.get(`/adventrues/offerings/${publicId}/`, {
+ params,
+ });
+ return data;
+}
+
+export async function listVendorAdventures(): Promise {
+ const { data } = await api.get("/adventrues/vendor/offerings/");
+ return data;
+}
+
+export async function createVendorAdventure(body: CreateVendorAdventureRequest): Promise {
+ const { data } = await api.post("/adventrues/vendor/offerings/", body);
+ return data;
+}
+
+export async function getEquipmentStorefront(slug: string): Promise {
+ const { data } = await publicApi.get(`/equipment/storefront/${slug}/`);
+ return data;
+}
+
+export async function getAdventureStorefront(slug: string): Promise {
+ const { data } = await publicApi.get(`/adventrues/storefront/${slug}/`);
+ return data;
+}
+
+export async function checkAvailability(params: {
+ equipment_item_id?: number;
+ adventure_offering_id?: number;
+ starts_at: string;
+ ends_at: string;
+}): Promise {
+ const { data } = await api.get("/booking/availability/", { params });
+ return data;
+}
+
+export async function listBookings(): Promise {
+ const { data } = await api.get("/booking/bookings/");
+ return data;
+}
+
+export async function getBooking(id: number): Promise {
+ const { data } = await api.get(`/booking/bookings/${id}/`);
+ return data;
+}
+
+export async function fetchVendorMarketingSummary(from: string, to: string): Promise {
+ const { data } = await api.get("/marketing/vendor/summary/", {
+ params: { from, to },
+ });
+ return data;
+}
+
+export async function fetchVendorListingClicks(
+ from: string,
+ to: string,
+ extra?: {
+ traffic_type?: "organic" | "marketing";
+ listing_type?: "equipment" | "adventure";
+ utm_campaign?: string;
+ },
+): Promise {
+ const { data } = await api.get("/marketing/vendor/clicks/", {
+ params: { from, to, ...extra },
+ });
+ return data;
+}
diff --git a/src/api/types.ts b/src/api/types.ts
new file mode 100644
index 0000000..3d6ab74
--- /dev/null
+++ b/src/api/types.ts
@@ -0,0 +1,273 @@
+/** Mirrors WaterTrekk booking API (see booking_backend/api.md). */
+
+export type TrafficType = "organic" | "marketing";
+
+export type VendorProfile = {
+ business_name: string;
+ slug: string;
+ description: string;
+ contact_phone: string;
+ contact_email: string;
+ address_line1: string;
+ address_line2: string;
+ city: string;
+ state: string;
+ postal_code: string;
+ country: string;
+ created_at: string;
+ updated_at: string;
+};
+
+export type CustomerProfile = {
+ preferred_contact_method: string;
+ emergency_contact_name: string;
+ emergency_contact_phone: string;
+ created_at: string;
+ updated_at: string;
+};
+
+export type User = {
+ id: number;
+ email: string;
+ first_name: string;
+ last_name: string;
+ phone_number: string;
+ is_vendor: boolean;
+ is_customer: boolean;
+ vendor_profile: VendorProfile | null;
+ customer_profile: CustomerProfile | null;
+};
+
+export type TokenPair = {
+ refresh: string;
+ access: string;
+};
+
+export type TokenRefreshResponse = {
+ access: string;
+};
+
+export type VendorRegisterRequest = {
+ email: string;
+ password: string;
+ first_name?: string;
+ last_name?: string;
+ phone_number?: string;
+ business_name: string;
+ description?: string;
+ contact_phone?: string;
+ contact_email?: string;
+ address_line1?: string;
+ city?: string;
+ state?: string;
+ postal_code?: string;
+ country?: string;
+};
+
+export type CustomerRegisterRequest = {
+ email: string;
+ password: string;
+ first_name?: string;
+ last_name?: string;
+ phone_number?: string;
+};
+
+export type LoginRequest = {
+ email: string;
+ password: string;
+};
+
+export type EquipmentCategory = {
+ id: number;
+ name: string;
+ slug: string;
+ description: string;
+};
+
+export type EquipmentImage = {
+ id: number;
+ image_url: string;
+ alt_text: string;
+ sort_order: number;
+ is_primary: boolean;
+};
+
+export type EquipmentItem = {
+ id: number;
+ public_id: string;
+ title: string;
+ description: string;
+ details: Record;
+ location: string;
+ price_per_day: string;
+ is_active: boolean;
+ category: EquipmentCategory;
+ vendor_slug: string;
+ vendor_business_name: string;
+ images: EquipmentImage[];
+ created_at: string;
+ updated_at: string;
+};
+
+export type EquipmentItemDetail = EquipmentItem & {
+ marketing_click_id: number;
+ click_traffic_type: TrafficType;
+};
+
+export type AdventureCategory = EquipmentCategory;
+
+export type AdventureOffering = {
+ id: number;
+ public_id: string;
+ title: string;
+ description: string;
+ meeting_point: string;
+ duration_minutes: number;
+ capacity: number;
+ price_per_person: string;
+ is_active: boolean;
+ category: AdventureCategory;
+ vendor_slug: string;
+ vendor_business_name: string;
+ images: EquipmentImage[];
+ created_at: string;
+ updated_at: string;
+};
+
+export type AdventureOfferingDetail = AdventureOffering & {
+ marketing_click_id: number;
+ click_traffic_type: TrafficType;
+};
+
+export type StorefrontVendor = {
+ business_name: string;
+ slug: string;
+ description: string;
+ contact_email: string;
+ contact_phone: string;
+ city: string;
+ country: string;
+};
+
+export type EquipmentStorefrontResponse = {
+ vendor: StorefrontVendor;
+ items: EquipmentItem[];
+};
+
+export type AdventureStorefrontResponse = {
+ vendor: StorefrontVendor;
+ offerings: AdventureOffering[];
+};
+
+export type BookingStatus =
+ | "requested"
+ | "approved"
+ | "declined"
+ | "cancelled"
+ | "confirmed";
+
+export type BookingEvent = {
+ id: number;
+ from_status: string;
+ to_status: string;
+ note: string;
+ actor_email: string;
+ created_at: string;
+};
+
+export type Booking = {
+ id: number;
+ customer_email: string;
+ vendor_slug: string;
+ equipment_item: number | null;
+ equipment_public_id: string | null;
+ adventure_public_id: string | null;
+ starts_at: string;
+ ends_at: string;
+ status: BookingStatus;
+ total_price: string;
+ customer_notes: string;
+ vendor_notes: string;
+ events: BookingEvent[];
+ listing_click: number | null;
+ created_at: string;
+ updated_at: string;
+};
+
+export type AvailabilityResponse = {
+ equipment_item_id: number | null;
+ adventure_offering_id: number | null;
+ starts_at: string;
+ ends_at: string;
+ is_available: boolean;
+ conflicts: number;
+};
+
+export type MarketingSummary = {
+ from: string;
+ to: string;
+ clicks: {
+ organic: number;
+ marketing: number;
+ total: number;
+ };
+ bookings_attributed: {
+ organic: number;
+ marketing: number;
+ total_attributed: number;
+ unattributed: number;
+ total_all: number;
+ };
+ conversion_rate_click_to_booking: {
+ organic: number | null;
+ marketing: number | null;
+ };
+ campaigns: {
+ utm_source: string;
+ utm_medium: string;
+ utm_campaign: string;
+ clicks: number;
+ bookings: number;
+ conversion_rate: number | null;
+ }[];
+};
+
+export type ListingClick = {
+ id: number;
+ listing_type: string;
+ traffic_type: TrafficType;
+ utm_source: string;
+ utm_medium: string;
+ utm_campaign: string;
+ created_at: string;
+};
+
+export type ApiErrorBody = {
+ detail?: string;
+ [key: string]: unknown;
+};
+
+/** POST /equipment/vendor/items/ — see booking_backend api.md */
+export type CreateVendorEquipmentRequest = {
+ public_id: string;
+ title: string;
+ description: string;
+ location: string;
+ price_per_day: string;
+ category_id: number;
+ details?: Record;
+ is_active?: boolean;
+};
+
+/** POST /adventrues/vendor/offerings/ — see booking_backend api.md */
+export type CreateVendorAdventureRequest = {
+ public_id: string;
+ title: string;
+ description: string;
+ meeting_point: string;
+ duration_minutes: number;
+ capacity: number;
+ price_per_person: string;
+ category_id: number;
+ is_active?: boolean;
+};
diff --git a/src/auth/AuthContext.tsx b/src/auth/AuthContext.tsx
new file mode 100644
index 0000000..cac2b8b
--- /dev/null
+++ b/src/auth/AuthContext.tsx
@@ -0,0 +1,103 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import { clearTokens, formatApiMessage, getAccessToken, setTokens } from "../api/client";
+import * as api from "../api/services";
+import type { User } from "../api/types";
+
+type AuthState = {
+ user: User | null;
+ loading: boolean;
+ error: string | null;
+ refreshUser: () => Promise;
+ signIn: (email: string, password: string) => Promise;
+ signOut: () => void;
+};
+
+const AuthContext = createContext(null);
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const refreshUser = useCallback(async () => {
+ if (!getAccessToken()) {
+ setUser(null);
+ return;
+ }
+ const me = await api.fetchMe();
+ setUser(me);
+ }, []);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ if (!getAccessToken()) {
+ if (!cancelled) setUser(null);
+ return;
+ }
+ const me = await api.fetchMe();
+ if (!cancelled) setUser(me);
+ } catch {
+ if (!cancelled) {
+ clearTokens();
+ setUser(null);
+ }
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const signIn = useCallback(async (email: string, password: string) => {
+ setError(null);
+ try {
+ const tokens = await api.login({ email, password });
+ setTokens(tokens.access, tokens.refresh);
+ const me = await api.fetchMe();
+ setUser(me);
+ return me;
+ } catch (e) {
+ const msg = formatApiMessage(e);
+ setError(msg);
+ throw e;
+ }
+ }, []);
+
+ const signOut = useCallback(() => {
+ clearTokens();
+ setUser(null);
+ setError(null);
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ user,
+ loading,
+ error,
+ refreshUser,
+ signIn,
+ signOut,
+ }),
+ [user, loading, error, refreshUser, signIn, signOut],
+ );
+
+ return {children} ;
+}
+
+export function useAuth(): AuthState {
+ const ctx = useContext(AuthContext);
+ if (!ctx) throw new Error("useAuth must be used within AuthProvider");
+ return ctx;
+}
diff --git a/src/auth/RequireSignIn.tsx b/src/auth/RequireSignIn.tsx
new file mode 100644
index 0000000..733ed64
--- /dev/null
+++ b/src/auth/RequireSignIn.tsx
@@ -0,0 +1,28 @@
+import type { ReactNode } from "react";
+import { Navigate, useLocation } from "react-router-dom";
+import { useAuth } from "./AuthContext";
+
+export function RequireSignIn({ children }: { children: ReactNode }) {
+ const { user, loading } = useAuth();
+ const location = useLocation();
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ return children;
+}
diff --git a/src/auth/RequireVendor.tsx b/src/auth/RequireVendor.tsx
new file mode 100644
index 0000000..472c095
--- /dev/null
+++ b/src/auth/RequireVendor.tsx
@@ -0,0 +1,31 @@
+import { Navigate, useLocation } from "react-router-dom";
+import { useAuth } from "./AuthContext";
+
+export function RequireVendor({ children }: { children: React.ReactNode }) {
+ const { user, loading } = useAuth();
+ const location = useLocation();
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ if (!user.is_vendor) {
+ return ;
+ }
+
+ return children;
+}
diff --git a/src/auth/returnPath.ts b/src/auth/returnPath.ts
new file mode 100644
index 0000000..d49fccc
--- /dev/null
+++ b/src/auth/returnPath.ts
@@ -0,0 +1,8 @@
+/** Safe in-app path for post-auth redirects (open redirects / auth loops). */
+export function safeReturnPath(from: unknown): string | null {
+ if (typeof from !== "string" || !from.startsWith("/")) return null;
+ if (from.startsWith("//")) return null;
+ if (from.startsWith("/auth/")) return null;
+ if (from.startsWith("/dashboard")) return null;
+ return from;
+}
diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx
new file mode 100644
index 0000000..22912f7
--- /dev/null
+++ b/src/components/AdminLayout.tsx
@@ -0,0 +1,84 @@
+import { NavLink, Outlet } from "react-router-dom";
+import { useAuth } from "../auth/AuthContext";
+
+const navItems = [
+ { to: "/dashboard", label: "Dashboard", end: true },
+ { to: "/dashboard/bookings", label: "Bookings" },
+ { to: "/dashboard/listings", label: "Listings" },
+ { to: "/dashboard/analytics", label: "Analytics" },
+ { to: "/dashboard/settings", label: "Settings" },
+];
+
+export function AdminLayout() {
+ const { user, signOut } = useAuth();
+ const business = user?.vendor_profile?.business_name ?? "Vendor";
+ const initial = (user?.first_name?.[0] || user?.email?.[0] || "V").toUpperCase();
+
+ return (
+
+
+
+
+
+
WaterTrekk Admin
+
Vendor console
+
+
+
+ {navItems.map(({ to, label, end }) => (
+
+ `admin-nav-item${isActive ? " admin-nav-item--active" : ""}`
+ }
+ >
+ {label}
+
+ ))}
+
+
+
+ ← Back to site
+
+
+
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+ {initial}
+
+
+ {business}
+ {user?.email}
+
+
+
signOut()}>
+ Sign out
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/CatalogStrip.tsx b/src/components/CatalogStrip.tsx
new file mode 100644
index 0000000..d6aef8c
--- /dev/null
+++ b/src/components/CatalogStrip.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { AdventureOffering, EquipmentItem } from "../api/types";
+
+export function CatalogStrip() {
+ const [equipment, setEquipment] = useState([]);
+ const [tours, setTours] = useState([]);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const [eq, adv] = await Promise.all([api.listEquipmentItems(), api.listAdventureOfferings()]);
+ if (!cancelled) {
+ setEquipment(eq.slice(0, 6));
+ setTours(adv.slice(0, 6));
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ if (error) {
+ return (
+
+
+ From the live catalog
+
+ Could not load listings: {error}
+
+ );
+ }
+
+ if (equipment.length === 0 && tours.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ From the live catalog
+
+ API
+
+ {equipment.length > 0 && (
+ <>
+ Equipment rentals
+
+ {equipment.map((item) => {
+ const img =
+ item.images.find((i) => i.is_primary)?.image_url ?? item.images[0]?.image_url;
+ return (
+
+
+
+
{item.vendor_business_name}
+
{item.title}
+
{item.location}
+
${item.price_per_day} / day
+
+
+ );
+ })}
+
+ >
+ )}
+ {tours.length > 0 && (
+ <>
+ Tours & adventures
+
+ {tours.map((o) => {
+ const img =
+ o.images.find((i) => i.is_primary)?.image_url ?? o.images[0]?.image_url;
+ return (
+
+
+
+
{o.vendor_business_name}
+
{o.title}
+
{o.meeting_point}
+
${o.price_per_person} / person
+
+
+ );
+ })}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
new file mode 100644
index 0000000..a7734c8
--- /dev/null
+++ b/src/components/Layout.tsx
@@ -0,0 +1,57 @@
+import { NavLink, Outlet, useLocation } from "react-router-dom";
+import { useAuth } from "../auth/AuthContext";
+
+export function Layout() {
+ const { pathname } = useLocation();
+ const { user, signOut } = useAuth();
+ const fullBleed =
+ pathname === "/" ||
+ pathname === "/boats" ||
+ pathname.startsWith("/boats/search") ||
+ pathname.startsWith("/boats/item/") ||
+ pathname.startsWith("/tours/");
+ const mainClass = fullBleed ? "site-main site-main--full" : "site-main";
+
+ return (
+
+
+
+ WaterTrekk
+
+
+
+ Boat rentals
+
+
+ Tours
+
+
+ For vendors
+
+ {user?.is_vendor ? (
+
+ Dashboard
+
+ ) : null}
+ {!user ? (
+ <>
+
+ Sign in
+
+
+ Register
+
+ >
+ ) : (
+ signOut()}>
+ Sign out
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/VendorCreateListingPanel.tsx b/src/components/VendorCreateListingPanel.tsx
new file mode 100644
index 0000000..9c054bd
--- /dev/null
+++ b/src/components/VendorCreateListingPanel.tsx
@@ -0,0 +1,417 @@
+import { useEffect, useState } from "react";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { EquipmentCategory } from "../api/types";
+
+type ListingKind = "rental" | "tour";
+
+type Props = {
+ onCreated: () => void;
+};
+
+export function VendorCreateListingPanel({ onCreated }: Props) {
+ const [kind, setKind] = useState("rental");
+
+ const [rentalPublicId, setRentalPublicId] = useState("");
+ const [rentalTitle, setRentalTitle] = useState("");
+ const [rentalDescription, setRentalDescription] = useState("");
+ const [rentalLocation, setRentalLocation] = useState("");
+ const [rentalPrice, setRentalPrice] = useState("");
+ const [rentalCategoryId, setRentalCategoryId] = useState("");
+ const [equipmentCategories, setEquipmentCategories] = useState([]);
+ const [equipmentCategoriesLoading, setEquipmentCategoriesLoading] = useState(true);
+ const [equipmentCategoriesError, setEquipmentCategoriesError] = useState(null);
+
+ const [tourPublicId, setTourPublicId] = useState("");
+ const [tourTitle, setTourTitle] = useState("");
+ const [tourDescription, setTourDescription] = useState("");
+ const [tourMeeting, setTourMeeting] = useState("");
+ const [tourDuration, setTourDuration] = useState("");
+ const [tourCapacity, setTourCapacity] = useState("");
+ const [tourPrice, setTourPrice] = useState("");
+ const [tourCategoryId, setTourCategoryId] = useState("");
+
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const rows = await api.listEquipmentCategories();
+ if (!cancelled) {
+ setEquipmentCategories(rows);
+ setEquipmentCategoriesError(null);
+ }
+ } catch (e) {
+ if (!cancelled) setEquipmentCategoriesError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setEquipmentCategoriesLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ function resetRentalFields() {
+ setRentalPublicId("");
+ setRentalTitle("");
+ setRentalDescription("");
+ setRentalLocation("");
+ setRentalPrice("");
+ setRentalCategoryId("");
+ }
+
+ function resetTourFields() {
+ setTourPublicId("");
+ setTourTitle("");
+ setTourDescription("");
+ setTourMeeting("");
+ setTourDuration("");
+ setTourCapacity("");
+ setTourPrice("");
+ setTourCategoryId("");
+ }
+
+ async function submitRental(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setSuccess(null);
+ const publicId = rentalPublicId.trim();
+ if (!publicId) {
+ setError("Enter a public ID (URL slug for this listing).");
+ return;
+ }
+ const categoryId = Number.parseInt(rentalCategoryId, 10);
+ if (Number.isNaN(categoryId) || categoryId < 1) {
+ setError("Select a category.");
+ return;
+ }
+ const price = rentalPrice.trim();
+ if (!price) {
+ setError("Enter a price per day.");
+ return;
+ }
+ setSubmitting(true);
+ try {
+ await api.createVendorEquipment({
+ public_id: publicId,
+ title: rentalTitle.trim(),
+ description: rentalDescription.trim(),
+ location: rentalLocation.trim(),
+ price_per_day: price,
+ category_id: categoryId,
+ details: {},
+ is_active: true,
+ });
+ setSuccess("Rental listing created.");
+ resetRentalFields();
+ onCreated();
+ } catch (err) {
+ setError(formatApiMessage(err));
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ async function submitTour(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setSuccess(null);
+ const publicId = tourPublicId.trim();
+ if (!publicId) {
+ setError("Enter a public ID (URL slug for this listing).");
+ return;
+ }
+ const categoryId = Number.parseInt(tourCategoryId, 10);
+ if (Number.isNaN(categoryId) || categoryId < 1) {
+ setError("Enter a valid category ID (positive integer).");
+ return;
+ }
+ const duration = Number.parseInt(tourDuration, 10);
+ const capacity = Number.parseInt(tourCapacity, 10);
+ if (Number.isNaN(duration) || duration < 1) {
+ setError("Duration must be a positive number of minutes.");
+ return;
+ }
+ if (Number.isNaN(capacity) || capacity < 1) {
+ setError("Capacity must be at least 1 guest.");
+ return;
+ }
+ const price = tourPrice.trim();
+ if (!price) {
+ setError("Enter a price per person.");
+ return;
+ }
+ setSubmitting(true);
+ try {
+ await api.createVendorAdventure({
+ public_id: publicId,
+ title: tourTitle.trim(),
+ description: tourDescription.trim(),
+ meeting_point: tourMeeting.trim(),
+ duration_minutes: duration,
+ capacity,
+ price_per_person: price,
+ category_id: categoryId,
+ is_active: true,
+ });
+ setSuccess("Tour listing created.");
+ resetTourFields();
+ onCreated();
+ } catch (err) {
+ setError(formatApiMessage(err));
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..16cff7e
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,2419 @@
+:root {
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell,
+ Noto Sans, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color: #0f172a;
+ background-color: #f8fafc;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+}
+
+#root {
+ min-height: 100vh;
+}
+
+.app-shell {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.site-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 1rem 1.5rem;
+ background: #fff;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.brand {
+ font-weight: 700;
+ font-size: 1.125rem;
+ color: #0f172a;
+ text-decoration: none;
+}
+
+.brand:hover {
+ color: #0369a1;
+}
+
+.site-nav {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.nav-link {
+ padding: 0.5rem 0.75rem;
+ border-radius: 0.375rem;
+ color: #475569;
+ text-decoration: none;
+ font-size: 0.9375rem;
+}
+
+.nav-link:hover {
+ color: #0f172a;
+ background: #f1f5f9;
+}
+
+.nav-link.active {
+ color: #0369a1;
+ background: #e0f2fe;
+ font-weight: 600;
+}
+
+.site-main {
+ flex: 1;
+ padding: 2rem 1.5rem 3rem;
+ max-width: 56rem;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.page h1 {
+ margin: 0 0 0.75rem;
+ font-size: 1.75rem;
+ letter-spacing: -0.02em;
+}
+
+.lede {
+ margin: 0 0 1.5rem;
+ font-size: 1.0625rem;
+ color: #334155;
+ max-width: 42rem;
+}
+
+.muted {
+ margin: 0;
+ font-size: 0.9375rem;
+ color: #64748b;
+}
+
+.card-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
+}
+
+.card {
+ margin: 0;
+ padding: 1.25rem;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+ box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
+}
+
+.card h2 {
+ margin: 0 0 0.5rem;
+ font-size: 1.125rem;
+}
+
+.card p {
+ margin: 0 0 1rem;
+ font-size: 0.9375rem;
+ color: #475569;
+}
+
+.text-link {
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: #0369a1;
+ text-decoration: none;
+}
+
+.text-link:hover {
+ text-decoration: underline;
+}
+
+.site-main--full {
+ max-width: none;
+ padding: 0;
+ margin: 0;
+}
+
+.nav-link--vendor {
+ border: 1px solid #e2e8f0;
+}
+
+.nav-link--vendor.active {
+ border-color: #bae6fd;
+}
+
+/* —— Admin (vendor) layout —— */
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.admin-shell {
+ min-height: 100vh;
+ display: flex;
+ background: #f1f5f9;
+}
+
+.admin-sidebar {
+ width: 16.5rem;
+ flex-shrink: 0;
+ background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
+ color: #e2e8f0;
+ display: flex;
+ flex-direction: column;
+ padding: 1.25rem 0;
+}
+
+.admin-sidebar-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0 1.25rem 1.5rem;
+ border-bottom: 1px solid rgb(148 163 184 / 0.2);
+}
+
+.admin-sidebar-logo {
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 0.5rem;
+ background: linear-gradient(135deg, #38bdf8, #6366f1);
+ flex-shrink: 0;
+}
+
+.admin-sidebar-title {
+ font-weight: 700;
+ font-size: 1rem;
+ letter-spacing: -0.02em;
+}
+
+.admin-sidebar-sub {
+ font-size: 0.75rem;
+ color: #94a3b8;
+ margin-top: 0.125rem;
+}
+
+.admin-sidebar-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ padding: 1rem 0.75rem;
+ flex: 1;
+}
+
+.admin-nav-item {
+ padding: 0.65rem 0.85rem;
+ border-radius: 0.5rem;
+ color: #cbd5e1;
+ text-decoration: none;
+ font-size: 0.9375rem;
+ transition: background 0.15s, color 0.15s;
+}
+
+.admin-nav-item:hover {
+ background: rgb(255 255 255 / 0.06);
+ color: #fff;
+}
+
+.admin-nav-item--active {
+ background: rgb(56 189 248 / 0.15);
+ color: #7dd3fc;
+ font-weight: 600;
+}
+
+.admin-sidebar-footer {
+ padding: 0 1rem;
+}
+
+.admin-back-link {
+ display: block;
+ padding: 0.5rem 0.85rem;
+ font-size: 0.875rem;
+ color: #94a3b8;
+ text-decoration: none;
+ border-radius: 0.5rem;
+}
+
+.admin-back-link:hover {
+ color: #e2e8f0;
+ background: rgb(255 255 255 / 0.06);
+}
+
+.admin-main-wrap {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.admin-topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.875rem 1.5rem;
+ background: #fff;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.admin-topbar-search-wrap {
+ flex: 1;
+ max-width: 28rem;
+}
+
+.admin-topbar-search {
+ width: 100%;
+ padding: 0.5rem 0.875rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+ font-size: 0.9375rem;
+ background: #f8fafc;
+}
+
+.admin-topbar-search:focus {
+ outline: 2px solid #38bdf8;
+ outline-offset: 0;
+ border-color: transparent;
+ background: #fff;
+}
+
+.admin-topbar-actions {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+.admin-icon-btn {
+ width: 2.5rem;
+ height: 2.5rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+ background: #fff;
+ cursor: pointer;
+ display: grid;
+ place-items: center;
+}
+
+.admin-icon-btn:hover {
+ background: #f8fafc;
+}
+
+.admin-bell {
+ width: 1.125rem;
+ height: 1.125rem;
+ border: 2px solid #64748b;
+ border-radius: 50% 50% 0 0;
+ border-bottom: none;
+ position: relative;
+}
+
+.admin-bell::after {
+ content: "";
+ position: absolute;
+ bottom: -4px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 4px;
+ height: 4px;
+ background: #64748b;
+ border-radius: 50%;
+}
+
+.admin-user-chip {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ padding: 0.35rem 0.65rem 0.35rem 0.35rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 999px;
+ background: #fff;
+}
+
+.admin-user-avatar {
+ width: 2rem;
+ height: 2rem;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #0ea5e9, #6366f1);
+ color: #fff;
+ font-size: 0.875rem;
+ font-weight: 700;
+ display: grid;
+ place-items: center;
+}
+
+.admin-user-meta {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.2;
+}
+
+.admin-user-name {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #0f172a;
+}
+
+.admin-user-role {
+ font-size: 0.6875rem;
+ color: #64748b;
+}
+
+.admin-content {
+ flex: 1;
+ padding: 1.5rem;
+ overflow: auto;
+}
+
+.admin-page {
+ max-width: 72rem;
+ margin: 0 auto;
+}
+
+.admin-page-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.admin-page-title {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: #0f172a;
+}
+
+.admin-page-sub {
+ margin: 0.35rem 0 0;
+ font-size: 0.9375rem;
+ color: #64748b;
+}
+
+.admin-btn {
+ font-size: 0.875rem;
+ font-weight: 600;
+ padding: 0.5rem 1rem;
+ border-radius: 0.5rem;
+ border: 1px solid #e2e8f0;
+ background: #fff;
+ color: #334155;
+ cursor: pointer;
+}
+
+.admin-btn:hover {
+ background: #f8fafc;
+}
+
+.admin-btn--primary {
+ background: #0ea5e9;
+ border-color: #0284c7;
+ color: #fff;
+}
+
+.admin-btn--primary:hover {
+ background: #0284c7;
+}
+
+.admin-btn--ghost {
+ background: transparent;
+ border-color: transparent;
+ color: #0ea5e9;
+}
+
+.admin-btn--ghost:hover {
+ background: #e0f2fe;
+}
+
+.admin-stats {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
+ margin-bottom: 1.5rem;
+}
+
+.admin-stat-card {
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.75rem;
+ padding: 1.25rem;
+ box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
+}
+
+.admin-stat-label {
+ margin: 0;
+ font-size: 0.8125rem;
+ color: #64748b;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.admin-stat-value {
+ margin: 0.5rem 0 0;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.admin-stat-delta {
+ margin: 0.35rem 0 0;
+ font-size: 0.8125rem;
+ color: #64748b;
+}
+
+.admin-stat-delta--up {
+ color: #059669;
+}
+
+.admin-panels {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: 1fr;
+}
+
+@media (min-width: 960px) {
+ .admin-panels {
+ grid-template-columns: 1fr 18rem;
+ }
+}
+
+.admin-panel {
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.75rem;
+ padding: 1.25rem;
+ box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
+}
+
+.admin-panel--wide {
+ grid-column: 1;
+}
+
+@media (min-width: 960px) {
+ .admin-panel--wide {
+ grid-row: span 2;
+ }
+}
+
+.admin-panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.admin-panel-head h2 {
+ margin: 0;
+ font-size: 1.0625rem;
+ font-weight: 600;
+ color: #0f172a;
+}
+
+.admin-table-wrap {
+ overflow-x: auto;
+ margin: 0 -0.25rem;
+}
+
+.admin-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.875rem;
+}
+
+.admin-table th {
+ text-align: left;
+ padding: 0.65rem 0.75rem;
+ color: #64748b;
+ font-weight: 600;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.admin-table td {
+ padding: 0.75rem;
+ border-bottom: 1px solid #f1f5f9;
+ color: #334155;
+}
+
+.admin-table-mono {
+ font-variant-numeric: tabular-nums;
+ color: #0f172a;
+ font-weight: 500;
+}
+
+.admin-badge {
+ display: inline-block;
+ padding: 0.2rem 0.5rem;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.admin-badge--confirmed {
+ background: #d1fae5;
+ color: #047857;
+}
+
+.admin-badge--completed {
+ background: #e0e7ff;
+ color: #4338ca;
+}
+
+.admin-badge--cancelled {
+ background: #fee2e2;
+ color: #b91c1c;
+}
+
+.admin-todo-list {
+ list-style: none;
+ margin: 0 0 1.25rem;
+ padding: 0;
+ font-size: 0.875rem;
+ color: #475569;
+}
+
+.admin-todo-list li {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ padding: 0.4rem 0;
+}
+
+.admin-todo-dot {
+ width: 0.5rem;
+ height: 0.5rem;
+ border-radius: 50%;
+ background: #0ea5e9;
+ margin-top: 0.35rem;
+ flex-shrink: 0;
+}
+
+.admin-todo-dot--muted {
+ background: #cbd5e1;
+}
+
+.admin-mini-chart {
+ border-top: 1px solid #f1f5f9;
+ padding-top: 1rem;
+}
+
+.admin-mini-chart-bars {
+ display: flex;
+ align-items: flex-end;
+ gap: 0.35rem;
+ height: 5rem;
+}
+
+.admin-mini-bar {
+ flex: 1;
+ background: linear-gradient(180deg, #38bdf8, #0ea5e9);
+ border-radius: 0.25rem 0.25rem 0 0;
+ min-height: 12%;
+}
+
+.admin-mini-chart-caption {
+ margin: 0.5rem 0 0;
+ font-size: 0.75rem;
+ color: #94a3b8;
+}
+
+/* —— Home (Rica-style tour landing) —— */
+.home-tour {
+ background: #fff;
+ color: #1e293b;
+}
+
+.home-hero {
+ text-align: center;
+ padding: 3.5rem 1.5rem 3rem;
+ background: linear-gradient(180deg, #f0f9ff 0%, #fff 85%);
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.home-hero-kicker {
+ margin: 0 0 0.5rem;
+ font-size: 0.8125rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: #0369a1;
+}
+
+.home-hero-title {
+ margin: 0;
+ font-size: clamp(2rem, 5vw, 3rem);
+ font-weight: 800;
+ letter-spacing: -0.03em;
+ text-transform: lowercase;
+ color: #0f172a;
+}
+
+.home-hero-brand {
+ margin: 0.5rem 0 1.75rem;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #64748b;
+ text-transform: lowercase;
+}
+
+.home-search {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: stretch;
+ justify-content: center;
+ gap: 0.5rem;
+ max-width: 36rem;
+ margin: 0 auto;
+}
+
+.home-search-input {
+ flex: 1;
+ min-width: 12rem;
+ padding: 0.75rem 1rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ background: #fff;
+}
+
+.home-search-input:focus {
+ outline: 2px solid #0ea5e9;
+ outline-offset: 0;
+ border-color: transparent;
+}
+
+.home-search-btn {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 0.5rem;
+ background: #0ea5e9;
+ color: #fff;
+ font-weight: 700;
+ font-size: 1rem;
+ cursor: pointer;
+}
+
+.home-search-btn:hover {
+ background: #0284c7;
+}
+
+.home-hero-split {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
+ gap: 0.75rem;
+ max-width: 40rem;
+ margin: 2rem auto 0;
+}
+
+.home-hero-split-link {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.25rem;
+ padding: 1rem 1.15rem;
+ border-radius: 0.75rem;
+ text-decoration: none;
+ border: 1px solid #e2e8f0;
+ background: #fff;
+ box-shadow: 0 2px 10px rgb(15 23 42 / 0.05);
+ transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
+}
+
+.home-hero-split-link:hover {
+ border-color: #7dd3fc;
+ box-shadow: 0 6px 20px rgb(14 165 233 / 0.12);
+ transform: translateY(-2px);
+}
+
+.home-hero-split-link--tours {
+ background: linear-gradient(135deg, #fff 0%, #f0f9ff 100%);
+}
+
+.home-hero-split-link--boats {
+ background: linear-gradient(135deg, #fff 0%, #ecfeff 100%);
+}
+
+.home-hero-split-label {
+ font-size: 0.6875rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #0369a1;
+}
+
+.home-hero-split-title {
+ font-size: 1.0625rem;
+ font-weight: 800;
+ color: #0f172a;
+ letter-spacing: -0.02em;
+}
+
+.home-section--boats-hub {
+ max-width: 75rem;
+}
+
+.home-boats-hub {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ align-items: stretch;
+ background: #0f172a;
+ color: #e2e8f0;
+ border-radius: 1rem;
+ overflow: hidden;
+ border: 1px solid #1e293b;
+}
+
+@media (min-width: 768px) {
+ .home-boats-hub {
+ grid-template-columns: 1fr minmax(14rem, 22rem);
+ }
+}
+
+.home-boats-hub-copy {
+ padding: 2rem 1.75rem;
+}
+
+.home-boats-hub-kicker {
+ margin: 0 0 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: #38bdf8;
+}
+
+.home-boats-hub-title {
+ margin: 0 0 0.75rem;
+ font-size: clamp(1.35rem, 3vw, 1.75rem);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ color: #fff;
+}
+
+.home-boats-hub-text {
+ margin: 0 0 1.25rem;
+ font-size: 0.9375rem;
+ line-height: 1.65;
+ color: #94a3b8;
+}
+
+.home-boats-hub-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.65rem;
+}
+
+.home-boats-hub-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.65rem 1.15rem;
+ border-radius: 0.5rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+ text-decoration: none;
+ border: 1px solid transparent;
+}
+
+.home-boats-hub-btn--primary {
+ background: #0ea5e9;
+ color: #fff;
+ border-color: #0284c7;
+}
+
+.home-boats-hub-btn--primary:hover {
+ background: #0284c7;
+}
+
+.home-boats-hub-btn--outline {
+ background: transparent;
+ color: #e2e8f0;
+ border-color: #334155;
+}
+
+.home-boats-hub-btn--outline:hover {
+ border-color: #7dd3fc;
+ color: #fff;
+}
+
+.home-boats-hub-visual {
+ min-height: 12rem;
+ background-size: cover;
+ background-position: center;
+}
+
+@media (min-width: 768px) {
+ .home-boats-hub-visual {
+ min-height: auto;
+ }
+}
+
+.home-section {
+ padding: 2.5rem 1.5rem;
+ max-width: 75rem;
+ margin: 0 auto;
+}
+
+.home-section--intro {
+ text-align: center;
+ max-width: 48rem;
+}
+
+.home-section-title {
+ margin: 0 0 1rem;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.home-section-lede {
+ margin: 0;
+ font-size: 1.0625rem;
+ line-height: 1.65;
+ color: #475569;
+}
+
+.home-section-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 0.5rem 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.home-section-title-lg {
+ margin: 0;
+ font-size: clamp(1.35rem, 3vw, 1.75rem);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ color: #0f172a;
+}
+
+.home-section-tag {
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: lowercase;
+ color: #0ea5e9;
+ padding: 0.2rem 0.5rem;
+ background: #e0f2fe;
+ border-radius: 0.25rem;
+}
+
+.home-section-tag--muted {
+ color: #64748b;
+ background: #f1f5f9;
+}
+
+.tour-carousel {
+ margin-bottom: 2.5rem;
+}
+
+.tour-carousel-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.tour-region-title {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 700;
+ text-transform: lowercase;
+ color: #334155;
+}
+
+.tour-carousel-nav {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.tour-carousel-btn {
+ padding: 0.4rem 0.85rem;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.375rem;
+ background: #fff;
+ color: #475569;
+ cursor: pointer;
+}
+
+.tour-carousel-btn:hover {
+ border-color: #0ea5e9;
+ color: #0369a1;
+}
+
+.tour-card-row {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
+}
+
+.tour-card {
+ border: 1px solid #e2e8f0;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ background: #fff;
+ box-shadow: 0 4px 14px rgb(15 23 42 / 0.06);
+}
+
+.tour-card-image {
+ height: 10rem;
+ background-size: cover;
+ background-position: center;
+}
+
+.tour-card-body {
+ padding: 1rem 1rem 1.15rem;
+}
+
+.tour-card-title {
+ margin: 0 0 0.5rem;
+ font-size: 1rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.tour-card-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ font-size: 0.8125rem;
+ color: #64748b;
+ margin-bottom: 0.5rem;
+}
+
+.tour-rating {
+ font-weight: 600;
+ color: #b45309;
+}
+
+.tour-price {
+ margin: 0;
+ font-size: 0.9375rem;
+}
+
+.tour-price-was {
+ text-decoration: line-through;
+ color: #94a3b8;
+ margin-right: 0.35rem;
+}
+
+.tour-price-now {
+ font-weight: 800;
+ color: #0f172a;
+}
+
+.tour-price-badge {
+ margin-left: 0.35rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: #dc2626;
+}
+
+.home-section--continents {
+ background: #f8fafc;
+ max-width: none;
+ padding-left: clamp(1rem, 4vw, 2rem);
+ padding-right: clamp(1rem, 4vw, 2rem);
+}
+
+.home-section--continents .home-section-head,
+.home-section--continents .continent-grid {
+ max-width: 75rem;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.continent-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
+}
+
+.continent-card {
+ border-radius: 0.75rem;
+ overflow: hidden;
+ border: 1px solid #e2e8f0;
+ background: #fff;
+ box-shadow: 0 2px 8px rgb(15 23 42 / 0.05);
+}
+
+.continent-card-image {
+ height: 7rem;
+ background-size: cover;
+ background-position: center;
+}
+
+.continent-card-body {
+ padding: 0.85rem 1rem 1rem;
+}
+
+.continent-card-title {
+ margin: 0 0 0.25rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.continent-card-price {
+ margin: 0;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #64748b;
+ text-transform: lowercase;
+}
+
+.theme-strip-wrap {
+ position: relative;
+}
+
+.theme-strip-nav {
+ margin-bottom: 1rem;
+}
+
+.theme-strip {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
+}
+
+.theme-card {
+ border-radius: 0.75rem;
+ overflow: hidden;
+ border: 1px solid #e2e8f0;
+ background: #fff;
+}
+
+.theme-card-image {
+ height: 8rem;
+ background-size: cover;
+ background-position: center;
+}
+
+.theme-card-body {
+ padding: 0.85rem 1rem 1rem;
+}
+
+.theme-card-title {
+ margin: 0 0 0.25rem;
+ font-size: 1rem;
+ font-weight: 700;
+}
+
+.theme-card-price {
+ margin: 0;
+ font-size: 0.8125rem;
+ color: #64748b;
+ text-transform: lowercase;
+}
+
+.home-section--testimonials {
+ background: linear-gradient(180deg, #fff 0%, #f0f9ff 100%);
+ max-width: none;
+}
+
+.home-section--testimonials .home-section-head,
+.home-section--testimonials .testimonial-block {
+ max-width: 48rem;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.testimonial-block {
+ text-align: center;
+ padding: 1rem 0 2rem;
+}
+
+.testimonial-quote {
+ margin: 1rem 0 0.75rem;
+ font-size: 1.0625rem;
+ line-height: 1.65;
+ font-style: italic;
+ color: #334155;
+}
+
+.testimonial-name {
+ margin: 0;
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: #0f172a;
+ text-transform: capitalize;
+}
+
+.home-footer {
+ background: #0f172a;
+ color: #e2e8f0;
+ padding: 2.5rem 1.5rem 1.5rem;
+}
+
+.home-footer-grid {
+ display: grid;
+ gap: 2rem;
+ grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
+ max-width: 75rem;
+ margin: 0 auto 2rem;
+}
+
+.home-footer-heading {
+ margin: 0 0 0.75rem;
+ font-size: 1rem;
+ font-weight: 700;
+ color: #fff;
+}
+
+.home-footer-text {
+ margin: 0 0 0.75rem;
+ font-size: 0.875rem;
+ line-height: 1.55;
+ color: #94a3b8;
+}
+
+.home-footer-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ font-size: 0.875rem;
+ color: #cbd5e1;
+ line-height: 1.7;
+}
+
+.home-footer-links {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ font-size: 0.875rem;
+ line-height: 1.9;
+}
+
+.home-footer-links a {
+ color: #cbd5e1;
+ text-decoration: none;
+}
+
+.home-footer-links a:hover {
+ color: #7dd3fc;
+}
+
+.home-footer-tags {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.home-footer-tags li {
+ font-size: 0.8125rem;
+ padding: 0.25rem 0.5rem;
+ background: rgb(255 255 255 / 0.08);
+ border-radius: 0.25rem;
+ color: #cbd5e1;
+}
+
+.home-footer-copy {
+ margin: 0;
+ text-align: center;
+ font-size: 0.8125rem;
+ color: #64748b;
+ max-width: 75rem;
+ margin-inline: auto;
+}
+
+/* —— Boat rentals landing —— */
+.boat-landing {
+ background: #fff;
+ color: #0f172a;
+}
+
+.boat-landing-hero {
+ text-align: center;
+ padding: 2.75rem 1.5rem 2.5rem;
+ background: linear-gradient(180deg, #ecfeff 0%, #fff 70%);
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.boat-landing-kicker {
+ margin: 0 0 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: #0e7490;
+}
+
+.boat-landing-title {
+ margin: 0 0 0.75rem;
+ font-size: clamp(1.75rem, 4vw, 2.35rem);
+ font-weight: 800;
+ letter-spacing: -0.03em;
+}
+
+.boat-landing-lede {
+ margin: 0 auto 1.5rem;
+ max-width: 40rem;
+ font-size: 1.0625rem;
+ line-height: 1.6;
+ color: #475569;
+}
+
+.boat-landing-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.65rem;
+ justify-content: center;
+}
+
+.boat-landing-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.7rem 1.25rem;
+ border-radius: 0.5rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+ text-decoration: none;
+ border: 1px solid transparent;
+}
+
+.boat-landing-btn--primary {
+ background: #0ea5e9;
+ color: #fff;
+ border-color: #0284c7;
+}
+
+.boat-landing-btn--primary:hover {
+ background: #0284c7;
+}
+
+.boat-landing-btn--ghost {
+ background: #fff;
+ color: #0f172a;
+ border-color: #e2e8f0;
+}
+
+.boat-landing-btn--ghost:hover {
+ border-color: #0ea5e9;
+ color: #0369a1;
+}
+
+.boat-landing-section {
+ padding: 2.5rem 1.5rem;
+ max-width: 75rem;
+ margin: 0 auto;
+}
+
+.boat-landing-h2 {
+ margin: 0 0 1.25rem;
+ font-size: 1.35rem;
+ font-weight: 800;
+ letter-spacing: -0.02em;
+}
+
+.boat-landing-highlights {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
+}
+
+.boat-landing-highlight {
+ margin: 0;
+ padding: 1.15rem 1.25rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.75rem;
+ background: #f8fafc;
+}
+
+.boat-landing-highlight h3 {
+ margin: 0 0 0.35rem;
+ font-size: 1rem;
+ font-weight: 700;
+}
+
+.boat-landing-highlight p {
+ margin: 0;
+ font-size: 0.875rem;
+ color: #64748b;
+ line-height: 1.5;
+}
+
+.boat-landing-section--featured {
+ background: #f8fafc;
+ max-width: none;
+ padding-left: clamp(1rem, 4vw, 2rem);
+ padding-right: clamp(1rem, 4vw, 2rem);
+}
+
+.boat-landing-section--featured .boat-landing-section-head,
+.boat-landing-section--featured .boat-landing-grid {
+ max-width: 75rem;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.boat-landing-section-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.75rem;
+ margin-bottom: 1.25rem;
+}
+
+.boat-landing-link-all {
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: #0ea5e9;
+ text-decoration: none;
+}
+
+.boat-landing-link-all:hover {
+ text-decoration: underline;
+}
+
+.boat-landing-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
+}
+
+.boat-landing-card {
+ border-radius: 0.75rem;
+ overflow: hidden;
+ border: 1px solid #e2e8f0;
+ background: #fff;
+ box-shadow: 0 2px 10px rgb(15 23 42 / 0.05);
+}
+
+.boat-landing-card-image {
+ height: 11rem;
+ background-size: cover;
+ background-position: center;
+}
+
+.boat-landing-card-body {
+ padding: 1rem 1.1rem 1.15rem;
+}
+
+.boat-landing-card-title {
+ margin: 0 0 0.35rem;
+ font-size: 1.0625rem;
+ font-weight: 700;
+}
+
+.boat-landing-card-tag {
+ margin: 0 0 0.5rem;
+ font-size: 0.8125rem;
+ color: #64748b;
+}
+
+.boat-landing-card-price {
+ margin: 0 0 0.75rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.boat-landing-card-cta {
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: #0ea5e9;
+ text-decoration: none;
+}
+
+.boat-landing-card-cta:hover {
+ text-decoration: underline;
+}
+
+.boat-landing-cta-banner {
+ padding: 2.5rem 1.5rem 3rem;
+ background: linear-gradient(135deg, #0c4a6e 0%, #0e7490 50%, #155e75 100%);
+ color: #ecfeff;
+}
+
+.boat-landing-cta-inner {
+ max-width: 40rem;
+ margin: 0 auto;
+ text-align: center;
+}
+
+.boat-landing-cta-title {
+ margin: 0 0 0.5rem;
+ font-size: 1.5rem;
+ font-weight: 800;
+}
+
+.boat-landing-cta-text {
+ margin: 0 0 1.25rem;
+ font-size: 0.9375rem;
+ line-height: 1.6;
+ color: #a5f3fc;
+}
+
+.boat-landing-btn--on-dark {
+ background: #fff;
+ color: #0c4a6e;
+ border-color: #fff;
+}
+
+.boat-landing-btn--on-dark:hover {
+ background: #ecfeff;
+}
+
+/* —— Boat map search (cab map–style) —— */
+.boat-search-page {
+ min-height: calc(100vh - 4rem);
+ display: flex;
+ flex-direction: column;
+ background: #f1f5f9;
+}
+
+.boat-search-top {
+ padding: 1rem 1.25rem;
+ background: #fff;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.boat-search-back {
+ display: inline-block;
+ margin-bottom: 0.35rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #0369a1;
+ text-decoration: none;
+}
+
+.boat-search-back:hover {
+ text-decoration: underline;
+}
+
+.boat-search-title {
+ margin: 0;
+ font-size: 1.35rem;
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ color: #0f172a;
+}
+
+.boat-search-sub {
+ margin: 0.25rem 0 0;
+ font-size: 0.875rem;
+ color: #64748b;
+}
+
+.boat-search-body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+@media (min-width: 900px) {
+ .boat-search-body {
+ flex-direction: row;
+ align-items: stretch;
+ }
+}
+
+.boat-filter-panel {
+ flex-shrink: 0;
+ width: 100%;
+ max-height: none;
+ padding: 1.25rem 1.25rem 1.5rem;
+ background: #fff;
+ border-bottom: 1px solid #e2e8f0;
+ overflow-y: auto;
+}
+
+@media (min-width: 900px) {
+ .boat-filter-panel {
+ width: 17.5rem;
+ max-width: 100%;
+ border-bottom: none;
+ border-right: 1px solid #e2e8f0;
+ max-height: calc(100vh - 4rem - 5.5rem);
+ position: sticky;
+ top: 0;
+ align-self: flex-start;
+ }
+}
+
+.boat-filter-heading {
+ margin: 0 0 1rem;
+ font-size: 0.8125rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: #64748b;
+}
+
+.boat-filter-fieldset {
+ margin: 0 0 1.25rem;
+ padding: 0;
+ border: none;
+}
+
+.boat-filter-fieldset--solo {
+ padding-bottom: 0.25rem;
+}
+
+.boat-filter-legend {
+ padding: 0;
+ margin: 0 0 0.5rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.boat-filter-hint {
+ margin: 0 0 0.5rem;
+ font-size: 0.75rem;
+ color: #64748b;
+ line-height: 1.4;
+}
+
+.boat-filter-actions {
+ margin-bottom: 0.5rem;
+}
+
+.boat-filter-chip {
+ padding: 0.35rem 0.65rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.375rem;
+ background: #f8fafc;
+ color: #475569;
+ cursor: pointer;
+}
+
+.boat-filter-chip:hover {
+ border-color: #0ea5e9;
+ color: #0369a1;
+}
+
+.boat-filter-checklist {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.boat-filter-checklist li {
+ margin: 0.35rem 0 0;
+}
+
+.boat-filter-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+ color: #334155;
+ cursor: pointer;
+}
+
+.boat-filter-label input {
+ width: 1rem;
+ height: 1rem;
+ accent-color: #0ea5e9;
+}
+
+.boat-filter-select {
+ width: 100%;
+ padding: 0.5rem 0.65rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+ background: #fff;
+ color: #0f172a;
+}
+
+.boat-filter-select:focus {
+ outline: 2px solid #38bdf8;
+ outline-offset: 0;
+ border-color: transparent;
+}
+
+.boat-filter-summary {
+ margin: 0 0 0.75rem;
+ font-size: 0.8125rem;
+ color: #64748b;
+ line-height: 1.45;
+}
+
+.boat-filter-reset {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ border: 1px dashed #cbd5e1;
+ border-radius: 0.5rem;
+ background: #fff;
+ color: #475569;
+ cursor: pointer;
+}
+
+.boat-filter-reset:hover {
+ border-color: #0ea5e9;
+ color: #0369a1;
+}
+
+.boat-search-main {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 1rem 1.25rem 1.5rem;
+ gap: 1rem;
+ overflow: auto;
+}
+
+.boat-map {
+ border-radius: 0.75rem;
+ overflow: hidden;
+ border: 1px solid #e2e8f0;
+ background: #fff;
+ box-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
+}
+
+.boat-map-inner {
+ position: relative;
+ height: min(22rem, 42vw);
+ min-height: 12rem;
+ background: linear-gradient(160deg, #bae6fd 0%, #7dd3fc 35%, #38bdf8 70%, #0ea5e9 100%);
+ background-image:
+ linear-gradient(rgb(255 255 255 / 0.12) 1px, transparent 1px),
+ linear-gradient(90deg, rgb(255 255 255 / 0.12) 1px, transparent 1px);
+ background-size: 48px 48px, 48px 48px;
+}
+
+.boat-map-pin {
+ position: absolute;
+ width: 0.875rem;
+ height: 0.875rem;
+ border-radius: 50% 50% 50% 0;
+ transform: rotate(-45deg);
+ background: #dc2626;
+ border: 2px solid #fff;
+ box-shadow: 0 2px 6px rgb(0 0 0 / 0.25);
+}
+
+.boat-map-pin--1 {
+ top: 28%;
+ left: 22%;
+}
+
+.boat-map-pin--2 {
+ top: 42%;
+ left: 55%;
+}
+
+.boat-map-pin--3 {
+ top: 58%;
+ left: 38%;
+}
+
+.boat-map-pin--4 {
+ top: 36%;
+ left: 72%;
+}
+
+.boat-map-caption {
+ margin: 0;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.75rem;
+ color: #64748b;
+ background: #f8fafc;
+ border-top: 1px solid #e2e8f0;
+}
+
+.boat-results-title {
+ margin: 0 0 0.75rem;
+ font-size: 1rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.boat-results-grid {
+ display: grid;
+ gap: 0.85rem;
+ grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
+}
+
+.boat-result-card {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.65rem;
+ overflow: hidden;
+ background: #fff;
+ box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
+}
+
+.boat-result-image {
+ height: 7.5rem;
+ background-size: cover;
+ background-position: center;
+}
+
+.boat-result-body {
+ padding: 0.75rem 0.85rem 0.9rem;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.boat-result-name {
+ margin: 0 0 0.35rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+}
+
+.boat-result-meta {
+ margin: 0 0 0.5rem;
+ font-size: 0.75rem;
+ color: #64748b;
+ line-height: 1.4;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+}
+
+.boat-result-price {
+ margin: 0 0 0.65rem;
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.boat-result-cta {
+ display: inline-block;
+ box-sizing: border-box;
+ margin-top: auto;
+ width: 100%;
+ padding: 0.45rem 0.65rem;
+ font-size: 0.8125rem;
+ font-weight: 700;
+ border: none;
+ border-radius: 0.375rem;
+ background: #0ea5e9;
+ color: #fff;
+ cursor: pointer;
+ text-align: center;
+ text-decoration: none;
+}
+
+.boat-result-cta:hover {
+ background: #0284c7;
+}
+
+.boat-results-empty {
+ margin: 1rem 0 0;
+ font-size: 0.9375rem;
+ color: #64748b;
+}
+
+.nav-link--btn {
+ font: inherit;
+ cursor: pointer;
+ background: none;
+ border: none;
+}
+
+.nav-link--btn:hover {
+ color: #0f172a;
+ background: #f1f5f9;
+}
+
+.admin-badge--requested {
+ background: #fef3c7;
+ color: #b45309;
+}
+
+.admin-badge--approved {
+ background: #dbeafe;
+ color: #1d4ed8;
+}
+
+.admin-badge--declined {
+ background: #fee2e2;
+ color: #b91c1c;
+}
+
+.admin-sign-out {
+ margin-left: 0.5rem;
+}
+
+.admin-panel-pad {
+ padding: 1rem 1.25rem;
+}
+
+.auth-loading {
+ min-height: 50vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #64748b;
+}
+
+.auth-page {
+ min-height: 100vh;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding: 2rem 1rem 3rem;
+ background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
+}
+
+.auth-card {
+ width: 100%;
+ max-width: 26rem;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.75rem;
+ padding: 2rem 1.75rem;
+ box-shadow: 0 10px 40px rgb(15 23 42 / 0.06);
+}
+
+.auth-card--wide {
+ max-width: 40rem;
+}
+
+.auth-kicker {
+ margin: 0 0 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: #0369a1;
+}
+
+.auth-title {
+ margin: 0 0 0.5rem;
+ font-size: 1.5rem;
+ letter-spacing: -0.02em;
+}
+
+.auth-lede {
+ margin: 0 0 1.25rem;
+ font-size: 0.9375rem;
+ color: #64748b;
+}
+
+.auth-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.auth-form--grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+.auth-label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: #334155;
+}
+
+.auth-label--full {
+ grid-column: 1 / -1;
+}
+
+.auth-input {
+ padding: 0.55rem 0.65rem;
+ border: 1px solid #cbd5e1;
+ border-radius: 0.375rem;
+ font-size: 0.9375rem;
+}
+
+.auth-textarea {
+ resize: vertical;
+ min-height: 4rem;
+}
+
+.auth-error {
+ margin: 0;
+ font-size: 0.875rem;
+ color: #b91c1c;
+}
+
+.auth-success {
+ margin: 0;
+ font-size: 0.875rem;
+ color: #047857;
+}
+
+.auth-submit {
+ padding: 0.65rem 1rem;
+ font-weight: 700;
+ font-size: 0.9375rem;
+ border: none;
+ border-radius: 0.375rem;
+ background: #0ea5e9;
+ color: #fff;
+ cursor: pointer;
+}
+
+.auth-submit:hover:not(:disabled) {
+ background: #0284c7;
+}
+
+.auth-submit:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+}
+
+.auth-footer {
+ margin: 1.25rem 0 0;
+ font-size: 0.875rem;
+ color: #64748b;
+}
+
+.auth-footer a {
+ color: #0369a1;
+ font-weight: 600;
+ text-decoration: none;
+}
+
+.auth-footer a:hover {
+ text-decoration: underline;
+}
+
+.auth-role-toggle {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.auth-role-btn {
+ flex: 1;
+ padding: 0.5rem 0.75rem;
+ border-radius: 0.375rem;
+ border: 1px solid #cbd5e1;
+ background: #f8fafc;
+ font-weight: 600;
+ font-size: 0.875rem;
+ cursor: pointer;
+ color: #475569;
+}
+
+.auth-role-btn--active {
+ border-color: #0ea5e9;
+ background: #e0f2fe;
+ color: #0369a1;
+}
+
+.vendor-why {
+ max-width: 56rem;
+ margin: 0 auto;
+ padding: 2rem 1.5rem 3rem;
+}
+
+.vendor-why-hero {
+ margin-bottom: 2.5rem;
+}
+
+.vendor-why-kicker {
+ margin: 0 0 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #0369a1;
+}
+
+.vendor-why-title {
+ margin: 0 0 0.75rem;
+ font-size: 2rem;
+ letter-spacing: -0.02em;
+}
+
+.vendor-why-lede {
+ margin: 0 0 1.5rem;
+ font-size: 1.0625rem;
+ color: #475569;
+ max-width: 40rem;
+}
+
+.vendor-why-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+.vendor-why-btn {
+ display: inline-block;
+ padding: 0.65rem 1.1rem;
+ border-radius: 0.375rem;
+ font-weight: 700;
+ font-size: 0.9375rem;
+ text-decoration: none;
+}
+
+.vendor-why-btn--primary {
+ background: #0ea5e9;
+ color: #fff;
+}
+
+.vendor-why-btn--primary:hover {
+ background: #0284c7;
+}
+
+.vendor-why-btn--ghost {
+ border: 1px solid #cbd5e1;
+ color: #0f172a;
+ background: #fff;
+}
+
+.vendor-why-grid {
+ list-style: none;
+ margin: 0 0 2.5rem;
+ padding: 0;
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
+}
+
+.vendor-why-card {
+ margin: 0;
+ padding: 1.25rem;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+}
+
+.vendor-why-card h2 {
+ margin: 0 0 0.5rem;
+ font-size: 1.0625rem;
+}
+
+.vendor-why-card p {
+ margin: 0;
+ font-size: 0.9375rem;
+ color: #64748b;
+}
+
+.vendor-why-cta {
+ padding: 1.5rem;
+ background: #0f172a;
+ color: #e2e8f0;
+ border-radius: 0.5rem;
+}
+
+.vendor-why-cta h2 {
+ margin: 0 0 0.5rem;
+ color: #fff;
+}
+
+.vendor-why-cta p {
+ margin: 0 0 1rem;
+ color: #94a3b8;
+}
+
+.listing-detail {
+ max-width: 56rem;
+ margin: 0 auto;
+ padding: 2rem 1.5rem 3rem;
+}
+
+.listing-detail-back {
+ display: inline-block;
+ margin-bottom: 1.25rem;
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: #0369a1;
+ text-decoration: none;
+}
+
+.listing-detail-back:hover {
+ text-decoration: underline;
+}
+
+.listing-detail-grid {
+ display: grid;
+ gap: 1.5rem;
+ grid-template-columns: 1fr;
+}
+
+@media (min-width: 48rem) {
+ .listing-detail-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+.listing-detail-image {
+ min-height: 14rem;
+ border-radius: 0.5rem;
+ background: linear-gradient(135deg, #e0f2fe, #7dd3fc);
+ background-size: cover;
+ background-position: center;
+}
+
+.listing-detail-kicker {
+ margin: 0 0 0.35rem;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: #0369a1;
+}
+
+.listing-detail-title {
+ margin: 0 0 0.5rem;
+ font-size: 1.75rem;
+ letter-spacing: -0.02em;
+}
+
+.listing-detail-meta {
+ margin: 0 0 0.75rem;
+ font-size: 0.9375rem;
+ color: #64748b;
+}
+
+.listing-detail-price {
+ margin: 0 0 1rem;
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.listing-detail-body {
+ margin: 0 0 1rem;
+ font-size: 0.9375rem;
+ color: #334155;
+ line-height: 1.6;
+}
+
+.listing-detail-actions {
+ margin: 1.25rem 0 1rem;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+}
+
+.listing-detail-book-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.65rem 1.25rem;
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: #fff;
+ text-decoration: none;
+ background: #0ea5e9;
+ border: none;
+ border-radius: 0.5rem;
+ box-shadow: 0 1px 2px rgb(14 165 233 / 0.35);
+}
+
+.listing-detail-book-btn:hover {
+ background: #0284c7;
+ color: #fff;
+}
+
+.listing-detail-book-btn--pending {
+ cursor: default;
+ opacity: 0.75;
+ pointer-events: none;
+}
+
+.listing-detail-book-hint {
+ margin: 0;
+ font-size: 0.875rem;
+ max-width: 28rem;
+}
+
+.booking-request-page .booking-request-summary {
+ margin-top: 1.5rem;
+ padding: 1.25rem;
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+}
+
+.booking-request-page .booking-request-summary h2 {
+ margin: 0 0 0.5rem;
+ font-size: 1.25rem;
+}
+
+.catalog-strip-sub {
+ margin: 1.5rem 0 0.75rem;
+ font-size: 1.0625rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.catalog-strip-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
+}
+
+.catalog-strip-card {
+ display: flex;
+ flex-direction: column;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ text-decoration: none;
+ color: inherit;
+ box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
+}
+
+.catalog-strip-card:hover {
+ border-color: #bae6fd;
+}
+
+.catalog-strip-image {
+ height: 7rem;
+ background-size: cover;
+ background-position: center;
+}
+
+.catalog-strip-body {
+ padding: 0.75rem 0.85rem 1rem;
+}
+
+.catalog-strip-vendor {
+ margin: 0 0 0.25rem;
+ font-size: 0.6875rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: #64748b;
+}
+
+.catalog-strip-title {
+ margin: 0 0 0.35rem;
+ font-size: 0.9375rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.catalog-strip-meta {
+ margin: 0 0 0.35rem;
+ font-size: 0.8125rem;
+ color: #64748b;
+}
+
+.catalog-strip-price {
+ margin: 0;
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: #0369a1;
+}
+
+.tours-card-image {
+ height: 8rem;
+ margin: -1.25rem -1.25rem 1rem;
+ border-radius: 0.5rem 0.5rem 0 0;
+ background-size: cover;
+ background-position: center;
+}
+
+.vendor-settings-form {
+ max-width: 40rem;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+.vendor-listing-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.vendor-listing-list li {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr auto auto;
+ gap: 0.75rem;
+ align-items: center;
+ padding: 0.75rem 1.25rem;
+ border-bottom: 1px solid #f1f5f9;
+ font-size: 0.875rem;
+}
+
+@media (max-width: 48rem) {
+ .vendor-listing-list li {
+ grid-template-columns: 1fr;
+ }
+
+ .vendor-settings-form {
+ grid-template-columns: 1fr;
+ }
+
+ .auth-form--grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.vendor-listing-title {
+ font-weight: 600;
+ color: #0f172a;
+}
+
+.vendor-create-listing-toggle {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1.25rem;
+}
+
+.vendor-create-listing-form {
+ margin-top: 0;
+}
+
+.vendor-create-listing-note {
+ margin: 0 0 1rem;
+ font-size: 0.875rem;
+ line-height: 1.45;
+}
+
+.vendor-create-listing-note code {
+ font-size: 0.8125rem;
+}
+
+.vendor-create-listing-category-fallback {
+ margin: 0.35rem 0 0;
+ font-size: 0.8125rem;
+ line-height: 1.4;
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..19e3f12
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,16 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
+import { AuthProvider } from "./auth/AuthContext";
+import "./index.css";
+import App from "./App.tsx";
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+
+ ,
+);
diff --git a/src/marketing/attribution.ts b/src/marketing/attribution.ts
new file mode 100644
index 0000000..3d04769
--- /dev/null
+++ b/src/marketing/attribution.ts
@@ -0,0 +1,72 @@
+import type { NavigateFunction } from "react-router-dom";
+
+/** Query keys forwarded to public listing detail GETs for Django `extract_attribution_from_request`. */
+export const ATTRIBUTION_QUERY_KEYS = [
+ "utm_source",
+ "utm_medium",
+ "utm_campaign",
+ "utm_term",
+ "utm_content",
+ "gclid",
+ "fbclid",
+] as const;
+
+export type ListingKind = "equipment" | "adventure";
+
+const STORAGE_PREFIX = "watertrek_marketing_click";
+
+function storageKey(kind: ListingKind, publicId: string): string {
+ return `${STORAGE_PREFIX}:${kind}:${publicId}`;
+}
+
+export function attributionParamsFromSearch(search: string): Record {
+ const sp = new URLSearchParams(search);
+ const out: Record = {};
+ for (const key of ATTRIBUTION_QUERY_KEYS) {
+ const v = sp.get(key);
+ if (v != null && v !== "") out[key] = v;
+ }
+ return out;
+}
+
+export function hasAttributionParams(params: Record): boolean {
+ return Object.keys(params).length > 0;
+}
+
+export function persistMarketingClickId(kind: ListingKind, publicId: string, id: number): void {
+ try {
+ sessionStorage.setItem(storageKey(kind, publicId), String(id));
+ } catch {
+ // ignore quota / private mode
+ }
+}
+
+export function readMarketingClickId(kind: ListingKind, publicId: string): number | null {
+ try {
+ const raw = sessionStorage.getItem(storageKey(kind, publicId));
+ if (raw == null) return null;
+ const n = Number(raw);
+ return Number.isFinite(n) ? n : null;
+ } catch {
+ return null;
+ }
+}
+
+/** Remove attribution keys from the URL without losing other query params (replace, no new history entry). */
+export function stripAttributionFromUrl(
+ navigate: NavigateFunction,
+ pathname: string,
+ search: string,
+): void {
+ const sp = new URLSearchParams(search);
+ let changed = false;
+ for (const key of ATTRIBUTION_QUERY_KEYS) {
+ if (sp.has(key)) {
+ sp.delete(key);
+ changed = true;
+ }
+ }
+ if (!changed) return;
+ const next = sp.toString();
+ navigate({ pathname, search: next ? `?${next}` : "" }, { replace: true });
+}
diff --git a/src/pages/AdventureBookPage.tsx b/src/pages/AdventureBookPage.tsx
new file mode 100644
index 0000000..274d257
--- /dev/null
+++ b/src/pages/AdventureBookPage.tsx
@@ -0,0 +1,97 @@
+import { useEffect, useState } from "react";
+import { Link, useNavigate, useParams } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import { useAuth } from "../auth/AuthContext";
+import {
+ attributionParamsFromSearch,
+ hasAttributionParams,
+ persistMarketingClickId,
+ readMarketingClickId,
+ stripAttributionFromUrl,
+} from "../marketing/attribution";
+import type { AdventureOfferingDetail } from "../api/types";
+
+export function AdventureBookPage() {
+ const { publicId } = useParams<{ publicId: string }>();
+ const navigate = useNavigate();
+ const { user } = useAuth();
+ const [offering, setOffering] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!publicId) return;
+ const pathnameAtStart = window.location.pathname;
+ const searchAtStart = window.location.search;
+ const attribution = attributionParamsFromSearch(searchAtStart);
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.getAdventureOffering(
+ publicId,
+ hasAttributionParams(attribution) ? attribution : undefined,
+ );
+ if (!cancelled) {
+ setOffering(data);
+ if (hasAttributionParams(attribution)) {
+ persistMarketingClickId("adventure", publicId, data.marketing_click_id);
+ stripAttributionFromUrl(navigate, pathnameAtStart, searchAtStart);
+ }
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [publicId, navigate]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !offering) {
+ return (
+
+
{error ?? "Tour not found."}
+
+ Back to tours
+
+
+ );
+ }
+
+ const marketingClickIdForConversion =
+ readMarketingClickId("adventure", offering.public_id) ?? offering.marketing_click_id;
+
+ return (
+
+
+ ← Back to tour
+
+
Request this tour
+
+ You are signed in as {user?.email} . Confirm details below; a full booking form can submit to
+ the WaterTrekk booking API next.
+
+
+ {offering.title}
+
+ {offering.vendor_business_name} · {offering.meeting_point} · ${offering.price_per_person} / person
+
+
+ marketing_click_id {marketingClickIdForConversion}{" "}
+ (send this on the booking request for attribution)
+
+
+
+ );
+}
diff --git a/src/pages/AdventureDetailPage.tsx b/src/pages/AdventureDetailPage.tsx
new file mode 100644
index 0000000..3c51ef8
--- /dev/null
+++ b/src/pages/AdventureDetailPage.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useState } from "react";
+import { Link, useNavigate, useParams } from "react-router-dom";
+import { useAuth } from "../auth/AuthContext";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import {
+ attributionParamsFromSearch,
+ hasAttributionParams,
+ persistMarketingClickId,
+ stripAttributionFromUrl,
+} from "../marketing/attribution";
+import type { AdventureOfferingDetail } from "../api/types";
+
+export function AdventureDetailPage() {
+ const { publicId } = useParams<{ publicId: string }>();
+ const navigate = useNavigate();
+ const { user, loading: authLoading } = useAuth();
+ const [offering, setOffering] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!publicId) return;
+ const pathnameAtStart = window.location.pathname;
+ const searchAtStart = window.location.search;
+ const attribution = attributionParamsFromSearch(searchAtStart);
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.getAdventureOffering(
+ publicId,
+ hasAttributionParams(attribution) ? attribution : undefined,
+ );
+ if (!cancelled) {
+ setOffering(data);
+ persistMarketingClickId("adventure", publicId, data.marketing_click_id);
+ if (hasAttributionParams(attribution)) {
+ stripAttributionFromUrl(navigate, pathnameAtStart, searchAtStart);
+ }
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [publicId, navigate]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !offering) {
+ return (
+
+
{error ?? "Offering not found."}
+
+ Back to tours
+
+
+ );
+ }
+
+ const img =
+ offering.images.find((i) => i.is_primary)?.image_url ?? offering.images[0]?.image_url;
+
+ return (
+
+
+ ← Tours
+
+
+
+
+
{offering.vendor_business_name}
+
{offering.title}
+
+ Meet at {offering.meeting_point} · Up to {offering.capacity} guests · {offering.duration_minutes} minutes
+
+
${offering.price_per_person} / person
+
{offering.description}
+
+ {authLoading ? (
+
Loading…
+ ) : (
+
+ {user ? "Request to book" : "Sign in to book"}
+
+ )}
+ {!authLoading && !user && (
+
+ You can browse without an account. Sign in (or create one) when you are ready to request this tour.
+
+ )}
+
+
+ marketing_click_id {offering.marketing_click_id} ({offering.click_traffic_type})
+
+
+
+
+ );
+}
diff --git a/src/pages/BoatRentalsPage.tsx b/src/pages/BoatRentalsPage.tsx
new file mode 100644
index 0000000..98c73c7
--- /dev/null
+++ b/src/pages/BoatRentalsPage.tsx
@@ -0,0 +1,129 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { EquipmentItem } from "../api/types";
+
+const highlights = [
+ {
+ title: "Licensed & insured",
+ text: "Every listing is verified so you can book with confidence.",
+ },
+ {
+ title: "Flexible dates",
+ text: "Half-day, full-day, and multi-day rentals depending on the vessel.",
+ },
+ {
+ title: "Local docks",
+ text: "Pick up from marinas and piers across the region.",
+ },
+];
+
+export function BoatRentalsPage() {
+ const [featured, setFeatured] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const items = await api.listEquipmentItems();
+ if (!cancelled) setFeatured(items.slice(0, 12));
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ return (
+
+
+ Boat rentals
+ Your day on the water starts here
+
+ Browse live equipment from WaterTrekk vendors. Filter on the map search page by location and price.
+
+
+
+ Search & filter
+
+
+ Prefer a guided tour?
+
+
+
+
+
+ Why rent with WaterTrekk
+
+ {highlights.map((h) => (
+
+ {h.title}
+ {h.text}
+
+ ))}
+
+
+
+
+
+
From the API catalog
+
+ Open search →
+
+
+ {loading && Loading listings…
}
+ {error && {error}
}
+
+ {featured.map((f) => {
+ const img =
+ f.images.find((i) => i.is_primary)?.image_url ?? f.images[0]?.image_url;
+ return (
+
+
+
+
{f.title}
+
{f.category.name}
+
+ From ${f.price_per_day} / day · {f.location}
+
+
+ View listing
+
+
+
+ );
+ })}
+
+ {!loading && !error && featured.length === 0 && (
+ No equipment published yet. Check back soon.
+ )}
+
+
+
+
+
Ready to pick a dock?
+
+ Use search to filter the live catalog by location and price, then open a listing for full detail.
+
+
+ Go to search
+
+
+
+
+ );
+}
diff --git a/src/pages/BoatSearchPage.tsx b/src/pages/BoatSearchPage.tsx
new file mode 100644
index 0000000..7c3cbc9
--- /dev/null
+++ b/src/pages/BoatSearchPage.tsx
@@ -0,0 +1,158 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { EquipmentItem } from "../api/types";
+
+export function BoatSearchPage() {
+ const [items, setItems] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [locationQ, setLocationQ] = useState("");
+ const [minPrice, setMinPrice] = useState("");
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.listEquipmentItems();
+ if (!cancelled) setItems(data);
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const filtered = useMemo(() => {
+ const q = locationQ.trim().toLowerCase();
+ const min = minPrice ? Number.parseFloat(minPrice) : null;
+ return items.filter((b) => {
+ if (q && !b.location.toLowerCase().includes(q)) return false;
+ if (min != null && !Number.isNaN(min) && Number.parseFloat(b.price_per_day) < min) return false;
+ return true;
+ });
+ }, [items, locationQ, minPrice]);
+
+ return (
+
+
+
+
+
+ Filters
+
+
+
+ Location contains
+
+
Matches the listing location field (case insensitive).
+
setLocationQ(e.target.value)}
+ placeholder="e.g. Miami"
+ />
+
+
+
+
+ Minimum price / day
+
+ setMinPrice(e.target.value)}
+ placeholder="Any"
+ />
+
+
+
+ {loading
+ ? "Loading…"
+ : `${filtered.length} listing${filtered.length === 1 ? "" : "s"} match your filters.`}
+
+
+ {
+ setLocationQ("");
+ setMinPrice("");
+ }}
+ >
+ Reset filters
+
+
+
+
+
+
+
+
+
+
+
+
Map preview — wire geocoded pins when backend exposes coordinates
+
+
+
+ Catalog
+ {error && {error}
}
+
+ {filtered.map((b) => {
+ const img =
+ b.images.find((i) => i.is_primary)?.image_url ?? b.images[0]?.image_url;
+ return (
+
+
+
+
{b.title}
+
+ {b.category.name}
+ ·
+ {b.location}
+ ·
+ {b.vendor_business_name}
+
+
${b.price_per_day} / day
+
+ View listing
+
+
+
+ );
+ })}
+
+ {!loading && !error && filtered.length === 0 && (
+ No listings match — try clearing filters.
+ )}
+
+
+
+
+ );
+}
diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx
new file mode 100644
index 0000000..e39734d
--- /dev/null
+++ b/src/pages/DashboardPage.tsx
@@ -0,0 +1,204 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { Booking, BookingStatus, MarketingSummary } from "../api/types";
+
+function rangeUtc(days: number): { from: string; to: string } {
+ const to = new Date();
+ const from = new Date(to);
+ from.setUTCDate(from.getUTCDate() - days);
+ return { from: from.toISOString(), to: to.toISOString() };
+}
+
+function inRange(iso: string, fromIso: string, toIso: string): boolean {
+ const t = new Date(iso).getTime();
+ return t >= new Date(fromIso).getTime() && t < new Date(toIso).getTime();
+}
+
+function badgeClass(status: BookingStatus): string {
+ return `admin-badge admin-badge--${status}`;
+}
+
+function formatWhen(iso: string): string {
+ return new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" });
+}
+
+function listingLabel(b: Booking): string {
+ if (b.equipment_public_id) return b.equipment_public_id;
+ if (b.adventure_public_id) return b.adventure_public_id;
+ return "—";
+}
+
+export function DashboardPage() {
+ const { from, to } = useMemo(() => rangeUtc(30), []);
+ const [summary, setSummary] = useState(null);
+ const [bookings, setBookings] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const [sum, bookList] = await Promise.all([
+ api.fetchVendorMarketingSummary(from, to),
+ api.listBookings(),
+ ]);
+ if (!cancelled) {
+ setSummary(sum);
+ setBookings(bookList);
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [from, to]);
+
+ const windowBookings = useMemo(
+ () => bookings.filter((b) => inRange(b.created_at, from, to)),
+ [bookings, from, to],
+ );
+
+ const revenue30 = useMemo(() => {
+ return windowBookings.reduce((acc, b) => acc + Number.parseFloat(b.total_price || "0"), 0);
+ }, [windowBookings]);
+
+ const pending = useMemo(() => bookings.filter((b) => b.status === "requested").length, [bookings]);
+
+ const recent = useMemo(() => {
+ return [...bookings]
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+ .slice(0, 8);
+ }, [bookings]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ const clicksTotal = summary?.clicks.total ?? 0;
+ const bookingsTotal = summary?.bookings_attributed.total_all ?? windowBookings.length;
+
+ return (
+
+
+
+
Dashboard
+
Live data from WaterTrekk (bookings + marketing, last 30 days UTC)
+
+
+ Full analytics
+
+
+
+
+
+ Revenue (30d)
+
+ {revenue30.toLocaleString(undefined, { style: "currency", currency: "USD" })}
+
+ {windowBookings.length} bookings in window
+
+
+ Listing clicks (30d)
+ {clicksTotal}
+
+ {summary?.clicks.organic ?? 0} organic · {summary?.clicks.marketing ?? 0} marketing
+
+
+
+ Bookings (30d)
+ {bookingsTotal}
+ {summary?.bookings_attributed.unattributed ?? 0} unattributed
+
+
+ Pending requests
+ {pending}
+ Across all time
+
+
+
+
+
+
+
Recent bookings
+
+ View all
+
+
+
+
+
+
+ ID
+ Guest
+ Listing
+ Starts
+ Status
+
+
+
+ {recent.map((row) => (
+
+ {row.id}
+ {row.customer_email}
+ {listingLabel(row)}
+ {formatWhen(row.starts_at)}
+
+ {row.status}
+
+
+ ))}
+
+
+
+ {recent.length === 0 && No bookings yet.
}
+
+
+
+
+
Attribution snapshot
+
+
+
+
+ Organic bookings: {summary?.bookings_attributed.organic ?? 0}
+
+
+
+ Marketing bookings: {summary?.bookings_attributed.marketing ?? 0}
+
+
+
+ Send marketing_click_id on checkout to reduce unattributed totals.
+
+
+
+ Conversion (organic):{" "}
+ {summary?.conversion_rate_click_to_booking.organic == null
+ ? "—"
+ : `${(summary.conversion_rate_click_to_booking.organic * 100).toFixed(2)}%`}
+
+
+
+
+ );
+}
diff --git a/src/pages/EquipmentBookPage.tsx b/src/pages/EquipmentBookPage.tsx
new file mode 100644
index 0000000..f203c93
--- /dev/null
+++ b/src/pages/EquipmentBookPage.tsx
@@ -0,0 +1,97 @@
+import { useEffect, useState } from "react";
+import { Link, useNavigate, useParams } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import { useAuth } from "../auth/AuthContext";
+import {
+ attributionParamsFromSearch,
+ hasAttributionParams,
+ persistMarketingClickId,
+ readMarketingClickId,
+ stripAttributionFromUrl,
+} from "../marketing/attribution";
+import type { EquipmentItemDetail } from "../api/types";
+
+export function EquipmentBookPage() {
+ const { publicId } = useParams<{ publicId: string }>();
+ const navigate = useNavigate();
+ const { user } = useAuth();
+ const [item, setItem] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!publicId) return;
+ const pathnameAtStart = window.location.pathname;
+ const searchAtStart = window.location.search;
+ const attribution = attributionParamsFromSearch(searchAtStart);
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.getEquipmentItem(
+ publicId,
+ hasAttributionParams(attribution) ? attribution : undefined,
+ );
+ if (!cancelled) {
+ setItem(data);
+ if (hasAttributionParams(attribution)) {
+ persistMarketingClickId("equipment", publicId, data.marketing_click_id);
+ stripAttributionFromUrl(navigate, pathnameAtStart, searchAtStart);
+ }
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [publicId, navigate]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !item) {
+ return (
+
+
{error ?? "Listing not found."}
+
+ Back to boat rentals
+
+
+ );
+ }
+
+ const marketingClickIdForConversion =
+ readMarketingClickId("equipment", item.public_id) ?? item.marketing_click_id;
+
+ return (
+
+
+ ← Back to listing
+
+
Request this rental
+
+ You are signed in as {user?.email} . Confirm details below; a full booking form can submit to
+ the WaterTrekk booking API next.
+
+
+ {item.title}
+
+ {item.vendor_business_name} · {item.location} · ${item.price_per_day} / day
+
+
+ marketing_click_id {marketingClickIdForConversion}{" "}
+ (send this on the booking request for attribution)
+
+
+
+ );
+}
diff --git a/src/pages/EquipmentDetailPage.tsx b/src/pages/EquipmentDetailPage.tsx
new file mode 100644
index 0000000..29c8b07
--- /dev/null
+++ b/src/pages/EquipmentDetailPage.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useState } from "react";
+import { Link, useNavigate, useParams } from "react-router-dom";
+import { useAuth } from "../auth/AuthContext";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import {
+ attributionParamsFromSearch,
+ hasAttributionParams,
+ persistMarketingClickId,
+ stripAttributionFromUrl,
+} from "../marketing/attribution";
+import type { EquipmentItemDetail } from "../api/types";
+
+export function EquipmentDetailPage() {
+ const { publicId } = useParams<{ publicId: string }>();
+ const navigate = useNavigate();
+ const { user, loading: authLoading } = useAuth();
+ const [item, setItem] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!publicId) return;
+ const pathnameAtStart = window.location.pathname;
+ const searchAtStart = window.location.search;
+ const attribution = attributionParamsFromSearch(searchAtStart);
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.getEquipmentItem(
+ publicId,
+ hasAttributionParams(attribution) ? attribution : undefined,
+ );
+ if (!cancelled) {
+ setItem(data);
+ persistMarketingClickId("equipment", publicId, data.marketing_click_id);
+ if (hasAttributionParams(attribution)) {
+ stripAttributionFromUrl(navigate, pathnameAtStart, searchAtStart);
+ }
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [publicId, navigate]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !item) {
+ return (
+
+
{error ?? "Listing not found."}
+
+ Back to search
+
+
+ );
+ }
+
+ const img = item.images.find((i) => i.is_primary)?.image_url ?? item.images[0]?.image_url;
+
+ return (
+
+
+ ← Search
+
+
+
+
+
{item.vendor_business_name}
+
{item.title}
+
+ {item.location} · {item.category.name}
+
+
${item.price_per_day} / day
+
{item.description}
+
+ {authLoading ? (
+
Loading…
+ ) : (
+
+ {user ? "Request to book" : "Sign in to book"}
+
+ )}
+ {!authLoading && !user && (
+
+ You can browse without an account. Sign in (or create one) when you are ready to request this rental.
+
+ )}
+
+
+ Use marketing_click_id {item.marketing_click_id} when creating a booking request to attribute
+ this visit ({item.click_traffic_type}).
+
+
+
+
+ );
+}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000..4e30a33
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,448 @@
+import { useCallback, useState } from "react";
+import { Link } from "react-router-dom";
+import { CatalogStrip } from "../components/CatalogStrip";
+
+type TourCard = {
+ title: string;
+ subtitle: string;
+ rating: string;
+ was: string;
+ now: string;
+ image: string;
+};
+
+const asiaTours: TourCard[] = [
+ {
+ title: "Bali & Singapore",
+ subtitle: "Hawaii Oahu",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=600&q=80",
+ },
+ {
+ title: "Hong Kong highlights",
+ subtitle: "City & harbor",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1536599018102-9f803c140fc1?w=600&q=80",
+ },
+ {
+ title: "Hawaii escape",
+ subtitle: "Islands & coast",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1542259683-9e0ba716065e?w=600&q=80",
+ },
+];
+
+const americaTours: TourCard[] = [
+ {
+ title: "Eastern USA",
+ subtitle: "USA, Canada",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=600&q=80",
+ },
+ {
+ title: "Canada escape",
+ subtitle: "USA, Canada",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1517935706615-2717063cc922?w=600&q=80",
+ },
+ {
+ title: "Canadian Rockies",
+ subtitle: "USA, Canada",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1503614472-8c93d56e92ce?w=600&q=80",
+ },
+];
+
+const europeTours: TourCard[] = [
+ {
+ title: "Europe escape",
+ subtitle: "Prague · Paris · Barcelona",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1467269204594-9661b134dd2b?w=600&q=80",
+ },
+ {
+ title: "Europe dream",
+ subtitle: "Prague · Paris · Barcelona",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=600&q=80",
+ },
+ {
+ title: "Europe Wonder",
+ subtitle: "Prague · Paris · Barcelona",
+ rating: "4.8",
+ was: "$250",
+ now: "$200",
+ image:
+ "https://images.unsplash.com/photo-1516483638261-f4dbaf036963?w=600&q=80",
+ },
+];
+
+const continents = [
+ { name: "European tour", price: "from $5 / km", img: "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=400&q=80" },
+ { name: "American tour", price: "from $5 / km", img: "https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=400&q=80" },
+ { name: "Australian tour", price: "from $5 / km", img: "https://images.unsplash.com/photo-1523482580672-f109ba8cb9be?w=400&q=80" },
+ { name: "Asian tour", price: "from $5 / km", img: "https://images.unsplash.com/photo-1480796927426-f609979314bd?w=400&q=80" },
+];
+
+const themes = [
+ { title: "Adventure", price: "from $5000", img: "https://images.unsplash.com/photo-1504280390367-361c6d9f38f4?w=500&q=80" },
+ { title: "City tour", price: "from $5000", img: "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=500&q=80" },
+ { title: "Historical", price: "from $5000", img: "https://images.unsplash.com/photo-1555993539-1732b0258235?w=500&q=80" },
+ { title: "Beach lover", price: "from $5000", img: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=500&q=80" },
+ { title: "Desert routes", price: "from $5000", img: "https://images.unsplash.com/photo-1509316785289-025f5b846b35?w=500&q=80" },
+];
+
+const testimonials = [
+ {
+ quote:
+ "When you innovate, you make mistakes. It is best to admit them quickly, and get on with improving your other innovations.",
+ name: "Lara Denal",
+ },
+ {
+ quote:
+ "Booking our coastal tour was effortless. Clear pricing, instant confirmation, and a memorable day on the water.",
+ name: "Sam Rivera",
+ },
+ {
+ quote:
+ "WaterTrekk made it easy to compare experiences. We booked a wine tour and a boat day in one checkout.",
+ name: "Alex Chen",
+ },
+];
+
+function TourCarousel({
+ regionLabel,
+ items,
+}: {
+ regionLabel: string;
+ items: TourCard[];
+}) {
+ const [i, setI] = useState(0);
+ const pageSize = 3;
+ const maxStart = Math.max(0, items.length - pageSize);
+ const start = Math.min(i, maxStart);
+ const visible = items.slice(start, start + pageSize);
+
+ const prev = useCallback(() => setI((x) => Math.max(0, x - 1)), []);
+ const next = useCallback(() => setI((x) => Math.min(maxStart, x + 1)), [maxStart]);
+
+ return (
+
+
+
{regionLabel}
+
+
+ Previous
+
+
+ Next
+
+
+
+
+ {visible.map((t) => (
+
+
+
+
{t.title}
+
+ ★ {t.rating}
+ {t.subtitle}
+
+
+ {t.was} {" "}
+ {t.now}
+ −25%
+
+
+
+ ))}
+
+
+ );
+}
+
+function ThemeStrip() {
+ const [i, setI] = useState(0);
+ const visible = 4;
+ const maxStart = Math.max(0, themes.length - visible);
+ const start = Math.min(i, maxStart);
+ const slice = themes.slice(start, start + visible);
+
+ return (
+
+
+ setI((x) => Math.max(0, x - 1))}>
+ Previous
+
+ setI((x) => Math.min(maxStart, x + 1))}
+ >
+ Next
+
+
+
+ {slice.map((th) => (
+
+
+
+
{th.title}
+
{th.price}
+
+
+ ))}
+
+
+ );
+}
+
+function TestimonialCarousel() {
+ const [i, setI] = useState(0);
+ const t = testimonials[i];
+
+ return (
+
+
+ setI((x) => (x - 1 + testimonials.length) % testimonials.length)}
+ >
+ Previous
+
+ setI((x) => (x + 1) % testimonials.length)}
+ >
+ Next
+
+
+
{t.quote}
+
{t.name}
+
+ );
+}
+
+export function HomePage() {
+ return (
+
+
+ WaterTrekk
+ Book your travel
+ tours & boats
+ e.preventDefault()}>
+
+ Search tours and boats
+
+
+
+ Search
+
+
+
+
+ Tours & packages
+ Guided experiences
+
+
+ On the water
+ Boat rentals
+
+
+
+
+
+ Welcome to WaterTrekk search
+
+ Find guided tours, coastal cruises, and boat rentals in one place. Compare
+ dates, read ratings, and book experiences that fit your trip—whether you are
+ planning a weekend on the water or a multi-day adventure.
+
+
+
+
+
+
+
+
+
Boat rentals
+
+ Rent the right boat for your crew
+
+
+ Pontoons, skiffs, sailboats, and more — filter by dock, vessel type, and how many
+ people you are bringing. Open the dedicated boat hub or jump straight to the map
+ search.
+
+
+
+ Boat rentals home
+
+
+ Search on map
+
+
+
+
+
+
+
+
+
+
Amazing holiday packages
+ tours
+
+
+
+
+
+
+
+
+
Explore through continents
+ tours
+
+
+ {continents.map((c) => (
+
+
+
+
+ ))}
+
+
+
+
+
+
Explore through themes
+ best
+
+
+
+
+
+
+ our
+
Our happy customers
+ customer
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/ToursPage.tsx b/src/pages/ToursPage.tsx
new file mode 100644
index 0000000..505553f
--- /dev/null
+++ b/src/pages/ToursPage.tsx
@@ -0,0 +1,74 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { AdventureOffering } from "../api/types";
+
+export function ToursPage() {
+ const [offerings, setOfferings] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.listAdventureOfferings();
+ if (!cancelled) setOfferings(data);
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ return (
+
+
Tours & adventures
+
+ Guided experiences from GET /api/v1/adventrues/offerings/. Open a listing for marketing
+ attribution IDs used at booking time.
+
+ {loading &&
Loading…
}
+ {error &&
{error}
}
+
+ {offerings.map((o) => {
+ const img =
+ o.images.find((i) => i.is_primary)?.image_url ?? o.images[0]?.image_url;
+ return (
+
+
+
+ {o.title}
+ {o.vendor_business_name}
+
+ {o.meeting_point} · {o.duration_minutes} min · up to {o.capacity} guests
+
+
+ ${o.price_per_person} / person
+
+
+ View details
+
+
+
+ );
+ })}
+
+ {!loading && !error && offerings.length === 0 && (
+
No adventures published yet.
+ )}
+
+ );
+}
diff --git a/src/pages/VendorAnalyticsPage.tsx b/src/pages/VendorAnalyticsPage.tsx
new file mode 100644
index 0000000..b89c609
--- /dev/null
+++ b/src/pages/VendorAnalyticsPage.tsx
@@ -0,0 +1,124 @@
+import { useEffect, useMemo, useState } from "react";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { MarketingSummary } from "../api/types";
+
+function rangeUtc(days: number): { from: string; to: string } {
+ const to = new Date();
+ const from = new Date(to);
+ from.setUTCDate(from.getUTCDate() - days);
+ return { from: from.toISOString(), to: to.toISOString() };
+}
+
+export function VendorAnalyticsPage() {
+ const { from, to } = useMemo(() => rangeUtc(30), []);
+ const [summary, setSummary] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.fetchVendorMarketingSummary(from, to);
+ if (!cancelled) setSummary(data);
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [from, to]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error || !summary) {
+ return (
+
+
{error ?? "No data."}
+
+ );
+ }
+
+ const pct = (n: number | null | undefined) =>
+ n == null ? "—" : `${(n * 100).toFixed(2)}%`;
+
+ return (
+
+
+
+
Analytics
+
+ Marketing summary (last 30 days, UTC). From {new Date(summary.from).toLocaleDateString()} to{" "}
+ {new Date(summary.to).toLocaleDateString()}.
+
+
+
+
+
+ Listing clicks
+ {summary.clicks.total}
+
+ {summary.clicks.organic} organic · {summary.clicks.marketing} marketing
+
+
+
+ Bookings (all)
+ {summary.bookings_attributed.total_all}
+ {summary.bookings_attributed.unattributed} unattributed
+
+
+ Conv. (organic clicks → bookings)
+ {pct(summary.conversion_rate_click_to_booking.organic)}
+
+
+ Conv. (marketing clicks → bookings)
+ {pct(summary.conversion_rate_click_to_booking.marketing)}
+
+
+
+
+
Campaigns (top by clicks)
+
+
+
+
+
+ Source
+ Medium
+ Campaign
+ Clicks
+ Bookings
+ Rate
+
+
+
+ {summary.campaigns.map((c, i) => (
+
+ {c.utm_source || "—"}
+ {c.utm_medium || "—"}
+ {c.utm_campaign || "—"}
+ {c.clicks}
+ {c.bookings}
+ {pct(c.conversion_rate)}
+
+ ))}
+
+
+ {summary.campaigns.length === 0 && (
+
No tagged campaigns in this window.
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/VendorBookingsPage.tsx b/src/pages/VendorBookingsPage.tsx
new file mode 100644
index 0000000..198357a
--- /dev/null
+++ b/src/pages/VendorBookingsPage.tsx
@@ -0,0 +1,106 @@
+import { useEffect, useState } from "react";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { Booking, BookingStatus } from "../api/types";
+
+function badgeClass(status: BookingStatus): string {
+ return `admin-badge admin-badge--${status}`;
+}
+
+function formatWhen(iso: string): string {
+ return new Date(iso).toLocaleString(undefined, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+}
+
+function listingLabel(b: Booking): string {
+ if (b.equipment_public_id) return `Equipment · ${b.equipment_public_id}`;
+ if (b.adventure_public_id) return `Adventure · ${b.adventure_public_id}`;
+ return "—";
+}
+
+export function VendorBookingsPage() {
+ const [rows, setRows] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const data = await api.listBookings();
+ if (!cancelled) {
+ setRows(
+ [...data].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()),
+ );
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
Bookings
+
Requests and reservations for your vendor profile
+
+
+
+
+
+
+ ID
+ Guest
+ Listing
+ Window
+ Total
+ Status
+
+
+
+ {rows.map((b) => (
+
+ {b.id}
+ {b.customer_email}
+ {listingLabel(b)}
+
+ {formatWhen(b.starts_at)} – {formatWhen(b.ends_at)}
+
+ ${b.total_price}
+
+ {b.status}
+
+
+ ))}
+
+
+ {rows.length === 0 &&
No bookings yet.
}
+
+
+ );
+}
diff --git a/src/pages/VendorListingsPage.tsx b/src/pages/VendorListingsPage.tsx
new file mode 100644
index 0000000..a669006
--- /dev/null
+++ b/src/pages/VendorListingsPage.tsx
@@ -0,0 +1,116 @@
+import { useCallback, useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { AdventureOffering, EquipmentItem } from "../api/types";
+import { VendorCreateListingPanel } from "../components/VendorCreateListingPanel";
+
+export function VendorListingsPage() {
+ const [equipment, setEquipment] = useState([]);
+ const [adventures, setAdventures] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const refetchListings = useCallback(async () => {
+ try {
+ const [eq, adv] = await Promise.all([api.listVendorEquipment(), api.listVendorAdventures()]);
+ setEquipment(eq);
+ setAdventures(adv);
+ setError(null);
+ } catch (e) {
+ setError(formatApiMessage(e));
+ }
+ }, []);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const [eq, adv] = await Promise.all([api.listVendorEquipment(), api.listVendorAdventures()]);
+ if (!cancelled) {
+ setEquipment(eq);
+ setAdventures(adv);
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
Listings
+
Equipment and adventures on your vendor account
+
+
+
+
+
+
+
Equipment
+
+
+ {equipment.map((item) => (
+
+ {item.title}
+ {item.public_id}
+ ${item.price_per_day}/day
+
+ {item.is_active ? "active" : "inactive"}
+
+
+ Public view
+
+
+ ))}
+
+ {equipment.length === 0 && No equipment items.
}
+
+
+
+
Adventures
+
+
+ {adventures.map((o) => (
+
+ {o.title}
+ {o.public_id}
+ ${o.price_per_person}/person
+
+ {o.is_active ? "active" : "inactive"}
+
+
+ Public view
+
+
+ ))}
+
+ {adventures.length === 0 && No adventure offerings.
}
+
+
+
+ );
+}
diff --git a/src/pages/VendorSettingsPage.tsx b/src/pages/VendorSettingsPage.tsx
new file mode 100644
index 0000000..08d85a9
--- /dev/null
+++ b/src/pages/VendorSettingsPage.tsx
@@ -0,0 +1,148 @@
+import { useEffect, useState } from "react";
+import { formatApiMessage } from "../api/client";
+import * as api from "../api/services";
+import type { VendorProfile } from "../api/types";
+
+export function VendorSettingsPage() {
+ const [profile, setProfile] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [saved, setSaved] = useState(false);
+
+ const [businessName, setBusinessName] = useState("");
+ const [description, setDescription] = useState("");
+ const [contactPhone, setContactPhone] = useState("");
+ const [contactEmail, setContactEmail] = useState("");
+ const [addressLine1, setAddressLine1] = useState("");
+ const [city, setCity] = useState("");
+ const [state, setState] = useState("");
+ const [postalCode, setPostalCode] = useState("");
+ const [country, setCountry] = useState("");
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const p = await api.fetchVendorProfile();
+ if (!cancelled) {
+ setProfile(p);
+ setBusinessName(p.business_name);
+ setDescription(p.description);
+ setContactPhone(p.contact_phone);
+ setContactEmail(p.contact_email);
+ setAddressLine1(p.address_line1);
+ setCity(p.city);
+ setState(p.state);
+ setPostalCode(p.postal_code);
+ setCountry(p.country);
+ }
+ } catch (e) {
+ if (!cancelled) setError(formatApiMessage(e));
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setSaved(false);
+ setSaving(true);
+ try {
+ const p = await api.patchVendorProfile({
+ business_name: businessName,
+ description,
+ contact_phone: contactPhone,
+ contact_email: contactEmail,
+ address_line1: addressLine1,
+ city,
+ state,
+ postal_code: postalCode,
+ country,
+ });
+ setProfile(p);
+ setSaved(true);
+ } catch (err) {
+ setError(formatApiMessage(err));
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/pages/VendorWhyPage.tsx b/src/pages/VendorWhyPage.tsx
new file mode 100644
index 0000000..523209f
--- /dev/null
+++ b/src/pages/VendorWhyPage.tsx
@@ -0,0 +1,58 @@
+import { Link } from "react-router-dom";
+
+const benefits = [
+ {
+ title: "Unified inventory",
+ text: "Publish boat and equipment rentals alongside guided adventures from one vendor profile and storefront URL.",
+ },
+ {
+ title: "Booking workflow",
+ text: "Guests submit requests; you approve or decline. Overlap protection and quoted totals are handled server-side.",
+ },
+ {
+ title: "Marketing attribution",
+ text: "Listing views capture UTM and ad click IDs. When guests book, tie revenue to organic vs paid traffic and campaigns.",
+ },
+ {
+ title: "Payments ready",
+ text: "Mock Stripe-style payment intents let you test checkout flows today and swap in live keys when you are ready.",
+ },
+];
+
+export function VendorWhyPage() {
+ return (
+
+
+ For operators
+ Why list on WaterTrekk
+
+ WaterTrekk powers this marketplace: real availability checks, booking states, and vendor analytics—not just a
+ brochure site.
+
+
+
+ Create vendor account
+
+
+ Vendor sign in
+
+
+
+
+ {benefits.map((b) => (
+
+ {b.title}
+ {b.text}
+
+ ))}
+
+
+ Ready to onboard?
+ Register as a vendor with your business name and contact details. You can refine your public profile anytime.
+
+ Get started
+
+
+
+ );
+}
diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx
new file mode 100644
index 0000000..3b15986
--- /dev/null
+++ b/src/pages/auth/ForgotPasswordPage.tsx
@@ -0,0 +1,70 @@
+import { useState } from "react";
+import { Link } from "react-router-dom";
+import { formatApiMessage } from "../../api/client";
+import * as api from "../../api/services";
+
+export function ForgotPasswordPage() {
+ const [email, setEmail] = useState("");
+ const [done, setDone] = useState(false);
+ const [error, setError] = useState(null);
+ const [submitting, setSubmitting] = useState(false);
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setSubmitting(true);
+ try {
+ await api.requestPasswordReset(email.trim() || undefined);
+ setDone(true);
+ } catch (err) {
+ setError(formatApiMessage(err));
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
WaterTrekk
+
Forgot password
+ {done ? (
+ <>
+
+ If an account exists for that email, you will receive reset instructions when outbound email is enabled
+ on the server.
+
+
+ Back to sign in
+
+ >
+ ) : (
+ <>
+
+ Enter the email you used to register. The API acknowledges the request even when email delivery is not yet
+ configured.
+
+
+
+ Email
+ setEmail(e.target.value)}
+ />
+
+ {error && {error}
}
+
+ {submitting ? "Sending…" : "Request reset"}
+
+
+
+ Back to sign in
+
+ >
+ )}
+
+
+ );
+}
diff --git a/src/pages/auth/RegisterPage.tsx b/src/pages/auth/RegisterPage.tsx
new file mode 100644
index 0000000..7d24377
--- /dev/null
+++ b/src/pages/auth/RegisterPage.tsx
@@ -0,0 +1,215 @@
+import { useState } from "react";
+import { Link, useLocation, useNavigate } from "react-router-dom";
+import { formatApiMessage, setTokens } from "../../api/client";
+import * as api from "../../api/services";
+import { safeReturnPath } from "../../auth/returnPath";
+import type { VendorRegisterRequest } from "../../api/types";
+
+type Role = "customer" | "vendor";
+
+export function RegisterPage() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const fromState = (location.state as { from?: string } | null)?.from;
+ const [role, setRole] = useState("customer");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [phone, setPhone] = useState("");
+
+ const [businessName, setBusinessName] = useState("");
+ const [description, setDescription] = useState("");
+ const [contactPhone, setContactPhone] = useState("");
+ const [contactEmail, setContactEmail] = useState("");
+ const [addressLine1, setAddressLine1] = useState("");
+ const [city, setCity] = useState("");
+ const [state, setState] = useState("");
+ const [postalCode, setPostalCode] = useState("");
+ const [country, setCountry] = useState("");
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setSubmitting(true);
+ try {
+ if (role === "customer") {
+ await api.registerCustomer({
+ email,
+ password,
+ first_name: firstName,
+ last_name: lastName,
+ phone_number: phone,
+ });
+ } else {
+ const body: VendorRegisterRequest = {
+ email,
+ password,
+ first_name: firstName,
+ last_name: lastName,
+ phone_number: phone,
+ business_name: businessName,
+ description,
+ contact_phone: contactPhone || phone,
+ contact_email: contactEmail || email,
+ address_line1: addressLine1,
+ city,
+ state,
+ postal_code: postalCode,
+ country,
+ };
+ await api.registerVendor(body);
+ }
+ const tokens = await api.login({ email, password });
+ setTokens(tokens.access, tokens.refresh);
+ if (role === "vendor") {
+ navigate("/dashboard", { replace: true });
+ } else {
+ navigate(safeReturnPath(fromState) ?? "/", { replace: true });
+ }
+ } catch (err) {
+ setError(formatApiMessage(err));
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/src/pages/auth/SignInPage.tsx b/src/pages/auth/SignInPage.tsx
new file mode 100644
index 0000000..0927adb
--- /dev/null
+++ b/src/pages/auth/SignInPage.tsx
@@ -0,0 +1,95 @@
+import { useState } from "react";
+import { Link, Navigate, useLocation, useNavigate } from "react-router-dom";
+import { formatApiMessage } from "../../api/client";
+import { useAuth } from "../../auth/AuthContext";
+import { safeReturnPath } from "../../auth/returnPath";
+
+export function SignInPage() {
+ const { signIn, user, loading } = useAuth();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const fromState = (location.state as { from?: string } | null)?.from;
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ if (!loading && user?.is_vendor) {
+ return ;
+ }
+
+ if (!loading && user && !user.is_vendor) {
+ return ;
+ }
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setSubmitting(true);
+ try {
+ const me = await signIn(email, password);
+ if (me.is_vendor) {
+ navigate(fromState && fromState.startsWith("/dashboard") ? fromState : "/dashboard", {
+ replace: true,
+ });
+ } else {
+ navigate(safeReturnPath(fromState) ?? "/", { replace: true });
+ }
+ } catch (err) {
+ setError(formatApiMessage(err));
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
WaterTrekk
+
Sign in
+
+ Vendors go to the dashboard after sign-in. Customers use the same account to book rentals and
+ tours.
+
+
+
+ Email
+ setEmail(e.target.value)}
+ required
+ />
+
+
+ Password
+ setPassword(e.target.value)}
+ required
+ />
+
+ {error && {error}
}
+
+ {submitting ? "Signing in…" : "Sign in"}
+
+
+
+ Forgot password?
+ {" · "}
+
+ Create an account
+
+
+
+ Why list as a vendor?
+
+
+
+ );
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..d43868c
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,9 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_API_BASE_URL?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..358ca9b
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..db0becc
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..47be5e7
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ "/api": {
+ target: "http://127.0.0.1:8003",
+ changeOrigin: true,
+ },
+ },
+ },
+});