diff --git a/spec.json b/spec.json index d22821d7fd6ecd38fdd014cc2859ec7fcbe472f1..aaf7188fa567a4aabafc50d37f247be3de855e4c 100644 --- a/spec.json +++ b/spec.json @@ -79,8 +79,8 @@ } ], "responses": { - "200": { - "description": "Friend request successfully accepted", + "404": { + "description": "Friend request not found", "content": { "*/*": { "schema": { @@ -89,8 +89,8 @@ } } }, - "404": { - "description": "Friend request not found", + "200": { + "description": "Friend request successfully accepted", "content": { "*/*": { "schema": { @@ -120,8 +120,8 @@ } ], "responses": { - "200": { - "description": "Friend successfully deleted or friend request cancelled", + "404": { + "description": "Friend or friend request not found", "content": { "*/*": { "schema": { @@ -130,8 +130,8 @@ } } }, - "404": { - "description": "Friend or friend request not found", + "200": { + "description": "Friend successfully deleted or friend request cancelled", "content": { "*/*": { "schema": { @@ -162,8 +162,8 @@ "required": true }, "responses": { - "200": { - "description": "No accounts associated with a bank user", + "404": { + "description": "Bank profile id does not exist", "content": { "*/*": { "schema": { @@ -172,8 +172,8 @@ } } }, - "404": { - "description": "Bank profile id does not exist", + "200": { + "description": "No accounts associated with a bank user", "content": { "*/*": { "schema": { @@ -204,8 +204,8 @@ "required": true }, "responses": { - "200": { - "description": "Successfully created a bank profile", + "400": { + "description": "Could not create profile", "content": { "*/*": { "schema": { @@ -214,8 +214,8 @@ } } }, - "400": { - "description": "Could not create profile", + "200": { + "description": "Successfully created a bank profile", "content": { "*/*": { "schema": { @@ -246,8 +246,8 @@ "required": true }, "responses": { - "200": { - "description": "Successfully created account", + "404": { + "description": "Provided bank profile id could not be found", "content": { "*/*": { "schema": { @@ -256,8 +256,8 @@ } } }, - "404": { - "description": "Provided bank profile id could not be found", + "200": { + "description": "Successfully created account", "content": { "*/*": { "schema": { @@ -339,11 +339,11 @@ "required": true }, "responses": { - "204": { - "description": "Password was reset successfully" - }, "403": { "description": "Invalid token" + }, + "204": { + "description": "Password was reset successfully" } }, "security": [] @@ -368,8 +368,8 @@ "required": true }, "responses": { - "200": { - "description": "Successfully updated notification", + "500": { + "description": "User is not found", "content": { "*/*": { "schema": { @@ -378,8 +378,8 @@ } } }, - "500": { - "description": "User is not found", + "200": { + "description": "Successfully updated notification", "content": { "*/*": { "schema": { @@ -411,12 +411,6 @@ } ], "responses": { - "201": { - "description": "Item purchased and added to inventory successfully", - "content": { - "application/json": {} - } - }, "403": { "description": "Insufficient points to purchase the item", "content": { @@ -426,6 +420,14 @@ } } } + }, + "201": { + "description": "Item purchased and added to inventory successfully", + "content": { + "application/json": { + + } + } } } } @@ -545,9 +547,6 @@ "required": true }, "responses": { - "200": { - "description": "Successfully updated the challenge" - }, "401": { "description": "Day is already completed or day outside of range", "content": { @@ -557,6 +556,9 @@ } } } + }, + "200": { + "description": "Successfully updated the challenge" } } } @@ -642,8 +644,8 @@ "required": true }, "responses": { - "200": { - "description": "Successfully updated budget", + "500": { + "description": "Budget is not found", "content": { "application/json": { "schema": { @@ -652,8 +654,8 @@ } } }, - "500": { - "description": "Budget is not found", + "200": { + "description": "Successfully updated budget", "content": { "application/json": { "schema": { @@ -695,8 +697,8 @@ "required": true }, "responses": { - "200": { - "description": "Successfully updated budget", + "500": { + "description": "Error updating expense", "content": { "application/json": { "schema": { @@ -705,8 +707,8 @@ } } }, - "500": { - "description": "Error updating expense", + "200": { + "description": "Successfully updated budget", "content": { "application/json": { "schema": { @@ -769,22 +771,22 @@ } ], "responses": { - "200": { - "description": "Email is valid", + "409": { + "description": "Email already exists", "content": { "*/*": { "schema": { - "type": "object" + "$ref": "#/components/schemas/ExceptionResponse" } } } }, - "409": { - "description": "Email already exists", + "200": { + "description": "Email is valid", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/ExceptionResponse" + "type": "object" } } } @@ -812,22 +814,22 @@ "required": true }, "responses": { - "201": { - "description": "Successfully signed up", + "409": { + "description": "Email already exists", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AuthenticationResponse" + "$ref": "#/components/schemas/ExceptionResponse" } } } }, - "409": { - "description": "Email already exists", + "201": { + "description": "Successfully signed up", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExceptionResponse" + "$ref": "#/components/schemas/AuthenticationResponse" } } } @@ -1110,8 +1112,8 @@ } ], "responses": { - "200": { - "description": "No accounts associated with a bank user", + "404": { + "description": "Bank profile id does not exist", "content": { "*/*": { "schema": { @@ -1123,8 +1125,8 @@ } } }, - "404": { - "description": "Bank profile id does not exist", + "200": { + "description": "No accounts associated with a bank user", "content": { "*/*": { "schema": { @@ -1739,8 +1741,8 @@ } ], "responses": { - "200": { - "description": "Successfully got budget", + "500": { + "description": "Budget is not found", "content": { "application/json": { "schema": { @@ -1749,8 +1751,8 @@ } } }, - "500": { - "description": "Budget is not found", + "200": { + "description": "Successfully got budget", "content": { "application/json": { "schema": { @@ -1861,8 +1863,8 @@ } ], "responses": { - "200": { - "description": "Successfully deleted budget", + "500": { + "description": "Budget is not found", "content": { "application/json": { "schema": { @@ -1871,8 +1873,8 @@ } } }, - "500": { - "description": "Budget is not found", + "200": { + "description": "Successfully deleted budget", "content": { "application/json": { "schema": { @@ -1972,8 +1974,8 @@ } ], "responses": { - "200": { - "description": "Successfully got budget", + "500": { + "description": "Badge is not found", "content": { "application/json": { "schema": { @@ -1982,8 +1984,8 @@ } } }, - "500": { - "description": "Badge is not found", + "200": { + "description": "Successfully got budget", "content": { "application/json": { "schema": { @@ -2234,6 +2236,18 @@ } } }, + "BankAccountResponseDTO": { + "type": "object", + "properties": { + "bban": { + "type": "integer", + "format": "int64" + }, + "balance": { + "type": "number" + } + } + }, "ChallengeDTO": { "type": "object", "properties": { @@ -2346,6 +2360,19 @@ } } }, + "PointDTO": { + "type": "object", + "properties": { + "currentPoints": { + "type": "integer", + "format": "int32" + }, + "totalEarnedPoints": { + "type": "integer", + "format": "int32" + } + } + }, "ProgressDTO": { "type": "object", "properties": { @@ -2366,6 +2393,35 @@ } } }, + "StreakDTO": { + "type": "object", + "properties": { + "currentStreak": { + "type": "integer", + "format": "int32" + }, + "currentStreakCreatedAt": { + "type": "string", + "format": "date-time" + }, + "currentStreakUpdatedAt": { + "type": "string", + "format": "date-time" + }, + "highestStreak": { + "type": "integer", + "format": "int32" + }, + "highestStreakCreatedAt": { + "type": "string", + "format": "date-time" + }, + "highestStreakEndedAt": { + "type": "string", + "format": "date-time" + } + } + }, "UserDTO": { "type": "object", "properties": { @@ -2383,6 +2439,10 @@ "type": "integer", "format": "int64" }, + "bannerImage": { + "type": "integer", + "format": "int64" + }, "email": { "type": "string" }, @@ -2395,6 +2455,18 @@ }, "subscriptionLevel": { "type": "string" + }, + "checkingAccountBBAN": { + "$ref": "#/components/schemas/BankAccountResponseDTO" + }, + "savingsAccountBBAN": { + "$ref": "#/components/schemas/BankAccountResponseDTO" + }, + "point": { + "$ref": "#/components/schemas/PointDTO" + }, + "streak": { + "$ref": "#/components/schemas/StreakDTO" } } }, @@ -2606,9 +2678,19 @@ "type": "integer", "format": "int64" }, + "bannerImage": { + "type": "integer", + "format": "int64" + }, "createdAt": { "type": "string", "format": "date-time" + }, + "point": { + "$ref": "#/components/schemas/PointDTO" + }, + "streak": { + "$ref": "#/components/schemas/StreakDTO" } } }, diff --git a/src/App.vue b/src/App.vue index d32330ffb583d487044a1e59f4703d7072a35ba5..d80fae0b5000eaab01e06674f5084b3da957048d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,13 +1,13 @@ <script setup lang="ts"> import { RouterView } from 'vue-router' -//import ErrorBoundaryCatcher from '@/components/Exceptions/ErrorBoundaryCatcher.vue'; +import ErrorBoundaryCatcher from '@/components/Exceptions/ErrorBoundaryCatcher.vue'; </script> <template> <main> - <error-boundary-catcher> + <ErrorBoundaryCatcher> <RouterView /> - </error-boundary-catcher> + </ErrorBoundaryCatcher> </main> </template> diff --git a/src/api/index.ts b/src/api/index.ts index 4ab22c2112459939cb385110bf7011d567d948a2..cfeb244eadd5c15b0d35dcb2b438c22b5e9a2ab6 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -13,6 +13,7 @@ export type { AccountResponseDTO } from './models/AccountResponseDTO'; export type { AuthenticationResponse } from './models/AuthenticationResponse'; export type { BadgeDTO } from './models/BadgeDTO'; export type { BankAccountDTO } from './models/BankAccountDTO'; +export type { BankAccountResponseDTO } from './models/BankAccountResponseDTO'; export type { BankIDRequest } from './models/BankIDRequest'; export type { BankProfile } from './models/BankProfile'; export type { BankProfileDTO } from './models/BankProfileDTO'; @@ -38,9 +39,11 @@ export type { MarkChallengeDTO } from './models/MarkChallengeDTO'; export { NotificationDTO } from './models/NotificationDTO'; export type { PasswordResetDTO } from './models/PasswordResetDTO'; export type { PasswordUpdateDTO } from './models/PasswordUpdateDTO'; +export type { PointDTO } from './models/PointDTO'; export type { ProfileDTO } from './models/ProfileDTO'; export type { ProgressDTO } from './models/ProgressDTO'; export type { SignUpRequest } from './models/SignUpRequest'; +export type { StreakDTO } from './models/StreakDTO'; export type { TransactionDTO } from './models/TransactionDTO'; export type { UserDTO } from './models/UserDTO'; export type { UserUpdateDTO } from './models/UserUpdateDTO'; diff --git a/src/api/models/BankAccountResponseDTO.ts b/src/api/models/BankAccountResponseDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed03efb50ae0c92f6556f22171663b4ef04d0d94 --- /dev/null +++ b/src/api/models/BankAccountResponseDTO.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type BankAccountResponseDTO = { + bban?: number; + balance?: number; +}; + diff --git a/src/api/models/PointDTO.ts b/src/api/models/PointDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..a122a85f12c3ade9a3d68966565295ce1ddd8d38 --- /dev/null +++ b/src/api/models/PointDTO.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type PointDTO = { + currentPoints?: number; + totalEarnedPoints?: number; +}; + diff --git a/src/api/models/ProfileDTO.ts b/src/api/models/ProfileDTO.ts index 4afa17a7faad4191cabd0e5be6450a80507779c1..7a621da3b05b3203bfb202cb74aa4ff9e95d954c 100644 --- a/src/api/models/ProfileDTO.ts +++ b/src/api/models/ProfileDTO.ts @@ -2,11 +2,16 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { PointDTO } from './PointDTO'; +import type { StreakDTO } from './StreakDTO'; export type ProfileDTO = { id?: number; firstName?: string; lastName?: string; profileImage?: number; + bannerImage?: number; createdAt?: string; + point?: PointDTO; + streak?: StreakDTO; }; diff --git a/src/api/models/StreakDTO.ts b/src/api/models/StreakDTO.ts new file mode 100644 index 0000000000000000000000000000000000000000..07822694f7855d8d7dd631accaa57c71723ee7f9 --- /dev/null +++ b/src/api/models/StreakDTO.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type StreakDTO = { + currentStreak?: number; + currentStreakCreatedAt?: string; + currentStreakUpdatedAt?: string; + highestStreak?: number; + highestStreakCreatedAt?: string; + highestStreakEndedAt?: string; +}; + diff --git a/src/api/models/UserDTO.ts b/src/api/models/UserDTO.ts index 2020ee7c36cf2f4a1a8304f61f398d76c8d5cda2..f3426ff96a2e26940a922007b6ddfea0e5b9042c 100644 --- a/src/api/models/UserDTO.ts +++ b/src/api/models/UserDTO.ts @@ -2,14 +2,22 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { BankAccountResponseDTO } from './BankAccountResponseDTO'; +import type { PointDTO } from './PointDTO'; +import type { StreakDTO } from './StreakDTO'; export type UserDTO = { id?: number; firstName?: string; lastName?: string; profileImage?: number; + bannerImage?: number; email?: string; createdAt?: string; role?: string; subscriptionLevel?: string; + checkingAccountBBAN?: BankAccountResponseDTO; + savingsAccountBBAN?: BankAccountResponseDTO; + point?: PointDTO; + streak?: StreakDTO; }; diff --git a/src/assets/loadingPig.png b/src/assets/loadingPig.png new file mode 100644 index 0000000000000000000000000000000000000000..d92b61b238d8684b57208a2bc94b951598e320ca Binary files /dev/null and b/src/assets/loadingPig.png differ diff --git a/src/assets/savingPigRun.png b/src/assets/savingPigRun.png new file mode 100644 index 0000000000000000000000000000000000000000..d7ad033c8220621dbb2f34547c8925387adbc17d Binary files /dev/null and b/src/assets/savingPigRun.png differ diff --git a/src/components/BaseComponents/NavBar.vue b/src/components/BaseComponents/NavBar.vue index 62cb230905705022153d26ddd20449d532782f4f..f99af99d8db075c5862baf6fc57d55f86bcd4fcd 100644 --- a/src/components/BaseComponents/NavBar.vue +++ b/src/components/BaseComponents/NavBar.vue @@ -56,22 +56,20 @@ </router-link> </li> </ul> - <ul v-else class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <ul v-else-if="notificationListRef.length === 0" class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <li>Ingen varslinger</li> </ul> </li> <li v-if="userStore.isLoggedIn" class="nav-item dropdown"> - - <a - data-cy="user" - :class="['nav-link', 'dropdown-toggle', 'username-text', 'text-white', { 'underline-active': !isAnyActivePage() }]" - href="#" - role="button" - data-bs-toggle="dropdown" - aria-expanded="false"> - <img src="@/assets/icons/person.svg">{{ useUserInfoStore().firstname }} -</a> + data-cy="user" + :class="['nav-link', 'dropdown-toggle', 'username-text', 'text-white', { 'underline-active': !isAnyActivePage() }]" + href="#" + role="button" + data-bs-toggle="dropdown" + aria-expanded="false"> + <img src="@/assets/icons/person.svg">{{ useUserInfoStore().firstname }} + </a> <ul class="dropdown-menu dropdown-username-content"> @@ -158,10 +156,7 @@ import { useRouter, useRoute } from "vue-router"; import { useUserInfoStore } from '@/stores/UserStore'; import {onMounted, ref} from "vue"; -import { type NotificationDTO, NotificationService } from '@/api' -import { afterWrite } from '@popperjs/core' - - +import { BadgeService, type NotificationDTO, NotificationService } from '@/api' const router = useRouter(); const route = useRoute(); @@ -206,8 +201,10 @@ const notificationPathMapper: any = { "BADGE": "/profile", "COMPLETED_GOAL": "/roadmap" } + const getNotifications = async () => { try { + await BadgeService.updateUnlockedBadges(); notificationListRef.value = await NotificationService.getUnreadNotificationByUser() } catch (error) { notificationListRef.value = [] diff --git a/src/components/Exceptions/unkownErrorHandler.ts b/src/components/Exceptions/unkownErrorHandler.ts index 68ecb6e09a91141f60b4641f7f38854a5aa0ca42..e15b5b45d824f51ffc23c860ac394b80b598950f 100644 --- a/src/components/Exceptions/unkownErrorHandler.ts +++ b/src/components/Exceptions/unkownErrorHandler.ts @@ -1,5 +1,6 @@ import { ApiError as BackendApiError } from '@/api'; import { AxiosError } from 'axios'; +import router from '@/router' /** * Finds the correct error message for the given error @@ -11,6 +12,11 @@ const handleUnknownError = (error: any): string => { if (error instanceof AxiosError) { return error.code!!; } else if (error instanceof BackendApiError) { + if (error.body.status == 403) { + router.push("/login"); + } else if (error.body.status == 401) { + router.push("/roadmap"); + } return error.body.message ?? error.body; } return error; diff --git a/src/components/SavingGoal/SavingGoal.vue b/src/components/SavingGoal/SavingGoal.vue index 3644980546960ef148747fa116d97ccdade6a8a6..73b63403e45151d2dce1b24f6da6b60340f1d81e 100644 --- a/src/components/SavingGoal/SavingGoal.vue +++ b/src/components/SavingGoal/SavingGoal.vue @@ -2,18 +2,21 @@ import SavingGoalList from "@/components/SavingGoal/SavingGoalList.vue"; import SavingGoalRoadmap from "@/components/SavingGoal/SavingGoalRoadmap.vue"; import SavingGoalCreate from "@/components/SavingGoal/SavingGoalCreate.vue"; +import SavingGoalDefault from "@/components/SavingGoal/SavingGoalDefault.vue"; import type {GoalDTO} from "@/api"; import {GoalService} from "@/api"; export default { - components: {SavingGoalCreate, SavingGoalRoadmap, SavingGoalList}, + components: {SavingGoalDefault, SavingGoalCreate, SavingGoalRoadmap, SavingGoalList}, data() { return { bluePanelMaxHeight: 'auto' as string, - createClicked: true as boolean, + createClicked: false as boolean, + savingGoalClicked: false as boolean, selectedGoal: [] as any, createdGoal: [] as any, key: 0 as number, + keyForList: 0 as number, }; }, mounted() { @@ -26,7 +29,8 @@ export default { if (timelineElement instanceof HTMLElement) { // Calculate the max-height based on the height of the timeline const timelineHeight = timelineElement.offsetHeight; - this.bluePanelMaxHeight = '700px'; + console.log(timelineHeight) + this.bluePanelMaxHeight = (timelineHeight * 1.5)+'px'; } else { this.bluePanelMaxHeight = '700px'; } @@ -36,16 +40,24 @@ export default { }, async goToSavingGoal(savingGoal: GoalDTO) { this.$emit('goToSavingGoal', savingGoal); - let response = await GoalService.getGoal({id: savingGoal.id as number}); - console.log(response) - this.selectedGoal = response + this.selectedGoal = await GoalService.getGoal({id: savingGoal.id as number}) this.createClicked = false; + this.savingGoalClicked = true; this.key++ + setTimeout(() => { + this.calculateBluePanelMaxHeight() + }, 500); }, - createSavingGoal(savingGoal: GoalDTO) { - this.$emit('createSavingGoal', savingGoal) - this.createdGoal = savingGoal; - this.createClicked = false; + async handleCreateGoalClicked(savingGoal: GoalDTO) { + this.$emit('goToSavingGoal', savingGoal); + let response = await GoalService.getGoal({id: savingGoal.id as number}); + setTimeout(() => { + this.selectedGoal = response + this.createClicked = false; + this.key++ + this.savingGoalClicked = true; + this.keyForList++ + }, 100); } }, }; @@ -55,15 +67,16 @@ export default { <div class="cont"> <div class="row"> <div class="col-lg-4 blue-background overflow-scroll" :style="{ 'max-height': bluePanelMaxHeight }"> - <h3 style="color: white; margin-bottom: 16px">Your saving goals</h3> + <h2>Dine sparemål</h2> <div> - <button class="btn btn-success btn-lg" style="font-weight: 600; margin-bottom: 20px" @click="createGoal">Create new saving goal</button> + <button class="btn btn-success btn-lg" style="font-weight: 600; margin-bottom: 20px" @click="createGoal">+ Lag et nytt sparemål</button> </div> - <saving-goal-list @goToSavingGoal="goToSavingGoal"></saving-goal-list> + <saving-goal-list :key="keyForList" @goToSavingGoal="goToSavingGoal"></saving-goal-list> </div> <div class="spacer"/> - <saving-goal-create @createGoalClick="createSavingGoal" v-if="createClicked"></saving-goal-create> - <saving-goal-roadmap :key="key" :selected-goal="selectedGoal" v-else></saving-goal-roadmap> + <saving-goal-create @createGoalClicked="handleCreateGoalClicked" v-if="createClicked"></saving-goal-create> + <saving-goal-roadmap :key="key" :selected-goal="selectedGoal" v-else-if="savingGoalClicked"></saving-goal-roadmap> + <saving-goal-default v-else></saving-goal-default> </div> </div> </template> @@ -82,11 +95,18 @@ export default { padding: 12px; background-color: #003A58; width: 27%; - border-radius: 0 2em 2em 0; + border-radius: 0 1em 1em 0; } .spacer { width: 10%; background-color: transparent; } + +h2 { + color: white; + margin-bottom: 16px; + font-weight: 600; + +} </style> \ No newline at end of file diff --git a/src/components/SavingGoal/SavingGoalCreate.vue b/src/components/SavingGoal/SavingGoalCreate.vue index e5b1d5a99201c6b085e6c8e97cd3783da1dd5120..2fe1e2cdd1914d0792453c465ea2b8789076a673 100644 --- a/src/components/SavingGoal/SavingGoalCreate.vue +++ b/src/components/SavingGoal/SavingGoalCreate.vue @@ -1,6 +1,8 @@ <script setup lang="ts"> import {GoalService, type CreateGoalDTO, type GoalDTO} from "@/api" -import {ref} from "vue"; +import {ref, defineProps, defineEmits} from "vue"; + +const emits = defineEmits(['createGoalClicked']); const name = ref("") const desc = ref("") @@ -10,13 +12,6 @@ const createdConfirm = ref(""); const errorMessage = ref("") const createGoalClick = async () => { - console.log("create goal clicked") - console.log(name.value) - console.log(desc.value) - console.log(date.value) - console.log(amount.value) - - const createGoalPayload: CreateGoalDTO = { name: name.value, description: desc.value, @@ -28,8 +23,9 @@ const createGoalClick = async () => { try { let response = await GoalService.createGoal({ requestBody: createGoalPayload }); if(response.name != "") { - createdConfirm.value = "Your goal has been created! Refresh the page to se it in the list" + createdConfirm.value = "Your goal has been created!" errorMessage.value = "" + emits('createGoalClicked', response) } } catch (error: any) { console.log(error.message); @@ -41,36 +37,37 @@ const createGoalClick = async () => { <template> <div class="col-lg-8"> - <h1>Create a new saving goal!</h1> + <h1>Lag et nytt sparemål!</h1> <br> - <p>Create a name for this saving goal </p> + <p>Gi sparemålet et navn </p> <div class="input-group mb-3"> - <span class="input-group-text" id="basic-addon1">Name</span> - <input v-model="name" type="text" class="form-control" placeholder="Name of Saving Goal" aria-label="Username" aria-describedby="basic-addon1" required> + <span class="input-group-text" id="basic-addon1">Navn</span> + <input v-model="name" type="text" class="form-control" placeholder="Navn på sparemålet" + aria-label="Username" aria-describedby="basic-addon1" required> </div> - <p>Add a description to this saving goal </p> + <p>Legg til en beskrivelse for sparemålet </p> <div class="input-group mb-3"> - <span class="input-group-text" id="basic-addon2">Description</span> + <span class="input-group-text" id="basic-addon2">Beskrivelse</span> <textarea v-model="desc" class="form-control" aria-label="With textarea"></textarea> </div> <!--Change this to date picker?--> - <p>When should this saving goal end?</p> + <p>Når skal pengene være spart?</p> <div class="input-group mb-3"> - <input v-model="date" type="date" class="form-control" aria-label="Amount of days" placeholder="Amount of days (as number)" required> + <input v-model="date" type="date" class="form-control" aria-label="Amount of days" required> </div> - <p>How much do you want to save during this saving goal? </p> + <p>Hvor mye vil du spare?</p> <div class="input-group"> - <input v-model="amount" type="number" class="form-control" aria-label="" placeholder="NOK (as number)" required> + <input v-model="amount" type="number" class="form-control" aria-label="NOK" required> <span class="input-group-text">NOK</span> </div> <br> <button class="btn btn-primary btn-lg" @click="createGoalClick">Create goal!</button> - <div style="color: green; font-size: 32px"> + <div class="confirmMessage"> {{ createdConfirm }} </div> @@ -87,4 +84,10 @@ const createGoalClick = async () => { padding-right: 56px; padding-bottom: 28px; } + +.confirmMessage { + color: green; + font-size: 32px; + min-height: 100px; +} </style> \ No newline at end of file diff --git a/src/components/SavingGoal/SavingGoalDefault.vue b/src/components/SavingGoal/SavingGoalDefault.vue new file mode 100644 index 0000000000000000000000000000000000000000..788e7e44ad896fdc1d6c9a0ee879cebdbe0136e3 --- /dev/null +++ b/src/components/SavingGoal/SavingGoalDefault.vue @@ -0,0 +1,103 @@ +<script setup lang="ts"> + +</script> + +<template> + <section id="hero" class="hero section"> + <div class="col-lg-8"> + <div class="container text-center"> + <div class="d-flex flex-column justify-content-center align-items-center"> + <h1 class="">Velkommen til <span>SpareSti</span></h1> + <br> + <p class="">Kom i økonomisk form: Ta på deg våre spareutfordringer!<br></p> + <img src="../../assets/savingPigRun.png" alt="SpareSti-logo"> + </div> + </div> + </div> + </section> +</template> + +<style scoped> +.col-lg-8 { + width: 100%; + margin-top: 50px; + padding-right: 56px; + padding-bottom: 28px; +} + +.hero { + width: 63%; + min-height: 70vh; + overflow: hidden; +} + +.hero img { + inset: 0; + display: block; + width: 60%; + height: 60%; + object-fit: cover; + z-index: 1; +} + +.hero .container { + position: relative; + z-index: 3; +} + +.hero h1 { + margin: 0; + font-size: 56px; + font-weight: 700; + line-height: 56px; +} + +.hero h1 span { + color: white; + background-color: #003A58; + padding: 4px 24px 14px 24px; + border-radius: 6px; +} + + +.hero p { + color: #003A58; + margin: 5px 0 30px 0; + font-size: 28px; + font-weight: 400; +} + +.hero .btn-watch-video i { + color: #003A58; + font-size: 32px; + transition: 0.3s; + line-height: 0; + margin-right: 8px; +} + +.hero .btn-watch-video:hover i { + color: #003A58; +} + +@media (max-width: 640px) { + .hero h1 { + font-size: 28px; + line-height: 36px; + } + + .hero p { + font-size: 18px; + line-height: 24px; + margin-bottom: 30px; + } + + .hero img { + inset: 0; + display: block; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; + } +} +</style> \ No newline at end of file diff --git a/src/components/SavingGoal/SavingGoalList.vue b/src/components/SavingGoal/SavingGoalList.vue index 23604b4bea8ff280bb018c04811c900a44fb362b..975aa69196b6faca35be84f99823b1e4d3068fc8 100644 --- a/src/components/SavingGoal/SavingGoalList.vue +++ b/src/components/SavingGoal/SavingGoalList.vue @@ -1,15 +1,28 @@ <script setup lang="ts"> import {ref, onMounted} from "vue"; -import {GoalService, type GoalDTO, type CreateGoalDTO} from "@/api" +import {GoalService, type GoalDTO, type ChallengeDTO} from "@/api" -const savingGoalList = ref([] as any) - -defineProps() +const savingGoalList = ref(null as any) +const oldSavingGoalList = ref(null as any) const getGoals = async () => { try { let response = await GoalService.getGoals(); - savingGoalList.value = response + savingGoalList.value = [] + oldSavingGoalList.value = [] + const date = new Date() + response.forEach((goal: GoalDTO) => { + if(goal.targetDate) { + const targetDate = new Date(goal.targetDate) + if(targetDate < date) { + console.log("old") + oldSavingGoalList.value.push(goal) + } else { + console.log("current") + savingGoalList.value.push(goal) + } + } + }) } catch (error: any) { console.log(error.message); } @@ -26,19 +39,70 @@ const goToSavingGoal = (savingGoal: GoalDTO) => { const deleteSavingGoal = () => { }; + +function calculateSavedSoFar (goal: GoalDTO) { + console.log("hehe") + let savedSoFar = 0; // Reset savedSoFar before calculating again + if(goal.challenges){ + console.log("hehehe") + goal.challenges.forEach((challenge: ChallengeDTO) => { + // Check if progressList exists before accessing its elements + if (challenge.progressList) { + challenge.progressList.forEach((progress: any) => { + // Assuming 'amount' is the property you want to add from progressList + savedSoFar += progress.amount; + console.log("he") + }); + } + }); + } + return savedSoFar +} + +function formatDate(date: string) { + const date1 = new Date(date); + return date1.toISOString().split('T')[0] +} </script> <template> - <div v-for="(savingGoal, index) in savingGoalList" :key="index" class="card bg-body"> - <div class="card-header"> - Saving goal {{ index + 1 }} + <div v-if="savingGoalList"> + <div v-if="oldSavingGoalList.lenght > 0"> + <h3>Current:</h3> </div> - <div class="card-body"> - <h5 class="card-title">{{ savingGoal.goalName }}</h5> - <p class="card-text">{{ savingGoal.description }}</p> - <a href="#" class="btn btn-primary" @click="goToSavingGoal(savingGoal)">Go to saving goal</a> - <a href="#" class="btn btn-danger" @click="deleteSavingGoal" style="margin-left: 8px">Delete</a> + <div v-for="(savingGoal, index) in savingGoalList" :key="index" class="card bg-body"> + <div class="card-header"> + Sparemål {{ index + 1 }} + </div> + <div class="card-body"> + <h5 class="card-title">{{ savingGoal.name }}</h5> + <p class="card-text">{{ savingGoal.description }}</p> + <p class="card-text">Spart: {{calculateSavedSoFar(savingGoal)}}/{{savingGoal.targetAmount}} Kr</p> + <p class="card-text">Avsluttes: {{formatDate(savingGoal.targetDate)}} </p> + <a class="btn btn-primary" @click="goToSavingGoal(savingGoal)">Gå til sparemål</a> + <a class="btn btn-danger" @click="deleteSavingGoal" style="margin-left: 8px"> + <img src="../../assets/icons/trash-can.svg"> Slett</a> + </div> </div> + <div v-if="oldSavingGoalList.lenght > 0"> + <h3>Old:</h3> + <div v-for="(savingGoal, index) in oldSavingGoalList" :key="index" class="card bg-body"> + <div class="card-header"> + Sparemål {{ index + 1 }} + </div> + <div class="card-body"> + <h5 class="card-title">{{ savingGoal.name }}</h5> + <p class="card-text">{{ savingGoal.description }}</p> + <p class="card-text">{{calculateSavedSoFar(savingGoal)}}/{{savingGoal.targetAmount}}</p> + <a class="btn btn-primary" @click="goToSavingGoal(savingGoal)">Gå til sparemål</a> + <a class="btn btn-danger" @click="deleteSavingGoal" style="margin-left: 8px">Slett</a> + </div> + </div> + </div> + </div> + <div class="loading" v-else> + Laster... + <img src="../../assets/loadingPig.png" alt="loadingPig"> </div> </template> @@ -63,4 +127,19 @@ const deleteSavingGoal = () => { margin-bottom: 20px; border: none; } + +.loading { + color: white; + font-size: 40px; + font-weight: 500; +} + +.loading img { + width: 50%; + height: 50%; +} + +h3 { + color: white; +} </style> \ No newline at end of file diff --git a/src/components/SavingGoal/SavingGoalRoadmap.vue b/src/components/SavingGoal/SavingGoalRoadmap.vue index 65bbfb8876c1ed6f2b076ee56cbed0621f3a187f..54a8ffd8fdf1e8652fbe69791511e41a52441ae4 100644 --- a/src/components/SavingGoal/SavingGoalRoadmap.vue +++ b/src/components/SavingGoal/SavingGoalRoadmap.vue @@ -1,8 +1,9 @@ <script lang="ts"> import {CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from 'chart.js' import {Line} from 'vue-chartjs' -import type {ChallengeDTO, GoalDTO, MarkChallengeDTO} from "@/api"; +import type {ChallengeDTO, CreateGoalDTO, GoalDTO, MarkChallengeDTO} from "@/api"; import {GoalService} from '@/api' +import {useUserInfoStore} from "@/stores/UserStore"; ChartJS.register( CategoryScale, @@ -23,6 +24,8 @@ export default { return { image: 'https://th.bing.com/th/id/OIG3.NMbdxmKYKVnxYGLOa0Z0?w=1024&h=1024&rs=1&pid=ImgDetMain' as string, altImage: 'https://th.bing.com/th/id/OIG4.gVWUC.rwCb8faTNx31yU?w=1024&h=1024&rs=1&pid=ImgDetMain' as string, + failedImage: 'https://cdn-icons-png.flaticon.com/512/6659/6659895.png' as string, + successImage: 'https://static-00.iconduck.com/assets.00/checkmark-running-icon-1024x1024-aakqv1qi.png' as string, title: 'Spain trip' as string, bluePanelMaxHeight: 'auto' as string, roadmapSelected: true as boolean, @@ -50,12 +53,14 @@ export default { newPrice: 0, savedSoFar: 0 as number, currentChallengeIndex: 0, + feedback: "" as string, }; }, async mounted() { setTimeout(() => { this.findCurrentChallenge() this.disableAllChecksThatNotCurrent() + this.checkIfToAmbitious() this.togglePanel(this.selectedGoal.challenges[this.currentChallengeIndex]) this.calculateSavedSoFar() this.onLoadDisableChecks(this.selectedGoal) @@ -65,8 +70,18 @@ export default { }, computed: { computeImageFilter() { - return (challenge: ChallengeDTO) => { - return challenge ? 'none' : 'grayscale(100%)'; + return (challenge: any) => { + const today = new Date() + const startDate = new Date(challenge.startDate) + + // Check if the challenge is in the past or future + if (today < startDate) { + // Challenge is in the future, apply grayscale + return 'grayscale(100%)' + } else { + // Challenge is currently active, no grayscale + return 'none'; + } }; } }, @@ -115,7 +130,7 @@ export default { convertTemplateTextToChallengeText(challenge: ChallengeDTO) { let challengeText: any challengeText = challenge.challengeTemplate?.text - challengeText = challengeText.replace('{unit_amount}', challenge.challengeTemplate?.amount?.toString()) + challengeText = challengeText.replace('{unit_amount}', challenge.amount?.toString()) challengeText = challengeText.replace('{checkDays}', challenge.checkDays?.toString()) challengeText = challengeText.replace('{totalDays}', challenge.totalDays?.toString()) let totalAmount: any @@ -132,7 +147,7 @@ export default { calculateTotalAmountFromChallenges() { let totalAmountFromChallenges = 0 for (const challenge of this.selectedGoal.challenges) { - totalAmountFromChallenges += challenge.amount + totalAmountFromChallenges += challenge.amount * challenge.checkDays } return totalAmountFromChallenges }, @@ -231,6 +246,12 @@ export default { if (today >= startDate && today <= endDate) { this.currentChallengeIndex = index + } else { + if (today >= endDate) { + console.log("In the past") + } else { + console.log("In the future") + } } }) }, @@ -251,15 +272,10 @@ export default { }, calculateSavedSoFarPerChallengeInPercent(challenge: ChallengeDTO) { - let savedSoFarOnChallenge = 0 + let savedSoFarOnChallenge = this.calculateSavedSoFarPerChallenge(challenge) let targetAmount = 1 - challenge.progressList?.forEach(progress => { - if(progress.amount) { - savedSoFarOnChallenge += progress.amount - } - }) - if(challenge.amount) { - targetAmount = challenge.amount + if(challenge.amount && challenge.checkDays) { + targetAmount = challenge.amount * challenge.checkDays } return (savedSoFarOnChallenge / targetAmount) * 100 @@ -273,7 +289,54 @@ export default { } }) return savedSoFar - } + }, + + async updateUnitPrice (challenge: ChallengeDTO) { + const createGoalPayload: MarkChallengeDTO = { + id: challenge.id, + amount: this.newPrice + }; + try { + await GoalService.updateChallengeAmount({requestBody: createGoalPayload}) + } catch (e: any) { + console.log(e.message) + } + }, + + checkIfToAmbitious() { + let possibleSaving = this.calculateTotalAmountFromChallenges() + let wantedSaving = this.selectedGoal.targetAmount + + console.log(possibleSaving + "," + wantedSaving) + if(wantedSaving > possibleSaving) { + this.feedback = "Vi beundrer din ambisjon, men å oppnå den ettertraktede" + + " summen er ikke lett. Men disse utfordringene tar deg på god vei!" + } + }, + + getImageSource(challenge: ChallengeDTO) { + const today = new Date(); + const endDate = new Date(challenge.endDate as any); + + // Check if the challenge is in the past + if (today > endDate) { + // Challenge is in the past, return alternative image source + if(challenge.progressList) { + if(challenge.checkDays == challenge.progressList.length) { + return this.successImage + } else { + return this.failedImage; + } + } + } else { + // Challenge is currently active or in the future, return default image source + return this.image; + } + }, + + transferMoney(amount: number) { + //need users bank accounts + }, }, }; </script> @@ -298,17 +361,19 @@ export default { <ul class="timeline"> <li v-for="(challenge, index) in selectedGoal.challenges" :key="index" :class="{ 'timeline-inverted': index % 2 !== 0 }"> <div class="timeline-image z-1" @click="togglePanel(challenge)"> - <img class="circular-image" :src="challenge.showPanel ? altImage : image" :style="{ filter: computeImageFilter(challenge) }" alt=""> + <img class="circular-image" :src="challenge.showPanel ? altImage : getImageSource(challenge)" :style="{ filter: computeImageFilter(challenge) }" alt=""> </div> <div class="timeline-panel z-3" :id="'panel-' + index" v-show="challenge.showPanel"> <div class="timeline-heading"> + <h5 style="margin-top: 12px">{{challenge.points}}<img src="../../assets/items/pigcoin.png" alt="pig coint" style="width: 2rem"></h5> <h4>Utfordring {{ index +1 }}</h4> + <p style="font-size: 12px">{{formatDate(challenge.startDate)}} til {{formatDate(challenge.endDate)}}</p> <h4 class="subheading">{{convertTemplateTextToChallengeText(challenge)}}</h4> </div> <div class="timeline-body"> <br> <p> - Pris per enhet: {{challenge.challengeTemplate.amount}} kr <img src="../../assets/icons/edit-button.svg" alt="editIcon" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="exampleModal"> + Pris per enhet: {{challenge.amount}} kr <img src="../../assets/icons/edit-button.svg" alt="editIcon" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="exampleModal"> </p> <br> <div class="collapse" id="collapseExample" style="background-color: white; padding: 12px; border-radius: 5px"> @@ -319,11 +384,12 @@ export default { </div> </div> <br> - <button class="btn btn-success">Bekreft endring</button> + <button @click="updateUnitPrice(challenge)" class="btn btn-success">Bekreft endring</button> </div> <br> + <p>Spart: {{ calculateSavedSoFarPerChallenge(challenge)}} Kr</p> <div class="progress"> - <div class="progress-bar" role="progressbar" :style="{ width: calculateSavedSoFarPerChallengeInPercent(challenge) + '%' }" :aria-valuenow="calculateSavedSoFarPerChallengeInPercent(challenge)" aria-valuemin="0" aria-valuemax="100">{{ calculateSavedSoFarPerChallenge(challenge)}} Kr</div> + <div class="progress-bar" role="progressbar" :style="{ width: calculateSavedSoFarPerChallengeInPercent(challenge) + '%' }" :aria-valuenow="calculateSavedSoFarPerChallengeInPercent(challenge)" aria-valuemin="0" aria-valuemax="100"></div> </div> <br> <div class="checkbox-row"> @@ -341,6 +407,10 @@ export default { <div v-else> <div class="row"> + <div v-if="feedback != ''" class="feedbackBox"> + <h3>Oops!</h3> + <h5 class="">{{ feedback }}</h5> + </div> <div class="col-sm-3"> <div class="card-box tilebox-one"><i class="icon-layers float-right text-muted"></i> <h6 class="text-muted text-uppercase mt-0">Du ønsker å spare</h6> @@ -384,7 +454,7 @@ export default { margin-bottom:40px; padding-bottom: 10px; color: white; - border-radius: 1em; + border-radius: 20px; background-color: #003A58; } @@ -393,6 +463,7 @@ export default { padding:4px 0 0 0; margin-top:22px; list-style: none; + margin-bottom: 300px; } .timeline>li:nth-child(even) { @@ -428,7 +499,8 @@ export default { text-align: right; background-color: #003A58; border-radius: 1em; - margin-left: 110px; + margin-left: 100px; + color: white; } .timeline>li .timeline-panel:before { @@ -451,7 +523,6 @@ export default { left: 50%; border: 7px solid #003A58; border-radius: 100%; - background-color: #00ffff; box-shadow: 0 0 5px #00e1ff; width: 100px; height: 100px; @@ -469,7 +540,7 @@ export default { float: right; padding: 0 30px 20px 20px; text-align: left; - margin-right: 110px; + margin-right: 100px; } .timeline>li.timeline-inverted>.timeline-panel:before { @@ -491,7 +562,7 @@ export default { } .timeline .timeline-heading h4 { - margin-top:22px; + margin-top:0px; margin-bottom: 4px; padding:0; color: white; @@ -704,4 +775,11 @@ export default { margin-right: 10px; /* Adjust as needed */ } +.feedbackBox { + box-shadow: rgba(255, 0, 0, 0.2) 0px 7px 29px 0px; + border-radius: 1em; + margin-bottom: 40px; + margin-top: 10px; + padding: 12px; +} </style> \ No newline at end of file diff --git a/src/components/Shop/ItemShop.vue b/src/components/Shop/ItemShop.vue index 15f17c636da19d0e18deb7e1f627027ff310a7ac..da198f45b152a166fe6c70d2c1457d39eb1f1869 100644 --- a/src/components/Shop/ItemShop.vue +++ b/src/components/Shop/ItemShop.vue @@ -3,6 +3,9 @@ <br> <div id="dropdownContainer"> <h1 class="box">Butikk</h1> + <div> + <p class="mb-1 h2" data-cy="points">{{points}}<img src="@/assets/items/pigcoin.png" style="width: 4rem"></p> + </div> </div> <div class="container d-flex justify-content-center"> <div class="row col-md-10"> @@ -83,19 +86,32 @@ import { useUserInfoStore } from '@/stores/UserStore'; import { ItemService } from '@/api'; const products = ref([] as any); +const points = ref(); const getStore = async () => { - const response = await ItemService.getStore(); - products.value = response; - console.log(response); + try { + const response = await ItemService.getStore(); + products.value = response; + } catch (error) { + console.log(error); + } +} + +const getPoints = async () => { + try { + const response = await UserService.getUser(); + points.value = response.point?.currentPoints; + } catch (error) { + console.log(error); + } } const buyItem = async (itemId: number) => { try { const response = await ItemService.buyItem({ itemId: itemId }); console.log(response); - const responseStore = await ItemService.getStore(); - products.value = responseStore; + getStore(); + getPoints(); } catch (error) { console.log(error); } @@ -125,6 +141,7 @@ const buyNoAds = async () => { onMounted(() => { getStore(); + getPoints(); }) </script> @@ -164,6 +181,7 @@ onMounted(() => { justify-content: center; align-items: center; margin-bottom: 2rem; + flex-direction: column; } #background {