pruning #3
21 changed files with 511 additions and 353 deletions
404
package-lock.json
generated
404
package-lock.json
generated
|
|
@ -8,16 +8,14 @@
|
|||
"name": "doof",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@types/node": "^20.19.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||
|
|
@ -27,12 +25,10 @@
|
|||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.3.0",
|
||||
"msw": "^2.5.2",
|
||||
"node-fetch": "^2.6.9",
|
||||
"openapi-fetch": "^0.9.5",
|
||||
"openapi-typescript": "^7.4.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "~5.5.4",
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^1.6.0",
|
||||
"vue-tsc": "^2.0.29"
|
||||
}
|
||||
|
|
@ -127,24 +123,6 @@
|
|||
"url": "https://opencollective.com/babel"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/eslint-parser": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz",
|
||||
"integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
|
||||
"eslint-visitor-keys": "^2.1.0",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || >=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.11.0",
|
||||
"eslint": "^7.5.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
|
|
@ -1821,6 +1799,13 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
||||
|
|
@ -2531,6 +2516,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/schema": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/schemas": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||
|
|
@ -2593,10 +2588,11 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
|
|
@ -2626,15 +2622,6 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eslint-scope": "5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-ipc/js-queue": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz",
|
||||
|
|
@ -3723,6 +3710,34 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz",
|
||||
"integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
"@bcoe/v8-coverage": "^0.2.3",
|
||||
"debug": "^4.3.4",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.4",
|
||||
"istanbul-reports": "^3.1.6",
|
||||
"magic-string": "^0.30.5",
|
||||
"magicast": "^0.3.3",
|
||||
"picocolors": "^1.0.0",
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"test-exclude": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
||||
|
|
@ -6249,6 +6264,7 @@
|
|||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz",
|
||||
"integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
|
@ -7350,15 +7366,6 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-webpack-plugin": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
|
||||
|
|
@ -9294,6 +9301,112 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
|
|
@ -10366,6 +10479,18 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
|
|
@ -13516,6 +13641,21 @@
|
|||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^7.1.4",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
|
@ -13912,16 +14052,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
|
|
@ -15515,17 +15645,6 @@
|
|||
"semver": "^6.3.1"
|
||||
}
|
||||
},
|
||||
"@babel/eslint-parser": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz",
|
||||
"integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
|
||||
"eslint-visitor-keys": "^2.1.0",
|
||||
"semver": "^6.3.1"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
|
|
@ -16667,6 +16786,12 @@
|
|||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@discoveryjs/json-ext": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
||||
|
|
@ -17023,6 +17148,12 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@istanbuljs/schema": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||
"dev": true
|
||||
},
|
||||
"@jest/schemas": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||
|
|
@ -17071,9 +17202,9 @@
|
|||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
|
|
@ -17100,15 +17231,6 @@
|
|||
"strict-event-emitter": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-scope": "5.1.1"
|
||||
}
|
||||
},
|
||||
"@node-ipc/js-queue": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz",
|
||||
|
|
@ -17868,6 +17990,27 @@
|
|||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"dev": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz",
|
||||
"integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
"@bcoe/v8-coverage": "^0.2.3",
|
||||
"debug": "^4.3.4",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.4",
|
||||
"istanbul-reports": "^3.1.6",
|
||||
"magic-string": "^0.30.5",
|
||||
"magicast": "^0.3.3",
|
||||
"picocolors": "^1.0.0",
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"test-exclude": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@vitest/expect": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
||||
|
|
@ -19755,7 +19898,8 @@
|
|||
"core-js": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz",
|
||||
"integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg=="
|
||||
"integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==",
|
||||
"dev": true
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.35.0",
|
||||
|
|
@ -20733,12 +20877,6 @@
|
|||
"estraverse": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-webpack-plugin": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
|
||||
|
|
@ -21907,6 +22045,76 @@
|
|||
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
|
||||
"dev": true
|
||||
},
|
||||
"istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true
|
||||
},
|
||||
"istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "^7.5.3"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
|
|
@ -22660,6 +22868,17 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
|
|
@ -24939,6 +25158,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^7.1.4",
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
|
@ -25212,12 +25442,6 @@
|
|||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"dev": true
|
||||
},
|
||||
"undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"dev": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
|
|
|
|||
37
package.json
37
package.json
|
|
@ -10,39 +10,35 @@
|
|||
"prepare": "husky install",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:vue": "vue-tsc --noEmit",
|
||||
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:vue": "vue-tsc --noEmit",
|
||||
"codegen:api": "openapi-typescript ../munch-ease-backend/openapi.json -o src/api/types.ts",
|
||||
"codegen": "npm run codegen:api",
|
||||
"codegen:check": "node scripts/codegen-check.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@types/node": "^20.19.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.3.0",
|
||||
"msw": "^2.5.2",
|
||||
"node-fetch": "^2.6.9",
|
||||
"openapi-fetch": "^0.9.5",
|
||||
"openapi-typescript": "^7.4.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "~5.5.4",
|
||||
"undici": "^6.19.8",
|
||||
"typescript": "~5.5.4",
|
||||
"vitest": "^1.6.0",
|
||||
"vue-tsc": "^2.0.29"
|
||||
},
|
||||
|
|
@ -80,10 +76,19 @@
|
|||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"],
|
||||
"excludedFiles": ["src/api/types.ts", "src/**/*.d.ts"],
|
||||
"files": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.vue",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"excludedFiles": [
|
||||
"src/api/types.ts",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.eslint.json"],
|
||||
"project": [
|
||||
"./tsconfig.eslint.json"
|
||||
],
|
||||
"tsconfigRootDir": "."
|
||||
},
|
||||
"rules": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { api } from '@/api/client'
|
||||
import type { components } from '@/api/types'
|
||||
import { toDate, decodeMeal, decodeRecipe, decodeIngredients, decodeShoppingList, decodeShoppingListItems, decodeIngredient } from '@/domain/decoders'
|
||||
import { toDate, decodeMeal, decodeRecipe, decodeIngredients, decodeShoppingList, decodeShoppingListItems, decodeIngredient, decodeLookup } from '@/domain/decoders'
|
||||
import type {
|
||||
Recipe,
|
||||
Meal,
|
||||
|
|
@ -19,22 +19,7 @@ function httpError(response: Response, error: unknown): Error {
|
|||
return new Error(`${response.status} ${response.statusText || 'HTTP error'}`)
|
||||
}
|
||||
|
||||
// Small helper to decode optional lookup maps without repeating loops everywhere
|
||||
function decodeLookup<TIn, TOut>(
|
||||
raw: Record<string, TIn> | null | undefined,
|
||||
decode: (v: TIn) => TOut | null
|
||||
): Record<string, TOut> | undefined {
|
||||
if (!raw) return undefined
|
||||
const out: Record<string, TOut> = {}
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(raw, key)) continue
|
||||
const maybe = raw[key]
|
||||
if (maybe === undefined) continue
|
||||
const decoded = decode(maybe)
|
||||
if (decoded) out[String(key)] = decoded
|
||||
}
|
||||
return out
|
||||
}
|
||||
// decodeLookup moved to domain/decoders to be reused across SDK and other modules
|
||||
|
||||
// Shopping list mapped view types now come from domain/types
|
||||
|
||||
|
|
@ -50,17 +35,21 @@ function attachItemRefs(
|
|||
const listId = item.listId ?? undefined
|
||||
const created = item.createdDate
|
||||
|
||||
if (ingredientId !== undefined && lookups.ingredientsLookup && lookups.ingredientsLookup[String(ingredientId)] !== undefined) {
|
||||
item.ingredient = lookups.ingredientsLookup[String(ingredientId)]
|
||||
if (ingredientId !== undefined && lookups.ingredientsLookup) {
|
||||
const v = lookups.ingredientsLookup[String(ingredientId)]
|
||||
if (v !== undefined) item.ingredient = v
|
||||
}
|
||||
if (mealId !== undefined && lookups.mealsLookup && lookups.mealsLookup[String(mealId)] !== undefined) {
|
||||
item.meal = lookups.mealsLookup[String(mealId)]
|
||||
if (mealId !== undefined && lookups.mealsLookup) {
|
||||
const v = lookups.mealsLookup[String(mealId)]
|
||||
if (v !== undefined) item.meal = v
|
||||
}
|
||||
if (recipeId !== undefined && lookups.recipesLookup && lookups.recipesLookup[String(recipeId)] !== undefined) {
|
||||
item.recipe = lookups.recipesLookup[String(recipeId)]
|
||||
if (recipeId !== undefined && lookups.recipesLookup) {
|
||||
const v = lookups.recipesLookup[String(recipeId)]
|
||||
if (v !== undefined) item.recipe = v
|
||||
}
|
||||
if (listId !== undefined && lookups.shoppingListLookup && lookups.shoppingListLookup[String(listId)] !== undefined) {
|
||||
item.list = lookups.shoppingListLookup[String(listId)]
|
||||
if (listId !== undefined && lookups.shoppingListLookup) {
|
||||
const v = lookups.shoppingListLookup[String(listId)]
|
||||
if (v !== undefined) item.list = v
|
||||
}
|
||||
if (created !== undefined) item.createdDate = toDate(created)
|
||||
}
|
||||
|
|
@ -88,7 +77,7 @@ export function mapPurchasedShoppingList(dto: components['schemas']['PurchasedSh
|
|||
...(ingredientsLookup && { ingredientsLookup }),
|
||||
...(mealsLookup && { mealsLookup }),
|
||||
...(recipesLookup && { recipesLookup }),
|
||||
shoppingListLookup: { [String(list.id)]: list },
|
||||
...(list && { shoppingListLookup: { [String(list.id)]: list } }),
|
||||
}
|
||||
attachItemRefs(list.items, lookups)
|
||||
}
|
||||
|
|
@ -99,6 +88,8 @@ export function mapPurchasedShoppingList(dto: components['schemas']['PurchasedSh
|
|||
...(mealsLookup && { mealsLookup }),
|
||||
...(recipesLookup && { recipesLookup }),
|
||||
...(list && { list }),
|
||||
// include shoppingListLookup when list exists for consistency with lookups type
|
||||
...(list && { shoppingListLookup: { [String(list.id)]: list } }),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,14 +146,15 @@ export async function listRecipes(params?: { q?: string | null; cursor?: string
|
|||
}
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes', { params: { query } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = fromOpenApiPage(data ?? null, (r) => decodeRecipe(r))
|
||||
return { ...mapped, items: mapped.items.filter((r): r is Recipe => !!r) }
|
||||
return fromOpenApiPage(data ?? null, (r) => decodeRecipe(r))
|
||||
}
|
||||
|
||||
export async function getRecipe(id: number | string): Promise<Recipe | null> {
|
||||
export async function getRecipe(id: number | string): Promise<Recipe> {
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes/{recipe_id}', { params: { path: { recipe_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeRecipe(data)
|
||||
const mapped = decodeRecipe(data)
|
||||
if (!mapped) throw new Error('Recipe not found')
|
||||
return mapped
|
||||
}
|
||||
|
||||
export async function saveRecipe(recipe: components['schemas']['Recipe-Input']): Promise<Recipe | null> {
|
||||
|
|
@ -201,7 +193,7 @@ export async function parseProduct(
|
|||
}
|
||||
|
||||
// Persons
|
||||
export async function listPersons(params?: { q?: string | null; cursor?: string | null; limit?: number }): Promise<Page<components['schemas']['Person']>> {
|
||||
async function listPersons(params?: { q?: string | null; cursor?: string | null; limit?: number }): Promise<Page<components['schemas']['Person']>> {
|
||||
const query: Record<string, unknown> = {}
|
||||
if (params) {
|
||||
if (params.q !== undefined) query.q = params.q
|
||||
|
|
@ -235,10 +227,12 @@ export async function getUpcomingMeals(from: Date, to: Date): Promise<Meal[]> {
|
|||
.sort((a, b) => ((a.suggestedDate?.getTime() ?? 0) - (b.suggestedDate?.getTime() ?? 0)))
|
||||
}
|
||||
|
||||
export async function getMeal(id: number | string): Promise<Meal | null> {
|
||||
export async function getMeal(id: number | string): Promise<Meal> {
|
||||
const { data, error, response } = await api.GET('/api/v1/meals/{meal_id}', { params: { path: { meal_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeMeal(data)
|
||||
const mapped = decodeMeal(data)
|
||||
if (!mapped) throw new Error('Meal not found')
|
||||
return mapped
|
||||
}
|
||||
|
||||
export async function saveMeal(meal: components['schemas']['Meal-Input']): Promise<Meal | null> {
|
||||
|
|
@ -257,12 +251,14 @@ export async function saveMeal(meal: components['schemas']['Meal-Input']): Promi
|
|||
}
|
||||
}
|
||||
|
||||
export async function markMealConsumed(mealId: number | string): Promise<Meal | null> {
|
||||
export async function markMealConsumed(mealId: number | string): Promise<Meal> {
|
||||
const { data, error, response } = await api.POST('/api/v1/meals/{meal_id}/consumed', {
|
||||
params: { path: { meal_id: Number(mealId) }, cookie: { user_id: 0 } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return decodeMeal(data)
|
||||
const mapped = decodeMeal(data)
|
||||
if (!mapped) throw new Error('Meal not found')
|
||||
return mapped
|
||||
}
|
||||
|
||||
export async function deleteMeal(mealId: number | string): Promise<void> {
|
||||
|
|
@ -288,24 +284,27 @@ export async function saveMyShoppingList(items: components['schemas']['Ingredien
|
|||
return decodeIngredients(data ?? [])
|
||||
}
|
||||
|
||||
export async function getShoppingList(id: number | string) {
|
||||
export async function getShoppingList(id: number | string): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
||||
const { data, error, response } = await api.GET('/api/v1/shopping/{list_id}', { params: { path: { list_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = mapPurchasedShoppingList(data)
|
||||
return mapped?.list ?? null
|
||||
}
|
||||
|
||||
export async function getCurrentShoppingList() {
|
||||
export async function getCurrentShoppingList(): Promise<CurrentShoppingListDTO> {
|
||||
const { data, error, response } = await api.GET('/api/v1/shopping/current')
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return mapCurrentShoppingList(data)
|
||||
const mapped = mapCurrentShoppingList(data)
|
||||
if (!mapped) throw new Error('Failed to map current shopping list')
|
||||
return mapped
|
||||
}
|
||||
|
||||
type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null }
|
||||
type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null }
|
||||
export type PurchaseRequest = PurchaseExisting | PurchaseRefs
|
||||
|
||||
export async function purchaseShoppingList(
|
||||
completedRequests: Array<PurchaseExisting | PurchaseRefs>
|
||||
completedRequests: PurchaseRequest[]
|
||||
): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
||||
if (!Array.isArray(completedRequests) || completedRequests.length === 0) return null
|
||||
// Map incoming requests: if id provided and >= 0, use it; otherwise send identifiers for ingredient/recipe/meal
|
||||
|
|
|
|||
|
|
@ -506,9 +506,10 @@ export interface components {
|
|||
prevCursor?: string | null;
|
||||
/**
|
||||
* Total
|
||||
* @description Optional total count
|
||||
* @description Total count
|
||||
* @default 0
|
||||
*/
|
||||
total?: number | null;
|
||||
total: number;
|
||||
};
|
||||
/** Page[Recipe] */
|
||||
Page_Recipe_: {
|
||||
|
|
@ -520,9 +521,10 @@ export interface components {
|
|||
prevCursor?: string | null;
|
||||
/**
|
||||
* Total
|
||||
* @description Optional total count
|
||||
* @description Total count
|
||||
* @default 0
|
||||
*/
|
||||
total?: number | null;
|
||||
total: number;
|
||||
};
|
||||
/** Person */
|
||||
Person: {
|
||||
|
|
@ -575,8 +577,6 @@ export interface components {
|
|||
imgSmall: string;
|
||||
/** Imglarge */
|
||||
imgLarge: string;
|
||||
/** Rawdata */
|
||||
rawData?: Record<string, never> | null;
|
||||
};
|
||||
/** ProductUrl */
|
||||
ProductUrl: {
|
||||
|
|
@ -626,7 +626,7 @@ export interface components {
|
|||
*/
|
||||
dateCreated?: string;
|
||||
/** Createdbyid */
|
||||
createdById: number | null;
|
||||
createdById: number;
|
||||
createdBy?: components["schemas"]["Person"] | null;
|
||||
/** Datehidden */
|
||||
dateHidden?: string | null;
|
||||
|
|
@ -659,7 +659,7 @@ export interface components {
|
|||
*/
|
||||
dateCreated?: string;
|
||||
/** Createdbyid */
|
||||
createdById: number | null;
|
||||
createdById: number;
|
||||
createdBy?: components["schemas"]["Person"] | null;
|
||||
/** Datehidden */
|
||||
dateHidden?: string | null;
|
||||
|
|
|
|||
|
|
@ -118,8 +118,8 @@
|
|||
<script setup lang="ts">
|
||||
import { reactive, onBeforeMount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getMeal, saveMeal, toMealInput } from '@/composables/useMeals'
|
||||
import { getRecipe } from '@/api/sdk'
|
||||
import { getMeal, saveMeal, getRecipe } from '@/api/sdk'
|
||||
import { toMealInput } from '@/domain/decoders'
|
||||
import { currentUser } from '@/api/auth'
|
||||
import { useAlert } from '@/composables/useAlert'
|
||||
import { parseRouteId } from '@/router/helpers'
|
||||
|
|
@ -162,13 +162,11 @@ const meal = reactive<Meal>({
|
|||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const id = parseRouteId(route.params.id)
|
||||
if (id !== null) {
|
||||
const loaded = await getMeal(id)
|
||||
if (loaded) {
|
||||
Object.assign(meal, loaded)
|
||||
}
|
||||
} else {
|
||||
const id = parseRouteId(route.params.id)
|
||||
if (id !== null) {
|
||||
const loaded = await getMeal(id)
|
||||
Object.assign(meal, loaded)
|
||||
} else {
|
||||
const self = await currentUser()
|
||||
if (self) {
|
||||
meal.chefs = [self]
|
||||
|
|
@ -214,8 +212,7 @@ function addPerson(list: PeopleKey, person: Person) {
|
|||
|
||||
async function selectRecipe(recipe: { id: number | string }) {
|
||||
// Refetch to get additional details
|
||||
const r = await getRecipe(recipe.id)
|
||||
if (!r) return
|
||||
const r = await getRecipe(recipe.id)
|
||||
|
||||
if (r.createdBy) {
|
||||
addPersonIfNotExists(meal.chefs, r.createdBy)
|
||||
|
|
|
|||
|
|
@ -34,18 +34,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ago } from '@/dateformats'
|
||||
|
||||
type Person = { id: number; name: string }
|
||||
type MealRecipe = { recipe: { name: string } }
|
||||
type Ingredient = { name: string }
|
||||
type Meal = {
|
||||
suggestedDate: Date | null
|
||||
chefs: Person[]
|
||||
consumers: Person[]
|
||||
recipes?: MealRecipe[]
|
||||
extraIngredients?: Ingredient[]
|
||||
purchaseDate?: Date | null
|
||||
}
|
||||
import type { Meal } from '@/domain/types'
|
||||
|
||||
const props = defineProps<{ meal: Meal }>()
|
||||
|
||||
|
|
@ -86,7 +75,10 @@ const dayOfWeek = computed(() =>
|
|||
const mealTitle = computed(() => {
|
||||
const recipes = props.meal.recipes ?? []
|
||||
const extras = props.meal.extraIngredients ?? []
|
||||
const recipesText = englishList(recipes.map((mr) => mr.recipe.name))
|
||||
const recipeNames = recipes
|
||||
.map((mr) => mr.recipe?.name)
|
||||
.filter((n): n is string => typeof n === 'string' && n.length > 0)
|
||||
const recipesText = englishList(recipeNames)
|
||||
const ingredientsText = englishList(extras.map((i) => i.name))
|
||||
|
||||
if (recipesText && ingredientsText) return `${recipesText} with ${ingredientsText}`
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
import { ref, onBeforeMount } from 'vue'
|
||||
import ActionItem from '@/components/ActionItem.vue'
|
||||
import MealCard from '@/components/meals/MealCard.vue'
|
||||
import { getUpcomingMeals, markMealConsumed, deleteMeal } from '@/composables/useMeals'
|
||||
import { getUpcomingMeals, markMealConsumed, deleteMeal } from '@/api/sdk'
|
||||
const chevronDown = new URL('@/assets/chevron-down.svg', import.meta.url).toString()
|
||||
const chevronUp = new URL('@/assets/chevron-up.svg', import.meta.url).toString()
|
||||
const planMeal = new URL('@/assets/plan-meal.svg', import.meta.url).toString()
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ async function refreshRecipe() {
|
|||
if (id !== null && id >= 0) {
|
||||
const r = await getRecipe(id)
|
||||
recipe.value = r
|
||||
link.value = r?.link ?? ''
|
||||
link.value = r.link ?? ''
|
||||
return
|
||||
} else if (link.value) {
|
||||
const r = await parseRecipe(link.value)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ import { ref, computed, onBeforeMount } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { useAlert } from '@/composables/useAlert'
|
||||
import { useShopping } from '@/composables/useShopping'
|
||||
import { useMeals } from '@/composables/useMeals'
|
||||
import { getUpcomingMeals } from '@/api/sdk'
|
||||
import { type Group } from '@/composables/useShopping'
|
||||
type UIMeal = import('@/domain/types').Meal
|
||||
import MealSelectionList from './MealSelectionList.vue'
|
||||
|
|
@ -117,7 +117,6 @@ const closeIcon = new URL('@/assets/close.svg', import.meta.url).toString()
|
|||
const router = useRouter()
|
||||
const { show: showAlert } = useAlert()
|
||||
const { getCurrentShoppingList, requestMeal, unrequestMeal, purchaseFromGroups, groupsFrom, mealsFrom } = useShopping()
|
||||
const { getUpcomingMeals } = useMeals()
|
||||
|
||||
const from = new Date()
|
||||
from.setTime(0)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ const router = useRouter()
|
|||
const { loadUser } = useAuth()
|
||||
const { getMyShoppingList, saveMyShoppingList } = useShopping()
|
||||
|
||||
const person = ref<{ id?: number; name?: string } | null>(null)
|
||||
const ingredients = ref<Ingredient[]>([])
|
||||
|
||||
async function updateShoppingList(save = false) {
|
||||
|
|
@ -82,7 +81,6 @@ async function onEditing(isStartingEdit: boolean) {
|
|||
onBeforeMount(async () => {
|
||||
const u = await loadUser()
|
||||
if (!u) return router.push({ name: 'login' })
|
||||
person.value = u
|
||||
await updateShoppingList()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@
|
|||
:key="source.id"
|
||||
>
|
||||
<span v-if="index">, and </span>
|
||||
<span v-if="source.person">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for
|
||||
{{ source.person.name }}
|
||||
<!-- Generic ingredient-only display when no recipe/meal context; person ref removed -->
|
||||
<span v-if="!source.recipe && !source.meal && source.ingredient">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }}
|
||||
</span>
|
||||
<span v-else-if="source.recipe">
|
||||
<span v-else-if="source.recipe && source.ingredient">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
||||
<router-link :to="`/recipes/${source.recipe.id}/`">{{
|
||||
source.recipe.name
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
: ''
|
||||
}}</router-link>
|
||||
</span>
|
||||
<span v-else-if="source.meal">
|
||||
<span v-else-if="source.meal && source.ingredient">
|
||||
{{ source.ingredient.line }} for
|
||||
<router-link :to="`/meals/${source.meal?.id}/`">{{
|
||||
source.meal?.suggestedDate
|
||||
|
|
@ -78,11 +78,11 @@
|
|||
:key="source.id"
|
||||
>
|
||||
<span v-if="index">, and </span>
|
||||
<span v-if="source.person">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for
|
||||
{{ source.person.name }}
|
||||
<!-- Generic ingredient-only display when no recipe/meal context; person ref removed -->
|
||||
<span v-if="!source.recipe && !source.meal && source.ingredient">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }}
|
||||
</span>
|
||||
<span v-else-if="source.recipe">
|
||||
<span v-else-if="source.recipe && source.ingredient">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
||||
<router-link :to="`/recipes/${source.recipe.id}/`">{{
|
||||
source.recipe.name
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
: ''
|
||||
}}</router-link>
|
||||
</span>
|
||||
<span v-else-if="source.meal">
|
||||
<span v-else-if="source.meal && source.ingredient">
|
||||
{{ source.ingredient.line }} for
|
||||
<router-link :to="`/meals/${source.meal.id}/`">{{
|
||||
source.meal.suggestedDate
|
||||
|
|
@ -134,10 +134,13 @@ const imageSrc = computed(() => {
|
|||
|
||||
const remainingRequiredTotals = computed(() =>
|
||||
calculateTotals(
|
||||
props.shoppingListItemGroup.shoppingListItems.map((item) => ({
|
||||
quantity: item.ingredient.quantity,
|
||||
unit: String(item.ingredient.unit || 'items'),
|
||||
}))
|
||||
props.shoppingListItemGroup.shoppingListItems
|
||||
.filter((item) => !!item.ingredient)
|
||||
.map((item) => ({
|
||||
// item.ingredient is defined due to filter above
|
||||
quantity: item.ingredient!.quantity,
|
||||
unit: String(item.ingredient!.unit || 'items'),
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import * as sdk from '@/api/sdk'
|
||||
import type { MealInput, Meal } from '@/domain/types'
|
||||
|
||||
export async function getUpcomingMeals(from: Date, to: Date) {
|
||||
return sdk.getUpcomingMeals(from, to)
|
||||
}
|
||||
|
||||
export async function getMeal(id: number | string) {
|
||||
return sdk.getMeal(id)
|
||||
}
|
||||
|
||||
export async function saveMeal(meal: MealInput) {
|
||||
return sdk.saveMeal(meal)
|
||||
}
|
||||
|
||||
export async function markMealConsumed(mealId: number | string) {
|
||||
return sdk.markMealConsumed(mealId)
|
||||
}
|
||||
|
||||
export async function deleteMeal(mealId: number | string) {
|
||||
return sdk.deleteMeal(mealId)
|
||||
}
|
||||
|
||||
// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary
|
||||
export function toMealInput(meal: Meal): MealInput {
|
||||
return {
|
||||
id: meal.id,
|
||||
suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(),
|
||||
consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null,
|
||||
purchaseDate: meal.purchaseDate ? meal.purchaseDate.toISOString() : null,
|
||||
chefs: meal.chefs,
|
||||
cleanup: meal.cleanup,
|
||||
consumers: meal.consumers,
|
||||
recipes: meal.recipes.map((r) => ({
|
||||
mealId: r.mealId,
|
||||
recipeId: r.recipeId,
|
||||
servings: r.servings,
|
||||
recipe: null
|
||||
})),
|
||||
extraIngredients: meal.extraIngredients,
|
||||
}
|
||||
}
|
||||
|
||||
export function useMeals() {
|
||||
return { getUpcomingMeals, getMeal, saveMeal, markMealConsumed, deleteMeal, toMealInput }
|
||||
}
|
||||
|
|
@ -1,27 +1,15 @@
|
|||
import * as sdk from '@/api/sdk'
|
||||
import type { ShoppingListItemWithRefs, Product, Meal, Ingredient, Person, Recipe } from '@/domain/types'
|
||||
import type { ShoppingListItemWithRefs, Product, Meal } from '@/domain/types'
|
||||
|
||||
export type UIShoppingListItem = {
|
||||
id: number
|
||||
ingredient: Ingredient
|
||||
person?: Person | null
|
||||
personId: number
|
||||
recipe?: Recipe | null
|
||||
meal?: Meal | null
|
||||
ingredientId?: number | null
|
||||
listId?: number | null
|
||||
createdDate?: Date | null
|
||||
}
|
||||
|
||||
export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: UIShoppingListItem[] }
|
||||
export type GroupByName = { type: 'name'; name: string; shoppingListItems: UIShoppingListItem[] }
|
||||
export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: ShoppingListItemWithRefs[] }
|
||||
export type GroupByName = { type: 'name'; name: string; shoppingListItems: ShoppingListItemWithRefs[] }
|
||||
export type Group = GroupByProduct | GroupByName
|
||||
|
||||
export function groupsToItems(groups: Group[]): UIShoppingListItem[] {
|
||||
export function groupsToItems(groups: Group[]): ShoppingListItemWithRefs[] {
|
||||
return groups.map((g) => g.shoppingListItems).flat()
|
||||
}
|
||||
|
||||
export function uniqueMeals(shoppingListItems: UIShoppingListItem[]): Meal[] {
|
||||
export function uniqueMeals(shoppingListItems: ShoppingListItemWithRefs[]): Meal[] {
|
||||
const mealsWithDuplicates = shoppingListItems.map((item) => item.meal).filter((m): m is Meal => !!m)
|
||||
const mealsLookup: Record<string | number, Meal> = mealsWithDuplicates.reduce<Record<string | number, Meal>>(
|
||||
(acc, meal) => {
|
||||
|
|
@ -33,11 +21,11 @@ export function uniqueMeals(shoppingListItems: UIShoppingListItem[]): Meal[] {
|
|||
return Object.values(mealsLookup)
|
||||
}
|
||||
|
||||
export function itemsToGroups(shoppingListItems: UIShoppingListItem[]): Group[] {
|
||||
export function itemsToGroups(shoppingListItems: ShoppingListItemWithRefs[]): Group[] {
|
||||
const ingredients_by_product_id: Record<string | number, GroupByProduct> = {}
|
||||
const ingredients_by_name: Record<string, GroupByName> = {}
|
||||
for (const item of shoppingListItems) {
|
||||
if (item.ingredient.product) {
|
||||
if (item.ingredient?.product) {
|
||||
let group = ingredients_by_product_id[item.ingredient.product.id]
|
||||
if (!group) {
|
||||
group = ingredients_by_product_id[item.ingredient.product.id] = {
|
||||
|
|
@ -48,11 +36,12 @@ export function itemsToGroups(shoppingListItems: UIShoppingListItem[]): Group[]
|
|||
}
|
||||
group.shoppingListItems.push(item)
|
||||
} else {
|
||||
let group = ingredients_by_name[item.ingredient.name]
|
||||
const name = item.ingredient?.name ?? ''
|
||||
let group = ingredients_by_name[name]
|
||||
if (!group) {
|
||||
group = ingredients_by_name[item.ingredient.name] = {
|
||||
group = ingredients_by_name[name] = {
|
||||
type: 'name',
|
||||
name: item.ingredient.name,
|
||||
name,
|
||||
shoppingListItems: [],
|
||||
}
|
||||
}
|
||||
|
|
@ -63,34 +52,8 @@ export function itemsToGroups(shoppingListItems: UIShoppingListItem[]): Group[]
|
|||
}
|
||||
|
||||
export function useShopping() {
|
||||
// Request shapes expected by sdk.purchaseShoppingList
|
||||
type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null }
|
||||
type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null }
|
||||
const toExisting = (id: number, personId: number, ingredientId?: number | null): PurchaseExisting => ({ type: 'existing', id, personId, ingredientId: ingredientId ?? null })
|
||||
const toRefs = (personId: number, ingredientId?: number | null, recipeId?: number | null, mealId?: number | null): PurchaseRefs => ({
|
||||
type: 'refs',
|
||||
personId,
|
||||
ingredientId: ingredientId ?? null,
|
||||
recipeId: recipeId ?? null,
|
||||
mealId: mealId ?? null,
|
||||
})
|
||||
const mapItemToUI = (i: ShoppingListItemWithRefs): UIShoppingListItem => ({
|
||||
id: Number(i.id),
|
||||
ingredient: i.ingredient!,
|
||||
person: null,
|
||||
personId: i.personId,
|
||||
recipe: i.recipe ?? null,
|
||||
meal: i.meal ?? null,
|
||||
ingredientId: i.ingredient?.id ?? null,
|
||||
listId: i.listId ?? null,
|
||||
createdDate: i.createdDate,
|
||||
})
|
||||
const groupsFrom = (items?: ShoppingListItemWithRefs[]): Group[] => {
|
||||
return itemsToGroups((items ?? []).map(mapItemToUI))
|
||||
}
|
||||
const mealsFrom = (items?: ShoppingListItemWithRefs[]): Meal[] => {
|
||||
return uniqueMeals((items ?? []).map(mapItemToUI))
|
||||
}
|
||||
const groupsFrom = (items?: ShoppingListItemWithRefs[]): Group[] => itemsToGroups(items ?? [])
|
||||
const mealsFrom = (items?: ShoppingListItemWithRefs[]): Meal[] => uniqueMeals(items ?? [])
|
||||
return {
|
||||
getCurrentShoppingList: sdk.getCurrentShoppingList,
|
||||
getShoppingList: sdk.getShoppingList,
|
||||
|
|
@ -100,15 +63,21 @@ export function useShopping() {
|
|||
getMyShoppingList: sdk.getMyShoppingList,
|
||||
saveMyShoppingList: sdk.saveMyShoppingList,
|
||||
// View-model helpers
|
||||
mapItemToUI,
|
||||
groupsFrom,
|
||||
mealsFrom,
|
||||
async purchaseFromGroups(groups: Group[]) {
|
||||
const items = groupsToItems(groups).map((i) =>
|
||||
typeof i.id === 'number' && i.id >= 0
|
||||
? toExisting(i.id, i.personId, i.ingredientId)
|
||||
: toRefs(i.personId, i.ingredient?.id ?? null, i.recipe?.id ?? null, i.meal?.id ?? null)
|
||||
)
|
||||
const items = groupsToItems(groups).map((i): sdk.PurchaseRequest => {
|
||||
if (typeof i.id === 'number' && i.id >= 0) {
|
||||
return { type: 'existing', id: i.id, personId: i.personId, ingredientId: i.ingredient?.id ?? null }
|
||||
}
|
||||
return {
|
||||
type: 'refs',
|
||||
personId: i.personId,
|
||||
ingredientId: i.ingredient?.id ?? null,
|
||||
recipeId: i.recipe?.id ?? null,
|
||||
mealId: i.meal?.id ?? null,
|
||||
}
|
||||
})
|
||||
if (!items?.length) return null
|
||||
return sdk.purchaseShoppingList(items)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { RecipeOut, Recipe, MealOut, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem } from './types'
|
||||
import type { RecipeOut, Recipe, MealOut, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem, ShoppingListItemWithRefs, MealInput } from './types'
|
||||
import type { components } from '@/api/types'
|
||||
|
||||
export function toDate(value: string | Date | null | undefined): Date | null {
|
||||
|
|
@ -6,8 +6,25 @@ export function toDate(value: string | Date | null | undefined): Date | null {
|
|||
return typeof value === 'string' ? new Date(value) : value
|
||||
}
|
||||
|
||||
export function decodeRecipe(r: RecipeOut | null | undefined): Recipe | null {
|
||||
if (!r) return null
|
||||
// Small helper to decode optional lookup maps without repeating loops everywhere
|
||||
export function decodeLookup<TIn, TOut>(
|
||||
raw: Record<string, TIn> | null | undefined,
|
||||
decode: (v: TIn) => TOut
|
||||
): Record<string, TOut> | undefined {
|
||||
if (!raw) return undefined
|
||||
const out: Record<string, TOut> = {}
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(raw, key)) continue
|
||||
const maybe = raw[key]
|
||||
if (maybe === undefined) continue
|
||||
const decoded = decode(maybe)
|
||||
out[String(key)] = decoded
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function decodeRecipe(r: RecipeOut | null | undefined): Recipe {
|
||||
if (!r) throw new Error('Invalid recipe payload')
|
||||
return {
|
||||
...r,
|
||||
dateCreated: toDate(r.dateCreated),
|
||||
|
|
@ -15,15 +32,10 @@ export function decodeRecipe(r: RecipeOut | null | undefined): Recipe | null {
|
|||
}
|
||||
}
|
||||
|
||||
export function decodeRecipes(list: RecipeOut[] | null | undefined): Recipe[] {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map((r) => decodeRecipe(r)).filter((r): r is Recipe => !!r)
|
||||
}
|
||||
|
||||
export function decodeMeal(m: MealOut | null | undefined): Meal | null {
|
||||
if (!m) return null
|
||||
export function decodeMeal(m: MealOut | null | undefined): Meal {
|
||||
if (!m) throw new Error('Invalid meal payload')
|
||||
const recipes = Array.isArray(m.recipes)
|
||||
? m.recipes.map(mr => decodeMealRecipe(mr)).filter((r): r is MealRecipe => !!r)
|
||||
? m.recipes.map((mr) => decodeMealRecipe(mr))
|
||||
: []
|
||||
|
||||
return {
|
||||
|
|
@ -39,40 +51,41 @@ export function decodeMeal(m: MealOut | null | undefined): Meal | null {
|
|||
}
|
||||
}
|
||||
|
||||
export function decodeMealRecipe(mr: components['schemas']['MealRecipe-Output'] | null | undefined): MealRecipe | null {
|
||||
if (!mr) return null
|
||||
function decodeMealRecipe(mr: components['schemas']['MealRecipe-Output'] | null | undefined): MealRecipe {
|
||||
if (!mr) throw new Error('Invalid meal recipe payload')
|
||||
return {
|
||||
...mr,
|
||||
recipe: mr.recipe ? decodeRecipe(mr.recipe) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeIngredient(i: components['schemas']['Ingredient'] | null | undefined): DomainIngredient | null {
|
||||
if (!i) return null
|
||||
export function decodeIngredient(i: components['schemas']['Ingredient'] | null | undefined): DomainIngredient {
|
||||
if (!i) throw new Error('Invalid ingredient payload')
|
||||
// API guarantees quantity is a number; pass through
|
||||
return { ...i }
|
||||
}
|
||||
|
||||
export function decodeIngredients(list: components['schemas']['Ingredient'][] | null | undefined): DomainIngredient[] {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map((i) => decodeIngredient(i)).filter((x): x is DomainIngredient => !!x)
|
||||
return list.map((i) => decodeIngredient(i))
|
||||
}
|
||||
|
||||
export function decodeShoppingListItem(i: components['schemas']['ShoppingListItem'] | null | undefined): ShoppingListItem | null {
|
||||
if (!i) return null
|
||||
function decodeShoppingListItem(i: components['schemas']['ShoppingListItem'] | null | undefined): ShoppingListItem {
|
||||
if (!i) throw new Error('Invalid shopping list item payload')
|
||||
return {
|
||||
...i,
|
||||
createdDate: toDate(i.createdDate),
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeShoppingListItems(list: components['schemas']['ShoppingListItem'][] | null | undefined): ShoppingListItem[] {
|
||||
export function decodeShoppingListItems(list: components['schemas']['ShoppingListItem'][] | null | undefined): ShoppingListItemWithRefs[] {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map((i) => decodeShoppingListItem(i)).filter((x): x is ShoppingListItem => !!x)
|
||||
// Build a new array with item clones to allow optional refs to be attached later
|
||||
return list.map((raw) => ({ ...decodeShoppingListItem(raw) }))
|
||||
}
|
||||
|
||||
export function decodeShoppingList(v: components['schemas']['ShoppingList'] | null | undefined): ShoppingList | null {
|
||||
if (!v) return null
|
||||
export function decodeShoppingList(v: components['schemas']['ShoppingList'] | null | undefined): ShoppingList {
|
||||
if (!v) throw new Error('Invalid shopping list payload')
|
||||
const { items: rawItems, ...rest } = v
|
||||
const items = Array.isArray(rawItems) ? decodeShoppingListItems(rawItems) : undefined
|
||||
return {
|
||||
|
|
@ -81,3 +94,23 @@ export function decodeShoppingList(v: components['schemas']['ShoppingList'] | nu
|
|||
...(items ? { items } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary
|
||||
export function toMealInput(meal: Meal): MealInput {
|
||||
return {
|
||||
id: meal.id,
|
||||
suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(),
|
||||
consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null,
|
||||
purchaseDate: meal.purchaseDate ? meal.purchaseDate.toISOString() : null,
|
||||
chefs: meal.chefs,
|
||||
cleanup: meal.cleanup,
|
||||
consumers: meal.consumers,
|
||||
recipes: meal.recipes.map((r) => ({
|
||||
mealId: r.mealId,
|
||||
recipeId: r.recipeId,
|
||||
servings: r.servings,
|
||||
recipe: null,
|
||||
})),
|
||||
extraIngredients: meal.extraIngredients,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ import type { components } from '@/api/types'
|
|||
export type Replace<T, M> = Omit<T, keyof M> & M
|
||||
export type WithDates<T, K extends keyof T> = Replace<T, { [P in K]: Date | null }>
|
||||
|
||||
// Common helpers
|
||||
export type Maybe<T> = T | null | undefined
|
||||
export type NonNull<T> = Exclude<T, null | undefined>
|
||||
// Common helpers (intentionally minimal to avoid unused exports)
|
||||
export type Lookup<T> = Record<string, T>
|
||||
// Refs are optional and may be attached later by mappers (e.g., in sdk)
|
||||
export type WithRefs<T, Refs extends object> = T & { [K in keyof Refs]?: Refs[K] | undefined }
|
||||
|
||||
// Domain type aliases
|
||||
|
|
@ -49,7 +48,7 @@ export type ShoppingListItemWithRefs = WithRefs<ShoppingListItem, { ingredient:
|
|||
|
||||
export type ShoppingListWithRefs = Replace<ShoppingList, { items?: ShoppingListItemWithRefs[] }>
|
||||
|
||||
// Lookup maps used by shopping mappings
|
||||
// Lookup maps used by shopping mappings (all optional; mappers may omit absent ones)
|
||||
export type ShoppingLookups = {
|
||||
ingredientsLookup?: Lookup<Ingredient>
|
||||
mealsLookup?: Lookup<Meal>
|
||||
|
|
|
|||
17
src/env.d.ts
vendored
17
src/env.d.ts
vendored
|
|
@ -1,17 +1,10 @@
|
|||
/* Ambient env typing for optional Vite-style env access */
|
||||
declare interface ImportMeta {
|
||||
env?: {
|
||||
VITE_API_BASE_URL?: string
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
/* Minimal ambient typing for optional import.meta.env usage in tooling */
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
declare interface ImportMeta {
|
||||
readonly env?: ImportMetaEnv
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const UNIT_KEYS_ARRAY: readonly ['kg', 'litres', 'items'] = ['kg', 'litres', 'items']
|
||||
export type UnitKey = typeof UNIT_KEYS_ARRAY[number]
|
||||
type UnitKey = typeof UNIT_KEYS_ARRAY[number]
|
||||
|
||||
export const equivalentUnits: Record<UnitKey, Record<string, number>> = {
|
||||
const equivalentUnits: Record<UnitKey, Record<string, number>> = {
|
||||
kg: {
|
||||
kgs: 1,
|
||||
kilograms: 1,
|
||||
|
|
@ -124,8 +124,8 @@ export function getConversionFactor(unit: string): { unit: UnitKey | string; fac
|
|||
return null
|
||||
}
|
||||
|
||||
export type Quantity = { quantity: number; unit: string }
|
||||
export type Total = { unit: string; quantity: number }
|
||||
type Quantity = { quantity: number; unit: string }
|
||||
type Total = { unit: string; quantity: number }
|
||||
|
||||
export function calculateTotals(quantityList: Quantity[]): Total[] {
|
||||
const totals: Record<string, number> = {}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { afterAll, afterEach, beforeAll } from 'vitest'
|
|||
import { setupServer } from 'msw/node'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
|
||||
// Ensure global fetch is available in Node tests
|
||||
// Use Node 18+ global fetch (undici). Do not override so MSW can intercept.
|
||||
// Ensure global fetch is available in Node tests (Node 18+ provides global fetch)
|
||||
// Do not override so MSW can intercept.
|
||||
|
||||
// Some CI envs inject corporate roots causing TLS issues; disable cert checks in tests only
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
// Legacy JS test shim: the authoritative tests live in tests/useAlert.test.ts
|
||||
// Keep a trivial passing test here so Vitest doesn't fail this file collection.
|
||||
import { it, expect } from 'vitest'
|
||||
|
||||
it('noop shim (see useAlert.test.ts)', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
"allowJs": false,
|
||||
"checkJs": false,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node", "vite/client"],
|
||||
"types": ["node"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.{test,spec}.js'],
|
||||
include: ['tests/**/*.{test,spec}.{js,ts}'],
|
||||
globals: true,
|
||||
reporters: 'default',
|
||||
setupFiles: ['tests/test-setup.js'],
|
||||
|
|
|
|||
Loading…
Reference in a new issue