From 0caa9d599fb3b65e55361b8ee512d39b0760ff54 Mon Sep 17 00:00:00 2001 From: tordtst Date: Tue, 29 Sep 2026 16:37:37 +0200 Subject: [PATCH 01/58] build(config): sett opp React, TypeScript og Vite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gi gruppen et kjørbart minimum med streng typekontroll og en enkel plassholder. TypeScript låses til 6.0.x fordi typescript-eslint ennå ikke støtter versjon 7. Refs #1 Co-Authored-By: sebasti --- .gitignore | 21 ++ .nvmrc | 1 + index.html | 16 + package-lock.json | 908 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 27 ++ src/App.tsx | 13 + src/main.tsx | 16 + tsconfig.app.json | 25 ++ tsconfig.json | 7 + tsconfig.node.json | 22 ++ vite.config.ts | 6 + 11 files changed, 1062 insertions(+) create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/main.tsx create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efd2889 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Avhengigheter +node_modules + +# Byggresultat +dist +*.tsbuildinfo + +# Logger +npm-debug.log* +*.log + +# Lokale miljøvariabler +.env +.env.*.local +.env.local + +# Editor og OS +.DS_Store +.idea +.vscode/* +!.vscode/extensions.json diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/index.html b/index.html new file mode 100644 index 0000000..f037032 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + Verktøyutlån Trondheim + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..23f1cf8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,908 @@ +{ + "name": "t36-verktoyutlan", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "t36-verktoyutlan", + "version": "0.0.0", + "dependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0" + }, + "devDependencies": { + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "typescript": "~6.0.0", + "vite": "^8.3.1" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=10" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e6c505 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "t36-verktoyutlan", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=22.12.0", + "npm": ">=10" + }, + "scripts": { + "dev": "vite", + "build": "npm run typecheck && vite build", + "preview": "vite preview", + "typecheck": "tsc -b" + }, + "dependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0" + }, + "devDependencies": { + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "typescript": "~6.0.0", + "vite": "^8.3.1" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..694665a --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,13 @@ +function App() { + return ( +
+

Verktøyutlån Trondheim

+

+ Her kommer en katalog der studenter og privatpersoner kan leie og låne + verktøy av hverandre. Siden er under utvikling. +

