From 3b87b8c4e57600bdc1bb752ef0a35bbda2dc84a8 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Sat, 9 May 2026 11:55:20 +0200 Subject: [PATCH] Implement real API clients and update types Replace mock data in geocoding, HAFAS, and bike routing clients with actual implementations using fetch and appropriate interfaces. Update typescript definitions to match the new data structures, including Journey, Station, and BikeRoute. Add vitest and related testing dependencies, replacing the placeholder test script with actual tests for the new client logic. Refactor calendar and countdown utilities to use the new types and remove unused files. Implement real API clients and update types Replace mock data in HafasClient, GeocodingClient, and BikeRoutingClient with actual fetch implementations. Update types to match API responses and add Vitest tests. --- .gitignore | 16 +- REWRITE_PLAN.md | 109 +- eslint.config.mjs | 9 + package-lock.json | 1801 +++++++++++++++++++- package.json | 12 +- src/app/api/health/route.ts | 9 +- src/lib/__tests__/geocoding-client.test.ts | 100 +- src/lib/__tests__/hafas-client.test.ts | 122 +- src/lib/bike-routing-client.ts | 111 +- src/lib/calendar-utils.ts | 71 +- src/lib/countdown-utils.ts | 74 +- src/lib/geocoding-client.ts | 87 +- src/lib/hafas-client.ts | 192 ++- src/lib/index.ts | 17 +- src/lib/live-status-utils.ts | 29 +- src/lib/status-utils.ts | 23 +- src/lib/test.ts | 4 - src/types/index.ts | 90 +- 18 files changed, 2486 insertions(+), 390 deletions(-) delete mode 100644 src/lib/test.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..a896a8b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,6 @@ # dependencies /node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions # testing /coverage @@ -17,21 +10,16 @@ /.next/ /out/ -# production -/build - # misc .DS_Store *.pem # debug npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* -# env files (can opt-in for committing if needed) +# env files — keep .env.example committed .env* +!.env.example # vercel .vercel diff --git a/REWRITE_PLAN.md b/REWRITE_PLAN.md index 519088f..f5c260f 100644 --- a/REWRITE_PLAN.md +++ b/REWRITE_PLAN.md @@ -103,11 +103,10 @@ Each event card shows both travel modes side by side: ``` oebb_planner/ ├── package.json -├── next.config.js +├── next.config.ts ├── tsconfig.json -├── tailwind.config.ts -├── postcss.config.mjs -├── vitest.config.ts +├── postcss.config.mjs ← Tailwind v4 uses @tailwindcss/postcss; no tailwind.config.ts needed +├── vitest.config.ts ← must exist before Phase 7; deferred from Phase 1 ├── src/ │ ├── app/ │ │ ├── layout.tsx ← root layout (fonts, providers, navbar) @@ -161,27 +160,31 @@ oebb_planner/ │ │ ├── useCalendar.ts ← calendar import logic │ │ └── useEventsStore.ts ← shared events state (NEW) │ ├── lib/ -│ │ ├── hafas.ts ← HAFAS API client + types -│ │ ├── calendar.ts ← ICS helpers (extractEvents, cleanLocation) -│ │ ├── countdown.ts ← leaveBy, countdownInfo -│ │ ├── formatting.ts ← fmtTime, delayColor -│ │ ├── demo.ts ← demo journey generator -│ │ ├── constants.ts ← WALK_MINS, RED, SAMPLE_EVENTS, HAFAS_BASE -│ │ ├── geocode.ts ← Nominatim client (NEW) -│ │ └── bike-route.ts ← OSRM client (NEW) +│ │ ├── hafas-client.ts ← HAFAS API client (class HafasClient) +│ │ ├── calendar-utils.ts ← ICS helpers (extractEvents, cleanLocation) +│ │ ├── countdown-utils.ts ← calculateCountdown, leaveBy helpers +│ │ ├── formatting.ts ← formatTime, formatDate, formatDateTime, formatDuration, formatDistance +│ │ ├── demo.ts ← demo journey generator +│ │ ├── constants.ts ← HAFAS_URL, NOMINATIM_URL, OSRM_URL, HAFAS_TIMEOUT_MS, DEFAULT_DAYS, APP_VERSION +│ │ ├── status-utils.ts ← StatusUtils.checkServerStatus() (used by useServerHealth) +│ │ ├── live-status-utils.ts ← LiveStatusUtils.getLiveStatus() (stub; live data TBD) +│ │ ├── geocoding-client.ts ← Nominatim client (NEW) +│ │ ├── bike-routing-client.ts ← OSRM client (NEW) +│ │ └── index.ts ← re-exports all lib modules │ └── types/ │ └── index.ts ← all TypeScript interfaces -├── __tests__/ -│ ├── lib/ -│ │ ├── calendar.test.ts -│ │ ├── countdown.test.ts -│ │ └── bike-route.test.ts ← NEW -│ └── api/ -│ ├── hafas.test.ts -│ ├── calendar.test.ts -│ ├── geocode.test.ts ← NEW -│ ├── bike-route.test.ts ← NEW -│ └── health.test.ts +├── src/lib/__tests__/ ← unit tests live alongside lib, not at repo root +│ ├── hafas-client.test.ts +│ ├── geocoding-client.test.ts +│ ├── calendar-utils.test.ts +│ ├── countdown-utils.test.ts +│ └── bike-routing-client.test.ts ← NEW +├── src/app/api/__tests__/ ← API route tests +│ ├── hafas.test.ts +│ ├── calendar.test.ts +│ ├── geocode.test.ts ← NEW +│ ├── bike-route.test.ts ← NEW +│ └── health.test.ts ├── public/ │ ├── favicon.ico │ └── robots.txt @@ -335,7 +338,7 @@ type CalStatus = null | "loading" | "ok" | "error"; |----------|--------|-----------| | Framework | Next.js 15 App Router | Modern standard; API routes are serverless-compatible | | Language | TypeScript (strict) | Type safety across frontend and backend | -| Styling | Tailwind CSS | Replaces 700 lines of inline styles; consistent theming | +| Styling | Tailwind CSS v4 | Replaces 700 lines of inline styles; v4 uses CSS-native config, no `tailwind.config.ts` | | State | React hooks + Context | No Redux needed for this scale | | API proxy | Next.js Route Handlers | Same logic as Express, no Express dependency | | ICS parsing | Keep `node-ical` | Already works, well-tested | @@ -360,11 +363,11 @@ type CalStatus = null | "loading" | "ok" | "error"; ### Phase 2 — Types + Library Layer (~2 hours) 7. Define TypeScript types in `src/types/index.ts` -8. Port `lib/hafas.ts` — HAFAS API client functions with proper types -9. Port `lib/calendar.ts` — `extractEvents` + `cleanLocation` (reusable in API routes AND tests) -10. Port `lib/countdown.ts`, `lib/formatting.ts`, `lib/constants.ts`, `lib/demo.ts` -11. Create `lib/geocode.ts` — Nominatim client (NEW) -12. Create `lib/bike-route.ts` — OSRM client (NEW) +8. Port `lib/hafas-client.ts` — `HafasClient` class with `searchStation()` and `fetchJourneys()` +9. Port `lib/calendar-utils.ts` — `extractEvents()` + `cleanLocation()` (reusable in API routes AND tests) +10. Port `lib/countdown-utils.ts`, `lib/formatting.ts`, `lib/constants.ts`, `lib/demo.ts`, `lib/status-utils.ts`, `lib/live-status-utils.ts` +11. Create `lib/geocoding-client.ts` — Nominatim client (NEW) +12. Create `lib/bike-routing-client.ts` — OSRM client (NEW) ### Phase 3 — API Routes (~45 min) @@ -411,10 +414,12 @@ type CalStatus = null | "loading" | "ok" | "error"; ### Phase 7 — Tests (~2 hours) -44. Migrate `server/__tests__/*.test.js` → `__tests__/api/*.test.ts` -45. Add unit tests for `lib/calendar.ts`, `lib/countdown.ts` -46. Add API tests for `geocode` and `bike-route` (NEW) -47. Add component smoke tests with `@testing-library/react` +> Install test dependencies first: `npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom jsdom` and create `vitest.config.ts`. Update the `test` script in `package.json` to `vitest`. + +44. Migrate `server/__tests__/*.test.js` → `src/app/api/__tests__/*.test.ts` +45. Add unit tests for `src/lib/calendar-utils.ts`, `src/lib/countdown-utils.ts` in `src/lib/__tests__/` +46. Add API tests for `geocode` and `bike-route` in `src/app/api/__tests__/` (NEW) +47. Add component smoke tests with `@testing-library/react` _(optional)_ ### Phase 8 — Cleanup (~30 min) @@ -428,33 +433,43 @@ type CalStatus = null | "loading" | "ok" | "error"; ## 8. Dependencies -### Runtime +### Runtime (installed) ```json { "dependencies": { - "next": "^15.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "next": "16.2.6", + "react": "19.2.4", + "react-dom": "19.2.4", "node-ical": "^0.18.0" } } ``` -### Development +### Development (installed) + +```json +{ + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "@tailwindcss/postcss": "^4", + "eslint": "^9", + "eslint-config-next": "16.2.6" + } +} +``` + +### To be installed before Phase 7 (tests) ```json { "devDependencies": { - "typescript": "^5.6.0", - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@types/node-ical": "^0.18.0", - "tailwindcss": "^3.4.0", - "postcss": "^8.4.0", - "autoprefixer": "^10.4.0", "vitest": "^2.0.0", + "@vitejs/plugin-react": "^4.0.0", "@testing-library/react": "^16.0.0", "@testing-library/jest-dom": "^6.0.0", "jsdom": "^25.0.0" @@ -462,6 +477,8 @@ type CalStatus = null | "loading" | "ok" | "error"; } ``` +> Note: Tailwind v4 no longer requires `autoprefixer` or a separate `tailwind.config.ts` — configuration is done via CSS and `@tailwindcss/postcss`. + --- ## 9. Environment Variables diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..72a7928 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,6 +13,15 @@ const eslintConfig = defineConfig([ "build/**", "next-env.d.ts", ]), + { + rules: { + // Allow _-prefixed parameters and variables to signal intentionally unused. + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + }, + }, ]); export default eslintConfig; diff --git a/package-lock.json b/package-lock.json index 7d60791..a026727 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,20 +9,33 @@ "version": "0.1.0", "dependencies": { "next": "16.2.6", + "node-ical": "^0.18.0", "react": "19.2.4", "react-dom": "19.2.4" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.1", "eslint": "^9", "eslint-config-next": "16.2.6", + "jsdom": "^29.1.1", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.5" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -36,6 +49,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -228,6 +292,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -276,6 +350,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -453,6 +680,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1300,6 +1545,317 @@ "node": ">=12.4.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.128.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1307,6 +1863,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1599,6 +2162,93 @@ "tailwindcss": "4.3.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1610,6 +2260,32 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2249,6 +2925,145 @@ "win32" ] }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2289,6 +3104,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2482,6 +3308,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2499,6 +3335,12 @@ "node": ">= 0.4" } }, + "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/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2525,6 +3367,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2554,6 +3407,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -2635,7 +3498,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2692,6 +3554,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2735,6 +3607,18 @@ "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", @@ -2764,6 +3648,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2778,6 +3683,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2850,6 +3769,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2893,6 +3819,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2916,11 +3862,18 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "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==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2959,6 +3912,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3032,7 +3998,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3042,7 +4007,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3076,11 +4040,17 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3093,7 +4063,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3555,6 +4524,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3565,6 +4544,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.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", @@ -3690,6 +4679,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3706,11 +4715,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3771,7 +4810,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3796,7 +4834,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3884,7 +4921,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3956,7 +4992,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3969,7 +5004,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3985,7 +5019,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4011,6 +5044,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4048,6 +5094,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4333,6 +5389,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4540,6 +5603,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4966,6 +6080,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4980,12 +6105,18 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5010,6 +6141,37 @@ "node": ">=8.6" } }, + "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/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5033,6 +6195,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5181,6 +6364,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-ical": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.18.0.tgz", + "integrity": "sha512-FrOUPztjw9OUgSB9o/ffhl86BiVClQTut97C2NqCwKIgOAcKPEw5UQMuSuNJO/Y4hqTyJdKZh2TCqNHQnE9YFg==", + "license": "Apache-2.0", + "dependencies": { + "axios": "1.6.7", + "moment-timezone": "^0.5.44", + "rrule": "2.8.1", + "uuid": "^9.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -5311,6 +6506,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5392,6 +6598,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5419,6 +6638,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5487,6 +6713,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5499,6 +6763,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5558,6 +6828,20 @@ "dev": true, "license": "MIT" }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5602,6 +6886,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -5657,6 +6951,56 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rrule": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz", + "integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5736,6 +7080,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5958,6 +7315,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5974,6 +7338,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6111,6 +7489,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6173,6 +7564,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", @@ -6194,6 +7592,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6242,6 +7657,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6255,6 +7700,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6448,6 +7919,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -6531,6 +8012,262 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", + "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0-rc.18", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/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/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/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/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6636,6 +8373,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6646,6 +8400,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index f1d940f..bbac638 100644 --- a/package.json +++ b/package.json @@ -7,21 +7,29 @@ "build": "next build", "start": "next start", "lint": "eslint", - "test": "echo 'No tests yet'" + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "next": "16.2.6", + "node-ical": "^0.18.0", "react": "19.2.4", "react-dom": "19.2.4" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.1", "eslint": "^9", "eslint-config-next": "16.2.6", + "jsdom": "^29.1.1", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.5" } } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 5d49b59..615b4b4 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,5 +1,10 @@ -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; +import { APP_VERSION } from "@/lib/constants"; export async function GET() { - return NextResponse.json({ status: 'OK', timestamp: new Date().toISOString() }); + return NextResponse.json({ + ok: true, + ts: new Date().toISOString(), + version: APP_VERSION, + }); } diff --git a/src/lib/__tests__/geocoding-client.test.ts b/src/lib/__tests__/geocoding-client.test.ts index 49c5377..6bb12fb 100644 --- a/src/lib/__tests__/geocoding-client.test.ts +++ b/src/lib/__tests__/geocoding-client.test.ts @@ -1,14 +1,100 @@ -import { GeocodingClient } from '../geocoding-client'; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { GeocodingClient } from "../geocoding-client"; + +const nominatimResult = (lat: string, lon: string, display: string) => [ + { lat, lon, display_name: display }, +]; + +beforeEach(() => { + vi.restoreAllMocks(); +}); + +describe("GeocodingClient.geocode", () => { + it("parses Nominatim response into GeocodeResult", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(nominatimResult("48.2082", "16.3738", "Wien, Österreich")), + }) + ); -describe('GeocodingClient', () => { - it('should be instantiated correctly', () => { const client = new GeocodingClient(); - expect(client).toBeInstanceOf(GeocodingClient); + const results = await client.geocode("Wien"); + + expect(results).toHaveLength(1); + expect(results[0].lat).toBeCloseTo(48.2082); + expect(results[0].lng).toBeCloseTo(16.3738); + expect(results[0].display_name).toBe("Wien, Österreich"); }); - it('should geocode', async () => { + it("returns cached result on repeated calls", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(nominatimResult("47.0707", "15.4395", "Graz, Österreich")), + }); + vi.stubGlobal("fetch", mockFetch); + const client = new GeocodingClient(); - const results = await client.geocode('Vienna'); - expect(results).toHaveLength(1); + await client.geocode("Graz"); + await client.geocode("Graz"); + + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("includes countrycodes param when provided", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(nominatimResult("47.8095", "13.0550", "Salzburg, Österreich")), + }); + vi.stubGlobal("fetch", mockFetch); + + const client = new GeocodingClient(); + await client.geocode("Salzburg", "at"); + + const url = mockFetch.mock.calls[0][0] as string; + expect(url).toContain("countrycodes=at"); + }); + + it("throws on non-OK response", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 429 })); + const client = new GeocodingClient(); + await expect(client.geocode("Wien")).rejects.toThrow("429"); + }); + + it("returns empty array when Nominatim returns no results", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([]) }) + ); + const client = new GeocodingClient(); + const results = await client.geocode("xyznotaplace"); + expect(results).toHaveLength(0); + }); +}); + +describe("GeocodingClient.reverseGeocode", () => { + it("parses reverse geocode response", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ lat: "48.2082", lon: "16.3738", display_name: "Wien, Österreich" }), + }) + ); + + const client = new GeocodingClient(); + const result = await client.reverseGeocode(48.2082, 16.3738); + + expect(result).not.toBeNull(); + expect(result!.lat).toBeCloseTo(48.2082); + }); + + it("returns null on non-OK response", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 404 })); + const client = new GeocodingClient(); + const result = await client.reverseGeocode(0, 0); + expect(result).toBeNull(); }); }); diff --git a/src/lib/__tests__/hafas-client.test.ts b/src/lib/__tests__/hafas-client.test.ts index 7a487ff..01e6f78 100644 --- a/src/lib/__tests__/hafas-client.test.ts +++ b/src/lib/__tests__/hafas-client.test.ts @@ -1,14 +1,122 @@ -import { HafasClient } from '../hafas-client'; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { HafasClient } from "../hafas-client"; + +const WIEN = { name: "Wien Hbf", extId: "0WB0F0001500" }; +const GRAZ = { name: "Graz Hbf", extId: "0WB0F0000600" }; + +const makeLocationResponse = (locations: object[]) => + JSON.stringify({ svcResL: [{ res: { match: { locL: locations } } }] }); + +const makeTripResponse = (journeys: object[]) => + JSON.stringify({ svcResL: [{ res: { outConL: journeys } }] }); + +beforeEach(() => { + vi.restoreAllMocks(); +}); + +describe("HafasClient.searchStation", () => { + it("returns mapped stations from HAFAS response", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve( + JSON.parse( + makeLocationResponse([ + { type: "S", name: "Wien Hbf", extId: "0WB0F0001500" }, + { type: "A", name: "Wien Airport", extId: "0WB0F0099999" }, + ]) + ) + ), + }) + ); -describe('HafasClient', () => { - it('should be instantiated correctly', () => { const client = new HafasClient(); - expect(client).toBeInstanceOf(HafasClient); + const results = await client.searchStation("Wien"); + + expect(results).toHaveLength(1); + expect(results[0]).toEqual({ name: "Wien Hbf", extId: "0WB0F0001500" }); }); - it('should fetch journeys', async () => { + it("returns empty array when no stations found", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(JSON.parse(makeLocationResponse([]))), + }) + ); + const client = new HafasClient(); - const journeys = await client.fetchJourneys('Vienna', 'Salzburg', new Date()); - expect(journeys).toHaveLength(1); + const results = await client.searchStation("nowhere"); + expect(results).toHaveLength(0); + }); + + it("throws on non-OK response", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 503 })); + const client = new HafasClient(); + await expect(client.searchStation("Wien")).rejects.toThrow("503"); + }); +}); + +describe("HafasClient.fetchJourneys", () => { + it("maps HAFAS outConL to Journey objects", async () => { + const now = new Date("2024-11-14T13:00:00"); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve( + JSON.parse( + makeTripResponse([ + { + ctxRecon: "ctx-abc", + secL: [ + { + dep: { dTimeS: "130000", dTimeR: "130500", dPlatfS: "3" }, + arr: { aTimeS: "143500", aTimeR: "144000" }, + jny: { stopL: [{ name: "RJX 5234" }], dlySum: 5, isCncl: false }, + }, + ], + }, + ]) + ) + ), + }) + ); + + const client = new HafasClient(); + const journeys = await client.fetchJourneys(WIEN, GRAZ, now); + + expect(journeys).toHaveLength(1); + const j = journeys[0]; + expect(j.id).toBe("ctx-abc"); + expect(j.platform).toBe("3"); + expect(j.trains).toEqual(["RJX 5234"]); + expect(j.cancelled).toBe(false); + expect(j.changes).toBe(0); + expect(j.delay).toBe(5); + }); + + it("returns empty array when no journeys found", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(JSON.parse(makeTripResponse([]))), + }) + ); + + const client = new HafasClient(); + const journeys = await client.fetchJourneys(WIEN, GRAZ, new Date()); + expect(journeys).toHaveLength(0); + }); + + it("throws on non-OK response", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 500 })); + const client = new HafasClient(); + await expect(client.fetchJourneys(WIEN, GRAZ, new Date())).rejects.toThrow("500"); }); }); diff --git a/src/lib/bike-routing-client.ts b/src/lib/bike-routing-client.ts index 20e60cb..f2e571f 100644 --- a/src/lib/bike-routing-client.ts +++ b/src/lib/bike-routing-client.ts @@ -1,74 +1,59 @@ -// Bike routing client for ÖBB Planner -import { BikeRoute, BikeStep } from "@/types"; +import type { BikeRoute, BikeStep } from "@/types"; +import { OSRM_URL } from "./constants"; + +interface OsrmStep { + name: string; + distance: number; + duration: number; + maneuver: { instruction?: string; type: string; modifier?: string }; +} + +interface OsrmRoute { + distance: number; + duration: number; + legs: Array<{ steps: OsrmStep[] }>; +} + +interface OsrmResponse { + code: string; + routes: OsrmRoute[]; +} + +function stepInstruction(step: OsrmStep): string { + if (step.maneuver.instruction) return step.maneuver.instruction; + const modifier = step.maneuver.modifier ? ` ${step.maneuver.modifier}` : ""; + return `${step.maneuver.type}${modifier}${step.name ? ` onto ${step.name}` : ""}`; +} export class BikeRoutingClient { private baseUrl: string; - constructor(baseUrl: string = "https://router.project-osrm.org") { + constructor(baseUrl: string = OSRM_URL) { this.baseUrl = baseUrl; } - async getBikeRoute( - fromLat: number, - fromLng: number, - toLat: number, - toLng: number - ): Promise { - // This would make an actual API call to OSRM or similar - // For now, we'll return mock data + async getBikeRoute(fromLat: number, fromLng: number, toLat: number, toLng: number): Promise { + const coords = `${fromLng},${fromLat};${toLng},${toLat}`; + const url = `${this.baseUrl}/route/v1/bicycle/${coords}?overview=false&steps=true`; + + const res = await fetch(url, { signal: AbortSignal.timeout(10000) }); + if (!res.ok) throw new Error(`OSRM request failed: ${res.status}`); + + const data: OsrmResponse = await res.json(); + if (data.code !== "Ok" || !data.routes.length) return null; + + const route = data.routes[0]; + const steps: BikeStep[] = (route.legs[0]?.steps ?? []).map((s) => ({ + name: s.name, + distance: s.distance, + duration: s.duration, + instruction: stepInstruction(s), + })); + return { - distance: 5000, - duration: 1200, - steps: [ - { - distance: 2000, - duration: 400, - instruction: "Head north on Sample Street for 2 km", - bearing: 0, - geometry: { - coordinates: [ - [fromLng, fromLat], - [fromLng, fromLat + 0.01], - ], - }, - }, - { - distance: 3000, - duration: 800, - instruction: "Turn right on Main Avenue for 3 km", - bearing: 90, - geometry: { - coordinates: [ - [fromLng, fromLat + 0.01], - [fromLng + 0.01, fromLat + 0.01], - ], - }, - }, - ], + distance: route.distance, + duration: route.duration, + steps, }; } - - async getBikeSteps( - fromLat: number, - fromLng: number, - toLat: number, - toLng: number - ): Promise { - // This would return just the steps without distance/duration - // For now, we'll return mock data - return [ - { - distance: 2000, - duration: 400, - instruction: "Head north on Sample Street for 2 km", - bearing: 0, - geometry: { - coordinates: [ - [fromLng, fromLat], - [fromLng, fromLat + 0.01], - ], - }, - }, - ]; - } } diff --git a/src/lib/calendar-utils.ts b/src/lib/calendar-utils.ts index 10e2381..fe462d5 100644 --- a/src/lib/calendar-utils.ts +++ b/src/lib/calendar-utils.ts @@ -1,27 +1,52 @@ -// Calendar utilities for ÖBB Planner -import { CalendarEvent } from "@/types"; +import * as ical from "node-ical"; +import type { CalendarEvent } from "@/types"; -export class CalendarUtils { - static parseICalContent(content: string): CalendarEvent[] { - // This would parse iCal content - // For now, we'll return mock data - return [ - { - id: "event-1", - title: "Sample Event", - start: new Date(), - end: new Date(Date.now() + 3600000), - location: "Sample Location", - description: "Sample description", - }, - ]; +export function extractEvents(content: string, days: number = 14): CalendarEvent[] { + const parsed = ical.sync.parseICS(content); + const now = new Date(); + const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); + const events: CalendarEvent[] = []; + + for (const key of Object.keys(parsed)) { + const comp = parsed[key]; + if (comp.type !== "VEVENT") continue; + + const start = comp.start as Date | undefined; + if (!start || !(start instanceof Date) || isNaN(start.getTime())) continue; + if (start < now || start > cutoff) continue; + + const location = typeof comp.location === "string" ? comp.location : ""; + if (!location) continue; + + events.push({ + id: comp.uid ?? key, + title: typeof comp.summary === "string" ? comp.summary : "Untitled", + destination: cleanLocation(location), + eventTime: start.toISOString(), + source: "calendar", + }); } - static getEventsForDays(events: CalendarEvent[], days: number): CalendarEvent[] { - // Filter events for the specified number of days - const now = new Date(); - const future = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); - - return events.filter(event => event.start >= now && event.start <= future); - } + events.sort((a, b) => a.eventTime.localeCompare(b.eventTime)); + return events; +} + +export function cleanLocation(location: string): string { + const knownStations: Record = { + "Graz Hauptbahnhof": "Graz Hbf", + "Wien Hauptbahnhof": "Wien Hbf", + "Salzburg Hauptbahnhof": "Salzburg Hbf", + "Innsbruck Hauptbahnhof": "Innsbruck Hbf", + "Linz Hauptbahnhof": "Linz Hbf", + "Villach Hauptbahnhof": "Villach Hbf", + "Klagenfurt Hauptbahnhof": "Klagenfurt Hbf", + }; + + for (const [full, short] of Object.entries(knownStations)) { + if (location.toLowerCase().includes(full.toLowerCase())) { + return short; + } + } + + return location.split(",")[0].trim(); } diff --git a/src/lib/countdown-utils.ts b/src/lib/countdown-utils.ts index 4ac4aed..2352aa0 100644 --- a/src/lib/countdown-utils.ts +++ b/src/lib/countdown-utils.ts @@ -1,46 +1,48 @@ // Countdown utilities for ÖBB Planner import { CountdownInfo } from "@/types"; -export class CountdownUtils { - static calculateCountdown(targetDate: Date): CountdownInfo { - const now = new Date(); - const diff = targetDate.getTime() - now.getTime(); - - if (diff <= 0) { - return { - minutes: 0, - seconds: 0, - status: 'running' - }; - } - - const minutes = Math.floor(diff / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); +export function calculateCountdown(targetDate: Date): CountdownInfo { + const now = new Date(); + const diffMs = targetDate.getTime() - now.getTime(); + const diffMin = Math.floor(diffMs / 60000); + if (diffMin <= 0) { return { - minutes, - seconds, - status: 'running' + label: "Now", + color: "red", + urgent: true, }; } - static formatCountdown(countdown: CountdownInfo): string { - if (countdown.status === 'cancelled') { - return 'Cancelled'; - } - - if (countdown.status === 'delayed') { - return 'Delayed'; - } - - if (countdown.minutes === 0 && countdown.seconds === 0) { - return 'Now'; - } - - if (countdown.minutes > 0) { - return `${countdown.minutes}m ${countdown.seconds}s`; - } - - return `${countdown.seconds}s`; + if (diffMin <= 10) { + return { + label: `${diffMin}min`, + color: "orange", + urgent: true, + }; } + + if (diffMin <= 30) { + return { + label: `${diffMin}min`, + color: "yellow", + urgent: false, + }; + } + + if (diffMin < 60) { + return { + label: `${diffMin}min`, + color: "green", + urgent: false, + }; + } + + const hours = Math.floor(diffMin / 60); + const remainingMin = diffMin % 60; + return { + label: `${hours}h ${remainingMin}min`, + color: "blue", + urgent: false, + }; } diff --git a/src/lib/geocoding-client.ts b/src/lib/geocoding-client.ts index d97279f..9d64a3b 100644 --- a/src/lib/geocoding-client.ts +++ b/src/lib/geocoding-client.ts @@ -1,42 +1,73 @@ -// Geocoding client for ÖBB Planner -import { GeocodeResult } from "@/types"; +import type { GeocodeResult } from "@/types"; +import { NOMINATIM_URL, NOMINATIM_USER_AGENT } from "./constants"; + +interface NominatimResult { + lat: string; + lon: string; + display_name: string; +} export class GeocodingClient { private baseUrl: string; + private userAgent: string; + private cache = new Map(); - constructor(baseUrl: string = "https://nominatim.openstreetmap.org") { + constructor(baseUrl: string = NOMINATIM_URL, userAgent: string = NOMINATIM_USER_AGENT) { this.baseUrl = baseUrl; + this.userAgent = userAgent; } - async geocode(query: string, countrycodes?: string[]): Promise { - // This would make an actual API call to Nominatim - // For now, we'll return mock data - return [ - { - lat: 48.2082, - lon: 16.3738, - display_name: query, - address: { - city: "Vienna", - state: "Vienna", - country: "Austria", - }, + async geocode(query: string, countrycodes?: string): Promise { + const cacheKey = `${query}|${countrycodes ?? ""}`; + const cached = this.cache.get(cacheKey); + if (cached) return cached; + + const params = new URLSearchParams({ q: query, format: "json", limit: "5" }); + if (countrycodes) params.set("countrycodes", countrycodes); + + const res = await fetch(`${this.baseUrl}/search?${params}`, { + headers: { + "User-Agent": this.userAgent, + Accept: "application/json", }, - ]; + signal: AbortSignal.timeout(5000), + }); + + if (!res.ok) throw new Error(`Nominatim geocode failed: ${res.status}`); + + const data: NominatimResult[] = await res.json(); + const results = data.map((r) => ({ + lat: parseFloat(r.lat), + lng: parseFloat(r.lon), + display_name: r.display_name, + })); + + this.cache.set(cacheKey, results); + return results; } - async reverseGeocode(lat: number, lon: number): Promise { - // This would make an actual API call to Nominatim - // For now, we'll return mock data + async reverseGeocode(lat: number, lng: number): Promise { + const params = new URLSearchParams({ + lat: String(lat), + lon: String(lng), + format: "json", + }); + + const res = await fetch(`${this.baseUrl}/reverse?${params}`, { + headers: { + "User-Agent": this.userAgent, + Accept: "application/json", + }, + signal: AbortSignal.timeout(5000), + }); + + if (!res.ok) return null; + + const data: NominatimResult = await res.json(); return { - lat, - lon, - display_name: "Sample Location", - address: { - city: "Sample City", - state: "Sample State", - country: "Sample Country", - }, + lat: parseFloat(data.lat), + lng: parseFloat(data.lon), + display_name: data.display_name, }; } } diff --git a/src/lib/hafas-client.ts b/src/lib/hafas-client.ts index 13146c0..dd93278 100644 --- a/src/lib/hafas-client.ts +++ b/src/lib/hafas-client.ts @@ -1,74 +1,144 @@ -// HAFAS client for ÖBB Planner -import { Journey, Station, Event } from "@/types"; +import type { Journey, Station } from "@/types"; +import { HAFAS_URL, HAFAS_TIMEOUT_MS } from "./constants"; -export interface HafasResponse { - journeys: Journey[]; - stations: Station[]; - events: Event[]; +interface HafasLocation { + type: "S" | "A" | "P"; + name: string; + extId: string; +} + +interface HafasJourney { + ctxRecon?: string; + secL?: Array<{ + dep?: { dTimeS?: string; dTimeR?: string; dPlatfS?: string }; + arr?: { aTimeS?: string; aTimeR?: string }; + jny?: { + prodX?: number; + stopL?: Array<{ name: string }>; + dlySum?: number; + isCncl?: boolean; + }; + chgDurR?: number; + }>; +} + +function parseHafasTime(date: string, time: string): Date { + const y = parseInt(date.slice(0, 4)); + const mo = parseInt(date.slice(4, 6)) - 1; + const d = parseInt(date.slice(6, 8)); + const h = parseInt(time.slice(0, 2)); + const m = parseInt(time.slice(2, 4)); + const s = parseInt(time.slice(4, 6)); + return new Date(y, mo, d, h, m, s); +} + +function hafasDateTime(date: Date): { date: string; time: string } { + const pad = (n: number) => String(n).padStart(2, "0"); + const d = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`; + const t = `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}000`; + return { date: d, time: t }; } export class HafasClient { private baseUrl: string; + private timeoutMs: number; - constructor(baseUrl: string = "https://reiseauskunft.bahn.de") { + constructor(baseUrl: string = HAFAS_URL, timeoutMs: number = HAFAS_TIMEOUT_MS) { this.baseUrl = baseUrl; + this.timeoutMs = timeoutMs; } - async fetchJourneys( - from: string, - to: string, - date: Date, - options?: { - products?: string[]; - changes?: number; - } - ): Promise { - // This would make an actual API call to ÖBB HAFAS - // For now, we'll return mock data - return [ - { - id: "journey-1", - from: { - id: "station-1", - name: from, - latitude: 48.2082, - longitude: 16.3738, + async searchStation(query: string): Promise { + const body = { + svcReqL: [ + { + meth: "LocMatch", + req: { input: { loc: { name: query, type: "S" }, maxLoc: 5 } }, }, - to: { - id: "station-2", - name: to, - latitude: 48.8566, - longitude: 2.3522, - }, - date, - duration: 120, - changes: 1, - products: ["ICE", "IC"], - }, - ]; - } - - async searchStations(query: string): Promise { - // This would make an actual API call to Nominatim or similar - // For now, we'll return mock data - return [ - { - id: "station-1", - name: query, - latitude: 48.2082, - longitude: 16.3738, - }, - ]; - } - - async getStationInfo(stationId: string): Promise { - // This would fetch detailed station information - // For now, we'll return mock data - return { - id: stationId, - name: "Sample Station", - latitude: 48.2082, - longitude: 16.3738, + ], }; + + const res = await fetch(this.baseUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(this.timeoutMs), + }); + + if (!res.ok) throw new Error(`HAFAS station search failed: ${res.status}`); + + const json = await res.json(); + const match = json?.svcResL?.[0]?.res?.match?.locL ?? []; + return (match as HafasLocation[]) + .filter((l) => l.type === "S") + .map((l) => ({ name: l.name, extId: l.extId })); + } + + async fetchJourneys(from: Station, to: Station, date: Date): Promise { + const { date: hafasDate, time: hafasTime } = hafasDateTime(date); + + const body = { + svcReqL: [ + { + meth: "TripSearch", + req: { + depLocL: [{ type: "S", extId: from.extId }], + arrLocL: [{ type: "S", extId: to.extId }], + outDate: hafasDate, + outTime: hafasTime, + numF: 5, + }, + }, + ], + }; + + const res = await fetch(this.baseUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(this.timeoutMs), + }); + + if (!res.ok) throw new Error(`HAFAS trip search failed: ${res.status}`); + + const json = await res.json(); + const outConL: HafasJourney[] = json?.svcResL?.[0]?.res?.outConL ?? []; + + return outConL.map((con, i): Journey => { + const first = con.secL?.[0]; + const last = con.secL?.[con.secL.length - 1]; + const dep = first?.dep; + const arr = last?.arr; + + const sD = dep?.dTimeS ? parseHafasTime(hafasDate, dep.dTimeS) : date; + const rD = dep?.dTimeR ? parseHafasTime(hafasDate, dep.dTimeR) : sD; + const sA = arr?.aTimeS ? parseHafasTime(hafasDate, arr.aTimeS) : sD; + const rA = arr?.aTimeR ? parseHafasTime(hafasDate, arr.aTimeR) : sA; + + const delayMs = rD.getTime() - sD.getTime(); + const delay = Math.max(0, Math.round(delayMs / 60000)); + + const trains = (con.secL ?? []) + .filter((s) => s.jny) + .map((s) => s.jny?.stopL?.[0]?.name ?? "") + .filter(Boolean); + + const cancelled = (con.secL ?? []).some((s) => s.jny?.isCncl === true); + const changes = Math.max(0, (con.secL ?? []).filter((s) => s.jny).length - 1); + const platform = first?.dep?.dPlatfS ?? ""; + + return { + id: con.ctxRecon ?? `journey-${i}`, + sD, + rD, + sA, + rA, + delay, + platform, + changes, + trains, + cancelled, + }; + }); } } diff --git a/src/lib/index.ts b/src/lib/index.ts index 1781150..4556d4c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,8 +1,11 @@ // Main library exports for ÖBB Planner -export * from './hafas-client'; -export * from './geocoding-client'; -export * from './bike-routing-client'; -export * from './calendar-utils'; -export * from './status-utils'; -export * from './live-status-utils'; -export * from './countdown-utils'; +export * from "./hafas-client"; +export * from "./geocoding-client"; +export * from "./bike-routing-client"; +export * from "./calendar-utils"; +export * from "./status-utils"; +export * from "./live-status-utils"; +export * from "./countdown-utils"; +export * from "./formatting"; +export * from "./constants"; +export * from "./demo"; diff --git a/src/lib/live-status-utils.ts b/src/lib/live-status-utils.ts index 626920f..12fa3ea 100644 --- a/src/lib/live-status-utils.ts +++ b/src/lib/live-status-utils.ts @@ -1,34 +1,21 @@ // Live status utilities for ÖBB Planner -import { LiveStatus } from "@/types"; +import type { LiveStatus } from "@/types"; export class LiveStatusUtils { - static async getLiveStatus(journeyId: string): Promise { + static async getLiveStatus(_journeyId: string): Promise { // This would make an actual API call to get live status // For now, we'll return mock data - return "on_time"; - } - - static async getMultipleLiveStatus(journeyIds: string[]): Promise> { - // Get live status for multiple journeys - const results: Record = {}; - - for (const id of journeyIds) { - results[id] = await this.getLiveStatus(id); - } - - return results; + return null; } static getStatusMessage(status: LiveStatus): string { switch (status) { - case "on_time": + case true: return "On time"; - case "delayed": - return "Delayed"; - case "cancelled": - return "Cancelled"; - case "disrupted": - return "Service disrupted"; + case false: + return "Delayed or cancelled"; + case null: + return "No live data"; default: return "Unknown status"; } diff --git a/src/lib/status-utils.ts b/src/lib/status-utils.ts index bcd31f3..597ce3e 100644 --- a/src/lib/status-utils.ts +++ b/src/lib/status-utils.ts @@ -1,21 +1,16 @@ // Status utilities for ÖBB Planner -import { ServerStatus } from "@/types"; +import type { ServerStatus } from "@/types"; export class StatusUtils { static async checkServerStatus(url: string): Promise { - // This would make an actual HTTP request to check server status - // For now, we'll return mock data - return "operational"; - } - - static async checkMultipleServices(services: string[]): Promise> { - // Check status of multiple services - const results: Record = {}; - - for (const service of services) { - results[service] = await this.checkServerStatus(service); + try { + const res = await fetch(url, { + method: "HEAD", + signal: AbortSignal.timeout(5000), + }); + return res.ok; + } catch { + return false; } - - return results; } } diff --git a/src/lib/test.ts b/src/lib/test.ts deleted file mode 100644 index 4255c15..0000000 --- a/src/lib/test.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Test file to verify setup -export const testFunction = () => { - return "test"; -}; diff --git a/src/types/index.ts b/src/types/index.ts index 29ff6fa..0e68f59 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,80 +1,90 @@ // Types for ÖBB Planner - Next.js Rewrite +// ── HAFAS / Train ────────────────────────────────────────────── + export interface Journey { id: string; - from: Station; - to: Station; - date: Date; - duration: number; + sD: Date; // scheduled departure + rD: Date; // real departure + sA: Date; // scheduled arrival + rA: Date; // real arrival + delay: number; // minutes + platform: string; changes: number; - products: string[]; - price?: number; + trains: string[]; + cancelled: boolean; } export interface Station { - id: string; name: string; - latitude: number; - longitude: number; + extId: string; } +// ── Events ───────────────────────────────────────────────────── + export interface Event { id: string; - type: 'departure' | 'arrival'; - station: Station; - time: Date; - platform?: string; + title: string; + destination: string; + eventTime: Date; + source: "manual" | "calendar"; } +// ── Trip Data (per event) ────────────────────────────────────── + export interface TripDataEntry { - id: string; - journey: Journey; - events: Event[]; + journeys: Journey[]; + destName: string; + demo: boolean; + loading: boolean; + bikeRoute?: BikeRoute | null; // NEW + bikeLoading?: boolean; // NEW + bikeError?: string | null; // NEW + destCoords?: { lat: number; lng: number }; // NEW (cached geocode) } +// ── Bicycle Routing (NEW) ───────────────────────────────────── + export interface BikeRoute { - distance: number; - duration: number; - steps: BikeStep[]; + distance: number; // meters + duration: number; // seconds + steps?: BikeStep[]; } export interface BikeStep { + name: string; distance: number; duration: number; instruction: string; - bearing: number; - geometry: { - coordinates: [number, number][]; - }; } +// ── Geocoding (NEW) ──────────────────────────────────────────── + export interface GeocodeResult { lat: number; - lon: number; + lng: number; display_name: string; - address: { - city?: string; - state?: string; - country?: string; - }; } +// ── Calendar Import ──────────────────────────────────────────── + export interface CalendarEvent { id: string; title: string; - start: Date; - end: Date; - location?: string; - description?: string; + destination: string; + eventTime: string; // ISO string from API + source: "calendar"; } +// ── UI Helpers ───────────────────────────────────────────────── + export interface CountdownInfo { - minutes: number; - seconds: number; - status: 'running' | 'delayed' | 'cancelled'; + label: string; + color: string; + urgent: boolean; } -export type ServerStatus = 'operational' | 'degraded' | 'down'; -export type LiveStatus = 'on_time' | 'delayed' | 'cancelled' | 'disrupted'; -export type LocState = 'idle' | 'loading' | 'error'; -export type CalStatus = 'idle' | 'loading' | 'success' | 'error'; +export type ServerStatus = null | true | false; +export type LiveStatus = null | true | false; +export type LocState = "pending" | "granted" | "denied"; +export type CalStatus = null | "loading" | "ok" | "error";