diff --git a/src/App.vue b/src/App.vue index 7c9c7769455d1bcd7dfb517dd65e6c06eb1a1be4..6de7916b23a1ebb6f33fb8bd5c8bae4edf83be8e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -23,7 +23,7 @@ const backgroundImageStyle = computed(() => { } } else { return { - backgroundImage: "none" + backgroundImage: 'none' } } }) @@ -153,21 +153,21 @@ const helpMessages = computed(() => { <RouterView /> </main> </div> - </template> - - <style> - nav { - display: flex; - justify-content: center; - gap: 1rem; - margin: 1rem 0; - } - - nav a.router-link-exact-active { - color: var(--color-text); - } - - nav a.router-link-exact-active:hover { - background-color: transparent; - } - </style> \ No newline at end of file +</template> + +<style> +nav { + display: flex; + justify-content: center; + gap: 1rem; + margin: 1rem 0; +} + +nav a.router-link-exact-active { + color: var(--color-text); +} + +nav a.router-link-exact-active:hover { + background-color: transparent; +} +</style> diff --git a/src/assets/base.css b/src/assets/base.css index 4d25b57a7854150d0981c93623e0b8c3e4d4fb40..db174147f881bd99eff24edb10a0ec0322d2a2c6 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -33,14 +33,14 @@ --section-gap: 160px; } -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; } -input[type="number"] { - -moz-appearance: textfield; /* Firefox */ +input[type='number'] { + -moz-appearance: textfield; /* Firefox */ } *, diff --git a/src/components/ButtonDisplayStreak.vue b/src/components/ButtonDisplayStreak.vue index 50ff4e445c0db7b234538d27824d34a65954824c..b9fbb10340301bf7a65c12b678844b7e5953771d 100644 --- a/src/components/ButtonDisplayStreak.vue +++ b/src/components/ButtonDisplayStreak.vue @@ -37,10 +37,22 @@ <Countdown v-if="screenSize > 768 && currentStreak! > 0" class="flex flex-row" - countdownSize="1rem" - labelSize=".5rem" - mainColor="white" - secondFlipColor="white" + countdownSize="1.4rem" + labelSize="0.8rem" + mainColor="black" + secondFlipColor="black" + mainFlipBackgroundColor="#30ab0e" + secondFlipBackgroundColor="#9af781" + :labels="{ days: 'dager', hours: 'timer', minutes: 'min', seconds: 'sek' }" + :deadlineISO="deadline" + ></Countdown> + <Countdown + v-if="screenSize <= 768 && currentStreak! > 0" + class="flex flex-row" + countdownSize="1.1rem" + labelSize=".6rem" + mainColor="black" + secondFlipColor="black" mainFlipBackgroundColor="#30ab0e" secondFlipBackgroundColor="#9af781" :labels="{ days: 'dager', hours: 'timer', minutes: 'min', seconds: 'sek' }" @@ -51,23 +63,24 @@ class="flex flex-row items-center mx-auto h-20 w-4/5 md:w-full bg-black-400 gap-4" > <div class="flex flex-1 overflow-x-auto"> - <div v-for="index in 6" :key="index" class="min-w-max mx-auto"> + <div v-for="index in 7" :key="index" class="min-w-max mx-auto"> <div class="flex flex-col justify-around items-center"> - <span class="text-black text-xs md:text-1xl font-bold">{{ - currentStreak! - ((currentStreak! % 7) - index) - }}</span> - <!-- Conditional rendering for streak images --> + <!-- Display the current streak day number adjusted by index --> + <span class="text-black text-xs md:text-1xl font-bold"> + {{ currentStreak! - ((currentStreak! % 7) - index) }} + </span> + <!-- Display images based on completion --> <img - v-if="index - 1 < currentStreak! % 7" src="@/assets/pengesekkStreak.png" - alt="challenge completed" - class="max-h-6 max-w-6 md:max-h-10 md:max-w-10" - /> - <img - v-else - src="@/assets/pengesekkStreak.png" - alt="challenge not completed" - class="max-h-6 max-w-6 md:max-h-10 md:max-w-10 grayscale" + :alt=" + index <= currentStreak! % 7 + ? 'challenge completed' + : 'challenge not completed' + " + :class="{ + 'max-h-6 max-w-6 md:max-h-10 md:max-w-10': true, + grayscale: index > currentStreak! % 7 + }" /> </div> </div> @@ -79,21 +92,19 @@ </template> <script setup lang="ts"> -import { onMounted, onUnmounted, ref, watch } from 'vue' +import { onMounted, onUnmounted, ref } from 'vue' import { useUserStore } from '@/stores/userStore' // @ts-ignore import { Countdown } from 'vue3-flip-countdown' const userStore = useUserStore() const currentStreak = ref<number>() -const streakStart = ref<string>() const deadline = ref<string>() onMounted(async () => { - await userStore.getUserStreak() + userStore.getUserStreak() if (userStore.streak) { currentStreak.value = userStore.streak?.streak - streakStart.value = userStore.streak?.streakStart - deadline.value = userStore.streak?.streakStart + deadline.value = userStore.streak?.firstDue } console.log('Streak:', currentStreak.value) if (typeof window !== 'undefined') { @@ -111,21 +122,13 @@ const handleWindowSizeChange = () => { screenSize.value = window.innerWidth } -watch( - () => currentStreak.value, - (newStreak, oldStreak) => { - if (newStreak !== oldStreak) { - currentStreak.value = newStreak - console.log('Updated Steak:', currentStreak) - } - }, - { immediate: true } -) - const displayStreakCard = ref(false) const display = () => { displayStreakCard.value = true + userStore.getUserStreak() + currentStreak.value = userStore.streak?.streak + deadline.value = userStore.streak?.firstDue } const hide = () => { diff --git a/src/components/CardChallengeSavingsPath.vue b/src/components/CardChallengeSavingsPath.vue new file mode 100644 index 0000000000000000000000000000000000000000..cb34ba2490ebcaa4e16a54ec2ac4823e210e4a0e --- /dev/null +++ b/src/components/CardChallengeSavingsPath.vue @@ -0,0 +1,113 @@ +<template> + <!-- Challenge Icon and Details --> + <div + v-if="challenge" + class="flex items-center justify-center shadow-black min-w-24 w-full h-auto md:max-h-full min-h-24 max-w-32 max-h-32 md:min-h-32 md:min-w-32 md:max-w-48 overflow-hidden" + > + <!-- Challenge Icon --> + <div class="flex flex-col items-center mx-auto md:mx-2 my-auto"> + <div class="flex flex-col flex-nowrap self-center"> + <!-- Check Icon --> + <div + v-if="challenge.completion !== undefined && challenge.completion >= 100" + class="min-w-6 min-h-6 max-w-6 max-h-6 md:min-h-8 md:max-h-8 md:min-w-8 md:max-w-8 ml-20 md:ml-32 p-1 basis-1/4 self-end" + > + <img src="@/assets/completed.png" alt="" />ï¸ + </div> + <div + v-else + class="min-w-6 min-h-6 max-w-6 max-h-6 md:min-h-8 md:max-h-8 md:min-w-8 md:max-w-8 ml-20 md:ml-32 p-1 basis-1/4 self-end" + > + <img src="@/assets/pending.png" alt="" />ï¸ + </div> + <div class="basis-3/4"> + <p + class="text-center text-wrap text-xs lg:text-lg md:text-md" + data-cy="challenge-title" + > + {{ challenge.title }} + </p> + </div> + </div> + <img + @click="editChallenge(challenge)" + :data-cy="'challenge-icon-' + challenge.id" + :src="getChallengeIcon(challenge)" + class="max-w-12 max-h-12 md:max-h-16 md:max-w-16 lg:max-w-20 lg:max-h-20 cursor-pointer hover:scale-125" + :alt="challenge.title" + /> + <!-- Progress Bar, if the challenge is not complete --> + <div + v-if="challenge.completion != undefined && challenge.completion < 100" + class="flex-grow w-full mt-2" + > + <div class="flex flex-row ml-5 md:ml-10 justify-center"> + <div class="flex flex-col"> + <div class="bg-gray-200 rounded-full h-2.5 dark:bg-gray-700"> + <div + class="bg-green-600 h-2.5 rounded-full" + data-cy="challenge-progress" + :style="{ + width: (challenge.saved / challenge.target) * 100 + '%' + }" + ></div> + </div> + <div class="text-center text-nowrap text-xs md:text-base"> + {{ challenge.saved }}kr / {{ challenge.target }}kr + </div> + </div> + + <button + @click="incrementSaved(challenge)" + :data-cy="'increment-challenge' + challenge.id" + type="button" + class="inline-block mb-2 ml-2 h-7 w-8 rounded-full p-1 uppercase leading-normal transition duration-150 ease-in-out focus:bg-green-accent-300 focus:shadow-green-2 focus:outline-none focus:ring-0 active:bg-green-600 active:shadow-green-200 motion-reduce:transition-none dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong" + > + + + </button> + </div> + </div> + <span v-else class="text-center text-xs md:text-base" + >Ferdig: {{ challenge.saved }}</span + > + </div> + </div> +</template> + +<script setup lang="ts"> +import type { Challenge } from '@/types/challenge' +import { useChallengeStore } from '@/stores/challengeStore' +import router from '@/router' + +const challengeStore = useChallengeStore() + +interface Props { + challenge: Challenge +} +defineProps<Props>() + +const emit = defineEmits(['update-challenge', 'complete-challenge']) + +// Increment saved amount +// In your incrementSaved function in the child component +const incrementSaved = async (challenge: Challenge) => { + challenge.saved += challenge.perPurchase + // Trigger the update in the store + + const updatedChallenge = (await challengeStore.editUserChallenge(challenge)) as Challenge + + console.log('updated challenge in child: ', updatedChallenge) + + // Emit an event to inform the parent component of the update + emit('update-challenge', updatedChallenge) +} + +const editChallenge = (challenge: Challenge) => { + router.push(`/spareutfordringer/rediger/${challenge.id}`) +} +// Helper methods to get icons +const getChallengeIcon = (challenge: Challenge): string => { + //TODO change to challenge.icon + return `src/assets/coffee.png` +} +</script> diff --git a/src/components/FormRegister.vue b/src/components/FormRegister.vue index ebaaf164832734d0b5a01a691c559abb1e5d983e..2863b6aa8d4f9fd47594ca36b2c5ba33d72bd2cc 100644 --- a/src/components/FormRegister.vue +++ b/src/components/FormRegister.vue @@ -137,7 +137,9 @@ watch( </div> <input v-model="confirm" - :class="{ 'border-2 border-lime-400': password == confirm && '' !== confirm.valueOf() }" + :class="{ + 'border-2 border-lime-400': password == confirm && '' !== confirm.valueOf() + }" class="mt-2" name="confirm" placeholder="Bekreft passord" @@ -145,7 +147,12 @@ watch( /> </div> <div class="flex flex-row gap-5"> - <button :disabled="isFormInvalid" class="grow-0 primary" name="submit" @click="submitForm"> + <button + :disabled="isFormInvalid" + class="grow-0 primary" + name="submit" + @click="submitForm" + > Registrer deg </button> <p>{{ errorMessage }}</p> diff --git a/src/components/ImgGifTemplate.vue b/src/components/ImgGifTemplate.vue index 46fecc79efe8cca7f8427a1a1e851e0c0ca15b79..f23f1723afa3e37e176788ddc3c565729e455fa0 100644 --- a/src/components/ImgGifTemplate.vue +++ b/src/components/ImgGifTemplate.vue @@ -1,10 +1,10 @@ <template> - <div class="hover:scale-125"> + <div class="hover:scale-110 flex justify-center items-center"> <img v-if="index % 6 === modValue" :src="url" alt="could not load" - class="h-32 w-32 border-2 rounded-lg border-stale-400 shadow-md shadow-black" + class="min-w-24 w-full h-auto min-h-24 max-w-32 max-h-32 md:min-h-32 md:max-h-44 md:min-w-32 md:max-w-44 border-2 rounded-lg border-stale-400 shadow-md shadow-black" /> </div> </template> diff --git a/src/components/NavBarComponent.vue b/src/components/NavBarComponent.vue index 795eca5619a508bcc3887ad1bbc547c3c74106cf..924816689e21f4e077342f8c2a448454838c8a5d 100644 --- a/src/components/NavBarComponent.vue +++ b/src/components/NavBarComponent.vue @@ -23,13 +23,14 @@ </div> <div v-if="!isHamburger" class="flex justify-center w-40"> - <button class="primary bg-[#95e35d] logout focus:ring focus:ring-black-300" @click="openModal">Logg ut</button> + <button + class="primary bg-[#95e35d] logout focus:ring focus:ring-black-300" + @click="openModal" + > + Logg ut + </button> </div> - <button - class="primary logout" - v-if="isHamburger" - @click="toggleMenu">☰ - </button> + <button class="primary logout" v-if="isHamburger" @click="toggleMenu">☰</button> </nav> <div v-if="hamburgerOpen" class="flex flex-col bg-white border border-slate-300 z-50"> @@ -39,9 +40,7 @@ >💰Spareutfordringer</router-link > <router-link to="/profil" @click="hamburgerOpen = false">ðŸ¤Profil</router-link> - <button class="focus:ring focus:ring-black-300" @click="openModal"> - Logg ut - </button> + <button class="focus:ring focus:ring-black-300" @click="openModal">Logg ut</button> </div> <ModalComponent :title="'Vil du logge ut?'" @@ -50,18 +49,8 @@ @close="isModalOpen = false" > <template v-slot:buttons> - <button - @click="logout" - class="primary" - > - Logg ut - </button> - <button - @click="closeModal" - class="primary danger" - > - Avbryt - </button> + <button @click="logout" class="primary">Logg ut</button> + <button @click="closeModal" class="primary danger">Avbryt</button> </template> </ModalComponent> </template> diff --git a/src/components/PageControl.vue b/src/components/PageControl.vue index 93ad991be3362b9ed171a251abe516ad3a074fd9..921f2c9f9d9651bbccebef8116c4b19a18a99ab8 100644 --- a/src/components/PageControl.vue +++ b/src/components/PageControl.vue @@ -17,15 +17,19 @@ defineProps({ <template> <div v-if="totalPages > 0" class="flex justify-center gap-4"> - <button + <button class="primary" - :disabled="currentPage === 0" @click="onPageChange(currentPage - 1)"> + :disabled="currentPage === 0" + @click="onPageChange(currentPage - 1)" + > Forrige </button> <p>{{ currentPage + 1 }} / {{ totalPages }}</p> - <button + <button class="primary" - :disabled="currentPage === totalPages - 1" @click="onPageChange(currentPage + 1)"> + :disabled="currentPage === totalPages - 1" + @click="onPageChange(currentPage + 1)" + > Neste </button> </div> diff --git a/src/components/ProgressBar.vue b/src/components/ProgressBar.vue index 65dd661902437170a404f3a34228a85a99257040..3a29f2fa6eec6da409198d57d50b13e84cd2fefc 100644 --- a/src/components/ProgressBar.vue +++ b/src/components/ProgressBar.vue @@ -6,8 +6,7 @@ defineProps({ <template> <div class="w-full bg-gray-200 rounded-full overflow-hidden"> - <div :style="{ width: completion + '%' }" class="bg-lime-400 h-2 rounded-full"> - </div> + <div :style="{ width: completion + '%' }" class="bg-lime-400 h-2 rounded-full"></div> </div> </template> diff --git a/src/components/SavingsPath.vue b/src/components/SavingsPath.vue index e0eefe109987a664c2953f47ffc8b0e2583186e4..ba90e65812ff6047a6fba7f2df80db6b54252812 100644 --- a/src/components/SavingsPath.vue +++ b/src/components/SavingsPath.vue @@ -1,5 +1,6 @@ <template> <div + v-if="isMounted" class="flex flex-col basis-2/3 max-h-full mx-auto md:ml-20 md:mr-2 max-w-5/6 md:basis-3/4 md:max-pr-20 md:pr-10 md:max-mr-20" > <div class="flex justify-center align-center"> @@ -10,6 +11,7 @@ </span> </div> <button + v-if="!allChallengesCompleted()" class="h-auto w-auto absolute flex text-center self-end mr-10 md:mr-20 text-wrap shadow-sm shadow-black sm:top-50 sm:text-xs sm:mr-20 lg:mr-32 top-60 z-50 p-2 text-xs md:text-sm" @click="scrollToFirstUncompleted" v-show="!isAtFirstUncompleted" @@ -18,6 +20,7 @@ </button> <div class="h-1 w-4/6 mx-auto my-2 opacity-10"></div> <div + v-if="challengesLocal" ref="containerRef" class="container relative pt-6 w-4/5 bg-cover bg-[center] md:[background-position: center;] mx-auto md:w-4/5 no-scrollbar h-full max-h-[60vh] md:max-h-[60vh] md:min-w-2/5 overflow-y-auto border-2 border-transparent rounded-xl bg-white shadow-lg shadow-slate-400" style="background-image: url('src/assets/backgroundSavingsPath.png')" @@ -27,7 +30,7 @@ </div> <div - v-for="(challenge, index) in challenges" + v-for="(challenge, index) in challengesLocal" :key="challenge.id" class="flex flex-col items-center" :ref="(el) => assignRef(el, challenge, index)" @@ -35,12 +38,12 @@ <!-- Challenge Row --> <div :class="{ - 'justify-end md:mx-auto md:justify-between': index % 2 === 1, - 'justify-start md:justify-between md:mx-auto': index % 2 === 0 + 'justify-center mx-auto md:justify-between': index % 2 === 1, + 'justify-center md:justify-between mx-auto': index % 2 === 0 }" - class="flex flex-row w-4/5 gap-8" + class="flex flex-row w-full md:w-4/5 justify-start gap-4 md:gap-8" > - <div class="right-auto just"> + <div class="flex"> <img-gif-template :index="index" :mod-value="1" @@ -57,83 +60,12 @@ url="src/assets/archerSpare.gif" ></img-gif-template> </div> - <!-- Challenge Icon and Details --> + <card-challenge-savings-path + :goal="goalLocal!" + :challenge="challenge" + @update-challenge="handleChallengeUpdate" + ></card-challenge-savings-path> <div class="flex"> - <!-- Challenge Icon --> - <div class="flex flex-col items-center gap-4"> - <div class="flex flex-row flex-nowrap"> - <p - class="text-center text-wrap text-xs md:text-lg" - data-cy="challenge-title" - > - {{ challenge.title }} - </p> - <display-info-for-challenge-or-goal - :goal="goal" - :challenge="challenge" - :is-challenge="true" - ></display-info-for-challenge-or-goal> - </div> - <img - @click="editChallenge(challenge)" - :data-cy="'challenge-icon-' + challenge.id" - :src="getChallengeIcon(challenge)" - class="max-w-20 max-h-20 cursor-pointer hover:scale-125" - :alt="challenge.title" - /> - <!-- Progress Bar, if the challenge is not complete --> - <div - v-if=" - challenge.completion != undefined && challenge.completion < 100 - " - class="flex-grow w-full mt-2" - > - <div class="flex flex-row ml-5 md:ml-10 justify-center"> - <div class="flex flex-col"> - <div - class="bg-gray-200 rounded-full h-2.5 dark:bg-gray-700" - > - <div - class="bg-green-600 h-2.5 rounded-full" - data-cy="challenge-progress" - :style="{ - width: - (challenge.saved / challenge.target) * 100 + - '%' - }" - ></div> - </div> - <div class="text-center text-xs md:text-base"> - {{ challenge.saved }}kr / {{ challenge.target }}kr - </div> - </div> - - <button - @click="incrementSaved(challenge)" - :data-cy="'increment-challenge' + challenge.id" - type="button" - class="inline-block mb-2 ml-2 h-7 w-8 rounded-full p-1 uppercase leading-normal transition duration-150 ease-in-out focus:bg-green-accent-300 focus:shadow-green-2 focus:outline-none focus:ring-0 active:bg-green-600 active:shadow-green-200 motion-reduce:transition-none dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong" - > - + - </button> - </div> - </div> - <span v-else class="text-center text-xs md:text-base" - >Ferdig: {{ challenge.saved }}</span - > - </div> - <!-- Check Icon --> - <div - v-if="challenge.completion !== undefined && challenge.completion >= 100" - class="md:max-w-10 min-w-4 max-w-6 max-h-6 w-full h-auto md:max-h-10 min-h-4" - > - <img src="@/assets/completed.png" alt="" />ï¸ - </div> - <div v-else class="max-w-6 max-h-6"> - <img src="@/assets/pending.png" alt="" />ï¸ - </div> - </div> - <div class=""> <img-gif-template :index="index" :mod-value="0" @@ -152,26 +84,30 @@ </div> </div> <!-- Piggy Steps, centered --> - <div v-if="index !== challenges.length" class="flex justify-center w-full"> + <div v-if="index !== challengesLocal.length" class="flex justify-center w-full"> <img :src="getPigStepsIcon()" :class="{ 'transform scale-x-[-1]': index % 2 === 0 }" - class="w-20 h-20" + class="w-20 md:w-24 lg:w-32 h-20 md:h-24 lg:h-32" alt="Pig Steps" /> </div> <div - v-if="index === challenges.length - 1 && index % 2 === 0" + v-if="index === challengesLocal.length - 1 && index % 2 === 0" class="flex flex-row mt-2" > <button class="text-2xl ml-48" @click="addSpareUtfordring">+</button> - <span class="">Legg til <br />Spareutfordring</span> + <p class="">Legg til <br />Spareutfordring</p> </div> - <div v-else-if="index === challenges.length - 1 && index % 2 !== 0" class="mr-40"> + <div + v-else-if="index === challengesLocal.length - 1 && index % 2 !== 0" + class="mr-20 flex flex-row" + > <button class="text-2xl ml-10 rounded-full" @click="addSpareUtfordring"> + </button> + <p class="">Legg til <br />Spareutfordring</p> </div> <!-- Finish line --> </div> @@ -182,28 +118,27 @@ /> </div> <!-- Goal --> - <div v-if="goal" class="flex flex-row justify-around m-t-2 pt-6 w-full mx-auto"> + <div v-if="goalLocal" class="flex flex-row justify-around m-t-2 pt-6 w-full mx-auto"> <div class="grid grid-rows-2 grid-flow-col gap 4"> - <div class="row-span-3 cursor-pointer" @click="editGoal(goal)"> - <img :src="getGoalIcon(goal)" class="w-12 h-12 mx-auto" :alt="goal.title" /> - <div class="text-lg font-bold" data-cy="goal-title">{{ goal.title }}</div> + <div class="row-span-3 cursor-pointer" @click="editGoal(goalLocal)"> + <img + :src="getGoalIcon(goalLocal)" + class="w-12 h-12 mx-auto" + :alt="goalLocal.title" + /> + <div class="text-lg font-bold" data-cy="goal-title">{{ goalLocal.title }}</div> </div> - <display-info-for-challenge-or-goal - class="col-span-2" - :goal="goal" - :challenge="null" - :is-challenge="false" - ></display-info-for-challenge-or-goal> </div> <div class="flex flex-col items-end"> <div @click="goToEditGoal" class="cursor-pointer"> <h3 class="text-blue-500 text-base">Endre mÃ¥l</h3> </div> <div + :key="componentKey" ref="targetRef" class="bg-yellow-400 px-4 py-1 rounded-full text-black font-bold" > - {{ goal.saved }}kr / {{ goal.target }}kr + {{ goalLocal.saved }}kr / {{ goalLocal.target }}kr </div> </div> </div> @@ -215,6 +150,13 @@ ref="iconRef" class="max-w-20 max-h-20 absolute opacity-0" /> + <img + v-if="goalLocal" + :src="getGoalIcon(goalLocal)" + alt="could not load" + ref="goalIconRef" + class="shadow-sm shadow-amber-300 max-w-20 max-h-20 absolute opacity-0" + /> </template> <script setup lang="ts"> @@ -225,8 +167,7 @@ import { onUnmounted, reactive, type Ref, - ref, - watch + ref } from 'vue' import anime from 'animejs' import type { Challenge } from '@/types/challenge' @@ -234,13 +175,11 @@ import type { Goal } from '@/types/goal' import confetti from 'canvas-confetti' import { useRouter } from 'vue-router' import { useGoalStore } from '@/stores/goalStore' -import { useChallengeStore } from '@/stores/challengeStore' -import DisplayInfoForChallengeOrGoal from '@/components/DisplayInfoForChallengeOrGoal.vue' import ImgGifTemplate from '@/components/ImgGifTemplate.vue' +import CardChallengeSavingsPath from '@/components/CardChallengeSavingsPath.vue' const router = useRouter() const goalStore = useGoalStore() -const challengeStore = useChallengeStore() interface Props { challenges: Challenge[] @@ -248,86 +187,59 @@ interface Props { } const props = defineProps<Props>() -const challenges = ref<Challenge[]>(props.challenges) -const goal = ref<Goal | null | undefined>(props.goal) +const challengesLocal = ref<Challenge[]>() +let goalLocal: Goal | null | undefined = reactive({ + title: '', // Default empty string to prevent undefined errors + saved: 0, + target: 0 +} as Goal) +const isMounted = ref<boolean>(false) +const componentKey = ref<number>(0) + +//Initialisation: onMounted(async () => { - await goalStore.getUserGoals() window.addEventListener('resize', handleWindowSizeChange) handleWindowSizeChange() + challengesLocal.value = props.challenges + goalLocal = props.goal sortChallenges() -}) - -const sortChallenges = () => { - challenges.value.sort((a, b) => { - // First, sort by completion status: non-completed (less than 100) before completed (100) - if (a.completion !== 100 && b.completion === 100) { - return 1 // 'a' is not completed and 'b' is completed, 'a' should come first - } else if (a.completion === 100 && b.completion !== 100) { - return -1 // 'a' is completed and 'b' is not, 'b' should come first - } else { - // Explicitly convert dates to numbers for subtraction - const dateA = new Date(a.due).getTime() - const dateB = new Date(b.due).getTime() - return dateA - dateB + allChallengesCompleted() + // Delay the execution of the following logic by 300ms + setTimeout(() => { + const container = containerRef.value + if (container) { + container.addEventListener('scroll', () => { + if (!firstUncompletedRef.value) return + const containerRect = container.getBoundingClientRect() + const firstUncompletedRect = firstUncompletedRef.value.getBoundingClientRect() + isAtFirstUncompleted.value = !( + firstUncompletedRect.top > containerRect.bottom || + firstUncompletedRect.bottom < containerRect.top + ) + }) } - }) -} - -const screenSize = ref<number>(window.innerWidth) - -onUnmounted(() => { - window.removeEventListener('resize', handleWindowSizeChange) -}) -const handleWindowSizeChange = () => { - screenSize.value = window.innerWidth -} - -interface ElementRefs { - [key: string]: HTMLElement | undefined -} - -const elementRefs = reactive<ElementRefs>({}) + scrollToFirstUncompleted() + }, 300) // Timeout set to 300 milliseconds + // Load existing animated states first + loadAnimatedStates() -const isAtFirstUncompleted = ref(false) // This state tracks visibility of the button -const firstUncompletedRef: Ref<HTMLElement | undefined> = ref() + // Get completed challenge IDs, ensuring that only defined IDs are considered + const completedChallenges = challengesLocal.value + .filter((challenge) => challenge.completion! >= 100 && challenge.id !== undefined) + .map((challenge) => challenge.id as number) // Use 'as number' to assert that ids are numbers after the check -function scrollToFirstUncompleted() { - let found = false - for (let i = 0; i < challenges.value.length; i++) { - if (challenges.value[i].completion! < 100) { - const refKey = `uncompleted-${i}` - if (elementRefs[refKey]) { - elementRefs[refKey]!.scrollIntoView({ behavior: 'smooth', block: 'start' }) - firstUncompletedRef.value = elementRefs[refKey] // Store the reference - found = true - isAtFirstUncompleted.value = true - break - } - } - } - if (!found) { - isAtFirstUncompleted.value = false - } -} + // Update only new completions that are not already in the animatedChallenges + const newAnimations = completedChallenges.filter((id) => !animatedChallenges.value.includes(id)) + animatedChallenges.value = [...animatedChallenges.value, ...newAnimations] -onMounted(() => { - const container = containerRef.value - if (container) { - container.addEventListener('scroll', () => { - if (!firstUncompletedRef.value) return - const containerRect = container.getBoundingClientRect() - const firstUncompletedRect = firstUncompletedRef.value.getBoundingClientRect() - isAtFirstUncompleted.value = !( - firstUncompletedRect.top > containerRect.bottom || - firstUncompletedRect.bottom < containerRect.top - ) - }) - } - scrollToFirstUncompleted() + // Save the updated list back to localStorage + localStorage.setItem('animatedChallenges', JSON.stringify(animatedChallenges.value)) + isMounted.value = true }) onUnmounted(() => { + window.removeEventListener('resize', handleWindowSizeChange) const container = containerRef.value if (container) { container.removeEventListener('scroll', () => { @@ -336,257 +248,448 @@ onUnmounted(() => { } }) -const assignRef = ( - el: Element | ComponentPublicInstance | null, - challenge: Challenge, - index: number -) => { - const refKey = `uncompleted-${index}` - if (el instanceof HTMLElement) { - // Ensure that el is an HTMLElement - if (challenge.completion! < 100) { - elementRefs[refKey] = el +const handleChallengeUpdate = (updatedChallenge: Challenge) => { + if (challengesLocal.value) { + const index = challengesLocal.value.findIndex((c) => c.id === updatedChallenge.id) + if (index !== -1) { + challengesLocal.value[index] = { ...updatedChallenge } } - } else { - // Cleanup if the element is unmounted or not an HTMLElement - if (elementRefs[refKey]) { - delete elementRefs[refKey] + + if ( + updatedChallenge.completion! >= 100 && + !animatedChallenges.value.includes(updatedChallenge.id as number) + ) { + animateChallenge(updatedChallenge) + saveAnimatedStateChallenge(updatedChallenge) + } + + if (goalLocal) { + incrementGoalSaved(updatedChallenge) + // Force component update right here might be more appropriate + componentKey.value++ } } } -// Utilizing watch to specifically monitor for changes in the props -watch( - () => props.goal, - (newGoal, oldGoal) => { - if (newGoal !== oldGoal) { - goal.value = newGoal - } - }, - { immediate: true } -) - -watch( - () => props.challenges, - (newChallenges, oldChallenges) => { - if (newChallenges !== oldChallenges) { - challenges.value = newChallenges - sortChallenges() +const incrementGoalSaved = async (challenge: Challenge) => { + if (goalLocal) { + // Correct the addition mistake and remove setTimeout + goalLocal.saved = goalLocal.saved + challenge.perPurchase + await nextTick() // Only add the perPurchase amount + + const completion = (goalLocal.saved / goalLocal.target) * 100 + if (completion >= 100 && !animatedGoals.value.includes(goalLocal.id as number)) { + animateGoal(goalLocal) + setTimeout(() => { + goalStore.getUserGoals() + goalLocal = goalStore.priorityGoal + }, 4000) // Keep this delay only for the store update and goal switch + } else { + await goalStore.getUserGoals() + goalLocal = goalStore.priorityGoal } - }, - { immediate: true } -) -// Reactive references for DOM elements -const iconRef = ref<HTMLElement | null>(null) -const containerRef = ref<HTMLElement | null>(null) -const targetRef = ref<HTMLElement | null>(null) - -// Define your goal + } +} -// AddSpareUtfordring +/** + * Navigates to the spareutfordringer page + */ const addSpareUtfordring = () => { - console.log('Attempting to navigate to /spareutfordringer') router.push('/spareutfordringer').catch((error) => { console.error('Routing error:', error) }) } -// Increment saved amount -const incrementSaved = async (challenge: Challenge) => { - // Safely increment the saved amount, ensuring it exists - challenge.saved += challenge.perPurchase - - // Check if the saved amount meets or exceeds the target - if (challenge.saved >= challenge.target) { - challenge.completion = 100 - await challengeStore.completeUserChallenge(challenge) - } - - // Safely update the goal's saved value, ensuring goal.value exists and is not null - if (goal.value) { - goal.value.saved = (goal.value.saved || 0) + challenge.perPurchase - // Update the goal in the store, ensuring goal is not null or undefined - if (goal.value) { - await goalStore.editUserGoal(goal.value) - } - } else { - console.error('No goal selected for incrementing saved value.') - } - - // Update the challenge in the store - await challengeStore.editUserChallenge(challenge) -} - -const recalculateAndAnimate = () => { - nextTick(() => { - if (iconRef.value && containerRef.value && targetRef.value) { - animateIcon() - } else { - console.error('Element references are not ready.') +/** + * Checks if all challenges are completed + */ +const allChallengesCompleted = () => { + // Assuming challenges.value is an array of challenge objects + if (challengesLocal.value) { + for (const challenge of challengesLocal.value) { + if (challenge.completion !== 100) { + return false // If any challenge is not completed, return false + } } - }) + return true + } // If all challenges are completed, return true } -const editChallenge = (challenge: Challenge) => { - router.push(`/spareutfordringer/rediger/${challenge.id}`) -} +//-----------Animation for goal and challenge completion-----------------// -const editGoal = (goal: Goal) => { - router.push(`/sparemaal/rediger/${goal.id}`) -} +// Reactive references for DOM elements +const iconRef = ref<HTMLElement | null>(null) +const goalIconRef = ref<HTMLElement | null>(null) +const containerRef = ref<HTMLElement | null>(null) +const targetRef = ref<HTMLElement | null>(null) // Declare the ref with a type annotation for an array of strings const animatedChallenges: Ref<number[]> = ref([]) +const animatedGoals: Ref<number[]> = ref([]) +/** + * Loads the states for animated goals and challenges + */ const loadAnimatedStates = () => { const animated = localStorage.getItem('animatedChallenges') + const animatedG = localStorage.getItem('animatedGoals') animatedChallenges.value = animated ? JSON.parse(animated) : [] + animatedGoals.value = animatedG ? JSON.parse(animatedG) : [] } -const saveAnimatedState = (challenge: Challenge) => { - console.log('Saving animated state for 1:', challenge.id) - if (challenge.id != null) { - animatedChallenges.value.push(challenge.id) - } - console.log('Saving animated state for:', challenge.title) - localStorage.setItem('animatedChallenges', JSON.stringify(animatedChallenges.value)) -} - +/** + * Saves the animated state for challenge + * triggers the confetti method + * triggers the recalculation of dom positioning + * @param challenge + */ const animateChallenge = (challenge: Challenge) => { if ( - challenge.completion === 100 && + challenge.completion! >= 100 && !animatedChallenges.value.includes(challenge.id as number) ) { - console.log('Animating for:', challenge.title) if (challenge.id != null) { animatedChallenges.value.push(challenge.id) } // Ensure no duplication - saveAnimatedState(challenge) // Refactor this to update localStorage correctly + saveAnimatedStateChallenge(challenge) // Refactor this to update localStorage correctly triggerConfetti() - recalculateAndAnimate() + recalculateAndAnimate(false) } } -const triggerConfetti = () => { - confetti({ - particleCount: 400, - spread: 80, - origin: { x: 0.8, y: 0.8 } - }) +/** + * Saves the animated state for goal + * triggers the confetti method + * triggers the recalculation of dom positioning + * @param goal + */ +const animateGoal = (goal: Goal) => { + console.log('im in animated goal') + + if (goal.id != null) { + animatedGoals.value.push(goal.id) + } // Ensure no duplication + saveAnimatedStateGoal(goal) // Refactor this to update localStorage correctly + triggerConfetti() + recalculateAndAnimate(true) } -watch( - challenges, - (newChallenges) => { - newChallenges.forEach((challenge) => { - //wait for 300ms before animating maybe? - nextTick(() => { - if (challenge.completion === 100) { - if (!animatedChallenges.value.includes(challenge.id as number)) { - console.log(!animatedChallenges.value.includes(challenge.id as number)) - console.log('Animating challenge in watcher:', challenge.id) - animateChallenge(challenge) - saveAnimatedState(challenge) // Refactor this to update localStorage correctly - } - } - }) - }) - }, - { deep: true } -) -onMounted(() => { - // Load existing animated states first - loadAnimatedStates() - - // Get completed challenge IDs, ensuring that only defined IDs are considered - const completedChallenges = challenges.value - .filter((challenge) => challenge.completion === 100 && challenge.id !== undefined) - .map((challenge) => challenge.id as number) // Use 'as number' to assert that ids are numbers after the check - - // Update only new completions that are not already in the animatedChallenges - const newAnimations = completedChallenges.filter((id) => !animatedChallenges.value.includes(id)) - animatedChallenges.value = [...animatedChallenges.value, ...newAnimations] +/** + * Recalculates the position of the dom elements + * @param isGoal + */ +const recalculateAndAnimate = (isGoal: boolean) => { + console.log('im in recalculate and animate') + + if (!isGoal && iconRef.value && containerRef.value && targetRef.value) { + animateIcon(isGoal) + } else if (isGoal && containerRef.value && goalIconRef.value) { + animateIcon(isGoal) + } else if (!isGoal && !targetRef.value) { + animateIcon(isGoal) + } else { + console.error('Element references are not ready.') + } +} - // Save the updated list back to localStorage +/** + * Saves the animated state for challenge + * @param challenge + */ +const saveAnimatedStateChallenge = (challenge: Challenge) => { + if (challenge.id != null) { + animatedChallenges.value.push(challenge.id) + } localStorage.setItem('animatedChallenges', JSON.stringify(animatedChallenges.value)) -}) +} + +/** + * Saves the animated state for goal + * @param goal + */ +const saveAnimatedStateGoal = (goal: Goal) => { + console.log('Saving animated state for:', goal.id) + if (goal.id != null) { + animatedGoals.value.push(goal.id) + } + localStorage.setItem('animatedGoals', JSON.stringify(animatedGoals.value)) +} -const animateIcon = () => { +/** + * animates the icon images + * @param isGoal + */ +const animateIcon = (isGoal: boolean) => { + console.log('im in animate icon') const icon = iconRef.value const container = containerRef.value const target = targetRef.value - if (!icon || !container || !target) { + if (!icon || !container) { console.error('Required animation elements are not available.') return } - + // Obtain bounding rectangles safely const containerRect = container.getBoundingClientRect() - const targetRect = target.getBoundingClientRect() + const targetRect = target?.getBoundingClientRect() const iconRect = icon.getBoundingClientRect() - const translateX1 = - containerRect.left + containerRect.width / 2 - iconRect.width / 2 - iconRect.left - const translateY1 = - containerRect.top + containerRect.height / 2 - iconRect.height / 2 - iconRect.top - - const translateX2 = targetRect.left + targetRect.width / 2 - iconRect.width / 2 - iconRect.left - const translateY2 = targetRect.top + targetRect.height / 2 - iconRect.height / 2 - iconRect.top - - anime - .timeline({ - easing: 'easeInOutQuad', - duration: 1500 - }) - .add({ - targets: icon, - translateX: translateX1, - translateY: translateY1, - opacity: 0, // Start invisible - duration: 1000 - }) - .add({ - targets: icon, - opacity: 1, // Reveal the icon once it starts moving to the container - duration: 1000, // Make the opacity change almost instantaneously - scale: 3 - }) - .add({ - targets: icon, - translateX: translateX2, - translateY: translateY2, - scale: 0.5, - opacity: 1, // Keep the icon visible while moving to the target - duration: 1500 - }) - .add({ - targets: icon, - opacity: 0, // Fade out once it reaches the target - scale: 1, - duration: 500 - }) - .add({ - targets: icon, - translateX: 0, // Reset translation to original - translateY: 0, // Reset translation to original - duration: 500 - }) + // Initialize translation coordinates + let translateX1 = 0, + translateY1 = 0, + translateX2 = 0, + translateY2 = 0 + + if (isGoal) { + const goal = goalIconRef.value + if (goal) { + const goalRect = goal.getBoundingClientRect() + if (goalRect) { + // Calculate the translation coordinates for the goal + translateX1 = + containerRect.left + + containerRect.width / 2 - + goalRect.width / 2 - + goalRect.left + translateY1 = + containerRect.top + + containerRect.height / 2 - + goalRect.height / 2 - + goalRect.top + + anime + .timeline({ + easing: 'easeInOutQuad', + duration: 1500 + }) + .add({ + targets: goal, + translateX: translateX1, + translateY: translateY1, + opacity: [0, 1], // Fix: start from 0 opacity and animate to 1 + duration: 1000 + }) + .add({ + targets: goal, + opacity: [1, 0], // Fade out after moving + duration: 3000, + scale: 3, + begin: function (anim) { + if (icon) icon.classList.add('glow') // Ensure icon exists before applying class + }, + complete: function (anim) { + if (icon) icon.classList.remove('glow') // Clean up: remove class after animation + } + }) + } else { + console.error('Goal rectangle is not available.') + } + } else { + console.error('Goal element is not available.') + } + } else if (!isGoal && target && targetRect) { + // Calculate the translation coordinates for the icon + translateX1 = + containerRect.left + containerRect.width / 2 - iconRect.width / 2 - iconRect.left + translateY1 = + containerRect.top + containerRect.height / 2 - iconRect.height / 2 - iconRect.top + translateX2 = targetRect.left + targetRect.width / 2 - iconRect.width / 2 - iconRect.left + translateY2 = targetRect.top + targetRect.height / 2 - iconRect.height / 2 - iconRect.top + + anime + .timeline({ + easing: 'easeInOutQuad', + duration: 1500 + }) + .add({ + targets: icon, + translateX: translateX1, + translateY: translateY1, + opacity: 0, // Start invisible + duration: 1000 + }) + .add({ + targets: icon, + opacity: 1, // Reveal the icon once it starts moving to the container + duration: 1000, // Make the opacity change almost instantaneously + scale: 3 + }) + .add({ + targets: icon, + translateX: translateX2, + translateY: translateY2, + scale: 0.5, + opacity: 1, // Keep the icon visible while moving to the target + duration: 1500 + }) + .add({ + targets: icon, + opacity: 0, // Fade out once it reaches the target + scale: 1, + duration: 500 + }) + .add({ + targets: icon, + translateX: 0, // Reset translation to original + translateY: 0, // Reset translation to original + duration: 500 + }) + } else if (!isGoal && !target) { + // Calculate the translation coordinates for the icon + translateX1 = + containerRect.left + containerRect.width / 2 - iconRect.width / 2 - iconRect.left + translateY1 = + containerRect.top + containerRect.height / 2 - iconRect.height / 2 - iconRect.top + anime + .timeline({ + easing: 'easeInOutQuad', + duration: 1500 + }) + .add({ + targets: icon, + translateX: translateX1, + translateY: translateY1, + opacity: 0, // Start invisible + duration: 1000 + }) + .add({ + targets: icon, + opacity: 1, // Reveal the icon once it starts moving to the container + duration: 1000, // Make the opacity change almost instantaneously + scale: 3 + }) + .add({ + targets: icon, + opacity: 0, // Fade out once it reaches the target + scale: 1, + duration: 500 + }) + .add({ + targets: icon, + translateX: 0, // Reset translation to original + translateY: 0, // Reset translation to original + duration: 500 + }) + } } - -// Helper methods to get icons -const getChallengeIcon = (challenge: Challenge): string => { - return `src/assets/${challenge.type.toLowerCase()}.png` +/** + * Triggers confeti animation + */ +const triggerConfetti = () => { + confetti({ + particleCount: 400, + spread: 80, + origin: { x: 0.8, y: 0.8 } + }) } +//fetching images const getGoalIcon = (goal: Goal): string => { - return `src/assets/${goal.title.toLowerCase()}.png` + if (goal) { + return `src/assets/${goal.title.toLowerCase()}.png` + } else { + return 'src/assets/pengesekkStreak.png' + } } const getPigStepsIcon = () => { return 'src/assets/pigSteps.png' } const goToEditGoal = () => { - router.push({ name: 'edit-goal', params: { id: goal.value?.id } }) + router.push({ name: 'edit-goal', params: { id: goalLocal?.id } }) +} + +const editGoal = (goal: Goal) => { + router.push(`/sparemaal/rediger/${goal.id}`) +} + +/** + * Sorts the challenges by completion status and due date + + */ +const sortChallenges = () => { + if (challengesLocal.value) { + challengesLocal.value.sort((a, b) => { + // First, sort by completion status: non-completed (less than 100) before completed (100) + if (a.completion !== 100 && b.completion === 100) { + return 1 // 'a' is not completed and 'b' is completed, 'a' should come first + } else if (a.completion === 100 && b.completion !== 100) { + return -1 // 'a' is completed and 'b' is not, 'b' should come first + } else { + // Explicitly convert dates to numbers for subtraction + const dateA = new Date(a.due).getTime() + const dateB = new Date(b.due).getTime() + return dateA - dateB + } + }) + } +} + +// Interface for element references +interface ElementRefs { + [key: string]: HTMLElement | undefined +} + +const elementRefs = reactive<ElementRefs>({}) +const isAtFirstUncompleted = ref(false) +const firstUncompletedRef: Ref<HTMLElement | undefined> = ref() +const screenSize = ref<number>(window.innerWidth) + +/** + * Handles the window size change event + */ +const handleWindowSizeChange = () => { + screenSize.value = window.innerWidth +} + +/** + * Scrolls to the first uncompleted challenge + + */ +const scrollToFirstUncompleted = () => { + if (challengesLocal.value) { + let found = false + for (let i = 0; i < challengesLocal.value.length; i++) { + if (challengesLocal.value[i].completion! < 100) { + const refKey = `uncompleted-${i}` + if (elementRefs[refKey]) { + elementRefs[refKey]!.scrollIntoView({ behavior: 'smooth', block: 'start' }) + firstUncompletedRef.value = elementRefs[refKey] // Store the reference + found = true + isAtFirstUncompleted.value = true + break + } + } + } + if (!found) { + isAtFirstUncompleted.value = false + } + } +} + +/** + * Assigns the reference to the element + * @param el + * @param challenge + * @param index + */ +const assignRef = ( + el: Element | ComponentPublicInstance | null, + challenge: Challenge, + index: number +) => { + const refKey = `uncompleted-${index}` + if (el instanceof HTMLElement) { + // Ensure that el is an HTMLElement + if (challenge.completion! < 100) { + elementRefs[refKey] = el + } + } else { + // Cleanup if the element is unmounted or not an HTMLElement + if (elementRefs[refKey]) { + delete elementRefs[refKey] + } + } } </script> diff --git a/src/components/__tests__/savingsPathTest.spec.ts b/src/components/__tests__/savingsPathTest.spec.ts deleted file mode 100644 index 40739f5a4e0a9b042239fadd17c40fe3ae9c6a61..0000000000000000000000000000000000000000 --- a/src/components/__tests__/savingsPathTest.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' -import SavingsPath from '@/components/SavingsPath.vue' - -vi.mock('canvas-confetti', () => ({ - default: vi.fn(() => ({ - reset: vi.fn(), - addFettis: vi.fn(), - render: vi.fn(), - clear: vi.fn() - })) -})) -const mocks = vi.hoisted(() => ({ - get: vi.fn(), - post: vi.fn() -})) - -vi.mock('axios', async (importActual) => { - const actual = await importActual<typeof import('axios')>() - - return { - default: { - ...actual.default, - create: vi.fn(() => ({ - ...actual.default.create(), - get: mocks.get, - post: mocks.post - })) - } - } -}) - -describe('SavingsPath Component', () => { - let wrapper: any - const pinia = createPinia() - - beforeEach(() => { - window.HTMLElement.prototype.scrollIntoView = function () {} - setActivePinia(pinia) - wrapper = mount(SavingsPath, { - global: { - plugins: [pinia] - }, - props: { - challenges: [ - { - id: 1, - title: 'Test challenge', - perPurchase: 20, - saved: 100, - target: 1000, - description: 'Test description', - due: '2022-01-01T00:00:00Z', - createdOn: '2021-01-01T00:00:00Z', - type: 'Challenge type', - completion: 10 - } - ], - goal: { - id: 1, - title: 'Test goal', - saved: 100, - target: 1000, - description: 'Test description', - due: '2022-01-01T00:00:00Z', - createdOn: '2021-01-01T00:00:00Z', - completion: 10 - } - } - }) - }) - - describe('Initial Render', () => { - it('should render challenge and goal details correctly', async () => { - await wrapper.vm.$nextTick() - const challengeText = wrapper.text() - expect(challengeText).toContain('Test challenge') - expect(challengeText).toContain('100kr / 1000kr') - expect(challengeText).toContain('Test goal') - expect(challengeText).toContain('100kr / 1000kr') - }) - - it('should display the correct number of challenge elements', () => { - const challengeElements = wrapper.findAll('[data-cy="challenge-title"]') - expect(challengeElements.length).toBe(1) - }) - }) - - describe('User Interactions', () => { - it('should update challenge progress when increment button is clicked', async () => { - await wrapper.vm.$nextTick() - const incrementButton = wrapper.find('[data-cy="increment-challenge1"]') - expect(incrementButton.exists()).toBe(true) - await incrementButton.trigger('click') - expect(wrapper.vm.challenges[0].saved).toBe(120) - }) - }) - - describe('State Management', () => { - it('should react to changes in challenge completion status', async () => { - // Initially incomplete - let progressBar = wrapper.find('.bg-green-600') - expect(progressBar.element.style.width).toBe('10%') - - // Update challenge to be almost complete - await wrapper.setProps({ - challenges: [ - { - ...wrapper.props().challenges[0], - saved: 900, - completion: 90 - } - ] - }) - await wrapper.vm.$nextTick() - await wrapper.vm.$nextTick() - - progressBar = wrapper.find('.bg-green-600') - expect(progressBar.element.style.width).toBe('90%') - }) - }) -}) diff --git a/src/stores/challengeStore.ts b/src/stores/challengeStore.ts index a8417aa62bf51495a6a06b401145b0fe06f34a06..7eb27b71f09821a97a22ee79e76055909b34a8ff 100644 --- a/src/stores/challengeStore.ts +++ b/src/stores/challengeStore.ts @@ -12,7 +12,6 @@ export const useChallengeStore = defineStore('challenge', () => { const response = await authInterceptor('/challenges') if (response.data && response.data.content) { challenges.value = response.data.content - console.log('Fetched Challenges:', challenges.value) } else { challenges.value = [] console.error('No challenge content found:', response.data) @@ -33,12 +32,15 @@ export const useChallengeStore = defineStore('challenge', () => { if (index !== -1) { challenges.value[index] = { ...challenges.value[index], ...response.data } console.log('Updated Challenge:', response.data) + return challenges.value[index] } } else { console.error('No challenge content found in response data') + return null } } catch (error) { console.error('Error updating challenge:', error) + return null } } const completeUserChallenge = async (challenge: Challenge) => { @@ -53,12 +55,16 @@ export const useChallengeStore = defineStore('challenge', () => { if (index !== -1) { challenges.value[index] = { ...challenges.value[index], ...response.data } console.log('Updated Challenge:', response.data) + console.log('Challenge Completed store:', challenges.value[index]) + return challenges.value[index] } } else { console.error('No challenge content found in response data') + return null } } catch (error) { console.error('Error updating challenge:', error) + return null } } diff --git a/src/stores/goalStore.ts b/src/stores/goalStore.ts index 30367ad29f6151e418d24d6a29ca53b6632963ed..ce033826cace512c30dacc7d9d297d6fe9e9e037 100644 --- a/src/stores/goalStore.ts +++ b/src/stores/goalStore.ts @@ -5,13 +5,21 @@ import authInterceptor from '@/services/authInterceptor' export const useGoalStore = defineStore('goal', () => { const goals = ref<Goal[]>([]) + const priorityGoal = ref<Goal | null>(null) const getUserGoals = async () => { try { const response = await authInterceptor('/goals') if (response.data && response.data.content) { goals.value = response.data.content + for (const goal of goals.value) { + if (goal.priority === 1) { + priorityGoal.value = goal + break + } else { + priorityGoal.value = null + } + } console.log(response.data.content) - console.log('Fetched Goals:', goals.value) } else { goals.value = [] console.error('No goal content found:', response.data) @@ -21,6 +29,7 @@ export const useGoalStore = defineStore('goal', () => { goals.value = [] // Ensure challenges is always an array } } + // Assuming 'challenges' is a reactive state in your store that holds the list of challenges const editUserGoal = async (goal: Goal) => { if (!goal || goal.id === null) { @@ -34,7 +43,6 @@ export const useGoalStore = defineStore('goal', () => { const index = goals.value.findIndex((g) => g.id === goal.id) if (index !== -1) { goals.value[index] = { ...goals.value[index], ...response.data } - console.log('Updated Goal:', response.data) } } else { console.error('No goal content found in response data') @@ -45,6 +53,7 @@ export const useGoalStore = defineStore('goal', () => { } return { goals, + priorityGoal, getUserGoals, editUserGoal } diff --git a/src/types/streak.ts b/src/types/streak.ts index 61dafae24c954c7ca99a3596ca43a659f1ef122c..f49346322939e0b0517ebca400e5fcaeccddedc3 100644 --- a/src/types/streak.ts +++ b/src/types/streak.ts @@ -1,4 +1,5 @@ export interface Streak { streakStart?: string streak?: number + firstDue?: string } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 052d5d3d447ecd1f0b8e4686a625a2d7417c7f8c..2aae863d362ac964b2e3b00a2da7cae122b6f178 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -26,7 +26,7 @@ /> </div> </div> - <savings-path :challenges="challenges" :goal="goal"></savings-path> + <savings-path v-if="isMounted" :challenges="challenges" :goal="goal"></savings-path> </div> <GeneratedChallengesModal v-show="showModal" @update:showModal="showModal = $event" /> </template> @@ -50,25 +50,23 @@ const challengeStore = useChallengeStore() const speech = ref<string[]>([]) const challenges = ref<Challenge[]>([]) -const goals = ref<Goal[]>([]) const showWelcome = ref<boolean>(false) const goal = ref<Goal | null | undefined>(null) +const isMounted = ref(false) onMounted(async () => { await goalStore.getUserGoals() await challengeStore.getUserChallenges() challenges.value = challengeStore.challenges - goals.value = goalStore.goals - goal.value = goals.value[0] - console.log('Goals:', goals.value) - + goal.value = goalStore.priorityGoal const lastModalShow = localStorage.getItem('lastModalShow') if (!lastModalShow || Date.now() - Number(lastModalShow) >= 24 * 60 * 60 * 1000) { showModal.value = true } firstLoggedInSpeech() SpareSpeech() + isMounted.value = true }) const firstLoggedInSpeech = () => { diff --git a/src/views/ManageChallengeView.vue b/src/views/ManageChallengeView.vue index 20fcdfd86ac8663a43ac07040729108f8e3c2921..a60084093ad69cd9d586c5cc61fb759cc6610a50 100644 --- a/src/views/ManageChallengeView.vue +++ b/src/views/ManageChallengeView.vue @@ -8,22 +8,20 @@ import ModalComponent from '@/components/ModalComponent.vue' const router = useRouter() -const modalTitle = ref(''); -const modalMessage = ref(''); -const confirmModalOpen = ref(false); -const errorModalOpen = ref(false); +const modalTitle = ref('') +const modalMessage = ref('') +const confirmModalOpen = ref(false) +const errorModalOpen = ref(false) const oneWeekFromNow = new Date() oneWeekFromNow.setDate(oneWeekFromNow.getDate() + 7) const minDate = new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().slice(0, 10) const selectedDate = ref<string>(minDate) - const thirtyDaysFromNow = new Date() thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30) const maxDate = thirtyDaysFromNow.toISOString().slice(0, 10) - const challengeInstance = ref<Challenge>({ title: '', perPurchase: 0, @@ -33,10 +31,9 @@ const challengeInstance = ref<Challenge>({ due: '' }) - watch(selectedDate, (newDate) => { - challengeInstance.value.due = newDate; -}); + challengeInstance.value.due = newDate +}) const isEdit = computed(() => router.currentRoute.value.name === 'edit-challenge') const pageTitle = computed(() => (isEdit.value ? 'Rediger utfordring🎨' : 'Ny utfordring🎨')) @@ -46,48 +43,48 @@ const completion = computed( ) function validateInputs() { - const errors = []; + const errors = [] - challengeInstance.value.due = selectedDate.value + 'T23:59:59.999Z'; + challengeInstance.value.due = selectedDate.value + 'T23:59:59.999Z' if (!challengeInstance.value.title || challengeInstance.value.title.length > 20) { - errors.push("Tittelen mÃ¥ være mellom 1 og 20 tegn."); + errors.push('Tittelen mÃ¥ være mellom 1 og 20 tegn.') } if (challengeInstance.value.description.length > 280) { - errors.push("Beskrivelsen mÃ¥ være under 280 tegn."); + errors.push('Beskrivelsen mÃ¥ være under 280 tegn.') } if (challengeInstance.value.target <= 0) { - errors.push("MÃ¥lbeløpet mÃ¥ være større enn 0."); + errors.push('MÃ¥lbeløpet mÃ¥ være større enn 0.') } if (new Date(challengeInstance.value.due) < new Date(minDate)) { - errors.push("Forfallsdatoen mÃ¥ være minst en uke frem i tid."); + errors.push('Forfallsdatoen mÃ¥ være minst en uke frem i tid.') } if (challengeInstance.value.perPurchase <= 0) { - errors.push("Pris per sparing mÃ¥ være større enn 0."); + errors.push('Pris per sparing mÃ¥ være større enn 0.') } - return errors; + return errors } -const submitAction = async() => { - const errors = validateInputs(); - if(errors.length > 0) { - const formatErrors = errors.join('\n'); - modalTitle.value = 'Oops! Noe er feil med det du har fylt ut🚨'; - modalMessage.value = formatErrors; - errorModalOpen.value = true; - return; +const submitAction = async () => { + const errors = validateInputs() + if (errors.length > 0) { + const formatErrors = errors.join('\n') + modalTitle.value = 'Oops! Noe er feil med det du har fylt ut🚨' + modalMessage.value = formatErrors + errorModalOpen.value = true + return } try { if (isEdit.value) { - updateChallenge(); + updateChallenge() } else { - createChallenge(); + createChallenge() } } catch (error) { - console.error(error); - modalTitle.value = 'Systemfeil'; - modalMessage.value = 'En feil oppstod under lagring av utfordringen.'; - errorModalOpen.value = true; + console.error(error) + modalTitle.value = 'Systemfeil' + modalMessage.value = 'En feil oppstod under lagring av utfordringen.' + errorModalOpen.value = true } } @@ -135,15 +132,16 @@ const updateChallenge = () => { } const cancelCreation = () => { - if (challengeInstance.value.title !== '' || + if ( + challengeInstance.value.title !== '' || challengeInstance.value.description !== '' || - challengeInstance.value.perPurchase !== 0 || - challengeInstance.value.saved !== 0 || - challengeInstance.value.target !== 0) - { - modalTitle.value = 'Du er i ferd med Ã¥ avbryte redigeringen🚨'; - modalMessage.value = 'Er du sikker pÃ¥ at du vil avbryte?'; - confirmModalOpen.value = true; + challengeInstance.value.perPurchase !== 0 || + challengeInstance.value.saved !== 0 || + challengeInstance.value.target !== 0 + ) { + modalTitle.value = 'Du er i ferd med Ã¥ avbryte redigeringen🚨' + modalMessage.value = 'Er du sikker pÃ¥ at du vil avbryte?' + confirmModalOpen.value = true } else { router.push({ name: 'challenges' }) } @@ -151,9 +149,8 @@ const cancelCreation = () => { const confirmCancel = () => { router.push({ name: 'challenges' }) - confirmModalOpen.value = false; + confirmModalOpen.value = false } - </script> <template> @@ -224,24 +221,20 @@ const confirmCancel = () => { type="date" /> </div> - + <div class="flex flex-col"> <p>Last opp ikon for utfordringen📸</p> - <button class="mt-2 font-bold cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-90">💾</button> + <button + class="mt-2 font-bold cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-90" + > + 💾 + </button> </div> </div> <div class="flex flex-row justify-between w-full"> + <button class="primary danger" @click="cancelCreation" v-text="'Avbryt'" /> - <button - class="primary danger" - @click="cancelCreation" - v-text="'Avbryt'" - /> - - <button - class="primary" - @click="submitAction" - v-text="submitButton" /> + <button class="primary" @click="submitAction" v-text="submitButton" /> </div> <ModalComponent :title="modalTitle" @@ -249,18 +242,13 @@ const confirmCancel = () => { :isModalOpen="errorModalOpen" @close="errorModalOpen = false" > - <template v-slot:input> - <div class="flex justify-center items-center"> - <div class="flex flex-col gap-5"> - <button - class="primary" - @click="errorModalOpen = false" - > - Lukk - </button> + <template v-slot:input> + <div class="flex justify-center items-center"> + <div class="flex flex-col gap-5"> + <button class="primary" @click="errorModalOpen = false">Lukk</button> + </div> </div> - </div> - </template> + </template> </ModalComponent> <ModalComponent @@ -269,24 +257,16 @@ const confirmCancel = () => { :isModalOpen="confirmModalOpen" @close="confirmModalOpen = false" > - <template v-slot:input> - <div class="flex justify-center items-center"> - <div class="flex flex-col gap-5"> - <button - class="primary" - @click="confirmCancel" - > - Bekreft - </button> - <button - class="primary danger" - @click="confirmModalOpen = false" - > - Avbryt - </button> + <template v-slot:input> + <div class="flex justify-center items-center"> + <div class="flex flex-col gap-5"> + <button class="primary" @click="confirmCancel">Bekreft</button> + <button class="primary danger" @click="confirmModalOpen = false"> + Avbryt + </button> + </div> </div> - </div> - </template> + </template> </ModalComponent> </div> </div> diff --git a/src/views/ManageGoalView.vue b/src/views/ManageGoalView.vue index f3fd1a1850d1ee962e2ca24cc592745e29ef6e53..d2d8e75bc429e79a1f0aed0dc3f498f6960c541a 100644 --- a/src/views/ManageGoalView.vue +++ b/src/views/ManageGoalView.vue @@ -11,13 +11,11 @@ const router = useRouter() const minDate = new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().slice(0, 10) const selectedDate = ref<string>(minDate) - const modalMessage = ref<string>('') const modalTitle = ref<string>('') const errorModalOpen = ref<boolean>(false) const confirmModalOpen = ref<boolean>(false) - const goalInstance = ref<Goal>({ title: '', saved: 0, @@ -27,8 +25,8 @@ const goalInstance = ref<Goal>({ }) watch(selectedDate, (newDate) => { - goalInstance.value.due = newDate; -}); + goalInstance.value.due = newDate +}) const isEdit = computed(() => router.currentRoute.value.name === 'edit-goal') const pageTitle = computed(() => (isEdit.value ? 'Rediger sparemÃ¥l🎨' : 'Nytt sparemÃ¥l🎨')) @@ -36,55 +34,54 @@ const submitButton = computed(() => (isEdit.value ? 'Oppdater' : 'Opprett')) const completion = computed(() => (goalInstance.value.saved / goalInstance.value.target) * 100) function validateInputs() { - const errors = []; + const errors = [] - goalInstance.value.due = selectedDate.value + 'T23:59:59.999Z'; + goalInstance.value.due = selectedDate.value + 'T23:59:59.999Z' if (!goalInstance.value.title) { - errors.push('Tittel mÃ¥ fylles ut'); + errors.push('Tittel mÃ¥ fylles ut') } - if (!goalInstance.value.target ) { - errors.push('MÃ¥lbeløp mÃ¥ fylles ut'); + if (!goalInstance.value.target) { + errors.push('MÃ¥lbeløp mÃ¥ fylles ut') } if (!goalInstance.value.due) { - errors.push('Forfallsdato mÃ¥ fylles ut'); + errors.push('Forfallsdato mÃ¥ fylles ut') } if (goalInstance.value.target < 1) { - errors.push('MÃ¥lbeløp mÃ¥ være større enn 0'); + errors.push('MÃ¥lbeløp mÃ¥ være større enn 0') } if (goalInstance.value.saved < 0) { - errors.push('Sparebeløp kan ikke være negativt'); + errors.push('Sparebeløp kan ikke være negativt') } if (goalInstance.value.saved > goalInstance.value.target) { - errors.push('Sparebeløp kan ikke være større enn mÃ¥lbeløp'); + errors.push('Sparebeløp kan ikke være større enn mÃ¥lbeløp') } - return errors; - + return errors } -const submitAction = async() => { - const errors = validateInputs(); - if(errors.length > 0) { - const formatErrors = errors.join('\n'); - modalTitle.value = 'Oops! Noe er feil med det du har fylt ut🚨'; - modalMessage.value = formatErrors.replace(/\n/g, "<br>") - errorModalOpen.value = true; - return; +const submitAction = async () => { + const errors = validateInputs() + if (errors.length > 0) { + const formatErrors = errors.join('\n') + modalTitle.value = 'Oops! Noe er feil med det du har fylt ut🚨' + modalMessage.value = formatErrors.replace(/\n/g, '<br>') + errorModalOpen.value = true + return } try { if (isEdit.value) { - updateGoal(); + updateGoal() } else { - createGoal(); + createGoal() } } catch (error) { - console.error(error); - modalTitle.value = 'Systemfeil'; - modalMessage.value = 'En feil oppstod under lagring av utfordringen.'; - errorModalOpen.value = true; + console.error(error) + modalTitle.value = 'Systemfeil' + modalMessage.value = 'En feil oppstod under lagring av utfordringen.' + errorModalOpen.value = true } } @@ -145,13 +142,15 @@ const deleteGoal = () => { } function cancelCreation() { - if (goalInstance.value.title !== '' || - goalInstance.value.description !== '' || + if ( + goalInstance.value.title !== '' || + goalInstance.value.description !== '' || goalInstance.value.target !== 0 || - selectedDate.value !== '') { - modalTitle.value = 'Du er i ferd med Ã¥ avbryte redigeringen🚨'; - modalMessage.value = 'Er du sikker pÃ¥ at du vil avbryte?'; - confirmModalOpen.value = true; + selectedDate.value !== '' + ) { + modalTitle.value = 'Du er i ferd med Ã¥ avbryte redigeringen🚨' + modalMessage.value = 'Er du sikker pÃ¥ at du vil avbryte?' + confirmModalOpen.value = true } else { router.push({ name: 'goals' }) } @@ -159,7 +158,7 @@ function cancelCreation() { const confirmCancel = () => { router.push({ name: 'goals' }) - confirmModalOpen.value = false; + confirmModalOpen.value = false } </script> @@ -216,7 +215,11 @@ const confirmCancel = () => { </div> <div class="flex flex-col"> <p>Last opp ikon for utfordringen📸</p> - <button class="mt-2 font-bold cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-90">💾</button> + <button + class="mt-2 font-bold cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-90" + > + 💾 + </button> </div> </div> @@ -233,10 +236,7 @@ const confirmCancel = () => { @click="cancelCreation" v-text="'Avbryt'" /> - <button - class="primary" - @click="submitAction" v-text="submitButton" - /> + <button class="primary" @click="submitAction" v-text="submitButton" /> </div> <ModalComponent :title="modalTitle" @@ -244,18 +244,13 @@ const confirmCancel = () => { :isModalOpen="errorModalOpen" @close="errorModalOpen = false" > - <template v-slot:input> - <div class="flex justify-center items-center"> - <div class="flex flex-col gap-5"> - <button - class="primary" - @click="errorModalOpen = false" - > - Lukk - </button> + <template v-slot:input> + <div class="flex justify-center items-center"> + <div class="flex flex-col gap-5"> + <button class="primary" @click="errorModalOpen = false">Lukk</button> + </div> </div> - </div> - </template> + </template> </ModalComponent> <ModalComponent @@ -264,24 +259,16 @@ const confirmCancel = () => { :isModalOpen="confirmModalOpen" @close="confirmModalOpen = false" > - <template v-slot:input> - <div class="flex justify-center items-center"> - <div class="flex flex-col gap-5"> - <button - class="primary" - @click="confirmCancel" - > - Bekreft - </button> - <button - class="primary danger" - @click="confirmModalOpen = false" - > - Avbryt - </button> + <template v-slot:input> + <div class="flex justify-center items-center"> + <div class="flex flex-col gap-5"> + <button class="primary" @click="confirmCancel">Bekreft</button> + <button class="primary danger" @click="confirmModalOpen = false"> + Avbryt + </button> + </div> </div> - </div> - </template> + </template> </ModalComponent> </div> </div> @@ -291,4 +278,4 @@ const confirmCancel = () => { .no-rezise { resize: none; } -</style> \ No newline at end of file +</style> diff --git a/src/views/UserChallengesView.vue b/src/views/UserChallengesView.vue index dbc1e82c8c2fcf54f6aa1cd7d80e951fd9eb7a68..2e05bb08119724ddaa01620cda852b335942b490 100644 --- a/src/views/UserChallengesView.vue +++ b/src/views/UserChallengesView.vue @@ -50,9 +50,7 @@ onMounted(async () => { <h1 class="font-bold text-center">Dine utfordringer</h1> <div class="flex flex-col gap-5 items-center"> <div class="flex flex-row gap-5"> - <button - class="primary" - @click="router.push({ name: 'new-challenge' })"> + <button class="primary" @click="router.push({ name: 'new-challenge' })"> Opprett en ny utfordring </button> </div> @@ -64,9 +62,7 @@ onMounted(async () => { :key="challenge.id" :challenge-instance="challenge" /> - <p v-if="!activeChallenges"> - Du har ingen aktive spareutfordringer😢 - </p> + <p v-if="!activeChallenges">Du har ingen aktive spareutfordringer😢</p> </div> <PageControl :currentPage="currentPageActive" @@ -91,5 +87,4 @@ onMounted(async () => { </div> </template> -<style scoped> -</style> \ No newline at end of file +<style scoped></style> diff --git a/src/views/UserGoalsView.vue b/src/views/UserGoalsView.vue index b41e7c44b7c36f44f09436d8d57f1ba38313dcfd..734f469364f66c7002b66895c3b7ef0de6b97cc2 100644 --- a/src/views/UserGoalsView.vue +++ b/src/views/UserGoalsView.vue @@ -60,9 +60,9 @@ const changeOrder = async () => { <template> <div class="flex flex-col gap-5 items-center"> <h1 class="font-bold m-0">Dine sparemÃ¥l</h1> - <button - class="primary" - @click="router.push({ name: 'new-goal' })">Opprett et nytt sparemÃ¥l</button> + <button class="primary" @click="router.push({ name: 'new-goal' })"> + Opprett et nytt sparemÃ¥l + </button> <h2 class="font-bold m-0">Aktive sparemÃ¥l🚀</h2> <p v-if="activeGoals.length === 0">Du har ingen aktive sparemÃ¥l</p> <draggable @@ -87,18 +87,20 @@ const changeOrder = async () => { </draggable> <button class="primary secondary" - :disabled="activeGoals.length === 0" @click="changeOrder()"> + :disabled="activeGoals.length === 0" + @click="changeOrder()" + > {{ isDraggable ? 'Lagre rekkefølge' : 'Endre rekkefølge' }} </button> <h2 class="font-bold m-0">Fullførte sparemÃ¥l💯</h2> <p v-if="completedGoals.length === 0">Du har ingen fullførte sparemÃ¥l😢</p> <div v-else class="flex flex-row flex-wrap justify-center gap-10"> - <CardGoal + <CardGoal class="border-2 border-slate-200 hover:bg-slate-50" - v-for="goal in completedGoals" - :key="goal.id" - :goal-instance="goal" - /> + v-for="goal in completedGoals" + :key="goal.id" + :goal-instance="goal" + /> </div> <PageControl :current-page="currentPage" diff --git a/src/views/ViewChallengeView.vue b/src/views/ViewChallengeView.vue index 76f11a2a0777ed49e23caf1b4291dbdd434a9d61..924d5ae1ab49df8b8b912655fb49b449b835224f 100644 --- a/src/views/ViewChallengeView.vue +++ b/src/views/ViewChallengeView.vue @@ -81,10 +81,10 @@ const completeChallenge = () => { <div class="flex flex-row flex-wrap items-center justify-center gap-10"> <div class="flex flex-col gap-5 max-w-96"> <button - class="w-min bg-transparent rounded-lg font-bold left-10 cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-100 justify-start" + class="w-min bg-transparent rounded-lg font-bold left-10 cursor-pointer transition-transform duration-300 ease-in-out hover:scale-110 hover:opacity-100 justify-start" @click="router.push({ name: 'challenges', params: { id: challengeInstance.id } })" > - 👈Oversikt + 👈Oversikt </button> <div @@ -108,17 +108,16 @@ const completeChallenge = () => { </p> <div class="justify-center pl-20"> <button - class="primary danger mt-2 rounded-2xl p-2 w-40" - @click=" - authInterceptor - .delete(`/challenges/${challengeInstance.id}`) - .then(() => router.push({ name: 'challenges' })) - .catch((error) => console.error(error)) - " - > - Slett - </button> - + class="primary danger mt-2 rounded-2xl p-2 w-40" + @click=" + authInterceptor + .delete(`/challenges/${challengeInstance.id}`) + .then(() => router.push({ name: 'challenges' })) + .catch((error) => console.error(error)) + " + > + Slett + </button> </div> </div> diff --git a/src/views/ViewGoalView.vue b/src/views/ViewGoalView.vue index 2c2fa3a3b2d930feb3c044b97fd05739310fca17..04f4b42d2cf34b1df7eba571c095f723e7740c90 100644 --- a/src/views/ViewGoalView.vue +++ b/src/views/ViewGoalView.vue @@ -73,13 +73,12 @@ const completeGoal = () => { <div class="flex flex-row flex-wrap items-center justify-center gap-10"> <div class="flex flex-col gap-5 max-w-96"> <button - class="w-min bg-transparent rounded-lg font-bold left-10 cursor-pointer transition-transform duration-200 ease-in-out hover:scale-110 hover:opacity-100 justify-start" - @click="router.push({ name: 'goals', params: { id: goalInstance.id } })" + class="w-min bg-transparent rounded-lg font-bold left-10 cursor-pointer transition-transform duration-200 ease-in-out hover:scale-110 hover:opacity-100 justify-start" + @click="router.push({ name: 'goals', params: { id: goalInstance.id } })" > - 👈Oversikt + 👈Oversikt </button> - <div class="flex flex-col justify-center border-2 rounded-3xl align-middle p-5 card-shadow overflow-hidden w-full" > @@ -91,9 +90,9 @@ const completeGoal = () => { <p class="text-wrap break-words">{{ goalInstance.description }}</p> <div> <img - class="w-20 h-20" - src="https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png" - alt="Profilbilde" + class="w-20 h-20" + src="https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png" + alt="Profilbilde" /> </div> </div> @@ -102,7 +101,8 @@ const completeGoal = () => { Du har spart {{ goalInstance.saved }}kr av {{ goalInstance.target }}kr </p> <ProgressBar :completion="completion" /> - <button class="primary secondary mt-6" + <button + class="primary secondary mt-6" v-if="!isCompleted" @click=" router.push({ @@ -124,7 +124,8 @@ const completeGoal = () => { > Slett </button> - <button class="primary mt-4" + <button + class="primary mt-4" v-if="!isCompleted" @click="completeGoal" v-text="'Marker mÃ¥let som ferdig'"