+
+ ); +} + +export default App; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..19c52f5 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; + +const rootElement = document.getElementById('root'); + +// Gir en tydelig feil i stedet for å skjule en manglende rot med en typepåstand. +if (!rootElement) { + throw new Error('Fant ikke #root i index.html'); +} + +createRoot(rootElement).render( + + + , +); diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..e045ce8 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..bc339db --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0466183 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +}); From 55a868fc3c1bf71b67438a1952d01af67c3655ec Mon Sep 17 00:00:00 2001 From: tordtst Date: Tue, 29 Sep 2026 16:37:37 +0200 Subject: [PATCH 02/58] docs(docs): dokumenter krav og oppstart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beskriv Node- og npm-versjon, oppstart og kommandoer slik at alle i gruppen og medstudenter kan kjøre prosjektet likt. Refs #1 Co-Authored-By: andretg --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 84239fe..e8ea7dd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,38 @@ Repository for team IT2810-H26-T36 in IT2810-H26. +Webapplikasjon for leie og gratis utlån av verktøy mellom studenter og +privatpersoner i Trondheim. Bygget med React, TypeScript og Vite. + +## Krav + +- Node.js 24 (LTS). Versjonen står i `.nvmrc`, så med nvm holder det å kjøre + `nvm use`. +- npm 10 eller nyere (følger med Node). + +`engines` i `package.json` angir minstekrav (Node 22.12 og npm 10), men npm +stopper ikke installasjonen hvis versjonen er feil. Sjekk selv med +`node -v` og `npm -v`. + +TypeScript er låst til 6.0.x fordi `typescript-eslint` foreløpig ikke støtter +TypeScript 7. + +## Oppstart + +```bash +npm ci # installer avhengigheter nøyaktig etter package-lock.json +npm run dev # start utviklingsserver på http://localhost:5173 +``` + +## Kommandoer + +| Kommando | Hva den gjør | +| ------------------- | ---------------------------------------------- | +| `npm run dev` | Starter utviklingsserver | +| `npm run build` | Kjører typekontroll og bygger til `dist/` | +| `npm run preview` | Viser det bygde resultatet lokalt | +| `npm run typecheck` | Kjører TypeScript med streng typekontroll | + ## Working in this repository `main` is protected — you cannot push to it directly. Work on a branch and From 5dc3f8024eb75ffa82dc75b227e4ba853ea761cd Mon Sep 17 00:00:00 2001 From: tordtst Date: Tue, 29 Sep 2026 17:20:37 +0200 Subject: [PATCH 03/58] build(config): sett opp Prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gi gruppen lik formatering med enkle anførselstegn, semikolon, to mellomrom og trailingComma: all. Byggfiler, avhengigheter og lockfil holdes utenfor formateringen. Refs #2 Co-Authored-By: birkjr --- .prettierignore | 6 ++++++ .prettierrc.json | 6 ++++++ README.md | 16 ++++++++-------- package-lock.json | 17 +++++++++++++++++ package.json | 5 ++++- 5 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..47d7191 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +# Genererte filer og avhengigheter. Prettier leser også .gitignore, +# men listen gjør det tydelig hva som holdes utenfor formateringen. +node_modules +dist +coverage +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..7eb5bce --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "tabWidth": 2, + "trailingComma": "all" +} diff --git a/README.md b/README.md index e8ea7dd..47efa37 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,12 @@ npm run dev # start utviklingsserver på http://localhost:5173 ## Kommandoer -| Kommando | Hva den gjør | -| ------------------- | ---------------------------------------------- | -| `npm run dev` | Starter utviklingsserver | -| `npm run build` | Kjører typekontroll og bygger til `dist/` | -| `npm run preview` | Viser det bygde resultatet lokalt | -| `npm run typecheck` | Kjører TypeScript med streng typekontroll | +| Kommando | Hva den gjør | +| ------------------- | ----------------------------------------- | +| `npm run dev` | Starter utviklingsserver | +| `npm run build` | Kjører typekontroll og bygger til `dist/` | +| `npm run preview` | Viser det bygde resultatet lokalt | +| `npm run typecheck` | Kjører TypeScript med streng typekontroll | ## Working in this repository @@ -50,7 +50,7 @@ Before a pull request can be merged: request, and approvals from staff do not count — you review each other's code. 2. **Every review comment must be marked resolved.** Reply to each one, fix it - or explain why not, then click *Resolve conversation*. + or explain why not, then click _Resolve conversation_. 3. **Pushing new commits dismisses the approval**, so if you change something after being approved, ask for another review. This is deliberate: what was - approved is what gets merged. \ No newline at end of file + approved is what gets merged. diff --git a/package-lock.json b/package-lock.json index 23f1cf8..cab8ca3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", + "prettier": "^3.9.9", "typescript": "~6.0.0", "vite": "^8.3.1" }, @@ -724,6 +725,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", diff --git a/package.json b/package.json index 2e6c505..b55d2d3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "dev": "vite", "build": "npm run typecheck && vite build", "preview": "vite preview", - "typecheck": "tsc -b" + "typecheck": "tsc -b", + "format": "prettier . --write", + "format:check": "prettier . --check" }, "dependencies": { "react": "^19.3.0", @@ -21,6 +23,7 @@ "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", + "prettier": "^3.9.9", "typescript": "~6.0.0", "vite": "^8.3.1" } From 669dc178e8e0d6f31822cbc7595d586541b45769 Mon Sep 17 00:00:00 2001 From: tordtst Date: Tue, 29 Sep 2026 17:20:37 +0200 Subject: [PATCH 04/58] build(config): sett opp ESLint for React og TypeScript Oppdag vanlige feil som betingede hooks, any og ubrukte variabler tidlig. eslint-config-prettier ligger sist slik at ESLint ikke overstyrer formateringen fra Prettier. Refs #2 Co-Authored-By: sebasti --- eslint.config.js | 27 + package-lock.json | 2362 ++++++++++++++++++++++++++++++++++++++++----- package.json | 8 + 3 files changed, 2144 insertions(+), 253 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6a2894c --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,27 @@ +import js from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier/flat'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default defineConfig([ + globalIgnores(['dist', 'coverage']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + // Varsler om eksporter som hindrer Vite i å oppdatere komponenter uten full omlasting. + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + }, + }, + // Må ligge sist: slår av regler som ellers ville kollidert med Prettier. + eslintConfigPrettier, +]); diff --git a/package-lock.json b/package-lock.json index cab8ca3..0c85b0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,18 @@ "react-dom": "^19.3.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", + "eslint": "^10.11.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "globals": "^17.12.0", "prettier": "^3.9.9", "typescript": "~6.0.0", + "typescript-eslint": "^8.71.0", "vite": "^8.3.1" }, "engines": { @@ -24,376 +31,1856 @@ "npm": ">=10" } }, - "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", - "cpu": [ - "arm" - ], + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", - "cpu": [ - "s390x" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.0.0" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", - "cpu": [ - "x64" - ], + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", - "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" } }, - "node_modules/@types/react-dom": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", - "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^19.3.0" + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" } }, - "node_modules/@vitejs/plugin-react": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", - "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "^1.0.1" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" }, "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "oxc-transform-react": "^0.145.0", - "vite": "^8.0.0" + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", + "integrity": "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/type-utils": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", + "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.0.tgz", + "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.0.tgz", + "integrity": "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, "babel-plugin-react-compiler": { "optional": true - }, - "oxc-transform-react": { + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.2.tgz", + "integrity": "sha512-/u9r8k8ue4ZhHCw9ovDtltpWdXeXzjhdxCGj6vm1RGLPXb8lu33EMdoCHF5Sx8oYBS+Q/FYYNmAqlHncxR3RmA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001812", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001813", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001813.tgz", + "integrity": "sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.440", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.440.tgz", + "integrity": "sha512-SghDzqdJokdz8zP8YNlvS74+CwLRoUPDGvP/gFA+HrKVw+pOshUAgx8pXR37xl/u16zGae746rFNWvZ/22kU2Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz", + "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { "optional": true } } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "bin": { + "json5": "lib/cli.js" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "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==", + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.8.0" } }, "node_modules/lightningcss": { @@ -657,6 +2144,55 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.19", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", @@ -676,6 +2212,93 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -725,6 +2348,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.9.9", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", @@ -741,6 +2374,36 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", @@ -802,6 +2465,39 @@ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -829,6 +2525,32 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -843,6 +2565,71 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.0.tgz", + "integrity": "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.0", + "@typescript-eslint/parser": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { "version": "8.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", @@ -920,6 +2707,75 @@ "optional": true } } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index b55d2d3..4a639ab 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build": "npm run typecheck && vite build", "preview": "vite preview", "typecheck": "tsc -b", + "lint": "eslint .", "format": "prettier . --write", "format:check": "prettier . --check" }, @@ -20,11 +21,18 @@ "react-dom": "^19.3.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", + "eslint": "^10.11.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "globals": "^17.12.0", "prettier": "^3.9.9", "typescript": "~6.0.0", + "typescript-eslint": "^8.71.0", "vite": "^8.3.1" } } From 5629deaf985a91b6877613fd210b11fa42b31c1b Mon Sep 17 00:00:00 2001 From: tordtst Date: Tue, 29 Sep 2026 17:20:37 +0200 Subject: [PATCH 05/58] docs(docs): dokumenter lint- og formateringskommandoer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vis hvordan lint og formatering kjøres, og hvordan ESLint og Prettier deler ansvaret. Refs #2 Co-Authored-By: andretg --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 47efa37..6e5364a 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,20 @@ npm run dev # start utviklingsserver på http://localhost:5173 ## Kommandoer -| Kommando | Hva den gjør | -| ------------------- | ----------------------------------------- | -| `npm run dev` | Starter utviklingsserver | -| `npm run build` | Kjører typekontroll og bygger til `dist/` | -| `npm run preview` | Viser det bygde resultatet lokalt | -| `npm run typecheck` | Kjører TypeScript med streng typekontroll | +| Kommando | Hva den gjør | +| ---------------------- | ----------------------------------------- | +| `npm run dev` | Starter utviklingsserver | +| `npm run build` | Kjører typekontroll og bygger til `dist/` | +| `npm run preview` | Viser det bygde resultatet lokalt | +| `npm run typecheck` | Kjører TypeScript med streng typekontroll | +| `npm run lint` | Kjører ESLint på hele prosjektet | +| `npm run format` | Formaterer alle filer med Prettier | +| `npm run format:check` | Sjekker formatering uten å endre filer | + +ESLint finner feil i koden, mens Prettier bestemmer formateringen. +`eslint-config-prettier` ligger sist i `eslint.config.js` og slår av +ESLint-regler som ellers ville kollidert med Prettier. Byggfiler, +avhengigheter og `package-lock.json` formateres ikke. ## Working in this repository From 27ecf6b01cded30201972eb21be6795ca92136e1 Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 09:39:49 +0200 Subject: [PATCH 06/58] feat(src): definer felles annonsetype og prisformat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gi alle visninger samme datastruktur og samme visning av pris, kategori og tilstand. Pris er hele kroner per døgn, og 0 vises som «Gratis utlån». Refs #4 Co-Authored-By: birkjr --- src/types/listing.ts | 28 ++++++++++++++++++++++++++++ src/utils/listingFormat.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/types/listing.ts create mode 100644 src/utils/listingFormat.ts diff --git a/src/types/listing.ts b/src/types/listing.ts new file mode 100644 index 0000000..b8c5abb --- /dev/null +++ b/src/types/listing.ts @@ -0,0 +1,28 @@ +/** + * Felles datatype for en verktøyannonse. + * + * Kategori og tilstand er faste engelske nøkler slik at de senere kan bli + * GraphQL-enums. Norske visningsnavn ligger i utils/listingFormat.ts. + */ + +export type ToolCategory = + 'powerTools' | 'handTools' | 'garden' | 'painting' | 'measuring' | 'cleaning'; + +export type ToolCondition = 'likeNew' | 'good' | 'worn'; + +export interface Listing { + /** Stabil ID. Brukes i URL-er og som React-nøkkel. */ + id: string; + title: string; + category: ToolCategory; + description: string; + /** Bydel eller område i Trondheim, for eksempel «Moholt». */ + area: string; + /** Pris i hele kroner per døgn. 0 betyr gratis utlån. */ + dailyPrice: number; + condition: ToolCondition; + /** Tilbehør som følger med. Tom liste når ingenting følger med. */ + accessories: string[]; + /** Adresse til bilde av verktøyet, eller null når annonsen mangler bilde. */ + imageUrl: string | null; +} diff --git a/src/utils/listingFormat.ts b/src/utils/listingFormat.ts new file mode 100644 index 0000000..6150896 --- /dev/null +++ b/src/utils/listingFormat.ts @@ -0,0 +1,28 @@ +import type { ToolCategory, ToolCondition } from '../types/listing.ts'; + +// Record krever en etikett for hver nøkkel, så TypeScript sier fra hvis en +// ny kategori eller tilstand legges til uten visningsnavn. +export const categoryLabels: Record = { + powerTools: 'Elektroverktøy', + handTools: 'Håndverktøy', + garden: 'Hage', + painting: 'Maling og overflate', + measuring: 'Måleverktøy', + cleaning: 'Rengjøring', +}; + +export const conditionLabels: Record = { + likeNew: 'Som ny', + good: 'God stand', + worn: 'Godt brukt', +}; + +const priceFormatter = new Intl.NumberFormat('nb-NO'); + +/** Viser døgnpris som «150 kr/døgn», eller «Gratis utlån» når prisen er 0. */ +export function formatDailyPrice(dailyPrice: number): string { + if (dailyPrice === 0) { + return 'Gratis utlån'; + } + return `${priceFormatter.format(dailyPrice)} kr/døgn`; +} From 697a1824e683a5371315e82d961bfdd09e859617 Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 09:39:49 +0200 Subject: [PATCH 07/58] feat(src): legg til eksempelannonser med illustrasjoner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gi gruppen felles, oppdiktede data å bygge katalog, detaljvisning og skjema på. Dataene dekker gratis og betalt utlån, ulik tekstlengde og annonser uten bilde. Refs #4 Co-Authored-By: sebasti --- src/assets/listings/drill.svg | 11 ++ src/assets/listings/handsaw.svg | 7 ++ src/assets/listings/paint-roller.svg | 6 ++ src/assets/listings/spade.svg | 6 ++ src/data/mockListings.ts | 144 +++++++++++++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 src/assets/listings/drill.svg create mode 100644 src/assets/listings/handsaw.svg create mode 100644 src/assets/listings/paint-roller.svg create mode 100644 src/assets/listings/spade.svg create mode 100644 src/data/mockListings.ts diff --git a/src/assets/listings/drill.svg b/src/assets/listings/drill.svg new file mode 100644 index 0000000..6441bbc --- /dev/null +++ b/src/assets/listings/drill.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/listings/handsaw.svg b/src/assets/listings/handsaw.svg new file mode 100644 index 0000000..23f0400 --- /dev/null +++ b/src/assets/listings/handsaw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/listings/paint-roller.svg b/src/assets/listings/paint-roller.svg new file mode 100644 index 0000000..6d78fe5 --- /dev/null +++ b/src/assets/listings/paint-roller.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/listings/spade.svg b/src/assets/listings/spade.svg new file mode 100644 index 0000000..fe5cec8 --- /dev/null +++ b/src/assets/listings/spade.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/data/mockListings.ts b/src/data/mockListings.ts new file mode 100644 index 0000000..be58e37 --- /dev/null +++ b/src/data/mockListings.ts @@ -0,0 +1,144 @@ +/** + * Oppdiktede eksempelannonser for prototypen. Ingen av annonsene er ekte + * tilbud, og ingen personopplysninger er brukt. Illustrasjonene er laget for + * prosjektet. + * + * Dataene er valgt for å dekke ulike tilfeller i visningene: gratis og betalt + * utlån, korte og lange titler, tomt tilbehør og annonser uten bilde. + * + * Datakilden erstattes senere av GraphQL-backenden. Komponenter bør derfor få + * annonser gjennom props i stedet for å importere denne filen direkte. + */ + +import drillImage from '../assets/listings/drill.svg'; +import handsawImage from '../assets/listings/handsaw.svg'; +import paintRollerImage from '../assets/listings/paint-roller.svg'; +import spadeImage from '../assets/listings/spade.svg'; +import type { Listing } from '../types/listing.ts'; + +export const mockListings: Listing[] = [ + { + id: 'listing-001', + title: 'Drill', + category: 'powerTools', + description: + 'Batteridrill som passer til å skru opp hyller og montere møbler.', + area: 'Moholt', + dailyPrice: 100, + condition: 'good', + accessories: ['2 batterier', 'Lader', 'Bitssett'], + imageUrl: drillImage, + }, + { + id: 'listing-002', + title: 'Slagdrill med borsett for betong og murvegger', + category: 'powerTools', + description: + 'Kraftig slagdrill for boring i betong, mur og tegl. Egner seg til å henge opp tunge hyller, TV-braketter og kjøkkenskap. Borsett fra 6 til 12 mm følger med. Bruk hørselvern ved lengre arbeid.', + area: 'Lade', + dailyPrice: 150, + condition: 'likeNew', + accessories: ['Borsett for betong', 'Koffert', 'Dybdestopper'], + imageUrl: drillImage, + }, + { + id: 'listing-003', + title: 'Fogsvans', + category: 'handTools', + description: 'Vanlig håndsag for tre. Bladet er skarpt.', + area: 'Gløshaugen', + dailyPrice: 0, + condition: 'good', + accessories: [], + imageUrl: handsawImage, + }, + { + id: 'listing-004', + title: 'Verktøykasse med hammer, skrutrekkere og tenger', + category: 'handTools', + description: + 'Grunnleggende verktøy for småreparasjoner i hybelen. Kassen har hammer, flere skrutrekkere, nebbtang, kombinasjonstang og målebånd.', + area: 'Singsaker', + dailyPrice: 50, + condition: 'worn', + accessories: ['Målebånd', 'Unbrakonøkler'], + imageUrl: null, + }, + { + id: 'listing-005', + title: 'Spade', + category: 'garden', + description: 'Solid spade til hagearbeid og planting.', + area: 'Byåsen', + dailyPrice: 0, + condition: 'worn', + accessories: [], + imageUrl: spadeImage, + }, + { + id: 'listing-006', + title: 'Hagespade og rive', + category: 'garden', + description: + 'Spade og rive som lånes ut sammen. Fint til å gjøre klar et lite bed eller rake løv om høsten.', + area: 'Tiller', + dailyPrice: 40, + condition: 'good', + accessories: ['Rive', 'Arbeidshansker'], + imageUrl: spadeImage, + }, + { + id: 'listing-007', + title: 'Malerrulle og brett', + category: 'painting', + description: + 'Rulle og malebrett for vegger. Rullene er vasket og klare til bruk.', + area: 'Møllenberg', + dailyPrice: 0, + condition: 'likeNew', + accessories: ['2 ruller', 'Malebrett', 'Teleskopskaft'], + imageUrl: paintRollerImage, + }, + { + id: 'listing-008', + title: + 'Komplett malersett til en liten leilighet: ruller, pensler, dekkplast og tape', + category: 'painting', + description: + 'Alt du trenger for å male om en hybel eller en liten leilighet, bortsett fra maling. Settet har ruller i to størrelser, pensler til kanter, dekkplast til gulv og møbler og maskeringstape. Rengjør ruller og pensler før levering.', + area: 'Midtbyen', + dailyPrice: 120, + condition: 'good', + accessories: [ + 'Ruller i to størrelser', + '3 pensler', + 'Dekkplast', + 'Maskeringstape', + ], + imageUrl: paintRollerImage, + }, + { + id: 'listing-009', + title: 'Laservater', + category: 'measuring', + description: + 'Krysslaser for å henge bilder og hyller rett. Rekker omtrent 15 meter innendørs.', + area: 'Heimdal', + dailyPrice: 80, + condition: 'likeNew', + accessories: ['Stativ', 'Batterier'], + imageUrl: null, + }, + { + id: 'listing-010', + title: 'Tepperenser', + category: 'cleaning', + description: + 'Rensemaskin for tepper og møbeltrekk. Nyttig ved utflytting. Rensemiddel følger ikke med.', + area: 'Nardo', + dailyPrice: 250, + condition: 'good', + accessories: ['Slange', '2 munnstykker'], + imageUrl: null, + }, +]; From 58cb0a6f133bcbaf7d3c9fc66b7457576aa68c9f Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 09:39:49 +0200 Subject: [PATCH 08/58] docs(docs): dokumenter datamodell og mockdata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forklar feltene, prisens enhet og hvordan manglende bilde håndteres, slik at alle bruker dataene likt og datakilden senere kan byttes. Refs #4 Co-Authored-By: andretg --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 6e5364a..f7286ca 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,38 @@ ESLint finner feil i koden, mens Prettier bestemmer formateringen. ESLint-regler som ellers ville kollidert med Prettier. Byggfiler, avhengigheter og `package-lock.json` formateres ikke. +## Datamodell og mockdata + +Alle annonser følger typen `Listing` i `src/types/listing.ts`: + +| Felt | Innhold | +| ------------- | ---------------------------------------------------------- | +| `id` | Stabil ID, for eksempel `listing-001` | +| `title` | Kort tittel | +| `category` | Fast nøkkel, for eksempel `powerTools` | +| `description` | Fritekst | +| `area` | Område i Trondheim, for eksempel «Moholt» | +| `dailyPrice` | Pris i hele kroner per døgn. `0` betyr gratis utlån | +| `condition` | `likeNew`, `good` eller `worn` | +| `accessories` | Liste med tilbehør. Tom liste når ingenting følger med | +| `imageUrl` | Adresse til bilde, eller `null` når annonsen mangler bilde | + +Kategori og tilstand lagres som engelske nøkler slik at de senere kan bli +GraphQL-enums. `src/utils/listingFormat.ts` gir de norske visningsnavnene og +`formatDailyPrice`, som viser `150 kr/døgn` eller `Gratis utlån`. Bruk disse i +stedet for å formatere på nytt i hver komponent. Når `imageUrl` er `null`, skal +visningen vise en nøytral plassholder. + +`src/data/mockListings.ts` har 10 oppdiktede eksempelannonser. De er ikke ekte +tilbud og inneholder ingen personopplysninger. Illustrasjonene i +`src/assets/listings/` er laget for prosjektet. Ekstern datakilde er ikke +valgt. + +Senere skal annonsene hentes fra en egen GraphQL-backend med database +(planlagt, ikke laget). Komponenter bør derfor få annonser gjennom props i +stedet for å importere mockdataene direkte. Da kan datakilden byttes uten at +visningene må skrives om. + ## Working in this repository `main` is protected — you cannot push to it directly. Work on a branch and From e373b91a71b143fc41fd70710a84e167a1902201 Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 10:20:31 +0200 Subject: [PATCH 09/58] style(css): legg til felles designvariabler og grunnstiler Gi alle visninger samme farger, typografi, avstander og synlig tastaturfokus. Fargeparene er kontrollert mot WCAG-kravene. Refs #5 Co-Authored-By: birkjr --- src/main.tsx | 2 ++ src/styles/base.css | 60 +++++++++++++++++++++++++++++++++++++++++++ src/styles/tokens.css | 56 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 src/styles/base.css create mode 100644 src/styles/tokens.css diff --git a/src/main.tsx b/src/main.tsx index 19c52f5..cbc41b1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,8 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.tsx'; +import './styles/tokens.css'; +import './styles/base.css'; const rootElement = document.getElementById('root'); diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..8e7766a --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,60 @@ +/* Grunnstiler for hele appen. Komponentspesifikk stil ligger i egne CSS-moduler. */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-family); + font-size: var(--font-size-md); + line-height: var(--line-height-body); +} + +h1, +h2, +h3 { + margin: 0 0 var(--space-4); + line-height: var(--line-height-tight); +} + +h1 { + font-size: var(--font-size-2xl); +} + +h2 { + font-size: var(--font-size-xl); +} + +h3 { + font-size: var(--font-size-lg); +} + +p { + margin: 0 0 var(--space-4); +} + +a { + color: var(--color-accent); + text-underline-offset: 0.15em; +} + +a:hover { + color: var(--color-accent-hover); +} + +img { + display: block; + max-width: 100%; + height: auto; +} + +/* Synlig fokus bare ved tastaturnavigasjon, slik at museklikk ikke gir ring. */ +:focus-visible { + outline: var(--focus-ring); + outline-offset: var(--focus-offset); +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..537cee7 --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,56 @@ +/* + * Felles designvariabler. Bruk disse i stedet for faste verdier i komponentene, + * slik at farger, avstander og tekst holder seg like i hele appen. + * Kontrasttall for fargeparene står i docs/design.md. + */ + +:root { + /* Farger */ + --color-bg: #faf8f4; + --color-surface: #ffffff; + --color-surface-muted: #f1ede5; + --color-text: #1f2421; + --color-text-muted: #5b625d; + --color-border: #ddd8ce; + --color-border-strong: #767c78; + --color-accent: #2f6b4f; + --color-accent-hover: #24543e; + --color-on-accent: #ffffff; + --color-free-bg: #e3f0e8; + --color-free-text: #1e5239; + --color-error: #b3261e; + + /* Typografi */ + --font-family: + system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + sans-serif; + --font-size-sm: 0.875rem; + --font-size-md: 1rem; + --font-size-lg: 1.25rem; + --font-size-xl: 1.5rem; + --font-size-2xl: 2rem; + --font-weight-normal: 400; + --font-weight-bold: 600; + --line-height-tight: 1.25; + --line-height-body: 1.5; + + /* Avstander, i steg på 4 px */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-12: 3rem; + + /* Form og layout */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-pill: 999px; + --content-width: 72rem; + --tap-target: 44px; + + /* Fokus */ + --focus-ring: 3px solid var(--color-accent); + --focus-offset: 2px; +} From 84ca237c825dddb2de87da2077566e6a53722103 Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 10:20:31 +0200 Subject: [PATCH 10/58] feat(src): lag prismerke og referanse for annonsekort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vis pris og gratis utlån likt overalt, med tekst og ikke bare farge. Annonsekortet viser bilde eller plassholder, tittel, område og pris som utgangspunkt for katalogen. Refs #5 Co-Authored-By: sebasti --- .../ListingCard/ListingCard.module.css | 48 +++++++++++++++++++ src/components/ListingCard/ListingCard.tsx | 41 ++++++++++++++++ src/components/PriceTag/PriceTag.module.css | 19 ++++++++ src/components/PriceTag/PriceTag.tsx | 19 ++++++++ 4 files changed, 127 insertions(+) create mode 100644 src/components/ListingCard/ListingCard.module.css create mode 100644 src/components/ListingCard/ListingCard.tsx create mode 100644 src/components/PriceTag/PriceTag.module.css create mode 100644 src/components/PriceTag/PriceTag.tsx diff --git a/src/components/ListingCard/ListingCard.module.css b/src/components/ListingCard/ListingCard.module.css new file mode 100644 index 0000000..daebee3 --- /dev/null +++ b/src/components/ListingCard/ListingCard.module.css @@ -0,0 +1,48 @@ +.card { + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.image, +.imagePlaceholder { + width: 100%; + aspect-ratio: 4 / 3; +} + +.image { + object-fit: cover; +} + +.imagePlaceholder { + display: grid; + place-items: center; + background: var(--color-surface-muted); + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} + +.body { + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-start; + gap: var(--space-2); + padding: var(--space-4); +} + +.title { + margin: 0; + font-size: var(--font-size-lg); + /* Lange titler brytes i stedet for å gi horisontal scrolling. */ + overflow-wrap: anywhere; +} + +.meta { + margin: 0; + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} diff --git a/src/components/ListingCard/ListingCard.tsx b/src/components/ListingCard/ListingCard.tsx new file mode 100644 index 0000000..bbed0ae --- /dev/null +++ b/src/components/ListingCard/ListingCard.tsx @@ -0,0 +1,41 @@ +import type { Listing } from '../../types/listing.ts'; +import { categoryLabels } from '../../utils/listingFormat.ts'; +import PriceTag from '../PriceTag/PriceTag.tsx'; +import styles from './ListingCard.module.css'; + +interface ListingCardProps { + listing: Listing; +} + +/** + * Visuell referanse for et annonsekort. Katalogen kan bygge videre på denne, + * for eksempel ved å gjøre tittelen til en lenke til detaljvisningen. + */ +function ListingCard({ listing }: ListingCardProps) { + return ( +
+ {listing.imageUrl ? ( + // Tom alt-tekst fordi tittelen rett under beskriver det samme. + + ) : ( +
Ingen bilde
+ )} +
+

