diff --git a/src/components/ContinueButtonComponent.vue b/src/components/ContinueButtonComponent.vue index b3572a1a04fe77c1f0505f5ab7cab9bf53e61b16..8905468de41b630dc9223c5256a1ab73402d1955 100644 --- a/src/components/ContinueButtonComponent.vue +++ b/src/components/ContinueButtonComponent.vue @@ -1,27 +1,26 @@ <template> - <button class="continue-button" @click="onClick">Fortsett</button> + <button + :class="{ 'opacity-60 cursor-not-allowed': disabled }" + :disabled="disabled" + @click="handleClick" + class="p-3 px-20 text-lg rounded-lg cursor-pointer transition-all font-bold bg-[var(--green)] hover:brightness-90 active:brightness-75" + > + Fortsett + </button> </template> -<script lang="ts"> -import { defineComponent } from 'vue' +<script setup lang="ts"> +import { defineProps, defineEmits } from 'vue' -export default defineComponent({ - name: 'ContinueButtonComponent', - setup(props, { emit }) { - const onClick = (event: Event) => { - emit('click', event) - } - - return { - onClick - } - } +const props = defineProps({ + disabled: Boolean }) -</script> -<style> -.continue-button { - @apply p-3 px-20 text-lg rounded-lg cursor-pointer transition-all font-bold - text-black bg-[var(--green)] hover:brightness-90 active:brightness-75; +const emit = defineEmits(['click']) + +const handleClick = () => { + if (!props.disabled) { + emit('click') + } } -</style> +</script> diff --git a/src/components/InteractiveSpare.vue b/src/components/InteractiveSpare.vue index 613b123f0e2832a21ddee0db3e508bd232636223..f63f7d51ba3de7ddcf106bddf38968323b3d5704 100644 --- a/src/components/InteractiveSpare.vue +++ b/src/components/InteractiveSpare.vue @@ -24,7 +24,7 @@ <script setup lang="ts"> -import { ref, defineProps, computed } from 'vue' +import { computed, defineProps, ref } from 'vue' import spareImageSrc from '@/assets/spare.png' interface Props { diff --git a/src/components/__tests__/ButtonAddGoalOrChallengeTest.spec.ts b/src/components/__tests__/ButtonAddGoalOrChallengeTest.spec.ts index 144e004d9fca598adc9c71298004944d5721c8af..e893c2abd810ffd27e9ee3c2d99ae792759bd837 100644 --- a/src/components/__tests__/ButtonAddGoalOrChallengeTest.spec.ts +++ b/src/components/__tests__/ButtonAddGoalOrChallengeTest.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest' +import { describe, expect, it } from 'vitest' import { mount } from '@vue/test-utils' import ButtonComponent from '@/components/ButtonAddGoalOrChallange.vue' // Adjust the import path as needed. diff --git a/src/components/__tests__/ContinueButtonTest.spec.ts b/src/components/__tests__/ContinueButtonTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9440d848118e74c4646c9dd43863800756f89cf4 --- /dev/null +++ b/src/components/__tests__/ContinueButtonTest.spec.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' + +describe('ContinueButtonComponent', () => { + it('renders correctly', () => { + const wrapper = mount(ContinueButtonComponent) + expect(wrapper.text()).toContain('Fortsett') + }) + + it('is disabled when the `disabled` prop is true', () => { + const wrapper = mount(ContinueButtonComponent, { + props: { disabled: true } + }) + const button = wrapper.find('button') + expect(button.attributes('disabled')).toBeDefined() + expect(button.classes()).toContain('opacity-60') + expect(button.classes()).toContain('cursor-not-allowed') + }) + + it('does not emit click event when disabled', async () => { + const wrapper = mount(ContinueButtonComponent, { + props: { disabled: true } + }) + await wrapper.trigger('click') + expect(wrapper.emitted()).not.toHaveProperty('click') + }) + + it('emits click event when not disabled', async () => { + const wrapper = mount(ContinueButtonComponent, { + props: { disabled: false } + }) + await wrapper.trigger('click') + expect(wrapper.emitted()).toHaveProperty('click') + }) +}) diff --git a/src/components/__tests__/HomeViewTest.spec.ts b/src/components/__tests__/HomeViewTest.spec.ts index 8d51d5d7d72355f883b1dabc6cbb4a44816800b9..b48a96bb1c9e244380fecc67461d4277305ae604 100644 --- a/src/components/__tests__/HomeViewTest.spec.ts +++ b/src/components/__tests__/HomeViewTest.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { mount } from '@vue/test-utils' import HomeView from '@/views/HomeView.vue' // Adjust the import path as needed. import anime from 'animejs' diff --git a/src/components/__tests__/InteractiveSpareTest.spec.ts b/src/components/__tests__/InteractiveSpareTest.spec.ts index 50351141febb49f6af92d3310d41b4de1ead3dfd..cc6ae09f86276592d787d69d15f655290fd68b46 100644 --- a/src/components/__tests__/InteractiveSpareTest.spec.ts +++ b/src/components/__tests__/InteractiveSpareTest.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest' +import { describe, expect, it } from 'vitest' import { mount } from '@vue/test-utils' import SpeechBubbleComponent from '@/components/InteractiveSpare.vue' // Adjust the import path as needed. diff --git a/src/router/index.ts b/src/router/index.ts index 60ec4bf746d83dab2e490dc232f26a061ad0b63c..0ed3db43f38e6df15f0ca65b633a60e4ca6e56b7 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -62,9 +62,17 @@ const router = createRouter({ { path: '/forsteSparemaal', name: 'firstSavingGoal', - component: () => import('@/views/FirstSavingGoalView.vue') + component: () => import('../views/FirstSavingGoalView.vue') + }, + { + path: '/forsteSpareutfordring', + name: 'firstSavingChallengde', + component: () => import('../views/FirstSavingChallengeView.vue') } - ] + ], + scrollBehavior(to, from, savedPosition) { + return { top: 0 } + } }) export default router diff --git a/src/services/authInterceptor.ts b/src/services/authInterceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..087f27d9bea3f2d2cd6b0dbe83e756f63204a386 --- /dev/null +++ b/src/services/authInterceptor.ts @@ -0,0 +1,48 @@ +import type { AxiosResponse } from 'axios' +import axios, { AxiosError } from 'axios' +import router from '@/router' + +const authInterceptor = axios.create({ + baseURL: 'http://localhost:8080' +}) + +authInterceptor.interceptors.request.use( + (config) => { + const accessToken = sessionStorage.getItem('accessToken') + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}` + } + return config + }, + (error: AxiosError) => { + return Promise.reject(error) + } +) + +authInterceptor.interceptors.response.use( + (response: AxiosResponse) => { + return response + }, + async (error) => { + const originalRequest = error.config + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + const refreshToken = localStorage.getItem('refreshToken') + axios + .post('/auth/renewToken', { refreshToken }) + .then((response) => { + sessionStorage.setItem('accessToken', response.data.accessToken) + authInterceptor.defaults.headers['Authorization'] = + `Bearer ${response.data.accessToken}` + return authInterceptor(originalRequest) + }) + .catch((err) => { + router.push({ name: 'login' }) + return Promise.reject(err) + }) + } + return Promise.reject(error) + } +) + +export default authInterceptor diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index f9a90d971d40dfa3cbafc91d8d5d3fa3622f1a4b..7d3b80eb1acea632670099dc8333161a71b47f71 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -1,8 +1,9 @@ import { ref } from 'vue' import { defineStore } from 'pinia' import type { User } from '@/types/user' -import axios, { AxiosError } from 'axios' import router from '@/router' +import type { AxiosError } from 'axios' +import axios from 'axios' export const useUserStore = defineStore('user', () => { const defaultUser: User = { @@ -14,53 +15,57 @@ export const useUserStore = defineStore('user', () => { const user = ref<User>(defaultUser) const errorMessage = ref<string>('') - async function register( + const register = async ( firstname: string, lastname: string, email: string, username: string, password: string - ) { - try { - const response = await axios.post(`http://localhost:8080/auth/register`, { + ) => { + await axios + .post(`http://localhost:8080/auth/register`, { firstName: firstname, //TODO rename all instances of firstname to firstName lastName: lastname, email: email, username: username, password: password }) + .then((response) => { + sessionStorage.setItem('accessToken', response.data.accessToken) + localStorage.setItem('refreshToken', response.data.refreshToken) - sessionStorage.setItem('accessToken', response.data.accessToken) - localStorage.setItem('refreshToken', response.data.refreshToken) + user.value.firstname = firstname + user.value.lastname = lastname + user.value.username = username - user.value.firstname = firstname - user.value.lastname = lastname - user.value.username = username - - await router.push('/') - } catch (error) { - const axiosError = error as AxiosError - errorMessage.value = (axiosError.response?.data as string) || 'An error occurred' - } + router.push('/') + }) + .catch((error) => { + const axiosError = error as AxiosError + errorMessage.value = (axiosError.response?.data as string) || 'An error occurred' + }) } - async function login(username: string, password: string) { - try { - const response = await axios.post(`http://localhost:8080/auth/login`, { + const login = async (username: string, password: string) => { + await axios + .post(`http://localhost:8080/auth/login`, { username: username, password: password }) + .then((response) => { + sessionStorage.setItem('accessToken', response.data.accessToken) + localStorage.setItem('refreshToken', response.data.refreshToken) - console.log(response) + user.value.firstname = response.data.firstName + user.value.lastname = response.data.lastName + user.value.username = response.data.username - sessionStorage.setItem('accessToken', response.data.accessToken) - localStorage.setItem('refreshToken', response.data.refreshToken) - user.value.username = username - await router.push('/') - } catch (error) { - const axiosError = error as AxiosError - errorMessage.value = (axiosError.response?.data as string) || 'An error occurred' - } + router.push('/') + }) + .catch((error) => { + const axiosError = error as AxiosError + errorMessage.value = (axiosError.response?.data as string) || 'An error occurred' + }) } return { diff --git a/src/views/ConfigFamiliarWithSavingsView.vue b/src/views/ConfigFamiliarWithSavingsView.vue index 76d60f4a520fa4a1c14af9f4a3710f0a4067a9eb..c7664c76fde01c1989200259eaf9a4801767a913 100644 --- a/src/views/ConfigFamiliarWithSavingsView.vue +++ b/src/views/ConfigFamiliarWithSavingsView.vue @@ -7,10 +7,10 @@ 'border-[var(--green)] border-4': selectedOption === 'litt', 'border-gray-300 border-2': selectedOption !== 'litt' }" - class="flex flex-col items-center justify-center w-64 h-64 p-2.5 cursor-pointer transition-colors rounded-lg hover:border-[var(--green)]-500" + class="flex flex-col items-center justify-center w-40 h-40 p-2 sm:w-64 sm:h-64 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" @click="selectOption('litt')" > - <img src="@/assets/nose.png" alt="Litt kjent" class="h-16 md:h-20" /> + <img src="@/assets/nose.png" alt="Pig nose" class="h-12 sm:h-1/3" /> <p class="mt-2 text-lg font-bold">Litt kjent</p> </div> <div @@ -18,10 +18,10 @@ 'border-[var(--green)] border-4': selectedOption === 'noe', 'border-gray-300 border-2': selectedOption !== 'noe' }" - class="flex flex-col items-center justify-center w-64 h-64 p-2.5 cursor-pointer transition-colors rounded-lg hover:border-[var(--green)]" + class="flex flex-col items-center justify-center w-40 h-40 p-2 sm:w-64 sm:h-64 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" @click="selectOption('noe')" > - <img src="@/assets/head.png" alt="Noe kjent" class="h-16 md:h-20" /> + <img src="@/assets/head.png" alt="Pig face" class="h-12 sm:h-1/3" /> <p class="mt-2 text-lg font-bold">Noe kjent</p> </div> <div @@ -29,16 +29,17 @@ 'border-[var(--green)] border-4': selectedOption === 'godt', 'border-gray-300 border-2': selectedOption !== 'godt' }" - class="flex flex-col items-center justify-center w-64 h-64 p-2.5 cursor-pointer transition-colors rounded-lg hover:border-[var(--green)]" + class="flex flex-col items-center justify-center w-40 h-40 p-2 sm:w-64 sm:h-64 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" @click="selectOption('godt')" > - <img src="@/assets/pig.png" alt="Godt kjent" class="h-16 md:h-20" /> + <img src="@/assets/pig.png" alt="Whole pig" class="h-12 sm:h-1/3" /> <p class="mt-2 text-lg font-bold">Godt kjent</p> </div> </div> <ContinueButtonComponent + :disabled="selectedOption === null" @click="onButtonClick" - class="px-10 py-3 text-2xl font-bold self-end" + class="px-10 py-3 text-2xl self-end mb-4 mt-0" ></ContinueButtonComponent> </div> </template> diff --git a/src/views/ConfigHabitChangeView.vue b/src/views/ConfigHabitChangeView.vue index 55080b49f42201a755844efdc213a5ef7c7e0c23..8f6497e4822f8e79439bc4049ca7918bc939fcfd 100644 --- a/src/views/ConfigHabitChangeView.vue +++ b/src/views/ConfigHabitChangeView.vue @@ -1,62 +1,63 @@ -<script setup lang="ts"> -import { ref } from 'vue' -import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' -import router from '@/router' - -const selectedOption = ref<string | null>(null) - -const selectOption = (option: string) => { - selectedOption.value = option -} - -const onButtonClick = () => { - router.push('/konfigurasjonSteg2') -} -</script> - <template> <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> - <h1 class="mb-16 text-4xl font-bold lg:mb-20"> + <h1 class="mb-8 text-2xl font-bold sm:mb-16 sm:text-4xl"> Hvor store vaneedringer er du villig til å gjøre? </h1> - <div class="grid grid-cols-1 gap-14 mb-20 md:grid-cols-3"> + <div class="grid grid-cols-1 gap-8 mb-16 sm:gap-14 sm:mb-20 md:grid-cols-3"> <div :class="{ 'border-[var(--green)] border-4': selectedOption === 'litt', 'border-gray-300 border-2': selectedOption !== 'litt' }" - class="flex flex-col items-center justify-center w-64 h-64 p-2.5 cursor-pointer transition-colors rounded-lg hover:border-[var(--green)]" + class="flex flex-col items-center justify-center w-40 h-40 p-2 sm:w-64 sm:h-64 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" @click="selectOption('litt')" > - <img src="@/assets/litt.png" alt="litt" class="object-contain h-1/3" /> - <p class="text-lg font-bold mt-2">Litt</p> + <img src="@/assets/litt.png" alt="Thumbs down emoji" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-md sm:text-lg font-bold">Litt</p> </div> <div :class="{ 'border-[var(--green)] border-4': selectedOption === 'passe', 'border-gray-300 border-2': selectedOption !== 'passe' }" - class="flex flex-col items-center justify-center w-64 h-64 p-2.5 cursor-pointer transition-colors rounded-lg hover:border-[var(--green)]" + class="flex flex-col items-center justify-center w-40 h-40 p-2 sm:w-64 sm:h-64 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" @click="selectOption('passe')" > - <img src="@/assets/passe.png" alt="passe" class="object-contain h-1/3" /> - <p class="text-lg font-bold mt-2">Passe</p> + <img src="@/assets/passe.png" alt="A little bit emoji" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-md sm:text-lg font-bold">Passe</p> </div> <div :class="{ 'border-[var(--green)] border-4': selectedOption === 'store', 'border-gray-300 border-2': selectedOption !== 'store' }" - class="flex flex-col items-center justify-center w-64 h-64 p-2.5 cursor-pointer transition-colors rounded-lg hover:border-[var(--green)]" + class="flex flex-col items-center justify-center w-40 h-40 p-2 sm:w-64 sm:h-64 transition-colors rounded-lg cursor-pointer hover:border-[var(--green)]" @click="selectOption('store')" > - <img src="@/assets/store.png" alt="store" class="object-contain h-1/3" /> - <p class="text-lg font-bold mt-2">Store</p> + <img src="@/assets/store.png" alt="Thumbs up emoji" class="h-12 sm:h-1/3" /> + <p class="mt-2 text-md sm:text-lg font-bold">Store</p> </div> </div> <ContinueButtonComponent + :disabled="selectedOption === null" @click="onButtonClick" - class="px-10 py-3 text-2xl font-bold self-end" + class="px-10 py-3 text-2xl self-end" ></ContinueButtonComponent> </div> </template> + +<script setup lang="ts"> +import { ref } from 'vue' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' + +const selectedOption = ref<string | null>(null) + +const selectOption = (option: string) => { + selectedOption.value = option +} + +const onButtonClick = () => { + router.push('/konfigurasjonSteg2') +} +</script> diff --git a/src/views/ConfigSpendingItemsAmountView.vue b/src/views/ConfigSpendingItemsAmountView.vue index a0f7dc530c8bf2987f1b79c0395a13e726f28c6a..8146317360f08c970b08df5a793addea913dfb10 100644 --- a/src/views/ConfigSpendingItemsAmountView.vue +++ b/src/views/ConfigSpendingItemsAmountView.vue @@ -1,50 +1,99 @@ +<template> + <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> + <h1 class="mb-8 lg:mb-12 text-4xl font-bold">Hvor mye bruker du per kjøp på ...</h1> + <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-6"> + <div class="flex flex-col items-center bg-white rounded-lg p-8 shadow-lg w-100"> + <div + v-for="(option, index) in options.slice(0, 3)" + :key="index" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.name }}</p> + <div class="flex items-center w-2/3"> + <input + type="text" + v-model="option.amount" + @input="($event) => filterAmount(index, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': option.amount === '', + 'border-[var(--green)]': option.amount !== '' + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + <div class="flex flex-col items-center bg-white rounded-lg p-8 shadow-lg w-100"> + <div + v-for="(option, index) in options.slice(3, 6)" + :key="index" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.name }}</p> + <div class="flex items-center w-2/3"> + <input + type="text" + v-model="option.amount" + @input="($event) => filterAmount(index + 3, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': option.amount === '', + 'border-[var(--green)]': option.amount !== '' + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + </div> + <div class="w-full text-right mb-3 mt-16"> + <ContinueButtonComponent + @click="onButtonClick" + :disabled="options.some((option) => option.amount === '')" + class="px-10 py-3 text-2xl font-bold mb-4" + ></ContinueButtonComponent> + </div> + </div> +</template> + <script setup lang="ts"> -import { ref } from 'vue' +import { computed, ref } from 'vue' import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' interface Option { name: string - amount: number | null + amount: string } const options = ref<Option[]>([ - { name: 'Snus', amount: null }, - { name: 'Kaffe', amount: null }, - { name: 'Kantina', amount: null } + { name: 'Snus', amount: '' }, + { name: 'Kaffe', amount: '' }, + { name: 'Kantina', amount: '' }, + { name: 'Annet', amount: '' }, + { name: 'Annet', amount: '' }, + { name: 'Annet', amount: '' } ]) -const onButtonClick = () => {} -</script> +const onButtonClick = () => { + router.push('/konfigurasjonSteg5') +} -<template> - <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> - <h1 class="mb-8 lg:mb-12 text-4xl font-bold">Hvor mye penger per kjøp bruker du på ...</h1> - <div class="flex flex-col gap-2 mb-6"> - <div - v-for="(option, index) in options" - :key="index" - :class="{ - 'border-[var(--green)] border-4': option.amount !== null && option.amount >= 0, - 'border-gray-300 border-2': !(option.amount !== null && option.amount >= 0) - }" - class="flex justify-between items-center w-72 p-2 cursor-pointer transition-colors bg-white rounded-lg my-4" - > - <p class="text-xl font-bold">{{ option.name }}</p> - <div class="flex items-center"> - <input - type="number" - v-model="option.amount" - class="w-20 h-10 p-1 rounded-md text-right text-lg" - placeholder="Beløp" - min="0" - /> - <p class="ml-2 font-bold">kr</p> - </div> - </div> - </div> - <ContinueButtonComponent - @click="onButtonClick" - class="px-10 py-3 text-2xl self-end mt-12 mb-0" - ></ContinueButtonComponent> - </div> -</template> +const filterAmount = (index: number, event: Event) => { + const input = event.target as HTMLInputElement + let filteredValue = input.value.replace(/[^\d,]/g, '') + if (filteredValue.includes(',')) { + filteredValue = filteredValue.replace(/,+/g, ',') + const firstCommaIndex = filteredValue.indexOf(',') + filteredValue = + filteredValue.slice(0, firstCommaIndex + 1) + + filteredValue.slice(firstCommaIndex + 1).replace(/,/g, '') + } + options.value[index].amount = filteredValue +} +</script> diff --git a/src/views/ConfigSpendingItemsTotalAmountView.vue b/src/views/ConfigSpendingItemsTotalAmountView.vue new file mode 100644 index 0000000000000000000000000000000000000000..0f8d51b5d4cb80e1253864260d588c050c82ae4f --- /dev/null +++ b/src/views/ConfigSpendingItemsTotalAmountView.vue @@ -0,0 +1,99 @@ +<template> + <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> + <h1 class="mb-8 lg:mb-12 text-4xl font-bold">Hvor mye bruker du totalt per uke på ...</h1> + <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-6"> + <div class="flex flex-col items-center bg-white rounded-lg p-8 shadow-lg w-100"> + <div + v-for="(option, index) in options.slice(0, 3)" + :key="index" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.name }}</p> + <div class="flex items-center w-2/3"> + <input + type="text" + v-model="option.amount" + @input="($event) => filterAmount(index, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': option.amount === '', + 'border-[var(--green)]': option.amount !== '' + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + <div class="flex flex-col items-center bg-white rounded-lg p-8 shadow-lg w-100"> + <div + v-for="(option, index) in options.slice(3, 6)" + :key="index" + class="w-full my-4" + > + <div class="flex justify-between items-center"> + <p class="text-xl font-bold mr-4">{{ option.name }}</p> + <div class="flex items-center w-2/3"> + <input + type="text" + v-model="option.amount" + @input="($event) => filterAmount(index + 3, $event)" + class="h-11 px-3 rounded-md text-lg focus:outline-none border-2 w-full" + :class="{ + 'border-gray-300': option.amount === '', + 'border-[var(--green)]': option.amount !== '' + }" + /> + <p class="text-xl font-bold ml-2">kr</p> + </div> + </div> + </div> + </div> + </div> + <div class="w-full text-right mb-3 mt-16"> + <ContinueButtonComponent + @click="onButtonClick" + :disabled="options.some((option) => option.amount === '')" + class="px-10 py-3 text-2xl font-bold mb-4" + ></ContinueButtonComponent> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue' +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' + +interface Option { + name: string + amount: string +} + +const options = ref<Option[]>([ + { name: 'Snus', amount: '' }, + { name: 'Kaffe', amount: '' }, + { name: 'Kantina', amount: '' }, + { name: 'Annet', amount: '' }, + { name: 'Annet', amount: '' }, + { name: 'Annet', amount: '' } +]) + +const onButtonClick = () => { + router.push('/') +} + +const filterAmount = (index: number, event: Event) => { + const input = event.target as HTMLInputElement + let filteredValue = input.value.replace(/[^\d,]/g, '') + if (filteredValue.includes(',')) { + filteredValue = filteredValue.replace(/,+/g, ',') + const firstCommaIndex = filteredValue.indexOf(',') + filteredValue = + filteredValue.slice(0, firstCommaIndex + 1) + + filteredValue.slice(firstCommaIndex + 1).replace(/,/g, '') + } + options.value[index].amount = filteredValue +} +</script> diff --git a/src/views/ConfigSpendingItemsView.vue b/src/views/ConfigSpendingItemsView.vue index 2b8b0c60381abbf0f1dca5d7c3d1a49d3ed6d7e6..2dea8dffdfe9c300127795cd5981cb95b305d3d2 100644 --- a/src/views/ConfigSpendingItemsView.vue +++ b/src/views/ConfigSpendingItemsView.vue @@ -1,54 +1,85 @@ <template> - <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> - <h1 class="mb-16 text-4xl font-bold lg:mb-20">Hva bruker du mye penger på?</h1> - <div class="flex flex-col gap-8 mb-6"> - <div - v-for="(option, index) in options" - :key="index" - class="flex flex-col items-center justify-center w-72 h-18 p-2.5 cursor-pointer transition-colors bg-white rounded-lg" - :class="{ - 'border-[var(--green)] border-4': option.selected, - 'border-gray-300 border-2': !option.selected - }" - @click="toggleOption(index)" - > - <p class="text-lg font-bold mt-2">{{ option.name }}</p> + <div class="flex flex-col items-center justify-center min-h-screen px-4 sm:px-2 text-center"> + <h1 class="mb-8 lg:mb-12 text-4xl font-bold">Hva bruker du mye penger på?</h1> + <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8"> + <div class="flex flex-col items-center bg-white rounded-lg p-8 shadow-lg md:w-96"> + <div + v-for="buttonText in ['Kaffe', 'Snus', 'Kantina']" + :key="buttonText" + class="w-full my-4" + > + <button + :class="[ + 'w-full h-11 rounded-md text-xl font-bold', + selectedOptions.includes(buttonText) + ? 'border-2 border-[var(--green)]' + : 'border-2 border-gray-300' + ]" + @click="toggleOption(buttonText)" + style="background: transparent" + > + {{ buttonText }} + </button> + </div> + </div> + + <div class="flex flex-col items-center bg-white rounded-lg p-8 shadow-lg md:w-96"> + <div v-for="(option, index) in options" :key="`input-${index}`" class="w-full my-4"> + <input + v-model="option.name" + :class="[ + 'w-full h-11 px-3 rounded-md text-xl focus:outline-none transition-colors', + option.name + ? 'border-2 border-[var(--green)]' + : 'border-2 border-gray-300' + ]" + type="text" + placeholder="Annet ..." + /> + </div> </div> </div> - <ContinueButtonComponent - @click="onButtonClick" - class="px-10 py-3 text-2xl self-end mt-16 mb-0" - ></ContinueButtonComponent> + <div class="w-full text-right mb-3 mt-14"> + <ContinueButtonComponent + @click="onButtonClick" + :disabled="!isFormValid" + class="px-10 py-3 text-2xl font-bold mb-4 mr-2" + ></ContinueButtonComponent> + </div> </div> </template> <script setup lang="ts"> -import { ref } from 'vue' +import { computed, ref } from 'vue' import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' import router from '@/router' interface Option { name: string - selected: boolean } -const options = ref<Option[]>([ - { name: 'Snus', selected: false }, - { name: 'Kaffe', selected: false }, - { name: 'Kantina', selected: false } -]) +const options = ref<Option[]>([{ name: '' }, { name: '' }, { name: '' }]) +const selectedOptions = ref<string[]>([]) -const toggleOption = (index: number) => { - options.value[index].selected = !options.value[index].selected +const toggleOption = (option: string) => { + const index = selectedOptions.value.indexOf(option) + if (index === -1) { + selectedOptions.value.push(option) + } else { + selectedOptions.value.splice(index, 1) + } } +const isFormValid = computed(() => { + return ( + selectedOptions.value.length > 0 || + options.value.some((option) => option.name.trim() !== '') + ) +}) + const onButtonClick = () => { - const selectedOptions = options.value.filter((option) => option.selected) - if (selectedOptions.length <= 3) { - const selectedOptionNames = selectedOptions.map((option) => option.name) + if (isFormValid.value) { router.push('/konfigurasjonSteg4') - } else { - console.error('Please select between 1 and 3 options.') } } </script> diff --git a/src/views/FirstSavingChallengeView.vue b/src/views/FirstSavingChallengeView.vue new file mode 100644 index 0000000000000000000000000000000000000000..23cc78e8411420523f7b280e58da5ef88564005b --- /dev/null +++ b/src/views/FirstSavingChallengeView.vue @@ -0,0 +1,112 @@ +<template> + <div class="flex flex-col items-center justify-start min-h-screen px-4 text-center"> + <div class="mb-20"> + <div + class="flex flex-col items-center justify-start bg-white shadow-md rounded-lg p-16" + style="height: 530px; min-height: 500px; min-width: 400px; max-width: 400px" + > + <template v-if="!skipped && !accepted"> + <div class="mb-6 w-full text-left"> + <label for="savings-goal" class="block text-4xl font-bold mb-2" + >Spareutfordring</label + > + </div> + <div class="flex flex-col w-full mb-4"> + <button + v-for="buttonText in buttonOptions" + :key="buttonText" + :class="[ + 'mb-4 text-xl font-bold w-full rounded-lg py-3 px-4', + selectedOptions.includes(buttonText) + ? 'bg-transparent border-2 border-[var(--green)]' + : 'bg-transparent border-2 border-gray-300' + ]" + @click="toggleOption(buttonText)" + > + {{ buttonText }} + </button> + </div> + <div class="flex justify-between w-full mt-4 space-x-2"> + <button + class="border-4 font-bold rounded-lg py-2 px-10 text-lg transition-all bg-[var(--green)] hover:brightness-90 active:brightness-75" + @click="skip" + style="margin-top: 29px" + > + Skip + </button> + <button + :class="[ + 'border-4 font-bold rounded-lg py-2 px-10 text-lg transition-all', + { + 'bg-[var(--green)] hover:brightness-90 active:brightness-75': + selectedOptions.length > 0 + }, + { + 'opacity-60 bg-[rgba(149,227,93,0.6)] cursor-not-allowed': + selectedOptions.length === 0 + } + ]" + :disabled="selectedOptions.length === 0" + @click="accept" + style="margin-top: 29px" + > + Godta + </button> + </div> + </template> + <template v-else> + <div class="flex justify-center items-center h-full"> + <div class="text-4xl font-bold">{{ acceptedMessage }}</div> + </div> + </template> + </div> + </div> + <ContinueButtonComponent + :disabled="!skipped && !accepted" + @click="onButtonClick" + class="px-10 py-3 text-2xl font-bold self-end mb-32 mt-[-10px]" + ></ContinueButtonComponent> + </div> +</template> + +<script setup lang="ts"> +import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' +import router from '@/router' +import { ref, watchEffect } from 'vue' + +const buttonOptions = ref(['Ikke kjøpe kaffe', 'Ikke kjøpe snus', 'Ikke kjøpe mat i kantina']) +const selectedOptions = ref<string[]>([]) +const skipped = ref(false) +const accepted = ref(false) + +const toggleOption = (option: string) => { + const index = selectedOptions.value.indexOf(option) + if (index === -1) { + selectedOptions.value.push(option) + } else { + selectedOptions.value.splice(index, 1) + } +} + +const onButtonClick = () => { + router.push('/') +} + +const skip = () => { + skipped.value = true +} + +const accept = () => { + accepted.value = true +} + +const acceptedMessage = ref('Du kan opprette spareutfordringer senere') + +watchEffect(() => { + if (accepted.value) { + acceptedMessage.value = 'Du har fått din første spareutfordring!' + } else if (skipped.value) { + acceptedMessage.value = 'Du kan opprette spareutfordringer senere' + } +}) +</script> diff --git a/src/views/FirstSavingGoalView.vue b/src/views/FirstSavingGoalView.vue index 079e5d7b4efa9397fcad4f85220eba23b604eb00..8a3458a855bb674ce18b058e928cb9490f472d7c 100644 --- a/src/views/FirstSavingGoalView.vue +++ b/src/views/FirstSavingGoalView.vue @@ -1,10 +1,95 @@ +<template> + <div class="flex flex-col items-center justify-start min-h-screen px-4 text-center"> + <div class="mb-20"> + <div + class="flex flex-col items-center justify-center bg-white shadow-md rounded-lg p-16" + style="height: 530px; min-height: 500px; min-width: 400px; max-width: 400px" + > + <template v-if="!skipped && !accepted"> + <div class="mb-6 w-full text-left"> + <label for="savings-goal" class="block text-xl font-bold mb-2" + >Jeg vil spare til:</label + > + <input + type="text" + id="savings-goal" + v-model="savingsGoal" + :class="{ + 'border-[var(--green)]': savingsGoal.valueOf(), + 'border-gray-300': !savingsGoal.valueOf() + }" + class="border-2 block w-full rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 text-xl" + placeholder="" + /> + </div> + <div class="mb-8 w-full flex items-center"> + <label for="amount" class="shrink-0 text-xl font-bold mr-2" + >Jeg vil spare:</label + > + <input + type="text" + id="amount" + v-model="rawAmount" + :class="{ + 'border-[var(--green)]': rawAmount.valueOf(), + 'border-gray-300': !rawAmount.valueOf() + }" + class="border-2 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 text-xl mr-2 block w-full" + placeholder="" + min="0" + /> + <span class="shrink-0 text-xl font-bold">kr</span> + </div> + <div class="w-full px-4 py-2"> + <img src="@/assets/coins.png" alt="Savings" class="mx-auto w-36 h-32" /> + </div> + <div class="flex justify-between w-full mt-4 space-x-2"> + <button + class="bg-[var(--green)] border-4 border-[var(--green)] hover:brightness-90 active:brightness-75 font-bold rounded-lg py-2 px-10 text-lg" + @click="skip" + > + Skip + </button> + <button + :class="[ + 'border-4 font-bold rounded-lg py-2 px-10 text-lg transition-all', + canAccept + ? 'bg-[var(--green)] hover:brightness-90 active:brightness-75' + : 'opacity-60 bg-gray-300 cursor-not-allowed' + ]" + :disabled="!canAccept" + @click="accept" + > + Godta + </button> + </div> + </template> + <template v-else> + <div + class="flex justify-start items-center h-full min-h-[400px] min-w-[400px] max-w-[400px]" + > + <div class="text-4xl font-bold">{{ acceptedMessage }}</div> + </div> + </template> + </div> + </div> + <ContinueButtonComponent + :disabled="!skipped && !accepted" + @click="onButtonClick" + class="px-10 py-3 text-lg font-bold self-end mb-80 mt-[-10px]" + ></ContinueButtonComponent> + </div> +</template> + <script setup lang="ts"> -import { ref, watch } from 'vue' +import { computed, ref, watch, watchEffect } from 'vue' import ContinueButtonComponent from '@/components/ContinueButtonComponent.vue' import router from '@/router' const savingsGoal = ref('') const rawAmount = ref('') +const skipped = ref(false) +const accepted = ref(false) const validateAmount = () => { const validPattern = /^(\d+)?(,\d*)?$/ @@ -25,60 +110,33 @@ const checkNegative = () => { watch(rawAmount, validateAmount) watch(() => parseFloat(rawAmount.value.replace(',', '.')), checkNegative) +const canAccept = computed(() => savingsGoal.value.trim() !== '' && rawAmount.value.trim() !== '') + +const skip = () => { + skipped.value = true + acceptedMessage.value = 'Du kan opprette sparemål senere' +} + +const accept = () => { + if (canAccept.value) { + accepted.value = true + acceptedMessage.value = 'Du har fått ditt første sparemål!' + } +} + const onButtonClick = () => { - router.push('/home') + if (skipped.value || accepted.value) { + router.push('/forsteSpareutfordring') + } } -</script> -<template> - <div class="flex flex-col items-center justify-center min-h-screen px-4 text-center"> - <div class="mb-20"> - <div - class="flex flex-col items-center justify-center bg-white shadow-md rounded-lg p-16" - style="width: 400px; height: 400px" - > - <div class="mb-6 w-full text-left"> - <label for="savings-goal" class="block text-xl font-bold mb-2" - >Jeg vil spare til:</label - > - <input - type="text" - id="savings-goal" - v-model="savingsGoal" - :class="{ - 'border-[var(--green)]': savingsGoal.valueOf(), - 'border-gray-300': !savingsGoal.valueOf() - }" - class="border-2 block w-full rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 text-xl" - placeholder="" - /> - </div> - <div class="mb-8 w-full flex items-center"> - <label for="amount" class="shrink-0 text-xl font-bold mr-2" - >Jeg vil spare:</label - > - <input - type="text" - id="amount" - v-model="rawAmount" - :class="{ - 'border-[var(--green)]': rawAmount.valueOf(), - 'border-gray-300': !rawAmount.valueOf() - }" - class="border-2 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 text-xl mr-2 block w-full" - placeholder="" - min="0" - /> - <span class="shrink-0 text-xl font-bold">kr</span> - </div> - <div class="w-full px-4 py-2"> - <img src="@/assets/coins.png" alt="Savings" class="mx-auto w-36 h-32" /> - </div> - </div> - </div> - <ContinueButtonComponent - @click="onButtonClick" - class="px-10 py-3 text-2xl font-bold self-end" - ></ContinueButtonComponent> - </div> -</template> +const acceptedMessage = ref('') + +watchEffect(() => { + if (accepted.value) { + acceptedMessage.value = 'Du har fått ditt første sparemål!' + } else if (skipped.value) { + acceptedMessage.value = 'Du kan opprette sparemål senere' + } +}) +</script> diff --git a/src/views/ProfileView.vue b/src/views/ProfileView.vue index 0d2bf5eb71a6344c64b0ed7b8b8531dc27ee83a7..91d81a32b5571b464ce5b7e396889398c2118fed 100644 --- a/src/views/ProfileView.vue +++ b/src/views/ProfileView.vue @@ -1,4 +1,18 @@ -<script lang="ts" setup></script> +<script lang="ts" setup> +import authInterceptor from '@/services/authInterceptor' +import { onMounted } from 'vue' + +onMounted(async () => { + await authInterceptor + .get('/users/me/config') + .then((response) => { + console.log(response.data) + }) + .catch((error) => { + console.log(error) + }) +}) +</script> <template> <h1>Din profil</h1>