diff --git a/index.html b/index.html index a888544898a5059056e4c148c28b2eabf4b2d67a..48d04d242838a0af8891ee79b127695b46798079 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <link rel="icon" href="/favicon.ico"> + <link rel="icon" href="/src/assets/Sparesti-logo.png"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Vite App</title> + <title>Sparesti</title> </head> <body> <div id="app"></div> diff --git a/package-lock.json b/package-lock.json index 05f3e7c65d110dbcb2eaa13715d7dac5e3c88dc5..a9b42c3119552e9ab948df2ed7ba8cafe9b5fe9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@popperjs/core": "^2.11.8", "axios": "^1.6.8", "bootstrap": "^5.3.3", + "install": "^0.13.0", "js-cookie": "^3.0.5", "oh-vue-icons": "^1.0.0-rc3", "pinia": "^2.1.7", @@ -26,6 +27,7 @@ "@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", @@ -568,6 +570,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", @@ -1236,6 +1244,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", @@ -2034,6 +2051,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", @@ -4857,6 +4901,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", @@ -5018,6 +5068,14 @@ "node": ">=10" } }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-ci": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", @@ -5190,6 +5248,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", @@ -5793,6 +5922,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", @@ -7397,6 +7585,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", diff --git a/package.json b/package.json index fe648fd2342c222ce21ce8de1351ed04156581de..88bf81d13c99ce997982fd7b8147f70b8cade92e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@popperjs/core": "^2.11.8", "axios": "^1.6.8", "bootstrap": "^5.3.3", + "install": "^0.13.0", "js-cookie": "^3.0.5", "oh-vue-icons": "^1.0.0-rc3", "pinia": "^2.1.7", @@ -34,6 +35,7 @@ "@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", diff --git a/spec.json b/spec.json index 26570a49b0bae494c990a7ffb525e6bedf4c6a7f..39d296813341dac9fecbc619df7eb63d3a4287e9 100644 --- a/spec.json +++ b/spec.json @@ -1,535 +1 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Sparesti API", - "description": "The Sparesti API", - "version": "3.0" - }, - "servers": [ - { - "url": "http://localhost:8080", - "description": "Generated server url" - } - ], - "security": [ - { - "Bearer Authentication": [] - } - ], - "tags": [ - { - "name": "Authentication", - "description": "User authentication" - }, - { - "name": "Leaderboard", - "description": "Retrieving leaderboard data" - } - ], - "paths": { - "/api/auth/valid-email/{email}": { - "post": { - "tags": [ - "Authentication" - ], - "summary": "Validate email", - "description": "Check that the given email is valid", - "operationId": "validateEmail", - "parameters": [ - { - "name": "email", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "409": { - "description": "Email already exists", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/ExceptionResponse" - } - } - } - }, - "200": { - "description": "Email is valid", - "content": { - "*/*": { - "schema": { - "type": "object" - } - } - } - } - }, - "security": [] - } - }, - "/api/auth/signup": { - "post": { - "tags": [ - "Authentication" - ], - "summary": "User Signup", - "description": "Sign up a new user", - "operationId": "signup", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SignUpRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Successfully signed up", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthenticationResponse" - } - } - } - }, - "409": { - "description": "Email already exists", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExceptionResponse" - } - } - } - } - }, - "security": [] - } - }, - "/api/auth/login": { - "post": { - "tags": [ - "Authentication" - ], - "summary": "User Login", - "description": "Log in with an existing user", - "operationId": "login", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successfully logged in", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthenticationResponse" - } - } - } - }, - "401": { - "description": "Invalid credentials", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExceptionResponse" - } - } - } - }, - "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExceptionResponse" - } - } - } - } - }, - "security": [] - } - }, - "/api/users": { - "patch": { - "tags": [ - "user-controller" - ], - "summary": "Update profile", - "description": "Update the profile of the authenticated user", - "operationId": "update", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserUpdateDTO" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successfully updated profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserDTO" - } - } - } - } - } - } - }, - "/api/users/{userId}/profile": { - "get": { - "tags": [ - "user-controller" - ], - "summary": "Get profile", - "description": "Get user profile", - "operationId": "getProfile", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "200": { - "description": "Successfully got profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProfileDTO" - } - } - } - } - } - } - }, - "/api/users/me": { - "get": { - "tags": [ - "user-controller" - ], - "summary": "Get user", - "description": "Get user information", - "operationId": "getUser", - "responses": { - "200": { - "description": "Successfully got user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserDTO" - } - } - } - } - } - } - }, - "/api/leaderboard": { - "get": { - "tags": [ - "Leaderboard" - ], - "operationId": "getLeaderboard", - "parameters": [ - { - "name": "type", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "filter", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "entryCount", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 10 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LeaderboardDTO" - } - } - } - } - } - } - }, - "/api/leaderboard/surrounding": { - "get": { - "tags": [ - "Leaderboard" - ], - "operationId": "getSurrounding", - "parameters": [ - { - "name": "type", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "filter", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "entryCount", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 10 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LeaderboardDTO" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "ExceptionResponse": { - "type": "object", - "properties": { - "status": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - }, - "SignUpRequest": { - "type": "object", - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "commitment": { - "type": "string" - }, - "experience": { - "type": "string" - }, - "challengeTypes": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "AuthenticationResponse": { - "type": "object", - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "role": { - "type": "string" - }, - "token": { - "type": "string" - } - } - }, - "LoginRequest": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "UserUpdateDTO": { - "type": "object", - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "commitment": { - "type": "string" - }, - "experience": { - "type": "string" - }, - "challengeTypes": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "UserDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "role": { - "type": "string" - } - } - }, - "ProfileDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "LeaderboardDTO": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "filter": { - "type": "string" - }, - "entries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LeaderboardEntryDTO" - } - } - } - }, - "LeaderboardEntryDTO": { - "type": "object", - "properties": { - "user": { - "$ref": "#/components/schemas/UserDTO" - }, - "score": { - "type": "integer", - "format": "int32" - } - } - } - }, - "securitySchemes": { - "Bearer Authentication": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } - } - } -} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"Sparesti API","description":"The Sparesti API","version":"3.0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"security":[{"Bearer Authentication":[]}],"tags":[{"name":"Authentication","description":"User authentication"},{"name":"Leaderboard","description":"Retrieving leaderboard data"}],"paths":{"/api/auth/valid-email/{email}":{"post":{"tags":["Authentication"],"summary":"Validate email","description":"Check that the given email is valid","operationId":"validateEmail","parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ExceptionResponse"}}}},"200":{"description":"Email is valid","content":{"*/*":{"schema":{"type":"object"}}}}},"security":[]}},"/api/auth/signup":{"post":{"tags":["Authentication"],"summary":"User Signup","description":"Sign up a new user","operationId":"signup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExceptionResponse"}}}},"201":{"description":"Successfully signed up","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}},"security":[]}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User Login","description":"Log in with an existing user","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successfully logged in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExceptionResponse"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExceptionResponse"}}}}},"security":[]}},"/api/users":{"patch":{"tags":["user-controller"],"summary":"Update profile","description":"Update the profile of the authenticated user","operationId":"update","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdateDTO"}}},"required":true},"responses":{"200":{"description":"Successfully updated profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}}}},"/api/users/{userId}/profile":{"get":{"tags":["user-controller"],"summary":"Get profile","description":"Get user profile","operationId":"getProfile","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Successfully got profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileDTO"}}}}}}},"/api/users/me":{"get":{"tags":["user-controller"],"summary":"Get user","description":"Get user information","operationId":"getUser","responses":{"200":{"description":"Successfully got user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}}}},"/api/leaderboard":{"get":{"tags":["Leaderboard"],"operationId":"getLeaderboard","parameters":[{"name":"type","in":"query","required":true,"schema":{"type":"string"}},{"name":"filter","in":"query","required":true,"schema":{"type":"string"}},{"name":"entryCount","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":10}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardDTO"}}}}}}},"/api/leaderboard/surrounding":{"get":{"tags":["Leaderboard"],"operationId":"getSurrounding","parameters":[{"name":"type","in":"query","required":true,"schema":{"type":"string"}},{"name":"filter","in":"query","required":true,"schema":{"type":"string"}},{"name":"entryCount","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":10}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardDTO"}}}}}}}},"components":{"schemas":{"ExceptionResponse":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"message":{"type":"string"}}},"SignUpRequest":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"commitment":{"type":"string"},"experience":{"type":"string"},"challengeTypes":{"type":"array","items":{"type":"string"}}}},"AuthenticationResponse":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string"},"token":{"type":"string"}}},"LoginRequest":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"}}},"UserUpdateDTO":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"commitment":{"type":"string"},"experience":{"type":"string"},"challengeTypes":{"type":"array","items":{"type":"string"}}}},"UserDTO":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"role":{"type":"string"}}},"ProfileDTO":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"LeaderboardDTO":{"type":"object","properties":{"type":{"type":"string"},"filter":{"type":"string"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/LeaderboardEntryDTO"}}}},"LeaderboardEntryDTO":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/UserDTO"},"score":{"type":"integer","format":"int32"},"rank":{"type":"integer","format":"int64"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index 22e1808268ec1c7b349141ce999d2e4b668cb45c..952a64c78269b8003d0635a30820fdd15932e689 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -12,6 +12,7 @@ export type { ExceptionResponse } from './models/ExceptionResponse'; export type { LeaderboardDTO } from './models/LeaderboardDTO'; export type { LeaderboardEntryDTO } from './models/LeaderboardEntryDTO'; export type { LoginRequest } from './models/LoginRequest'; +export type { PasswordResetDTO } from './models/PasswordResetDTO'; export type { ProfileDTO } from './models/ProfileDTO'; export type { SignUpRequest } from './models/SignUpRequest'; export type { UserDTO } from './models/UserDTO'; @@ -19,4 +20,4 @@ export type { UserUpdateDTO } from './models/UserUpdateDTO'; export { AuthenticationService } from './services/AuthenticationService'; export { LeaderboardService } from './services/LeaderboardService'; -export { UserControllerService } from './services/UserControllerService'; +export { UserService } from './services/UserService'; diff --git a/src/api/models/LeaderboardEntryDTO.ts b/src/api/models/LeaderboardEntryDTO.ts index 9a93b2ff579409b4fa78077ba9f9cb47befc465d..6a7dc1d7c916baae5ea0b496ac69ef08792f4e98 100644 --- a/src/api/models/LeaderboardEntryDTO.ts +++ b/src/api/models/LeaderboardEntryDTO.ts @@ -6,5 +6,6 @@ import type { UserDTO } from './UserDTO'; export type LeaderboardEntryDTO = { user?: UserDTO; score?: number; + rank?: number; }; diff --git a/src/api/services/UserControllerService.ts b/src/api/services/UserService.ts similarity index 53% rename from src/api/services/UserControllerService.ts rename to src/api/services/UserService.ts index 5a1ea3560cb016455a4d2c77c2e34e9b0754a684..d4bec02c95c05faf669d7d0663da47d34ff67771 100644 --- a/src/api/services/UserControllerService.ts +++ b/src/api/services/UserService.ts @@ -2,15 +2,55 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { PasswordResetDTO } from '../models/PasswordResetDTO'; import type { ProfileDTO } from '../models/ProfileDTO'; import type { UserDTO } from '../models/UserDTO'; import type { UserUpdateDTO } from '../models/UserUpdateDTO'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class UserControllerService { +export class UserService { /** - * Update profile + * Initiate a password reset + * Send a password reset mail to the user with the specified email + * @returns any Successfully initiated a password reset + * @throws ApiError + */ + public static resetPassword({ + requestBody, + }: { + requestBody: string, + }): CancelablePromise<any> { + return __request(OpenAPI, { + method: 'POST', + url: '/api/users/reset-password', + body: requestBody, + mediaType: 'text/plain', + }); + } + /** + * Confirm a password reset + * Confirms a password reset using a token and a new password + * @returns void + * @throws ApiError + */ + public static confirmPasswordReset({ + requestBody, + }: { + requestBody: PasswordResetDTO, + }): CancelablePromise<void> { + return __request(OpenAPI, { + method: 'POST', + url: '/api/users/confirm-password', + body: requestBody, + mediaType: 'application/json', + errors: { + 403: `Invalid token`, + }, + }); + } + /** + * Update a profile * Update the profile of the authenticated user * @returns UserDTO Successfully updated profile * @throws ApiError @@ -28,8 +68,8 @@ export class UserControllerService { }); } /** - * Get profile - * Get user profile + * Get a profile + * Get the profile of a user * @returns ProfileDTO Successfully got profile * @throws ApiError */ @@ -47,8 +87,8 @@ export class UserControllerService { }); } /** - * Get user - * Get user information + * Get the authenticated user + * Get all user information for the authenticated user * @returns UserDTO Successfully got user * @throws ApiError */ 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/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/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/items/pigcoin.png b/src/assets/items/pigcoin.png new file mode 100644 index 0000000000000000000000000000000000000000..302e7e190ca30d4868094fb733fc1b8f2a438a55 Binary files /dev/null and b/src/assets/items/pigcoin.png differ diff --git a/src/components/Buttons/ShopButton.vue b/src/components/Buttons/ShopButton.vue index 4e28fcdb6e68eb87602d970bd0d612a46270668d..4ed5e02632d891b47f6752b8e870987b3e178edc 100644 --- a/src/components/Buttons/ShopButton.vue +++ b/src/components/Buttons/ShopButton.vue @@ -1,5 +1,5 @@ <template> - <button type="button" class="btn btn-primary" id="buttonStyle"><img src="@/assets/items/v-buck.png" style="width: 2rem"> +{{ buttonText }}</button> + <button type="button" class="btn btn-primary" id="buttonStyle"><img src="@/assets/items/pigcoin.png" style="width: 2rem"> +{{ buttonText }}</button> </template> <script> diff --git a/src/components/LeaderboardComponents/Leaderboard.vue b/src/components/LeaderboardComponents/Leaderboard.vue index 0a1b9a5b472971d79cac4aa62380e891caa72ec7..9003a25a4c37050a30eafc00cb1551c4885b0dc7 100644 --- a/src/components/LeaderboardComponents/Leaderboard.vue +++ b/src/components/LeaderboardComponents/Leaderboard.vue @@ -1,161 +1,197 @@ <template> - <div id="leaderboard"> - <div class="ribbon"></div> - <table> - <tr v-for="(entry, index) in leaderboard" :key="entry.user.id"> - <td class="number">{{ index + 1 }}</td> + <div id="leaderboard"> + <div class="ribbon"></div> + <table> + <tbody> + <tr v-for="(entry, index) in leaderboard" :key="entry.user.id" :class="{ 'is-user-5': entry.user.firstName === 'User' }"> + <td class="number">{{ entry.rank }}</td> <td class="name" @click="navigateToUserProfile(entry.user.id)">{{ entry.user.firstName }}</td> <td class="points" v-if="index === 0"> {{ entry.score }} - <div class = "medal"> - <img class="gold-medal" src="https://github.com/malunaridev/Challenges-iCodeThis/blob/master/4-leaderboard/assets/gold-medal.png?raw=true" alt="gold medal" /> + <div class="medal"> + <img class="gold-medal" + src="https://github.com/malunaridev/Challenges-iCodeThis/blob/master/4-leaderboard/assets/gold-medal.png?raw=true" + alt="gold medal" /> </div> - </td> - <td v-else class="points">{{ entry.score }}</td> + </td> + <td v-else class="points">{{ entry.score }}</td> </tr> - </table> - </div> - </template> - - <script setup lang="ts"> - import { ref } from 'vue'; - import { useRouter } from 'vue-router'; - - const router = useRouter(); - - const props = defineProps({ - leaderboard: { - type: Array, - required: true - } - }); - - const navigateToUserProfile = () => { - router.push({ name: 'news' }); - }; - </script> - - <style scoped> - #leaderboard { - width: 100%; - position: relative; - } - - table { - width: 100%; - border-collapse: collapse; - table-layout: fixed; - color: #141a39; - cursor: default; - } - - tr { - transition: all 0.2s ease-in-out; - border-radius: 0.2rem; - display: flex; - align-items: center; - justify-content: space-between; - height: 4rem; - } - - tr:not(:first-child):hover { - background-color: #fff; - transform: scale(1.1); - -webkit-box-shadow: 0px 5px 15px 8px #e4e7fb; - box-shadow: 0px 5px 15px 8px #e4e7fb; - } - - tr:nth-child(even) { - background-color: #f9f9f9; - } - - tr:nth-child(1) { - color: #fff; + </tbody> + <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' }"> + <td class="number">{{ entry.rank }}</td> + <td class="name" @click="navigateToUserProfile(entry.user.id)">{{ entry.user.firstName }}</td> + <td class="points">{{ entry.score }}</td> + </tr> + </tbody> + <tbody v-else></tbody> + </table> + </div> +</template> + + +<script setup lang="ts"> +import { computed } from 'vue'; +import { useRouter } from 'vue-router'; +import { useUserInfoStore } from '@/stores/UserStore'; + +const router = useRouter(); +const userStore = useUserInfoStore(); + +const props = defineProps({ + leaderboard: { + type: Array, + required: true + }, + leaderboardExtra: { + type: Array, + required: true + } +}); + +console.log(props.leaderboardExtra); + +const userInLeaderboard = computed(() => props.leaderboard.some(entry => entry.user.email === userStore.email)); + +const navigateToUserProfile = () => { + router.push({ name: 'user-profile' }); +}; +</script> + +<style scoped> +#leaderboard { + width: 100%; + position: relative; +} + +table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + color: #141a39; + cursor: default; +} + +tr { + transition: all 0.2s ease-in-out; + border-radius: 0.2rem; + display: flex; + align-items: center; + justify-content: space-between; + height: 4rem; +} + +tr:not(:first-child):hover { + background-color: #fff; + transform: scale(1.1); + -webkit-box-shadow: 0px 5px 15px 8px #e4e7fb; + box-shadow: 0px 5px 15px 8px #e4e7fb; +} + +tr:nth-child(even) { + background-color: #f9f9f9; +} + +tr:nth-child(1) { + color: #fff; +} + +td { + height: 2rem; + font-family: "Rubik", sans-serif; + font-size: 1.4rem; + padding: 1rem 2rem; + position: relative; +} + +.number { + width: 1rem; + font-size: 2.2rem; + font-weight: bold; + text-align: left; + display: flex; + align-items: center; +} + +.name { + font-size: 1.3rem; + cursor: pointer; + display: flex; + align-items: center; +} + +.points { + font-weight: bold; + font-size: 1.3rem; + display: flex; + justify-content: flex-end; + align-items: center; +} + +@media (max-width: 1000px) { + .number .name .points { + font-size: 0.5rem; } - + td { - height: 2rem; - font-family: "Rubik", sans-serif; - font-size: 1.4rem; - padding: 1rem 2rem; - position: relative; - } - - .number { - width: 1rem; - font-size: 2.2rem; - font-weight: bold; - text-align: left; - display: flex; - align-items: center; - } - - .name { - font-size: 1.3rem; - cursor: pointer; - display: flex; - align-items: center; - } - - .points { - font-weight: bold; - font-size: 1.3rem; - display: flex; - justify-content: flex-end; - align-items: center; + padding: 0.2rem 0.5rem; } +} - @media (max-width: 1000px) { - .number .name .points { - font-size: 0.5rem; - } - td { - padding: 0.2rem 0.5rem; - } - } +.points:first-child { + width: 10rem; +} - - .points:first-child { - width: 10rem; - } - - .gold-medal { - height: 3rem; - margin-left: 1.5rem; - } - - .ribbon { - width: 106%; - height: 4.5rem; - top: -0.5rem; - background-color: #0A58CA; - position: absolute; - /**left: -1rem;*/ - box-shadow: 0px 15px 11px -6px #7a7a7d; - } - - .ribbon::before { - content: ""; - height: 1.5rem; - width: 1.5rem; - bottom: -0.8rem; - left: 0.35rem; - transform: rotate(45deg); - background-color: #0A58CA; - position: absolute; - z-index: -1; - } - - .ribbon::after { - content: ""; - height: 1.5rem; - width: 1.5rem; - bottom: -0.8rem; - right: 0.35rem; - transform: rotate(45deg); - background-color: #0A58CA; - position: absolute; - z-index: -1; - } - </style> \ No newline at end of file +.gold-medal { + height: 3rem; + margin-left: 1.5rem; +} + +.ribbon { + width: 106%; + height: 4.5rem; + top: -0.5rem; + background-color: #0A58CA; + position: absolute; + /**left: -1rem;*/ + box-shadow: 0px 15px 11px -6px #7a7a7d; +} + +.ribbon::before { + content: ""; + height: 1.5rem; + width: 1.5rem; + bottom: -0.8rem; + left: 0.35rem; + transform: rotate(45deg); + background-color: #0A58CA; + position: absolute; + z-index: -1; +} + +.ribbon::after { + content: ""; + height: 1.5rem; + width: 1.5rem; + bottom: -0.8rem; + right: 0.35rem; + transform: rotate(45deg); + background-color: #0A58CA; + position: absolute; + z-index: -1; +} + +#line { + width: 100%; + height: 0.01rem; + border-top: 8px solid #0A58CA; +} + +tr.is-user-5 { + background-color: #419c5c !important; + color: #fff !important; +} +</style> \ No newline at end of file diff --git a/src/components/Login/Login.vue b/src/components/Login/Login.vue index 25469a19d2cbd3c352545218fe480114ae8c6181..834e1b7e7e3c7c62076dbfdb0c3897abc3f1daaa 100644 --- a/src/components/Login/Login.vue +++ b/src/components/Login/Login.vue @@ -3,11 +3,29 @@ import LoginForm from '@/components/Login/LoginForm.vue' </script> <template> - <div class="container-fluid"> - <LoginForm/> + <div class="containers"> + <div class="box"> + <LoginForm/> + </div> </div> </template> -<style> +<style scoped> + .containers { + background-color: #A2CC99; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + } + .box { + background-color: white; + border-radius: 3rem; + max-width: 450px; + padding: 1rem 4rem; + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; + } + + </style> \ No newline at end of file diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue index 6091ab82d28b5c081b081370cd01996d78d327d2..3401af96d2977c1f508c15d2dc47e3e9748a2c6d 100644 --- a/src/components/Login/LoginForm.vue +++ b/src/components/Login/LoginForm.vue @@ -93,6 +93,7 @@ const handleSubmit = async () => { valid-message="Valid password" invalid-message="Password must be between 4 and 16 characters and contain one capital letter, small letter and a number" /> + <p>Forgotten password? <RouterLink to="/forgotten-password">Reset password</RouterLink></p> <p class="text-danger">{{ errorMsg }}</p> <button1 id="confirmButton" type="submit" @click="handleSubmit" button-text="Login"></button1> diff --git a/src/components/SignUp/SignUp.vue b/src/components/SignUp/SignUp.vue index 4dff39dcd4bbe2a47e1471473d7a1632eb540827..eb272c6f1966c45c9d61045a1375fc8dcfb14981 100644 --- a/src/components/SignUp/SignUp.vue +++ b/src/components/SignUp/SignUp.vue @@ -4,9 +4,27 @@ import SignUpForm from '@/components/SignUp/SignUpForm.vue' </script> <template> - <SignUpForm/> + <div class="containers"> + <div class="box"> + <SignUpForm /> + </div> + </div> </template> <style scoped> +.containers { + background-color: #A2CC99; + height: 100vh; + display: flex; + justify-content: center; +} +.box { + width: 450px; + margin: 2rem; + background-color: white; + border-radius: 3rem; + padding: 1rem 4rem; + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; +} </style> \ No newline at end of file diff --git a/src/components/SignUp/SignUpForm.vue b/src/components/SignUp/SignUpForm.vue index dad760e62b52dab00c22e31e5c63dba26ad0c0bb..b0b04ad2309aeaa1c82d1d86e651838a98decc68 100644 --- a/src/components/SignUp/SignUpForm.vue +++ b/src/components/SignUp/SignUpForm.vue @@ -68,6 +68,7 @@ const handleSubmit = async () => { <template> <div class="container"> + <img src="@/assets/Sparesti-logo.png" style="width: 120px"> <form ref="formRef" id="signUpForm" @submit.prevent="handleSubmit" novalidate> <BaseInput :model-value=firstNameRef @input-change-event="handleFirstNameInputEvent" @@ -124,12 +125,17 @@ const handleSubmit = async () => { .container { max-width: 450px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; } #signUpForm { display: flex; flex-direction: column; justify-items: center; + width: 100%; } #firstNameInput, #surnameInput, #emailInput, #passwordInput, #confirmButton, #confirmPasswordInput { diff --git a/src/components/UpdateUserComponents/UpdateUserLayout.vue b/src/components/UpdateUserComponents/UpdateUserLayout.vue new file mode 100644 index 0000000000000000000000000000000000000000..511ece58743fbe4502419a4646f6247c680d2530 --- /dev/null +++ b/src/components/UpdateUserComponents/UpdateUserLayout.vue @@ -0,0 +1,326 @@ +<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"; + +const firstNameRef = ref() +const surnameRef = ref('') +const emailRef = ref('') +const passwordRef = ref('') +const confirmPasswordRef = ref('') +const formRef = ref() +let samePasswords = ref(true) + +async function setupForm() { + try { + let response = await UserService.getUser(); + console.log(response.firstName) + + firstNameRef.value = response.firstName; + if (response.lastName != null) { + surnameRef.value = response.lastName; + } + if (response.email != null) { + emailRef.value = response.email + } + } catch (err) { + console.error(err) + } +} + + + +const handleFirstNameInputEvent = (newValue: any) => { + firstNameRef.value = newValue +} + + +const handleSurnameInputEvent = (newValue: any) => { + surnameRef.value = newValue +} + +const handleEmailInputEvent = (newValue: any) => { + emailRef.value = newValue +} + +const handlePasswordInputEvent = (newValue: any) => { + passwordRef.value = newValue +} + +const handleConfirmPasswordInputEvent = (newValue: any) => { + confirmPasswordRef.value = newValue +} + +const handleSubmit = async () => { + + samePasswords.value = (passwordRef.value === confirmPasswordRef.value) + console.log(samePasswords.value) + formRef.value.classList.add("was-validated") + const form = formRef.value; + + const updateUserPayload: UserUpdateDTO = { + firstName: firstNameRef.value, + lastName: surnameRef.value, + email: emailRef.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'); + } + +} +onMounted(() => { + setupForm() +}) + + + +</script> + +<template> + <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> + <h5 class="user-name">Yuki Hayashi</h5> + <h6 class="user-email">yuki@Maxwell.com</h6> + </div> + </div> + </div> + </div> + </div> + <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="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 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="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> + </div> + </div> + </div> + </div> + +</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 h5.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 e36115715e64b79691d773ea44286d46558620fd..db2c4bca1ccc4af534c3a5aacd5dd162657ffaec 100644 --- a/src/components/UserProfile/UserProfileLayout.vue +++ b/src/components/UserProfile/UserProfileLayout.vue @@ -2,7 +2,8 @@ import Menu from "@/components/BaseComponents/Menu.vue"; import Footer from "@/components/BaseComponents/Footer.vue"; -import {useRouter} from "vue-router"; +import { useRouter } from "vue-router"; +import { useUserInfoStore } from "../../stores/UserStore"; let numberOfHistory = 6; @@ -12,109 +13,119 @@ let points = 0; let streak = 0; let route = useRouter() -function toRoadmap(){ +function toRoadmap() { route.push('/roadmap') } + +function toUpdateUserSettings() { + route.push('/update-user') +} </script> <template> - <div class="container text-center"> - <div class="row"> - <div class="col"> - <img src="/src/assets/userprofile.png" class="img-fluid"> - <p class="h2">Username</p> - <p><a class="link-dark" href="#">Edit profile</a></p> - </div> - </div> - <div class="row"> - <div class="col"> - <img src="/src/assets/icons/fire.png" class="img-fluid" style="width: 30px; height: 30px" alt="dollar"> - <p>Streak: 10</p> - </div> - </div> - <div class="row"> - <div class="col-12"> - <img src="/src/assets/icons/dollar.png" class="img-fluid" style="width: 30px; height: 30px" alt="dollar"> - <p class="">Points: 2000 </p> - </div> - </div> - - <div class="row"> - <div class="col"> - total points earned - </div> - <div class="col"> - total badges earned - </div> - </div> - <div class="row"> - <div class="col"> - <!-- Here is the badges of the user --> - <div class="container-fluid"> - <h1 class="mt-5 text-start badges-text">Badges</h1> - <div class="scrolling-wrapper-badges row flex-row flex-nowrap mt-4 pb-4 pt-2"> - - <div class="col-5"> - <div class="card badges-block card-1"></div> + <div class="container py-5 h-100"> + <div class="row d-flex justify-content-center align-items-center h-100"> + <div class="col 12"> + <div class="card"> + <div class="rounded-top text-white d-flex flex-row bg-primary" style="height:200px;"> + <div class="ms-4 mt-5 d-flex flex-column" style="width: 150px;"> + <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"> + Edit profile + </button> </div> - <div class="col-5"> - <div class="card badges-block card-2"></div> + <div class="ms-3" style="margin-top: 130px;"> + <h1>Andy Horwitz</h1> </div> - <div class="col-5"> - <div class="card badges-block card-3"></div> - </div> - <div class="col-5"> - <div class="card badges-block card-4"></div> - </div> - <div class="col-5"> - <div class="card badges-block card-5"></div> - </div> - <div class="col-5"> - <div class="card badges-block card-6"></div> - </div> - <div class="col-5"> - <div class="card badges-block card-7"></div> - </div> - <div class="col-5"> - <div class="card badges-block card-8"></div> - </div> - <div class="col-5"> - <div class="card badges-block card-9"></div> - </div> - <div class="col-5"> - <div class="card badges-block card-10"></div> + </div> + <div class="p-4 text-black" style="background-color: #f8f9fa;"> + <div class="d-flex justify-content-end text-center py-1"> + <div> + <p class="mb-1 h2">253 <img src="@/assets/items/pigcoin.png" style="width: 4rem"></p> + <p class="small text-muted mb-0">Points</p> + </div> + <div class="px-3"> + <p class="mb-1 h2">1026 <img src="@/assets/icons/fire.png" style="width: 4rem"></p> + <p class="small text-muted mb-0">Streak</p> + </div> </div> </div> - </div> - </div> - </div> - <div class="row"> - <div class="col"> - <!-- Here is the history of saving target --> - <div class="container-fluid mb-5"> - <h1 class="mt-5 text-start history-text">History</h1> - <div class="row scrolling-wrapper-history"> - <div v-for="index in numberOfHistory" :key="index" class="col-md-4 col-sm-4 col-lg-4 col-xs-4 col-xl-4 control-label"> - <div class="card history-block" > - <div class="card mb-3" style="max-width: 540px;"> - <div class="row g-0"> - <div class="col-md-4"> - <img src="/src/assets/icons/piggybank.svg" class="img-fluid rounded-start h-40 mx-auto d-none d-md-block" alt="..."> + <div class="card-body p-1 text-black"> + <div class="row"> + <div class="col"> + <div class="container-fluid"> + <h1 class="mt-5 text-start badges-text">Badges</h1> + <div class="scrolling-wrapper-badges row flex-row flex-nowrap mt-4 pb-4 pt-2"> + + <div class="col-5"> + <div class="card badges-block card-1"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-2"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-3"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-4"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-5"></div> </div> - <div class="col-md-8"> - <div class="card-body"> - <h5 class="card-title">{{cardTitles[index-1]}}</h5> - <p class="card-text">Money saved: 200 <br/>You are one challenge: 21</p> - <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p> - <a href="#" class="btn stretched-link" @click="toRoadmap"></a> + <div class="col-5"> + <div class="card badges-block card-6"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-7"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-8"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-9"></div> + </div> + <div class="col-5"> + <div class="card badges-block card-10"></div> + </div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col"> + <!-- Here is the history of saving target --> + <div class="container-fluid mb-5"> + <h1 class="mt-5 text-start history-text">History</h1> + <div class="row scrolling-wrapper-history"> + <div v-for="index in numberOfHistory" :key="index" + class="col-md-4 col-sm-4 col-lg-4 col-xs-4 col-xl-4 control-label"> + <div class="card history-block"> + <div class="card mb-3" style="max-width: 540px;"> + <div class="row g-0"> + <div class="col-md-4"> + <img src="/src/assets/icons/piggybank.svg" + class="img-fluid rounded-start h-40 mx-auto d-none d-md-block" alt="..."> + </div> + <div class="col-md-8"> + <div class="card-body"> + <h5 class="card-title">{{ cardTitles[index - 1] }}</h5> + <p class="card-text">Money saved: 200 <br />You are one challenge: 21</p> + <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p> + <a href="#" class="btn stretched-link" @click="toRoadmap"></a> + </div> + </div> + </div> + </div> </div> </div> </div> + </div> </div> </div> </div> - </div> </div> </div> @@ -122,28 +133,28 @@ function toRoadmap(){ </template> <style scoped> -.scrolling-wrapper-badges{ +.scrolling-wrapper-badges { overflow-x: auto; } -.scrolling-wrapper-history{ +.scrolling-wrapper-history { max-height: 300px; overflow: auto; } -.badges-text{ +.badges-text { font-weight: 500; font-size: 2.0em; } -.history-text{ +.history-text { font-weight: 500; font-size: 2.0em; } -.badges-block{ +.badges-block { height: 200px; background-color: #fff; border: none; @@ -151,14 +162,15 @@ function toRoadmap(){ background-size: cover; transition: all 0.2s ease-in-out !important; border-radius: 24px; - &:hover{ + + &:hover { transform: translateY(-5px); box-shadow: none; opacity: 0.9; } } -.history-block{ +.history-block { height: 200px; background-color: #fff; @@ -168,61 +180,68 @@ function toRoadmap(){ transition: all 0.2s ease-in-out !important; border-radius: 24px; margin: 20px; - &:hover{ + + &:hover { transform: translateY(-5px); box-shadow: none; opacity: 0.9; } } -.card-1{ +.card-1 { background-color: #4158D0; background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%); } -.card-2{ +.card-2 { background-color: #0093E9; background-image: linear-gradient(160deg, #0093E9 0%, #80D0C7 100%); } -.card-3{ +.card-3 { background-color: #00DBDE; background-image: linear-gradient(90deg, #00DBDE 0%, #FC00FF 100%); } -.card-4{ +.card-4 { background-color: #FBAB7E; background-image: linear-gradient(62deg, #FBAB7E 0%, #F7CE68 100%); } -.card-5{ +.card-5 { background-color: #85FFBD; background-image: linear-gradient(45deg, #85FFBD 0%, #FFFB7D 100%); } -.card-6{ +.card-6 { background-color: #FA8BFF; background-image: linear-gradient(45deg, #FA8BFF 0%, #2BD2FF 52%, #2BFF88 90%); } -.card-7{ +.card-7 { background-color: #FA8BFF; background-image: linear-gradient(45deg, #FA8BFF 0%, #2BD2FF 52%, #2BFF88 90%); } -.card-8{ +.card-8 { background-color: #FBDA61; background-image: linear-gradient(45deg, #FBDA61 0%, #FF5ACD 100%); } -.card-9{ +.card-9 { background-color: #4158D0; background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%); } -.card-10{ +.card-10 { background-color: #FF3CAC; background-image: linear-gradient(225deg, #FF3CAC 0%, #784BA0 50%, #2B86C5 100%); } + + +/*-------*/ +.rounded-top { + background-color: #00DBDE; +} </style> \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 170e39ea9491fc3e2defbab475f4ae3a74b41080..70bd7ba69d8236b5ae338cec2490cb7c47ed272e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -4,6 +4,7 @@ import LoginView from '../views/Authentication/LoginView.vue'; import { useUserInfoStore } from '@/stores/UserStore'; import UserProfileView from "@/views/User/UserProfileView.vue"; import SignUp from '@/components/SignUp/SignUp.vue' +import UpdateUserView from "@/views/UpdateUser/UpdateUserView.vue"; const routes = [ @@ -33,6 +34,16 @@ const routes = [ name: 'test', component: () => import('@/views/TestView.vue'), }, + { + path: '/profile', + name: 'profile', + component: UserProfileView + }, + { + path: 'update-user', + name: 'update-user', + component: UpdateUserView + }, { path: 'roadmap', name: 'roadmap', diff --git a/src/views/Authentication/ChangePasswordView.vue b/src/views/Authentication/ChangePasswordView.vue index ad52ab1b300380fa1c665e882c88b83f217424ac..36e2e75e365f0f9bb01d81155533be0fa48e2ec0 100644 --- a/src/views/Authentication/ChangePasswordView.vue +++ b/src/views/Authentication/ChangePasswordView.vue @@ -1,61 +1,86 @@ <template> - <div class="container"> - <div class="row justify-content-center"> - <div class="col-lg-5"> - <div class="card shadow-lg border-0 rounded-lg mt-5"> - <div class="card-header"><h3 class="text-center font-weight-light my-4">Password Recovery</h3></div> - <div class="card-body"> - <div class="small mb-3 text-muted">Enter the new password for your account</div> - <form @submit.prevent="submitForm"> - <div class="form-floating mb-3"> - <input v-model="newPassword" class="form-control" id="newPassword" type="password" placeholder="New Password" required> - <label for="newPassword">Enter your new password</label> + <div class="containers"> + <div class="row justify-content-center"> + <div class="col-lg-5"> + <div class="card shadow-lg border-0 rounded-lg mt-5"> + <div class="card-header"> + <h3 class="text-center font-weight-light my-4">Password Recovery</h3> + </div> + <div class="card-body"> + <div class="small mb-3 text-muted">Enter the new password for your account</div> + <form @submit.prevent="submitForm"> + <div class="form-floating mb-3"> + <input v-model="newPassword" class="form-control" id="newPassword" type="password" + placeholder="New Password" required> + <label for="newPassword">Enter your new password</label> + </div> + <div class="form-floating mb-3"> + <input v-model="confirmPassword" class="form-control" id="confirmPassword" + type="password" placeholder="Confirm Password" required> + <label for="confirmPassword">Confirm your new password</label> + </div> + <div class="errorMsg">{{ errormsg }}</div> + <div class="d-flex align-items-center justify-content-between mt-4 mb-0"> + <router-link to="/login" class="small">Return to login</router-link> + <button class="btn btn-primary" type="submit">Confirm Password</button> + </div> + </form> + </div> + <div class="card-footer text-center py-3"> + <div class="small"><router-link to="/sign-up">Need an account? Sign up!</router-link></div> + </div> </div> - <div class="form-floating mb-3"> - <input v-model="confirmPassword" class="form-control" id="confirmPassword" type="password" placeholder="Confirm Password" required> - <label for="confirmPassword">Confirm your new password</label> - </div> - <div class="d-flex align-items-center justify-content-between mt-4 mb-0"> - <router-link to="/login" class="small">Return to login</router-link> - <button class="btn btn-primary" type="submit">Confirm Password</button> - </div> - </form> </div> - <div class="card-footer text-center py-3"> - <div class="small"><router-link to="/sign-up">Need an account? Sign up!</router-link></div> - </div> - </div> </div> - </div> </div> - </template> - - <script setup lang="ts"> - import { ref } from 'vue'; - import { useRouter } from 'vue-router'; - import axios from 'axios'; - - const router = useRouter(); - - const newPassword = ref(''); - const confirmPassword = ref(''); - - const submitForm = async () => { +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import { useRouter, useRoute } from 'vue-router'; +import axios from 'axios'; +import { UserService } from '@/api'; + +const router = useRouter(); +const route = useRoute(); + +const token = route.params.token; + +const newPassword = ref(''); +const confirmPassword = ref(''); + +let errormsg = ref(''); + +const submitForm = async () => { if (newPassword.value !== confirmPassword.value) { - alert('Passwords do not match!'); - return; + errormsg.value = 'The passwords do not match'; + return; } + errormsg.value = ''; try { - const response = await axios.post('/api/reset-password', { - password: newPassword.value, - confirmPassword: confirmPassword.value - }); - console.log('Success:', response.data); + const resetPassword = { + password: newPassword.value, + token: token + }; + const response = await UserService.confirmPasswordReset({ requestBody: resetPassword }); + console.log(response); router.push('/login'); } catch (error) { - console.error('Error:', error); + console.error('Error:', error); + } +}; + +</script> + +<style scoped> + .containers { + width: 100%; + background-color: #A2CC99; + height: 100vh; + } + + .row { + margin-right: 0px; + margin-left: 0px; } - }; - - </script> - \ No newline at end of file +</style> \ No newline at end of file diff --git a/src/views/Authentication/ForgottenPasswordView.vue b/src/views/Authentication/ForgottenPasswordView.vue index 521c2aa54b52169ae720d94d9862f8464cb10ebf..07501b6f6593a120c171fc99bc72a7bbf0bfe893 100644 --- a/src/views/Authentication/ForgottenPasswordView.vue +++ b/src/views/Authentication/ForgottenPasswordView.vue @@ -1,52 +1,72 @@ <template> - <div class="container"> - <div class="row justify-content-center"> - <div class="col-lg-5"> - <div class="card shadow-lg border-0 rounded-lg mt-5"> - <div class="card-header"><h3 class="text-center font-weight-light my-4">Password Recovery</h3></div> - <div class="card-body"> - <div class="small mb-3 text-muted">Enter your email address and we will send you a link to reset your password.</div> - <form @submit.prevent="submitForm"> - <div class="form-floating mb-3"> - <input v-model="email" class="form-control" id="inputEmail" type="email" placeholder="name@example.com" required> - <label for="inputEmail">Enter email address</label> + <div class="containers"> + <div class="row justify-content-center"> + <div class="col-lg-5"> + <div class="card shadow-lg border-0 rounded-lg mt-5"> + <div class="card-header"> + <h3 class="text-center font-weight-light my-4">Password Recovery</h3> + </div> + <div class="card-body"> + <div class="small mb-3 text-muted">Enter your email address and we will send you a link to reset + your password.</div> + <form @submit.prevent="submitForm"> + <div class="form-floating mb-3"> + <input v-model="email" class="form-control" id="inputEmail" type="email" + placeholder="name@example.com" required> + <label for="inputEmail">Enter email address</label> + </div> + <div class="d-flex align-items-center justify-content-between mt-4 mb-0"> + <router-link to="/login" class="small">Return to login</router-link> + <button class="btn btn-primary" type="submit">Reset Password</button> + </div> + <div class="text-success"> + {{ confirmationMessage }} + </div> + </form> + </div> + <div class="card-footer text-center py-3"> + <div class="small"><router-link to="/sign-up">Need an account? Sign up!</router-link></div> + </div> </div> - <div class="d-flex align-items-center justify-content-between mt-4 mb-0"> - <router-link to="/login" class="small">Return to login</router-link> - <button class="btn btn-primary" type="submit">Reset Password</button> - </div> - <div class="text-success"> - {{ confirmationMessage }} - </div> - </form> - </div> - <div class="card-footer text-center py-3"> - <div class="small"><router-link to="/sign-up">Need an account? Sign up!</router-link></div> </div> - </div> </div> - </div> </div> - </template> - - <script setup lang="ts"> - import { ref } from 'vue'; - import { useRouter } from 'vue-router'; - import axios from 'axios'; - - const router = useRouter(); - const email = ref(''); +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import { useRouter, useRoute } from 'vue-router'; +import axios from 'axios'; +import { UserService } from '@/api'; + +const router = useRouter(); + +const email = ref(''); +let confirmationMessage = ref(''); - let confirmationMessage = ref(''); - - const submitForm = async () => { +const submitForm = async () => { try { - const response = await axios.post('/api/password-reset', { email: email.value }); - console.log('Success:', response.data); - confirmationMessage.value = 'An email has been sent to your email address with a link to reset your password.'; + const response = await UserService.resetPassword({ + requestBody: email.value + }); + console.log('Success:', response.data); + confirmationMessage.value = 'An email has been sent to your email address with a link to reset your password.'; } catch (error) { - console.error('Error:', error); + console.error('Error:', error); + } +}; + +</script> + +<style scoped> + .containers { + width: 100%; + background-color: #A2CC99; + height: 100vh; + } + + .row { + margin-right: 0px; + margin-left: 0px; } - }; - </script> - \ No newline at end of file +</style> \ No newline at end of file diff --git a/src/views/LeaderboardView.vue b/src/views/LeaderboardView.vue index 769c67973bae2d8b15612e38a1141279d845ed3e..16b4ae404a28b1fa484d981980f072ce8fa8824d 100644 --- a/src/views/LeaderboardView.vue +++ b/src/views/LeaderboardView.vue @@ -17,16 +17,16 @@ </div> <main> <div id="leaderboard"> - <h1><img src="@/assets/items/v-buck.png" style="width: 2rem"> Total points</h1> - <Leaderboard :leaderboard="pointsLeaderboardData" @navigateToUserProfile="navigateToUserProfile" /> + <h1><img src="@/assets/items/pigcoin.png" style="width: 3rem"> Total points</h1> + <Leaderboard :leaderboard="pointsLeaderboardData" :leaderboardExtra="pointsLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> </div> <div id="leaderboard"> <h1><img src="@/assets/icons/fire.png" style="width: 2rem"> Current streak</h1> - <Leaderboard :leaderboard="currentLeaderboardData" @navigateToUserProfile="navigateToUserProfile" /> + <Leaderboard :leaderboard="currentLeaderboardData" :leaderboardExtra="currentLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> </div> <div id="leaderboard"> <h1><img src="@/assets/icons/fire.png" style="width: 2rem"> Highest streak</h1> - <Leaderboard :leaderboard="streakLeaderboardData" @navigateToUserProfile="navigateToUserProfile" /> + <Leaderboard :leaderboard="streakLeaderboardData" :leaderboardExtra="streakLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> </div> </main> </div> @@ -41,12 +41,16 @@ import { onMounted, ref } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import Leaderboard from '@/components/LeaderboardComponents/Leaderboard.vue'; import { on } from 'events'; -import { LeaderboardService } from '@/api'; +import { LeaderboardService, UserControllerService } from '@/api'; let streakLeaderboardData = ref([]); let currentLeaderboardData = ref([]); let pointsLeaderboardData = ref([]); +let streakLeaderboardDataExtra = ref([]); +let currentLeaderboardDataExtra = ref([]); +let pointsLeaderboardDataExtra = ref([]); + const router = useRouter(); async function fetchQuizData() { @@ -73,23 +77,26 @@ async function global() { let globalPointsYou = await LeaderboardService.getSurrounding({ type: "TOTAL_POINTS", filter: "GLOBAL", + entryCount: 2, }); let globalStreakYou = await LeaderboardService.getSurrounding({ type: "TOP_STREAK", filter: "GLOBAL", + entryCount: 2, }); let globalCurrentStreakYou = await LeaderboardService.getSurrounding({ type: "CURRENT_STREAK", filter: "GLOBAL", + entryCount: 2, }); - - console.log(globalPointsYou); - console.log(globalStreakYou); - console.log(globalCurrentStreakYou); - + pointsLeaderboardData.value = globalPoints.entries; currentLeaderboardData.value = globalCurrentStreak.entries; streakLeaderboardData.value = globalStreak.entries; + + pointsLeaderboardDataExtra.value = globalPointsYou.entries; + currentLeaderboardDataExtra.value = globalCurrentStreakYou.entries; + streakLeaderboardDataExtra.value = globalStreakYou.entries; } async function friends() { @@ -105,9 +112,30 @@ async function friends() { type: "CURRENT_STREAK", filter: "FRIENDS", }); + let friendsPointsYou = await LeaderboardService.getSurrounding({ + type: "TOTAL_POINTS", + filter: "FRIENDS", + entryCount: 2, + }); + let friendsStreakYou = await LeaderboardService.getSurrounding({ + type: "TOP_STREAK", + filter: "FRIENDS", + entryCount: 2, + }); + let friendsCurrentStreakYou = await LeaderboardService.getSurrounding({ + type: "CURRENT_STREAK", + filter: "FRIENDS", + entryCount: 2, + }); + + pointsLeaderboardData.value = friendsPoints.entries; currentLeaderboardData.value = friendsCurrentStreak.entries; streakLeaderboardData.value = friendsStreak.entries; + + pointsLeaderboardDataExtra.value = friendsPointsYou.entries; + currentLeaderboardDataExtra.value = friendsStreakYou.entries; + streakLeaderboardDataExtra.value = friendsCurrentStreakYou.entries; } const navigateToUserProfile = (userId: number) => { @@ -121,7 +149,7 @@ main { width: 80%; display: flex; justify-content: space-around; - align-items: center; + align-items: start; flex-wrap: wrap; flex-direction: row; } @@ -138,7 +166,6 @@ main { #content { display: flex; flex-direction: row; - justify-content: center; flex-wrap: wrap; } @@ -156,7 +183,6 @@ h1 { display: flex; justify-content: center; margin-bottom: 2rem; - } #radioContainer { diff --git a/src/views/ShopView.vue b/src/views/ShopView.vue index 2bf2eafe93dff64150ce1365535defa33e28fa01..86c22ee13bdac2ab935a2bb0dce6c1712aeab55d 100644 --- a/src/views/ShopView.vue +++ b/src/views/ShopView.vue @@ -73,14 +73,14 @@ <img src="@/assets/items/adfree.png" class="card-img-top" alt="..."> <div class="card-body"> <h5 class="card-title">Adfree</h5> - <ShopButton button-text="35kr"></ShopButton> + <button type="button" class="btn btn-primary" id="buttonStyle"> +35kr</button> </div> </div> <div class="card text-center" style="width: 16rem; border: none"> <img src="@/assets/items/piggybank.webp" class="card-img-top" alt="..."> <div class="card-body"> <h5 class="card-title">Premium</h5> - <ShopButton button-text="50kr"></ShopButton> + <button type="button" class="btn btn-primary" id="buttonStyle">+50kr</button> </div> </div> </div> diff --git a/src/views/UpdateUser/UpdateUserView.vue b/src/views/UpdateUser/UpdateUserView.vue new file mode 100644 index 0000000000000000000000000000000000000000..aae915c1e1225dca1b3f6ac4b34e5b987c765515 --- /dev/null +++ b/src/views/UpdateUser/UpdateUserView.vue @@ -0,0 +1,12 @@ +<script setup lang="ts"> + +import UpdateUserLayout from "@/components/UpdateUserComponents/UpdateUserLayout.vue"; +</script> + +<template> +<UpdateUserLayout></UpdateUserLayout> +</template> + +<style scoped> + +</style> \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index 4b1c897997739635a6e14248a6448b67b2703c44..f5088576a014873999241d0a3d0fe0313984f2b3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,7 +8,10 @@ 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' + } } }) )