{listing.title}

+

+ {categoryLabels[listing.category]} · {listing.area} +

+ +
+
+ ); +} + +export default ListingCard; diff --git a/src/components/PriceTag/PriceTag.module.css b/src/components/PriceTag/PriceTag.module.css new file mode 100644 index 0000000..47a6cc5 --- /dev/null +++ b/src/components/PriceTag/PriceTag.module.css @@ -0,0 +1,19 @@ +.paid, +.free { + display: inline-block; + padding: var(--space-1) var(--space-3); + border-radius: var(--radius-pill); + font-weight: var(--font-weight-bold); + white-space: nowrap; +} + +.paid { + background: var(--color-surface-muted); + color: var(--color-text); +} + +/* Gratis utlån skilles med både tekst og farge, ikke bare farge. */ +.free { + background: var(--color-free-bg); + color: var(--color-free-text); +} diff --git a/src/components/PriceTag/PriceTag.tsx b/src/components/PriceTag/PriceTag.tsx new file mode 100644 index 0000000..e312e5b --- /dev/null +++ b/src/components/PriceTag/PriceTag.tsx @@ -0,0 +1,19 @@ +import { formatDailyPrice } from '../../utils/listingFormat.ts'; +import styles from './PriceTag.module.css'; + +interface PriceTagProps { + /** Pris i hele kroner per døgn. 0 vises som «Gratis utlån». */ + dailyPrice: number; +} + +function PriceTag({ dailyPrice }: PriceTagProps) { + const isFree = dailyPrice === 0; + + return ( + + {formatDailyPrice(dailyPrice)} + + ); +} + +export default PriceTag; From 39899c07368e453d2a2ef320a9bfe9e0c5894498 Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 10:20:32 +0200 Subject: [PATCH 11/58] feat(src): legg til designreferanse for utvikling Gi gruppen en felles visuell referanse med farger, tekst og komponenter. Siden er bare tilgjengelig med npm run dev og er ikke med i produksjonsbygget. Refs #5 Co-Authored-By: andretg --- design.html | 17 +++++ src/design/DesignPreview.module.css | 79 ++++++++++++++++++++ src/design/DesignPreview.tsx | 109 ++++++++++++++++++++++++++++ src/design/main.tsx | 17 +++++ 4 files changed, 222 insertions(+) create mode 100644 design.html create mode 100644 src/design/DesignPreview.module.css create mode 100644 src/design/DesignPreview.tsx create mode 100644 src/design/main.tsx diff --git a/design.html b/design.html new file mode 100644 index 0000000..5933a92 --- /dev/null +++ b/design.html @@ -0,0 +1,17 @@ + + + + + + + + Designreferanse – Verktøyutlån Trondheim + + +
+ + + diff --git a/src/design/DesignPreview.module.css b/src/design/DesignPreview.module.css new file mode 100644 index 0000000..86ac7c7 --- /dev/null +++ b/src/design/DesignPreview.module.css @@ -0,0 +1,79 @@ +.page { + max-width: var(--content-width); + margin: 0 auto; + padding: var(--space-8) var(--space-4); +} + +.lead { + max-width: 40rem; + color: var(--color-text-muted); +} + +.section { + margin-top: var(--space-12); +} + +.muted { + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} + +.sizeXl { + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-tight); +} + +.sizeLg { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); +} + +.swatches, +.spaces { + margin: 0; + padding: 0; + list-style: none; +} + +.swatches { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); + gap: var(--space-4); +} + +.swatch { + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.swatchColor { + height: 3rem; + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); +} + +.space { + display: flex; + align-items: center; + gap: var(--space-4); + margin-bottom: var(--space-2); +} + +.spaceBar { + height: var(--space-4); + background: var(--color-accent); +} + +.row { + display: flex; + flex-wrap: wrap; + gap: var(--space-4); +} + +.cardGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: var(--space-6); +} diff --git a/src/design/DesignPreview.tsx b/src/design/DesignPreview.tsx new file mode 100644 index 0000000..33000a3 --- /dev/null +++ b/src/design/DesignPreview.tsx @@ -0,0 +1,109 @@ +import ListingCard from '../components/ListingCard/ListingCard.tsx'; +import PriceTag from '../components/PriceTag/PriceTag.tsx'; +import { mockListings } from '../data/mockListings.ts'; +import styles from './DesignPreview.module.css'; + +const colorTokens = [ + { token: '--color-bg', usage: 'Sidebakgrunn' }, + { token: '--color-surface', usage: 'Kort og felt' }, + { token: '--color-surface-muted', usage: 'Prismerke, bildeplassholder' }, + { token: '--color-text', usage: 'Brødtekst og overskrifter' }, + { token: '--color-text-muted', usage: 'Sekundær tekst' }, + { token: '--color-border', usage: 'Dekorative rammer' }, + { token: '--color-border-strong', usage: 'Rammer på skjemafelt' }, + { token: '--color-accent', usage: 'Hovedknapp, lenker, fokus' }, + { token: '--color-free-bg', usage: 'Gratismerke, bakgrunn' }, + { token: '--color-free-text', usage: 'Gratismerke, tekst' }, + { token: '--color-error', usage: 'Feilmeldinger' }, +]; + +const spaceTokens = [ + '--space-1', + '--space-2', + '--space-3', + '--space-4', + '--space-6', + '--space-8', + '--space-12', +]; + +// Viser betalt annonse med bilde, gratis annonse og annonse uten bilde og med lang tittel. +const exampleIds = ['listing-001', 'listing-003', 'listing-004', 'listing-008']; +const exampleListings = mockListings.filter((listing) => + exampleIds.includes(listing.id), +); + +/** Felles designreferanse for gruppen. Kun tilgjengelig under utvikling. */ +function DesignPreview() { + return ( +
+

Designreferanse

+

+ Felles farger, tekst og komponenter for Verktøyutlån Trondheim. Siden + finnes bare under utvikling. Innholdet er eksempeldata. +

+ +
+

Farger

+
    + {colorTokens.map(({ token, usage }) => ( +
  • + + {token} + {usage} +
  • + ))} +
+
+ +
+

Typografi

+

Overskrift, 2xl

+

Kortittel, lg

+

Brødtekst, md. Systemfont gir rask lasting og kjent utseende.

+

Sekundær tekst, sm og dempet farge.

+

+ Eksempel på lenke +

+
+ +
+

Avstander

+
    + {spaceTokens.map((token) => ( +
  • + + {token} +
  • + ))} +
+
+ +
+

Pris og gratis utlån

+
+ + + +
+
+ +
+

Annonsekort

