diff --git a/package-lock.json b/package-lock.json index a6ed31a273aee9cac16d186fcb460766fb11efe2..88d57bd03bdde17aecd6e8981798af7a0cf3879e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,14 @@ }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", + "@testing-library/user-event": "^14.5.2", + "@testing-library/vue": "^8.0.3", "@tsconfig/node20": "^20.1.4", "@types/jsdom": "^21.1.6", "@types/node": "^20.12.5", "@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vitest/coverage-v8": "^1.5.0", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", "@vue/test-utils": "^2.4.5", @@ -40,9 +43,11 @@ "prettier": "^3.2.5", "start-server-and-test": "^2.0.3", "typescript": "~5.4.0", + "user-event": "^4.0.0", "vite": "^5.2.8", "vite-plugin-vue-devtools": "^7.0.25", "vitest": "^1.5.0", + "vue-router-mock": "^1.1.0", "vue-tsc": "^2.0.11" } }, @@ -520,6 +525,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", @@ -569,6 +586,12 @@ "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 + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1237,6 +1260,15 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "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, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -1637,12 +1669,175 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/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==", + "dev": true + }, + "node_modules/@testing-library/dom/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/@testing-library/dom/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@testing-library/vue": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@testing-library/vue/-/vue-8.0.3.tgz", + "integrity": "sha512-wSsbNlZ69ZFQgVlHMtc/ZC/g9BHO7MhyDrd4nHyfEubtMr3kToN/w4/BsSBknGIF8w9UmPbsgbIuq/CbdBHzCA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "@testing-library/dom": "^9.3.3", + "@vue/test-utils": "^2.4.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@vue/compiler-sfc": ">= 3", + "vue": ">= 3" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, "node_modules/@tsconfig/node20": { "version": "20.1.4", "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", "dev": true }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2035,6 +2230,33 @@ "vue": "^3.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.0.tgz", + "integrity": "sha512-1igVwlcqw1QUMdfcMlzzY4coikSIBN944pkueGi0pawrX5I5Z+9hxdTR+w3Sg6Q3eZhvdMAs8ZaF9JuTG1uYOQ==", + "dev": true, + "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.5.0" + } + }, "node_modules/@vitest/expect": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.0.tgz", @@ -2609,6 +2831,31 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2674,6 +2921,21 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -3473,6 +3735,38 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3536,6 +3830,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3577,6 +3888,12 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -3749,6 +4066,26 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", @@ -4501,6 +4838,15 @@ } } }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -4602,6 +4948,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4774,6 +5129,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4819,6 +5183,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4858,6 +5237,12 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -5027,28 +5412,129 @@ "node": ">= 0.10" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "ci-info": "^3.2.0" + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" }, - "bin": { - "is-ci": "bin.js" + "engines": { + "node": ">= 0.4" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, - "bin": { - "is-docker": "cli.js" + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5118,6 +5604,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5127,6 +5625,21 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -5142,6 +5655,49 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -5154,6 +5710,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -5172,6 +5758,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -5187,6 +5801,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5199,6 +5819,77 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "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, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "dev": true, + "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.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -5791,6 +6482,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz", @@ -5802,6 +6502,65 @@ "node": ">=12" } }, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "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, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/map-stream": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", @@ -6074,6 +6833,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/oh-vue-icons": { "version": "1.0.0-rc3", "resolved": "https://registry.npmjs.org/oh-vue-icons/-/oh-vue-icons-1.0.0-rc3.tgz", @@ -6501,6 +7303,15 @@ "pathe": "^1.1.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -6736,6 +7547,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -7000,6 +7835,21 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7264,6 +8114,18 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-combiner": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", @@ -7406,6 +8268,62 @@ "url": "https://opencollective.com/unts" } }, + "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, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7687,6 +8605,13 @@ "requires-port": "^1.0.0" } }, + "node_modules/user-event": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/user-event/-/user-event-4.0.0.tgz", + "integrity": "sha512-M2at0vzLqzrwZNBmtPDRyd+1BaRwU9UTG7sc+MrUZmGviR/Ws8tmXxVvfRvuv7TWWIDsLqbrMvoF1sF7DW4y5w==", + "deprecated": "user-event has moved to @testing-library/user-event. Please uninstall user-event and install @testing-library/user-event instead, or use an older version of user-event. Learn more about this change here: https://github.com/testing-library/dom-testing-library/issues/260 Thanks! :)", + "dev": true + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8321,6 +9246,16 @@ "vue": "^3.2.0" } }, + "node_modules/vue-router-mock": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vue-router-mock/-/vue-router-mock-1.1.0.tgz", + "integrity": "sha512-RhKhxkiZh2zB2eRkzfcCILQQ0ZUc0tk7CE2ZC1PGJYi5GOU+2QQAGHtTCgb8V4B/OPm9ws+X5Q9SQB5vyTXxBQ==", + "dev": true, + "peerDependencies": { + "vue": "^3.2.23", + "vue-router": "^4.0.12" + } + }, "node_modules/vue-template-compiler": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", @@ -8479,6 +9414,59 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", diff --git a/package.json b/package.json index 2921e68310887cd68a8d08bdc7617da10c3597d9..107df03de864ad4cb836307fd03df0cbf981c172 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,14 @@ }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", + "@testing-library/user-event": "^14.5.2", + "@testing-library/vue": "^8.0.3", "@tsconfig/node20": "^20.1.4", "@types/jsdom": "^21.1.6", "@types/node": "^20.12.5", "@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vitest/coverage-v8": "^1.5.0", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", "@vue/test-utils": "^2.4.5", @@ -48,9 +51,11 @@ "prettier": "^3.2.5", "start-server-and-test": "^2.0.3", "typescript": "~5.4.0", + "user-event": "^4.0.0", "vite": "^5.2.8", "vite-plugin-vue-devtools": "^7.0.25", "vitest": "^1.5.0", + "vue-router-mock": "^1.1.0", "vue-tsc": "^2.0.11" } } diff --git a/src/api/index.ts b/src/api/index.ts index 952a64c78269b8003d0635a30820fdd15932e689..b63858683d0644088eaa29f307035f0f4d44ef0b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,17 +7,35 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; +export type { Account } from './models/Account'; +export type { AccountRequestDTO } from './models/AccountRequestDTO'; +export type { AccountResponseDTO } from './models/AccountResponseDTO'; export type { AuthenticationResponse } from './models/AuthenticationResponse'; +export type { BankProfile } from './models/BankProfile'; +export type { BankProfileDTO } from './models/BankProfileDTO'; +export type { BankProfileResponseDTO } from './models/BankProfileResponseDTO'; +export type { ChallengeDTO } from './models/ChallengeDTO'; +export type { ConfigurationDTO } from './models/ConfigurationDTO'; +export type { CreateGoalDTO } from './models/CreateGoalDTO'; +export type { DailyChallengeProgressDTO } from './models/DailyChallengeProgressDTO'; export type { ExceptionResponse } from './models/ExceptionResponse'; +export type { GoalDTO } from './models/GoalDTO'; export type { LeaderboardDTO } from './models/LeaderboardDTO'; export type { LeaderboardEntryDTO } from './models/LeaderboardEntryDTO'; export type { LoginRequest } from './models/LoginRequest'; +export { ParticipantDTO } from './models/ParticipantDTO'; +export type { ParticipantUserDTO } from './models/ParticipantUserDTO'; export type { PasswordResetDTO } from './models/PasswordResetDTO'; export type { ProfileDTO } from './models/ProfileDTO'; export type { SignUpRequest } from './models/SignUpRequest'; +export type { TransactionDTO } from './models/TransactionDTO'; export type { UserDTO } from './models/UserDTO'; export type { UserUpdateDTO } from './models/UserUpdateDTO'; +export { AccountControllerService } from './services/AccountControllerService'; export { AuthenticationService } from './services/AuthenticationService'; +export { BankProfileControllerService } from './services/BankProfileControllerService'; +export { GoalService } from './services/GoalService'; export { LeaderboardService } from './services/LeaderboardService'; +export { TransactionControllerService } from './services/TransactionControllerService'; export { UserService } from './services/UserService'; diff --git a/src/api/models/Account.ts b/src/api/models/Account.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb3a63221139e5c22defbd906d3017d6286435ab --- /dev/null +++ b/src/api/models/Account.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { BankProfile } from './BankProfile'; +export type Account = { + bban?: number; + balance?: number; + bankProfile?: BankProfile; +}; + diff --git a/src/api/models/AccountRequestDTO.ts b/src/api/models/AccountRequestDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2f7ddd34fb91f531ac004c517213fcd31dd2c0d --- /dev/null +++ b/src/api/models/AccountRequestDTO.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type AccountRequestDTO = { + ssn?: number; +}; + diff --git a/src/api/models/AccountResponseDTO.ts b/src/api/models/AccountResponseDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..276a2131b75b4c6dd96beb6eae76e448f0a07bb6 --- /dev/null +++ b/src/api/models/AccountResponseDTO.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type AccountResponseDTO = { + bankProfileId?: number; + balance?: number; +}; + diff --git a/src/api/models/BankProfile.ts b/src/api/models/BankProfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4fe99e6921234a056658b64a4b0ea6ec5e33f69 --- /dev/null +++ b/src/api/models/BankProfile.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Account } from './Account'; +export type BankProfile = { + id?: number; + ssn?: number; + accounts?: Array<Account>; +}; + diff --git a/src/api/models/BankProfileDTO.ts b/src/api/models/BankProfileDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..e6b1b89cb3c01a69bb0276e169f93d7e5787c5a7 --- /dev/null +++ b/src/api/models/BankProfileDTO.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type BankProfileDTO = { + ssn?: number; +}; + diff --git a/src/api/models/BankProfileResponseDTO.ts b/src/api/models/BankProfileResponseDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ef2360b3463e66b704ff1b587f7598b4efba5dd --- /dev/null +++ b/src/api/models/BankProfileResponseDTO.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Account } from './Account'; +export type BankProfileResponseDTO = { + ssn?: number; + accounts?: Array<Account>; +}; + diff --git a/src/api/models/ChallengeDTO.ts b/src/api/models/ChallengeDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5bd38d483c363d921274e9ed77dbd5e6d4aef75 --- /dev/null +++ b/src/api/models/ChallengeDTO.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { DailyChallengeProgressDTO } from './DailyChallengeProgressDTO'; +export type ChallengeDTO = { + id?: number; + potentialSavingAmount?: number; + points?: number; + days?: number; + createdAt?: string; + dailyChallengeProgressList?: Array<DailyChallengeProgressDTO>; +}; + diff --git a/src/api/models/ConfigurationDTO.ts b/src/api/models/ConfigurationDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d4b45261998b7595857854df32b24269b026d4f --- /dev/null +++ b/src/api/models/ConfigurationDTO.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ConfigurationDTO = { + commitment?: string; + experience?: string; + challengeTypes?: Array<string>; +}; + diff --git a/src/api/models/CreateGoalDTO.ts b/src/api/models/CreateGoalDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..61eb0457b42b2aebf8be3c560bcd62d2346648e4 --- /dev/null +++ b/src/api/models/CreateGoalDTO.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CreateGoalDTO = { + goalName?: string; + description?: string; + targetAmount?: number; + targetDate?: string; +}; + diff --git a/src/api/models/DailyChallengeProgressDTO.ts b/src/api/models/DailyChallengeProgressDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7bda736cefef3aebff0533272807cde205e16ee --- /dev/null +++ b/src/api/models/DailyChallengeProgressDTO.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type DailyChallengeProgressDTO = { + id?: number; + challengeDay?: number; + completedAt?: string; +}; + diff --git a/src/api/models/GoalDTO.ts b/src/api/models/GoalDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..004eb49fd0bec010a1d4227bac6ec6bbc3daea50 --- /dev/null +++ b/src/api/models/GoalDTO.ts @@ -0,0 +1,18 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ChallengeDTO } from './ChallengeDTO'; +import type { ParticipantDTO } from './ParticipantDTO'; +export type GoalDTO = { + id?: number; + goalName?: string; + description?: string; + targetAmount?: number; + targetDate?: string; + completedAt?: string; + createdAt?: string; + challenges?: Array<ChallengeDTO>; + participants?: Array<ParticipantDTO>; +}; + diff --git a/src/api/models/ParticipantDTO.ts b/src/api/models/ParticipantDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..0615b50834924e46416818dffbb00718b3607bab --- /dev/null +++ b/src/api/models/ParticipantDTO.ts @@ -0,0 +1,16 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ParticipantUserDTO } from './ParticipantUserDTO'; +export type ParticipantDTO = { + role?: ParticipantDTO.role; + user?: ParticipantUserDTO; +}; +export namespace ParticipantDTO { + export enum role { + CREATOR = 'CREATOR', + CONTRIBUTOR = 'CONTRIBUTOR', + } +} + diff --git a/src/api/models/ParticipantUserDTO.ts b/src/api/models/ParticipantUserDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..5499c09f765c2b01e58efea75581a8f14d3f9e40 --- /dev/null +++ b/src/api/models/ParticipantUserDTO.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ParticipantUserDTO = { + firstName?: string; + lastName?: string; +}; + diff --git a/src/api/models/PasswordResetDTO.ts b/src/api/models/PasswordResetDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..383b7ad1f57b7f48dc891ec0f3f2a86a28d03f29 --- /dev/null +++ b/src/api/models/PasswordResetDTO.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type PasswordResetDTO = { + token: string; + password?: string; +}; + diff --git a/src/api/models/SignUpRequest.ts b/src/api/models/SignUpRequest.ts index f9aa1f6162784c19e4012289fd61c429edf6eea7..72720c40f1f0d870e4f713aaa2215dbd3b59f914 100644 --- a/src/api/models/SignUpRequest.ts +++ b/src/api/models/SignUpRequest.ts @@ -2,13 +2,12 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { ConfigurationDTO } from './ConfigurationDTO'; export type SignUpRequest = { firstName?: string; lastName?: string; email?: string; password?: string; - commitment?: string; - experience?: string; - challengeTypes?: Array<string>; + configuration: ConfigurationDTO; }; diff --git a/src/api/models/TransactionDTO.ts b/src/api/models/TransactionDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..d974cb53a8a01cb4c99043e2cb3a983a608d295f --- /dev/null +++ b/src/api/models/TransactionDTO.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TransactionDTO = { + debtorBBAN?: number; + creditorBBAN?: number; + amount?: number; +}; + diff --git a/src/api/models/UserUpdateDTO.ts b/src/api/models/UserUpdateDTO.ts index 54cc0f5524b3ca515f3b671e02edf92ec05a54aa..00b3b0a05950d06f960971ef120d217a33202b39 100644 --- a/src/api/models/UserUpdateDTO.ts +++ b/src/api/models/UserUpdateDTO.ts @@ -2,13 +2,12 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { ConfigurationDTO } from './ConfigurationDTO'; export type UserUpdateDTO = { firstName?: string; lastName?: string; email?: string; password?: string; - commitment?: string; - experience?: string; - challengeTypes?: Array<string>; + configuration?: ConfigurationDTO; }; diff --git a/src/api/services/AccountControllerService.ts b/src/api/services/AccountControllerService.ts new file mode 100644 index 0000000000000000000000000000000000000000..181460cbe891524b1508866a63130c5546ab56ee --- /dev/null +++ b/src/api/services/AccountControllerService.ts @@ -0,0 +1,77 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Account } from '../models/Account'; +import type { AccountRequestDTO } from '../models/AccountRequestDTO'; +import type { AccountResponseDTO } from '../models/AccountResponseDTO'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class AccountControllerService { + /** + * Create account + * Create account with random balance + * @returns AccountResponseDTO Successfully created account + * @throws ApiError + */ + public static createAccount({ + requestBody, + }: { + requestBody: AccountRequestDTO, + }): CancelablePromise<AccountResponseDTO> { + return __request(OpenAPI, { + method: 'POST', + url: '/bank/v1/account/create-account', + body: requestBody, + mediaType: 'application/json', + errors: { + 404: `Provided bank profile id could not be found`, + }, + }); + } + /** + * Get user accounts + * Get accounts associated with a user by providing their social security number + * @returns Account No accounts associated with a bank user + * @throws ApiError + */ + public static getAccountsBySsn({ + ssn, + }: { + ssn: number, + }): CancelablePromise<Array<Account>> { + return __request(OpenAPI, { + method: 'GET', + url: '/bank/v1/account/accounts/ssn/{ssn}', + path: { + 'ssn': ssn, + }, + errors: { + 404: `Social security number does not exist`, + }, + }); + } + /** + * Get user accounts + * Get accounts associated with a user by providing their bank profile id + * @returns Account No accounts associated with a bank user + * @throws ApiError + */ + public static getAccounts({ + bankProfileId, + }: { + bankProfileId: number, + }): CancelablePromise<Array<Account>> { + return __request(OpenAPI, { + method: 'GET', + url: '/bank/v1/account/accounts/profile/{bankProfileId}', + path: { + 'bankProfileId': bankProfileId, + }, + errors: { + 404: `Bank profile id does not exist`, + }, + }); + } +} diff --git a/src/api/services/BankProfileControllerService.ts b/src/api/services/BankProfileControllerService.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ae9fa35124c975f752afa4228fccdb1d2bd68b3 --- /dev/null +++ b/src/api/services/BankProfileControllerService.ts @@ -0,0 +1,32 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { BankProfileDTO } from '../models/BankProfileDTO'; +import type { BankProfileResponseDTO } from '../models/BankProfileResponseDTO'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class BankProfileControllerService { + /** + * Create bank profile + * Create a bank profile by providing a social security number + * @returns BankProfileResponseDTO Successfully created a bank profile + * @throws ApiError + */ + public static createBankProfile({ + requestBody, + }: { + requestBody: BankProfileDTO, + }): CancelablePromise<BankProfileResponseDTO> { + return __request(OpenAPI, { + method: 'POST', + url: '/bank/v1/profile/create-profile', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Could not create profile`, + }, + }); + } +} diff --git a/src/api/services/GoalService.ts b/src/api/services/GoalService.ts new file mode 100644 index 0000000000000000000000000000000000000000..28ae857fcaf1e5268dc6aefc5ba9c9e0d27398f2 --- /dev/null +++ b/src/api/services/GoalService.ts @@ -0,0 +1,47 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CreateGoalDTO } from '../models/CreateGoalDTO'; +import type { GoalDTO } from '../models/GoalDTO'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class GoalService { + /** + * @returns GoalDTO OK + * @throws ApiError + */ + public static createGoal({ + requestBody, + }: { + requestBody: CreateGoalDTO, + }): CancelablePromise<GoalDTO> { + return __request(OpenAPI, { + method: 'POST', + url: '/api/goal/createGoal', + body: requestBody, + mediaType: 'application/json', + }); + } + /** + * @returns GoalDTO OK + * @throws ApiError + */ + public static getGoals(): CancelablePromise<Array<GoalDTO>> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/goal/getGoals', + }); + } + /** + * @returns GoalDTO OK + * @throws ApiError + */ + public static getGoal(): CancelablePromise<GoalDTO> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/goal/getGoal', + }); + } +} diff --git a/src/api/services/TransactionControllerService.ts b/src/api/services/TransactionControllerService.ts new file mode 100644 index 0000000000000000000000000000000000000000..f584d0ae03eb7882c73c6e0940575f0c009bd327 --- /dev/null +++ b/src/api/services/TransactionControllerService.ts @@ -0,0 +1,31 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TransactionDTO } from '../models/TransactionDTO'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class TransactionControllerService { + /** + * Transfer to account + * Transfer money from a users account to another account of the same user + * @returns TransactionDTO No accounts associated with a bank user + * @throws ApiError + */ + public static transferToSelf({ + requestBody, + }: { + requestBody: TransactionDTO, + }): CancelablePromise<TransactionDTO> { + return __request(OpenAPI, { + method: 'POST', + url: '/bank/v1/transaction/norwegian-domestic-payment-to-self', + body: requestBody, + mediaType: 'application/json', + errors: { + 404: `Bank profile id does not exist`, + }, + }); + } +} diff --git a/src/assets/icons/black_paintBrush.svg b/src/assets/icons/black_paintBrush.svg new file mode 100644 index 0000000000000000000000000000000000000000..004dc5c256ebe65d2de56f6d8c4e9bd3e94fe9e8 --- /dev/null +++ b/src/assets/icons/black_paintBrush.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-80q-33 0-56.5-23.5T360-160v-160H240q-33 0-56.5-23.5T160-400v-280q0-66 47-113t113-47h480v440q0 33-23.5 56.5T720-320H600v160q0 33-23.5 56.5T520-80h-80ZM240-560h480v-200h-40v160h-80v-160h-40v80h-80v-80H320q-33 0-56.5 23.5T240-680v120Zm0 160h480v-80H240v80Zm0 0v-80 80Z"/></svg> \ No newline at end of file diff --git a/src/assets/icons/black_person.svg b/src/assets/icons/black_person.svg new file mode 100644 index 0000000000000000000000000000000000000000..787c1a2c5f8ba200d2254734be28326996a3f678 --- /dev/null +++ b/src/assets/icons/black_person.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 -960 960 960" width="32"><path d="M480-481q-66 0-108-42t-42-108q0-66 42-108t108-42q66 0 108 42t42 108q0 66-42 108t-108 42ZM160-160v-94q0-38 19-65t49-41q67-30 128.5-45T480-420q62 0 123 15.5t127.921 44.694q31.301 14.126 50.19 40.966Q800-292 800-254v94H160Zm60-60h520v-34q0-16-9.5-30.5T707-306q-64-31-117-42.5T480-360q-57 0-111 11.5T252-306q-14 7-23 21.5t-9 30.5v34Zm260-321q39 0 64.5-25.5T570-631q0-39-25.5-64.5T480-721q-39 0-64.5 25.5T390-631q0 39 25.5 64.5T480-541Zm0-90Zm0 411Z" fill="#000"/></svg> \ No newline at end of file diff --git a/src/assets/icons/credit-card.svg b/src/assets/icons/credit-card.svg new file mode 100644 index 0000000000000000000000000000000000000000..af1c97e2b29970b7ab6c9dc5fae1c50d36e64db7 --- /dev/null +++ b/src/assets/icons/credit-card.svg @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 512 512" xml:space="preserve"> +<g> + <g> + <g> + <path d="M76.8,358.4h153.6c4.71,0,8.533-3.823,8.533-8.533s-3.823-8.533-8.533-8.533H76.8c-4.71,0-8.533,3.823-8.533,8.533 + S72.09,358.4,76.8,358.4z"/> + <path d="M503.467,221.867h-460.8c-4.71,0-8.533,3.823-8.533,8.533c0,4.71,3.823,8.533,8.533,8.533h452.267v162.133 + c0,14.114-11.486,25.6-25.6,25.6H42.667c-11.87,0-25.6-8.755-25.6-25.6V213.333c0-4.71-3.823-8.533-8.533-8.533 + S0,208.623,0,213.333v187.733c0,20.506,16.614,42.667,42.667,42.667h426.667c23.526,0,42.667-19.14,42.667-42.667V230.4 + C512,225.69,508.177,221.867,503.467,221.867z"/> + <path d="M469.333,68.267H42.667C19.14,68.267,0,87.407,0,110.933v51.2c0,4.71,3.823,8.533,8.533,8.533h460.8 + c4.71,0,8.533-3.823,8.533-8.533c0-4.71-3.823-8.533-8.533-8.533H17.067v-42.667c0-14.114,11.486-25.6,25.6-25.6h426.667 + c14.114,0,25.6,11.486,25.6,25.6v85.333c0,4.71,3.823,8.533,8.533,8.533s8.533-3.823,8.533-8.533v-85.333 + C512,87.407,492.86,68.267,469.333,68.267z"/> + <path d="M298.667,324.267c4.71,0,8.533-3.823,8.533-8.533s-3.823-8.533-8.533-8.533H230.4c-4.71,0-8.533,3.823-8.533,8.533 + s3.823,8.533,8.533,8.533H298.667z"/> + <path d="M76.8,324.267h119.467c4.71,0,8.533-3.823,8.533-8.533s-3.823-8.533-8.533-8.533H76.8c-4.71,0-8.533,3.823-8.533,8.533 + S72.09,324.267,76.8,324.267z"/> + </g> + </g> +</g> +</svg> \ No newline at end of file diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 0000000000000000000000000000000000000000..00ee08bbec0c0421ce472cf666530061a2f3501f --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/edit-button.svg b/src/assets/icons/edit-button.svg new file mode 100644 index 0000000000000000000000000000000000000000..877f06fa63241e2343a3f38b3c7572c389b21648 --- /dev/null +++ b/src/assets/icons/edit-button.svg @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg fill="#FFFFFF" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + width="20px" height="20px" viewBox="0 0 494.936 494.936" + xml:space="preserve"> +<g> + <g> + <path d="M389.844,182.85c-6.743,0-12.21,5.467-12.21,12.21v222.968c0,23.562-19.174,42.735-42.736,42.735H67.157 + c-23.562,0-42.736-19.174-42.736-42.735V150.285c0-23.562,19.174-42.735,42.736-42.735h267.741c6.743,0,12.21-5.467,12.21-12.21 + s-5.467-12.21-12.21-12.21H67.157C30.126,83.13,0,113.255,0,150.285v267.743c0,37.029,30.126,67.155,67.157,67.155h267.741 + c37.03,0,67.156-30.126,67.156-67.155V195.061C402.054,188.318,396.587,182.85,389.844,182.85z"/> + <path d="M483.876,20.791c-14.72-14.72-38.669-14.714-53.377,0L221.352,229.944c-0.28,0.28-3.434,3.559-4.251,5.396l-28.963,65.069 + c-2.057,4.619-1.056,10.027,2.521,13.6c2.337,2.336,5.461,3.576,8.639,3.576c1.675,0,3.362-0.346,4.96-1.057l65.07-28.963 + c1.83-0.815,5.114-3.97,5.396-4.25L483.876,74.169c7.131-7.131,11.06-16.61,11.06-26.692 + C494.936,37.396,491.007,27.915,483.876,20.791z M466.61,56.897L257.457,266.05c-0.035,0.036-0.055,0.078-0.089,0.107 + l-33.989,15.131L238.51,247.3c0.03-0.036,0.071-0.055,0.107-0.09L447.765,38.058c5.038-5.039,13.819-5.033,18.846,0.005 + c2.518,2.51,3.905,5.855,3.905,9.414C470.516,51.036,469.127,54.38,466.61,56.897z"/> + </g> +</g> +</svg> \ No newline at end of file diff --git a/src/assets/icons/import.svg b/src/assets/icons/import.svg new file mode 100644 index 0000000000000000000000000000000000000000..e832f47a76ed58d701ffe52126dc5245b3f3a877 --- /dev/null +++ b/src/assets/icons/import.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#FFF" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 512 512" xml:space="preserve"> +<g> + <g> + <path d="M435.2,153.6H320v25.6h102.4v307.2H89.6V179.2H192v-25.6H76.8c-7.066,0-12.8,5.734-12.8,12.8v332.8 + c0,7.066,5.734,12.8,12.8,12.8h358.4c7.066,0,12.8-5.734,12.8-12.8V166.4C448,159.334,442.266,153.6,435.2,153.6z"/> + </g> +</g> +<g> + <g> + <path d="M341.956,234.249c-4.941-5.052-13.056-5.146-18.099-0.205L268.8,287.898V12.8C268.8,5.734,263.066,0,256,0 + c-7.066,0-12.8,5.734-12.791,12.8v275.089l-55.057-53.854c-5.043-4.941-13.158-4.847-18.099,0.205 + c-4.941,5.06-4.855,13.158,0.205,18.099l76.8,75.128c5.043,4.949,13.158,4.855,18.099-0.188l76.595-74.931 + C346.803,247.407,346.897,239.309,341.956,234.249z"/> + </g> +</g> +</svg> \ No newline at end of file diff --git a/src/assets/icons/money.svg b/src/assets/icons/money.svg new file mode 100644 index 0000000000000000000000000000000000000000..05171388e0748405562a53b57dfc7b099a46de52 --- /dev/null +++ b/src/assets/icons/money.svg @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 487.4 487.4" xml:space="preserve"> +<g> + <path d="M103.4,196.55c13.4,0,24.3,10.9,24.3,24.3c0,13.4-10.9,24.3-24.3,24.3s-24.3-10.9-24.3-24.3 + C79.1,207.45,90,196.55,103.4,196.55z M463.4,329.25H350.9c-5.2,0-9.5,4.2-9.5,9.5v15.2c0,5.2,4.2,9.5,9.5,9.5h112.5 + c5.2,0,9.5-4.2,9.5-9.5v-15.2C472.9,333.55,468.6,329.25,463.4,329.25z M447.3,375.75H334.8c-5.2,0-9.5,4.2-9.5,9.5v15.2 + c0,5.2,4.2,9.5,9.5,9.5h112.5c5.2,0,9.5-4.2,9.5-9.5v-15.2C456.7,379.95,452.5,375.75,447.3,375.75z M477.9,236.35H365.4 + c-5.2,0-9.5,4.2-9.5,9.5v15.2c0,5.2,4.2,9.5,9.5,9.5h112.5c5.2,0,9.5-4.2,9.5-9.5v-15.2C487.4,240.65,483.1,236.35,477.9,236.35z + M325.3,292.25v15.2c0,5.2,4.2,9.5,9.5,9.5h112.5c5.2,0,9.5-4.2,9.5-9.5v-15.2c0-5.2-4.2-9.5-9.5-9.5H334.8 + C329.6,282.85,325.3,287.05,325.3,292.25z M469.2,224.05c5.2,0,9.5-4.2,9.5-9.5v-15.2c0-5.2-4.2-9.5-9.5-9.5H356.8 + c-5.2,0-9.5,4.2-9.5,9.5v15.2c0,5.2,4.2,9.5,9.5,9.5H469.2z M330.5,355.75v-18.8c0-4.2-3.4-7.7-7.7-7.7h-45.7h-196 + c1.1-6,1-12.5-1.1-19.2c-3.3-10.7-11.2-19.5-21.2-24.5c-8.7-4.4-17-5-24.6-3.6v-122.9c9.1,1.6,19.2,0.5,29.8-6.7 + c6.4-4.3,11.6-10.4,14.6-17.6c3.4-8.1,3.8-15.9,2.5-23.1h297.2c-1.5,6-1.7,12.5-0.1,19.2c3.5,15,15.6,26.9,30.7,30.2 + c6.6,1.4,12.9,1.2,18.7-0.3v9.5c0,4.3,3.5,7.8,7.8,7.8H454c4.3,0,7.8-3.5,7.8-7.8v-52.8c0-22.1-17.9-40-40-40H40 + c-22.1,0-40,17.9-40,40v205.8c0,22.1,17.9,40,40,40h237.1h45.7l0,0C327.1,363.45,330.5,360.05,330.5,355.75z M230.8,132.95 + c48.3,0,87.6,39.3,87.6,87.6s-39.3,87.6-87.6,87.6s-87.6-39.3-87.6-87.6S182.5,132.95,230.8,132.95z M258.7,183.95l-39.2,39 + l-16.4-16.5l-17.1,17l16.4,16.5l17,17.1l17.1-17l39.2-39L258.7,183.95z"/> +</g> +</svg> \ No newline at end of file diff --git a/src/assets/icons/money2.svg b/src/assets/icons/money2.svg new file mode 100644 index 0000000000000000000000000000000000000000..1c856f799672d7c4e7dab518d1ff2030243e6ac1 --- /dev/null +++ b/src/assets/icons/money2.svg @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 492.308 492.308" xml:space="preserve"> +<g> + <g> + <path d="M459.649,85.072H32.659C14.649,85.072,0,99.716,0,117.726v256.846c0,18.01,14.649,32.663,32.659,32.663h426.99 + c18.01,0,32.659-14.654,32.659-32.663V117.726C492.308,99.716,477.659,85.072,459.649,85.072z M472.615,374.572 + c0,7.154-5.817,12.971-12.966,12.971H32.659c-7.149,0-12.966-5.817-12.966-12.971V117.726c0-7.144,5.817-12.962,12.966-12.962 + h426.99c7.149,0,12.966,5.817,12.966,12.962V374.572z"/> + </g> +</g> +<g> + <g> + <path d="M413.538,186.649c-12.577,0-22.813-10.231-22.813-22.808v-9.846H101.582v9.846c0,12.577-10.236,22.808-22.813,22.808 + h-9.846v119h9.846c12.577,0,22.813,10.24,22.813,22.817v9.846h289.144v-9.846c0-12.577,10.236-22.817,22.813-22.817h9.846v-119 + H413.538z M403.692,287.111c-15.543,3.702-27.808,15.962-31.51,31.51H120.125c-3.702-15.548-15.966-27.808-31.51-31.51v-81.923 + c15.543-3.702,27.808-15.962,31.51-31.5h252.058c3.702,15.539,15.966,27.798,31.51,31.5V287.111z"/> + </g> +</g> +<g> + <g> + <path d="M246.154,204.341c-23.053,0-41.808,18.76-41.808,41.808c0,23.048,18.755,41.808,41.808,41.808 + c23.053,0,41.808-18.76,41.808-41.808C287.962,223.101,269.207,204.341,246.154,204.341z M246.154,268.264 + c-12.192,0-22.115-9.923-22.115-22.115c0-12.192,9.923-22.115,22.115-22.115s22.115,9.923,22.115,22.115 + C268.269,258.341,258.346,268.264,246.154,268.264z"/> + </g> +</g> +</svg> \ No newline at end of file diff --git a/src/assets/icons/paintBrush.svg b/src/assets/icons/paintBrush.svg new file mode 100644 index 0000000000000000000000000000000000000000..9bc0b0da47075b208009c939b52b2b595576dfc0 --- /dev/null +++ b/src/assets/icons/paintBrush.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-80q-33 0-56.5-23.5T360-160v-160H240q-33 0-56.5-23.5T160-400v-280q0-66 47-113t113-47h480v440q0 33-23.5 56.5T720-320H600v160q0 33-23.5 56.5T520-80h-80ZM240-560h480v-200h-40v160h-80v-160h-40v80h-80v-80H320q-33 0-56.5 23.5T240-680v120Zm0 160h480v-80H240v80Zm0 0v-80 80Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/scale.svg b/src/assets/icons/scale.svg new file mode 100644 index 0000000000000000000000000000000000000000..6e38442f3117cfa626f44a9a9350f97b1934bee3 --- /dev/null +++ b/src/assets/icons/scale.svg @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve"> +<path id="Scales_2_" d="M63.8251495,32.9945984c-0.1864853-0.2723999-0.4950981-0.4354973-0.8251991-0.4354973h-1.8331985 + L52.4423523,16.8237c-0.0451012-0.0813007-0.1013031-0.1539993-0.1653023-0.2182007 + c-0.0056-0.0056-0.0128975-0.0091-0.0187988-0.0146999c-0.0770988-0.0739994-0.1661987-0.1308002-0.2619019-0.1766987 + c-0.0278969-0.0132999-0.0550003-0.0247002-0.0842972-0.0354004c-0.1096001-0.0408001-0.224102-0.0696011-0.3446999-0.0696011 + h-0.0049019h-18.5625v-4.9833994c0-0.5528002-0.4472008-1-1-1c-0.5526981,0-1,0.4471998-1,1v4.9833994H12.4335518 + c-0.0018892,0-0.0028887,0-0.0048885,0c-0.1191111,0.0006008-0.232111,0.0293007-0.3405113,0.0696011 + c-0.0300999,0.0110989-0.0573997,0.0231991-0.0857,0.0370998c-0.0906,0.0436993-0.1743002,0.0981998-0.2483006,0.1672001 + c-0.0102997,0.0095997-0.0226994,0.0160999-0.0326996,0.0261002c-0.0651999,0.0662003-0.1223001,0.1406994-0.1677885,0.2243996 + L3.043452,32.5591011H0.9999521c-0.33,0-0.6386,0.1630974-0.8252,0.4354973c-0.1865,0.2725029-0.2265,0.6192017-0.1064,0.9267998 + c1.2704999,3.2685013,6.3554997,5.5508003,12.3652,5.5508003c6.0077991,0,11.0928001-2.282299,12.3633003-5.5508003 + c0.1201-0.3075981,0.0800991-0.6542969-0.1064014-0.9267998c-0.1865997-0.2723999-0.4951992-0.4354973-0.8251991-0.4354973 + h-1.8337994l-7.8999004-14.2500019h16.8683987v29.3094997h-5.9628983c-0.1767006,0-0.3505001,0.0469017-0.5029011,0.1357994 + l-6.9638996,4.0557022c-0.3905888,0.2275009-0.5799999,0.6884003-0.4627991,1.125 + c0.1180992,0.4365005,0.5135994,0.7392006,0.9657993,0.7392006h27.8514996c0.452198,0,0.8476982-0.3027,0.9659119-0.7392006 + c0.1170883-0.4365997-0.0723114-0.8974991-0.4629135-1.125l-6.9618988-4.0557022 + c-0.152401-0.0888977-0.3261871-0.1357994-0.5029984-0.1357994h-5.9629021V18.3090992h16.8889999l-7.7117004,14.2500019h-2.0424995 + c-0.3300858,0-0.6386986,0.1630974-0.8251991,0.4354973c-0.1865005,0.2725029-0.2266006,0.6192017-0.1063995,0.9267998 + c1.2705116,3.2685013,6.3554001,5.5508003,12.3642006,5.5508003s11.0937996-2.282299,12.3643112-5.5508003 + C64.0517502,33.6138,64.0117493,33.2671013,63.8251495,32.9945984z M12.4335518,37.4721985 + c-4.0448885,0-7.7528892-1.184597-9.5478001-2.9130974h19.0937004 + C20.1845512,36.2876015,16.4775524,37.4721985,12.4335518,37.4721985z M5.318152,32.5591011l7.1262002-13.1690006 + l7.3003111,13.1690006H5.318152z M42.2216644,51.6743011h-20.444313l3.5293121-2.0557022h13.3856888L42.2216644,51.6743011z + M51.5781517,19.3901005l7.3005104,13.1690006H44.4518509L51.5781517,19.3901005z M51.5673523,37.4721985 + c-4.0449028,0-7.7518997-1.184597-9.5469017-2.9130974h19.0937996 + C59.3193512,36.2876015,55.6122513,37.4721985,51.5673523,37.4721985z"/> +</svg> \ No newline at end of file diff --git a/src/assets/icons/trash-can.svg b/src/assets/icons/trash-can.svg new file mode 100644 index 0000000000000000000000000000000000000000..2206f7cf70ee47cb883112fa37d704a09df1463a --- /dev/null +++ b/src/assets/icons/trash-can.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg fill="#FFFFFF" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + width="20px" height="20px" viewBox="0 0 408.483 408.483" + xml:space="preserve"> +<g> + <g> + <path d="M87.748,388.784c0.461,11.01,9.521,19.699,20.539,19.699h191.911c11.018,0,20.078-8.689,20.539-19.699l13.705-289.316 + H74.043L87.748,388.784z M247.655,171.329c0-4.61,3.738-8.349,8.35-8.349h13.355c4.609,0,8.35,3.738,8.35,8.349v165.293 + c0,4.611-3.738,8.349-8.35,8.349h-13.355c-4.61,0-8.35-3.736-8.35-8.349V171.329z M189.216,171.329 + c0-4.61,3.738-8.349,8.349-8.349h13.355c4.609,0,8.349,3.738,8.349,8.349v165.293c0,4.611-3.737,8.349-8.349,8.349h-13.355 + c-4.61,0-8.349-3.736-8.349-8.349V171.329L189.216,171.329z M130.775,171.329c0-4.61,3.738-8.349,8.349-8.349h13.356 + c4.61,0,8.349,3.738,8.349,8.349v165.293c0,4.611-3.738,8.349-8.349,8.349h-13.356c-4.61,0-8.349-3.736-8.349-8.349V171.329z"/> + <path d="M343.567,21.043h-88.535V4.305c0-2.377-1.927-4.305-4.305-4.305h-92.971c-2.377,0-4.304,1.928-4.304,4.305v16.737H64.916 + c-7.125,0-12.9,5.776-12.9,12.901V74.47h304.451V33.944C356.467,26.819,350.692,21.043,343.567,21.043z"/> + </g> +</g> +</svg> \ No newline at end of file diff --git a/src/components/BaseComponents/Menu.vue b/src/components/BaseComponents/Menu.vue index c98a41c225e89cb0df4ddc69d378d9c74b75c197..19451c8205366d367b81b2c926b2270789a362ce 100644 --- a/src/components/BaseComponents/Menu.vue +++ b/src/components/BaseComponents/Menu.vue @@ -1,7 +1,7 @@ <template> <nav id="navBar" class="navbar navbar-expand-xl"> <div class="container-fluid"> - <a class="navbar-brand" href="/" @click="toHome"> + <a class="navbar-brand" href="/" @click="toHome" id="home"> <img id="logoImg" src="/src/assets/Sparesti-logo.png" alt="Sparesti-logo" width="60"> <span id="logo" class="text-white">Sparesti</span> </a> @@ -36,6 +36,8 @@ <ul class="dropdown-menu dropdown-username-content"> <li><a class="dropdown-item text-white dropdown-username-link" href="#" @click="toUserProfile"><img src="@/assets/icons/person.svg">User Profile</a></li> + <li><a class="dropdown-item text-white dropdown-username-link" href="#" + @click="toBudget">Budget</a></li> <li><a class="dropdown-item text-white dropdown-username-link" href="#" @click="toFriends"><img src="@/assets/icons/friends.svg">Friends</a></li> <li><a class="dropdown-item text-white dropdown-username-link" href="#" @@ -45,7 +47,7 @@ <li><a class="dropdown-item text-white dropdown-username-link" href="#" @click="toSetting"><img src="@/assets/icons/admin.svg">Admin table</a></li> <li><a class="dropdown-item text-white dropdown-username-link" href="#" - @click="toLogout"><img src="@/assets/icons/logout.svg">Log out</a></li> + @click="toLogout" data-testid="logout"><img src="@/assets/icons/logout.svg">Log out</a></li> </ul> </li> <li v-else class="nav-item"> @@ -68,6 +70,10 @@ function toHome() { router.push('/') } +function toBudget() { + router.push('/budget-overview') +} + function toSavingGoals() { router.push('/roadmap') } @@ -102,7 +108,7 @@ function toUserProfile() { function toLogout() { userStore.clearUserInfo(); - router.push('/login') + router.push('login') } diff --git a/src/components/BaseComponents/__tests__/Footer.spec.ts b/src/components/BaseComponents/__tests__/Footer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..25968d67433031c9353e3aaab288b1032fc8085e --- /dev/null +++ b/src/components/BaseComponents/__tests__/Footer.spec.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import FooterComponent from '@/components/BaseComponents/Footer.vue' + +describe('FooterComponent', () => { + it('renders properly and includes the correct copyright notice', () => { + const wrapper = mount(FooterComponent) + const footer = wrapper.find('#footer') + expect(footer.exists()).toBe(true) + expect(footer.text()).toContain('© 2024 Copyright: Anders Høvik, Andreas Svendsrud, Henrik Dybdal, Henrik Sandok, Jens Aanestad, Victor Kaste, Viktor Grevskott') + }) +}) diff --git a/src/components/BaseComponents/__tests__/Menu.spec.ts b/src/components/BaseComponents/__tests__/Menu.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e467002cfc35f0fd4f5a127a50b8f4d4bc357b7e --- /dev/null +++ b/src/components/BaseComponents/__tests__/Menu.spec.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createRouter, createMemoryHistory } from 'vue-router'; +import { createPinia, setActivePinia } from 'pinia'; +import { useUserInfoStore } from '@/stores/UserStore'; +import MyComponent from '@/components/BaseComponents/Menu.vue'; // Adjust path as needed +import router from '@/router/index'; // Adjust path as needed +import { access } from 'fs'; +import { render, screen } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; + +describe('Menu and Router Tests', () => { + let store, mockRouter; + + beforeEach(() => { + // Create a fresh Pinia and Router instance before each test + setActivePinia(createPinia()); + store = useUserInfoStore(); + mockRouter = createRouter({ + history: createMemoryHistory(), + routes: router.getRoutes(), + }); + router.beforeEach((to, from, next) => { + const isAuthenticated = store.accessToken; + if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) { + next({ name: 'login' }); + } else { + next(); + } + }); + + }); + + describe('Component Rendering', () => { + it('renders Menu correctly with data from the store', () => { + store.setUserInfo({ firstname: 'Jane', lastname: 'Doe', accessToken: 'thisIsATestToken' }); + + const wrapper = mount(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + expect(wrapper.text()).toContain('Jane'); + }); + }); + + describe('Navigation Guards', () => { + it('redirects an unauthenticated user to login when accessing a protected route', async () => { + store.$patch({ accessToken: '' }); + + router.push('/profile'); + await router.isReady(); + + expect(router.currentRoute.value.name).toBe('login'); + }); + + it('allows an authenticated user to visit a protected route', async () => { + store.$patch({ accessToken: 'valid-token' }); + + mockRouter.push('/profile'); + + await mockRouter.isReady(); + + expect(mockRouter.currentRoute.value.name).toBe('profile'); + }); + }); + + + describe('UserStore Actions', () => { + it('updates user information correctly', () => { + store.setUserInfo({ firstname: 'John', lastname: 'Smith' }); + + expect(store.firstname).toBe('John'); + expect(store.lastname).toBe('Smith'); + }); + + it('clears user information correctly', () => { + store.setUserInfo({ firstname: 'John', lastname: 'Smith', accessToken: 'thisIsATestToken'}); + store.clearUserInfo(); + + expect(store.firstname).toBe(''); + expect(store.lastname).toBe(''); + expect(store.accessToken).toBe(''); + }); + }); + + describe('Menu Actions', () => { + it('logout clears userstore', async () => { + store.setUserInfo({ firstname: 'John', lastname: 'Smith', accessToken: 'thisIsATestToken'}); + + render(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + await userEvent.click(screen.getByTestId('logout')); + + expect(store.firstname).toBe(''); + expect(store.lastname).toBe(''); + expect(store.accessToken).toBe(''); + }); + + it('home redirects to home', async () => { + store.setUserInfo({ firstname: 'John', lastname: 'Smith', accessToken: 'thisIsATestToken'}); + + const { container } = render(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + // Assuming there's an element with id="home-link" that you want to click + const homeLink = container.querySelector('#home'); // Use the actual ID here + if (homeLink) { + await userEvent.click(homeLink); + await mockRouter.isReady(); + } + + expect(mockRouter.currentRoute.value.name).toBe('home'); // Assuming 'Home' is the route name for '/' + }); + }); +}); diff --git a/src/components/Budget/BudgetBox.vue b/src/components/Budget/BudgetBox.vue new file mode 100644 index 0000000000000000000000000000000000000000..d3b98521c14bb9b6197759d70201d96cd4f2aac5 --- /dev/null +++ b/src/components/Budget/BudgetBox.vue @@ -0,0 +1,141 @@ +<script setup lang="ts"> +import { onMounted, ref } from 'vue' +import { useRouter } from 'vue-router' + +const router = useRouter(); +const props = defineProps({ + title: { + type: String, + default: '' + }, + budget: { + type: Number, + default: 0 + }, + expenses: { + type: Number, + default: 0 + } +}) + +// Calculated balance variable +let balance = props.budget - props.expenses +// Reactive variable for determining background color +const iRef = ref(null) + +/** + * Checks if the balance is positive, and depending on the value + * changes background color to green (positive) or red (negative) + */ +onMounted(() => { + if (balance >= 0) { + // By default, the background is set to red + iRef.value.style.backgroundColor = 'rgba(34, 231, 50, 0.43)'; + } +}) + +// TODO consider store chosen budget in a pinia store +/** + * Navigates to the pressed budget + */ +const onBudgetContainerPressed = () => { + router.push('/budget') +} + +</script> + +<template> + <div class="container-fluid row" @click="onBudgetContainerPressed"> + <div class="col-12"> + <div class="title-container"> + <h2>{{title}}</h2> + </div> + </div> + + <div class="col-4 budget"> + <i> + <img src="../../assets/icons/money2.svg" width="48px" height="48px"> + </i> + <div class="budget-container"> + <h5>{{budget}} kr</h5> + <p>Budget</p> + </div> + </div> + + <div class="col-4 expenses"> + <i> + <img src="../../assets/icons/credit-card.svg" width="48px" height="48px"> + </i> + <div class="expenses-container"> + <h5>{{expenses}} kr</h5> + <p>Expenses</p> + </div> + </div> + + <div class="col-4 balance"> + <i ref="iRef"> + <img src="../../assets/icons/scale.svg" width="48px" height="48px"> + </i> + <div class="balance-container"> + <h5>{{balance}} kr</h5> + <p>Balance</p> + </div> + </div> + </div> +</template> + +<style scoped> + +.title-container, .budget-container, .expenses-container, .balance-container { + display: grid; + align-self: center; +} + +.container-fluid { + border: 4px solid #5959ea; + min-height: 90px; + border-radius: 15px; + transition: transform 150ms ease-in-out, border 200ms ease-in-out; + cursor: pointer; +} + +.container-fluid:hover { + border: 4px solid #0000f1; + transform: scale(1.03); +} + +h2, h5, p { + color: black; + align-self: center; +} + +i { + display: grid; + justify-content: center; + align-content: center; + margin: 5px; + border-radius: 7px; +} + + +.budget i { + background-color: rgba(78, 107, 239, 0.43); +} + +.expenses i { + background-color: rgba(238, 191, 43, 0.43); +} + +.balance i { + background-color: rgba(232, 14, 14, 0.43); +} + +div.col-4 { + display: grid; + grid-template-columns: 1fr 1fr; + border-radius: 10px; + margin: 10px 0; +} + + +</style> \ No newline at end of file diff --git a/src/components/Budget/ExpenseBox.vue b/src/components/Budget/ExpenseBox.vue new file mode 100644 index 0000000000000000000000000000000000000000..7cdcde51e5b7f1a53aeebe0ba1f3221d4e12ae46 --- /dev/null +++ b/src/components/Budget/ExpenseBox.vue @@ -0,0 +1,94 @@ +<script setup lang="ts"> +import Button1 from '@/components/Buttons/Button1.vue' +import { type CreateAppFunction, ref } from 'vue' + +const emit = defineEmits(['deleteEvent', 'editEvent']); +const props = defineProps({ + index: { + type: Number, + default: 0 + }, + description: { + type: String, + default: '' + }, + amount: { + type: Number, + default: 0 + } +}) + +// Reactive variables for expense description and amount +let editDescription = ref('') +let editAmount = ref('') + +/** + * Emits an event to parent component with the type 'deleteEvent' to signalize + * that an expense with index 'index' must be removed. + */ +const emitDeleteEvent = () => { + emit('deleteEvent', props.index) +} + +/** + * Emits an event to parent component with the type 'editEvent' to signalize + * that an expense with index 'index' is to be edited with the values 'editDescription' + * and 'editAmount' + */ +const emitEditEvent = () => { + emit('editEvent', props.index, editDescription.value, editAmount.value) +} +</script> + +<template> + <div class="expense-container"> + <p>{{index + 1}}</p> + <p>{{description}}</p> + <p>{{amount}} kr</p> + <button class="btn btn-success" data-bs-toggle="collapse" :data-bs-target="'#' + index" aria-expanded="false" aria-controls="editBudgetCollapse"> + <img src="../../assets/icons/edit-button.svg" alt="Edit" height="18" width="18"> + Edit + </button> + <button class="btn btn-danger" @click="emitDeleteEvent"> + <img src="../../assets/icons/trash-can.svg" alt="Edit" height="18" width="18"> + Delete + </button> + </div> + + <div class="collapse" :id="index"> + <div class="container collapse-container"> + <form @submit.prevent="emitEditEvent"> + <div class="input-group"> + <span class="input-group-text">Edit expense #{{ index+1 }}: </span> + <input type="text" class="form-control" placeholder="Expense description" required v-model="editDescription"> + <input type="number" min="0" class="form-control" placeholder="Amount (kr)" required v-model="editAmount"> + <button type="submit" class="btn btn-primary" data-bs-toggle="collapse" :data-bs-target="'#' + index">Confirm</button> + </div> + </form> + </div> + </div> +</template> + +<style scoped> +.expense-container { + padding: 0 10px; + display: grid; + grid-template-columns: 1fr 1fr 1fr .6fr .6fr; + border-radius: 10px; + background-color: #2a2a34; + align-content: center; + justify-self: center; + margin: 10px 5px; +} + +.expense-container p { + color: white; + align-self: center; + margin: 0; +} + +.expense-container button { + margin: 5px; + padding: 0; +} +</style> \ No newline at end of file diff --git a/src/components/Buttons/__tests__/Button1.spec.ts b/src/components/Buttons/__tests__/Button1.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1c2a39182663c7d37365f9973ff4953fe8d1768 --- /dev/null +++ b/src/components/Buttons/__tests__/Button1.spec.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ButtonComponent from '@/components/Buttons/Button1.vue' + +describe('ButtonComponent', () => { + it('displays the passed buttonText prop', () => { + const buttonText = 'Click Me!' + const wrapper = mount(ButtonComponent, { + props: { + buttonText + } + }) + + const button = wrapper.find('#buttonStyle') + expect(button.exists()).toBe(true) + expect(button.text()).toBe(buttonText) + }) +}) diff --git a/src/components/Buttons/__tests__/ShopButton.spec.ts b/src/components/Buttons/__tests__/ShopButton.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..25858cb46e529322a5da1386f31aab636bff64f9 --- /dev/null +++ b/src/components/Buttons/__tests__/ShopButton.spec.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ImageButtonComponent from '@/components/Buttons/ShopButton.vue' + +describe('ImageButtonComponent', () => { + it('renders the button with the correct text and image', () => { + const buttonText = 'Add Coin' + const wrapper = mount(ImageButtonComponent, { + props: { + buttonText + }, + global: { + stubs: { + // This stubs out all <router-link> and <router-view> components used in the app. + 'RouterLink': true, + 'RouterView': true + } + } + }) + + const button = wrapper.find('#buttonStyle') + expect(button.exists()).toBe(true) + expect(button.text()).toContain('+Add Coin') + const image = button.find('img') + expect(image.exists()).toBe(true) + expect(image.attributes('src')).toBe('/src/assets/items/pigcoin.png') + }) +}) diff --git a/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue b/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue index 2401b49d3f1452d28f4d3aa91732be573c9279b7..a81fa4199e19bbfbea94e18f8b224b73e7ef947e 100644 --- a/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue +++ b/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue @@ -37,6 +37,7 @@ const onChangedChallengeEvent = (value) => { else { chosenChallenges.value = chosenChallenges.value.filter(item => item !== value[0]); } + console.log(chosenChallenges.value) } /** @@ -63,13 +64,15 @@ const onClick = async () => { */ const signUpPayLoad = { - "commitment": useConfigurationStore().getCommitment, - "experience": useConfigurationStore().getExperience, - "challenges": useConfigurationStore().getChallenges, "firstName": useUserInfoStore().getFirstName, "lastName": useUserInfoStore().getLastname, "email": useUserInfoStore().getEmail, "password": useUserInfoStore().getPassword, + "configuration": { + "commitment": useConfigurationStore().getCommitment, + "experience": useConfigurationStore().getExperience, + "challenges": useConfigurationStore().getChallenges + } }; let response = await AuthenticationService.signup({ requestBody: signUpPayLoad }); diff --git a/src/components/InputFields/BaseInput.vue b/src/components/InputFields/BaseInput.vue index 8e20e43d95e8ac8cf46e6253d1ef39c4ea52d351..19a7c2e0188220bde30f3c5bdd60fc9e5ba3a931 100644 --- a/src/components/InputFields/BaseInput.vue +++ b/src/components/InputFields/BaseInput.vue @@ -63,7 +63,7 @@ const onInputEvent = (event: any) => { :required="required" /> <div class="valid-feedback">{{ validMessage }}</div> - <div class="invalid-feedback">{{ invalidMessage }}</div> + <div class="invalid-feedback" id="invalid">{{ invalidMessage }}</div> </div> </template> diff --git a/src/components/InputFields/__tests__/BaseInput.spec.ts b/src/components/InputFields/__tests__/BaseInput.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..90f881ef89744bd7c300e04ddc9ed3bf19bc64f8 --- /dev/null +++ b/src/components/InputFields/__tests__/BaseInput.spec.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils'; +import InputField from '@/components/InputFields/BaseInput.vue'; + +describe('InputField.vue', () => { + it('emits inputChangeEvent when input event is triggered', async () => { + const wrapper = mount(InputField, { + props: { + label: 'Test Label', + inputId: 'testId', + modelValue: '' + } + }); + + const input = wrapper.find('input'); + await input.setValue('Test Value'); + + expect(wrapper.emitted().inputChangeEvent).toBeTruthy(); + expect(wrapper.emitted().inputChangeEvent[0]).toEqual(['Test Value']); + }); +}); diff --git a/src/components/LeaderboardComponents/Leaderboard.vue b/src/components/LeaderboardComponents/Leaderboard.vue index 9003a25a4c37050a30eafc00cb1551c4885b0dc7..735ef4c014946080061da6e7c1e991b3b6a347eb 100644 --- a/src/components/LeaderboardComponents/Leaderboard.vue +++ b/src/components/LeaderboardComponents/Leaderboard.vue @@ -20,7 +20,7 @@ <tbody id="line">`</tbody> <tbody v-if="!userInLeaderboard"> <tr></tr> - <tr v-for="(entry, index) in leaderboardExtra" :key="entry.user.id" :class="{ 'is-user-5': entry.user.firstName === 'User' }"> + <tr v-for="(entry, index) in leaderboardExtra" :key="entry.user.id" :class="{ 'is-user-5': entry.user.firstName === userStore.firstname }"> <td class="number">{{ entry.rank }}</td> <td class="name" @click="navigateToUserProfile(entry.user.id)">{{ entry.user.firstName }}</td> <td class="points">{{ entry.score }}</td> diff --git a/src/components/LeaderboardComponents/__tests__/Leaderboard.spec.ts b/src/components/LeaderboardComponents/__tests__/Leaderboard.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f069a058b885113e8e7f86f97834b561d90d2a9 --- /dev/null +++ b/src/components/LeaderboardComponents/__tests__/Leaderboard.spec.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createRouter, createMemoryHistory } from 'vue-router'; +import { createPinia, setActivePinia } from 'pinia'; +import Leaderboard from '@/components/LeaderboardComponents/Leaderboard.vue'; +import { useUserInfoStore } from '@/stores/UserStore'; +import router from '@/router/index'; + +describe('Leaderboard', () => { + let wrapper, store, mockRouter; + + const leaderboard = [ + { user: { id: 1, firstName: 'Alice', email: 'alice@example.com' }, rank: 1, score: 50 }, + { user: { id: 2, firstName: 'Bob', email: 'bob@example.com' }, rank: 2, score: 45 } + ]; + + const leaderboardExtra = [ + { user: { id: 3, firstName: 'Charlie', email: 'charlie@example.com' }, rank: 1, score: 40 } + ]; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useUserInfoStore(); + store.$state = { email: 'alice@example.com' }; // Setting initial state + mockRouter = createRouter({ + history: createMemoryHistory(), + routes: router.getRoutes(), + }); + + router.beforeEach((to, from, next) => { + const isAuthenticated = store.accessToken; + if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) { + next({ name: 'login' }); + } else { + next(); + } + }); + + wrapper = mount(Leaderboard, { + props: { leaderboard, leaderboardExtra }, + global: { + plugins: [mockRouter], + stubs: ['router-link', 'router-view'] + } + }); + }); + + it('renders all entries from the leaderboard and leaderboardExtra props', () => { + const rows = wrapper.findAll('tbody > tr'); + expect(rows.length).toBe(2); + }); + + it('correctly determines if the user is in the leaderboard', () => { + expect(wrapper.vm.userInLeaderboard).toBe(true); + }); + + it('shows the gold medal image only for the first entry', () => { + const medals = wrapper.findAll('.gold-medal'); + expect(medals.length).toBe(1); // Only the first entry should have a gold medal + }); + + it('applies the is-user-5 class based on user firstName', () => { + store.$state.firstname = 'User'; // Change state to match the condition + expect(wrapper.find('.is-user-5').exists()).toBe(false); // Check if the class is applied + }); +}); diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue index 3401af96d2977c1f508c15d2dc47e3e9748a2c6d..51d0c18846013b298212d27285bf0823958262da 100644 --- a/src/components/Login/LoginForm.vue +++ b/src/components/Login/LoginForm.vue @@ -32,7 +32,14 @@ const handleSubmit = async () => { console.log(emailRef.value) console.log(passwordRef.value) + formRef.value.classList.add("was-validated") + + const form = formRef.value; + if (!form.checkValidity()) { + return; + } + const loginUserPayload: LoginRequest = { email: emailRef.value, password: passwordRef.value @@ -55,6 +62,9 @@ const handleSubmit = async () => { email: emailRef.value, role: response.role, }); + + console.log() + await router.push({ name: 'home' }); } catch (error: any) { errorMsg.value = handleUnknownError(error); diff --git a/src/components/Login/LoginLink.vue b/src/components/Login/LoginLink.vue index 0f23a2edbeed887995e5db725c0aac37092effa6..8f1db76b8746d93bbdb0ec2c16526d93fc55149b 100644 --- a/src/components/Login/LoginLink.vue +++ b/src/components/Login/LoginLink.vue @@ -3,7 +3,7 @@ </script> <template> - <p>Already have an account? <RouterLink to="/login">Login</RouterLink></p> + <p>Already have an account? <RouterLink to="/login" id="login">Login</RouterLink></p> </template> <style scoped> diff --git a/src/components/Login/__tests__/LoginForm.spec.ts b/src/components/Login/__tests__/LoginForm.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..399d96c794edc716ef1749a30801147067a99b2c --- /dev/null +++ b/src/components/Login/__tests__/LoginForm.spec.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createRouter, createMemoryHistory } from 'vue-router'; +import { createPinia, setActivePinia } from 'pinia'; +import { useUserInfoStore } from '@/stores/UserStore'; +import MyComponent from '@/components/Login/LoginForm.vue'; // Adjust path as needed +import router from '@/router/index'; // Adjust path as needed +import { access } from 'fs'; +import { render, fireEvent, cleanup, screen } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; + +describe('Menu and Router Tests', () => { + let store: any, mockRouter: any; + + beforeEach(() => { + // Create a fresh Pinia and Router instance before each test + setActivePinia(createPinia()); + store = useUserInfoStore(); + mockRouter = createRouter({ + history: createMemoryHistory(), + routes: router.getRoutes(), + }); + router.beforeEach((to, from, next) => { + const isAuthenticated = store.accessToken; + if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) { + next({ name: 'login' }); + } else { + next(); + } + }); + + }); + + describe('Component Rendering', () => { + it('renders form correctly', () => { + store.setUserInfo({ firstname: 'Jane', lastname: 'Doe', accessToken: 'thisIsATestToken' }); + + const wrapper = mount(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + expect(wrapper.text()).toContain('email'); + expect(wrapper.text()).toContain('password'); + }); + }); + + describe('Navigation Guards', () => { + it('redirects an unauthenticated user to login when accessing a protected route', async () => { + store.$patch({ accessToken: '' }); + + router.push('/'); + await router.isReady(); + + expect(router.currentRoute.value.name).toBe('login'); + }); + + it('allows an unauthenticated user to visit signup', async () => { + store.$patch({ accessToken: 'valid-token' }); + + mockRouter.push('/sign-up'); + + await mockRouter.isReady(); + + expect(mockRouter.currentRoute.value.name).toBe('sign up'); + }); + }); + + + describe('Input fields', () => { + it('updates user credetials correctly', async () => { + const { getByPlaceholderText } = render(MyComponent); + + const emailInput = getByPlaceholderText('Enter your email'); + const passwordInput = getByPlaceholderText('Enter password'); + await fireEvent.update(emailInput, 'user@example.com'); + await fireEvent.update(passwordInput, 'Password1'); + + expect(emailInput.value).toBe('user@example.com'); + expect(passwordInput.value).toBe('Password1'); + }); + + it('Password error msg', async () => { + const { container } = render(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + const errorMsg = container.querySelector('#invalid'); // Use the actual ID here + expect(errorMsg?.textContent === "Password must be between 4 and 16 characters and contain one capital letter, small letter and a number") + }); + + it('logout should have empty store at application start', () => { + expect(store.firstname).toBe(''); + expect(store.lastname).toBe(''); + expect(store.accessToken).toBe(''); + }); + }); + + describe('Menu Actions', () => { + it('signup redirects to signup', async () => { + const { container } = render(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + // Assuming there's an element with id="home-link" that you want to click + const signupLink = container.querySelector('#signup'); // Use the actual ID here + if (signupLink) { + await userEvent.click(signupLink); + await mockRouter.isReady(); + } + + expect(mockRouter.currentRoute.value.name).toBe('sign up'); // Assuming 'Home' is the route name for '/' + }); + }); +}); diff --git a/src/components/Login/__tests__/LoginLink.spec.ts b/src/components/Login/__tests__/LoginLink.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..acb566e492bf595bd940ce5e09f0f335f6766070 --- /dev/null +++ b/src/components/Login/__tests__/LoginLink.spec.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render } from '@testing-library/vue'; +import { createPinia, setActivePinia } from 'pinia'; +import { createRouter, createMemoryHistory } from 'vue-router'; +import LoginPrompt from '@/components/Login/LoginLink.vue'; +import { useUserInfoStore } from '@/stores/UserStore'; +import router from '@/router/index'; +import { render, screen } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; + +describe('LoginPrompt', () => { + let store, mockRouter; + + beforeEach(() => { + // Create a fresh Pinia and Router instance before each test + setActivePinia(createPinia()); + store = useUserInfoStore(); + mockRouter = createRouter({ + history: createMemoryHistory(), + routes: router.getRoutes(), + }); + router.beforeEach((to, from, next) => { + const isAuthenticated = store.accessToken; + if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) { + next({ name: 'login' }); + } else { + next(); + } + }); + }); + + + it('renders login link correctly', async () => { + const router = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/login', component: { template: 'Login Page' } }], + }); + + const { getByText } = render(LoginPrompt, { + global: { + plugins: [router], + }, + }); + + await router.isReady(); // Ensure the router is ready before asserting + + const loginLink = getByText('Login'); + expect(loginLink).toBeDefined(); // Check if the 'Login' link is rendered + }); + + it('navigates to the login page when the login link is clicked', async () => { + const mockRouter = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/login', name: 'login', component: { template: 'Login Page' } }], + }); + + const { container } = render(LoginPrompt, { + global: { + plugins: [mockRouter], + }, + }); + + await mockRouter.isReady(); // Ensure the router is ready before asserting + + const loginLink = container.querySelector('#login'); // Use the actual ID here + if (loginLink) { + await userEvent.click(loginLink); + await mockRouter.isReady(); + } + + expect(mockRouter.currentRoute.value.path).toBe('/login'); // Check if the router navigated to the login page + }, 10000); +}); diff --git a/src/components/NewsComponents/__tests__/NewsComponent.spec.ts b/src/components/NewsComponents/__tests__/NewsComponent.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f5f15c28c46180c819f1b3fe25e8b2ea1718862 --- /dev/null +++ b/src/components/NewsComponents/__tests__/NewsComponent.spec.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import MyComponent from '@/components/NewsComponents/NewsComponent.vue'; // Adjust the import path according to your setup + +// Mocking the global fetch API +global.fetch = vi.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ + articles: [ + { + urlToImage: 'example-image.jpg', + title: 'Test Title', + description: 'Test Description', + url: 'http://example.com' + } + ] + }) + }) +); + +describe('MyComponent', () => { + let wrapper; + + beforeEach(() => { + vi.useFakeTimers(); // Set up fake timers + vi.spyOn(global, 'setInterval'); // Spy on setInterval + + // Setting up the wrapper before each test + wrapper = mount(MyComponent); + }); + + afterEach(() => { + // Clearing all mocks and timers after each test + vi.clearAllMocks(); + vi.restoreAllMocks(); // Restore original implementations + vi.runOnlyPendingTimers(); + vi.useRealTimers(); // Use real timers again + }); + + it('fetches news and updates articles data on component mount', async () => { + await vi.advanceTimersByTime(0); // Fast-forward any timers (like setInterval) + expect(fetch).toHaveBeenCalledTimes(1); + expect(wrapper.vm.articles).toEqual([ + { + urlToImage: 'example-image.jpg', + title: 'Test Title', + description: 'Test Description', + url: 'http://example.com' + } + ]); + }); + + it('sets up an interval to fetch news every 5 minutes', () => { + expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 300000); + }); +}); diff --git a/src/components/SignUp/SignUpLink.vue b/src/components/SignUp/SignUpLink.vue index f7c354b6b9d64f7cc776d64f5336d739a95fa116..e16871208dcc1d42fd4dc7783587772215d8e1c9 100644 --- a/src/components/SignUp/SignUpLink.vue +++ b/src/components/SignUp/SignUpLink.vue @@ -3,7 +3,7 @@ </script> <template> - <p>Don't have an account? <RouterLink to="/sign-up">Sign up</RouterLink></p> + <p>Don't have an account? <RouterLink to="/sign-up" id="signup">Sign up</RouterLink></p> </template> <style scoped> diff --git a/src/components/SignUp/__tests__/SignUpForm.spec.ts b/src/components/SignUp/__tests__/SignUpForm.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0146a6e93ee78315a2bf670c1e3871e473bb6ffb --- /dev/null +++ b/src/components/SignUp/__tests__/SignUpForm.spec.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createRouter, createMemoryHistory } from 'vue-router'; +import { createPinia, setActivePinia } from 'pinia'; +import { useUserInfoStore } from '@/stores/UserStore'; +import MyComponent from '@/components/SignUp/SignUpForm.vue'; // Adjust path as needed +import router from '@/router/index'; // Adjust path as needed +import { access } from 'fs'; +import { render, fireEvent, cleanup, screen } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; + +describe('Menu and Router Tests', () => { + let store: any, mockRouter: any; + + beforeEach(() => { + // Create a fresh Pinia and Router instance before each test + setActivePinia(createPinia()); + store = useUserInfoStore(); + mockRouter = createRouter({ + history: createMemoryHistory(), + routes: router.getRoutes(), + }); + router.beforeEach((to, from, next) => { + const isAuthenticated = store.accessToken; + if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) { + next({ name: 'login' }); + } else { + next(); + } + }); + + }); + + describe('Component Rendering', () => { + it('renders form correctly', () => { + const wrapper = mount(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + expect(wrapper.text()).toContain('First name'); + expect(wrapper.text()).toContain('Surname'); + expect(wrapper.text()).toContain('Email'); + }); + }); + + describe('Navigation Guards', () => { + it('redirects an unauthenticated user to login when accessing a protected route', async () => { + store.$patch({ accessToken: '' }); + + router.push('/'); + await router.isReady(); + + expect(router.currentRoute.value.name).toBe('login'); + }); + + it('allows an unauthenticated user to visit login', async () => { + store.$patch({ accessToken: 'valid-token' }); + + mockRouter.push('/login'); + + await mockRouter.isReady(); + + expect(mockRouter.currentRoute.value.name).toBe('login'); + }); + }); + + + describe('Input fields', () => { + it('updates user credetials correctly', async () => { + const { getByPlaceholderText } = render(MyComponent); + + const firstInput = getByPlaceholderText('Enter your first name'); + const lastInput = getByPlaceholderText('Enter your surname'); + const emailInput = getByPlaceholderText('Enter your email'); + const passwordInput = getByPlaceholderText('Enter password'); + await fireEvent.update(firstInput, 'Alice'); + await fireEvent.update(lastInput, 'Alicon'); + await fireEvent.update(emailInput, 'user@example.com'); + await fireEvent.update(passwordInput, 'Password1'); + + expect(firstInput.value).toBe('Alice'); + expect(lastInput.value).toBe('Alicon'); + expect(emailInput.value).toBe('user@example.com'); + expect(passwordInput.value).toBe('Password1'); + }); + + it('Password error msg', async () => { + const { container } = render(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + const errorMsg = container.querySelector('#invalid'); // Use the actual ID here + expect(errorMsg?.textContent === "Password must be between 4 and 16 characters and contain one capital letter, small letter and a number") + }); + + it('logout should have empty store at application start', () => { + expect(store.firstname).toBe(''); + expect(store.lastname).toBe(''); + expect(store.accessToken).toBe(''); + }); + }); + + describe('Menu Actions', () => { + it('signup redirects to signup', async () => { + const { container } = render(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + // Assuming there's an element with id="home-link" that you want to click + const signupLink = container.querySelector('#login'); // Use the actual ID here + if (signupLink) { + await userEvent.click(signupLink); + await mockRouter.isReady(); + } + + expect(mockRouter.currentRoute.value.name).toBe('login'); // Assuming 'Home' is the route name for '/' + }); + }); +}); diff --git a/src/components/SignUp/__tests__/SignUpLink.spec.ts b/src/components/SignUp/__tests__/SignUpLink.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4701d55d2f7d7c4690b0b24cc5f687b8c0fc2285 --- /dev/null +++ b/src/components/SignUp/__tests__/SignUpLink.spec.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render } from '@testing-library/vue'; +import { createPinia, setActivePinia } from 'pinia'; +import { createRouter, createMemoryHistory } from 'vue-router'; +import LoginPrompt from '@/components/SignUp/SignUpLink.vue'; +import { useUserInfoStore } from '@/stores/UserStore'; +import router from '@/router/index'; +import { render, screen } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; + +describe('LoginPrompt', () => { + let store, mockRouter; + + beforeEach(() => { + // Create a fresh Pinia and Router instance before each test + setActivePinia(createPinia()); + store = useUserInfoStore(); + mockRouter = createRouter({ + history: createMemoryHistory(), + routes: router.getRoutes(), + }); + router.beforeEach((to, from, next) => { + const isAuthenticated = store.accessToken; + if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) { + next({ name: 'login' }); + } else { + next(); + } + }); + }); + + + it('renders login link correctly', async () => { + const router = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/signup', component: { template: 'Signup Page' } }], + }); + + const { getByText } = render(LoginPrompt, { + global: { + plugins: [router], + }, + }); + + const loginLink = getByText('Sign up'); + expect(loginLink).toBeDefined(); // Check if the 'Login' link is rendered + }); + + it('navigates to the login page when the login link is clicked', async () => { + const mockRouter = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/login', name: 'login', component: { template: 'Login Page' } }], + }); + + const { container } = render(LoginPrompt, { + global: { + plugins: [mockRouter], + }, + }); + + await mockRouter.isReady(); // Ensure the router is ready before asserting + + const signupLink = container.querySelector('#signup'); // Use the actual ID here + if (signupLink) { + await userEvent.click(signupLink); + await mockRouter.isReady(); + } + + expect(mockRouter.currentRoute.value.path).toBe('/sign-up'); // Check if the router navigated to the login page + }, 10000); +}); diff --git a/src/components/UpdateUserComponents/UpdateUserLayout.vue b/src/components/UpdateUserComponents/UpdateUserLayout.vue index e90c5cd6f503c23c640bafa9a11b90d60e4ad582..838d8544930828865399e42f0559f0353f34e206 100644 --- a/src/components/UpdateUserComponents/UpdateUserLayout.vue +++ b/src/components/UpdateUserComponents/UpdateUserLayout.vue @@ -1,12 +1,8 @@ <script setup lang="ts"> import BaseInput from "@/components/InputFields/BaseInput.vue"; -import {onMounted, ref} from "vue"; -import {AuthenticationService, LeaderboardService, UserService, type UserUpdateDTO} from "@/api"; -import {useUserInfoStore} from "@/stores/UserStore"; - - - - +import { onMounted, ref } from "vue"; +import { AuthenticationService, LeaderboardService, UserService, type UserUpdateDTO } from "@/api"; +import { useUserInfoStore } from "@/stores/UserStore"; const firstNameRef = ref() const surnameRef = ref('') @@ -28,7 +24,7 @@ async function setupForm() { if (response.email != null) { emailRef.value = response.email } - }catch (err){ + } catch (err) { console.error(err) } } @@ -74,27 +70,27 @@ const handleSubmit = async () => { - if (form.checkValidity()) { - if(samePasswords.value){ - try { - UserControllerService.update({requestBody: updateUserPayload}) - useUserInfoStore().setUserInfo({ - email: emailRef.value, - firstname: firstNameRef.value, - lastname: surnameRef.value, - password: passwordRef.value - }) + if (form.checkValidity()) { + if (samePasswords.value) { + try { + UserService.update({ requestBody: updateUserPayload }) + useUserInfoStore().setUserInfo({ + email: emailRef.value, + firstname: firstNameRef.value, + lastname: surnameRef.value, + password: passwordRef.value + }) - }catch (err){ - cosole.error(err) - } - } - } else { - console.log('Form is not valid'); + } catch (err) { + cosole.error(err) + } } + } else { + console.log('Form is not valid'); + } } -onMounted(()=>{ +onMounted(() => { setupForm() }) @@ -103,123 +99,132 @@ onMounted(()=>{ </script> <template> - <div class="container"> - <!-- The userprofile and the form that will update name, email, password --> - <div class="row"> - <div class="col-md-2 text-center"> - <img src="/src/assets/userprofile.png" class="img-fluid" alt="userprofile"> - <p class="h2">{{useUserInfoStore().getFirstName}}</p> + <div class="containers"> + <div class="row gutters"> + <div class="col-xl-3 col-lg-3 col-md-12 col-sm-12 col-12"> + <div class="card h-100"> + <div class="card-body"> + <div class="account-settings"> + <div class="user-profile"> + <div class="user-avatar"> + <img src="https://bootdey.com/img/Content/avatar/avatar7.png" alt="Maxwell Admin"> + </div> + <div class="text-center"> + <div class="mt-2"> + <span class="btn btn-primary"><img src="@/assets/icons/download.svg"></span> + </div> + </div> + <br> + <h3 class="user-name">Yuki Hayashi</h3> + <h6 class="user-email">yuki@Maxwell.com</h6> + </div> + </div> + </div> + </div> </div> - <div class="col-md-10"> - <!-- May need to deactive @submit.prevent --> - <form ref="formRef" @submit.prevent="handleSubmit" id="newForm"> - <div class="row"> - <div class="form-group col-md-6" > - <!-- <label for="inputFirstName" class="form-label">First Name</label> - <input type="text" class="form-control" id="inputFirstName" placeholder="ex: Brian">--> - <BaseInput :model-value="firstNameRef" - @input-change-event="handleFirstNameInputEvent" - id="firstNameInputChange" - input-id="first-name-new" - type="text" - label="First name" - placeholder="Enter your first name" - invalid-message="Please enter your first name" - - /> + <div class="col-xl-9 col-lg-9 col-md-12 col-sm-12 col-12"> + <div class="card h-100"> + <div class="card-body"> + <div class="row gutters"> + <div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-12"> + <h6 class="mb-2 text-primary">Personal Details <img src="@/assets/icons/black_person.svg"></h6> + </div> + <div class="col-xl-6 col-lg-6 col-md-6 col-sm-6 col-12"> + <div class="form-group"> + <BaseInput :model-value="firstNameRef" @input-change-event="handleFirstNameInputEvent" + id="firstNameInputChange" input-id="first-name-new" type="text" label="First name" + placeholder="Enter your first name" invalid-message="Please enter your first name" /> + </div> + </div> + <div class="col-xl-6 col-lg-6 col-md-6 col-sm-6 col-12"> + <div class="form-group"> + <BaseInput :model-value="surnameRef" @input-change-event="handleSurnameInputEvent" + id="surnameInput-change" input-id="surname-new" type="text" label="Surname" + placeholder="Enter your surname" invalid-message="Please enter your surname" /> + + </div> + </div> + <div class="col-xl-6 col-lg-6 col-md-6 col-sm-6 col-12"> + <div class="form-group"> + <BaseInput :model-value="emailRef" @input-change-event="handleEmailInputEvent" id="emailInput-change" + input-id="email-new" type="email" label="Email" placeholder="Enter your email" + invalid-message="Invalid email" /> + + </div> + </div> + <div class="col-xl-6 col-lg-6 col-md-6 col-sm-6 col-12"> + <div class="form-group"> + <BaseInput :model-value="passwordRef" @input-change-event="handlePasswordInputEvent" + id="passwordInput-change" input-id="password-new" type="password" + pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,16}" label="Password" placeholder="Enter password" + invalid-message="Password must be between 4 and 16 characters and contain one capital letter, small letter and a number" /> + + </div> + </div> </div> - <div class="form-group col-md-6"> - <!-- <label for="inputPassword4">Last Name</label> - <input type="text" class="form-control" id="inputLastName" placeholder="ex: Cox">--> - <BaseInput :model-value="surnameRef" - @input-change-event="handleSurnameInputEvent" - id="surnameInput-change" - input-id="surname-new" - type="text" - label="Surname" - placeholder="Enter your surname" - invalid-message="Please enter your surname" - /> + + <div class="row gutters"> + <div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-12" style="margin-top: 10px;"> + <h6 class="mb-2 text-primary">Personal Configuration <img src="@/assets/icons/black_person.svg"></h6> + </div> + <div class="accordion" id="accordionExample"> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" + data-bs-target="#collapseThree" aria-expanded="true" aria-controls="collapseThree"> + Configuration + </button> + </h2> + <div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + Hallo + </div> + </div> + </div> + </div> </div> - </div> - <div class="form-group col-md-6"> - <!-- <label for="inputAddress">Email</label> - <input type="email" class="form-control" id="inputMail" placeholder="ex: briancox@mail.com">--> - <BaseInput :model-value="emailRef" - @input-change-event="handleEmailInputEvent" - id="emailInput-change" - input-id="email-new" - type="email" - label="Email" - placeholder="Enter your email" - invalid-message="Invalid email" - /> - </div> - <div class="row"> - <div class="form-group col-md-6"> - <!-- <label for="inputPassword">Password</label> - <input type="password" class="form-control" id="inputPassword" placeholder="Password with capital letter, number and a special character">--> - <BaseInput :model-value="passwordRef" - @input-change-event="handlePasswordInputEvent" - id="passwordInput-change" - input-id="password-new" - type="password" - pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,16}" - label="Password" - placeholder="Enter password" - invalid-message="Password must be between 4 and 16 characters and contain one capital letter, small letter and a number" - /> + <div class="row gutters"> + <div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-12"> + <h6 class="mt-3 mb-2 text-primary">Styles <img src="@/assets/icons/black_paintBrush.svg"></h6> + </div> + <div class="accordion" id="accordionExample"> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" + data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne"> + Profile pictures + </button> + </h2> + <div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + Hallo + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" + data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> + Road styles + </button> + </h2> + <div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + Hallo + </div> + </div> + </div> + </div> </div> - <div class="form-group col-md-6"> - <!-- <label for="inputPasswordConfirmed">Confirmed Password</label> - <input type="password" class="form-control" id="inputPasswordConfirmed" placeholder="Repeat password">--> - <BaseInput :modelValue="confirmPasswordRef" - @input-change-event="handleConfirmPasswordInputEvent" - id="confirmPasswordInput-change" - input-id="confirmPassword-new" - type="password" - pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,16}" - label="Confirm Password" - placeholder="Confirm password" - invalid-message="Password must be between 4 and 16 characters and contain one capital letter, small letter and a number" - /> - - <p v-if="!samePasswords" class="text-danger">The passwords are not identical</p> + <div class="row gutters"> + <div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-12"> + <div class="text-right"> + <button type="button" id="submit" name="submit" class="btn btn-secondary">Cancel</button> + <button type="button" id="submit" name="submit" class="btn btn-primary">Update</button> + </div> + </div> </div> </div> - <button type="submit" @click="handleSubmit" class="btn btn-primary">Change settings</button> - </form> - </div> - </div> - <!-- Maybe a profile-pictures here that collapses or not --> - <div class="row"> - <div class="col"> - <p> - <a class="btn" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="collapseExample"> - Profile Pictures - </a> - </p> - </div> - </div> - <div class="row"> - <div class="collapse" id="collapseExample"> - <div class="card card-body"> - This is the content of the user profile - </div> - </div> - </div> - <!-- Div that contains the configuration --> - <div class="row"> - <div class="col"><p> - <a class="btn" data-bs-toggle="collapse" href="#collapseConfiguration" role="button" aria-expanded="false" aria-controls="collapseConfiguration"> - User Configuration - </a> - </p></div> - </div> - <div class="row"> - <div class="collapse" id="collapseConfiguration"> - <div class="card card-body"> - This is the configuration of the user configuration </div> </div> </div> @@ -228,5 +233,94 @@ onMounted(()=>{ </template> <style scoped> +body { + margin: 0; + padding-top: 40px; + color: #2e323c; + background: #f5f6fa; + position: relative; + height: 100%; +} +.row { + margin: 0px; +} + +.containers { + width: 100%; + justify-content: center; + display: flex; + align-items: center; + margin-top: 2rem; + margin-bottom: 4rem; +} + +.account-settings .user-profile { + margin: 0 0 1rem 0; + padding-bottom: 1rem; + text-align: center; +} + +.account-settings .user-profile .user-avatar { + margin: 0 0 1rem 0; +} + +.account-settings .user-profile .user-avatar img { + width: 90px; + height: 90px; + -webkit-border-radius: 100px; + -moz-border-radius: 100px; + border-radius: 100px; +} + +.account-settings .user-profile h3.user-name { + margin: 0 0 0.5rem 0; +} + +.account-settings .user-profile h6.user-email { + margin: 0; + font-size: 0.8rem; + font-weight: 400; + color: #9fa8b9; +} + +.account-settings .about { + margin: 2rem 0 0 0; + text-align: center; +} + +.account-settings .about h5 { + margin: 0 0 15px 0; + color: #007ae1; +} + +.account-settings .about p { + font-size: 0.825rem; +} + +.form-control { + border: 1px solid #cfd1d8; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + font-size: .825rem; + background: #ffffff; + color: #2e323c; +} + +.text-right { + display: flex; + justify-content: flex-end; + margin-top: 10px; +} + +.card { + background: #efefef; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + border: 0; + margin-bottom: 1rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); +} </style> \ No newline at end of file diff --git a/src/components/UserProfile/UserProfileLayout.vue b/src/components/UserProfile/UserProfileLayout.vue index 8d761b1e22026776ab4d665886e6b2ebf14a46d4..7036e347b7ad8398651125fc7a8e897c4ab5c76d 100644 --- a/src/components/UserProfile/UserProfileLayout.vue +++ b/src/components/UserProfile/UserProfileLayout.vue @@ -1,7 +1,5 @@ <script setup lang="ts"> - -import Menu from "@/components/BaseComponents/Menu.vue"; -import Footer from "@/components/BaseComponents/Footer.vue"; +import { ref } from "vue"; import { useRouter } from "vue-router"; import { useUserInfoStore } from "../../stores/UserStore"; @@ -11,15 +9,23 @@ let cardTitles = ["Spain tour", "Food waste", "Coffee", "Concert", "New book", " let points = 0; let streak = 0; +let firstname = ref(""); +let lastname = ref(""); -let route = useRouter() -function toRoadmap() { - route.push('/roadmap') -} +const router = useRouter(); +const userStore = useUserInfoStore(); +firstname.value = userStore.firstname; +lastname.value = userStore.lastname; -function toUpdateUserSettings() { - route.push('/update-user') -} + +const toRoadmap = () => { + router.push('/'); +}; + +// Function to navigate to update user settings +const toUpdateUserSettings = () => { + router.push('/update-user'); +}; </script> <template> @@ -32,12 +38,12 @@ function toUpdateUserSettings() { <img src="https://bootdey.com/img/Content/avatar/avatar3.png" alt="Generic placeholder image" class="img-fluid img-thumbnail mt-4 mb-2" style="width: 150px; z-index: 1"> <button type="button" data-mdb-button-init data-mdb-ripple-init class="btn btn-outline-primary" - data-mdb-ripple-color="dark" style="z-index: 1;" @click="toUpdateUserSettings"> + data-mdb-ripple-color="dark" style="z-index: 1;" id="toUpdate" @click="toUpdateUserSettings"> Edit profile </button> </div> <div class="ms-3" style="margin-top: 130px;"> - <h1>{{useUserInfoStore().firstname}}</h1> + <h1>{{ firstname }} {{ lastname }}</h1> </div> </div> <div class="p-4 text-black" style="background-color: #f8f9fa;"> diff --git a/src/components/UserProfile/__tests__/UserProfileLayout.spec.ts b/src/components/UserProfile/__tests__/UserProfileLayout.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3028b0cb27deeb0de90684578082eeb735fe6ea --- /dev/null +++ b/src/components/UserProfile/__tests__/UserProfileLayout.spec.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createRouter, createMemoryHistory } from 'vue-router'; +import { createPinia, setActivePinia } from 'pinia'; +import { useUserInfoStore } from '@/stores/UserStore'; +import MyComponent from '@/components/UserProfile/UserProfileLayout.vue'; // Adjust path as needed +import router from '@/router/index'; // Adjust path as needed +import { access } from 'fs'; + +describe('MyComponent and Router Tests', () => { + let store, mockRouter; + + beforeEach(() => { + // Create a fresh Pinia and Router instance before each test + setActivePinia(createPinia()); + store = useUserInfoStore(); + mockRouter = createRouter({ + history: createMemoryHistory(), + routes: router.getRoutes(), + }); + router.beforeEach((to, from, next) => { + const isAuthenticated = store.accessToken; + if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) { + next({ name: 'login' }); + } else { + next(); + } + }); + + }); + + describe('Component Rendering', () => { + it('renders MyComponent correctly with data from the store', () => { + // Mock user information + store.setUserInfo({ firstname: 'Jane', lastname: 'Doe', accessToken: 'thisIsATestToken' }); + + const wrapper = mount(MyComponent, { + global: { + plugins: [mockRouter], + }, + }); + + // Check for text or elements that depend on user info + expect(wrapper.text()).toContain('Jane'); + expect(wrapper.text()).toContain('Doe'); + }); + }); + + describe('Navigation Guards', () => { + it('redirects an unauthenticated user to login when accessing a protected route', async () => { + // Simulate the user being unauthenticated + store.$patch({ accessToken: '' }); + + router.push('/profile'); + await router.isReady(); + + expect(router.currentRoute.value.name).toBe('login'); + }); + + it('allows an authenticated user to visit a protected route', async () => { + store.$patch({ accessToken: 'valid-token' }); // Token is present + mockRouter.push('/profile'); + await mockRouter.isReady(); + expect(mockRouter.currentRoute.value.name).toBe('profile'); + }); + }); + + + describe('UserStore Actions', () => { + it('updates user information correctly', () => { + store.setUserInfo({ firstname: 'John', lastname: 'Smith' }); + + expect(store.firstname).toBe('John'); + expect(store.lastname).toBe('Smith'); + }); + + it('clears user information correctly', () => { + store.setUserInfo({ firstname: 'John', lastname: 'Smith', accessToken: 'thisIsATestToken'}); + store.clearUserInfo(); + + expect(store.firstname).toBe(''); + expect(store.lastname).toBe(''); + expect(store.accessToken).toBe(''); + }); + }); +}); diff --git a/src/router/index.ts b/src/router/index.ts index 1e212e0da992f71c7e07e3fe5d3ba53df75ddd8f..af9f9e2a4ad76ced1692f1d100d2223440f4bc7e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -35,7 +35,7 @@ const routes = [ component: () => import('@/views/TestView.vue'), }, { - path: '/profile', + path: 'profile', name: 'profile', component: UserProfileView }, @@ -60,9 +60,14 @@ const routes = [ component: () => import('@/views/ShopView.vue'), }, { - path: 'profile', - name: 'profile', - component: UserProfileView + path: '/budget-overview', + name: 'budget overview', + component: () => import('@/views/BudgetOverview.vue'), + }, + { + path: '/budget', + name: 'budget', + component: () => import('@/views/BudgetView.vue'), }, { path: '/profile/:id', @@ -156,7 +161,7 @@ const routes = [ ]; const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), + history: createWebHistory(import.meta.env.BASE_URL || '/'), routes, scrollBehavior() { return { top: 0 }; diff --git a/src/views/BudgetOverview.vue b/src/views/BudgetOverview.vue new file mode 100644 index 0000000000000000000000000000000000000000..e58fb67ed304fcffc341f2baea40d1ace164c119 --- /dev/null +++ b/src/views/BudgetOverview.vue @@ -0,0 +1,74 @@ +<script setup lang="ts"> +import Button1 from '@/components/Buttons/Button1.vue' +import BudgetBox from '@/components/Budget/BudgetBox.vue' +</script> + +<template> + <div class="container"> + <h1 class="text-center">Your Budgets</h1> + <button1 id="createBudgetButton" button-text="Create new budget" class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample"/> + + <div class="collapse" id="collapseExample"> + <div class="container collapse-container"> + <div class="input-group row"> + <input id="collapseInput" class="col-5 form-control" type="text" placeholder="Enter name of budget"> + <button1 id="collapseButton" class="col-1" button-text="Create" data-bs-dismiss="modal"/> + </div> + </div> + </div> + + <!--TODO make this more generic--> + <ul class="budgetContainer"> + <li><budget-box title="April 2024" budget="1000" expenses="908700"></budget-box></li> + <li><budget-box title="Mai 2024" budget="1000" expenses="87"></budget-box></li> + <li><budget-box title="Juni 2024" budget="1000" expenses="87"></budget-box></li> + <li><budget-box title="Juli 2024" budget="1000" expenses="87"></budget-box></li> + <li><budget-box title="August 2024" budget="1000" expenses="87"></budget-box></li> + <li><budget-box title="September 2024" budget="1000" expenses="87"></budget-box></li> + <li><budget-box title="Oktober 2024" budget="1000" expenses="87"></budget-box></li> + <li><budget-box title="November 2024" budget="1000" expenses="87"></budget-box></li> + <li><budget-box title="Desember 2024" budget="1000" expenses="87"></budget-box></li> + </ul> + + <nav id="navbar" aria-label="Page navigation example"> + <ul class="pagination"> + <li class="page-item"> + <a class="page-link" href="#" aria-label="Previous"> + <span aria-hidden="true">«</span> + </a> + </li> + <li class="page-item"><a class="page-link" href="#">1</a></li> + <li class="page-item"><a class="page-link" href="#">2</a></li> + <li class="page-item"><a class="page-link" href="#">3</a></li> + <li class="page-item"> + <a class="page-link" href="#" aria-label="Next"> + <span aria-hidden="true">»</span> + </a> + </li> + </ul> + </nav> + + </div> + +</template> + +<style scoped> +.collapse-container { + align-content: center; + justify-content: center; + justify-items: center; +} + +.container { + padding: 10px; +} + +.budgetContainer { + list-style: none; + padding-left: 10px; +} + +ul > li { + margin: 10px 0; +} +</style> \ No newline at end of file diff --git a/src/views/BudgetView.vue b/src/views/BudgetView.vue new file mode 100644 index 0000000000000000000000000000000000000000..0297c102ccddc3c1f463cf93fb3049b5f1901b1c --- /dev/null +++ b/src/views/BudgetView.vue @@ -0,0 +1,328 @@ +<script setup lang="ts"> +import { onMounted, ref } from 'vue' +import Button1 from '@/components/Buttons/Button1.vue' +import ExpenseBox from '@/components/Budget/ExpenseBox.vue' +import router from '@/router' + +// TODO Need endpoint in order to retrieve budget +// Mocked values +let title = ref('Mai 2024'); +let budget = ref(10000); +let expenses = ref(0); +let balance = ref(budget.value - expenses.value); +let expenseJSONObject = ref({ + "expenses": [ + { + "title": "Ost", + "value": 30 + }, + { + "title": "Skinke", + "value": 20 + }, + { + "title": "Bread", + "value": 15 + } + ] +}); + +// Initially updates the total expense display +for (let expense of expenseJSONObject.value.expenses) { + expenses.value += expense.value +} + +// Reactive input values +let budgetTitle = ref('') +let budgetValue = ref() +let expenseDescription = ref('') +let expenseAmount = ref() +// Reactive background variable +const iRef = ref() + +/** + * Checks the value of the balance and adjust background color depending + * on negative or positive value after rendering. + */ +onMounted(() => { + if (balance.value >= 0) { + iRef.value.style.backgroundColor = 'rgba(34, 231, 50, 0.43)'; + } + balance.value = budget.value - expenses.value +}) + +/** + * Updates the balance and background color based on the budget and expenses. + */ +const updateBalance = () => { + // Resets expenses and then re-calculates it + expenses.value = 0 + for (let expense of expenseJSONObject.value.expenses) { + expenses.value += expense.value + } + // Updates balance value and background + balance.value = budget.value - expenses.value + if (balance.value >= 0) { + iRef.value.style.backgroundColor = 'rgba(34, 231, 50, 0.43)'; + } else { + iRef.value.style.backgroundColor= 'rgba(232, 14, 14, 0.43)'; + } +} + +/** + * Calculates a new budget and updates the balance. + * + * @param newBudget The new budget value. + */ +const calculateNewBudget = (newBudget: number) => { + budget.value = newBudget + updateBalance() +} + +/** + * TODO update javadoc when backend integration is done + * Adds a new expense to the expense JSON object and updates the balance. + * + * @param expenseDescription The description of the expense. + * @param expenseValue The value of the expense. + */ +const addNewExpense = (expenseDescription: string, expenseValue: number) => { + expenseJSONObject.value.expenses.push({ + "title": expenseDescription, + "value": expenseValue + }); + updateBalance() +} + + +/** + * Updates the title of the budget. + * + * @param newTitle The new title for the budget. + */ +const editBudgetTitle = (newTitle: string) => { + title.value = newTitle +} + +/** + * Deletes an expense from the list of expenses. + * + * @param index The index of the expense to delete. + */ +const deleteExpense = (index: number) => { + expenseJSONObject.value.expenses.splice(index, 1); + updateBalance() +} + +/** + * Edits an existing expense in the list of expenses. + * + * @param index The index of the expense to edit. + * @param newDescription The new description for the expense. + * @param newAmount The new amount for the expense. + */ +const editExpense = (index: number, newDescription: string, newAmount: number) => { + console.log('Reached') + expenseJSONObject.value.expenses[index].title = newDescription + expenseJSONObject.value.expenses[index].value = newAmount + updateBalance() +} + +// TODO add delete functionality +const onDeleteBudgetPressed = () => { + router.push('/budget-overview') +} + +</script> + +<template> + <div class="container"> + <h1 class="text-center">{{ title }}</h1> + + <div class="button-container"> + <button1 id="optionButton" button-text="Options" data-bs-toggle="modal" + data-bs-target="#modal"/> + <button1 id="saveChanges" button-text="Save changes"/> + </div> + + <div class="modal fade" id="modal"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h3>Options</h3> + <button class="btn btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <button id="importButton" class="btn btn-primary"><img src="../assets/icons/import.svg" height="20" width="20">Import budget</button> + <button id="editBudget" class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#editBudgetCollapse" aria-expanded="false" aria-controls="editBudgetCollapse"><img src="../assets/icons/edit-button.svg" alt="editButton">Rename budget</button> + <div class="collapse" id="editBudgetCollapse"> + <div class="container collapse-container"> + <form @submit.prevent="editBudgetTitle(budgetTitle)"> + <div class="input-group"> + <input id="collapseInput" class="col-5 form-control" type="text" placeholder="Enter new name of budget" v-model="budgetTitle"> + <button1 id="collapseButton" type="submit" button-text="Edit" data-bs-dismiss="modal" /> + </div> + </form> + </div> + </div> + <button id="deleteButton" class="btn btn-primary" data-bs-toggle="modal" @click="onDeleteBudgetPressed"><img src="../assets/icons/trash-can.svg" height="20" width="20">Delete budget</button> + </div> + </div> + </div> + </div> + + <div class="budget-info-container"> + <div class="info budget-container"> + <i><img src="../assets/icons/money2.svg" width="48px" height="48px"></i> + <div class="budget-text-container"> + <h5>{{budget}} kr</h5> + <p>Budget</p> + </div> + </div> + + <div class="info expenses-container"> + <i><img src="../assets/icons/credit-card.svg" width="48px" height="48px"></i> + <div class="expenses-text-container"> + <h5>{{expenses}} kr</h5> + <p>Expenses</p> + </div> + </div> + + <div class="info balance-container"> + <i ref="iRef"><img src="../assets/icons/scale.svg" width="48px" height="48px"></i> + <div class="balance-text-container"> + <h5>{{balance}} kr</h5> + <p>Balance</p> + </div> + </div> + </div> + + + <div class="budget-content-container"> + <form class="budget-from" @submit.prevent="calculateNewBudget(budgetValue)"> + <div class="input-group"> + <span class="input-group-text">Your budget: </span> + <input type="text" class="form-control" placeholder="Enter your budget" required v-model="budgetValue"> + <button type="submit" class="btn btn-primary">Calculate</button> + </div> + </form> + + <form class="expenses-form" @submit.prevent="addNewExpense(expenseDescription, expenseAmount)"> + <div class="input-group"> + <span class="input-group-text">Add new expense: </span> + <input type="text" class="form-control" placeholder="Name of expense" required v-model="expenseDescription"> + <input type="number" min="0" class="form-control" placeholder="Amount (kr)" required v-model="expenseAmount"> + <button type="submit" class="btn btn-primary">Calculate</button> + </div> + </form> + </div> + + <div class="expenses-details-container"> + <h3>Expenses details</h3> + <div class="expense-box-container"> + <expense-box v-for="(expense, index) in expenseJSONObject.expenses" + :key="index" + :index="index" + :description="expense.title" + :amount="expense.value" + @deleteEvent="deleteExpense" + @editEvent="editExpense"/> + </div> + </div> + + </div> +</template> + +<style scoped> + +.button-container { + display: flex; + gap: 10px; +} + +.container.collapse-container { + padding: 0; + margin: 0; +} + +.modal-header { + display: flex; +} + +.modal-body { + display: grid; + gap: 10px +} + +div.budget-info-container { + margin-top: 2rem; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + justify-content: center; + gap: 1rem; +} + +div.info { + display: flex; + flex-direction: row; + background-color: rgba(221, 221, 224, 0.5); + border-radius: 10px; + padding: 10px; + transition: transform 150ms ease-in-out; +} + +div.info:hover { + transform: scale(1.03); +} + +i { + display: grid; + justify-content: center; + align-content: center; + margin: 5px; + border-radius: 7px; + min-width: 90px; +} + +.budget-container i { + background-color: rgba(78, 107, 239, 0.43); +} + +.expenses-container i { + background-color: rgba(238, 191, 43, 0.43); +} + +.balance-container i { + background-color: rgba(232, 14, 14, 0.43); +} + +.budget-content-container { + margin: 2rem 0; + display: grid; + gap: 5px; +} +.budget-content-container label { + display: flex; + align-items: center; +} + + +.expenses-details-container { + margin: 1rem 0; + min-height: 80px; + border-radius: 8px; + background-color: rgba(234, 234, 234, 0.8); +} + +.expenses-details-container h3 { + margin-top: 1rem; + padding: 10px; +} + +.expense-box-container { + overflow-y: auto; + overflow-x: hidden; + max-height: 100vh; +} + +</style> \ No newline at end of file diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json index 571995d11e6acb21020f2570fb6a034609ee654e..0f0268e051af3a98dd7ea5656c937c3012d22623 100644 --- a/tsconfig.vitest.json +++ b/tsconfig.vitest.json @@ -4,8 +4,8 @@ "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", - + "lib": [], - "types": ["node", "jsdom"] + "types": ["node", "jsdom", "vitest/globals"] } } diff --git a/vitest.config.ts b/vitest.config.ts index 4b1c897997739635a6e14248a6448b67b2703c44..c29610e1b27cd789d15fedc6576e22276cab6a63 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,7 +8,11 @@ export default mergeConfig( test: { environment: 'jsdom', exclude: [...configDefaults.exclude, 'e2e/**'], - root: fileURLToPath(new URL('./', import.meta.url)) + root: fileURLToPath(new URL('./', import.meta.url)), + coverage: { + provider: 'v8', + exclude: ['src/views/'] + } } }) )