From 5bb4eee5135e1e88067ec6f1d2093df271b3fcd9 Mon Sep 17 00:00:00 2001 From: Oscar Grande Date: Wed, 11 Nov 2026 21:11:44 +0100 Subject: [PATCH 01/42] feat: integrate infrastructure for Firebase authentication --- frontend/package-lock.json | 2140 ++++++++++++++++++++++--- frontend/package.json | 2 + frontend/src/context/AuthContext.ts | 21 + frontend/src/context/AuthProvider.tsx | 37 + frontend/src/lib/firebase.ts | 11 + frontend/src/main.tsx | 11 +- 6 files changed, 2038 insertions(+), 184 deletions(-) create mode 100644 frontend/src/context/AuthContext.ts create mode 100644 frontend/src/context/AuthProvider.tsx create mode 100644 frontend/src/lib/firebase.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b260a60..70a02c7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.3.3", + "backend": "^0.0.0", + "firebase": "^13.2.0", "lucide-react": "^1.64.0", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -669,261 +671,1744 @@ } } }, - "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, + "node_modules/@firebase/ai": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-3.0.0.tgz", + "integrity": "sha512-66jJAi6JUmyXna89xaDONmfvupO1wNc3f2BegPwUImagbbpKdVeycE3kMqbU4jiDLL14N5PHzadovvxpaqD2hw==", "license": "Apache-2.0", "dependencies": { - "@humanfs/types": "^0.15.0" + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" }, "engines": { - "node": ">=18.18.0" + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" } }, - "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, + "node_modules/@firebase/analytics": { + "version": "0.10.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.25.tgz", + "integrity": "sha512-oDagV3PHeNXBdxemksZCdj+GmUT19eCnTOcLdQePIIovFXlm3zWy3Gjeaokt9Nqx24kc4ljK8lU0XGcGmmKZ1A==", "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.31.tgz", + "integrity": "sha512-msTlG9REujk5rjtOHNLngiuPmvaWGQte8ugL4/F6ZCMb+92Ue7SgjSEpNxINkM8ZtTrbGS83j5SZkt0r+M1Tjw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.25", + "@firebase/analytics-types": "0.8.5", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.5.tgz", + "integrity": "sha512-kdnooE7Bis2jEnsqcerRwn/UQpH5D3uvHpku7OdUM9TJN3omlu6iYtbyAQ6XkAJRgJtO0aNf9OOkW8J6D59oCA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.16.5.tgz", + "integrity": "sha512-hVFsDVHCD86RjS+e2IDujnB5oZ57DVKcvhZLs7GVBnFk3SuGA4XkGUma85bFyQkxJ1XZI60Zfj+JRkIkos7omg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" }, "engines": { - "node": ">=18.18.0" + "node": ">=24.12.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, + "node_modules/@firebase/app-check": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.13.1.tgz", + "integrity": "sha512-l8y3dmnhodXks/APAwx4tWqRl3tk8b9874KF1FJyKg1DIU+kMD0l52iz7S9/MW+eK8k6cjJg0G0iJ5KQAsQpow==", "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=18.18.0" + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "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, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.7.tgz", + "integrity": "sha512-fBb/xSMyIKv1nDmccfzz3IAGBzx/cQOxmZai66rJzifb1hMMkztf824Xx7LhpTUe7HNUbXwY90IvJ66fi8q6yg==", "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.13.1", + "@firebase/app-check-types": "0.5.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=12.22" + "node": ">=20.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.5.tgz", + "integrity": "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.5.tgz", + "integrity": "sha512-+DF4gzFrlwGFyky38o4T/YN/r51l70yCtJ2HTkwmRj8FCMwPjm4hLH4fQbj6BUvzkiFqliBkitFsRpf1/YZkWA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.21.tgz", + "integrity": "sha512-TM5ROeRhbOXj+n1umENLZkjxm5f3myWonM+O7Na4VvCfFKWTH0cmNxjfMjrNoMkvoit4XtewMt4M7jaCAy4enQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.16.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" } }, - "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, + "node_modules/@firebase/app-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz", + "integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.2" + } + }, + "node_modules/@firebase/auth": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.9.tgz", + "integrity": "sha512-skKWiI33P138PTCl6NDvLecoy/ho6C31/2t/MdKMaWLLN1/ZFT5FaWgQUtif4HWIonk/JMQp8jYmlmUugFvETA==", "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=18.18" + "node": ">=24.12.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } } }, - "node_modules/@inquirer/ansi": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", - "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/auth-compat": { + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.14.tgz", + "integrity": "sha512-sc6HHIbAt8Ah///K5ufZEpEBBPvstXnqde1VQUME7o6Nh+OKYYniuCxqEvPwtLiwuyyQ8rC4fljymedwvTDtGw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.9", + "@firebase/auth-types": "0.13.2", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" } }, - "node_modules/@inquirer/confirm": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.5.tgz", - "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.6.tgz", + "integrity": "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.2.tgz", + "integrity": "sha512-OU+miuoSxIWYN7GT291d2ylVJBL3k/OePo7/JDSoQtkFQoEiGBc4AHuMjj/seqFIsHrqnjrAfRCk4pfufoJolQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.5.tgz", + "integrity": "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==", + "license": "Apache-2.0", "dependencies": { - "@inquirer/core": "^12.0.6", - "@inquirer/type": "4.2.1" + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/crashlytics": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/crashlytics/-/crashlytics-0.1.0.tgz", + "integrity": "sha512-OiFx/LeHbocsQ5+6yMru0l6uMup6jmoeVDQCPYlXZ/u+xDGbO5dY/yh983CmscTPTvUGiON5ytvxHpH7vL8KqA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/util": "1.15.3", + "@opentelemetry/api": "1.9.1", + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/browser-instrumentation": "0.6.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/exporter-logs-otlp-http": "0.220.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.220.0", + "@opentelemetry/instrumentation": "0.220.0", + "@opentelemetry/instrumentation-document-load": "0.65.0", + "@opentelemetry/instrumentation-fetch": "0.220.0", + "@opentelemetry/instrumentation-xml-http-request": "0.220.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-trace-base": "2.10.0", + "@opentelemetry/sdk-trace-web": "2.10.0", + "@opentelemetry/semantic-conventions": "1.43.0", + "tslib": "^2.1.0" + }, + "bin": { + "crashlytics-setconstants": "setconstants.js" }, "peerDependencies": { - "@types/node": ">=18" + "@angular/core": ">= 14.0.0", + "@angular/router": ">= 14.0.0", + "@firebase/app": "0.x", + "@firebase/app-types": "0.x", + "@types/react": ">= 16.8.0", + "next": ">= 13.0.0", + "react": ">= 16.8.0", + "react-router-dom": ">= 6.0.0" }, "peerDependenciesMeta": { - "@types/node": { + "@angular/core": { + "optional": true + }, + "@angular/router": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-router-dom": { "optional": true } } }, - "node_modules/@inquirer/core": { - "version": "12.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", - "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/data-connect": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.7.4.tgz", + "integrity": "sha512-su1aGWlzhxb+xtggCUSsufJn1FDa06SBDK71y+fpQ+g2zMhMExik6FUv/odkKzOF/xfWVjabCbWBcjJY3MceDA==", + "license": "Apache-2.0", "dependencies": { - "@inquirer/ansi": "^2.0.10", - "@inquirer/figures": "^2.0.10", - "@inquirer/type": "4.2.1", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.5.tgz", + "integrity": "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.7.tgz", + "integrity": "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/database": "1.1.5", + "@firebase/database-types": "1.0.22", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" }, "peerDependencies": { - "@types/node": ">=18" + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" }, "peerDependenciesMeta": { - "@types/node": { + "@firebase/app": { + "optional": true + }, + "@firebase/app-compat": { "optional": true } } }, - "node_modules/@inquirer/figures": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", - "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/database-types": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.22.tgz", + "integrity": "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.6", + "@firebase/util": "1.15.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.19.1.tgz", + "integrity": "sha512-W14adbYBDKvvLNnVIeUjjANvvDNOJiPIc1xq5GfRDBO0y/3At3cJ3CLqY71YMVF88y2eZIrOvlz8qrQUX5y1aA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "@firebase/webchannel-wrapper": "1.0.7", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "re2js": "^2.8.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.17.tgz", + "integrity": "sha512-3DfKwSg4e/mhoMaS965GjEG0+CvEbKZD3UW9wKufIW2LmWglEhXaKyrlSJQJLqEnLBd0+6eNXeeuCF5GKq1kAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/firestore": "4.19.1", + "@firebase/firestore-types": "3.0.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.5.tgz", + "integrity": "sha512-dbdMAQkMd5dwWc48eupz/Y6/E9ruat3+gY5lhVKscvvT/HnDBEEMzJW38zdKhgdnglZHGk2vUsJMOHAphMHGMA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.14.0.tgz", + "integrity": "sha512-DhuYFr0eMhp+s/PNEk6SiMsYkc00+XVOUeKbrl8MOzQNZI3SKQpqoDR1d+keNDAutwmHRWTUAdXBoVPD+xxVUw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.5.0.tgz", + "integrity": "sha512-T3BDIToESZUHt7438wyKYMkRjKg3m1xAIux5fVpNvTRQlDOFLukWniQWBDhJ2znpU9kxMTR6+e31TVo8P15PZw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/functions": "0.14.0", + "@firebase/functions-types": "0.6.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.5.tgz", + "integrity": "sha512-Zc0pURjthHXzSj54ZivCkzKDSV1r/wIpnmHdhq82q2yFFPVoncG/ZJjnVMiANvQfeww/QnElhzODpfwUucVwdA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.24", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.24.tgz", + "integrity": "sha512-Ui52ey8wHoWqkBbXRKJEKYWylI0JZogZmoLS+o8Anh1bxWtK27ZYjLla1UJAAdC5DCKLJ2qAp0VpJOjg8VOX1g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.24.tgz", + "integrity": "sha512-8M5nlcWwYt881x83COP2odq5vgf+NgwJh+RMd4LRSv8JI1pxwDfgrDJOERrjTgfC9J5Z0vnQX4b1pdN914e0Zw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/installations-types": "0.5.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.5.tgz", + "integrity": "sha512-e9UYcju3puDl1vdrcKIi5dExzHLameOT/Tc61Q48PYwxtsM1NzZh/ikGbdBQTsbRgg0EMZqdPr0/m5ODUBobrg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.2.tgz", + "integrity": "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.13.3.tgz", + "integrity": "sha512-nRXPAp+z0kA3KziJtRY9kktHECvRwaaONzHvislFgllhCwJMscW7icWNQA5IjyP8Uc2+GS/4px8Uz6rx6ftTag==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.30.tgz", + "integrity": "sha512-m1DPT2ET5x0qYqXLoHwKMAyf1mgZZOhuoE6H6/H4OPsp8zSxMIDEFPaW7MaffPJsCA5J7kHzo+BHVNXK4ZoV+w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/messaging": "0.13.3", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.6.tgz", + "integrity": "sha512-MVzvkKe2V4H2dHu5oOxRfeKQcfTwWmCgnzsC4V1q3ixun5iiL8riCZ2qI35rDhCL4glPGiu0jHxDbpVNuTKfow==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.14.tgz", + "integrity": "sha512-9PH1XEZVHErxGdbXluvGz4Uyjw4W955H8v7Mv9rHIybRrg5F2Ac2tvf5+mSf5EtnxWNmxrD2WLnvMFaZP4sMrQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.27.tgz", + "integrity": "sha512-O/ozTf/EbChN94Pk7bd1eUC0PAC36726AwsaiJyC0bZzSBWfLGSWASGPH5pebUWXQPJtGxIJYRkkbX5l0Qa+Hw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/performance": "0.7.14", + "@firebase/performance-types": "0.2.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.5.tgz", + "integrity": "sha512-PRzOgB+/M+6AKlEkY8a9xy5Ff5SfZPIh4iShXhn2WAcL/euSbK7bdLqWysHduunNJhGFsXa22Ag3ixqL6rQtYg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.9.3.tgz", + "integrity": "sha512-9DbuFNAFeUbVJQZmwLZfdoLRNFWV43V4UqaRpDkaSIdTmVWhzctPeISGkWe4sKTsqpoyldRumquS0gqWZg05fQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.30.tgz", + "integrity": "sha512-Q4sQKhQ8TPEMOcCUvUBfGeAP1DfcG7FGBfA9yFUn/XRr4vSToY0DTmK28Nh7lgLQYzs6D5Ng6knSoXH+aGn1Tg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/remote-config": "0.9.3", + "@firebase/remote-config-types": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.2.tgz", + "integrity": "sha512-i8k1omVfoAnaT1ZPv2FFjxMZrATYsO/GnFgHEj5+7fAJLzi8wLnGGv1WK06oEAD6ltrWXSO1HzeNyajn/NjMWw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.6.tgz", + "integrity": "sha512-9WjohxpjbfcySjCp8kIbd1ZhtmCQFQLqPtDQ+wmZ5PVXimTDjDsSx/RZjwwK89MjCTXB9fCdmvZkJxplZn4W1A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.6.tgz", + "integrity": "sha512-te0IsqX1bM+OZCJF7jNSKXcMqCA2yYmpshRHjX4axy0lI/P9ZP9GLjEZMVL8wrjUWF4e3cZxMZTbck3Zd5E1hA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/storage": "0.14.6", + "@firebase/storage-types": "0.8.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=24.12.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.5.tgz", + "integrity": "sha512-GEDs5P+rNUfNcS+wxIdOLAHficife2YXtvTJnyi3ssrX11AAtBg+nDUdbYh8vuBzWehVQZPmztGUUnud4L+4yg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz", + "integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.7.tgz", + "integrity": "sha512-phBFwieDLvkZGYN9CE9ZFNEIoBVksprzsnCzQejCmCHtgwCXReeuRpoEGN9C4EbhONztv8NRV1tau6Rb9pONwQ==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@grpc/proto-loader/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/@inquirer/ansi": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", + "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.5.tgz", + "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", + "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", + "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@msw/url": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@msw/url/-/url-0.1.2.tgz", + "integrity": "sha512-DdeBLQxQ+ZrtmwEmaj7GQ7lbzPCgbbbyAqOz/I4ulo68hwBpfdNvQJ5bmihJUkAAKOeCmqCIG3OFZGosqFN4mA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mswjs/interceptors": { + "version": "0.45.10", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.45.10.tgz", + "integrity": "sha512-pGZkvxzv6PZAQCpZHuu+TRs/4U8GWlClRToQETz4MHwwnq0dSxS279xRQ2StBVPpwXxTKtXpLHeK6MGSuxh8uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/until": "^3.0.1", + "@types/debug": "^4.1.13", + "debug": "^4.4.3", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "rettime": "^0.11.12" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@open-draft/until": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-3.0.1.tgz", + "integrity": "sha512-s7/9ELP4aP9YZtW7RJaa0Xf3RISaRH9+EFN18FtykJYRHGNrl8f9ymvoNXzjhrjmftFvkQudtuDU9RYhk0xs3A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/browser-instrumentation": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/browser-instrumentation/-/browser-instrumentation-0.6.0.tgz", + "integrity": "sha512-ZYi3WaiCix1D8K6SY5y+i9zvo0lWsnd/tKKnk5PaqWLLxUhVl0gud8G79RJZ3NApDZvQ57PAvNC7povMHIj7jw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "web-vitals": "^5.3.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.1" + } + }, + "node_modules/@opentelemetry/browser-instrumentation/node_modules/web-vitals": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==", + "license": "Apache-2.0" + }, + "node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.220.0.tgz", + "integrity": "sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/sdk-logs": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.220.0.tgz", + "integrity": "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-document-load": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.65.0.tgz", + "integrity": "sha512-xAhNXUIywjqmaSkpimsuUpRr0QYebMyTn0IJS9wFWYVLyH+2Q5IYWYys4IrT60foeyf2gyFmRBQyHupblNdaMA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.23.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.220.0.tgz", + "integrity": "sha512-9LW+zgvKK6kqojB0GVqbOCRGBUWiatHPmRTusjSVdwYCImIgJRrJTKu6WhjHdejJSjRS/mGsw/w4ETZBqav3kQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/instrumentation": "0.220.0", + "@opentelemetry/sdk-trace-web": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.9.0.tgz", + "integrity": "sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-xml-http-request/-/instrumentation-xml-http-request-0.220.0.tgz", + "integrity": "sha512-9FTY+kT3z5D8f7dC+Tubd4oJTu6tOGlQmws5J8e7DLMeIUwpWmLquRFV2BuUShiAPxTMGqqsZnuT3CVVbTu0JA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/instrumentation": "0.220.0", + "@opentelemetry/sdk-trace-web": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request/node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.9.0.tgz", + "integrity": "sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@inquirer/type": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", - "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", - "dev": true, - "license": "MIT", + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, "engines": { - "node": ">=6.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "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", + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.10.0.tgz", + "integrity": "sha512-6WqXdWSlBH0GNjDhv1gg6SLVtMtIBVhLaWqd1hiKSXS8meXXP13sVPQoYFHz1uVo0feUTUCdP9vMS54KflisAw==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" }, "engines": { - "node": ">= 18" + "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "keyv": "^5.6.0" + "@opentelemetry/api": ">=1.0.0 <1.10.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/@msw/url": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@msw/url/-/url-0.1.2.tgz", - "integrity": "sha512-DdeBLQxQ+ZrtmwEmaj7GQ7lbzPCgbbbyAqOz/I4ulo68hwBpfdNvQJ5bmihJUkAAKOeCmqCIG3OFZGosqFN4mA==", - "dev": true, - "license": "MIT" + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } }, - "node_modules/@mswjs/interceptors": { - "version": "0.45.10", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.45.10.tgz", - "integrity": "sha512-pGZkvxzv6PZAQCpZHuu+TRs/4U8GWlClRToQETz4MHwwnq0dSxS279xRQ2StBVPpwXxTKtXpLHeK6MGSuxh8uA==", - "dev": true, - "license": "MIT", + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", "dependencies": { - "@open-draft/until": "^3.0.1", - "@types/debug": "^4.1.13", - "debug": "^4.4.3", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "rettime": "^0.11.12" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=22" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@open-draft/until": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-3.0.1.tgz", - "integrity": "sha512-s7/9ELP4aP9YZtW7RJaa0Xf3RISaRH9+EFN18FtykJYRHGNrl8f9ymvoNXzjhrjmftFvkQudtuDU9RYhk0xs3A==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/kettanaito" + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" } }, "node_modules/@oxc-project/types": { @@ -935,6 +2420,63 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", @@ -1872,7 +3414,6 @@ "version": "24.19.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" @@ -2273,9 +3814,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2314,6 +3853,12 @@ "node": ">=12" } }, + "node_modules/backend": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/backend/-/backend-0.0.0.tgz", + "integrity": "sha512-Fq2aG5+zmmsKv2Dhm3ijAU5spnKOb5ldJlnnC/Vhk6n8In6zaq9eCPBMRiz2j94P/r84QEaBmtwh9tjaDPiQqg==", + "license": "BSD" + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2439,6 +3984,12 @@ "node": ">=18" } }, + "node_modules/cjs-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.3.0.tgz", + "integrity": "sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==", + "license": "MIT" + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -2482,6 +4033,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2564,7 +4133,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2670,7 +4238,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2942,6 +4509,18 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2986,6 +4565,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-13.2.0.tgz", + "integrity": "sha512-29ye5F+viPTv48mcljy84ZnwnrYmTTIULrWf3P113SfeTB/USIUZTs8gREreN5PmndQdKs3D06KQUPJIrFeTGw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "3.0.0", + "@firebase/analytics": "0.10.25", + "@firebase/analytics-compat": "0.2.31", + "@firebase/app": "0.16.5", + "@firebase/app-check": "0.13.1", + "@firebase/app-check-compat": "0.4.7", + "@firebase/app-compat": "0.5.21", + "@firebase/app-types": "0.9.6", + "@firebase/auth": "1.13.9", + "@firebase/auth-compat": "0.6.14", + "@firebase/crashlytics": "0.1.0", + "@firebase/data-connect": "0.7.4", + "@firebase/database": "1.1.5", + "@firebase/database-compat": "2.1.7", + "@firebase/firestore": "4.19.1", + "@firebase/firestore-compat": "0.4.17", + "@firebase/functions": "0.14.0", + "@firebase/functions-compat": "0.5.0", + "@firebase/installations": "0.6.24", + "@firebase/installations-compat": "0.2.24", + "@firebase/messaging": "0.13.3", + "@firebase/messaging-compat": "0.2.30", + "@firebase/performance": "0.7.14", + "@firebase/performance-compat": "0.2.27", + "@firebase/remote-config": "0.9.3", + "@firebase/remote-config-compat": "0.2.30", + "@firebase/storage": "0.14.6", + "@firebase/storage-compat": "0.4.6", + "@firebase/util": "1.15.3" + } + }, "node_modules/flat-cache": { "version": "6.1.23", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", @@ -3033,7 +4649,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -3145,6 +4760,18 @@ "node": "^22.13.0 || >=24.0.0" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3155,6 +4782,26 @@ "node": ">= 4" } }, + "node_modules/import-in-the-middle": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.6.0.tgz", + "integrity": "sha512-xRU7hI24S2FhucXtuZXn8EpcOjfVP6iiEQyR0USayPeaOroEmSsNlqnl95866iRdRtkHrd6Q/Z4D14R60au0+A==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.4", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/import-in-the-middle/node_modules/es-module-lexer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3185,6 +4832,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3614,6 +5270,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3692,11 +5360,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msw": { @@ -3966,6 +5639,29 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3996,6 +5692,15 @@ "dev": true, "license": "MIT" }, + "node_modules/re2js": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.10.0.tgz", + "integrity": "sha512-/P6FNnLbHjXJMRF1dPoZoVTyHvFcuVhbDpzml9DPMlbeErYlZ18GueAaG9GhYhAm/9AN7i7Y6crmDrLX0DeJ4Q==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", @@ -4039,6 +5744,15 @@ "node": ">=8" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4049,6 +5763,19 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/rettime": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.12.tgz", @@ -4089,6 +5816,26 @@ "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -4373,6 +6120,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4454,7 +6207,6 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "devOptional": true, "license": "MIT" }, "node_modules/until-async": { @@ -4691,6 +6443,12 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -4701,6 +6459,29 @@ "node": ">=20" } }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", @@ -4835,7 +6616,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/frontend/package.json b/frontend/package.json index ac94c40..4f8a4d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,8 @@ }, "dependencies": { "@tailwindcss/vite": "^4.3.3", + "backend": "^0.0.0", + "firebase": "^13.2.0", "lucide-react": "^1.64.0", "react": "^19.2.8", "react-dom": "^19.2.8", diff --git a/frontend/src/context/AuthContext.ts b/frontend/src/context/AuthContext.ts new file mode 100644 index 0000000..ade226e --- /dev/null +++ b/frontend/src/context/AuthContext.ts @@ -0,0 +1,21 @@ +import { createContext, useContext } from "react"; +import type { User } from "firebase/auth"; + +type AuthContextValue = { + user: User | null; + loading: boolean; + login: () => Promise; + logout: () => Promise; +}; + +export const AuthContext = createContext(null); + +export function useAuth() { + const context = useContext(AuthContext); + + if (!context) { + throw new Error("useAuth must be used inside AuthProvider"); + } + + return context; +} \ No newline at end of file diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx new file mode 100644 index 0000000..2f763fa --- /dev/null +++ b/frontend/src/context/AuthProvider.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { + GoogleAuthProvider, + onAuthStateChanged, + signInWithPopup, + signOut, +} from "firebase/auth"; +import type { User } from "firebase/auth"; +import { auth } from "@/lib/firebase"; +import { AuthContext } from "./AuthContext"; + +export default function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + return onAuthStateChanged(auth, (currentUser) => { + setUser(currentUser); + setLoading(false); + }); + }, []); + + async function login() { + await signInWithPopup(auth, new GoogleAuthProvider()); + } + + async function logout() { + await signOut(auth); + } + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/frontend/src/lib/firebase.ts b/frontend/src/lib/firebase.ts new file mode 100644 index 0000000..ef086d0 --- /dev/null +++ b/frontend/src/lib/firebase.ts @@ -0,0 +1,11 @@ +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; + +const app = initializeApp({ + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, + appId: import.meta.env.VITE_FIREBASE_APP_ID, +}); + +export const auth = getAuth(app); \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d1778d6..77613fc 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,12 +2,15 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.tsx"; -import BuildProvider from './context/BuildProvider' +import BuildProvider from "./context/BuildProvider"; +import AuthProvider from "./context/AuthProvider"; createRoot(document.getElementById("root")!).render( - - - + + + + + , ); From 7e821d5c8ba798d0079b49f2948b3ada3278e27a Mon Sep 17 00:00:00 2001 From: Wilhelm Svensgaard Rolstad Date: Thu, 12 Nov 2026 13:29:35 +0100 Subject: [PATCH 02/42] added user feedback ui for when adding a part over an existing part in the buil, also implemented fetching specifications only when needed (add, partview), and added lazy loading of images --- frontend/src/api/parts.ts | 43 ++++++- frontend/src/components/AddToBuildButton.tsx | 73 ++++++++--- .../src/components/AddToBuildFeedback.tsx | 115 ++++++++++++++++++ frontend/src/components/Button.tsx | 7 +- frontend/src/components/PcPartCard.tsx | 28 ++++- frontend/src/index.css | 5 + 6 files changed, 244 insertions(+), 27 deletions(-) create mode 100644 frontend/src/components/AddToBuildFeedback.tsx diff --git a/frontend/src/api/parts.ts b/frontend/src/api/parts.ts index 1c665f6..9671bae 100644 --- a/frontend/src/api/parts.ts +++ b/frontend/src/api/parts.ts @@ -76,7 +76,6 @@ export async function getParts( manufacturer priceEur imageUrl - specs } } totalCount @@ -139,3 +138,45 @@ export async function getParts( pageInfo: connection.pageInfo, } } + +export async function getPartSpecifications( + id: string, + signal?: AbortSignal, +): Promise { + const response = await fetch('/api/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + signal, + body: JSON.stringify({ + query: ` + query GetPartSpecifications($id: ID!) { + part(id: $id) { + specs + } + } + `, + variables: { id }, + }), + }) + + if (!response.ok) { + throw new Error(`Could not load specifications (HTTP ${response.status}).`) + } + + const result: { + data?: { part: { specs: PcPart['details'] } | null } + errors?: { message: string }[] + } = await response.json() + + if (result.errors?.length) { + throw new Error(result.errors.map((error) => error.message).join(', ')) + } + if (!result.data) { + throw new Error('The server returned no data.') + } + if (!result.data.part) { + throw new Error('Part not found.') + } + + return result.data.part.specs +} diff --git a/frontend/src/components/AddToBuildButton.tsx b/frontend/src/components/AddToBuildButton.tsx index fef6868..8254028 100644 --- a/frontend/src/components/AddToBuildButton.tsx +++ b/frontend/src/components/AddToBuildButton.tsx @@ -1,9 +1,11 @@ import type { ReactNode } from "react"; +import { useRef, useState } from "react"; import type { PcPart } from "@/types/types"; import { useBuild } from "@/context/BuildContext"; -import { flushSync } from "react-dom"; import { twMerge } from "tailwind-merge"; import Button from "./Button"; +import AddToBuildFeedback from "./AddToBuildFeedback"; +import { getPartSpecifications } from "@/api/parts"; type AddToBuildButtonProps = { part: PcPart; @@ -16,26 +18,61 @@ export default function AddToBuildButton({ children, className, }: AddToBuildButtonProps) { - const { parts, addPart, removePart } = useBuild(); + const { parts, addPart } = useBuild(); + const [showFeedback, setShowFeedback] = useState(false); const inBuild = parts[part.category]?.id === part.id; + const [stage, setStage] = useState<"confirm" | "loading" | "success">("confirm"); + const adding = useRef(false); + + async function handleAdd(){ //Reuse existing specifications, or fetch them before adding the part. + if (adding.current) return; + adding.current = true; + const loadingTimer = window.setTimeout(() => setStage("loading"), 500); + try { + const hasSpecifications = part.details && Object.keys(part.details).length > 0; + const specifications = hasSpecifications + ? part.details + : await getPartSpecifications(part.id); + const completePart = {...part, details: specifications,} as PcPart; + addPart(completePart); + setStage("success"); + } finally { + window.clearTimeout(loadingTimer); + adding.current = false; + } + } + + async function handleClick(){ //adding component, first checks if an existing component exists for the category added for. If not add it to the build. + if (adding.current) return; + setStage("confirm"); + setShowFeedback(true); + if(!parts[part.category]){ + await handleAdd() + } + } return ( - + + {showFeedback && (stage !== "confirm" || parts[part.category]) && ( + setShowFeedback(false)} + /> )} - onClick={() => { - if (inBuild) { - removePart(part.category); - return; - } - flushSync(() => addPart(part)); //add the part to the build - window.location.href = `?page=builder&highlight=${encodeURIComponent(part.category)}`; - }} - > - {children ?? (inBuild ? "Remove from build" : "Add to build")} - + ); } diff --git a/frontend/src/components/AddToBuildFeedback.tsx b/frontend/src/components/AddToBuildFeedback.tsx new file mode 100644 index 0000000..d8bc403 --- /dev/null +++ b/frontend/src/components/AddToBuildFeedback.tsx @@ -0,0 +1,115 @@ +import type { PcPart } from "@/types/types"; +import { useBuild } from "@/context/BuildContext"; +import { useEffect, useId, useRef } from "react"; +import { flushSync } from "react-dom"; +import { ArrowRight, Cpu, X } from "lucide-react"; + +type AddToBuildFeedbackProps = { + part: PcPart; + stage: "confirm" | "loading" | "success"; + onReplace: () => Promise; + onClose: () => void; +}; + +export default function AddToBuildFeedback({ part, stage, onReplace, onClose }: AddToBuildFeedbackProps) { + const dialogRef = useRef(null); + const titleId = useId(); + const descriptionId = useId(); + const { parts } = useBuild(); + const savedPart = parts[part.category]; + const secondaryButton = "cursor-pointer rounded-lg border border-secondary/50 px-4 py-2 text-xs font-medium text-white transition-colors hover:bg-surface-dark"; + const primaryButton = "cursor-pointer rounded-lg border border-accent bg-accent px-5 py-2 text-xs font-semibold text-background transition-colors hover:bg-accent/90"; + + useEffect(() => { + dialogRef.current?.showModal(); + }, []); + + useEffect(() => { + if (stage !== "success") return; + + const timer = window.setTimeout(() => dialogRef.current?.close(), 5000); + return () => window.clearTimeout(timer); + }, [stage]); + + function closeFeedback() { + dialogRef.current?.close(); + } + + function goToBuild() { + // Remove the feedback before the browser saves this page for Back navigation. + flushSync(() => onClose()); + window.location.href = "?page=builder"; + } + + return ( + + + +
+
+
+

+ {stage === "confirm" ? "Replace component?" : stage === "loading" ? "Adding component…" : "Component added"} +

+

+ {stage === "confirm" + ? `You have already selected ${savedPart?.name}. Would you like to replace it with ${part.name}?` + : stage === "loading" + ? `Adding ${part.name} to your build.` + : `${part.name} was added to your build. Would you like to go back to the build page?`} +

+
+
+ + {stage === "confirm" && ( +
+
+
+
+ )} +
+ + {stage !== "loading" && ( +
+ + +
+ )} + {stage === "success" && ( +
+ ); +} diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index a457711..8ad2fad 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -8,6 +8,7 @@ type ButtonProps = { className?: string onClick?: () => void type?: 'button' | 'submit' + disabled?: boolean } export default function Button({ @@ -15,12 +16,14 @@ export default function Button({ className = '', onClick, type = 'button', + disabled = false, }: ButtonProps) { - const inactive = !onClick && type !== 'submit' + const inactive = disabled || (!onClick && type !== 'submit') return ( @@ -96,11 +111,12 @@ export default function PcPartCard({ part }: { part: PcPart }) { if (e.target === e.currentTarget) closeDetails(); }} > - {isDesktop ? ( - - ) : ( - - )} + {completePart && ( + isDesktop? ( + + ) : ( + + ))} ); diff --git a/frontend/src/index.css b/frontend/src/index.css index c3fbb7b..a9cc681 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -31,3 +31,8 @@ body { background-color: #166534; } } + +@keyframes feedback-countdown { + from { transform: scaleX(0); } + to { transform: scaleX(1); } +} From e34afe21e67fb76dbcaa225214e71f23f03be909 Mon Sep 17 00:00:00 2001 From: Wilhelm Svensgaard Rolstad Date: Thu, 12 Nov 2026 17:21:52 +0100 Subject: [PATCH 03/42] feat: added memo to stop re-rendering cards that are already rendered, also fix a bug wher if part details where fetched when showing details they were lost when exiting that dialog --- frontend/src/components/AllPartsPreview.tsx | 98 +++++++++------ frontend/src/components/PcPartCard.tsx | 59 ++++++++- frontend/src/test/AllPartsPreview.test.tsx | 113 +++++++++++++++++- frontend/src/test/CatalogInteraction.test.tsx | 8 +- frontend/src/test/PcPartCard.test.tsx | 110 +++++++++++++++++ 5 files changed, 344 insertions(+), 44 deletions(-) create mode 100644 frontend/src/test/PcPartCard.test.tsx diff --git a/frontend/src/components/AllPartsPreview.tsx b/frontend/src/components/AllPartsPreview.tsx index 3713a8c..1c157c3 100644 --- a/frontend/src/components/AllPartsPreview.tsx +++ b/frontend/src/components/AllPartsPreview.tsx @@ -2,10 +2,10 @@ import { useParts } from "@/hooks/useParts"; import PcPartCard from "@/components/PcPartCard"; import ViewSelector, { type ViewType } from "@/components/ViewSelector"; import type { PcPart } from "@/types/types"; -import { getPartSpecifications, type PartsOptions } from "@/api/parts"; +import type { PartsOptions } from "@/api/parts"; import MobileComponentDetailPage from "./MobileComponentDetailPage"; import DesktopComponentDetailPage from "./DesktopComponentDetailPage"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; type AllPartsPreviewProps = PartsOptions & { featured?: boolean; @@ -13,6 +13,18 @@ type AllPartsPreviewProps = PartsOptions & { viewenabled?: boolean; }; +// Compare summary values, not the new objects returned by each API request. +const PreviewPartCard = memo(function PreviewPartCard({ + onShowDetails, + ...summary +}: Omit & { + onShowDetails: (part: PcPart) => void; +}) { + // Catalogue results omit specifications; card actions fetch them when needed. + const part = { ...summary, details: {} } as PcPart; + return ; +}); + export default function AllPartsPreview({ featured = false, defaultview = "4-grid-view", @@ -23,7 +35,6 @@ export default function AllPartsPreview({ return ( void; }) { - const [cursors, setCursors] = useState([]); - const [allParts, setAllParts] = useState([]); + const filterKey = JSON.stringify(options); + const [pagination, setPagination] = useState({ + filterKey, + after: undefined as string | undefined, + parts: [] as PcPart[], + }); + // Reset pagination without unmounting cards that may match the new filters. + if (pagination.filterKey !== filterKey) { + setPagination({ filterKey, after: undefined, parts: [] }); + } const loadMoreRef = useRef(null); - const requestedCursorRef = useRef(null); const first = options.first ?? 40; const dialogRef = useRef(null); - const detailRequest = useRef(0); const [selectedPart, setSelectedPart] = useState(null); - const [detailError, setDetailError] = useState(""); const [isDesktop, setIsDesktop] = useState(false); - async function handleShowDetails(part: PcPart) { - const request = ++detailRequest.current; - setDetailError(""); - try { - const details = await getPartSpecifications(part.id); - if (request !== detailRequest.current || !dialogRef.current) return; - setIsDesktop(window.matchMedia("(min-width: 48rem)").matches); - setSelectedPart({ ...part, details } as PcPart); - } catch { - if (request === detailRequest.current && dialogRef.current) { - setDetailError("Could not load component details. Please try again."); - } - } - } + const handleShowDetails = useCallback((part: PcPart) => { + setIsDesktop(window.matchMedia("(min-width: 48rem)").matches); + setSelectedPart(part); + }, []); useEffect(() => { if (!selectedPart) return; @@ -91,10 +97,23 @@ function PartsResults({ const { parts, totalCount, pageInfo, loading, error } = useParts({ ...options, first, - after: cursors.at(-1), + after: pagination.filterKey === filterKey ? pagination.after : undefined, }); - const visibleParts = [...allParts, ...parts]; + const allParts = pagination.parts; + const [lastResult, setLastResult] = useState({ allParts, parts }); + if ( + !loading && + !error && + pagination.filterKey === filterKey && + (lastResult.parts !== parts || lastResult.allParts !== allParts) + ) { + setLastResult({ allParts, parts }); + } + // Keep the last completed list mounted while the next request is pending. + const visibleParts = loading + ? [...lastResult.allParts, ...lastResult.parts] + : [...allParts, ...parts]; const handleLoadMore = useCallback(() => { const cursor = pageInfo?.endCursor; @@ -104,15 +123,17 @@ function PartsResults({ error || !pageInfo?.hasNextPage || !cursor || - requestedCursorRef.current === cursor + pagination.after === cursor ) { return; } - requestedCursorRef.current = cursor; - setAllParts((previous) => [...previous, ...parts]); - setCursors((previous) => [...previous, cursor]); - }, [loading, error, pageInfo, parts]); + setPagination((previous) => + previous.after === cursor + ? previous + : { filterKey, after: cursor, parts: [...previous.parts, ...parts] }, + ); + }, [loading, error, pageInfo, parts, pagination.after, filterKey]); function getStyle() { switch (viewType) { @@ -155,17 +176,25 @@ function PartsResults({ )} - {!featured && !loading && !error && ( -

+ {!featured && ( +

Showing {visibleParts.length} of {totalCount} components

)} -
+
{visibleParts.map((part) => ( - ))} @@ -212,7 +241,6 @@ function PartsResults({ ))} - {detailError &&

{detailError}

} {loading &&

Loading parts...

} {error &&

{error}

} diff --git a/frontend/src/components/PcPartCard.tsx b/frontend/src/components/PcPartCard.tsx index c11471d..622b2b2 100644 --- a/frontend/src/components/PcPartCard.tsx +++ b/frontend/src/components/PcPartCard.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { PcPart } from "@/types/types"; +import { getPartSpecifications } from "@/api/parts"; import AddToBuildButton from "./buttons/AddToBuildButton"; import Button from "@/components/buttons/Button"; @@ -11,7 +12,54 @@ export default function PcPartCard({ onShowDetails: (part: PcPart) => void; }) { const [failedImage, setFailedImage] = useState(""); + // Keep fetched specifications in this card even after the detail view closes. + // They are discarded when the card unmounts, for example when filtered out. + const [specifications, setSpecifications] = useState< + PcPart["details"] | null + >(null); + const [loadingDetails, setLoadingDetails] = useState(false); + const [detailError, setDetailError] = useState(""); + const detailRequest = useRef(null); const imageUrl = part.image_url?.trim() ?? ""; + // Combine saved specifications with the latest summary (including price). + // This creates a new object without changing the part prop. + const completePart = specifications + ? ({ ...part, details: specifications } as PcPart) + : part; + + // Cancel an unfinished request when this card leaves the page. + useEffect(() => () => detailRequest.current?.abort(), []); + + async function handleShowDetails() { + // Ignore repeated clicks while this card is already fetching details. + if (detailRequest.current) return; + setDetailError(""); + // Reuse specifications saved here or already supplied with the part. + if (specifications || Object.keys(part.details).length > 0) { + onShowDetails(completePart); + return; + } + + const controller = new AbortController(); + detailRequest.current = controller; + setLoadingDetails(true); + try { + const details = await getPartSpecifications(part.id, controller.signal); + if (controller.signal.aborted) return; + // Save details for later actions and give the parent the complete part. + // Use the fetched value directly because setting state takes effect next render. + setSpecifications(details); + onShowDetails({ ...part, details } as PcPart); + } catch { + if (!controller.signal.aborted) { + setDetailError("Could not load component details. Please try again."); + } + } finally { + // Allow another click, including a retry if fetching failed. + detailRequest.current = null; + if (!controller.signal.aborted) setLoadingDetails(false); + } + } return ( <> @@ -46,12 +94,15 @@ export default function PcPartCard({ + {detailError &&

{detailError}

} + {/* Reuse specifications loaded by Show details when adding to a build. */}
diff --git a/frontend/src/test/AllPartsPreview.test.tsx b/frontend/src/test/AllPartsPreview.test.tsx index 2ec2bcf..db13331 100644 --- a/frontend/src/test/AllPartsPreview.test.tsx +++ b/frontend/src/test/AllPartsPreview.test.tsx @@ -3,6 +3,7 @@ import { render, screen, act } from "@testing-library/react"; import AllPartsPreview from "@/components/AllPartsPreview"; import { useParts } from "@/hooks/useParts"; import type { PcPart } from "@/types/types"; +import PcPartCard from "@/components/PcPartCard"; // -------------------------------------------------- // Mock IntersectionObserver @@ -50,9 +51,12 @@ vi.mock("@/hooks/useParts", () => ({ })); vi.mock("@/components/PcPartCard", () => ({ - default: ({ part }: { part: PcPart }) => ( -
{part.name}
- ), + default: vi.fn(({ part }: { part: PcPart }) => ( +
+ {part.name} + €{part.price_eur} +
+ )), })); vi.mock("@/components/ViewSelector", () => ({ @@ -254,6 +258,14 @@ it("loads the next page without duplicating previous results", () => { expect(screen.getByText("RTX 4060")).toBeInTheDocument(); expect(screen.getByText("RTX 4070")).toBeInTheDocument(); + vi.mocked(useParts).mockReturnValue({ + parts: [], + totalCount: 0, + pageInfo: undefined, + loading: true, + error: null, + }); + // Simulate scrolling to the bottom of the page act(() => { expect(triggerIntersection).toBeDefined(); @@ -298,3 +310,98 @@ it("loads the next page without duplicating previous results", () => { // Check the total number of displayed components expect(screen.getByText("Showing 4 of 4 components")).toBeInTheDocument(); }); + +it("reuses unchanged cards across layout changes, filtering and fresh API objects", () => { + const initial = { + parts: mockParts.slice(0, 3), + totalCount: 3, + pageInfo: { hasNextPage: false, endCursor: null }, + loading: false, + error: null, + }; + vi.mocked(useParts).mockReturnValue(initial); + const { rerender } = render(); + const retainedCard = screen.getByTestId("part-2"); + vi.mocked(PcPartCard).mockClear(); + + rerender(); + expect(PcPartCard).not.toHaveBeenCalled(); + + vi.mocked(useParts).mockReturnValue({ + parts: [], + totalCount: 0, + pageInfo: undefined, + loading: true, + error: null, + }); + rerender(); + expect(screen.getByTestId("part-2")).toBe(retainedCard); + expect(screen.getByRole("status")).toHaveTextContent("Loading parts"); + expect(PcPartCard).not.toHaveBeenCalled(); + + vi.mocked(useParts).mockReturnValue({ + ...initial, + parts: [ + { ...mockParts[2], price_eur: 899 }, + { ...mockParts[1], details: {} } as PcPart, + mockParts[3], + ], + }); + rerender(); + expect(screen.getByTestId("part-2")).toBe(retainedCard); + expect(screen.queryByTestId("part-1")).not.toBeInTheDocument(); + expect(screen.getByTestId("part-3")).toHaveTextContent("€899"); + expect( + screen.getAllByTestId(/^part-/).map((card) => card.dataset.testid), + ).toEqual(["part-3", "part-2", "part-4"]); + expect( + vi.mocked(PcPartCard).mock.calls.map(([props]) => props.part.id), + ).toEqual(["3", "4"]); +}); + +it("resets pagination on filter changes and replaces old results even when empty", () => { + const firstPage = { + parts: mockParts.slice(0, 2), + totalCount: 4, + pageInfo: { hasNextPage: true, endCursor: "next" }, + loading: false, + error: null, + }; + const pending = { + parts: [], + totalCount: 0, + pageInfo: undefined, + loading: true, + error: null, + }; + vi.mocked(useParts).mockReturnValue(firstPage); + const { rerender } = render(); + vi.mocked(PcPartCard).mockClear(); + vi.mocked(useParts).mockReturnValue(pending); + act(() => triggerIntersection?.()); + expect(PcPartCard).not.toHaveBeenCalled(); + expect(screen.getAllByTestId(/^part-/)).toHaveLength(2); + + vi.mocked(useParts).mockReturnValue({ + ...firstPage, + parts: mockParts.slice(2), + pageInfo: { hasNextPage: false, endCursor: null }, + }); + rerender(); + expect(screen.getAllByTestId(/^part-/)).toHaveLength(4); + expect( + vi.mocked(PcPartCard).mock.calls.map(([props]) => props.part.id), + ).toEqual(["3", "4"]); + + vi.mocked(useParts).mockReturnValue(pending); + rerender(); + expect(useParts).toHaveBeenLastCalledWith( + expect.objectContaining({ after: undefined }), + ); + expect(screen.getAllByTestId(/^part-/)).toHaveLength(4); + + vi.mocked(useParts).mockReturnValue({ ...pending, loading: false }); + rerender(); + expect(screen.queryAllByTestId(/^part-/)).toHaveLength(0); + expect(screen.getByText("No parts found.")).toBeInTheDocument(); +}); diff --git a/frontend/src/test/CatalogInteraction.test.tsx b/frontend/src/test/CatalogInteraction.test.tsx index 762e66d..9f47f3d 100644 --- a/frontend/src/test/CatalogInteraction.test.tsx +++ b/frontend/src/test/CatalogInteraction.test.tsx @@ -99,12 +99,15 @@ it("updates search results when the user types", async () => { details: {}, } as PcPart; + // Match useParts: completed requests keep the same result array between renders. + const matchingParts = [rtxPart]; + const emptyParts: PcPart[] = []; // Return results based on the search term vi.mocked(useParts).mockImplementation(({ search } = {}) => { const matchesSearch = search === "RTX"; return { - parts: matchesSearch ? [rtxPart] : [], + parts: matchesSearch ? matchingParts : emptyParts, totalCount: matchesSearch ? 1 : 0, pageInfo: { hasNextPage: false, @@ -167,6 +170,7 @@ it("updates results when the user filters and sorts", async () => { }, ] as PcPart[]; + const emptyParts: PcPart[] = []; // Return results when the expected filters are selected vi.mocked(useParts).mockImplementation( ({ category, sortBy, sortDirection } = {}) => { @@ -174,7 +178,7 @@ it("updates results when the user filters and sorts", async () => { category === "GPU" && sortBy === "PRICE" && sortDirection === "ASC"; return { - parts: matchesFilters ? gpuParts : [], + parts: matchesFilters ? gpuParts : emptyParts, totalCount: matchesFilters ? 2 : 0, pageInfo: { hasNextPage: false, diff --git a/frontend/src/test/PcPartCard.test.tsx b/frontend/src/test/PcPartCard.test.tsx new file mode 100644 index 0000000..c8e778d --- /dev/null +++ b/frontend/src/test/PcPartCard.test.tsx @@ -0,0 +1,110 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { act, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import PcPartCard from "@/components/PcPartCard"; +import { getPartSpecifications } from "@/api/parts"; +import { useBuild } from "@/context/BuildContext"; +import type { PcPart } from "@/types/types"; + +vi.mock("@/api/parts", () => ({ getPartSpecifications: vi.fn() })); +vi.mock("@/context/BuildContext", () => ({ useBuild: vi.fn() })); +vi.mock("@/components/AddToBuildFeedback", () => ({ default: () => null })); + +const part = { + id: "gpu-1", + name: "RTX 4070", + category: "GPU", + manufacturer: "NVIDIA", + price_eur: 599, + image_url: "", + details: {}, +} as PcPart; +const details = { memory: 12 } as PcPart["details"]; +const addPart = vi.fn(); + +beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(useBuild).mockReturnValue({ + parts: {}, + addPart, + removePart: vi.fn(), + resetBuild: vi.fn(), + }); +}); + +it("retains specifications for reopening details and adding from the card", async () => { + const user = userEvent.setup(); + const onShowDetails = vi.fn(); + vi.mocked(getPartSpecifications).mockResolvedValue(details); + const { rerender } = render( + , + ); + + await user.click(screen.getByRole("button", { name: "Show details" })); + expect(onShowDetails).toHaveBeenCalledWith({ ...part, details }); + + // Closing the parent's dialog leaves the same card mounted. A fresh summary + // can also arrive without discarding its specifications or freezing its price. + const updatedPart = { ...part, price_eur: 499 }; + rerender(); + await user.click(screen.getByRole("button", { name: "Show details" })); + expect(onShowDetails).toHaveBeenLastCalledWith({ ...updatedPart, details }); + await user.click(screen.getByRole("button", { name: "Add to build" })); + + expect(addPart).toHaveBeenCalledWith({ ...updatedPart, details }); + expect(getPartSpecifications).toHaveBeenCalledTimes(1); +}); + +it("shows an error and lets the user retry a failed details request", async () => { + const user = userEvent.setup(); + const onShowDetails = vi.fn(); + vi.mocked(getPartSpecifications) + .mockRejectedValueOnce(new Error("Network error")) + .mockResolvedValueOnce(details); + render(); + + await user.click(screen.getByRole("button", { name: "Show details" })); + expect(screen.getByRole("alert")).toHaveTextContent( + "Could not load component details", + ); + expect(onShowDetails).not.toHaveBeenCalled(); + await user.click(screen.getByRole("button", { name: "Show details" })); + expect(onShowDetails).toHaveBeenCalledWith({ ...part, details }); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); +}); + +it("disables repeat detail clicks and ignores a response after the card unmounts", async () => { + const user = userEvent.setup(); + const onShowDetails = vi.fn(); + let resolve!: (details: PcPart["details"]) => void; + vi.mocked(getPartSpecifications).mockReturnValue( + new Promise((done) => { + resolve = done; + }), + ); + const { unmount } = render( + , + ); + + await user.click(screen.getByRole("button", { name: "Show details" })); + expect( + screen.getByRole("button", { name: "Loading details..." }), + ).toBeDisabled(); + const signal = vi.mocked(getPartSpecifications).mock.calls[0][1]; + unmount(); + expect(signal?.aborted).toBe(true); + await act(async () => resolve(details)); + expect(onShowDetails).not.toHaveBeenCalled(); +}); + +it("can still add directly without opening details first", async () => { + const user = userEvent.setup(); + vi.mocked(getPartSpecifications).mockResolvedValue(details); + render(); + + await user.click(screen.getByRole("button", { name: "Add to build" })); + await waitFor(() => + expect(addPart).toHaveBeenCalledWith({ ...part, details }), + ); + expect(getPartSpecifications).toHaveBeenCalledTimes(1); +}); From e05c20a9884b5e0b59818e915695c23f4f490547 Mon Sep 17 00:00:00 2001 From: Wilhelm Svensgaard Rolstad Date: Thu, 12 Nov 2026 17:29:35 +0100 Subject: [PATCH 04/42] feat: added a chache to catalogue filter searches, searching for the same filters within a minute will now fetch data from a cache instead of the backend. --- frontend/src/hooks/useParts.ts | 78 +++++++++++++++++-------- frontend/src/test/useParts.test.tsx | 91 ++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 26 deletions(-) diff --git a/frontend/src/hooks/useParts.ts b/frontend/src/hooks/useParts.ts index 98f35f2..e2895d9 100644 --- a/frontend/src/hooks/useParts.ts +++ b/frontend/src/hooks/useParts.ts @@ -7,6 +7,14 @@ type RequestResult = { error: string | null; }; +// Shared between catalogue instances and kept until the browser page reloads. +const CACHE_TTL_MS = 60_000; +const MAX_CACHED_PAGES = 100; +const catalogueCache = new Map< + string, + { data: PartsPage; expiresAt: number } +>(); + export function useParts({ search = "", category, @@ -19,7 +27,7 @@ export function useParts({ const [result, setResult] = useState(null); const requestKey = JSON.stringify({ - //the calling component, may call this hook on render, this KEY will stop a request if the last render had the same parameters + // Include every filter and pagination option so only identical queries share data. search, category, manufacturers: manufacturers ?? [], @@ -33,28 +41,52 @@ export function useParts({ const controller = new AbortController(); //Creates a controller that lets you abort the fetch if a new call comes before this one finishes,e.g. typing a new letter after the search call has started but not finished. const options: PartsOptions = JSON.parse(requestKey); //Turns requestKey into object of type PartsOptions. - const timeout = setTimeout(() => { - getParts(options, controller.signal) - .then((data) => { - if (!controller.signal.aborted) { - setResult({ - key: requestKey, - data, - error: null, - }); - } - }) - .catch((error: unknown) => { - if (!controller.signal.aborted) { - setResult({ - key: requestKey, - data: null, - error: - error instanceof Error ? error.message : "Could not load parts", - }); - } - }); - }, 300); //wait 300ms before executing in case the request changes + // Remove expired pages before checking for a reusable response. + for (const [key, entry] of catalogueCache) { + if (entry.expiresAt <= Date.now()) catalogueCache.delete(key); + } + const cached = catalogueCache.get(requestKey); + + const timeout = setTimeout( + () => { + const request = cached + ? Promise.resolve(cached.data) + : getParts(options, controller.signal); + request + .then((data) => { + if (!controller.signal.aborted) { + if (!cached) { + catalogueCache.set(requestKey, { + data, + expiresAt: Date.now() + CACHE_TTL_MS, + }); + // Bound memory usage when browsing many filters and pages. + if (catalogueCache.size > MAX_CACHED_PAGES) { + catalogueCache.delete(catalogueCache.keys().next().value!); + } + } + setResult({ + key: requestKey, + data, + error: null, + }); + } + }) + .catch((error: unknown) => { + if (!controller.signal.aborted) { + setResult({ + key: requestKey, + data: null, + error: + error instanceof Error + ? error.message + : "Could not load parts", + }); + } + }); + }, + cached ? 0 : 300, + ); // Cached pages skip the search debounce delay. return () => { clearTimeout(timeout); diff --git a/frontend/src/test/useParts.test.tsx b/frontend/src/test/useParts.test.tsx index af51969..36569aa 100644 --- a/frontend/src/test/useParts.test.tsx +++ b/frontend/src/test/useParts.test.tsx @@ -1,21 +1,31 @@ -import { it, expect, vi } from "vitest"; +import { afterEach, beforeEach, it, expect, vi } from "vitest"; import { renderHook, waitFor } from "@testing-library/react"; import { useParts } from "@/hooks/useParts"; -import { getParts, type PartsPage } from "@/api/parts"; +import { getParts, type PartsOptions, type PartsPage } from "@/api/parts"; vi.mock("@/api/parts", () => ({ getParts: vi.fn(), })); -vi.mocked(getParts).mockResolvedValue({ +const emptyPage: PartsPage = { parts: [], totalCount: 0, pageInfo: { hasNextPage: false, endCursor: null, }, +}; + +let now = Date.now(); +beforeEach(() => { + // Expire previous tests' entries without exposing cache internals to tests. + now += 60_001; + vi.spyOn(Date, "now").mockImplementation(() => now); + vi.mocked(getParts).mockReset().mockResolvedValue(emptyPage); }); +afterEach(() => vi.restoreAllMocks()); + it("calls getParts with the search term", async () => { renderHook(() => useParts({ search: "RTX" })); @@ -117,3 +127,78 @@ it("returns the correct parts from the API", async () => { expect(result.current.totalCount).toBe(2); expect(result.current.error).toBeNull(); }); + +it("reuses a recent catalogue page when returning to the same filters", async () => { + const gpuPage = { ...emptyPage, totalCount: 100 }; + const cpuPage = { ...emptyPage, totalCount: 50 }; + vi.mocked(getParts) + .mockResolvedValueOnce(gpuPage) + .mockResolvedValueOnce(cpuPage); + const { result, rerender } = renderHook( + (options: PartsOptions) => useParts(options), + { initialProps: { category: "GPU" } as PartsOptions }, + ); + await waitFor(() => expect(result.current.totalCount).toBe(100)); + rerender({ category: "CPU" }); + await waitFor(() => expect(result.current.totalCount).toBe(50)); + rerender({ category: "GPU" }); + await waitFor(() => expect(result.current.totalCount).toBe(100)); + expect(getParts).toHaveBeenCalledTimes(2); +}); + +it("shares cached results with a newly mounted catalogue and refetches after expiry", async () => { + vi.mocked(getParts).mockResolvedValueOnce({ ...emptyPage, totalCount: 100 }); + const first = renderHook(() => useParts({ category: "GPU" })); + await waitFor(() => expect(first.result.current.totalCount).toBe(100)); + first.unmount(); + + const second = renderHook(() => useParts({ category: "GPU" })); + await waitFor(() => expect(second.result.current.totalCount).toBe(100)); + expect(getParts).toHaveBeenCalledTimes(1); + second.unmount(); + + now += 60_001; + vi.mocked(getParts).mockResolvedValueOnce({ ...emptyPage, totalCount: 101 }); + const third = renderHook(() => useParts({ category: "GPU" })); + await waitFor(() => expect(third.result.current.totalCount).toBe(101)); + expect(getParts).toHaveBeenCalledTimes(2); +}); + +it("keeps pagination and sorting results separate and caches pagination metadata", async () => { + const firstPage = { + ...emptyPage, + totalCount: 100, + pageInfo: { hasNextPage: true, endCursor: "page-2" }, + }; + vi.mocked(getParts) + .mockResolvedValueOnce(firstPage) + .mockResolvedValueOnce({ ...emptyPage, totalCount: 100 }) + .mockResolvedValueOnce({ ...emptyPage, totalCount: 100 }); + const { result, rerender } = renderHook( + (options: PartsOptions) => useParts(options), + { initialProps: { category: "GPU", first: 40 } as PartsOptions }, + ); + await waitFor(() => expect(result.current.loading).toBe(false)); + rerender({ category: "GPU", first: 40, after: "page-2" }); + await waitFor(() => expect(result.current.loading).toBe(false)); + expect(result.current.pageInfo?.hasNextPage).toBe(false); + rerender({ category: "GPU", first: 40, sortBy: "PRICE" }); + await waitFor(() => expect(result.current.loading).toBe(false)); + rerender({ category: "GPU", first: 40 }); + await waitFor(() => + expect(result.current.pageInfo).toEqual(firstPage.pageInfo), + ); + expect(getParts).toHaveBeenCalledTimes(3); +}); + +it("does not cache failed requests", async () => { + vi.mocked(getParts).mockRejectedValueOnce(new Error("Network error")); + const first = renderHook(() => useParts({ search: "RTX" })); + await waitFor(() => expect(first.result.current.error).toBe("Network error")); + first.unmount(); + + const second = renderHook(() => useParts({ search: "RTX" })); + await waitFor(() => expect(second.result.current.loading).toBe(false)); + expect(second.result.current.error).toBeNull(); + expect(getParts).toHaveBeenCalledTimes(2); +}); From cb45b82ad80b46f4790e48ba11710db1560ce670 Mon Sep 17 00:00:00 2001 From: Wilhelm Svensgaard Rolstad Date: Thu, 12 Nov 2026 17:39:07 +0100 Subject: [PATCH 05/42] fix:specified canvas.toDataUrl format to be webp instead of png --- frontend/src/hooks/usePartImageLayout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hooks/usePartImageLayout.ts b/frontend/src/hooks/usePartImageLayout.ts index 5c3236b..07d5486 100644 --- a/frontend/src/hooks/usePartImageLayout.ts +++ b/frontend/src/hooks/usePartImageLayout.ts @@ -50,7 +50,7 @@ function trimImageWhitespace(image: HTMLImageElement): string { canvas.width, height, ); - return canvas.toDataURL(); + return canvas.toDataURL("image/webp", 0.9); } catch { // External images may block pixel access; keep displaying the original. return image.src; From 906d96e8fc6d42242bd957c73441c785e422bab7 Mon Sep 17 00:00:00 2001 From: Wilhelm Svensgaard Rolstad Date: Thu, 12 Nov 2026 17:44:22 +0100 Subject: [PATCH 06/42] fix: fixed minor inconsistency in the close button on mobile detail page --- frontend/src/components/MobileComponentDetailPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/MobileComponentDetailPage.tsx b/frontend/src/components/MobileComponentDetailPage.tsx index 1cbe16a..7dc309a 100644 --- a/frontend/src/components/MobileComponentDetailPage.tsx +++ b/frontend/src/components/MobileComponentDetailPage.tsx @@ -1,6 +1,7 @@ import type { PcPart } from "@/types/types"; import { useRef, useState } from "react"; import { usePartImageLayout } from "@/hooks/usePartImageLayout"; +import { X } from "lucide-react" import { formatSpecification, getSpecifications, @@ -51,11 +52,9 @@ export default function ComponentDetailPage({ part }: { part: PcPart }) { @@ -97,6 +96,7 @@ export default function ComponentDetailPage({ part }: { part: PcPart }) { ${expanded ? "rounded-none" : "rounded-t-3xl"}`} style={specsStyle} > +

Specifications

Date: Thu, 12 Nov 2026 17:54:01 +0100 Subject: [PATCH 07/42] docs: updated readme for acessibility and sustainability as well as the state of the current prototype --- README.md | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4ca1aa8..30e0618 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,22 @@ The app displays a component catalog backed by PostgreSQL and GraphQL, with sear We have worked to make Full Circuit usable for as many people as possible. We use semantic HTML elements, labels for inputs and buttons, and alternative text for component images. Keyboard users can navigate using focusable controls with a visible focus outline. We have also made the layout responsive so it works on both mobile and desktop. We will continue checking accessibility as the project develops. +The add-to-build feedback dialog confirms the action on the current page and lets users choose between **Continue exploring** and **Go back to the build page**. This reduces the "boomerang effect" of moving back and forth between the catalog and builder just to check whether a component was added. Keeping users in their browsing context reduces the need to reorient themselves and gives them control over when to navigate. When replacing a component, the dialog also lets users keep their current selection or confirm the replacement. The dialog has an accessible title and description, and loading and success messages use a status announcement for assistive technologies. + ## Sustainability -We have tried to reduce unnecessary data transfer. The catalog loads a limited number of components at a time, and we only request the information needed to display them. More detailed information is fetched when users choose to view a component. We also use WebP images and a dark color scheme, which may help reduce energy use. These choices can reduce resource use while keeping the application easy to use. +We aim to make as few unnecessary backend calls as possible by fetching data only when needed and reusing information already available in the browser. The catalog's data flow works as follows: + +1. **Check the catalogue cache.** Each query is identified by its search, filters, sorting, page size, and cursor. If a matching response is less than one minute old, the frontend reuses its parts, total count, and pagination information without contacting the backend. For example, switching from GPUs to CPUs and back can reuse the previous GPU results. +2. **Fetch summaries when there is no fresh cached response.** The catalog requests 40 components at a time, including fields such as name, price, category, and image URL. Full specifications are left out, so browsing does not download detailed data for every displayed part. The response is then cached for reuse. Requests are debounced by 300 ms to avoid a backend call for every keystroke during rapid typing. +3. **Fetch specifications when the user needs them.** Opening a component's details fetches its specifications if they are missing. Adding directly from a card also fetches missing specifications, because the build needs them for compatibility checks. +4. **Reuse loaded specifications.** A card keeps specifications fetched for its detail view while it remains mounted. Opening details, closing them, and then adding from that same card therefore requires only one specifications request. Adding from inside the detail view also reuses the complete part already available. + +The catalogue cache holds at most 100 pages to limit memory use and is cleared on a browser reload. Specifications stored in an individual card are discarded when that card unmounts. These measures reduce repeated network transfers and backend queries while allowing catalogue results to be refreshed after their cache entries expire. + +Memoized component cards and a stable details callback let React skip rendering unchanged cards. Stable IDs and keeping the previous results mounted during loading also avoid unnecessarily removing and recreating matching cards when filters change. This reduces repeated rendering work on the user's device and removes the brief empty-list flash. The result-count area keeps its space during loading to prevent the cards from jumping. + +Component images use WebP. When the mobile detail view crops image whitespace, it explicitly requests WebP output at quality `0.9` instead of the canvas default of PNG. This avoids generating PNG copies of WebP images and aims to keep the encoded image data compact. Cropping still requires pixel processing and re-encoding; the generated image is local and does not require another server download. Together, these choices target lower resource use, although we have not measured their energy or carbon savings. ## Dataset @@ -135,32 +148,37 @@ npm run dev ## Current prototype -The frontend is built with React, TypeScript, Vite, and Tailwind CSS. The catalog handles loading, error, and empty states. Catalog cards and the component detail dialog display data retrieved from the backend. The full-screen detail dialog uses separate mobile and desktop layouts, with an expandable specifications panel on desktop. +The frontend is built with React, TypeScript, Vite, and Tailwind CSS. The catalog supports search, filtering, sorting, and switching between list and grid layouts. It loads 40 components at a time through infinite scrolling or the **Load more** button, and handles loading, error, and empty states. Recent catalogue responses are cached for one minute. Previous cards remain visible while new results load, and unchanged cards can skip rendering through memoization. + +Catalog cards initially display summary data from the backend. Full specifications are fetched when needed for details or adding to a build, and cards retain specifications loaded for their detail view while mounted. The full-screen detail dialog has separate mobile and desktop layouts, both with expandable specifications. See [Sustainability](#sustainability) for the caching and data-flow details. ### PC builder -Open the builder through the navigation bar or by visiting `?page=builder`. Use **Add to build** on a catalog card or component detail page to select a part. +Open the builder through the navigation bar or by visiting `/project2/builder`. Use **Add to build** on a catalog card or component detail page to select a part. -- Each of the eight categories has one slot. Adding another component in the same category replaces the previous selection. +- Each of the eight categories has one slot. Adding another component in the same category opens a confirmation dialog, allowing users to keep the current component or replace it. +- Feedback confirms when a component has been added and offers **Continue exploring** or **Go back to the build page**, so users choose when to leave the catalog. - Empty slots have a **Choose** action that opens the category catalog. Selected slots show the component image, name, first three specifications, and price, with actions to change or delete the selection. - The summary shows the selected component count, progress, and total price in EUR. Missing prices currently contribute zero to the total, so the displayed sum may be incomplete. -- **Clear build** removes all selections. +- **Clear build** asks for confirmation before removing all selections. Selections are stored as JSON under the `pc-builder` key in `localStorage` and restored when the app loads. This preserves the current build across refreshes and page navigation in the same browser and site. Localhost and the deployed site have separate storage. The compatibility panel checks selected parts and shows results such as compatible, incompatible, warning, or unknown. The checks may not cover every possible combination. Users can also publish their builds to the database, but private builds are not synchronized between browsers. ### Navigation and shared build state -`frontend/src/App.tsx` renders the page based on URL query parameters: +`frontend/src/App.tsx` uses React Router to render pages based on URL paths: + +| Page | Route within the app | +| ------------------ | ---------------------------------------------- | +| Home | `/` | +| Explore components | `/explore` | +| Component category | `/explore/category/CPU` (or another category) | +| PC builder | `/builder` | +| Community builds | `/community` | -| Page | URL relative to the app base | -| ------------------ | ------------------------------------------- | -| Home | No query parameters | -| Explore components | `?page=explore` | -| Component category | `?category=CPU` (or another category value) | -| PC builder | `?page=builder` | -| Community builds | `?page=community` | +`BrowserRouter` in `frontend/src/main.tsx` uses `import.meta.env.BASE_URL` as its base path. With the `/project2/` deployment base, the builder's full path is `/project2/builder`. Category URLs use the values defined in `frontend/src/types/categories.ts`, such as `CPU`, rather than display labels such as `Processors`. Unknown routes display a 404 message. -The builder takes precedence if both `page=builder` and a category are supplied. Category URLs use the values defined in `frontend/src/types/categories.ts`, such as `CPU`, rather than display labels such as `Processors`. Navigation uses regular links or `window.location.href`, which reload the page; the build provider restores saved selections afterward. Home links use `import.meta.env.BASE_URL` to support deployment under `/project2/`. +Most navigation uses React Router links or `navigate()`, changing pages without a full browser reload. The feedback dialog's **Go back to the build page** action currently uses `window.location.href`, which reloads the page; the build provider then restores saved selections from local storage. `BuildProvider` wraps the app in `frontend/src/main.tsx`. Components use `useBuild()` from `frontend/src/context/BuildContext.ts` to access: @@ -175,7 +193,7 @@ Use `AddToBuildButton` for actions that add a component to this shared state. Th Users can publish their current build from the PC builder by entering a name and an optional comment. Publishing saves the chosen parts in PostgreSQL through GraphQL. Empty builds cannot be published, and the page shows an error if publishing fails. -The Community page at `?page=community` lists published builds with their names, comments, and parts. Viewing the list does not change the visitor's current build. Individual build pages and shareable links are not available yet. +The Community page at `/project2/community` lists published builds with their names, comments, and parts. It displays up to 20 builds per page, with **Previous** and **Next** controls when available, and handles loading, error, and empty states. Viewing the list does not change the visitor's current build. Individual build pages and shareable links are not available yet. ## Planned functionality From 7ae41bb268256595cac57ef20884a3126a08ca30 Mon Sep 17 00:00:00 2001 From: Wilhelm Svensgaard Rolstad Date: Thu, 12 Nov 2026 18:07:27 +0100 Subject: [PATCH 08/42] feat: added reload on failed search, filter reset on a category specific page sets the category to that one --- frontend/src/components/AllPartsPreview.tsx | 10 +-- frontend/src/components/FilterComponent.tsx | 6 +- frontend/src/hooks/useParts.ts | 10 ++- .../src/pages/categoryPage/categoryPage.tsx | 1 + frontend/src/test/AllPartsPreview.test.tsx | 65 ++++++++++++++----- frontend/src/test/CatalogInteraction.test.tsx | 2 + frontend/src/test/useParts.test.tsx | 24 ++++++- 7 files changed, 94 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/AllPartsPreview.tsx b/frontend/src/components/AllPartsPreview.tsx index 1c157c3..b9ffd86 100644 --- a/frontend/src/components/AllPartsPreview.tsx +++ b/frontend/src/components/AllPartsPreview.tsx @@ -94,7 +94,7 @@ function PartsResults({ setSelectedPart(null); } - const { parts, totalCount, pageInfo, loading, error } = useParts({ + const { parts, totalCount, pageInfo, loading, error, retry } = useParts({ ...options, first, after: pagination.filterKey === filterKey ? pagination.after : undefined, @@ -252,17 +252,17 @@ function PartsResults({ + )}
- - {stage !== "loading" && ( -
- - -
- )} - {stage === "success" && ( - ); } From 5a686a0ffaf0de3f48d0198140f2d0779e6a4fd7 Mon Sep 17 00:00:00 2001 From: Kristian Gjertsen Date: Thu, 12 Nov 2026 19:10:56 +0100 Subject: [PATCH 15/42] Remove manufacturers option from PartsOptions and related components --- frontend/src/api/parts.ts | 5 --- frontend/src/components/FilterComponent.tsx | 38 +------------------ .../src/components/pageSections/Header.tsx | 2 +- frontend/src/hooks/useParts.ts | 2 - 4 files changed, 3 insertions(+), 44 deletions(-) diff --git a/frontend/src/api/parts.ts b/frontend/src/api/parts.ts index 8a91da0..4a1a0c4 100644 --- a/frontend/src/api/parts.ts +++ b/frontend/src/api/parts.ts @@ -15,7 +15,6 @@ type ApiPart = { export type PartsOptions = { search?: string; category?: PcPart["category"]; - manufacturers?: string[]; sortBy?: "NAME" | "PRICE" | "MANUFACTURER" | "CATEGORY"; sortDirection?: "ASC" | "DESC"; first?: number; @@ -35,7 +34,6 @@ export async function getParts( { search = "", category, - manufacturers, sortBy = "NAME", sortDirection = "ASC", first = 20, @@ -52,7 +50,6 @@ export async function getParts( query GetParts( $search: String $category: Category - $manufacturers: [String!] $sortBy: SortField! $sortDirection: SortDirection! $first: Int! @@ -63,7 +60,6 @@ export async function getParts( after: $after search: $search category: $category - manufacturers: $manufacturers sortBy: $sortBy sortDirection: $sortDirection @@ -89,7 +85,6 @@ export async function getParts( variables: { search: search.trim() || null, category: category ?? null, - manufacturers: manufacturers?.length ? manufacturers : null, sortBy, sortDirection, first, diff --git a/frontend/src/components/FilterComponent.tsx b/frontend/src/components/FilterComponent.tsx index 04deefb..8756cf8 100644 --- a/frontend/src/components/FilterComponent.tsx +++ b/frontend/src/components/FilterComponent.tsx @@ -4,7 +4,7 @@ import type { PartsOptions } from "@/api/parts"; import { categories } from "@/types/categories"; import Button from "./buttons/Button"; -// Sort and both filters sit inline only when there is room next to the search field. +// Sort and category controls sit inline when there is room next to the search field. const DESKTOP_FILTER_WIDTH = 1200; type FilterComponentProps = { @@ -52,7 +52,7 @@ export default function FilterComponent({ setSearch(event.target.value)} className="min-w-0 flex-1 rounded-xl bg-surface px-4 py-3 placeholder:text-gray-300" @@ -110,23 +110,6 @@ export default function FilterComponent({ - - -

- Filter +
+

+ Filters

-