+
+ {exampleListings.map((listing) => ( + + ))} +
+
+
+ ); +} + +export default DesignPreview; diff --git a/src/design/main.tsx b/src/design/main.tsx new file mode 100644 index 0000000..31a28db --- /dev/null +++ b/src/design/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import '../styles/tokens.css'; +import '../styles/base.css'; +import DesignPreview from './DesignPreview.tsx'; + +const rootElement = document.getElementById('root'); + +if (!rootElement) { + throw new Error('Fant ikke #root i design.html'); +} + +createRoot(rootElement).render( + + + , +); From e9b556a4826d76292f69e63d3bf049b9c747a61e Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 10:20:32 +0200 Subject: [PATCH 12/58] docs(docs): dokumenter designgrunnlag og mappestruktur Beskriv farger med kontrasttall, typografi, avstander, komponentbruk og mappestruktur, slik at alle bygger likt videre. Refs #5 Co-Authored-By: birkjr --- README.md | 23 ++++++++++ docs/design.md | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 docs/design.md diff --git a/README.md b/README.md index f7286ca..511f667 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,29 @@ ESLint finner feil i koden, mens Prettier bestemmer formateringen. ESLint-regler som ellers ville kollidert med Prettier. Byggfiler, avhengigheter og `package-lock.json` formateres ikke. +## Mappestruktur + +``` +src/ + types/ Felles datatyper, for eksempel Listing + data/ Mockdata. Byttes med GraphQL-backend senere + utils/ Formatering av pris og etiketter + assets/ Illustrasjoner til eksempelannonsene + styles/ Designvariabler og grunnstiler + components/ Gjenbrukbare komponenter, én mappe per komponent + design/ Designreferanse, bare under utvikling +``` + +Komponenter i `components/` får data gjennom props og importerer ikke +`data/` direkte. Da kan datakilden byttes uten at visningene må skrives om. + +## Design + +Felles farger, typografi, avstander og komponenter er beskrevet i +[docs/design.md](docs/design.md). Under utvikling viser + en designreferanse med eksempler. Den er +ikke med i produksjonsbygget. + ## Datamodell og mockdata Alle annonser følger typen `Listing` i `src/types/listing.ts`: diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..860cd2f --- /dev/null +++ b/docs/design.md @@ -0,0 +1,120 @@ +# Design + +Felles visuelt grunnlag for Verktøyutlån Trondheim. Målet er et rolig og +tydelig uttrykk der pris, gratis utlån og område er lett å se. + +## Designreferanse + +Kjør `npm run dev` og åpne . Siden viser +farger, typografi, avstander, prismerking og annonsekort med eksempeldata. +Den finnes bare under utvikling og er ikke med i `npm run build`. + +## Retning + +- Lys, varm bakgrunn, hvite flater og nesten svart tekst. +- Én dempet grønn aksentfarge for hovedhandlinger, lenker og fokus. +- Diskrete rammer, 8 px avrunding og god luft. +- Systemfont for rask lasting og kjent utseende. +- Status og pris vises alltid med tekst, ikke bare med farge. + +## Farger + +Variablene ligger i `src/styles/tokens.css`. Kontrasten er regnet ut etter +WCAG 2.1. Krav: 4,5:1 for vanlig tekst og 3:1 for rammer på skjemafelt og +fokusmarkering. + +| Variabel | Verdi | Bruk | +| ----------------------- | --------- | ----------------------------- | +| `--color-bg` | `#faf8f4` | Sidebakgrunn | +| `--color-surface` | `#ffffff` | Kort og felt | +| `--color-surface-muted` | `#f1ede5` | Prismerke og bildeplassholder | +| `--color-text` | `#1f2421` | Brødtekst og overskrifter | +| `--color-text-muted` | `#5b625d` | Sekundær tekst | +| `--color-border` | `#ddd8ce` | Dekorative rammer | +| `--color-border-strong` | `#767c78` | Rammer på skjemafelt | +| `--color-accent` | `#2f6b4f` | Hovedknapp, lenker og fokus | +| `--color-accent-hover` | `#24543e` | Hover på knapp og lenke | +| `--color-on-accent` | `#ffffff` | Tekst på aksentfarge | +| `--color-free-bg` | `#e3f0e8` | Gratismerke, bakgrunn | +| `--color-free-text` | `#1e5239` | Gratismerke, tekst | +| `--color-error` | `#b3261e` | Feilmeldinger | + +Kontrollerte fargepar: + +| Forgrunn | Bakgrunn | Kontrast | Krav | +| ----------------------- | ----------------------- | -------- | ----- | +| `--color-text` | `--color-bg` | 14,9:1 | 4,5:1 | +| `--color-text-muted` | `--color-bg` | 5,9:1 | 4,5:1 | +| `--color-text-muted` | `--color-surface-muted` | 5,4:1 | 4,5:1 | +| `--color-on-accent` | `--color-accent` | 6,3:1 | 4,5:1 | +| `--color-on-accent` | `--color-accent-hover` | 8,7:1 | 4,5:1 | +| `--color-accent` | `--color-bg` | 5,9:1 | 4,5:1 | +| `--color-free-text` | `--color-free-bg` | 7,7:1 | 4,5:1 | +| `--color-text` | `--color-surface-muted` | 13,5:1 | 4,5:1 | +| `--color-error` | `--color-surface` | 6,5:1 | 4,5:1 | +| `--color-border-strong` | `--color-surface` | 4,3:1 | 3:1 | + +`--color-border` har lav kontrast (1,3:1) og brukes bare til dekorative rammer, +der innholdet er forståelig uten rammen. + +## Typografi + +| Variabel | Størrelse | Bruk | +| ----------------- | --------- | -------------------------- | +| `--font-size-sm` | 14 px | Sekundær tekst og metadata | +| `--font-size-md` | 16 px | Brødtekst | +| `--font-size-lg` | 20 px | Korttitler, `h3` | +| `--font-size-xl` | 24 px | `h2` | +| `--font-size-2xl` | 32 px | `h1` | + +Brødtekst har linjehøyde 1,5 og overskrifter 1,25. Fet skrift er vekt 600. + +## Avstander, form og fokus + +- Avstander går i steg på 4 px: `--space-1` (4 px) til `--space-12` (48 px). + Bruk `--space-4` (16 px) som vanlig sidemarg på mobil. +- `--radius-md` (8 px) for kort og felt, `--radius-pill` for merker. +- `--content-width` (72rem) er maks bredde på innholdet. +- `--tap-target` (44 px) er minste høyde for knapper og andre trykkflater. +- Fokus vises med `:focus-visible` som en 3 px grønn ring med 2 px avstand. + Ikke fjern `outline` uten å erstatte den med noe like tydelig. + +## Komponenter + +Komponentene får data gjennom props og bruker CSS-moduler, slik at klassenavn +ikke kolliderer mellom filer. Hver komponent har sin egen mappe med komponent, +stil og etter hvert tester: + +``` +src/components/PriceTag/ + PriceTag.tsx + PriceTag.module.css +``` + +### PriceTag + +Viser døgnpris, eller «Gratis utlån» når prisen er 0. Gratis utlån får grønn +bakgrunn, men teksten alene er nok til å skille dem. + +```tsx + // Gratis utlån + // 150 kr/døgn +``` + +### ListingCard + +Visuell referanse for et annonsekort med bilde, tittel, kategori, område og +pris. Når `imageUrl` er `null`, vises en plassholder med teksten «Ingen bilde». +Bildet har tom alt-tekst fordi tittelen rett under beskriver det samme. + +```tsx + +``` + +Tittelen er en `h2`. Katalogen kan gjøre tittelen til en lenke til +detaljvisningen når den finnes. + +## Ikke laget ennå + +- Knapp, skjemafelt, sidemal og navigasjon kommer i egen oppgave. +- Mørk modus er ikke planlagt for første innlevering. From 660d867121ff74f725f8d68f51a4636c198743c2 Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 11:46:53 +0200 Subject: [PATCH 13/58] feat(app): legg til ruting, sidemal og plassholdersider Gi appen felles header, navigasjon og hopp-lenke, og egne ruter for katalog, detaljvisning og utleie slik at gruppen kan fylle ut hver side for seg. Fokus flyttes til hovedinnholdet ved sidebytte. React Router krever Node 22.22 eller nyere. Refs #6 Co-Authored-By: sebasti --- package-lock.json | 37 +++++- package.json | 5 +- src/App.tsx | 26 +++-- src/components/Layout/Layout.module.css | 107 ++++++++++++++++++ src/components/Layout/Layout.tsx | 67 +++++++++++ src/hooks/usePageTitle.ts | 13 +++ src/main.tsx | 7 +- src/pages/CatalogPage/CatalogPage.tsx | 33 ++++++ .../ListingDetailPage/ListingDetailPage.tsx | 45 ++++++++ src/pages/NotFoundPage/NotFoundPage.tsx | 18 +++ src/pages/RentOutPage/RentOutPage.tsx | 18 +++ 11 files changed, 365 insertions(+), 11 deletions(-) create mode 100644 src/components/Layout/Layout.module.css create mode 100644 src/components/Layout/Layout.tsx create mode 100644 src/hooks/usePageTitle.ts create mode 100644 src/pages/CatalogPage/CatalogPage.tsx create mode 100644 src/pages/ListingDetailPage/ListingDetailPage.tsx create mode 100644 src/pages/NotFoundPage/NotFoundPage.tsx create mode 100644 src/pages/RentOutPage/RentOutPage.tsx diff --git a/package-lock.json b/package-lock.json index 0c85b0d..cbee598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.3.0", - "react-dom": "^19.3.0" + "react-dom": "^19.3.0", + "react-router": "^8.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -573,6 +574,12 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@remix-run/route-pattern": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/@remix-run/route-pattern/-/route-pattern-0.22.1.tgz", + "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", @@ -1301,6 +1308,12 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2425,6 +2438,28 @@ "react": "^19.3.0" } }, + "node_modules/react-router": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.4.0.tgz", + "integrity": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "license": "MIT", + "dependencies": { + "@remix-run/route-pattern": "^0.22.1", + "cookie-es": "^3.1.1" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "react": ">=19.2.7", + "react-dom": ">=19.2.7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/rolldown": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", diff --git a/package.json b/package.json index 4a639ab..ce2ee1c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "engines": { - "node": ">=22.12.0", + "node": ">=22.22.0", "npm": ">=10" }, "scripts": { @@ -18,7 +18,8 @@ }, "dependencies": { "react": "^19.3.0", - "react-dom": "^19.3.0" + "react-dom": "^19.3.0", + "react-router": "^8.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/App.tsx b/src/App.tsx index 694665a..8ebcb26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,24 @@ +import { Route, Routes } from 'react-router'; +import Layout from './components/Layout/Layout.tsx'; +import CatalogPage from './pages/CatalogPage/CatalogPage.tsx'; +import ListingDetailPage from './pages/ListingDetailPage/ListingDetailPage.tsx'; +import NotFoundPage from './pages/NotFoundPage/NotFoundPage.tsx'; +import RentOutPage from './pages/RentOutPage/RentOutPage.tsx'; + +/** + * Rutene i appen. Routeren selv ligger i main.tsx, slik at tester kan pakke + * App inn i en MemoryRouter. + */ function App() { return ( -
-

Verktøyutlån Trondheim

-

- Her kommer en katalog der studenter og privatpersoner kan leie og låne - verktøy av hverandre. Siden er under utvikling. -

