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">&laquo;</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">&raquo;</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/']
+      }
     }
   })
 )