diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 3b3337710c66b4a24afb1c82330a9681663072d4..3dba281cd4821c44dbe43c8600cf71e3c8ca1302 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -1,6 +1,6 @@ describe('Login', () => { beforeEach(() => { - cy.visit('/login') + cy.visit('/logginn') }) function fullInput() { diff --git a/cypress/e2e/register.cy.ts b/cypress/e2e/register.cy.ts index a0c04d07ecf4d60e9ecc5934b1bbaeb12aa63f30..508e05569bccb1553a227967aad3bf9e55ee3625 100644 --- a/cypress/e2e/register.cy.ts +++ b/cypress/e2e/register.cy.ts @@ -1,6 +1,6 @@ describe('Register', () => { beforeEach(() => { - cy.visit('/login') + cy.visit('/registrer') cy.contains('h3', 'Registrer deg').click() }) diff --git a/src/assets/spare_og_sti.png b/src/assets/spare_og_sti.png new file mode 100644 index 0000000000000000000000000000000000000000..9716e1ab8e85e9302c7d616e17f486d610855ed3 Binary files /dev/null and b/src/assets/spare_og_sti.png differ diff --git a/src/components/FormLogin.vue b/src/components/FormLogin.vue index ebdac31421a23a0fcccc7573bbc596a9ba7df078..8d22292fcbce4efd3d79887b6c73745578f31324 100644 --- a/src/components/FormLogin.vue +++ b/src/components/FormLogin.vue @@ -1,22 +1,51 @@ <script lang="ts" setup> -import { ref, watch } from 'vue' +import { computed, ref, watch } from 'vue' import { useUserStore } from '@/stores/userStore' +import ModalComponent from '@/components/ModalComponent.vue' +import { useRouter } from 'vue-router' +import axios from 'axios' const username = ref<string>('') const password = ref<string>('') const showPassword = ref<boolean>(false) const errorMessage = ref<string>('') +const isModalOpen = ref<boolean>(false) +const resetEmail = ref<string>('') +const emailRegex = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/ const userStore = useUserStore() +const router = useRouter() + +const isEmailValid = computed(() => emailRegex.test(resetEmail.value)) const submitForm = () => { userStore.login(username.value, password.value) + router.push('/hjem') } const toggleShowPassword = () => { showPassword.value = !showPassword.value } +const openForgotPasswordModal = (event: MouseEvent) => { + event.preventDefault() + isModalOpen.value = true +} + +const submitReset = async () => { + await axios.post('http://localhost:8080/forgotPassword/changePasswordRequest', { + email: resetEmail.value + }) + + resetEmail.value = '' + isModalOpen.value = false +} + +const closeModal = () => { + resetEmail.value = '' + isModalOpen.value = false +} + watch( () => userStore.errorMessage, (newValue: string) => { @@ -49,6 +78,11 @@ watch( <button class="absolute right-0 top-1 bg-transparent" @click="toggleShowPassword"> {{ showPassword ? '🔓' : '🔒' }} </button> + <a + @click="openForgotPasswordModal" + class="absolute right-3 top-10 hover:underline hover:bg-transparent cursor-pointer" + >Glemt passord?</a + > </div> </div> <div class="flex flex-row gap-5"> @@ -63,6 +97,34 @@ watch( <p>{{ errorMessage }}</p> </div> </div> + <modal-component + :title="'Glemt passord'" + :message="'Vennligst skriv inn e-posten din for Ã¥ endre passordet.'" + :is-modal-open="isModalOpen" + @close="isModalOpen = false" + > + <template v-slot:input> + <input + type="email" + v-model="resetEmail" + class="border border-gray-300 p-2 w-full mb-7" + placeholder="Skriv e-postadressen din her" + /> + </template> + <template v-slot:buttons> + <button + :disabled="!isEmailValid" + @click="submitReset" + class="active-button font-bold py-2 px-4 w-1/2 hover:bg-[#f7da7c] border-2 border-[#f7da7c] disabled:border-transparent" + > + Send mail + </button> + <button + @click="closeModal" + class="active-button font-bold py-2 px-4 w-1/2 hover:bg-[#f7da7c] border-2 border-[#f7da7c] disabled:border-transparent" + > + Lukk + </button> + </template> + </modal-component> </template> - -<style scoped></style> diff --git a/src/components/FormRegister.vue b/src/components/FormRegister.vue index 4a94b4eedb0db873bc5e43672d969c1c7f512582..d32c4d0d3682864b2fd8298b62858b7d13f5ddf3 100644 --- a/src/components/FormRegister.vue +++ b/src/components/FormRegister.vue @@ -2,6 +2,7 @@ import { computed, ref, watch } from 'vue' import { useUserStore } from '@/stores/userStore' import ToolTip from '@/components/ToolTip.vue' +import { useRouter } from 'vue-router' const firstname = ref<string>('') const lastname = ref<string>('') @@ -14,6 +15,7 @@ const showPassword = ref<boolean>(false) const errorMessage = ref<string>('') const userStore = useUserStore() +const router = useRouter() const nameRegex = /^[a-zA-Z ,.'-]+$/ const emailRegex = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/ @@ -35,6 +37,7 @@ const isFormInvalid = computed( const submitForm = () => { userStore.register(firstname.value, lastname.value, email.value, username.value, password.value) + router.push('/konfigurasjonSteg1') } const toggleShowPassword = () => { @@ -148,5 +151,3 @@ watch( </div> </div> </template> - -<style scoped></style> diff --git a/src/components/ModalComponent.vue b/src/components/ModalComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..43a34f224eaa25cae666068b36aaa7f21024f8f8 --- /dev/null +++ b/src/components/ModalComponent.vue @@ -0,0 +1,25 @@ +<template> + <div + v-if="isModalOpen" + class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center" + > + <div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full text-center"> + <h2 class="font-bold mb-4">{{ title }}</h2> + <p class="mb-4">{{ message }}</p> + + <slot name="input"></slot> + + <div class="flex flex-col justify-center items-center gap-3 mt-3 w-full"> + <slot name="buttons"></slot> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +defineProps({ + title: String, + message: String, + isModalOpen: Boolean +}) +</script> diff --git a/src/components/__tests__/ModalTest.spec.ts b/src/components/__tests__/ModalTest.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..80f93265e199899e02ff5dbb1e6824cfaa5112ae --- /dev/null +++ b/src/components/__tests__/ModalTest.spec.ts @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { shallowMount } from '@vue/test-utils' +import ModalComponent from '@/components/ModalComponent.vue' + +describe('ModalComponent', () => { + let wrapper: any + + beforeEach(() => { + wrapper = shallowMount(ModalComponent, { + props: { + title: 'Test Title', + message: 'Test Message', + button1: 'Test button', + isModalOpen: true, + showButton: true, + showInput: false, + typeValue: 'text', + inputPlaceholder: 'Placeholder', + isInputValid: true + } + }) + }) + + it('opens modal when button is clicked', async () => { + expect(wrapper.props().isModalOpen).toBe(true) + }) +}) diff --git a/src/router/index.ts b/src/router/index.ts index 0ed3db43f38e6df15f0ca65b633a60e4ca6e56b7..9a6758c7dcc2a73e732cb6d0173a96ea5f09e4ac 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -15,10 +15,20 @@ const router = createRouter({ component: HomeView }, { - path: '/login', + path: '/logginn', name: 'login', component: () => import('@/views/RegisterLoginView.vue') }, + { + path: '/registrer', + name: 'register', + component: () => import('@/views/RegisterLoginView.vue') + }, + { + path: '/forgotPassword', + name: 'resetPassword', + component: () => import('@/views/ResetPasswordView.vue') + }, { path: '/profil', name: 'profile', @@ -62,12 +72,12 @@ 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') + component: () => import('@/views/FirstSavingChallengeView.vue') } ], scrollBehavior(to, from, savedPosition) { diff --git a/src/views/RegisterLoginView.vue b/src/views/RegisterLoginView.vue index d664d4704fb812849ab1872ab0f624fbd4376a5b..738002afa9461e9df0acbd0f592b327303ec33ba 100644 --- a/src/views/RegisterLoginView.vue +++ b/src/views/RegisterLoginView.vue @@ -1,17 +1,28 @@ <script setup lang="ts"> import FormLogin from '@/components/FormLogin.vue' import FormRegister from '@/components/FormRegister.vue' -import { ref } from 'vue' +import { onMounted, ref } from 'vue' +import { useRouter } from 'vue-router' + +const router = useRouter() const isLogin = ref<boolean>(true) + +onMounted(() => { + isLogin.value = router.currentRoute.value.path === '/logginn' +}) </script> <template> - <div class="flex flex-col items-center gap-5 justify-center sm:flex-row"> - <div class="border-2 border-black flex items-center"> - <h1>Dette er et bilde</h1> + <div class="flex flex-col items-center gap-5 justify-center md:flex-row h-screen"> + <div class="flex items-center justify-center md:w-2/3"> + <img + src="@/assets/spare_og_sti.png" + alt="Spare og sti logo" + class="w-5/6 ml-10 md:mb-64" + /> </div> - <div class="flex flex-col"> + <div class="flex flex-col md:mr-10 md:mt-20 md:w-1/3 h-screen justify-start"> <div class="flex flex-row gap-5 justify-center"> <h3 :class="{ selected: isLogin }" diff --git a/src/views/ResetPasswordView.vue b/src/views/ResetPasswordView.vue new file mode 100644 index 0000000000000000000000000000000000000000..d2816f564c9b2ec96a6657ae7f3f6e085a7638d4 --- /dev/null +++ b/src/views/ResetPasswordView.vue @@ -0,0 +1,119 @@ +<template> + <div> + <h1 class="flex flex-col items-center mt-8">Oppdater passord</h1> + <p class="flex flex-col items-center mb-16">Skriv inn ditt nye passord ðŸ”</p> + <div class="flex justify-center items-center w-full"> + <div class="flex flex-col md:w-1/4 w-2/3"> + <div class="flex flex-row justify-between mx-4"> + <p>Nytt passord:</p> + <ToolTip + :message="'Must be at least 8 characters, including at least one number, one lowercase letter, one uppercase letter, one special character (@#$%^&+=!), and no spaces.'" + /> + </div> + <div class="relative"> + <input + name="password" + v-model="newPassword" + :type="showPassword ? 'text' : 'password'" + placeholder="Skriv inn passord" + class="w-full" + :class="{ 'bg-green-200': isPasswordValid }" + /> + <button + class="absolute right-0 top-1 bg-transparent" + @click="toggleShowPassword" + > + {{ showPassword ? '🔓' : '🔒' }} + </button> + </div> + <input + v-model="confirm" + :class="{ 'bg-green-200': newPassword == confirm && '' !== confirm.valueOf() }" + class="mt-2 w-full" + name="confirm" + placeholder="Bekreft passord" + type="password" + /> + <div class="flex justify-center mt-10"> + <button + :disabled="!canResetPassword" + @click="resetPassword" + class="p-2 w-2/3 md:w-5/6 disabled:opacity-50" + > + Oppdater passord + </button> + </div> + </div> + </div> + <ModalComponent + :title="'Passordet er oppdatert ✨'" + :message="'Passordet ditt er nÃ¥ oppdatert. Du kan nÃ¥ logge inn med ditt nye passord.'" + :is-modal-open="isModalOpen" + @close="isModalOpen = false" + > + <template v-slot:buttons> + <button + @click="goToLogin" + class="active-button font-bold py-2 px-4 w-1/2 hover:bg-[#f7da7c] border-2 border-[#f7da7c] disabled:border-transparent" + > + Logg inn + </button> + </template> + </ModalComponent> + </div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue' +import { useRoute, useRouter } from 'vue-router' +import axios from 'axios' +import ToolTip from '@/components/ToolTip.vue' +import ModalComponent from '@/components/ModalComponent.vue' + +const route = useRoute() +const router = useRouter() + +const resetID = ref(route.query.resetID) +const userID = ref(route.query.userID) +const newPassword = ref<string>('') +const confirm = ref<string>('') +const showPassword = ref<boolean>(false) +const isModalOpen = ref<boolean>(false) + +const passwordRegex = /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!])(?=\S+$).{8,}$/ + +const isPasswordValid = computed(() => passwordRegex.test(newPassword.value)) + +const canResetPassword = computed(() => { + return isPasswordValid.value && newPassword.value === confirm.value && confirm.value !== '' +}) + +const resetPassword = async () => { + isModalOpen.value = true + + const resetPasswordDTO = { + resetID: resetID.value, + userID: userID.value, + newPassword: newPassword.value + } + try { + await axios.post('http://localhost:8080/forgotPassword/resetPassword', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(resetPasswordDTO) + }) + } catch (error) { + const err = error as Error + console.error(err.message) + } +} + +const toggleShowPassword = () => { + showPassword.value = !showPassword.value +} + +const goToLogin = () => { + isModalOpen.value = false + router.push('/logginn') +} +</script> diff --git a/src/views/StartView.vue b/src/views/StartView.vue index a57dfcaa9d2c5605849f32435304738235a5dbb5..8646db14f5c3fc58b81d2144a766bb0b9397cfaf 100644 --- a/src/views/StartView.vue +++ b/src/views/StartView.vue @@ -22,7 +22,7 @@ > <button class="md:py-3 md:px-0 md:w-3/4 py-2 px-12 w-1/2 border border-[#95E35D] shadow-lg rounded-lg transition-all duration-500 bg-[#95E35D] hover:bg-white text-sm md:text-base" - @click="goToLogin" + @click="goToRegister" > Start her </button> @@ -74,6 +74,10 @@ import { useRouter } from 'vue-router' const router = useRouter() const goToLogin = () => { - router.push('/login') + router.push('/logginn') +} + +const goToRegister = () => { + router.push('/registrer') } </script>