-
+ + }> + } /> + } /> + } /> + } /> + + ); } diff --git a/src/components/Layout/Layout.module.css b/src/components/Layout/Layout.module.css new file mode 100644 index 0000000..d584e23 --- /dev/null +++ b/src/components/Layout/Layout.module.css @@ -0,0 +1,107 @@ +.page { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +/* Skjult til den får tastaturfokus, og vises da øverst til venstre. */ +.skipLink { + position: absolute; + top: var(--space-2); + left: var(--space-2); + z-index: 10; + padding: var(--space-2) var(--space-4); + background: var(--color-surface); + border-radius: var(--radius-md); + transform: translateY(-200%); +} + +.skipLink:focus { + transform: none; +} + +.header { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); +} + +.headerInner, +.main, +.footer { + width: 100%; + max-width: var(--content-width); + margin: 0 auto; + padding-inline: var(--space-4); +} + +.headerInner { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--space-2) var(--space-6); + padding-block: var(--space-2); +} + +.brand { + display: inline-flex; + align-items: center; + min-height: var(--tap-target); + color: var(--color-text); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + text-decoration: none; +} + +.navList { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + margin: 0; + padding: 0; + list-style: none; +} + +.navLink { + display: inline-flex; + align-items: center; + min-height: var(--tap-target); + padding-inline: var(--space-3); + border-radius: var(--radius-md); + color: var(--color-text); + text-decoration: none; +} + +.navLink:hover { + background: var(--color-surface-muted); + color: var(--color-text); +} + +/* Aktiv side markeres med tykk understrek og fet tekst, ikke bare farge. */ +.navLink[aria-current='page'] { + font-weight: var(--font-weight-bold); + text-decoration: underline; + text-decoration-thickness: 3px; + text-decoration-color: var(--color-accent); + text-underline-offset: 0.4em; +} + +.main { + flex: 1; + padding-block: var(--space-8); +} + +/* main får fokus programmatisk ved sidebytte, uten synlig ring. */ +.main:focus { + outline: none; +} + +.footer { + padding-block: var(--space-6); + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} + +.footer p { + margin: 0; +} diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..d69e80b --- /dev/null +++ b/src/components/Layout/Layout.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef } from 'react'; +import { Link, NavLink, Outlet, useLocation } from 'react-router'; +import styles from './Layout.module.css'; + +/** Felles sidemal med hopp-lenke, header, navigasjon og hovedinnhold. */ +function Layout() { + const { pathname } = useLocation(); + const mainRef = useRef(null); + const previousPathname = useRef(pathname); + + // Ved sidebytte flyttes fokus til hovedinnholdet, slik at skjermlesere og + // tastaturbrukere starter på den nye siden. Sammenligningen med forrige sti + // hindrer fokusflytting ved første innlasting, også når StrictMode kjører + // effekten to ganger. + useEffect(() => { + if (previousPathname.current === pathname) { + return; + } + previousPathname.current = pathname; + mainRef.current?.focus(); + }, [pathname]); + + return ( +
+ + Hopp til hovedinnhold + + +
+
+ + Verktøyutlån Trondheim + + +
+
+ +
+ +
+ +
+

Prototype. Alle annonser er oppdiktede eksempler.

+
+
+ ); +} + +export default Layout; diff --git a/src/hooks/usePageTitle.ts b/src/hooks/usePageTitle.ts new file mode 100644 index 0000000..047ccef --- /dev/null +++ b/src/hooks/usePageTitle.ts @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; + +const SITE_NAME = 'Verktøyutlån Trondheim'; + +/** + * Setter fanetittelen, for eksempel «Katalog – Verktøyutlån Trondheim». + * Unik tittel per side hjelper skjermleserbrukere og faner i nettleseren. + */ +export function usePageTitle(pageTitle: string) { + useEffect(() => { + document.title = `${pageTitle} – ${SITE_NAME}`; + }, [pageTitle]); +} diff --git a/src/main.tsx b/src/main.tsx index cbc41b1..0fdc649 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router'; import App from './App.tsx'; import './styles/tokens.css'; import './styles/base.css'; @@ -11,8 +12,12 @@ if (!rootElement) { throw new Error('Fant ikke #root i index.html'); } +// basename følger Vites base-sti, slik at rutene også virker når appen senere +// publiseres under en undermappe som /project2. createRoot(rootElement).render( - + + + , ); diff --git a/src/pages/CatalogPage/CatalogPage.tsx b/src/pages/CatalogPage/CatalogPage.tsx new file mode 100644 index 0000000..d31c47b --- /dev/null +++ b/src/pages/CatalogPage/CatalogPage.tsx @@ -0,0 +1,33 @@ +import { Link } from 'react-router'; +import { mockListings } from '../../data/mockListings.ts'; +import { usePageTitle } from '../../hooks/usePageTitle.ts'; + +/** + * Plassholder for katalogen. Søk, filtre, sortering og annonsekort lages i + * egen oppgave. Lenkelisten gjør det mulig å åpne detaljvisningen imens. + */ +function CatalogPage() { + usePageTitle('Katalog'); + + return ( + <> +

Finn verktøy

+

+ Lei eller lån verktøy av andre studenter og privatpersoner i Trondheim. + Her kommer katalogen med søk, filtre og sortering. +

+ +

Eksempelannonser

+

Midlertidig liste til katalogen er på plass.

+
    + {mockListings.map((listing) => ( +
  • + {listing.title} +
  • + ))} +
+ + ); +} + +export default CatalogPage; diff --git a/src/pages/ListingDetailPage/ListingDetailPage.tsx b/src/pages/ListingDetailPage/ListingDetailPage.tsx new file mode 100644 index 0000000..0e97456 --- /dev/null +++ b/src/pages/ListingDetailPage/ListingDetailPage.tsx @@ -0,0 +1,45 @@ +import { Link, useParams } from 'react-router'; +import PriceTag from '../../components/PriceTag/PriceTag.tsx'; +import { mockListings } from '../../data/mockListings.ts'; +import { usePageTitle } from '../../hooks/usePageTitle.ts'; + +/** + * Plassholder for detaljvisningen. Beskrivelse, tilstand, tilbehør og + * leieforespørsel lages i egen oppgave. + */ +function ListingDetailPage() { + const { listingId } = useParams(); + const listing = mockListings.find((item) => item.id === listingId); + usePageTitle(listing ? listing.title : 'Fant ikke annonsen'); + + if (!listing) { + return ( + <> +

Fant ikke annonsen

+

Annonsen finnes ikke, eller den er fjernet.

+

+ Tilbake til katalogen +

+ + ); + } + + return ( + <> +

+ Tilbake til katalogen +

+

{listing.title}

+

Område: {listing.area}

+

+ +

+

+ Her kommer detaljvisningen med bilde, beskrivelse, tilstand, tilbehør og + leieforespørsel. +

+ + ); +} + +export default ListingDetailPage; diff --git a/src/pages/NotFoundPage/NotFoundPage.tsx b/src/pages/NotFoundPage/NotFoundPage.tsx new file mode 100644 index 0000000..487ce95 --- /dev/null +++ b/src/pages/NotFoundPage/NotFoundPage.tsx @@ -0,0 +1,18 @@ +import { Link } from 'react-router'; +import { usePageTitle } from '../../hooks/usePageTitle.ts'; + +function NotFoundPage() { + usePageTitle('Fant ikke siden'); + + return ( + <> +

Fant ikke siden

+

Adressen finnes ikke.

+

+ Gå til katalogen +

+ + ); +} + +export default NotFoundPage; diff --git a/src/pages/RentOutPage/RentOutPage.tsx b/src/pages/RentOutPage/RentOutPage.tsx new file mode 100644 index 0000000..b81173a --- /dev/null +++ b/src/pages/RentOutPage/RentOutPage.tsx @@ -0,0 +1,18 @@ +import { usePageTitle } from '../../hooks/usePageTitle.ts'; + +/** Plassholder for skjemaet der brukere legger ut egne verktøy. */ +function RentOutPage() { + usePageTitle('Lei ut verktøy'); + + return ( + <> +

Lei ut verktøy

+

+ Her kommer skjemaet for å legge ut et verktøy med tittel, kategori, + beskrivelse, område og døgnpris. Pris 0 betyr gratis utlån. +

+ + ); +} + +export default RentOutPage; From eebe6cfc50c4b89af82114b31846e93d181c6b5c Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 11:46:53 +0200 Subject: [PATCH 14/58] feat(src): lag Button og TextField med tilgjengelige tilstander Gi skjemaer og handlinger felles knapper og felt med synlig fokus, koblede etiketter, feilmeldinger og deaktivert tilstand. Refs #6 Co-Authored-By: andretg --- src/components/Button/Button.module.css | 38 +++++++++++++ src/components/Button/Button.tsx | 26 +++++++++ src/components/TextField/TextField.module.css | 46 ++++++++++++++++ src/components/TextField/TextField.tsx | 53 +++++++++++++++++++ src/design/DesignPreview.module.css | 6 +++ src/design/DesignPreview.tsx | 34 ++++++++++++ 6 files changed, 203 insertions(+) create mode 100644 src/components/Button/Button.module.css create mode 100644 src/components/Button/Button.tsx create mode 100644 src/components/TextField/TextField.module.css create mode 100644 src/components/TextField/TextField.tsx diff --git a/src/components/Button/Button.module.css b/src/components/Button/Button.module.css new file mode 100644 index 0000000..8462bcc --- /dev/null +++ b/src/components/Button/Button.module.css @@ -0,0 +1,38 @@ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: var(--tap-target); + padding: var(--space-2) var(--space-6); + border: 2px solid transparent; + border-radius: var(--radius-md); + font: inherit; + font-weight: var(--font-weight-bold); + cursor: pointer; +} + +.primary { + background: var(--color-accent); + color: var(--color-on-accent); +} + +.primary:hover:not(:disabled) { + background: var(--color-accent-hover); +} + +.secondary { + background: var(--color-surface); + border-color: var(--color-border-strong); + color: var(--color-text); +} + +.secondary:hover:not(:disabled) { + background: var(--color-surface-muted); +} + +.button:disabled { + background: var(--color-surface-muted); + border-color: var(--color-border); + color: var(--color-text-muted); + cursor: not-allowed; +} diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx new file mode 100644 index 0000000..2ef4776 --- /dev/null +++ b/src/components/Button/Button.tsx @@ -0,0 +1,26 @@ +import type { ButtonHTMLAttributes } from 'react'; +import styles from './Button.module.css'; + +interface ButtonProps extends ButtonHTMLAttributes { + /** primary brukes til én hovedhandling per visning, secondary til resten. */ + variant?: 'primary' | 'secondary'; +} + +/** + * Vanlig knapp med felles stil. Standard type er "button", slik at knappen + * ikke sender inn et skjema ved et uhell. Bruk type="submit" i skjemaer. + */ +function Button({ + variant = 'primary', + type = 'button', + className, + ...buttonProps +}: ButtonProps) { + const classNames = [styles.button, styles[variant], className] + .filter(Boolean) + .join(' '); + + return + + + + + +
+

Skjemafelt

+
+ + + + +
+
+

Annonsekort

From 373ad83e8d76e9a0fd6bcad1b382f983cc66c41b Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 11:46:53 +0200 Subject: [PATCH 15/58] docs(docs): dokumenter sider, ruter og UI-komponenter Vis hvordan nye sider legges til, og hvordan Button, TextField og sidemalen brukes, slik at gruppen kan bygge videre likt. Refs #6 Co-Authored-By: birkjr --- README.md | 23 ++++++++++++++++++++-- docs/design.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 511f667..2303283 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ privatpersoner i Trondheim. Bygget med React, TypeScript og Vite. `nvm use`. - npm 10 eller nyere (følger med Node). -`engines` i `package.json` angir minstekrav (Node 22.12 og npm 10), men npm +`engines` i `package.json` angir minstekrav (Node 22.22 og npm 10), men npm stopper ikke installasjonen hvis versjonen er feil. Sjekk selv med `node -v` og `npm -v`. @@ -52,11 +52,30 @@ src/ assets/ Illustrasjoner til eksempelannonsene styles/ Designvariabler og grunnstiler components/ Gjenbrukbare komponenter, én mappe per komponent + pages/ Én mappe per side + hooks/ Små felles hooks, for eksempel usePageTitle design/ Designreferanse, bare under utvikling ``` Komponenter i `components/` får data gjennom props og importerer ikke -`data/` direkte. Da kan datakilden byttes uten at visningene må skrives om. +`data/` direkte. Bare sidene i `pages/` henter data. Da kan datakilden byttes +uten at visningene må skrives om. + +## Sider og ruter + +Rutene er definert i `src/App.tsx` med React Router. Alle sidene er +foreløpig plassholdere som fylles ut i egne oppgaver. + +| Rute | Side | Innhold nå | +| ---------------------- | ------------------- | ------------------------------------------ | +| `/` | `CatalogPage` | Plassholder og lenker til eksempelannonser | +| `/annonser/:listingId` | `ListingDetailPage` | Tittel, område og pris for annonsen | +| `/lei-ut` | `RentOutPage` | Plassholder for utleieskjema | +| Alle andre | `NotFoundPage` | «Fant ikke siden» | + +Ny side: lag en mappe i `src/pages/`, legg til en `Route` i `App.tsx` og kall +`usePageTitle` i siden. Sidemalen (`Layout`) gir header, navigasjon og +hopp-lenke automatisk. ## Design diff --git a/docs/design.md b/docs/design.md index 860cd2f..53fc73f 100644 --- a/docs/design.md +++ b/docs/design.md @@ -112,9 +112,58 @@ Bildet har tom alt-tekst fordi tittelen rett under beskriver det samme. ``` Tittelen er en `h2`. Katalogen kan gjøre tittelen til en lenke til -detaljvisningen når den finnes. +detaljvisningen, for eksempel `/annonser/listing-001`. + +### Button + +Vanlig ` + + +``` + +Ikke lag knapper som later som de lagrer noe. Før backend finnes, skal en +handling tydelig vise at den er en demo. + +### TextField + +Tekstfelt med synlig etikett som også blir feltets tilgjengelige navn. +`hint` og `error` kobles til feltet med `aria-describedby`, og `error` setter +`aria-invalid`. Alle vanlige input-attributter sendes videre. + +```tsx + + + +``` + +Feltet lager sin egen `id`. Nedtrekkslister og tekstområder lages med vanlige +HTML-elementer når skjemaet trenger dem, med samme stil som `TextField`. + +### Layout + +Felles sidemal for alle sider. Den har en hopp-lenke til hovedinnholdet, +header med navigasjon til «Katalog» og «Lei ut verktøy», `
` og en +footer som sier at dataene er eksempler. Aktiv side markeres med fet tekst +og understrek, i tillegg til `aria-current="page"`. Ved sidebytte flyttes +fokus til `
`. + +Hver side setter fanetittel med `usePageTitle`: + +```tsx +usePageTitle('Katalog'); // «Katalog – Verktøyutlån Trondheim» +``` ## Ikke laget ennå -- Knapp, skjemafelt, sidemal og navigasjon kommer i egen oppgave. - Mørk modus er ikke planlagt for første innlevering. From 41bd5bd948b28421130c66095b4fc6157e77e251 Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 18:43:07 +0200 Subject: [PATCH 16/58] =?UTF-8?q?feat(src):=20gj=C3=B8r=20annonsekortet=20?= =?UTF-8?q?til=20en=20lenke=20til=20detaljvisningen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ListingCard/ListingCard.module.css | 33 +++++++++++++++++++ src/components/ListingCard/ListingCard.tsx | 12 +++++-- src/design/main.tsx | 7 +++- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/components/ListingCard/ListingCard.module.css b/src/components/ListingCard/ListingCard.module.css index daebee3..90446a9 100644 --- a/src/components/ListingCard/ListingCard.module.css +++ b/src/components/ListingCard/ListingCard.module.css @@ -1,4 +1,6 @@ .card { + /* Referansepunkt for lenkeflaten som dekker hele kortet. */ + position: relative; display: flex; flex-direction: column; overflow: hidden; @@ -7,6 +9,16 @@ border-radius: var(--radius-md); } +.card:hover { + border-color: var(--color-border-strong); +} + +/* Fokusringen tegnes rundt hele kortet, siden hele kortet er klikkbart. */ +.card:has(.link:focus-visible) { + outline: var(--focus-ring); + outline-offset: var(--focus-offset); +} + .image, .imagePlaceholder { width: 100%; @@ -41,6 +53,27 @@ overflow-wrap: anywhere; } +.link { + color: var(--color-text); + text-decoration: none; +} + +/* Strekker lenkeflaten over hele kortet. */ +.link::after { + content: ''; + position: absolute; + inset: 0; +} + +.link:focus-visible { + outline: none; +} + +.card:hover .link { + color: var(--color-accent); + text-decoration: underline; +} + .meta { margin: 0; color: var(--color-text-muted); diff --git a/src/components/ListingCard/ListingCard.tsx b/src/components/ListingCard/ListingCard.tsx index bbed0ae..2c18fac 100644 --- a/src/components/ListingCard/ListingCard.tsx +++ b/src/components/ListingCard/ListingCard.tsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router'; import type { Listing } from '../../types/listing.ts'; import { categoryLabels } from '../../utils/listingFormat.ts'; import PriceTag from '../PriceTag/PriceTag.tsx'; @@ -8,8 +9,9 @@ interface ListingCardProps { } /** - * Visuell referanse for et annonsekort. Katalogen kan bygge videre på denne, - * for eksempel ved å gjøre tittelen til en lenke til detaljvisningen. + * Annonsekort som åpner detaljvisningen. Selve lenken ligger på tittelen, men + * dekker hele kortet med CSS, så skjermlesere bare leser tittelen som lenketekst + * og tastaturbrukere får ett tabulatorstopp per kort. */ function ListingCard({ listing }: ListingCardProps) { return ( @@ -28,7 +30,11 @@ function ListingCard({ listing }: ListingCardProps) {
Ingen bilde
)}
-

