From 0b13d7d098eb7aaae331b3ff6a49679d3ece422d Mon Sep 17 00:00:00 2001 From: sofiesty Date: Thu, 15 Oct 2026 13:15:15 +0200 Subject: [PATCH 1/8] Sett opp ESLint og Prettier i hele prosjektet --- .prettierrc.json | 6 + .vscode/extensions.json | 3 + .vscode/settings.json | 11 + README.md | 22 +- backend/.prettierignore | 5 + backend/eslint.config.js | 21 + backend/package-lock.json | 1626 ++++++++++++++++++++----- backend/package.json | 13 +- backend/scripts/import.ts | 166 ++- backend/src/db.ts | 6 +- backend/src/index.ts | 64 +- client/.prettierignore | 3 + client/README.md | 10 +- client/eslint.config.js | 20 +- client/package-lock.json | 34 + client/package.json | 6 +- client/src/apollo/client.ts | 2 +- client/src/components/WineCatalog.tsx | 30 +- client/src/graphql/queries.ts | 37 + client/src/index.css | 7 +- client/vite.config.ts | 6 +- 21 files changed, 1619 insertions(+), 479 deletions(-) create mode 100644 .prettierrc.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 backend/.prettierignore create mode 100644 backend/eslint.config.js create mode 100644 client/.prettierignore create mode 100644 client/src/graphql/queries.ts diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ebd8881 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d7df89c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..519b35f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.workingDirectories": ["./client", "./backend"], + "[markdown]": { + "editor.formatOnSave": false + } +} diff --git a/README.md b/README.md index 43e1a49..cb2221d 100644 --- a/README.md +++ b/README.md @@ -412,7 +412,7 @@ type WineMatch { | Backend | Node.js, TypeScript, Apollo Server | GraphQL er krav i oppgaven, og Apollo Server er godt dokumentert | | Database | PostgreSQL | Relasjonsdatabase som passer godt til viner, brukere og favoritter. Støtter også effektiv søking, filtrering og sortering | | Testing | Vitest | Vitest er krav i oppgaven | -| Kodekvalitet | oxlint og Prettier | Linting og felles formatering | +| Kodekvalitet | ESLint og Prettier | ESLint finner feil og feil bruk av React-hooks, og Prettier gir lik formatering i hele prosjektet. ESLint fulgte med Vite-malen, så vi bruker det samme oppsettet i backend | @@ -553,6 +553,26 @@ npm run dev Åpne http://localhost:5173/project2/. Frontend henter data fra http://localhost:3001/. En annen adresse kan settes med `VITE_GRAPHQL_URL` (se `client/.env.example`). +### Linting og formatering + +Hele prosjektet bruker ESLint og Prettier, med samme oppsett i `client/` og `backend/`: + +- **ESLint** finner feil og risikable mønstre i koden, for eksempel ubrukte variabler og feil bruk av React-hooks. Oppsettet ligger i `eslint.config.js` i hver mappe. +- **Prettier** formaterer koden likt for alle, så kodegjennomgang handler om innholdet og ikke om mellomrom og anførselstegn. Reglene ligger i `.prettierrc.json` i rotmappen og gjelder begge mappene. +- **eslint-config-prettier** slår av ESLint-reglene som ellers ville vært uenige med Prettier. + +Kommandoene kjøres i `client/` eller `backend/`: + +| Kommando | Hva den gjør | +|---|---| +| `npm run lint` | Finner feil med ESLint | +| `npm run format` | Formaterer alle filene med Prettier | +| `npm run format:check` | Sjekker formateringen uten å endre noe | + +Repoet har VS Code-innstillinger i `.vscode/` som formaterer med Prettier hver gang du lagrer, og retter det ESLint kan rette automatisk. VS Code foreslår utvidelsene Prettier og ESLint når du åpner prosjektet. Kjør `npm run lint` og `npm run format:check` i begge mappene før du lager en pull request. + +Backend bruker TypeScript 6, samme versjon som klienten, fordi typescript-eslint ennå ikke støtter TypeScript 7. + ### Kjøring på VM-en Applikasjonen kjører på http://it2810-01.idi.ntnu.no/project2, og GraphQL-backend på http://it2810-01.idi.ntnu.no:3001/. VM-en kan bare nås fra NTNU-nettet eller via VPN. Hvilken commit som kjører, står i http://it2810-01.idi.ntnu.no/project2/version.txt. diff --git a/backend/.prettierignore b/backend/.prettierignore new file mode 100644 index 0000000..0cb1fb6 --- /dev/null +++ b/backend/.prettierignore @@ -0,0 +1,5 @@ +# Build output and the X-Wines data are not our code. +dist +data +# Generated by npm, so it should not be reformatted. +package-lock.json diff --git a/backend/eslint.config.js b/backend/eslint.config.js new file mode 100644 index 0000000..689f5d8 --- /dev/null +++ b/backend/eslint.config.js @@ -0,0 +1,21 @@ +// ESLint finds bugs and risky patterns in the backend code. +// Formatting is left to Prettier: eslint-config-prettier (last in the list) +// turns off the ESLint rules that would disagree with it. +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier/flat'; +import { defineConfig, globalIgnores } from 'eslint/config'; + +export default defineConfig([ + globalIgnores(['dist', 'data']), + { + files: ['**/*.ts'], + extends: [js.configs.recommended, tseslint.configs.recommended], + languageOptions: { + // The backend runs in Node.js, not in a browser + globals: globals.node, + }, + }, + eslintConfigPrettier, +]); diff --git a/backend/package-lock.json b/backend/package-lock.json index 1e0229a..a26751f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,10 +16,16 @@ "pg": "^8.23.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^26.6.4", "@types/pg": "^8.23.1", + "eslint": "^10.12.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.13.0", + "prettier": "^3.9.10", "tsx": "^4.23.15", - "typescript": "^7.0.2" + "typescript": "~6.0.2", + "typescript-eslint": "^8.71.2" } }, "node_modules/@apollo/cache-control-types": { @@ -259,6 +265,30 @@ "node": ">=16" } }, + "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": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "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", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -701,6 +731,134 @@ "node": ">=18" } }, + "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": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.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/@graphql-tools/merge": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.2.6.tgz", @@ -761,6 +919,96 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "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/@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/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -824,6 +1072,27 @@ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, + "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/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -852,356 +1121,286 @@ "pg-types": "^2.2.0" } }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=16.20.0" + "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.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">= 4" } }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, "engines": { - "node": ">=16.20.0" + "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/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, "engines": { - "node": ">=16.20.0" + "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/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "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/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=16.20.0" + "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/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=16.20.0" + "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/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" + }, "engines": { - "node": ">=16.20.0" + "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/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], + "node_modules/@whatwg-node/promise-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-2.0.0.tgz", + "integrity": "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, "engines": { - "node": ">=16.20.0" + "node": ">=22.15.0" } }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=16.20.0" + "node": ">=0.4.0" } }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], + "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": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@whatwg-node/promise-helpers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-2.0.0.tgz", - "integrity": "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ==", "license": "MIT", "dependencies": { - "tslib": "^2.6.3" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=22.15.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/async-retry": { @@ -1228,6 +1427,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -1265,6 +1474,19 @@ "url": "https://opencollective.com/express" } }, + "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/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1274,6 +1496,20 @@ "node": ">= 0.8" } }, + "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/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -1359,6 +1595,21 @@ "node": ">=16.0.0" } }, + "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/csv-parse": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.3.tgz", @@ -1382,6 +1633,13 @@ } } }, + "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/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1530,6 +1788,239 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "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.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", + "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-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/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/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1551,6 +2042,42 @@ "url": "https://opencollective.com/express" } }, + "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/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -1614,17 +2141,43 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/gopd": { @@ -1687,6 +2240,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -1699,6 +2265,13 @@ "node": ">= 0.4" } }, + "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/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1735,6 +2308,26 @@ "url": "https://opencollective.com/express" } }, + "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1753,6 +2346,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -1774,6 +2390,67 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, + "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/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/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "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": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -1855,12 +2532,35 @@ "url": "https://opencollective.com/express" } }, + "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==", "license": "MIT" }, + "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/negotiator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", @@ -1923,6 +2623,56 @@ "node": ">= 0.8" } }, + "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/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1932,6 +2682,26 @@ "node": ">= 0.8" } }, + "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/pg": { "version": "8.23.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", @@ -2021,6 +2791,19 @@ "split2": "^4.1.0" } }, + "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/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -2069,6 +2852,62 @@ "node": ">=0.10.0" } }, + "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.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "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/qs": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", @@ -2135,6 +2974,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "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/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2178,6 +3030,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -2268,6 +3143,23 @@ "node": ">= 0.8" } }, + "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/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -2291,6 +3183,19 @@ "node": ">=0.6" } }, + "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/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2316,6 +3221,19 @@ "fsevents": "~2.3.3" } }, + "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/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -2362,38 +3280,41 @@ } }, "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "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" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16.20.0" + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" + "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/undici-types": { @@ -2412,6 +3333,16 @@ "node": ">= 0.8" } }, + "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/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2430,6 +3361,22 @@ "node": ">=18" } }, + "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/which-typed-array": { "version": "1.1.24", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", @@ -2451,6 +3398,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -2459,6 +3416,19 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/backend/package.json b/backend/package.json index 1abc594..b728e05 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,10 @@ "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", - "import": "tsx scripts/import.ts" + "import": "tsx scripts/import.ts", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "keywords": [], "author": "", @@ -21,9 +24,15 @@ "pg": "^8.23.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^26.6.4", "@types/pg": "^8.23.1", + "eslint": "^10.12.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.13.0", + "prettier": "^3.9.10", "tsx": "^4.23.15", - "typescript": "^7.0.2" + "typescript": "~6.0.2", + "typescript-eslint": "^8.71.2" } } diff --git a/backend/scripts/import.ts b/backend/scripts/import.ts index bfacfe3..962814b 100644 --- a/backend/scripts/import.ts +++ b/backend/scripts/import.ts @@ -6,25 +6,25 @@ // // Skriptet kan kjøres flere ganger. Viner oppdateres, og favoritter beholdes. -import { createReadStream, readFileSync } from "node:fs"; -import { parse } from "csv-parse"; -import type { PoolClient } from "pg"; -import { pool } from "../src/db.js"; +import { createReadStream, readFileSync } from 'node:fs'; +import { parse } from 'csv-parse'; +import type { PoolClient } from 'pg'; +import { pool } from '../src/db.js'; -const WINES_FILE = process.argv[2] ?? "data/XWines_Test_100_wines.csv"; -const RATINGS_FILE = process.argv[3] ?? "data/XWines_Test_1K_ratings.csv"; +const WINES_FILE = process.argv[2] ?? 'data/XWines_Test_100_wines.csv'; +const RATINGS_FILE = process.argv[3] ?? 'data/XWines_Test_1K_ratings.csv'; const BATCH_SIZE = 1000; -const FOOD_CATEGORIES_FILE = "db/seeds/food_categories.sql"; +const FOOD_CATEGORIES_FILE = 'db/seeds/food_categories.sql'; const CURRENT_YEAR = new Date().getFullYear(); const BODY_LEVELS = new Set([ - "Very light-bodied", - "Light-bodied", - "Medium-bodied", - "Full-bodied", - "Very full-bodied", + 'Very light-bodied', + 'Light-bodied', + 'Medium-bodied', + 'Full-bodied', + 'Very full-bodied', ]); -const ACIDITY_LEVELS = new Set(["Low", "Medium", "High"]); +const ACIDITY_LEVELS = new Set(['Low', 'Medium', 'High']); type CsvRow = Record; @@ -48,16 +48,17 @@ interface Wine { vintages: number[]; } -type Validation = { ok: true; wine: Wine; warnings: string[] } | { ok: false; reason: string }; +type Validation = + { ok: true; wine: Wine; warnings: string[] } | { ok: false; reason: string }; // ---------- Hjelpefunksjoner for rensing ---------- function text(value: string | undefined): string { - return (value ?? "").trim(); + return (value ?? '').trim(); } function orNull(value: string): string | null { - return value === "" ? null : value; + return value === '' ? null : value; } function toId(value: string | undefined): number | null { @@ -69,8 +70,8 @@ function toId(value: string | undefined): number | null { function parseList(value: string): string[] { const items: string[] = []; for (const match of value.matchAll(/'([^']*)'|"([^"]*)"/g)) { - const item = (match[1] ?? match[2] ?? "").trim(); - if (item !== "") items.push(item); + const item = (match[1] ?? match[2] ?? '').trim(); + if (item !== '') items.push(item); } return [...new Set(items)]; } @@ -90,45 +91,45 @@ function validateWine(row: CsvRow): Validation { const warnings: string[] = []; const id = toId(row.WineID); - if (id === null) return fail("ugyldig WineID"); + if (id === null) return fail('ugyldig WineID'); const name = text(row.WineName); - if (name === "") return fail("mangler WineName"); + if (name === '') return fail('mangler WineName'); const type = text(row.Type); - if (type === "") return fail("mangler Type"); + if (type === '') return fail('mangler Type'); const wineryId = toId(row.WineryID); const wineryName = text(row.WineryName); - if (wineryId === null || wineryName === "") return fail("mangler produsent"); + if (wineryId === null || wineryName === '') return fail('mangler produsent'); const regionId = toId(row.RegionID); const regionName = text(row.RegionName); - if (regionId === null || regionName === "") return fail("mangler region"); + if (regionId === null || regionName === '') return fail('mangler region'); const countryCode = text(row.Code).toUpperCase(); const countryName = text(row.Country); - if (countryCode === "" || countryName === "") return fail("mangler land"); + if (countryCode === '' || countryName === '') return fail('mangler land'); // Valgfrie felt: ugyldig verdi blir NULL, vinen beholdes. const abvText = text(row.ABV); const abvNumber = Number(abvText); let abv: number | null = null; - if (abvText !== "") { + if (abvText !== '') { if (Number.isFinite(abvNumber) && abvNumber > 0 && abvNumber <= 50) { abv = Math.round(abvNumber * 10) / 10; } else { - warnings.push("ugyldig ABV"); + warnings.push('ugyldig ABV'); } } const bodyText = text(row.Body); const body = BODY_LEVELS.has(bodyText) ? bodyText : null; - if (bodyText !== "" && body === null) warnings.push("ugyldig Body"); + if (bodyText !== '' && body === null) warnings.push('ugyldig Body'); const acidityText = text(row.Acidity); const acidity = ACIDITY_LEVELS.has(acidityText) ? acidityText : null; - if (acidityText !== "" && acidity === null) warnings.push("ugyldig Acidity"); + if (acidityText !== '' && acidity === null) warnings.push('ugyldig Acidity'); return { ok: true, @@ -169,15 +170,21 @@ async function readWines() { const examples: string[] = []; let line = 1; // linje 1 er overskriften - const parser = createReadStream(WINES_FILE).pipe(parse({ columns: true, relax_column_count: true })); + const parser = createReadStream(WINES_FILE).pipe( + parse({ columns: true, relax_column_count: true }), + ); for await (const row of parser as AsyncIterable) { line++; let result = validateWine(row); - if (result.ok && seenIds.has(result.wine.id)) result = { ok: false, reason: "duplikat WineID" }; + if (result.ok && seenIds.has(result.wine.id)) + result = { ok: false, reason: 'duplikat WineID' }; if (!result.ok) { countReason(skipped, result.reason); - if (examples.length < 10) examples.push(`linje ${line}: ${result.reason} (WineID=${text(row.WineID) || "tom"})`); + if (examples.length < 10) + examples.push( + `linje ${line}: ${result.reason} (WineID=${text(row.WineID) || 'tom'})`, + ); continue; } @@ -194,20 +201,23 @@ async function readRatings(wineIds: Set) { const skipped = new Map(); let read = 0; - const parser = createReadStream(RATINGS_FILE).pipe(parse({ columns: true, relax_column_count: true })); + const parser = createReadStream(RATINGS_FILE).pipe( + parse({ columns: true, relax_column_count: true }), + ); for await (const row of parser as AsyncIterable) { read++; - if (read % 1_000_000 === 0) console.log(` ... ${read.toLocaleString("nb-NO")} vurderinger lest`); + if (read % 1_000_000 === 0) + console.log(` ... ${read.toLocaleString('nb-NO')} vurderinger lest`); const wineId = toId(row.WineID); const rating = Number(text(row.Rating)); if (wineId === null || !wineIds.has(wineId)) { - countReason(skipped, "ukjent vin"); + countReason(skipped, 'ukjent vin'); continue; } if (!Number.isFinite(rating) || rating < 1 || rating > 5) { - countReason(skipped, "ugyldig rating"); + countReason(skipped, 'ugyldig rating'); continue; } @@ -223,7 +233,11 @@ async function readRatings(wineIds: Set) { // ---------- Skriving til databasen ---------- // Sender radene som JSON i bolker på BATCH_SIZE, i stedet for én spørring per rad. -async function insertInBatches(client: PoolClient, sql: string, rows: object[]) { +async function insertInBatches( + client: PoolClient, + sql: string, + rows: object[], +) { for (let i = 0; i < rows.length; i += BATCH_SIZE) { await client.query(sql, [JSON.stringify(rows.slice(i, i + BATCH_SIZE))]); } @@ -235,14 +249,21 @@ function uniqueBy(items: T[], key: (item: T) => string | number): T[] { return [...map.values()]; } -async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map) { +async function writeToDatabase( + client: PoolClient, + wines: Wine[], + totals: Map, +) { // 1. Oppslagstabellene, i den rekkefølgen fremmednøklene krever await insertInBatches( client, `INSERT INTO countries (code, name) SELECT code, name FROM jsonb_to_recordset($1::jsonb) AS x(code text, name text) ON CONFLICT DO NOTHING`, - uniqueBy(wines, (w) => w.countryCode).map((w) => ({ code: w.countryCode, name: w.countryName })) + uniqueBy(wines, (w) => w.countryCode).map((w) => ({ + code: w.countryCode, + name: w.countryName, + })), ); await insertInBatches( @@ -250,7 +271,11 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map w.regionId).map((w) => ({ id: w.regionId, name: w.regionName, country_code: w.countryCode })) + uniqueBy(wines, (w) => w.regionId).map((w) => ({ + id: w.regionId, + name: w.regionName, + country_code: w.countryCode, + })), ); await insertInBatches( @@ -258,7 +283,11 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map w.wineryId).map((w) => ({ id: w.wineryId, name: w.wineryName, website: w.website })) + uniqueBy(wines, (w) => w.wineryId).map((w) => ({ + id: w.wineryId, + name: w.wineryName, + website: w.website, + })), ); // 2. Vinene. ON CONFLICT DO UPDATE gjør at en ny kjøring oppdaterer vinene @@ -286,22 +315,26 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map w.grapes.map((name) => ({ wine_id: w.id, name }))); - const foodLinks = wines.flatMap((w) => w.foods.map((name) => ({ wine_id: w.id, name }))); + const grapeLinks = wines.flatMap((w) => + w.grapes.map((name) => ({ wine_id: w.id, name })), + ); + const foodLinks = wines.flatMap((w) => + w.foods.map((name) => ({ wine_id: w.id, name })), + ); await insertInBatches( client, `INSERT INTO grapes (name) SELECT name FROM jsonb_to_recordset($1::jsonb) AS x(name text) ON CONFLICT DO NOTHING`, - uniqueBy(grapeLinks, (l) => l.name).map((l) => ({ name: l.name })) + uniqueBy(grapeLinks, (l) => l.name).map((l) => ({ name: l.name })), ); await insertInBatches( client, `INSERT INTO foods (name) SELECT name FROM jsonb_to_recordset($1::jsonb) AS x(name text) ON CONFLICT DO NOTHING`, - uniqueBy(foodLinks, (l) => l.name).map((l) => ({ name: l.name })) + uniqueBy(foodLinks, (l) => l.name).map((l) => ({ name: l.name })), ); await insertInBatches( @@ -310,7 +343,7 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map ({ id, sum: t.sum, count: t.count })) + [...totals].map(([id, t]) => ({ id, sum: t.sum, count: t.count })), ); // 5. Matkategorier: grupperingen ligger i en egen SQL-fil, så den er lett å endre - await client.query(readFileSync(FOOD_CATEGORIES_FILE, "utf8")); + await client.query(readFileSync(FOOD_CATEGORIES_FILE, 'utf8')); return { grapeLinks: grapeLinks.length, foodLinks: foodLinks.length }; } @@ -354,41 +387,46 @@ async function main() { console.log(`Leser vurderinger fra ${RATINGS_FILE}`); const ratings = await readRatings(new Set(wines.map((w) => w.id))); - console.log(` ${ratings.read} vurderinger lest, ${ratings.totals.size} viner har rating`); + console.log( + ` ${ratings.read} vurderinger lest, ${ratings.totals.size} viner har rating`, + ); - console.log("Skriver til databasen"); + console.log('Skriver til databasen'); const client = await pool.connect(); try { - await client.query("BEGIN"); + await client.query('BEGIN'); const links = await writeToDatabase(client, wines, ratings.totals); - await client.query("COMMIT"); - console.log(` ${links.grapeLinks} drue-koblinger og ${links.foodLinks} mat-koblinger`); + await client.query('COMMIT'); + console.log( + ` ${links.grapeLinks} drue-koblinger og ${links.foodLinks} mat-koblinger`, + ); } catch (error) { - await client.query("ROLLBACK"); - console.error("Importen feilet, ingenting ble lagret."); + await client.query('ROLLBACK'); + console.error('Importen feilet, ingenting ble lagret.'); throw error; } finally { client.release(); } - console.log(""); - printCounts("Viner som ble hoppet over:", skipped); + console.log(''); + printCounts('Viner som ble hoppet over:', skipped); if (examples.length > 0) { - console.log("Eksempler:"); + console.log('Eksempler:'); for (const example of examples) console.log(` ${example}`); } - printCounts("Felt satt til NULL på grunn av ugyldig verdi:", warnings); - printCounts("Vurderinger som ble hoppet over:", ratings.skipped); + printCounts('Felt satt til NULL på grunn av ugyldig verdi:', warnings); + printCounts('Vurderinger som ble hoppet over:', ratings.skipped); const uncategorized = await pool.query( `SELECT f.name, count(*)::int AS wines FROM foods f JOIN wine_foods wf ON wf.food_id = f.id WHERE f.category IS NULL - GROUP BY f.name ORDER BY wines DESC` + GROUP BY f.name ORDER BY wines DESC`, ); if (uncategorized.rows.length > 0) { console.log(`Mat uten kategori (legg dem til i ${FOOD_CATEGORIES_FILE}):`); - for (const row of uncategorized.rows) console.log(` ${row.name}: ${row.wines} viner`); + for (const row of uncategorized.rows) + console.log(` ${row.name}: ${row.wines} viner`); } console.log(`Ferdig på ${((Date.now() - started) / 1000).toFixed(1)} s`); } @@ -398,4 +436,4 @@ main() console.error(error); process.exitCode = 1; }) - .finally(() => pool.end()); \ No newline at end of file + .finally(() => pool.end()); diff --git a/backend/src/db.ts b/backend/src/db.ts index 46ab7b4..dbcdc46 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -1,5 +1,5 @@ -import pg from "pg"; -import "dotenv/config"; +import pg from 'pg'; +import 'dotenv/config'; const { Pool } = pg; @@ -9,4 +9,4 @@ export const pool = new Pool({ database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, -}); \ No newline at end of file +}); diff --git a/backend/src/index.ts b/backend/src/index.ts index 8e06108..bb7ebb6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,7 +1,7 @@ -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import {GraphQLError} from "graphql"; -import { pool } from "./db.js"; +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { GraphQLError } from 'graphql'; +import { pool } from './db.js'; const MAX_LIMIT = 100; @@ -11,12 +11,12 @@ function readPaging(args: { limit?: number; offset?: number }) { if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) { throw new GraphQLError(`limit må være mellom 1 og ${MAX_LIMIT}`, { - extensions: { code: "BAD_USER_INPUT" }, + extensions: { code: 'BAD_USER_INPUT' }, }); } if (!Number.isInteger(offset) || offset < 0) { - throw new GraphQLError("offset kan ikke være negativ", { - extensions: { code: "BAD_USER_INPUT" }, + throw new GraphQLError('offset kan ikke være negativ', { + extensions: { code: 'BAD_USER_INPUT' }, }); } return { limit, offset }; @@ -89,12 +89,9 @@ const typeDefs = `#graphql const resolvers = { Query: { - health: () => "OK", - - wines: async ( - _: unknown, - args: { limit?: number; offset?: number } - ) => { + health: () => 'OK', + + wines: async (_: unknown, args: { limit?: number; offset?: number }) => { const { limit, offset } = readPaging(args); const winesResult = await pool.query( @@ -119,12 +116,10 @@ const resolvers = { LIMIT $1 OFFSET $2 `, - [limit, offset] + [limit, offset], ); - const countResult = await pool.query( - `SELECT COUNT(*) FROM wines` - ); + const countResult = await pool.query(`SELECT COUNT(*) FROM wines`); return { items: winesResult.rows, @@ -132,18 +127,15 @@ const resolvers = { }; }, - wine: async ( - _: unknown, - args: { id: string } -) => { - const wineId = Number(args.id); + wine: async (_: unknown, args: { id: string }) => { + const wineId = Number(args.id); - if (!Number.isInteger(wineId)) { - return null; - } + if (!Number.isInteger(wineId)) { + return null; + } - const result = await pool.query( - ` + const result = await pool.query( + ` SELECT w.id, w.name, @@ -186,11 +178,11 @@ const resolvers = { JOIN countries c ON w.country_code = c.code WHERE w.id = $1 `, - [wineId] - ); + [wineId], + ); - return result.rows[0] ?? null; - }, + return result.rows[0] ?? null; + }, foodCategories: async () => { const result = await pool.query( @@ -201,14 +193,14 @@ const resolvers = { WHERE f.category IS NOT NULL GROUP BY f.category ORDER BY f.category - ` + `, ); return result.rows; }, winesByFoodCategory: async ( _: unknown, - args: { category: string; limit?: number; offset?: number } + args: { category: string; limit?: number; offset?: number }, ) => { const { limit, offset } = readPaging(args); const inCategory = ` @@ -242,12 +234,12 @@ const resolvers = { LIMIT $2 OFFSET $3 `, - [args.category, limit, offset] + [args.category, limit, offset], ); const countResult = await pool.query( `SELECT count(*)::int AS count FROM wines w WHERE ${inCategory}`, - [args.category] + [args.category], ); return { @@ -267,4 +259,4 @@ const { url } = await startStandaloneServer(server, { listen: { port: Number(process.env.PORT ?? 3001) }, }); -console.log(`GraphQL server running at ${url}`); \ No newline at end of file +console.log(`GraphQL server running at ${url}`); diff --git a/client/.prettierignore b/client/.prettierignore new file mode 100644 index 0000000..38175d0 --- /dev/null +++ b/client/.prettierignore @@ -0,0 +1,3 @@ +# Prettier also skips everything in .gitignore (dist, node_modules ...). +# Generated by npm, so it should not be reformatted. +package-lock.json diff --git a/client/README.md b/client/README.md index 2d31cb6..d8cb914 100644 --- a/client/README.md +++ b/client/README.md @@ -40,16 +40,15 @@ export default defineConfig([ // other options... }, }, -]) - +]); ``` You can also install [eslint-plugin-react-x](https://npmx.dev/package/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://npmx.dev/package/eslint-plugin-react-dom) for React-specific lint rules: ```js // eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; export default defineConfig([ globalIgnores(['dist']), @@ -70,6 +69,5 @@ export default defineConfig([ // other options... }, }, -]) - +]); ``` diff --git a/client/eslint.config.js b/client/eslint.config.js index ef614d2..e3927d1 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -1,9 +1,14 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +// ESLint finds bugs and risky patterns in the client code, including +// wrong use of React hooks. Formatting is left to Prettier: +// eslint-config-prettier (last in the list) turns off the ESLint rules +// that would disagree with it. +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier/flat'; +import { defineConfig, globalIgnores } from 'eslint/config'; export default defineConfig([ globalIgnores(['dist']), @@ -19,4 +24,5 @@ export default defineConfig([ globals: globals.browser, }, }, -]) + eslintConfigPrettier, +]); diff --git a/client/package-lock.json b/client/package-lock.json index cb9fa52..62b26de 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,9 +22,11 @@ "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", + "prettier": "^3.9.10", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.3.0" @@ -1562,6 +1564,22 @@ } } }, + "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", @@ -2493,6 +2511,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "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", diff --git a/client/package.json b/client/package.json index a82364f..40d957e 100644 --- a/client/package.json +++ b/client/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@apollo/client": "^4.3.2", @@ -24,9 +26,11 @@ "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", + "prettier": "^3.9.10", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.3.0" diff --git a/client/src/apollo/client.ts b/client/src/apollo/client.ts index e0a5bff..296653f 100644 --- a/client/src/apollo/client.ts +++ b/client/src/apollo/client.ts @@ -14,4 +14,4 @@ export const client = new ApolloClient({ uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/', }), cache: new InMemoryCache(), -}); \ No newline at end of file +}); diff --git a/client/src/components/WineCatalog.tsx b/client/src/components/WineCatalog.tsx index fbb4d69..041d3bb 100644 --- a/client/src/components/WineCatalog.tsx +++ b/client/src/components/WineCatalog.tsx @@ -1,9 +1,8 @@ import { useEffect, useRef, useState } from 'react'; -import { gql } from '@apollo/client'; import { useQuery } from '@apollo/client/react'; import WineList from './WineList'; import Pagination from './Pagination'; -import type { WineListItem } from '../types/wine'; +import { WINES_QUERY, type WinesData } from '../graphql/queries'; export const PAGE_SIZE = 20; @@ -13,31 +12,10 @@ export const PAGE_SIZE = 20; * Data fetching is kept here and display in WineList, so WineList can be * reused and tested without Apollo. * - * We fetch only the fields the list shows, and always with a limit, so we - * never load the whole dataset (scalability and sustainability). - * Pagination comes in issue 10. + * The query itself is in ../graphql/queries.ts. It fetches one page at a + * time with limit and offset, so we never load the whole dataset + * (scalability and sustainability). */ -export const WINES_QUERY = gql` - query Wines($offset: Int, $limit: Int) { - wines(offset: $offset, limit: $limit) { - totalCount - items { - id - name - type - winery - region - country - avgRating - } - } - } -`; - -type WinesData = { - wines: { totalCount: number; items: WineListItem[] }; -}; - export default function WineCatalog() { const [page, setPage] = useState(1); const offset = (page - 1) * PAGE_SIZE; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts new file mode 100644 index 0000000..3377ba9 --- /dev/null +++ b/client/src/graphql/queries.ts @@ -0,0 +1,37 @@ +import { gql } from '@apollo/client'; +import type { WineListItem } from '../types/wine'; + +/** + * GraphQL queries used by the client. + * + * They live in their own file instead of next to the components, so that + * React Fast Refresh keeps working (a component file should only export + * components), and so that tests can reuse the same queries. + */ + +/** + * One page of wines for the catalog. + * + * We fetch only the fields the list shows, and always with a limit, so we + * never load the whole dataset (scalability and sustainability). + */ +export const WINES_QUERY = gql` + query Wines($offset: Int, $limit: Int) { + wines(offset: $offset, limit: $limit) { + totalCount + items { + id + name + type + winery + region + country + avgRating + } + } + } +`; + +export type WinesData = { + wines: { totalCount: number; items: WineListItem[] }; +}; diff --git a/client/src/index.css b/client/src/index.css index 148f63a..19b30f9 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -11,7 +11,12 @@ --color-accent: #7b1e2b; /* wine red */ --color-border: #e4dcd9; - font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + font-family: + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; line-height: 1.5; color: var(--color-text); background: var(--color-bg); diff --git a/client/vite.config.ts b/client/vite.config.ts index 2a1e3b1..1af8bed 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,9 +1,9 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ // The app is served from http://it2810-xx.idi.ntnu.no/project2/ base: '/project2/', plugins: [react()], -}) +}); From a200fad11162276d311a56fdb4fa18c13e154902 Mon Sep 17 00:00:00 2001 From: Celine Hagen Date: Tue, 20 Oct 2026 11:13:03 +0200 Subject: [PATCH 2/8] =?UTF-8?q?Implementere=20s=C3=B8k=20med=20debouncing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../004_add_winery_search_index.sql | 0 backend/src/index.ts | 36 +++++++++++++--- client/src/components/WineCatalog.tsx | 43 ++++++++++++++++--- client/src/graphql/queries.ts | 10 ++++- 4 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 backend/db/migrations/004_add_winery_search_index.sql diff --git a/backend/db/migrations/004_add_winery_search_index.sql b/backend/db/migrations/004_add_winery_search_index.sql new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/index.ts b/backend/src/index.ts index bb7ebb6..b4956dd 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -71,6 +71,7 @@ const typeDefs = `#graphql health: String! wines( + search: String limit: Int = 20 offset: Int = 0 ): WinePage! @@ -91,8 +92,12 @@ const resolvers = { Query: { health: () => 'OK', - wines: async (_: unknown, args: { limit?: number; offset?: number }) => { + wines: async ( + _: unknown, + args: { search?: string; limit?: number; offset?: number }, + ) => { const { limit, offset } = readPaging(args); + const search = args.search?.trim() || null; const winesResult = await pool.query( ` @@ -112,14 +117,35 @@ const resolvers = { JOIN wineries wi ON w.winery_id = wi.id JOIN regions r ON w.region_id = r.id JOIN countries c ON w.country_code = c.code + WHERE ( + $1::text IS NULL + OR w.name ILIKE $1 || '%' + OR w.name ILIKE '% ' || $1 || '%' + OR wi.name ILIKE $1 || '%' + OR wi.name ILIKE '% ' || $1 || '%' + ) ORDER BY w.name, w.id - LIMIT $1 - OFFSET $2 + LIMIT $2 + OFFSET $3 `, - [limit, offset], + [search, limit, offset], ); - const countResult = await pool.query(`SELECT COUNT(*) FROM wines`); + const countResult = await pool.query( + ` + SELECT COUNT(*) + FROM wines w + JOIN wineries wi ON w.winery_id = wi.id + WHERE ( + $1::text IS NULL + OR w.name ILIKE $1 || '%' + OR w.name ILIKE '% ' || $1 || '%' + OR wi.name ILIKE $1 || '%' + OR wi.name ILIKE '% ' || $1 || '%' + ) + `, + [search], + ); return { items: winesResult.rows, diff --git a/client/src/components/WineCatalog.tsx b/client/src/components/WineCatalog.tsx index 041d3bb..82906d2 100644 --- a/client/src/components/WineCatalog.tsx +++ b/client/src/components/WineCatalog.tsx @@ -2,7 +2,11 @@ import { useEffect, useRef, useState } from 'react'; import { useQuery } from '@apollo/client/react'; import WineList from './WineList'; import Pagination from './Pagination'; -import { WINES_QUERY, type WinesData } from '../graphql/queries'; +import { + WINES_QUERY, + type WinesData, + type WinesVariables, +} from '../graphql/queries'; export const PAGE_SIZE = 20; @@ -18,12 +22,29 @@ export const PAGE_SIZE = 20; */ export default function WineCatalog() { const [page, setPage] = useState(1); + const [searchInput, setSearchInput] = useState(''); + const [search, setSearch] = useState(''); const offset = (page - 1) * PAGE_SIZE; - const { data, previousData, loading, error } = useQuery( - WINES_QUERY, - { variables: { offset, limit: PAGE_SIZE } }, - ); + useEffect(() => { + const timeoutId = window.setTimeout(() => { + setSearch(searchInput.trim()); + setPage(1); + }, 350); + + return () => window.clearTimeout(timeoutId); + }, [searchInput]); + + const { data, previousData, loading, error } = useQuery< + WinesData, + WinesVariables + >(WINES_QUERY, { + variables: { + search: search || null, + offset, + limit: PAGE_SIZE, + }, + }); const shown = data ?? previousData; @@ -53,6 +74,18 @@ export default function WineCatalog() { return ( // aria-busy tells screen readers the content is being updated
+
+ + + setSearchInput(event.target.value)} + placeholder="For eksempel Barolo" + /> +
+ Date: Tue, 20 Oct 2026 11:29:12 +0200 Subject: [PATCH 3/8] Lag Explore by taste som forside MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Smakskart der viner plasseres etter fylde og syre, farget etter vintype - Filtre for vintype, drue, land, årgang og vurdering - Panel med valgt vin og lenke videre til detaljsiden (midlertidig side til den er laget) - Toppbar med meny, og katalogen flyttet til /wines - Backend: nye spørringer tasteMap og tasteOptions - Felles fargevariabler i index.css Co-Authored-By: Claude Opus 5.5 --- backend/src/index.ts | 162 ++++++++- client/src/App.tsx | 49 ++- client/src/assets/vineyard.jpg | Bin 0 -> 149048 bytes client/src/components/ExploreByTaste.css | 410 +++++++++++++++++++++++ client/src/components/ExploreByTaste.tsx | 127 +++++++ client/src/components/SelectedWine.tsx | 140 ++++++++ client/src/components/SiteHeader.css | 57 ++++ client/src/components/SiteHeader.tsx | 47 +++ client/src/components/TasteFilters.tsx | 124 +++++++ client/src/components/TasteMap.tsx | 205 ++++++++++++ client/src/data/taste.ts | 54 +++ client/src/graphql/queries.ts | 72 +++- client/src/index.css | 34 +- client/src/types/wine.ts | 35 ++ 14 files changed, 1502 insertions(+), 14 deletions(-) create mode 100644 client/src/assets/vineyard.jpg create mode 100644 client/src/components/ExploreByTaste.css create mode 100644 client/src/components/ExploreByTaste.tsx create mode 100644 client/src/components/SelectedWine.tsx create mode 100644 client/src/components/SiteHeader.css create mode 100644 client/src/components/SiteHeader.tsx create mode 100644 client/src/components/TasteFilters.tsx create mode 100644 client/src/components/TasteMap.tsx create mode 100644 client/src/data/taste.ts diff --git a/backend/src/index.ts b/backend/src/index.ts index bb7ebb6..56422ce 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -10,18 +10,66 @@ function readPaging(args: { limit?: number; offset?: number }) { const offset = args.offset ?? 0; if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) { - throw new GraphQLError(`limit må være mellom 1 og ${MAX_LIMIT}`, { + throw new GraphQLError(`limit must be between 1 and ${MAX_LIMIT}`, { extensions: { code: 'BAD_USER_INPUT' }, }); } if (!Number.isInteger(offset) || offset < 0) { - throw new GraphQLError('offset kan ikke være negativ', { + throw new GraphQLError('offset cannot be negative', { extensions: { code: 'BAD_USER_INPUT' }, }); } return { limit, offset }; } +// Smakskartet viser prikker, ikke sider, så det får en egen grense. +// Flere enn ca. 300 prikker blir uleselig og tungt å tegne. +const TASTE_MAP_MAX = 300; + +type TasteFilter = { + type?: string | null; + grape?: string | null; + country?: string | null; + year?: number | null; + minRating?: number | null; + minRatingCount?: number | null; +}; + +/** + * Lager WHERE-delen for smakskartet ut fra filtrene. + * Verdiene sendes alltid som parametere ($1, $2, ...), aldri rett inn i SQL, + * så brukerinput kan ikke endre spørringen (SQL-injeksjon). + */ +function tasteWhere(filter: TasteFilter = {}) { + const where = ['w.body IS NOT NULL', 'w.acidity IS NOT NULL']; + const params: unknown[] = []; + const add = (sql: string, value: unknown) => { + params.push(value); + where.push(sql.replace('?', `$${params.length}`)); + }; + + if (filter.type) add('w.type = ?', filter.type); + if (filter.country) add('c.name = ?', filter.country); + if (filter.grape) { + add( + `EXISTS ( + SELECT 1 FROM wine_grapes wg + JOIN grapes g ON g.id = wg.grape_id + WHERE wg.wine_id = w.id AND g.name = ? + )`, + filter.grape, + ); + } + // @> bruker GIN-indeksen wines_vintages_idx + if (filter.year != null) add('w.vintages @> ARRAY[?]::int[]', filter.year); + if (filter.minRating != null) add('w.avg_rating >= ?', filter.minRating); + if (filter.minRatingCount != null) { + add('w.rating_count >= ?', filter.minRatingCount); + } + + return { sql: where.join(' AND '), params }; +} + const typeDefs = `#graphql type Wine { @@ -67,6 +115,24 @@ const typeDefs = `#graphql wineCount: Int! } + # Filtre for Explore by taste. Alle er valgfrie; det som mangler, filtreres ikke på. + input TasteFilter { + type: String + grape: String + country: String + year: Int + minRating: Float + minRatingCount: Int + } + + # Verdiene til nedtrekksmenyene i filterpanelet på Explore by taste + type TasteOptions { + types: [String!]! + countries: [String!]! + grapes: [String!]! + years: [Int!]! + } + type Query { health: String! @@ -84,6 +150,11 @@ const typeDefs = `#graphql limit: Int = 20 offset: Int = 0 ): WinePage! + + # Viner til smakskartet. Bare viner med både fylde og syre kommer med. + tasteMap(filter: TasteFilter, limit: Int = 150): WinePage! + + tasteOptions: TasteOptions! } `; @@ -247,6 +318,93 @@ const resolvers = { totalCount: countResult.rows[0].count, }; }, + + tasteMap: async ( + _: unknown, + args: { filter?: TasteFilter | null; limit?: number }, + ) => { + const limit = args.limit ?? 150; + if (!Number.isInteger(limit) || limit < 1 || limit > TASTE_MAP_MAX) { + throw new GraphQLError(`limit must be between 1 and ${TASTE_MAP_MAX}`, { + extensions: { code: 'BAD_USER_INPUT' }, + }); + } + + const { sql, params } = tasteWhere(args.filter ?? {}); + + // Vi viser de vinene flest har vurdert, så kartet fylles med kjente + // viner i stedet for viner med én enkelt vurdering på 5.0. + const winesResult = await pool.query( + ` + SELECT + w.id, + w.name, + w.type, + w.abv::float AS abv, + w.body, + w.acidity, + wi.name AS winery, + r.name AS region, + c.name AS country, + w.avg_rating::float AS "avgRating", + w.rating_count AS "ratingCount" + FROM wines w + JOIN wineries wi ON w.winery_id = wi.id + JOIN regions r ON w.region_id = r.id + JOIN countries c ON w.country_code = c.code + WHERE ${sql} + ORDER BY w.rating_count DESC, w.id + LIMIT $${params.length + 1} + `, + [...params, limit], + ); + + // Totalen telles med de samme filtrene, så "X wines found" viser alle + // vinene som passer, selv om kartet bare tegner de første `limit`. + const countResult = await pool.query( + ` + SELECT count(*)::int AS count + FROM wines w + JOIN countries c ON w.country_code = c.code + WHERE ${sql} + `, + params, + ); + + return { + items: winesResult.rows, + totalCount: countResult.rows[0].count, + }; + }, + + tasteOptions: async () => { + // Fire små spørringer i parallell. Druer begrenses til de 60 vanligste, + // ellers blir nedtrekksmenyen for lang til å være nyttig. + const [types, countries, grapes, years] = await Promise.all([ + pool.query(`SELECT DISTINCT type FROM wines ORDER BY type`), + pool.query(`SELECT name FROM countries ORDER BY name`), + pool.query(` + SELECT g.name + FROM grapes g + JOIN wine_grapes wg ON wg.grape_id = g.id + GROUP BY g.name + ORDER BY count(*) DESC + LIMIT 60 + `), + pool.query(` + SELECT DISTINCT unnest(vintages) AS year + FROM wines + ORDER BY year DESC + `), + ]); + + return { + types: types.rows.map((r) => r.type), + countries: countries.rows.map((r) => r.name), + grapes: grapes.rows.map((r) => r.name).sort(), + years: years.rows.map((r) => r.year), + }; + }, }, }; diff --git a/client/src/App.tsx b/client/src/App.tsx index ec50821..1249a83 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,17 +1,52 @@ +import { Link, Route, Routes, useParams } from 'react-router'; +import SiteHeader from './components/SiteHeader'; +import ExploreByTaste from './components/ExploreByTaste'; import WineCatalog from './components/WineCatalog'; +/** + * NB: Temporary placeholder. The detail page is not built yet. + * "View wine details" on Explore by taste links here, so the link + * already works. Replace this with the real detail page component. + */ +function WineDetailsPlaceholder() { + const { id } = useParams(); + return ( +
+

Wine details

+

The detail page for wine {id} is not built yet.

+ ← Back to Explore by taste +
+ ); +} + /** * Root component for Wine not? * - *
marks the page's main content, so screen reader and keyboard users - * can jump straight to it. The page has one

, and WineList uses

, - * so the heading hierarchy is correct. + * SiteHeader is shown on every page. Routes picks the page from the URL: + * - / Explore by taste (front page) + * - /wines the wine catalog + * - /wines/:id detail page (placeholder for now) + * + * Each page has its own
and one

, so the heading hierarchy is + * correct on every page. */ export default function App() { return ( -
-

Wine not?

- -
+ <> + + + } /> + +

All wines

+ +

+ } + /> + } /> + + ); } diff --git a/client/src/assets/vineyard.jpg b/client/src/assets/vineyard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a11b3aa4d764fd4547eccd1074d364f869e6b3e8 GIT binary patch literal 149048 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 t|wa$jyN{3L=_LESogMleKAq!? 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 + const [selectedId, setSelectedId] = useState(null); + + const options = useQuery(TASTE_OPTIONS_QUERY); + const { data, previousData, loading, error } = useQuery( + TASTE_MAP_QUERY, + { variables: toVariables(filter) }, + ); + + // Keep showing the previous wines while new ones load, so the map + // doesn't flash empty every time a filter changes + const shown = (data ?? previousData)?.tasteMap; + const wines = shown?.items ?? []; + + // The selected wine. If it disappears because of a filter, or nothing is + // selected yet, we show the first (most-rated) wine instead. + const selected = wines.find((w) => w.id === selectedId) ?? wines[0] ?? null; + + return ( + <> +
+
+

Explore by taste

+

+ Find wines that match your taste. See how wines compare on body and + acidity, and filter to find exactly what you like. +

+
+
+ +
+ + +
+ {/* role="status" announces the new count when filters change */} +

+ {shown + ? `${shown.totalCount.toLocaleString('en')} wines found` + : 'Loading wines …'} +

+ {shown && shown.totalCount > wines.length && ( +

+ The map shows the {wines.length} most-rated of them. +

+ )} + + {error && ( +

Could not load wines. Please try again later.

+ )} + {shown && wines.length === 0 && ( +

+ No wines match these filters. Try removing some of them. +

+ )} + + +
+ + +
+ + ); +} diff --git a/client/src/components/SelectedWine.tsx b/client/src/components/SelectedWine.tsx new file mode 100644 index 0000000..895966e --- /dev/null +++ b/client/src/components/SelectedWine.tsx @@ -0,0 +1,140 @@ +import { Link } from 'react-router'; +import { useQuery } from '@apollo/client/react'; +import type { TasteWine } from '../types/wine'; +import { WINE_GRAPES_QUERY, type WineGrapesData } from '../graphql/queries'; +import { ACIDITY_LEVELS, BODY_LEVELS, wineTypeColor } from '../data/taste'; + +type Props = { + wine: TasteWine | null; +}; + +/** A wine glass filled with the colour of the wine type. Decorative only. */ +function WineGlass({ type }: { type: string }) { + return ( + + ); +} + +/** One row with label, bar and value, e.g. "Body ████░ Full-bodied". */ +function TasteBar(props: { label: string; share: number; value: string }) { + return ( +
+ {props.label} + + {props.value} +
+ ); +} + +/** + * The panel to the right of the map, showing the selected wine. + * + * The map query only has the fields every dot needs. Grapes and vintages + * are fetched here, for one wine at a time, with the existing wine(id) query. + */ +export default function SelectedWine({ wine }: Props) { + const { data } = useQuery(WINE_GRAPES_QUERY, { + variables: { id: wine?.id }, + skip: !wine, + }); + + if (!wine) { + return ( + + ); + } + + // Only use the extra details if they belong to the wine shown now + const details = data?.wine?.id === wine.id ? data.wine : null; + const vintages = details?.vintages ?? []; + const bodyStep = BODY_LEVELS.indexOf(wine.body) + 1; + const acidityStep = ACIDITY_LEVELS.indexOf(wine.acidity) + 1; + + return ( + // aria-live: screen readers announce the new wine when the user picks one + + ); +} diff --git a/client/src/components/SiteHeader.css b/client/src/components/SiteHeader.css new file mode 100644 index 0000000..2a2e279 --- /dev/null +++ b/client/src/components/SiteHeader.css @@ -0,0 +1,57 @@ +/* Top bar. A bit darker than the page, so it stands out. */ +.site-header { + background: var(--color-header); + border-bottom: 1px solid var(--color-border); +} + +.site-header-inner { + max-width: 78rem; + margin: 0 auto; + padding: 0.75rem 1rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem 2rem; +} + +.site-logo { + display: flex; + align-items: center; + gap: 0.4rem; + color: var(--color-text); + font-family: var(--font-display); + font-size: 1.6rem; + text-decoration: none; +} + +/* The glass in the logo is wine red (currentColor inside the svg) */ +.site-logo svg { + width: 22px; + height: 28px; + color: var(--color-accent); +} + +.site-header nav ul { + display: flex; + gap: 1.5rem; + margin: 0; + padding: 0; + list-style: none; +} + +.site-header nav a { + padding-block: 0.4rem; + border-bottom: 2px solid transparent; + color: var(--color-text); + text-decoration: none; +} + +.site-header nav a:hover { + border-color: var(--color-muted); +} + +.site-header nav a[aria-current='page'] { + border-color: var(--color-accent); + color: var(--color-accent); + font-weight: 600; +} diff --git a/client/src/components/SiteHeader.tsx b/client/src/components/SiteHeader.tsx new file mode 100644 index 0000000..a348fda --- /dev/null +++ b/client/src/components/SiteHeader.tsx @@ -0,0 +1,47 @@ +import { Link, NavLink } from 'react-router'; +import './SiteHeader.css'; + +/** + * The top bar with logo and main menu, shown on every page. + * + * NavLink sets aria-current="page" on the link to the page we are on, + * so screen readers say which page is active, and CSS can underline it. + * + * NB: Not built yet, add here when the pages exist: + * - "Favourites" (needs login and the favourites page) + * - search field and "Log in" + */ +export default function SiteHeader() { + return ( +
+
+ + + Wine not? + + + +
+
+ ); +} diff --git a/client/src/components/TasteFilters.tsx b/client/src/components/TasteFilters.tsx new file mode 100644 index 0000000..38bdc94 --- /dev/null +++ b/client/src/components/TasteFilters.tsx @@ -0,0 +1,124 @@ +import type { TasteFilter } from '../types/wine'; +import type { TasteOptionsData } from '../graphql/queries'; +import { EMPTY_TASTE_FILTER, POPULAR_MIN_RATINGS } from '../data/taste'; + +type Props = { + filter: TasteFilter; + onChange: (filter: TasteFilter) => void; + /** Choices for the dropdowns. Undefined while they are loading. */ + options: TasteOptionsData['tasteOptions'] | undefined; +}; + +/** + * The filter panel on Explore by taste. + * + * Controlled component: the filter values live in ExploreByTaste, and every + * change is sent up with onChange. That way the parent decides when to fetch + * new wines, and this component is easy to test. + * + * Every control has a

+ ); +} diff --git a/client/src/components/WineCatalog.css b/client/src/components/WineCatalog.css new file mode 100644 index 0000000..a2784d3 --- /dev/null +++ b/client/src/components/WineCatalog.css @@ -0,0 +1,29 @@ +/* Search and sort side by side, stacked on narrow screens */ +.catalog-controls { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: end; +} + +.catalog-field { + display: grid; + gap: 0.3rem; +} + +.catalog-field label { + font-size: 0.85rem; + font-weight: 600; +} + +.catalog-field input, +.catalog-field select { + /* At least 44 px high, so they are easy to hit on touch screens */ + min-height: 2.75rem; + padding: 0.45rem 0.6rem; + border: 1px solid var(--color-border); + border-radius: 0.5rem; + background: var(--color-field); + color: var(--color-text); + font: inherit; +} diff --git a/client/src/components/WineCatalog.tsx b/client/src/components/WineCatalog.tsx index 9d60543..6f5a3d2 100644 --- a/client/src/components/WineCatalog.tsx +++ b/client/src/components/WineCatalog.tsx @@ -2,11 +2,14 @@ import { useEffect, useRef, useState } from 'react'; import { useQuery } from '@apollo/client/react'; import WineList from './WineList'; import Pagination from './Pagination'; +import SortSelect from './SortSelect'; import { WINES_QUERY, type WinesData, type WinesVariables, } from '../graphql/queries'; +import type { WineSort } from '../types/wine'; +import './WineCatalog.css'; export const PAGE_SIZE = 20; @@ -24,6 +27,7 @@ export default function WineCatalog() { const [page, setPage] = useState(1); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); + const [sort, setSort] = useState('NAME_ASC'); const offset = (page - 1) * PAGE_SIZE; useEffect(() => { @@ -41,6 +45,7 @@ export default function WineCatalog() { >(WINES_QUERY, { variables: { search: search || null, + sort, offset, limit: PAGE_SIZE, }, @@ -62,6 +67,12 @@ export default function WineCatalog() { setPage(newPage); } + // A new sort order gives a new list, so start again from the first page + function changeSort(newSort: WineSort) { + setSort(newSort); + setPage(1); + } + // role="status" and role="alert" make screen readers announce the messages if (error) { return

Could not load wines. Please try again later.

; @@ -74,16 +85,20 @@ export default function WineCatalog() { return ( // aria-busy tells screen readers the content is being updated
-
- +
+
+ + + setSearchInput(event.target.value)} + placeholder="For example Barolo" + /> +
- setSearchInput(event.target.value)} - placeholder="For example Barolo" - /> +
+ +
Alcohol
+
+ {wine.abv != null ? `${wine.abv.toFixed(1)} %` : 'Unknown'} +
diff --git a/client/src/data/exampleWines.ts b/client/src/data/exampleWines.ts index c454650..4f2c2d7 100644 --- a/client/src/data/exampleWines.ts +++ b/client/src/data/exampleWines.ts @@ -14,6 +14,7 @@ export const exampleWines: WineListItem[] = [ region: 'Piemonte', country: 'Italy', avgRating: 4.5, + abv: 14.5, }, { id: '2', @@ -23,6 +24,7 @@ export const exampleWines: WineListItem[] = [ region: 'Bourgogne', country: 'France', avgRating: 4.1, + abv: 12.5, }, { id: '3', @@ -32,6 +34,7 @@ export const exampleWines: WineListItem[] = [ region: 'Rioja', country: 'Spain', avgRating: 3.9, + abv: 13.5, }, { id: '4', @@ -41,5 +44,6 @@ export const exampleWines: WineListItem[] = [ region: 'Mosel', country: 'Germany', avgRating: null, + abv: null, }, ]; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index aa33aef..2aa2678 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -1,5 +1,5 @@ import { gql } from '@apollo/client'; -import type { TasteWine, WineListItem } from '../types/wine'; +import type { TasteWine, WineListItem, WineSort } from '../types/wine'; /** * GraphQL queries used by the client. @@ -16,8 +16,8 @@ import type { TasteWine, WineListItem } from '../types/wine'; * never load the whole dataset (scalability and sustainability). */ export const WINES_QUERY = gql` - query Wines($search: String, $offset: Int, $limit: Int) { - wines(search: $search, offset: $offset, limit: $limit) { + query Wines($search: String, $sort: WineSort, $offset: Int, $limit: Int) { + wines(search: $search, sort: $sort, offset: $offset, limit: $limit) { totalCount items { id @@ -27,6 +27,7 @@ export const WINES_QUERY = gql` region country avgRating + abv } } } @@ -38,6 +39,7 @@ export type WinesData = { export type WinesVariables = { search?: string | null; + sort?: WineSort; offset?: number; limit?: number; }; diff --git a/client/src/types/wine.ts b/client/src/types/wine.ts index 88feb4b..8c177ad 100644 --- a/client/src/types/wine.ts +++ b/client/src/types/wine.ts @@ -12,8 +12,19 @@ export type WineListItem = { country: string; /** null when the wine has no ratings */ avgRating: number | null; + /** Alcohol in percent, null when unknown */ + abv: number | null; }; +/** Sort orders for the wine catalog. Matches enum WineSort in the backend. */ +export type WineSort = + | 'NAME_ASC' + | 'NAME_DESC' + | 'RATING_DESC' + | 'RATING_ASC' + | 'ABV_DESC' + | 'ABV_ASC'; + /** The body levels in the database (enum body_level), lightest first. */ export type BodyLevel = | 'Very light-bodied' From 4b6ffa161cec563fc7aba32514b48d3798101427 Mon Sep 17 00:00:00 2001 From: Lea Tveteraas Date: Tue, 20 Oct 2026 12:24:22 +0200 Subject: [PATCH 6/8] Implementer filtrering av viner (#50) --- backend/src/index.ts | 183 +++++++++++++++++++++++--- client/src/components/WineCatalog.css | 56 ++++++++ client/src/components/WineCatalog.tsx | 48 ++++++- client/src/components/WineFilters.tsx | 163 +++++++++++++++++++++++ client/src/components/WineList.tsx | 6 + client/src/data/catalog.ts | 17 +++ client/src/data/exampleWines.ts | 8 ++ client/src/graphql/queries.ts | 71 +++++++++- client/src/types/wine.ts | 18 +++ 9 files changed, 548 insertions(+), 22 deletions(-) create mode 100644 client/src/components/WineFilters.tsx create mode 100644 client/src/data/catalog.ts diff --git a/backend/src/index.ts b/backend/src/index.ts index 55d740a..35861a0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -45,6 +45,97 @@ const WINE_ORDER = { type WineSort = keyof typeof WINE_ORDER; +// Samme verdier som typene body_level og acidity_level i databasen. +// Ukjente verdier avvises her, ellers gir PostgreSQL en intern feil. +const BODY_LEVELS = [ + 'Very light-bodied', + 'Light-bodied', + 'Medium-bodied', + 'Full-bodied', + 'Very full-bodied', +]; +const ACIDITY_LEVELS = ['Low', 'Medium', 'High']; + +type WineFilter = { + type?: string | null; + country?: string | null; + region?: string | null; + grape?: string | null; + body?: string | null; + acidity?: string | null; + minRatingCount?: number | null; +}; + +function badInput(message: string) { + return new GraphQLError(message, { extensions: { code: 'BAD_USER_INPUT' } }); +} + +/** + * Lager WHERE-delen for vinkatalogen ut fra søk og filtre. + * Samme mønster som tasteWhere: verdiene sendes alltid som parametere. + * Både vinlisten og tellingen bruker denne, så totalCount stemmer med listen. + * Spørringen må ha w (wines) og wi (wineries) med, fordi søket ser på produsent. + */ +function catalogWhere(search: string | null, filter: WineFilter = {}) { + const where: string[] = []; + const params: unknown[] = []; + const add = (sql: string, value: unknown) => { + params.push(value); + where.push(sql.replaceAll('?', `$${params.length}`)); + }; + + if (search) { + // Treff i starten av et ord i vinnavnet eller produsentnavnet + add( + `( + w.name ILIKE ?::text || '%' + OR w.name ILIKE '% ' || ?::text || '%' + OR wi.name ILIKE ?::text || '%' + OR wi.name ILIKE '% ' || ?::text || '%' + )`, + search, + ); + } + if (filter.type) add('w.type = ?', filter.type); + // Landkoden ligger på wines, så landfilteret trenger ingen join + if (filter.country) add('w.country_code = ?', filter.country); + if (filter.region) { + const regionId = Number(filter.region); + if (!Number.isInteger(regionId)) throw badInput('region must be an id'); + add('w.region_id = ?', regionId); + } + if (filter.grape) { + add( + `EXISTS ( + SELECT 1 FROM wine_grapes wg + JOIN grapes g ON g.id = wg.grape_id + WHERE wg.wine_id = w.id AND g.name = ? + )`, + filter.grape, + ); + } + if (filter.body) { + if (!BODY_LEVELS.includes(filter.body)) throw badInput('unknown body'); + add('w.body = ?', filter.body); + } + if (filter.acidity) { + if (!ACIDITY_LEVELS.includes(filter.acidity)) { + throw badInput('unknown acidity'); + } + add('w.acidity = ?', filter.acidity); + } + // Gjør sortering på rating nyttig: uten dette kommer viner med + // én enkelt vurdering på 5.0 først. + if (filter.minRatingCount != null) { + if (!Number.isInteger(filter.minRatingCount) || filter.minRatingCount < 0) { + throw badInput('minRatingCount cannot be negative'); + } + add('w.rating_count >= ?', filter.minRatingCount); + } + + return { sql: where.join(' AND ') || 'TRUE', params }; +} + type TasteFilter = { type?: string | null; grape?: string | null; @@ -144,6 +235,37 @@ const typeDefs = `#graphql ABV_ASC } + # Filtre for vinkatalogen. Alle er valgfrie; det som mangler, filtreres ikke på. + input WineFilter { + type: String + # Landkode, f.eks. "FR" + country: String + region: ID + grape: String + body: String + acidity: String + # Bare viner med minst så mange vurderinger + minRatingCount: Int + } + + type Country { + code: String! + name: String! + } + + type Region { + id: ID! + name: String! + } + + # Verdiene til nedtrekksmenyene i filterpanelet i vinkatalogen. + # Fylde og syre har faste verdier og ligger i klienten. + type CatalogOptions { + types: [String!]! + countries: [Country!]! + grapes: [String!]! + } + # Filtre for Explore by taste. Alle er valgfrie; det som mangler, filtreres ikke på. input TasteFilter { type: String @@ -167,11 +289,17 @@ const typeDefs = `#graphql wines( search: String + filter: WineFilter sort: WineSort = NAME_ASC limit: Int = 20 offset: Int = 0 ): WinePage! + catalogOptions: CatalogOptions! + + # Regionene i ett land, til regionfilteret + regions(country: String!): [Region!]! + wine(id: ID!): WineDetails foodCategories: [FoodCategory!]! @@ -197,6 +325,7 @@ const resolvers = { _: unknown, args: { search?: string; + filter?: WineFilter | null; sort?: WineSort | null; limit?: number; offset?: number; @@ -204,6 +333,7 @@ const resolvers = { ) => { const { limit, offset } = readPaging(args); const search = args.search?.trim() || null; + const { sql, params } = catalogWhere(search, args.filter ?? {}); // GraphQL godtar bare verdiene i enum WineSort, så oppslaget finner alltid noe const orderBy = WINE_ORDER[args.sort ?? 'NAME_ASC']; @@ -225,18 +355,12 @@ const resolvers = { JOIN wineries wi ON w.winery_id = wi.id JOIN regions r ON w.region_id = r.id JOIN countries c ON w.country_code = c.code - WHERE ( - $1::text IS NULL - OR w.name ILIKE $1 || '%' - OR w.name ILIKE '% ' || $1 || '%' - OR wi.name ILIKE $1 || '%' - OR wi.name ILIKE '% ' || $1 || '%' - ) + WHERE ${sql} ORDER BY ${orderBy} - LIMIT $2 - OFFSET $3 + LIMIT $${params.length + 1} + OFFSET $${params.length + 2} `, - [search, limit, offset], + [...params, limit, offset], ); const countResult = await pool.query( @@ -244,15 +368,9 @@ const resolvers = { SELECT COUNT(*) FROM wines w JOIN wineries wi ON w.winery_id = wi.id - WHERE ( - $1::text IS NULL - OR w.name ILIKE $1 || '%' - OR w.name ILIKE '% ' || $1 || '%' - OR wi.name ILIKE $1 || '%' - OR wi.name ILIKE '% ' || $1 || '%' - ) + WHERE ${sql} `, - [search], + params, ); return { @@ -261,6 +379,35 @@ const resolvers = { }; }, + catalogOptions: async () => { + // Alle druer som minst én vin har, ikke bare de vanligste som på + // Explore by taste, så filteret dekker hele datasettet. + const [types, countries, grapes] = await Promise.all([ + pool.query(`SELECT DISTINCT type FROM wines ORDER BY type`), + pool.query(`SELECT code, name FROM countries ORDER BY name`), + pool.query(` + SELECT g.name + FROM grapes g + WHERE EXISTS (SELECT 1 FROM wine_grapes wg WHERE wg.grape_id = g.id) + ORDER BY g.name + `), + ]); + + return { + types: types.rows.map((r) => r.type), + countries: countries.rows, + grapes: grapes.rows.map((r) => r.name), + }; + }, + + regions: async (_: unknown, args: { country: string }) => { + const result = await pool.query( + `SELECT id, name FROM regions WHERE country_code = $1 ORDER BY name`, + [args.country], + ); + return result.rows; + }, + wine: async (_: unknown, args: { id: string }) => { const wineId = Number(args.id); diff --git a/client/src/components/WineCatalog.css b/client/src/components/WineCatalog.css index a2784d3..289e7e2 100644 --- a/client/src/components/WineCatalog.css +++ b/client/src/components/WineCatalog.css @@ -16,6 +16,62 @@ font-weight: 600; } +/* Filter dropdowns in a grid: as many columns as fit the screen */ +.catalog-filters { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + gap: 1rem; + align-items: end; + margin: 1.5rem 0 0; + padding: 1rem 1.25rem 1.25rem; + background: var(--color-panel); + border: 1px solid var(--color-border); + border-radius: 0.75rem; +} + +.catalog-filters legend { + padding: 0 0.25rem; + font-weight: 600; +} + +.catalog-field select:disabled { + color: var(--color-muted); + cursor: not-allowed; +} + +/* Checkbox and text on one line; the whole label is clickable */ +.catalog-check { + display: flex; + align-items: center; + gap: 0.5rem; + min-height: 2.75rem; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; +} + +.catalog-check input { + width: 1.25rem; + height: 1.25rem; + accent-color: var(--color-accent); +} + +.catalog-reset { + min-height: 2.75rem; + padding: 0.45rem 1rem; + font: inherit; + color: var(--color-accent); + background: transparent; + border: 2px solid var(--color-accent); + border-radius: 0.5rem; + cursor: pointer; +} + +.catalog-reset:hover { + color: var(--color-accent-ink); + background: var(--color-accent); +} + .catalog-field input, .catalog-field select { /* At least 44 px high, so they are easy to hit on touch screens */ diff --git a/client/src/components/WineCatalog.tsx b/client/src/components/WineCatalog.tsx index 6f5a3d2..a360cc6 100644 --- a/client/src/components/WineCatalog.tsx +++ b/client/src/components/WineCatalog.tsx @@ -3,16 +3,40 @@ import { useQuery } from '@apollo/client/react'; import WineList from './WineList'; import Pagination from './Pagination'; import SortSelect from './SortSelect'; +import WineFilters from './WineFilters'; import { + CATALOG_OPTIONS_QUERY, + REGIONS_QUERY, WINES_QUERY, + type CatalogOptionsData, + type RegionsData, + type WineFilterInput, type WinesData, type WinesVariables, } from '../graphql/queries'; -import type { WineSort } from '../types/wine'; +import { EMPTY_CATALOG_FILTER } from '../data/catalog'; +import { POPULAR_MIN_RATINGS } from '../data/taste'; +import type { CatalogFilter, WineSort } from '../types/wine'; import './WineCatalog.css'; export const PAGE_SIZE = 20; +/** + * Turns the filter state into the GraphQL filter. + * Empty values become null, which the backend reads as "no filter". + */ +function toFilterInput(filter: CatalogFilter): WineFilterInput { + return { + type: filter.type || null, + country: filter.country || null, + region: filter.region || null, + grape: filter.grape || null, + body: filter.body || null, + acidity: filter.acidity || null, + minRatingCount: filter.popularOnly ? POPULAR_MIN_RATINGS : null, + }; +} + /** * Fetches wines from the GraphQL backend and passes them to WineList. * @@ -28,6 +52,7 @@ export default function WineCatalog() { const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [sort, setSort] = useState('NAME_ASC'); + const [filter, setFilter] = useState(EMPTY_CATALOG_FILTER); const offset = (page - 1) * PAGE_SIZE; useEffect(() => { @@ -45,12 +70,20 @@ export default function WineCatalog() { >(WINES_QUERY, { variables: { search: search || null, + filter: toFilterInput(filter), sort, offset, limit: PAGE_SIZE, }, }); + const options = useQuery(CATALOG_OPTIONS_QUERY); + // Regions are only fetched once a country is chosen + const regions = useQuery(REGIONS_QUERY, { + variables: { country: filter.country }, + skip: !filter.country, + }); + const shown = data ?? previousData; const pageChanged = useRef(false); @@ -73,6 +106,12 @@ export default function WineCatalog() { setPage(1); } + // New filters give a new list too + function changeFilter(newFilter: CatalogFilter) { + setFilter(newFilter); + setPage(1); + } + // role="status" and role="alert" make screen readers announce the messages if (error) { return

Could not load wines. Please try again later.

; @@ -101,6 +140,13 @@ export default function WineCatalog() {
+ + void; + /** Choices for the dropdowns. Undefined while they are loading. */ + options: CatalogOptionsData['catalogOptions'] | undefined; + /** Regions in the chosen country. Undefined when no country is chosen. */ + regions: RegionsData['regions'] | undefined; +}; + +/** + * The filter panel in the wine catalog. + * + * Controlled component, like TasteFilters: the values live in WineCatalog, + * which sends them to the backend. The filtering happens in the database, + * so it covers the whole dataset, not just the page that is shown. + * + *
and tell screen readers that the dropdowns belong + * together, and every set('type', e.target.value)} + > + + {options?.types.map((t) => ( + + ))} + +
+ +
+ + +
+ +
+ + {/* Disabled until a country is chosen, so the list stays short */} + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + ); +} diff --git a/client/src/components/WineList.tsx b/client/src/components/WineList.tsx index b617219..6db48a9 100644 --- a/client/src/components/WineList.tsx +++ b/client/src/components/WineList.tsx @@ -68,6 +68,12 @@ export default function WineList({ wines, totalCount, offset = 0 }: Props) {
{wine.abv != null ? `${wine.abv.toFixed(1)} %` : 'Unknown'}
+ +
Body
+
{wine.body ?? 'Unknown'}
+ +
Acidity
+
{wine.acidity ?? 'Unknown'}
diff --git a/client/src/data/catalog.ts b/client/src/data/catalog.ts new file mode 100644 index 0000000..9fc4b56 --- /dev/null +++ b/client/src/data/catalog.ts @@ -0,0 +1,17 @@ +import type { CatalogFilter } from '../types/wine'; + +/** + * Constants for the wine catalog. + * They live in their own file (not in a component file) so that + * React Fast Refresh keeps working, the same reason as data/taste.ts. + */ + +export const EMPTY_CATALOG_FILTER: CatalogFilter = { + type: '', + country: '', + region: '', + grape: '', + body: '', + acidity: '', + popularOnly: false, +}; diff --git a/client/src/data/exampleWines.ts b/client/src/data/exampleWines.ts index 4f2c2d7..a569be4 100644 --- a/client/src/data/exampleWines.ts +++ b/client/src/data/exampleWines.ts @@ -15,6 +15,8 @@ export const exampleWines: WineListItem[] = [ country: 'Italy', avgRating: 4.5, abv: 14.5, + body: 'Full-bodied', + acidity: 'High', }, { id: '2', @@ -25,6 +27,8 @@ export const exampleWines: WineListItem[] = [ country: 'France', avgRating: 4.1, abv: 12.5, + body: 'Light-bodied', + acidity: 'High', }, { id: '3', @@ -35,6 +39,8 @@ export const exampleWines: WineListItem[] = [ country: 'Spain', avgRating: 3.9, abv: 13.5, + body: 'Full-bodied', + acidity: 'Medium', }, { id: '4', @@ -45,5 +51,7 @@ export const exampleWines: WineListItem[] = [ country: 'Germany', avgRating: null, abv: null, + body: null, + acidity: null, }, ]; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index 2aa2678..176909f 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -1,5 +1,10 @@ import { gql } from '@apollo/client'; -import type { TasteWine, WineListItem, WineSort } from '../types/wine'; +import type { + CatalogFilter, + TasteWine, + WineListItem, + WineSort, +} from '../types/wine'; /** * GraphQL queries used by the client. @@ -16,8 +21,20 @@ import type { TasteWine, WineListItem, WineSort } from '../types/wine'; * never load the whole dataset (scalability and sustainability). */ export const WINES_QUERY = gql` - query Wines($search: String, $sort: WineSort, $offset: Int, $limit: Int) { - wines(search: $search, sort: $sort, offset: $offset, limit: $limit) { + query Wines( + $search: String + $filter: WineFilter + $sort: WineSort + $offset: Int + $limit: Int + ) { + wines( + search: $search + filter: $filter + sort: $sort + offset: $offset + limit: $limit + ) { totalCount items { id @@ -28,6 +45,8 @@ export const WINES_QUERY = gql` country avgRating abv + body + acidity } } } @@ -37,13 +56,59 @@ export type WinesData = { wines: { totalCount: number; items: WineListItem[] }; }; +/** Filters as the backend expects them: null means "no filter". */ +export type WineFilterInput = { + [K in Exclude]: string | null; +} & { minRatingCount: number | null }; + export type WinesVariables = { search?: string | null; + filter?: WineFilterInput; sort?: WineSort; offset?: number; limit?: number; }; +/** The choices in the filter dropdowns in the wine catalog. */ +export const CATALOG_OPTIONS_QUERY = gql` + query CatalogOptions { + catalogOptions { + types + countries { + code + name + } + grapes + } + } +`; + +export type CatalogOptionsData = { + catalogOptions: { + types: string[]; + countries: { code: string; name: string }[]; + grapes: string[]; + }; +}; + +/** + * The regions in one country, for the region filter. + * Fetched only when a country is chosen, since the full dataset has + * thousands of regions. + */ +export const REGIONS_QUERY = gql` + query Regions($country: String!) { + regions(country: $country) { + id + name + } + } +`; + +export type RegionsData = { + regions: { id: string; name: string }[]; +}; + /** * Wines for the Explore by taste map. * diff --git a/client/src/types/wine.ts b/client/src/types/wine.ts index 8c177ad..95c8dbb 100644 --- a/client/src/types/wine.ts +++ b/client/src/types/wine.ts @@ -14,6 +14,24 @@ export type WineListItem = { avgRating: number | null; /** Alcohol in percent, null when unknown */ abv: number | null; + /** null when X-Wines has no value for the wine */ + body: BodyLevel | null; + acidity: AcidityLevel | null; +}; + +/** + * Filters in the wine catalog. Empty strings mean "no filter". + * country is a country code (e.g. 'FR') and region a region id. + */ +export type CatalogFilter = { + type: string; + country: string; + region: string; + grape: string; + body: string; + acidity: string; + /** Only wines with at least POPULAR_MIN_RATINGS ratings */ + popularOnly: boolean; }; /** Sort orders for the wine catalog. Matches enum WineSort in the backend. */ From 4fa4684093afd89542e91677e5acf1f8674f6556 Mon Sep 17 00:00:00 2001 From: adelethorberg-sudo Date: Tue, 20 Oct 2026 12:42:59 +0200 Subject: [PATCH 7/8] feature/explore-by-taste-endring2 --- backend/src/index.ts | 54 +++++++++++++++--------- client/src/components/ExploreByTaste.tsx | 19 ++++----- client/src/data/taste.ts | 2 +- client/src/graphql/queries.ts | 3 +- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 1a7054c..852ee98 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -153,7 +153,7 @@ const typeDefs = `#graphql ): WinePage! # Viner til smakskartet. Bare viner med både fylde og syre kommer med. - tasteMap(filter: TasteFilter, limit: Int = 150): WinePage! + tasteMap(filter: TasteFilter, limit: Int = 100): WinePage! tasteOptions: TasteOptions! } @@ -349,7 +349,7 @@ const resolvers = { _: unknown, args: { filter?: TasteFilter | null; limit?: number }, ) => { - const limit = args.limit ?? 150; + const limit = args.limit ?? 100; if (!Number.isInteger(limit) || limit < 1 || limit > TASTE_MAP_MAX) { throw new GraphQLError(`limit must be between 1 and ${TASTE_MAP_MAX}`, { extensions: { code: 'BAD_USER_INPUT' }, @@ -360,26 +360,40 @@ const resolvers = { // Vi viser de vinene flest har vurdert, så kartet fylles med kjente // viner i stedet for viner med én enkelt vurdering på 5.0. + // De mest vurderte er nesten bare rødvin, så vi tar dem på omgang per + // type: den mest vurderte av hver type først, så den nest mest vurderte + // osv. Uten typefilter blir kartet en jevn miks (100 viner = 20 av hver + // av de fem typene); med typefilter finnes bare én type, og rekkefølgen + // blir som før. 'Dessert' og 'Dessert/Port' regnes som én type, slik + // som i fargeforklaringen på kartet. const winesResult = await pool.query( ` - SELECT - w.id, - w.name, - w.type, - w.abv::float AS abv, - w.body, - w.acidity, - wi.name AS winery, - r.name AS region, - c.name AS country, - w.avg_rating::float AS "avgRating", - w.rating_count AS "ratingCount" - FROM wines w - JOIN wineries wi ON w.winery_id = wi.id - JOIN regions r ON w.region_id = r.id - JOIN countries c ON w.country_code = c.code - WHERE ${sql} - ORDER BY w.rating_count DESC, w.id + SELECT * + FROM ( + SELECT + w.id, + w.name, + w.type, + w.abv::float AS abv, + w.body, + w.acidity, + wi.name AS winery, + r.name AS region, + c.name AS country, + w.avg_rating::float AS "avgRating", + w.rating_count AS "ratingCount", + ROW_NUMBER() OVER ( + PARTITION BY + CASE WHEN w.type LIKE 'Dessert%' THEN 'Dessert' ELSE w.type END + ORDER BY w.rating_count DESC, w.id + ) AS type_rank + FROM wines w + JOIN wineries wi ON w.winery_id = wi.id + JOIN regions r ON w.region_id = r.id + JOIN countries c ON w.country_code = c.code + WHERE ${sql} + ) ranked + ORDER BY type_rank, "ratingCount" DESC, id LIMIT $${params.length + 1} `, [...params, limit], diff --git a/client/src/components/ExploreByTaste.tsx b/client/src/components/ExploreByTaste.tsx index a8a30ad..b6a7a1d 100644 --- a/client/src/components/ExploreByTaste.tsx +++ b/client/src/components/ExploreByTaste.tsx @@ -46,7 +46,8 @@ function toVariables(filter: TasteFilter) { * only display what they get as props (same split as WineCatalog/WineList). * * Scalability: the map never loads the whole dataset. The backend returns at - * most TASTE_MAP_LIMIT wines (the most-rated ones that match the filters), + * most TASTE_MAP_LIMIT wines (the most-rated ones that match the filters, + * taken in turn from each wine type so the map is not almost all red), * and totalCount tells how many match in total. */ export default function ExploreByTaste() { @@ -92,17 +93,15 @@ export default function ExploreByTaste() { aria-label="Taste map" aria-busy={loading} > +

Wine map

{/* role="status" announces the new count when filters change */} -

- {shown - ? `${shown.totalCount.toLocaleString('en')} wines found` - : 'Loading wines …'} +

+ {!shown + ? 'Loading wines …' + : shown.totalCount > wines.length + ? `Showing ${wines.length} wines` + : `${shown.totalCount.toLocaleString('en')} ${shown.totalCount === 1 ? 'wine' : 'wines'} found.`}

- {shown && shown.totalCount > wines.length && ( -

- The map shows the {wines.length} most-rated of them. -

- )} {error && (

Could not load wines. Please try again later.

diff --git a/client/src/data/taste.ts b/client/src/data/taste.ts index 98f4da2..c953fd3 100644 --- a/client/src/data/taste.ts +++ b/client/src/data/taste.ts @@ -39,7 +39,7 @@ export function wineTypeColor(type: string): string { } /** How many wines the map shows at most. The backend allows up to 300. */ -export const TASTE_MAP_LIMIT = 150; +export const TASTE_MAP_LIMIT = 100; /** "Only popular wines" means at least this many ratings. */ export const POPULAR_MIN_RATINGS = 100; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index aa33aef..c8d73f2 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -45,7 +45,8 @@ export type WinesVariables = { /** * Wines for the Explore by taste map. * - * The backend returns the most-rated wines that match the filters, at most + * The backend returns the most-rated wines that match the filters, taken in + * turn from each wine type so all types show up, at most * `limit` of them. totalCount is the number of all matching wines. */ export const TASTE_MAP_QUERY = gql` From ee01558ca1bc6c7b8dd0dc29ca1f8bee04bf24c0 Mon Sep 17 00:00:00 2001 From: adelethorberg-sudo Date: Tue, 20 Oct 2026 13:45:19 +0200 Subject: [PATCH 8/8] feature/lag-siden-find-wine-by-food --- backend/src/index.ts | 94 +++++-- client/src/App.tsx | 3 + client/src/assets/food/cheese.jpg | Bin 0 -> 8018 bytes client/src/assets/food/dessert.jpg | Bin 0 -> 8164 bytes client/src/assets/food/fish.jpg | Bin 0 -> 8726 bytes client/src/assets/food/meat.jpg | Bin 0 -> 8846 bytes client/src/assets/food/pasta.jpg | Bin 0 -> 9654 bytes client/src/assets/food/poultry.jpg | Bin 0 -> 9157 bytes client/src/assets/food/snacks.jpg | Bin 0 -> 7837 bytes client/src/assets/food/spicy.jpg | Bin 0 -> 10005 bytes client/src/assets/food/vegetarian.jpg | Bin 0 -> 10163 bytes client/src/components/FindWineByFood.css | 342 +++++++++++++++++++++++ client/src/components/FindWineByFood.tsx | 227 +++++++++++++++ client/src/components/FoodWineCard.tsx | 151 ++++++++++ client/src/components/SiteHeader.tsx | 3 + client/src/data/food.ts | 91 ++++++ client/src/graphql/queries.ts | 56 ++++ client/src/index.css | 10 + client/src/types/wine.ts | 14 + 19 files changed, 970 insertions(+), 21 deletions(-) create mode 100644 client/src/assets/food/cheese.jpg create mode 100644 client/src/assets/food/dessert.jpg create mode 100644 client/src/assets/food/fish.jpg create mode 100644 client/src/assets/food/meat.jpg create mode 100644 client/src/assets/food/pasta.jpg create mode 100644 client/src/assets/food/poultry.jpg create mode 100644 client/src/assets/food/snacks.jpg create mode 100644 client/src/assets/food/spicy.jpg create mode 100644 client/src/assets/food/vegetarian.jpg create mode 100644 client/src/components/FindWineByFood.css create mode 100644 client/src/components/FindWineByFood.tsx create mode 100644 client/src/components/FoodWineCard.tsx create mode 100644 client/src/data/food.ts diff --git a/backend/src/index.ts b/backend/src/index.ts index b59e5f8..a06f4b6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -26,6 +26,11 @@ function readPaging(args: { limit?: number; offset?: number }) { // Flere enn ca. 300 prikker blir uleselig og tungt å tegne. const TASTE_MAP_MAX = 300; +// Find wine by food: viner med minst så mange vurderinger kommer først. +// Ellers havner ukjente viner med én vurdering øverst, fordi de tilfeldigvis +// passer til flest retter i kategorien. +const FOOD_POPULAR_MIN = 50; + /** * ORDER BY for hver sortering i vinkatalogen. * Klienten sender bare navnet (enum WineSort), og SQL-en hentes herfra, @@ -223,6 +228,30 @@ const typeDefs = `#graphql type FoodCategory { name: String! wineCount: Int! + # Rettene i kategorien, verdier fra Harmonize, f.eks. ["Beef", "Lamb"] + foods: [String!]! + } + + # En vin på Find wine by food, med rettene i den valgte kategorien som + # vinen passer til ifølge Harmonize + type FoodWine { + id: ID! + name: String! + type: String! + abv: Float + body: String + acidity: String + winery: String! + region: String! + country: String! + avgRating: Float + ratingCount: Int! + matchedFoods: [String!]! + } + + type FoodWinePage { + items: [FoodWine!]! + totalCount: Int! } # Sortering i vinkatalogen @@ -304,11 +333,15 @@ const typeDefs = `#graphql foodCategories: [FoodCategory!]! + # Viner som passer til en matkategori. food snevrer inn til én rett i + # kategorien. De som passer til flest retter i kategorien kommer først, + # deretter de flest har vurdert. winesByFoodCategory( category: String! + food: String limit: Int = 20 offset: Int = 0 - ): WinePage! + ): FoodWinePage! # Viner til smakskartet. Bare viner med både fylde og syre kommer med. tasteMap(filter: TasteFilter, limit: Int = 100): WinePage! @@ -468,7 +501,10 @@ const resolvers = { foodCategories: async () => { const result = await pool.query( ` - SELECT f.category AS name, count(DISTINCT wf.wine_id)::int AS "wineCount" + SELECT + f.category AS name, + count(DISTINCT wf.wine_id)::int AS "wineCount", + array_agg(DISTINCT f.name ORDER BY f.name) AS foods FROM foods f JOIN wine_foods wf ON wf.food_id = f.id WHERE f.category IS NOT NULL @@ -481,46 +517,62 @@ const resolvers = { winesByFoodCategory: async ( _: unknown, - args: { category: string; limit?: number; offset?: number }, + args: { + category: string; + food?: string | null; + limit?: number; + offset?: number; + }, ) => { const { limit, offset } = readPaging(args); - const inCategory = ` - EXISTS ( - SELECT 1 - FROM wine_foods wf - JOIN foods f ON f.id = wf.food_id - WHERE wf.wine_id = w.id AND f.category = $1 - )`; + + // Én rad per vin, med rettene i kategorien vinen passer til. + // Med food må vinen passe til akkurat den retten, men listen viser + // fortsatt alle rettene i kategorien den passer til. + const matches = ` + SELECT wf.wine_id, array_agg(f.name ORDER BY f.name) AS foods + FROM wine_foods wf + JOIN foods f ON f.id = wf.food_id + WHERE f.category = $1 + GROUP BY wf.wine_id + HAVING $2::text IS NULL OR bool_or(f.name = $2::text)`; + const params = [args.category, args.food || null]; const winesResult = await pool.query( ` + WITH matches AS (${matches}) SELECT w.id, w.name, w.type, - w.abv, + w.abv::float AS abv, w.body, w.acidity, wi.name AS winery, r.name AS region, c.name AS country, - w.avg_rating AS "avgRating", - w.rating_count AS "ratingCount" - FROM wines w + w.avg_rating::float AS "avgRating", + w.rating_count AS "ratingCount", + m.foods AS "matchedFoods" + FROM matches m + JOIN wines w ON w.id = m.wine_id JOIN wineries wi ON w.winery_id = wi.id JOIN regions r ON w.region_id = r.id JOIN countries c ON w.country_code = c.code - WHERE ${inCategory} - ORDER BY w.name, w.id - LIMIT $2 - OFFSET $3 + ORDER BY + w.rating_count >= $3 DESC, + cardinality(m.foods) DESC, + w.rating_count DESC, + w.id + LIMIT $4 + OFFSET $5 `, - [args.category, limit, offset], + [...params, FOOD_POPULAR_MIN, limit, offset], ); const countResult = await pool.query( - `SELECT count(*)::int AS count FROM wines w WHERE ${inCategory}`, - [args.category], + `WITH matches AS (${matches}) SELECT count(*)::int AS count FROM matches`, + params, ); return { diff --git a/client/src/App.tsx b/client/src/App.tsx index 1249a83..be3fa02 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,6 +2,7 @@ import { Link, Route, Routes, useParams } from 'react-router'; import SiteHeader from './components/SiteHeader'; import ExploreByTaste from './components/ExploreByTaste'; import WineCatalog from './components/WineCatalog'; +import FindWineByFood from './components/FindWineByFood'; /** * NB: Temporary placeholder. The detail page is not built yet. @@ -24,6 +25,7 @@ function WineDetailsPlaceholder() { * * SiteHeader is shown on every page. Routes picks the page from the URL: * - / Explore by taste (front page) + * - /food Find wine by food * - /wines the wine catalog * - /wines/:id detail page (placeholder for now) * @@ -45,6 +47,7 @@ export default function App() { } /> + } /> } /> diff --git a/client/src/assets/food/cheese.jpg b/client/src/assets/food/cheese.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b681eb1cebdc9f1e2b40d21e29b48ae941747f39 GIT binary patch literal 8018 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 literal 0 HcmV?d00001 diff --git a/client/src/assets/food/dessert.jpg b/client/src/assets/food/dessert.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a42c4fe328fd509370c083312e54ef7ee604f4a7 GIT binary patch literal 8164 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 CmrR!c literal 0 HcmV?d00001 diff --git a/client/src/assets/food/meat.jpg b/client/src/assets/food/meat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..752e0a810219e15a9e96e50bdae00f0619ca8fe4 GIT binary patch literal 8846 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 literal 0 HcmV?d00001 diff --git a/client/src/assets/food/pasta.jpg b/client/src/assets/food/pasta.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d6e1104b93aed6d96c720b1919a18270c97c613 GIT binary patch literal 9654 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 QQy>Qmn#P?MB#WN=4+~o&ZU6uP literal 0 HcmV?d00001 diff --git a/client/src/assets/food/poultry.jpg b/client/src/assets/food/poultry.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2618c0497e8b08421c0aeff56407599267b48b60 GIT binary patch literal 9157 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 Uq9^^f1*H#Pu@=Oz65r1M2S}<800000 literal 0 HcmV?d00001 diff --git a/client/src/components/FindWineByFood.css b/client/src/components/FindWineByFood.css new file mode 100644 index 0000000..31409d5 --- /dev/null +++ b/client/src/components/FindWineByFood.css @@ -0,0 +1,342 @@ +/* + * Find wine by food. + * Colours come from the variables in index.css. + */ + +/* ---------- Hero ---------- */ + +.food-hero { + background: + radial-gradient(circle at 82% 50%, rgb(123 30 43 / 0.12), transparent 45%), + linear-gradient(90deg, var(--color-bg) 35%, var(--color-panel)); + border-bottom: 1px solid var(--color-border); +} + +.food-hero-inner { + max-width: 78rem; + margin: 0 auto; + padding: 3rem 1rem; +} + +.food-eyebrow { + margin: 0 0 0.4rem; + font-size: 0.75rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--color-muted); +} + +.food-hero h1 { + margin: 0 0 0.5rem; + font-family: var(--font-display); + font-weight: 400; + font-size: clamp(2rem, 5vw, 3rem); + line-height: 1.1; + color: var(--color-text); +} + +.food-hero p:last-child { + margin: 0; + max-width: 28rem; + font-size: 1.1rem; + color: var(--color-muted); +} + +/* ---------- Page ---------- */ + +main.food-page { + max-width: 78rem; + display: grid; + gap: 2rem; + padding: 1.75rem 1rem 3rem; +} + +.food-page h2 { + margin: 0 0 1rem; + font-family: var(--font-display); + font-weight: 400; + font-size: 1.6rem; +} + +.food-note { + margin: 0; + font-size: 0.9rem; + color: var(--color-muted); +} + +/* ---------- Category cards ---------- */ + +.food-categories { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); + gap: 0.75rem; + margin: 0; + padding: 0; + list-style: none; +} + +.food-category { + width: 100%; + height: 100%; + display: grid; + justify-items: center; + gap: 0.15rem; + padding: 0.5rem 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: 0.75rem; + background: var(--color-surface); + color: var(--color-text); + font: inherit; + cursor: pointer; + transition: + border-color 0.15s, + background 0.15s; +} + +.food-category:hover { + border-color: var(--color-muted); +} + +.food-category[aria-pressed='true'] { + border-color: var(--color-accent); + background: var(--color-panel); + box-shadow: inset 0 0 0 1px var(--color-accent); +} + +/* Photo tile at the top of the card */ +.food-category-image { + width: 100%; + height: 5.25rem; + margin-bottom: 0.35rem; + border-radius: 0.5rem; + object-fit: cover; +} + +/* Same height as the photos, so the names line up */ +.food-category-emoji { + display: grid; + place-items: center; + height: 5.25rem; + margin-bottom: 0.35rem; + font-size: 2.8rem; +} + +.food-category-name { + font-weight: 600; +} + +/* ---------- Dish chips ---------- */ + +.food-dishes { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; +} + +.food-dishes-label { + margin-right: 0.25rem; + font-size: 0.85rem; + font-weight: 600; +} + +.food-dishes button { + padding: 0.35rem 0.85rem; + border: 1px solid var(--color-border); + border-radius: 999px; + background: var(--color-surface); + color: var(--color-text); + font: inherit; + font-size: 0.88rem; + cursor: pointer; +} + +.food-dishes button:hover { + border-color: var(--color-muted); +} + +.food-dishes button[aria-pressed='true'] { + border-color: var(--color-accent); + background: var(--color-accent); + color: var(--color-accent-ink); +} + +/* ---------- Results ---------- */ + +.food-results { + display: grid; + gap: 1rem; +} + +.food-results-head { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + gap: 0.25rem 1rem; +} + +.food-results-head h2 { + margin: 0; + scroll-margin-top: 1rem; +} + +.food-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr)); + gap: 1rem; + margin: 0; + padding: 0; + list-style: none; +} + +.food-card { + height: 100%; + display: grid; + grid-template-columns: 4.5rem minmax(0, 1fr); + gap: 1rem; + padding: 1.1rem; + border: 1px solid var(--color-border); + border-radius: 0.75rem; + background: var(--color-surface); +} + +.food-bottle { + width: 100%; + height: auto; + max-height: 10rem; + align-self: center; +} + +.food-card-body { + display: grid; + align-content: start; + gap: 0.5rem; +} + +.food-card h3 { + margin: 0; + font-family: var(--font-display); + font-weight: 400; + font-size: 1.25rem; + line-height: 1.2; +} + +.food-card h3 a { + color: var(--color-text); + text-decoration: none; +} + +.food-card h3 a:hover { + color: var(--color-accent); + text-decoration: underline; +} + +.food-winery { + margin: -0.35rem 0 0; + font-size: 0.9rem; + color: var(--color-muted); +} + +/* "Pairs with" chip, in place of the "% match" chip in the sketch */ +.food-match { + justify-self: start; + margin: 0; + padding: 0.2rem 0.65rem; + border-radius: 999px; + background: var(--color-panel); + color: var(--color-accent); + font-size: 0.8rem; + font-weight: 600; +} + +.food-meta { + display: flex; + flex-wrap: wrap; + gap: 0.2rem 0.9rem; + margin: 0; + padding: 0; + list-style: none; + font-size: 0.82rem; + color: var(--color-muted); +} + +.food-meta li { + display: inline-flex; + align-items: center; + gap: 0.3rem; +} + +.food-meta svg { + width: 0.9rem; + height: 0.9rem; + color: var(--color-accent); +} + +.food-star { + color: var(--color-star); +} + +.food-bars { + display: grid; + gap: 0.35rem; + margin-top: 0.25rem; +} + +.food-bar-row { + display: grid; + grid-template-columns: 4.25rem 1fr; + align-items: center; + gap: 0.6rem; + font-size: 0.82rem; +} + +.food-bar { + height: 0.45rem; + overflow: hidden; + border-radius: 999px; + background: var(--color-sunk); +} + +.food-bar i { + display: block; + height: 100%; + border-radius: 999px; + background: var(--color-accent); +} + +/* ---------- Why this pairing works ---------- */ + +.food-why { + display: flex; + gap: 1.25rem; + align-items: flex-start; + padding: 1.25rem 1.5rem; + border-radius: 0.75rem; + background: var(--color-panel); +} + +.food-why svg { + flex: none; + width: 2.25rem; + height: 2.25rem; + color: var(--color-accent); +} + +.food-why h2 { + margin: 0 0 0.25rem; + font-size: 1.25rem; +} + +.food-why p { + margin: 0; + max-width: 50rem; + color: var(--color-muted); +} + +@media (prefers-reduced-motion: reduce) { + .food-category { + transition: none; + } +} diff --git a/client/src/components/FindWineByFood.tsx b/client/src/components/FindWineByFood.tsx new file mode 100644 index 0000000..53b0d11 --- /dev/null +++ b/client/src/components/FindWineByFood.tsx @@ -0,0 +1,227 @@ +import { useSearchParams } from 'react-router'; +import { useQuery } from '@apollo/client/react'; +import FoodWineCard from './FoodWineCard'; +import Pagination from './Pagination'; +import { + FOOD_CATEGORIES_QUERY, + WINES_BY_FOOD_QUERY, + type FoodCategoriesData, + type WinesByFoodData, +} from '../graphql/queries'; +import { + DEFAULT_FOOD_CATEGORY, + FOOD_CATEGORY_INFO, + FOOD_PAGE_SIZE, + foodCategoryOrder, +} from '../data/food'; +import './FindWineByFood.css'; + +/** + * Find wine by food. + * + * The user picks a food category (e.g. Meat), and can narrow it down to one + * dish (e.g. Beef). We show the wines that pair with it according to the + * Harmonize column in X-Wines, and explain why the pairing works. + * + * On purpose there are no wine filters or sorting here: the user chooses by + * food only. The backend decides the order: well-known wines (at least 50 + * ratings) that pair with the most dishes in the category come first. + * + * The choices live in the URL (?category=Meat&food=Beef&page=2), so the back + * button works and a search can be shared as a link. + */ +export default function FindWineByFood() { + const [params, setParams] = useSearchParams(); + const category = params.get('category') ?? DEFAULT_FOOD_CATEGORY; + const food = params.get('food'); + const page = Math.max(1, Number(params.get('page')) || 1); + + const categoriesQuery = useQuery(FOOD_CATEGORIES_QUERY); + const { data, previousData, loading, error } = useQuery( + WINES_BY_FOOD_QUERY, + { + variables: { + category, + food, + limit: FOOD_PAGE_SIZE, + offset: (page - 1) * FOOD_PAGE_SIZE, + }, + }, + ); + + const categories = [...(categoriesQuery.data?.foodCategories ?? [])].sort( + (a, b) => + foodCategoryOrder(a.name) - foodCategoryOrder(b.name) || + a.name.localeCompare(b.name), + ); + const current = categories.find((c) => c.name === category); + const info = FOOD_CATEGORY_INFO[category]; + const label = info?.label ?? category; + + // Keep showing the previous wines while new ones load, so the list + // doesn't flash empty on every click + const shown = (data ?? previousData)?.winesByFoodCategory; + const wines = shown?.items ?? []; + const totalPages = shown ? Math.ceil(shown.totalCount / FOOD_PAGE_SIZE) : 0; + + /** Changes the URL. A new category or dish starts at page 1. */ + function choose(next: { category?: string; food?: string | null }) { + const p = new URLSearchParams(); + p.set('category', next.category ?? category); + const nextFood = next.category ? null : (next.food ?? null); + if (nextFood) p.set('food', nextFood); + setParams(p); + } + + function goToPage(n: number) { + const p = new URLSearchParams(params); + p.set('page', String(n)); + setParams(p); + document + .getElementById('food-results-heading') + ?.scrollIntoView({ block: 'start' }); + } + + return ( + <> +
+
+

Explore

+

Find wine by food

+

+ Choose what you’re eating and discover wines that make it even + better. +

+
+
+ +
+
+

Choose a food category

+ {categoriesQuery.error && ( +

Could not load the food categories.

+ )} +
    + {categories.map((c) => { + const cInfo = FOOD_CATEGORY_INFO[c.name]; + return ( +
  • + +
  • + ); + })} +
+ + {current && current.foods.length > 1 && ( +
+ + + {current.foods.map((f) => ( + + ))} +
+ )} +
+ +
+
+

Best matches for {food ?? label}

+ {/* role="status" announces the new count when the choice changes */} +

+ {!shown + ? 'Loading wines …' + : `${shown.totalCount.toLocaleString('en')} ${shown.totalCount === 1 ? 'wine' : 'wines'} pair with ${food ?? label.toLowerCase()}`} +

+
+ + {error && ( +

Could not load wines. Please try again later.

+ )} + {shown && wines.length === 0 && ( +

No wines pair with this food yet.

+ )} + +
    + {wines.map((w) => ( +
  • + +
  • + ))} +
+ + +
+ + {info && ( + + )} +
+ + ); +} diff --git a/client/src/components/FoodWineCard.tsx b/client/src/components/FoodWineCard.tsx new file mode 100644 index 0000000..225e1a9 --- /dev/null +++ b/client/src/components/FoodWineCard.tsx @@ -0,0 +1,151 @@ +import { Link } from 'react-router'; +import type { FoodWine } from '../types/wine'; +import { ACIDITY_LEVELS, BODY_LEVELS, wineTypeColor } from '../data/taste'; + +/** + * A wine bottle in the colour of the wine type. Decorative only. + * X-Wines has no pictures of the wines, so we draw one. + */ +function Bottle({ type }: { type: string }) { + return ( + + ); +} + +/** One row with label and bar, e.g. "Body ████░". The exact value is read by screen readers and shown on hover. */ +function Bar(props: { label: string; share: number; value: string }) { + return ( +
+ {props.label} +
+ +
+
+ ); +} + +/** + * A card on Find wine by food: the wine, where it is from, which dishes in + * the chosen category it pairs with, and its taste. + * + * Display only, like WineList: it gets the wine as a prop. + */ +export default function FoodWineCard({ wine }: { wine: FoodWine }) { + return ( +
+ + +
+

+ {/* + NB: The detail page (/wines/:id) is not built yet. Until then this + link opens a placeholder page, see App.tsx. + */} + {wine.name} +

+

{wine.winery}

+ +

+ Pairs with + {wine.matchedFoods.join(', ')} +

+ +
    +
  • + + {wine.region}, {wine.country} +
  • +
  • + + {wine.type} wine +
  • +
  • + + {wine.avgRating != null + ? `${wine.avgRating.toFixed(1)} (${wine.ratingCount.toLocaleString('en')} ratings)` + : 'No ratings yet'} +
  • +
+ +
+ {wine.body && ( + + )} + {wine.acidity && ( + + )} + {wine.abv != null && ( + + )} +
+
+
+ ); +} diff --git a/client/src/components/SiteHeader.tsx b/client/src/components/SiteHeader.tsx index a348fda..18bb0eb 100644 --- a/client/src/components/SiteHeader.tsx +++ b/client/src/components/SiteHeader.tsx @@ -39,6 +39,9 @@ export default function SiteHeader() {
  • Wines
  • +
  • + Food pairing +
  • diff --git a/client/src/data/food.ts b/client/src/data/food.ts new file mode 100644 index 0000000..e79e3d7 --- /dev/null +++ b/client/src/data/food.ts @@ -0,0 +1,91 @@ +import pastaImage from '../assets/food/pasta.jpg'; +import meatImage from '../assets/food/meat.jpg'; +import fishImage from '../assets/food/fish.jpg'; +import poultryImage from '../assets/food/poultry.jpg'; +import cheeseImage from '../assets/food/cheese.jpg'; +import vegetarianImage from '../assets/food/vegetarian.jpg'; +import spicyImage from '../assets/food/spicy.jpg'; +import snacksImage from '../assets/food/snacks.jpg'; +import dessertImage from '../assets/food/dessert.jpg'; + +/** + * Constants for Find wine by food. + * In their own file (not in a component file) so that React Fast Refresh + * keeps working, the same reason as graphql/queries.ts. + */ + +/** How many wines are shown per page. Three columns, three rows. */ +export const FOOD_PAGE_SIZE = 9; + +/** The category that is selected when the page opens. */ +export const DEFAULT_FOOD_CATEGORY = 'Pasta, pizza & stews'; + +type FoodCategoryInfo = { + /** Short name on the category card */ + label: string; + /** Photo on the card (AI-generated in Canva, on a light background) */ + image: string; + /** Text in the "Why this pairing works" box */ + why: string; +}; + +/** + * Display text for each category. The names (keys) must match + * foods.category in the database, see backend/db/seeds/food_categories.sql. + * The order here is the order of the cards. Categories from the backend + * that are not listed here come last, with their database name as label + * and a plate emoji instead of a photo. + */ +export const FOOD_CATEGORY_INFO: Record = { + 'Pasta, pizza & stews': { + label: 'Pasta & pizza', + image: pastaImage, + why: 'Tomato sauces, cheese and slow-cooked stews are rich and savoury. Wines with bright acidity and moderate tannins cut through the richness and match the acidity of the tomatoes, so every bite feels balanced.', + }, + Meat: { + label: 'Meat', + image: meatImage, + why: 'Red meat is full of fat and protein, which soften the tannins in red wine. A full-bodied wine stands up to grilled and roasted flavours, while the tannins clean the palate between bites.', + }, + 'Fish & seafood': { + label: 'Fish & seafood', + image: fishImage, + why: 'Fish and shellfish are delicate and often served with lemon. Light, fresh wines with good acidity lift the flavours without overpowering them, while heavy tannins can make fish taste metallic.', + }, + Poultry: { + label: 'Poultry', + image: poultryImage, + why: 'Chicken and other poultry are mild and work with many styles. Medium-bodied whites and lighter reds follow the sauce and seasoning without hiding the meat.', + }, + Cheese: { + label: 'Cheese', + image: cheeseImage, + why: 'Cheese is salty and creamy. Acidity cuts through the fat, and sweet or fortified wines balance the salt in strong and blue cheeses. Fresh goat cheese loves crisp whites.', + }, + Vegetarian: { + label: 'Vegetarian', + image: vegetarianImage, + why: 'Vegetables, salads and mushrooms are lighter and often earthy or green. Fresh, aromatic wines match green flavours, and soft reds go well with mushrooms.', + }, + 'Spicy food': { + label: 'Spicy food', + image: spicyImage, + why: 'Heat from chili is made stronger by alcohol and tannins. Wines with a little sweetness, lower alcohol and plenty of fruit cool the burn and balance the spices.', + }, + 'Snacks & starters': { + label: 'Snacks & starters', + image: snacksImage, + why: 'Starters, cured meat and salty snacks need a wine that refreshes. Sparkling and light, crisp wines clean the palate and wake up the appetite before the meal.', + }, + Dessert: { + label: 'Dessert', + image: dessertImage, + why: 'The wine should be at least as sweet as the dessert, or it will taste sour and thin. Sweet and fortified wines match cake, fruit and chocolate.', + }, +}; + +/** Sort key for a category: its place in FOOD_CATEGORY_INFO, unknown last. */ +export function foodCategoryOrder(name: string): number { + const index = Object.keys(FOOD_CATEGORY_INFO).indexOf(name); + return index === -1 ? Infinity : index; +} diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index ea42253..6f0ffff 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -1,6 +1,8 @@ import { gql } from '@apollo/client'; import type { CatalogFilter, + FoodCategory, + FoodWine, TasteWine, WineListItem, WineSort, @@ -179,3 +181,57 @@ export const WINE_GRAPES_QUERY = gql` export type WineGrapesData = { wine: { id: string; grapes: string[]; vintages: number[] } | null; }; + +/** The food categories on Find wine by food, with their dishes. */ +export const FOOD_CATEGORIES_QUERY = gql` + query FoodCategories { + foodCategories { + name + foods + } + } +`; + +export type FoodCategoriesData = { + foodCategories: FoodCategory[]; +}; + +/** + * One page of wines that pair with a food category. + * food narrows it down to one dish in the category (null = all dishes). + */ +export const WINES_BY_FOOD_QUERY = gql` + query WinesByFood( + $category: String! + $food: String + $offset: Int + $limit: Int + ) { + winesByFoodCategory( + category: $category + food: $food + offset: $offset + limit: $limit + ) { + totalCount + items { + id + name + type + winery + region + country + avgRating + ratingCount + abv + body + acidity + matchedFoods + } + } + } +`; + +export type WinesByFoodData = { + winesByFoodCategory: { totalCount: number; items: FoodWine[] }; +}; diff --git a/client/src/index.css b/client/src/index.css index 27e139c..7e3afda 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -72,3 +72,13 @@ h1 { outline: 3px solid var(--color-accent); outline-offset: 2px; } + +/* Text only for screen readers, hidden on screen */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} diff --git a/client/src/types/wine.ts b/client/src/types/wine.ts index 95c8dbb..277a49c 100644 --- a/client/src/types/wine.ts +++ b/client/src/types/wine.ts @@ -77,3 +77,17 @@ export type TasteFilter = { /** Only wines with at least 100 ratings */ popularOnly: boolean; }; + +/** A food category on Find wine by food, e.g. 'Meat' with its dishes. */ +export type FoodCategory = { + name: string; + /** The dishes in the category, from Harmonize in X-Wines */ + foods: string[]; +}; + +/** A wine on Find wine by food. */ +export type FoodWine = WineListItem & { + ratingCount: number; + /** The dishes in the chosen category that the wine pairs with */ + matchedFoods: string[]; +};