diff --git a/src/components/Configuration/Configuration.vue b/src/components/Configuration/Configuration.vue index 372b3d38384fbb5e9b5951dd73b7d843d11a5227..8398e677734374a3f413feebd8b6017024aab5b5 100644 --- a/src/components/Configuration/Configuration.vue +++ b/src/components/Configuration/Configuration.vue @@ -4,22 +4,14 @@ import { ref } from 'vue' import { useRouter } from 'vue-router' import { useRoute } from 'vue-router' -// Configuration variables -let bankId = ref('') -let commitment = ref('') -let experience = ref('') -let suitableChallenges = ref([]) -let savingGoalTitle = ref('') -let sumToSpare = ref(0) -let dueDate = ref(null) - const router = useRouter() + // The configuration steps with path and order value. -const configurationSteps = {'/bank-id': 1, '/commitment': 2, '/experience': 3, '/suitable-challenges': 4, '/first-saving-goal': 5} +const configurationSteps = {'/commitment': 1, '/experience': 2, '/suitable-challenges': 3} const length = Object.keys(configurationSteps).length let percentage = ref(1/length); -// Initially pushes to '/bank-id' RouterView and sets current path to this path. +// Initially pushes to the commitment-RouterView and sets current path to this path. router.push(Object.keys(configurationSteps)[0]) let currentRoute = useRoute() let currentPath = currentRoute.fullPath @@ -30,22 +22,6 @@ const onNewRouteEvent = (path) => { percentage.value = (1/length) * configurationSteps[path] } -const onBankIdSelectedEvent = (value) => { - bankId.value = value -} - -const onCommitmentSelectedEvent = (value) => { - commitment.value = value -} - -const onExperienceSelectedEvent = (value) => { - experience.value = value -} - -const onChallengesSelectedEvent = (value) => { - suitableChallenges.value = value -} - </script> <template> @@ -54,12 +30,7 @@ const onChallengesSelectedEvent = (value) => { <ProgressBar id="progressbar" :percentage="percentage"/> </div> <div class="configuration-container"> - <RouterView @changeRouterEvent="onNewRouteEvent" - @bankIdSelectedEvent="onBankIdSelectedEvent" - @commitmentSelectedEvent="onCommitmentSelectedEvent" - @experienceSelectedEvent="onExperienceSelectedEvent" - @challengesSelectedEvent="onChallengesSelectedEvent" - /> + <RouterView @changeRouterEvent="onNewRouteEvent"/> </div> </div> </template> diff --git a/src/components/Configuration/ConfigurationSteps/BankId.vue b/src/components/Configuration/ConfigurationSteps/BankId.vue index 12a9e65031ef42821712c61996bbbce0e167aa91..bc40db3e7cd5aa6501e2daef33dc14842df2637c 100644 --- a/src/components/Configuration/ConfigurationSteps/BankId.vue +++ b/src/components/Configuration/ConfigurationSteps/BankId.vue @@ -1,4 +1,5 @@ <script setup lang="ts"> +/* import Button1 from '@/components/Buttons/Button1.vue' import { useRouter } from 'vue-router' import { ref } from 'vue' @@ -10,7 +11,7 @@ const vippsRef = ref(false) const router = useRouter(); -const emit = defineEmits(['changeRouterEvent', 'bankIdSelectedEvent']); +const emit = defineEmits(['changeRouterEvent']); emit('changeRouterEvent', '/bank-id'); const onClick = () => { @@ -30,10 +31,11 @@ const onClick = () => { emit('bankIdSelectedEvent', choice) router.push('/commitment') } - +*/ </script> <template> + <!-- <div class="container"> <div> <h3 class="d-flex align-items-center justify-content-center"> @@ -62,9 +64,11 @@ const onClick = () => { </div> </div> + --> </template> <style scoped> +/* #confirmButton { margin-bottom: 2rem; width: 300px; @@ -74,4 +78,5 @@ const onClick = () => { display: flex; justify-content: center; } + */ </style> \ No newline at end of file diff --git a/src/components/Configuration/ConfigurationSteps/Commitment.vue b/src/components/Configuration/ConfigurationSteps/Commitment.vue index af7c1b29bd9ca24afb3ce8505cca14700e76b06a..05f071e4966bdd1148a58295340c56185847080e 100644 --- a/src/components/Configuration/ConfigurationSteps/Commitment.vue +++ b/src/components/Configuration/ConfigurationSteps/Commitment.vue @@ -1,59 +1,63 @@ <script setup lang="ts"> - import Button1 from '@/components/Buttons/Button1.vue' import { useRouter } from 'vue-router' import { ref } from 'vue' +import { useConfigurationStore } from '@/stores/ConfigurationStore' + +const router = useRouter(); -const emit = defineEmits(['changeRouterEvent', 'commitmentSelectedEvent']) +// Updates progress bar in the parent Configuration component. +const emit = defineEmits(['changeRouterEvent']) emit('changeRouterEvent', '/commitment') +// Reactive variables for form and radio buttons. const formRef = ref() const lowRef = ref('') const mediumRef = ref('') const highRef = ref('') -const router = useRouter(); - +let errorMsg = ref(''); + +/** + * Validates the commitment form radio buttons and updates the commitment choice in the store. + * If form validation is successful, updates the commitment choice in the store + * and navigates to the '/experience' route. If form validation fails, displays + * an error message prompting the user to select an option before continuing. + */ const onClick = () => { - const radios = formRef.value.querySelectorAll('input[type="radio"]'); - const checkedRadios = Array.from(radios).filter(radio => radio.checked); - - if (checkedRadios.length === 0) { - alert('Please select an option.'); - return; + const form = formRef.value; + if (form.checkValidity()) { + let choice = ''; + if (lowRef.value.checked) choice = 'LITTLE' + else if (mediumRef.value.checked) choice = 'SOME' + else if (highRef.value.checked) choice = 'MUCH' + useConfigurationStore().setCommitment(choice) + router.push('/experience') + } + else { + errorMsg.value = 'Please select an option before continuing' } - - let choice = ''; - if (lowRef.value.checked) choice = 'Low' - else if (mediumRef.value.checked) choice = 'Medium' - else if (highRef.value.checked) choice = 'High' - - emit('commitmentSelectedEvent', choice) - router.push('/experience') } </script> <template> <div class="container"> - <div> - <h3 class="d-flex align-items-center justify-content-center"> - In which degree are you willing to make changes? - </h3> - </div> - + <h3 id="commitmentText" class="align-items-center justify-content-center"> + In which degree are you willing to make changes? + </h3> <form class="btn-group-vertical" ref="formRef" @submit.prevent="onClick"> - <input ref="lowRef" type="radio" class="btn-check" name="commitment" id="btn-check-outlined" autocomplete="off"> + <input ref="lowRef" type="radio" class="btn-check" name="commitment" id="btn-check-outlined" autocomplete="off" required> <label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check-outlined">Low</label> - <input ref="mediumRef" type="radio" class="btn-check" name="commitment" id="btn-check2-outlined" autocomplete="off"> + <input ref="mediumRef" type="radio" class="btn-check" name="commitment" id="btn-check2-outlined" autocomplete="off" required> <label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check2-outlined">Medium</label> - <input ref="highRef" type="radio" class="btn-check" name="commitment" id="btn-check3-outlined" autocomplete="off"> + <input ref="highRef" type="radio" class="btn-check" name="commitment" id="btn-check3-outlined" autocomplete="off" required> <label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check3-outlined">High</label> </form> - + <p class="text-danger">{{ errorMsg }}</p> <div class="confirm-button-container"> <button1 id="confirmButton" @click="onClick" button-text="Continue"></button1> </div> @@ -61,6 +65,13 @@ const onClick = () => { </template> <style scoped> +div.container { + display: flex; + flex-direction: column; + justify-self: center; + max-width: 500px; +} + #confirmButton { margin-bottom: 2rem; width: 300px; diff --git a/src/components/Configuration/ConfigurationSteps/Experience.vue b/src/components/Configuration/ConfigurationSteps/Experience.vue index 4e890f5d230d4c7a222c85ae1017b0dcbf3b1f90..ae4e5b2ef104e3963229dcb8ffb1e58764b0d109 100644 --- a/src/components/Configuration/ConfigurationSteps/Experience.vue +++ b/src/components/Configuration/ConfigurationSteps/Experience.vue @@ -2,32 +2,40 @@ import Button1 from '@/components/Buttons/Button1.vue' import { useRouter } from 'vue-router' import { ref } from 'vue' +import { useConfigurationStore } from '@/stores/ConfigurationStore' const router = useRouter(); -const emit = defineEmits(['changeRouterEvent', 'experienceSelectedEvent']) + +// Updates progress bar in the parent Configuration component. +const emit = defineEmits(['changeRouterEvent']) emit('changeRouterEvent', '/experience') +// Declaration of reactive variables for the form and radio buttons const formRef = ref() const beginnerRef = ref('') const someExperienceRef = ref('') const expertRef = ref('') +let errorMsg = ref(''); +/** + * Validates the experience form radio buttons and updates the commitment choice in the store. + * If form validation is successful, updates the commitment choice in the store + * and navigates to the '/suitable challenges' route. If form validation fails, displays + * an error message prompting the user to select an option before continuing. + */ const onClick = () => { - const radios = formRef.value.querySelectorAll('input[type="radio"]'); - const checkedRadios = Array.from(radios).filter(radio => radio.checked); - - if (checkedRadios.length === 0) { - alert('Please select an option.'); - return; + const form = formRef.value; + if (form.checkValidity()) { + let choice = '' + if (beginnerRef.value.checked) choice = 'NONE' + else if (someExperienceRef.value.checked) choice = 'SOME' + else if (expertRef.value.checked) choice = 'EXPERT' + useConfigurationStore().setExperience(choice) + router.push('/suitable-challenges') + } + else { + errorMsg.value = 'Please select an option before continuing' } - - let choice = '' - if (beginnerRef.value.checked) choice = 'Beginner' - else if (someExperienceRef.value.checked) choice = 'Some experience' - else if (expertRef.value.checked) choice = 'Expert' - - emit('experienceSelectedEvent', choice) - router.push('/suitable-challenges') } </script> @@ -42,16 +50,16 @@ const onClick = () => { <form class="btn-group-vertical" ref="formRef" @submit.prevent="onClick"> - <input ref="beginnerRef" type="radio" class="btn-check" name="experience" id="btn-check-outlined" autocomplete="off"> + <input ref="beginnerRef" type="radio" class="btn-check" name="experience" id="btn-check-outlined" autocomplete="off" required> <label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check-outlined">Beginner</label> - <input ref="someExperienceRef" type="radio" class="btn-check" name="experience" id="btn-check2-outlined" autocomplete="off"> + <input ref="someExperienceRef" type="radio" class="btn-check" name="experience" id="btn-check2-outlined" autocomplete="off" required> <label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check2-outlined">Some experience</label> - <input ref="expertRef" type="radio" class="btn-check" name="experience" id="btn-check3-outlined" autocomplete="off"> + <input ref="expertRef" type="radio" class="btn-check" name="experience" id="btn-check3-outlined" autocomplete="off" required> <label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check3-outlined">Expert</label> </form> - + <p class="text-danger">{{ errorMsg }}</p> <div class="confirm-button-container"> <button1 id="confirmButton" @click="onClick" button-text="Continue"></button1> </div> @@ -59,6 +67,13 @@ const onClick = () => { </template> <style scoped> +div.container { + display: flex; + flex-direction: column; + justify-self: center; + max-width: 500px; +} + #confirmButton { margin-bottom: 2rem; width: 300px; diff --git a/src/components/Configuration/ConfigurationSteps/FirstSavingGoal.vue b/src/components/Configuration/ConfigurationSteps/FirstSavingGoal.vue index 50e010cb29ea36b8d581d89ab7af59b21c0bb22b..da397304a8cdc40ea8e21212fc440a64d30c2a1c 100644 --- a/src/components/Configuration/ConfigurationSteps/FirstSavingGoal.vue +++ b/src/components/Configuration/ConfigurationSteps/FirstSavingGoal.vue @@ -1,4 +1,5 @@ <script setup lang="ts"> +/* import BaseInput from '@/components/InputFields/BaseInput.vue' import { ref } from 'vue' import Button1 from '@/components/Buttons/Button1.vue' @@ -33,10 +34,11 @@ const getTodayDate = () => { return `${year}-${month}-${day}`; }; - + */ </script> <template> + <!-- <div class="container"> <div> <h3 class="d-flex align-items-center justify-content-center"> @@ -72,9 +74,11 @@ const getTodayDate = () => { <button1 id="confirmButton" @click="onClick" button-text="Continue"></button1> </div> </div> + --> </template> <style scoped> +/* #confirmButton { margin-bottom: 2rem; width: 300px; @@ -84,4 +88,5 @@ const getTodayDate = () => { display: flex; justify-content: center; } +*/ </style> \ No newline at end of file diff --git a/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue b/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue index dc849d5cdde9afd61f9e53e53b4eaefba203bf64..2401b49d3f1452d28f4d3aa91732be573c9279b7 100644 --- a/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue +++ b/src/components/Configuration/ConfigurationSteps/SuitableChallenges.vue @@ -3,15 +3,31 @@ import { useRouter } from 'vue-router' import ChallangeCheckBox from '@/components/Configuration/ChallangeCheckBox.vue' import Button1 from '@/components/Buttons/Button1.vue' import { ref } from 'vue' +import { useConfigurationStore } from '@/stores/ConfigurationStore' +import { useUserInfoStore } from '@/stores/UserStore' +import { AuthenticationService, OpenAPI, SignUpRequest } from '@/api' +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' -const emit = defineEmits(['changeRouterEvent', 'challengesSelectedEvent']) -emit('changeRouterEvent', '/suitable-challenges') const router = useRouter(); +// Updates progress bar in the parent Configuration component. +const emit = defineEmits(['changeRouterEvent']) +emit('changeRouterEvent', '/suitable-challenges') + +// Reactive variables for chosen challenges and error message. let chosenChallenges = ref([]) +let errorMsg = ref('') + +// Represents a list of available challenges. const challenges = ['Make packed lunch', 'Stop shopping', 'Drop coffee', 'Quit subscription', 'Drop car', 'Short showers', 'Exercise outside', 'Make budget'] +/** + * Handles the event when a challenge is selected or deselected. + * @param {Array} value - An array containing the challenge value and its checked status. + * The first element is the challenge value, and the second element + * indicates whether the challenge is checked (true) or unchecked (false). + */ const onChangedChallengeEvent = (value) => { // if challenge is checked then add it to the chosenChallenges variable if (value[1]) { @@ -19,15 +35,59 @@ const onChangedChallengeEvent = (value) => { } // if challenge is unchecked then remove it from the chosenChallenges variable else { - console.log('Reached') chosenChallenges.value = chosenChallenges.value.filter(item => item !== value[0]); } - console.log(chosenChallenges.value) } -const onClick = () => { - emit('challengesSelectedEvent', chosenChallenges.value) - router.push('/first-saving-goal') +/** + * Retrieves user configuration and signup information, sends a signup request to the backend. + * + * @throws {Error} Throws an error if signup fails. + */ +const onClick = async () => { + try { + // Saves the chosen challenges to the configuration store + useConfigurationStore().setChallenges(chosenChallenges.value) + + /* + TODO: 'changeWilling' are updated to 'commitment' in backend, must update it in frontend + const signUpPayLoad: SignUpRequest = { + changeWilling: useConfigurationStore().getCommitment, + experience: useConfigurationStore().getExperience, + challenges: useConfigurationStore().getChallenges, + firstName: useUserInfoStore().getFirstName, + lastName: useUserInfoStore().getLastname, + email: useUserInfoStore().getEmail, + password: useUserInfoStore().getPassword, + }; + */ + + const signUpPayLoad = { + "commitment": useConfigurationStore().getCommitment, + "experience": useConfigurationStore().getExperience, + "challenges": useConfigurationStore().getChallenges, + "firstName": useUserInfoStore().getFirstName, + "lastName": useUserInfoStore().getLastname, + "email": useUserInfoStore().getEmail, + "password": useUserInfoStore().getPassword, + }; + + let response = await AuthenticationService.signup({ requestBody: signUpPayLoad }); + if (response.token == null) { + errorMsg.value = 'A valid token could not be created'; + return; + } + OpenAPI.TOKEN = response.token; + useUserInfoStore().setUserInfo({ + accessToken: response.token, + role: response.role, + }); + useUserInfoStore().resetPassword() + await router.push({ name: 'home' }); + } + catch (error) { + errorMsg.value = handleUnknownError(error); + } } </script> @@ -46,8 +106,10 @@ const onClick = () => { /> </div> + <p class="text-danger">{{ errorMsg }}</p> + <div class="confirm-button-container"> - <button1 id="confirmButton" @click="onClick" button-text="Continue"></button1> + <button1 id="confirmButton" @click="onClick" button-text="Finish configuration"></button1> </div> </div> </template> diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue index 21d4de86686d68850b9ee49b09cb9246be596ced..0d2476e38bc649774937dae44c5e3a056cad885a 100644 --- a/src/components/Login/LoginForm.vue +++ b/src/components/Login/LoginForm.vue @@ -54,7 +54,7 @@ const handleSubmit = async () => { email: emailRef.value, role: response.role, }); - router.push({ name: 'home' }); + await router.push({ name: 'home' }); } catch (error: any) { errorMsg.value = handleUnknownError(error); } diff --git a/src/components/SignUp/SignUpForm.vue b/src/components/SignUp/SignUpForm.vue index 9a8dab783a1c291575375d0f8ea69fb1d3a6b525..aefb991c0520a76089b48c023c8bcfc077de9280 100644 --- a/src/components/SignUp/SignUpForm.vue +++ b/src/components/SignUp/SignUpForm.vue @@ -3,8 +3,12 @@ import BaseInput from '@/components/InputFields/BaseInput.vue' import Button1 from '@/components/Buttons/Button1.vue' import { ref } from 'vue' import { useRouter } from 'vue-router' +import { AuthenticationService } from '@/api' +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' +import { useUserInfoStore } from '@/stores/UserStore' const router = useRouter(); +const userStore = useUserInfoStore(); const firstNameRef = ref('') const surnameRef = ref('') @@ -13,10 +17,10 @@ const passwordRef = ref('') const confirmPasswordRef = ref('') const formRef = ref() let samePasswords = ref(true) +let errorMsg = ref(''); const handleFirstNameInputEvent = (newValue: any) => { firstNameRef.value = newValue - console.log(firstNameRef.value) } const handleSurnameInputEvent = (newValue: any) => { @@ -29,41 +33,43 @@ const handleEmailInputEvent = (newValue: any) => { const handlePasswordInputEvent = (newValue: any) => { passwordRef.value = newValue - console.log(passwordRef.value) } const handleConfirmPasswordInputEvent = (newValue: any) => { confirmPasswordRef.value = newValue - console.log(confirmPasswordRef.value) } const handleSubmit = async () => { - console.log(firstNameRef.value) samePasswords.value = (passwordRef.value === confirmPasswordRef.value) - console.log(samePasswords.value) - const form = formRef.value; + formRef.value.classList.add("was-validated") - // Check if the form is valid + const form = formRef.value; if (form.checkValidity()) { - // Form is valid, submit the form or perform other actions - console.log('Form is valid'); - } else { - console.log('Form is not valid'); + if (samePasswords.value) { + try { + let response = await AuthenticationService.validateEmail({email: emailRef.value}); + userStore.setUserInfo({ + firstname: firstNameRef.value, + lastname: surnameRef.value, + email: emailRef.value, + }); + userStore.setPassword(passwordRef.value) + await router.push('/configuration') + } catch (error) { + errorMsg.value = handleUnknownError(error); + } + } } - - - formRef.value.classList.add("was-validated") } </script> <template> <div class="container"> - <form ref="formRef" id="signUpForm" @submit.prevent="handleSubmit"> - <BaseInput :model-value="firstNameRef" + <form ref="formRef" id="signUpForm" @submit.prevent="handleSubmit" novalidate> + <BaseInput :model-value=firstNameRef @input-change-event="handleFirstNameInputEvent" - ref="firstNameRef" id="firstNameInput" input-id="first-name" type="text" @@ -72,7 +78,6 @@ const handleSubmit = async () => { invalid-message="Please enter your first name"/> <BaseInput :model-value="surnameRef" @input-change-event="handleSurnameInputEvent" - ref="surnameRef" id="surnameInput" input-id="surname" type="text" @@ -81,7 +86,6 @@ const handleSubmit = async () => { invalid-message="Please enter your surname"/> <BaseInput :model-value="emailRef" @input-change-event="handleEmailInputEvent" - ref="emailRef" id="emailInput" input-id="email" type="email" @@ -90,7 +94,6 @@ const handleSubmit = async () => { invalid-message="Invalid email"/> <BaseInput :model-value="passwordRef" @input-change-event="handlePasswordInputEvent" - ref="passwordRef" id="passwordInput" input-id="password" type="password" @@ -100,7 +103,6 @@ const handleSubmit = async () => { invalid-message="Password must be between 4 and 16 characters and contain one capital letter, small letter and a number"/> <BaseInput :modelValue="confirmPasswordRef" @input-change-event="handleConfirmPasswordInputEvent" - ref="confirmPasswordRef" id="confirmPasswordInput" input-id="confirmPassword" type="password" @@ -108,7 +110,8 @@ const handleSubmit = async () => { label="Confirm Password" placeholder="Confirm password" invalid-message="Password must be between 4 and 16 characters and contain one capital letter, small letter and a number"/> - <p v-if="samePasswords" class="text-danger">The passwords are not identical</p> + <p class="text-danger">{{ errorMsg }}</p> + <p v-if="!samePasswords" class="text-danger">The passwords are not identical</p> <button1 id="confirmButton" @click="handleSubmit" button-text="Sign up"></button1> </form> </div> diff --git a/src/stores/ConfigurationStore.ts b/src/stores/ConfigurationStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb9e0dd0d13fecdf719f8bc68970fe793b465027 --- /dev/null +++ b/src/stores/ConfigurationStore.ts @@ -0,0 +1,38 @@ +import { defineStore } from 'pinia' +export const useConfigurationStore = defineStore('ConfigurationStore', { + state: () => ({ + commitment: '', + experience: '', + challenges: [] + }), + actions: { + setCommitment(commitment: string) { + this.commitment = commitment + }, + setExperience(experience: string) { + this.experience = experience + }, + setChallenges(challenges: Array<string>) { + this.challenges = challenges + }, + resetConfiguration() { + this.commitment = '' + this.experience = '' + this.challenges = [] + } + }, + getters: { + getCommitment(): string { + return this.commitment + }, + getExperience(): string { + return this.experience + }, + getChallenges(): string { + return this.challenges + } + }, + persist: { + enabled: true, + } +}); diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 74e952ab685696d19016d1e57fc88912d10d37be..5622d43f2392f7194e8d386cdce96ab4ba5930cf 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -75,6 +75,15 @@ export const useUserInfoStore = defineStore('UserInfoStore', { getPassword(): string { return this.password }, + getFirstName(): string { + return this.firstname + }, + getLastname(): string { + return this.lastname + }, + getEmail(): string { + return this.email + }, isLoggedIn(): boolean { return this.accessToken !== ''; }, diff --git a/src/views/ConfigurationView.vue b/src/views/ConfigurationView.vue index 6732fee8b3dc4710002df7d991334fdaa15e3ea5..58a63f43e9d6cb7ab5f205a8fb09489b9bd2701c 100644 --- a/src/views/ConfigurationView.vue +++ b/src/views/ConfigurationView.vue @@ -5,7 +5,5 @@ import Footer from '@/components/BaseComponents/Footer.vue' </script> <template> - <Menu/> <Configuration/> - <Footer/> </template> \ No newline at end of file