{listing.title}

+

+ + {listing.title} + +

{categoryLabels[listing.category]} · {listing.area}

diff --git a/src/design/main.tsx b/src/design/main.tsx index 31a28db..d42065f 100644 --- a/src/design/main.tsx +++ b/src/design/main.tsx @@ -1,5 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { MemoryRouter } from 'react-router'; import '../styles/tokens.css'; import '../styles/base.css'; import DesignPreview from './DesignPreview.tsx'; @@ -10,8 +11,12 @@ if (!rootElement) { throw new Error('Fant ikke #root i design.html'); } +// Annonsekortene inneholder lenker, så forhåndsvisningen trenger en router. +// MemoryRouter holder lenkene inne i forhåndsvisningen. createRoot(rootElement).render( - + + + , ); From 3a864bd6df28f6b8a97780f5856463e3f0a569c2 Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 18:47:04 +0200 Subject: [PATCH 17/58] feat(src): lag rutenett for annonsekort --- .../ListingGrid/ListingGrid.module.css | 19 ++++++++++++++ src/components/ListingGrid/ListingGrid.tsx | 26 +++++++++++++++++++ src/design/DesignPreview.module.css | 6 ----- src/design/DesignPreview.tsx | 8 ++---- 4 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/components/ListingGrid/ListingGrid.module.css create mode 100644 src/components/ListingGrid/ListingGrid.tsx diff --git a/src/components/ListingGrid/ListingGrid.module.css b/src/components/ListingGrid/ListingGrid.module.css new file mode 100644 index 0000000..b64e1c4 --- /dev/null +++ b/src/components/ListingGrid/ListingGrid.module.css @@ -0,0 +1,19 @@ +.grid { + display: grid; + /* min() hindrer at kolonnene blir bredere enn skjermen på smale mobiler. */ + grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr)); + gap: var(--space-6); + margin: 0; + padding: 0; + list-style: none; +} + +/* Lar kortet fylle hele raden, slik at kortene i samme rad blir like høye. */ +.item { + display: flex; + flex-direction: column; +} + +.item > * { + flex: 1; +} diff --git a/src/components/ListingGrid/ListingGrid.tsx b/src/components/ListingGrid/ListingGrid.tsx new file mode 100644 index 0000000..96ab233 --- /dev/null +++ b/src/components/ListingGrid/ListingGrid.tsx @@ -0,0 +1,26 @@ +import type { Listing } from '../../types/listing.ts'; +import ListingCard from '../ListingCard/ListingCard.tsx'; +import styles from './ListingGrid.module.css'; + +interface ListingGridProps { + /** Annonsene som skal vises, i den rekkefølgen de skal stå. */ + listings: Listing[]; +} + +/** + * Responsivt rutenett med annonsekort. Rutenettet er en liste, slik at + * skjermlesere kan fortelle hvor mange annonser som vises. + */ +function ListingGrid({ listings }: ListingGridProps) { + return ( +
    + {listings.map((listing) => ( +
  • + +
  • + ))} +
+ ); +} + +export default ListingGrid; diff --git a/src/design/DesignPreview.module.css b/src/design/DesignPreview.module.css index ba7cae1..921cbf9 100644 --- a/src/design/DesignPreview.module.css +++ b/src/design/DesignPreview.module.css @@ -77,9 +77,3 @@ flex-direction: column; gap: var(--space-6); } - -.cardGrid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); - gap: var(--space-6); -} diff --git a/src/design/DesignPreview.tsx b/src/design/DesignPreview.tsx index 9e806da..06a294f 100644 --- a/src/design/DesignPreview.tsx +++ b/src/design/DesignPreview.tsx @@ -1,5 +1,5 @@ import Button from '../components/Button/Button.tsx'; -import ListingCard from '../components/ListingCard/ListingCard.tsx'; +import ListingGrid from '../components/ListingGrid/ListingGrid.tsx'; import PriceTag from '../components/PriceTag/PriceTag.tsx'; import TextField from '../components/TextField/TextField.tsx'; import { mockListings } from '../data/mockListings.ts'; @@ -130,11 +130,7 @@ function DesignPreview() {

Annonsekort

-
- {exampleListings.map((listing) => ( - - ))} -
+
); From 49419182520b697282a60c70e820455397f38043 Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 18:50:06 +0200 Subject: [PATCH 18/58] feat(pages): vis annonsekort i katalogen --- src/pages/CatalogPage/CatalogPage.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/pages/CatalogPage/CatalogPage.tsx b/src/pages/CatalogPage/CatalogPage.tsx index d31c47b..7b83bc3 100644 --- a/src/pages/CatalogPage/CatalogPage.tsx +++ b/src/pages/CatalogPage/CatalogPage.tsx @@ -1,10 +1,11 @@ -import { Link } from 'react-router'; +import ListingGrid from '../../components/ListingGrid/ListingGrid.tsx'; import { mockListings } from '../../data/mockListings.ts'; import { usePageTitle } from '../../hooks/usePageTitle.ts'; /** - * Plassholder for katalogen. Søk, filtre, sortering og annonsekort lages i - * egen oppgave. Lenkelisten gjør det mulig å åpne detaljvisningen imens. + * Katalog med alle annonsene. Siden henter dataene og sender dem videre som + * props, så bare denne filen må endres når mockdataene byttes ut med GraphQL. + * Søk, filtre og sortering lages i egne oppgaver. */ function CatalogPage() { usePageTitle('Katalog'); @@ -14,18 +15,9 @@ function CatalogPage() {

Finn verktøy

Lei eller lån verktøy av andre studenter og privatpersoner i Trondheim. - Her kommer katalogen med søk, filtre og sortering.

-

Eksempelannonser

-

Midlertidig liste til katalogen er på plass.

-
    - {mockListings.map((listing) => ( -
  • - {listing.title} -
  • - ))} -
+ ); } From 3bae1493833209a5b3c618068f5b1708efbe546f Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 19:58:00 +0200 Subject: [PATCH 19/58] docs(docs): dokumenter katalog og annonsekort --- README.md | 18 +++++++++--------- docs/design.md | 30 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2303283..4824a89 100644 --- a/README.md +++ b/README.md @@ -63,15 +63,15 @@ uten at visningene må skrives om. ## Sider og ruter -Rutene er definert i `src/App.tsx` med React Router. Alle sidene er -foreløpig plassholdere som fylles ut i egne oppgaver. - -| Rute | Side | Innhold nå | -| ---------------------- | ------------------- | ------------------------------------------ | -| `/` | `CatalogPage` | Plassholder og lenker til eksempelannonser | -| `/annonser/:listingId` | `ListingDetailPage` | Tittel, område og pris for annonsen | -| `/lei-ut` | `RentOutPage` | Plassholder for utleieskjema | -| Alle andre | `NotFoundPage` | «Fant ikke siden» | +Rutene er definert i `src/App.tsx` med React Router. Sidene som ikke er +ferdige, er plassholdere som fylles ut i egne oppgaver. + +| Rute | Side | Innhold nå | +| ---------------------- | ------------------- | ------------------------------------- | +| `/` | `CatalogPage` | Alle eksempelannonser som annonsekort | +| `/annonser/:listingId` | `ListingDetailPage` | Tittel, område og pris for annonsen | +| `/lei-ut` | `RentOutPage` | Plassholder for utleieskjema | +| Alle andre | `NotFoundPage` | «Fant ikke siden» | Ny side: lag en mappe i `src/pages/`, legg til en `Route` i `App.tsx` og kall `usePageTitle` i siden. Sidemalen (`Layout`) gir header, navigasjon og diff --git a/docs/design.md b/docs/design.md index 53fc73f..96938cd 100644 --- a/docs/design.md +++ b/docs/design.md @@ -103,16 +103,36 @@ bakgrunn, men teksten alene er nok til å skille dem. ### ListingCard -Visuell referanse for et annonsekort med bilde, tittel, kategori, område og -pris. Når `imageUrl` er `null`, vises en plassholder med teksten «Ingen bilde». -Bildet har tom alt-tekst fordi tittelen rett under beskriver det samme. +Annonsekort med bilde, tittel, kategori, område og pris. Når `imageUrl` er +`null`, vises en plassholder med teksten «Ingen bilde». Bildet har tom +alt-tekst fordi tittelen rett under beskriver det samme. ```tsx ``` -Tittelen er en `h2`. Katalogen kan gjøre tittelen til en lenke til -detaljvisningen, for eksempel `/annonser/listing-001`. +Tittelen er en `h2` med en lenke til detaljvisningen, for eksempel +`/annonser/listing-001`. Lenken dekker hele kortet med CSS, så hele kortet er +klikkbart. Likevel har hvert kort bare ett tabulatorstopp, og skjermlesere +leser bare tittelen som lenketekst. Fokusringen og hover-effekten tegnes rundt +hele kortet. + +Kortet bruker `Link` fra React Router og må derfor ligge inne i en router. +Designreferansen bruker `MemoryRouter` av samme grunn. + +### ListingGrid + +Responsivt rutenett med ett `ListingCard` per annonse. Annonsene vises i den +rekkefølgen de sendes inn, så søk, filtre og sortering gjøres før listen +sendes til rutenettet. + +```tsx + +``` + +Rutenettet er en `
    `, slik at skjermlesere kan si hvor mange annonser som +vises. Kolonnene er minst 16rem brede og fyller bredden. På smale skjermer blir +det én kolonne uten horisontal scrolling. Kortene i samme rad blir like høye. ### Button From e939db0c544f550929cf8c2ca84ef2ea6159c358 Mon Sep 17 00:00:00 2001 From: Sebastian Ingebrigtsen Date: Wed, 30 Sep 2026 22:07:59 +0200 Subject: [PATCH 20/58] feat(src): trekk ut annonsebilde med plassholder til egen komponent Co-authored-by: tordtst Co-authored-by: andretg Co-authored-by: birkjr --- .../ListingCard/ListingCard.module.css | 18 ----------- src/components/ListingCard/ListingCard.tsx | 15 ++------- .../ListingImage/ListingImage.module.css | 17 ++++++++++ src/components/ListingImage/ListingImage.tsx | 32 +++++++++++++++++++ 4 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 src/components/ListingImage/ListingImage.module.css create mode 100644 src/components/ListingImage/ListingImage.tsx diff --git a/src/components/ListingCard/ListingCard.module.css b/src/components/ListingCard/ListingCard.module.css index 90446a9..588cde6 100644 --- a/src/components/ListingCard/ListingCard.module.css +++ b/src/components/ListingCard/ListingCard.module.css @@ -19,24 +19,6 @@ outline-offset: var(--focus-offset); } -.image, -.imagePlaceholder { - width: 100%; - aspect-ratio: 4 / 3; -} - -.image { - object-fit: cover; -} - -.imagePlaceholder { - display: grid; - place-items: center; - background: var(--color-surface-muted); - color: var(--color-text-muted); - font-size: var(--font-size-sm); -} - .body { display: flex; flex: 1; diff --git a/src/components/ListingCard/ListingCard.tsx b/src/components/ListingCard/ListingCard.tsx index 2c18fac..6b65829 100644 --- a/src/components/ListingCard/ListingCard.tsx +++ b/src/components/ListingCard/ListingCard.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router'; import type { Listing } from '../../types/listing.ts'; import { categoryLabels } from '../../utils/listingFormat.ts'; +import ListingImage from '../ListingImage/ListingImage.tsx'; import PriceTag from '../PriceTag/PriceTag.tsx'; import styles from './ListingCard.module.css'; @@ -16,19 +17,7 @@ interface ListingCardProps { function ListingCard({ listing }: ListingCardProps) { return (
    - {listing.imageUrl ? ( - // Tom alt-tekst fordi tittelen rett under beskriver det samme. - - ) : ( -
    Ingen bilde
    - )} +

    diff --git a/src/components/ListingImage/ListingImage.module.css b/src/components/ListingImage/ListingImage.module.css new file mode 100644 index 0000000..14492bb --- /dev/null +++ b/src/components/ListingImage/ListingImage.module.css @@ -0,0 +1,17 @@ +.image, +.placeholder { + width: 100%; + aspect-ratio: 4 / 3; +} + +.image { + object-fit: cover; +} + +.placeholder { + display: grid; + place-items: center; + background: var(--color-surface-muted); + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} diff --git a/src/components/ListingImage/ListingImage.tsx b/src/components/ListingImage/ListingImage.tsx new file mode 100644 index 0000000..5475a0d --- /dev/null +++ b/src/components/ListingImage/ListingImage.tsx @@ -0,0 +1,32 @@ +import styles from './ListingImage.module.css'; + +interface ListingImageProps { + /** Adresse til bildet, eller null når annonsen mangler bilde. */ + imageUrl: string | null; + /** lazy på annonsekort, eager når bildet er det første brukeren ser. */ + loading?: 'lazy' | 'eager'; +} + +/** + * Annonsebilde i fast format 4:3, eller en nøytral plassholder når bildet + * mangler. Bildet har tom alt-tekst fordi tittelen alltid står i nærheten og + * beskriver det samme. + */ +function ListingImage({ imageUrl, loading = 'lazy' }: ListingImageProps) { + if (!imageUrl) { + return
    Ingen bilde
    ; + } + + return ( + + ); +} + +export default ListingImage; From fbe16260dc034a318fee611559c25836d82dc9a8 Mon Sep 17 00:00:00 2001 From: Sebastian Ingebrigtsen Date: Wed, 30 Sep 2026 22:08:26 +0200 Subject: [PATCH 21/58] =?UTF-8?q?feat(src):=20legg=20til=20hjelpefunksjone?= =?UTF-8?q?r=20for=20leieperiode=20og=20bel=C3=B8p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tordtst Co-authored-by: andretg Co-authored-by: birkjr --- src/utils/listingFormat.ts | 7 ++++- src/utils/rentalPeriod.ts | 63 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/utils/rentalPeriod.ts diff --git a/src/utils/listingFormat.ts b/src/utils/listingFormat.ts index 6150896..89ccd8b 100644 --- a/src/utils/listingFormat.ts +++ b/src/utils/listingFormat.ts @@ -19,10 +19,15 @@ export const conditionLabels: Record = { const priceFormatter = new Intl.NumberFormat('nb-NO'); +/** Viser et kronebeløp som «1 200 kr». */ +export function formatKroner(amount: number): string { + return `${priceFormatter.format(amount)} kr`; +} + /** Viser døgnpris som «150 kr/døgn», eller «Gratis utlån» når prisen er 0. */ export function formatDailyPrice(dailyPrice: number): string { if (dailyPrice === 0) { return 'Gratis utlån'; } - return `${priceFormatter.format(dailyPrice)} kr/døgn`; + return `${formatKroner(dailyPrice)}/døgn`; } diff --git a/src/utils/rentalPeriod.ts b/src/utils/rentalPeriod.ts new file mode 100644 index 0000000..4c84ddf --- /dev/null +++ b/src/utils/rentalPeriod.ts @@ -0,0 +1,63 @@ +/** + * Hjelpefunksjoner for leieperioden i en leieforespørsel. Datoene er strenger + * på formen «2026-10-05», slik de kommer fra . + */ + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +/** + * Rene datoer som «2026-10-05» leses som UTC-midnatt. Da gir sommertid ikke + * døgn på 23 eller 25 timer når to datoer trekkes fra hverandre. + */ +function parseDate(date: string): number { + return Date.parse(date); +} + +/** Dagens dato på samme form som datofeltene, i brukerens lokale tid. */ +export function todayAsDateString(): string { + const now = new Date(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${now.getFullYear()}-${month}-${day}`; +} + +/** + * Antall døgn fra henting til retur. Henting og retur samme dag regnes som + * ett døgn, slik at en leie aldri blir gratis ved en feil. + */ +export function countRentalDays(startDate: string, endDate: string): number { + const days = (parseDate(endDate) - parseDate(startDate)) / MS_PER_DAY; + return Math.max(1, Math.round(days)); +} + +export interface RentalPeriodErrors { + startDate?: string; + endDate?: string; +} + +/** + * Sjekker datoene i en leieforespørsel. Returnerer et tomt objekt når begge + * datoene er gyldige. Datostrengene kan sammenlignes direkte fordi formatet + * er år-måned-dag. + */ +export function validateRentalPeriod( + startDate: string, + endDate: string, + today: string, +): RentalPeriodErrors { + const errors: RentalPeriodErrors = {}; + + if (!startDate) { + errors.startDate = 'Velg når du vil hente verktøyet.'; + } else if (startDate < today) { + errors.startDate = 'Fradatoen kan ikke være tilbake i tid.'; + } + + if (!endDate) { + errors.endDate = 'Velg når du vil levere verktøyet tilbake.'; + } else if (startDate && endDate < startDate) { + errors.endDate = 'Tildatoen kan ikke være før fradatoen.'; + } + + return errors; +} From 72a2873f7d7435095258357b0eb2b5462289df8f Mon Sep 17 00:00:00 2001 From: Sebastian Ingebrigtsen Date: Wed, 30 Sep 2026 22:08:56 +0200 Subject: [PATCH 22/58] =?UTF-8?q?feat(src):=20lag=20skjema=20for=20leiefor?= =?UTF-8?q?esp=C3=B8rsel=20med=20datovalg=20og=20demomelding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tordtst Co-authored-by: andretg Co-authored-by: birkjr --- .../RentalRequestForm.module.css | 37 ++++++ .../RentalRequestForm/RentalRequestForm.tsx | 120 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/components/RentalRequestForm/RentalRequestForm.module.css create mode 100644 src/components/RentalRequestForm/RentalRequestForm.tsx diff --git a/src/components/RentalRequestForm/RentalRequestForm.module.css b/src/components/RentalRequestForm/RentalRequestForm.module.css new file mode 100644 index 0000000..572981e --- /dev/null +++ b/src/components/RentalRequestForm/RentalRequestForm.module.css @@ -0,0 +1,37 @@ +.form { + display: flex; + flex-direction: column; + gap: var(--space-4); +} + +/* Datofeltene står ved siden av hverandre når det er plass. */ +.dates { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: var(--space-4); +} + +.summary { + margin: 0; + padding: var(--space-3) var(--space-4); + background: var(--color-surface-muted); + border-radius: var(--radius-md); +} + +.demoNote { + margin: 0; + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} + +.status { + margin: 0; +} + +/* Bekreftelsen får ramme bare når den har innhold. */ +.status:not(:empty) { + padding: var(--space-3) var(--space-4); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + background: var(--color-surface); +} diff --git a/src/components/RentalRequestForm/RentalRequestForm.tsx b/src/components/RentalRequestForm/RentalRequestForm.tsx new file mode 100644 index 0000000..06fa38a --- /dev/null +++ b/src/components/RentalRequestForm/RentalRequestForm.tsx @@ -0,0 +1,120 @@ +import { useState, type SubmitEvent } from 'react'; +import { formatKroner } from '../../utils/listingFormat.ts'; +import { + countRentalDays, + todayAsDateString, + validateRentalPeriod, +} from '../../utils/rentalPeriod.ts'; +import Button from '../Button/Button.tsx'; +import TextField from '../TextField/TextField.tsx'; +import styles from './RentalRequestForm.module.css'; + +interface RentalRequestFormProps { + /** Pris i hele kroner per døgn. 0 betyr gratis utlån. */ + dailyPrice: number; +} + +/** + * Skjema for å velge leieperiode og sende leieforespørsel. Før backend finnes + * er innsendingen en demo: ingenting sendes, lagres eller reserveres, og det + * sier både knappen og bekreftelsen tydelig. + */ +function RentalRequestForm({ dailyPrice }: RentalRequestFormProps) { + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + // Feil vises først etter et forsøk på innsending, så brukeren ikke får + // feilmeldinger mens hen fyller ut feltene for første gang. + const [hasTriedSubmit, setHasTriedSubmit] = useState(false); + const [isDemoSent, setIsDemoSent] = useState(false); + + const today = todayAsDateString(); + const errors = validateRentalPeriod(startDate, endDate, today); + const isValid = !errors.startDate && !errors.endDate; + const visibleErrors = hasTriedSubmit ? errors : {}; + + function handleSubmit(event: SubmitEvent) { + event.preventDefault(); + setHasTriedSubmit(true); + setIsDemoSent(isValid); + } + + return ( +
    +
    + { + setStartDate(event.target.value); + setIsDemoSent(false); + }} + /> + { + setEndDate(event.target.value); + setIsDemoSent(false); + }} + /> +
    + + {isValid && ( + + )} + +

    + Dette er en demo. Forespørselen blir ikke sendt, og verktøyet blir ikke + reservert. +

    +
    + +
    + + {/* Alltid i DOM-en, slik at skjermlesere leser opp ny tekst i den. */} +

    + {isDemoSent && + 'Demo: Ingen forespørsel er sendt, og ingenting er lagret. Ekte leieforespørsler kommer når backend er på plass.'} +

    + + ); +} + +interface RentalSummaryProps { + days: number; + dailyPrice: number; +} + +function RentalSummary({ days, dailyPrice }: RentalSummaryProps) { + const dayText = days === 1 ? '1 døgn' : `${days} døgn`; + + return ( +

    + {dailyPrice === 0 ? ( + <> + {dayText} · Gratis utlån, det koster ingenting. + + ) : ( + <> + {dayText} × {formatKroner(dailyPrice)} ={' '} + {formatKroner(days * dailyPrice)} + + )} +

    + ); +} + +export default RentalRequestForm; From a36f235bab79b0348165b908b9f273873ab83f7e Mon Sep 17 00:00:00 2001 From: Sebastian Ingebrigtsen Date: Wed, 30 Sep 2026 22:09:54 +0200 Subject: [PATCH 23/58] =?UTF-8?q?feat(pages):=20vis=20alle=20annonseopplys?= =?UTF-8?q?ninger=20og=20leieforesp=C3=B8rsel=20i=20detaljvisningen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tordtst Co-authored-by: andretg Co-authored-by: birkjr --- .../ListingDetailPage.module.css | 72 +++++++++++++++++++ .../ListingDetailPage/ListingDetailPage.tsx | 71 +++++++++++++++--- 2 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 src/pages/ListingDetailPage/ListingDetailPage.module.css diff --git a/src/pages/ListingDetailPage/ListingDetailPage.module.css b/src/pages/ListingDetailPage/ListingDetailPage.module.css new file mode 100644 index 0000000..95a8ff2 --- /dev/null +++ b/src/pages/ListingDetailPage/ListingDetailPage.module.css @@ -0,0 +1,72 @@ +/* Bilde og opplysninger under hverandre på mobil, side om side på bred skjerm. */ +.listing { + display: grid; + gap: var(--space-6); + margin-bottom: var(--space-8); +} + +@media (min-width: 48rem) { + .listing { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + align-items: start; + } +} + +.imageWrapper { + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-4); +} + +.title { + margin: 0; + /* Lange titler brytes i stedet for å gi horisontal scrolling. */ + overflow-wrap: anywhere; +} + +.facts { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); + gap: var(--space-4); + width: 100%; + margin: 0; +} + +.facts dt { + color: var(--color-text-muted); + font-size: var(--font-size-sm); +} + +.facts dd { + margin: 0; + font-weight: var(--font-weight-bold); +} + +.subheading { + margin: 0; + font-size: var(--font-size-lg); +} + +.info p { + margin: 0; +} + +.accessories { + margin: 0; + padding-left: var(--space-6); +} + +.request { + max-width: 40rem; + padding: var(--space-6); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} diff --git a/src/pages/ListingDetailPage/ListingDetailPage.tsx b/src/pages/ListingDetailPage/ListingDetailPage.tsx index 0e97456..e93c23c 100644 --- a/src/pages/ListingDetailPage/ListingDetailPage.tsx +++ b/src/pages/ListingDetailPage/ListingDetailPage.tsx @@ -1,11 +1,16 @@ import { Link, useParams } from 'react-router'; +import ListingImage from '../../components/ListingImage/ListingImage.tsx'; import PriceTag from '../../components/PriceTag/PriceTag.tsx'; +import RentalRequestForm from '../../components/RentalRequestForm/RentalRequestForm.tsx'; import { mockListings } from '../../data/mockListings.ts'; import { usePageTitle } from '../../hooks/usePageTitle.ts'; +import { categoryLabels, conditionLabels } from '../../utils/listingFormat.ts'; +import styles from './ListingDetailPage.module.css'; /** - * Plassholder for detaljvisningen. Beskrivelse, tilstand, tilbehør og - * leieforespørsel lages i egen oppgave. + * Detaljvisning for én annonse, med alle opplysninger og skjema for + * leieforespørsel. Siden henter annonsen selv, så bare denne filen må endres + * når mockdataene byttes ut med GraphQL. */ function ListingDetailPage() { const { listingId } = useParams(); @@ -29,15 +34,59 @@ function ListingDetailPage() {

    Tilbake til katalogen

    -

    {listing.title}

    -

    Område: {listing.area}

    -

    - -

    -

    - Her kommer detaljvisningen med bilde, beskrivelse, tilstand, tilbehør og - leieforespørsel. -

    + +
    +
    + +
    + +
    +

    {listing.title}

    + + +
    +
    +
    Kategori
    +
    {categoryLabels[listing.category]}
    +
    +
    +
    Område
    +
    {listing.area}
    +
    +
    +
    Tilstand
    +
    {conditionLabels[listing.condition]}
    +
    +
    + +

    Beskrivelse

    +

    {listing.description}

    + +

    Tilbehør

    + {listing.accessories.length > 0 ? ( +
      + {listing.accessories.map((accessory) => ( +
    • {accessory}
    • + ))} +
    + ) : ( +

    Ingen tilbehør følger med.

    + )} +
    +
    + +
    +

    Send leieforespørsel

    +

    + Velg når du vil hente og levere verktøyet. Eieren godkjenner eller + avslår forespørselen. +

    + {/* key nullstiller skjemaet når brukeren går til en annen annonse. */} + +
    ); } From 13c7b57b20ec0e4694ac820b50d3472f36590f54 Mon Sep 17 00:00:00 2001 From: Sebastian Ingebrigtsen Date: Wed, 30 Sep 2026 22:10:16 +0200 Subject: [PATCH 24/58] =?UTF-8?q?docs(docs):=20dokumenter=20detaljvisning,?= =?UTF-8?q?=20annonsebilde=20og=20leieforesp=C3=B8rsel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tordtst Co-authored-by: andretg Co-authored-by: birkjr --- README.md | 22 +++++++++++++--------- docs/design.md | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4824a89..1155ece 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,12 @@ uten at visningene må skrives om. Rutene er definert i `src/App.tsx` med React Router. Sidene som ikke er ferdige, er plassholdere som fylles ut i egne oppgaver. -| Rute | Side | Innhold nå | -| ---------------------- | ------------------- | ------------------------------------- | -| `/` | `CatalogPage` | Alle eksempelannonser som annonsekort | -| `/annonser/:listingId` | `ListingDetailPage` | Tittel, område og pris for annonsen | -| `/lei-ut` | `RentOutPage` | Plassholder for utleieskjema | -| Alle andre | `NotFoundPage` | «Fant ikke siden» | +| Rute | Side | Innhold nå | +| ---------------------- | ------------------- | ------------------------------------------- | +| `/` | `CatalogPage` | Alle eksempelannonser som annonsekort | +| `/annonser/:listingId` | `ListingDetailPage` | Alle opplysninger og leieforespørsel (demo) | +| `/lei-ut` | `RentOutPage` | Plassholder for utleieskjema | +| Alle andre | `NotFoundPage` | «Fant ikke siden» | Ny side: lag en mappe i `src/pages/`, legg til en `Route` i `App.tsx` og kall `usePageTitle` i siden. Sidemalen (`Layout`) gir header, navigasjon og @@ -102,9 +102,13 @@ Alle annonser følger typen `Listing` i `src/types/listing.ts`: Kategori og tilstand lagres som engelske nøkler slik at de senere kan bli GraphQL-enums. `src/utils/listingFormat.ts` gir de norske visningsnavnene og -`formatDailyPrice`, som viser `150 kr/døgn` eller `Gratis utlån`. Bruk disse i -stedet for å formatere på nytt i hver komponent. Når `imageUrl` er `null`, skal -visningen vise en nøytral plassholder. +`formatDailyPrice`, som viser `150 kr/døgn` eller `Gratis utlån`, og +`formatKroner` for vanlige beløp. Bruk disse i +stedet for å formatere på nytt i hver komponent. Når `imageUrl` er `null`, viser +`ListingImage` en nøytral plassholder. + +`src/utils/rentalPeriod.ts` regner ut antall døgn og sjekker datoene i en +leieforespørsel. Henting og retur samme dag regnes som ett døgn. `src/data/mockListings.ts` har 10 oppdiktede eksempelannonser. De er ikke ekte tilbud og inneholder ingen personopplysninger. Illustrasjonene i diff --git a/docs/design.md b/docs/design.md index 96938cd..1bcb988 100644 --- a/docs/design.md +++ b/docs/design.md @@ -103,9 +103,8 @@ bakgrunn, men teksten alene er nok til å skille dem. ### ListingCard -Annonsekort med bilde, tittel, kategori, område og pris. Når `imageUrl` er -`null`, vises en plassholder med teksten «Ingen bilde». Bildet har tom -alt-tekst fordi tittelen rett under beskriver det samme. +Annonsekort med bilde, tittel, kategori, område og pris. Bildet vises med +`ListingImage`. ```tsx @@ -120,6 +119,34 @@ hele kortet. Kortet bruker `Link` fra React Router og må derfor ligge inne i en router. Designreferansen bruker `MemoryRouter` av samme grunn. +### ListingImage + +Annonsebilde i fast format 4:3. Når `imageUrl` er `null`, vises en plassholder +med teksten «Ingen bilde». Bildet har tom alt-tekst fordi tittelen alltid står +i nærheten og beskriver det samme. Bruk `loading="eager"` når bildet er det +første brukeren ser, som på detaljvisningen. + +```tsx + + +``` + +### RentalRequestForm + +Skjema for leieforespørsel med fra- og tildato. Når datoene er gyldige, vises +antall døgn og totalpris, eller at gratis utlån ikke koster noe. Feil vises +ved feltet først etter et forsøk på innsending. Fradatoen kan ikke være +tilbake i tid, og tildatoen kan ikke være før fradatoen. + +```tsx + +``` + +Innsendingen er en demo. Knappen heter «Send forespørsel (demo)», og +bekreftelsen sier at ingenting er sendt eller lagret. Bekreftelsen ligger i et +`role="status"`-område, så skjermlesere leser den opp. Bruk `key`, slik at +skjemaet nullstilles når brukeren går til en annen annonse. + ### ListingGrid Responsivt rutenett med ett `ListingCard` per annonse. Annonsene vises i den From 48d88f9956669f02281dd6ee60e8f14fe7f9417a Mon Sep 17 00:00:00 2001 From: Sebastian Ingebrigtsen Date: Wed, 30 Sep 2026 22:21:00 +0200 Subject: [PATCH 25/58] =?UTF-8?q?feat(src):=20lag=20nedtrekksliste=20og=20?= =?UTF-8?q?tekstomr=C3=A5de=20med=20samme=20stil=20som=20tekstfelt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tordtst Co-authored-by: andretg Co-authored-by: birkjr --- src/components/SelectField/SelectField.tsx | 77 +++++++++++++++++++ .../TextAreaField/TextAreaField.module.css | 4 + .../TextAreaField/TextAreaField.tsx | 62 +++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 src/components/SelectField/SelectField.tsx create mode 100644 src/components/TextAreaField/TextAreaField.module.css create mode 100644 src/components/TextAreaField/TextAreaField.tsx diff --git a/src/components/SelectField/SelectField.tsx b/src/components/SelectField/SelectField.tsx new file mode 100644 index 0000000..a4f0474 --- /dev/null +++ b/src/components/SelectField/SelectField.tsx @@ -0,0 +1,77 @@ +import { useId, type SelectHTMLAttributes } from 'react'; +// Samme stil som TextField, slik at alle skjemafelt ser like ut. +import styles from '../TextField/TextField.module.css'; + +interface SelectOption { + value: string; + label: string; +} + +interface SelectFieldProps extends Omit< + SelectHTMLAttributes, + 'id' | 'children' +> { + /** Synlig etikett. Blir også feltets tilgjengelige navn. */ + label: string; + options: SelectOption[]; + /** Tekst for det tomme valget øverst, for eksempel «Velg kategori». */ + placeholder?: string; + /** Kort hjelpetekst under etiketten. */ + hint?: string; + /** Feilmelding. Når den finnes, markeres feltet som ugyldig. */ + error?: string; +} + +/** + * Nedtrekksliste med synlig etikett, valgfri hjelpetekst og feilmelding. + * Fungerer som TextField: hjelpetekst og feil kobles til feltet med + * aria-describedby. + */ +function SelectField({ + label, + options, + placeholder, + hint, + error, + ...selectProps +}: SelectFieldProps) { + const id = useId(); + const hintId = `${id}-hint`; + const errorId = `${id}-error`; + const describedBy = + [hint && hintId, error && errorId].filter(Boolean).join(' ') || undefined; + + return ( +
    + + {hint && ( +

    + {hint} +

    + )} + + {error && ( +

    + {error} +

    + )} +
    + ); +} + +export default SelectField; diff --git a/src/components/TextAreaField/TextAreaField.module.css b/src/components/TextAreaField/TextAreaField.module.css new file mode 100644 index 0000000..7b8e1ea --- /dev/null +++ b/src/components/TextAreaField/TextAreaField.module.css @@ -0,0 +1,4 @@ +/* Brukeren kan gjøre feltet høyere, men ikke bredere enn skjemaet. */ +.textArea { + resize: vertical; +} diff --git a/src/components/TextAreaField/TextAreaField.tsx b/src/components/TextAreaField/TextAreaField.tsx new file mode 100644 index 0000000..dd05e09 --- /dev/null +++ b/src/components/TextAreaField/TextAreaField.tsx @@ -0,0 +1,62 @@ +import { useId, type TextareaHTMLAttributes } from 'react'; +// Samme stil som TextField, slik at alle skjemafelt ser like ut. +import textFieldStyles from '../TextField/TextField.module.css'; +import styles from './TextAreaField.module.css'; + +interface TextAreaFieldProps extends Omit< + TextareaHTMLAttributes, + 'id' +> { + /** Synlig etikett. Blir også feltets tilgjengelige navn. */ + label: string; + /** Kort hjelpetekst under etiketten. */ + hint?: string; + /** Feilmelding. Når den finnes, markeres feltet som ugyldig. */ + error?: string; +} + +/** + * Tekstområde for lengre tekst, med etikett, hjelpetekst og feilmelding på + * samme måte som TextField. + */ +function TextAreaField({ + label, + hint, + error, + rows = 5, + ...textAreaProps +}: TextAreaFieldProps) { + const id = useId(); + const hintId = `${id}-hint`; + const errorId = `${id}-error`; + const describedBy = + [hint && hintId, error && errorId].filter(Boolean).join(' ') || undefined; + + return ( +
    + + {hint && ( +

    + {hint} +

    + )} +