Skip to content
Snippets Groups Projects
Commit 33e514e8 authored by Eline Evje's avatar Eline Evje
Browse files

Merge branch 'dev' into test/unit-tests-component

parents 1a1050aa 3c7ae333
No related branches found
No related tags found
3 merge requests!66Final merge,!59Test/unit tests component,!4Pipeline fix
Pipeline #284859 failed
Showing
with 192 additions and 97 deletions
...@@ -17,19 +17,20 @@ const showNavBar = computed(() => { ...@@ -17,19 +17,20 @@ const showNavBar = computed(() => {
}) })
const backgroundImageStyle = computed(() => { const backgroundImageStyle = computed(() => {
if (showSti.value) { if (dontShowSti.value) {
console.log(dontShowSti.value)
return { return {
backgroundImage: "url('src/assets/sti.png')" backgroundImage: 'none'
} }
} else { } else {
return { return {
backgroundImage: 'none' backgroundImage: "url('src/assets/sti.png')"
} }
} }
}) })
const showSti = computed(() => { const dontShowSti = computed(() => {
return !( return (
route.path == '/' || route.path == '/' ||
route.path == '/registrer' || route.path == '/registrer' ||
route.path == '/logginn' || route.path == '/logginn' ||
...@@ -110,7 +111,7 @@ const helpMessages = computed(() => { ...@@ -110,7 +111,7 @@ const helpMessages = computed(() => {
messages.push( messages.push(
'Du kan også se hvor mye du har spart av utfordringen din, og hvor mye du har igjen' 'Du kan også se hvor mye du har spart av utfordringen din, og hvor mye du har igjen'
) )
} else if (route.path.startsWith('/sparemaal/rediger')) { } else if (route.path.startsWith('/sparemaal/rediger/ny')) {
messages.push('Her kan du opprette et nytt sparemål 🌸') messages.push('Her kan du opprette et nytt sparemål 🌸')
messages.push( messages.push(
'Tittel er navnet på sparemålet, og beskrivelse er en kort forklaring på hva sparemålet går ut på.' 'Tittel er navnet på sparemålet, og beskrivelse er en kort forklaring på hva sparemålet går ut på.'
...@@ -120,7 +121,7 @@ const helpMessages = computed(() => { ...@@ -120,7 +121,7 @@ const helpMessages = computed(() => {
) )
messages.push('Forfallsdato er datoen du ønsker å ha nådd sparemålet ditt.') messages.push('Forfallsdato er datoen du ønsker å ha nådd sparemålet ditt.')
messages.push('Lykke til med sparingen! 🌴') messages.push('Lykke til med sparingen! 🌴')
} else if (route.path.startsWith('/spareutfordring/rediger')) { } else if (route.path.startsWith('/spareutfordringer/ny')) {
messages.push('Her kan du opprette en ny utfordring ☕️') messages.push('Her kan du opprette en ny utfordring ☕️')
messages.push( messages.push(
'Tittel er navnet på utfordringen, og beskrivelse er en kort forklaring på hva utfordringen går ut på.' 'Tittel er navnet på utfordringen, og beskrivelse er en kort forklaring på hva utfordringen går ut på.'
......
src/assets/bakgrunn.png

72.3 KiB

src/assets/finishline2.png

220 KiB

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
src/assets/savingsPathBg.png

212 KiB

src/assets/start-sign.png

64.4 KiB

src/assets/varsel.png

25.5 KiB

<template> <template>
<button <button
class="primary w-full max-w-60 py-2 flex items-center justify-start pl-4 space-x-2 focus:outline-none focus:ring-2 focus:ring-opacity-50 shadow-md text-xs md:text-sm lg:text-base" class="primary w-full max-w-64 py-2 flex items-center justify-start pl-4 space-x-2 focus:outline-none focus:ring-2 focus:ring-opacity-50 shadow-md text-xs md:text-sm lg:text-base"
@click="routeToGoalOrChallenge" @click="routeToGoalOrChallenge"
> >
<svg <svg
......
<template> <template>
<p class="mt-2 font-bold">Streak</p>
<div class="flex flex-col items-center relative"> <div class="flex flex-col items-center relative">
<button <button
@mouseover="display" @mouseover="display"
...@@ -8,7 +9,7 @@ ...@@ -8,7 +9,7 @@
<img <img
src="@/assets/pengesekkStreak.png" src="@/assets/pengesekkStreak.png"
alt="streak" alt="streak"
class="mx-auto w-6 h-6 md:w-12 md:h-12" class="mx-auto w-10 h-10 md:w-12 md:h-12"
/> />
</button> </button>
...@@ -17,19 +18,20 @@ ...@@ -17,19 +18,20 @@
class="w-[30vh] h-[20vh] md:w-auto md:h-auto group z-50 bg-opacity-50 overflow-hidden absolute right-[-4rem] top-14 md:top-20 flex flex-col justify-evenly text-wrap" class="w-[30vh] h-[20vh] md:w-auto md:h-auto group z-50 bg-opacity-50 overflow-hidden absolute right-[-4rem] top-14 md:top-20 flex flex-col justify-evenly text-wrap"
> >
<div <div
class="flex flex-col justify-evenly w-full h-full py-2 px-4 md:py-0 bg-white rounded-2xl border-4 border-green-300" class="flex flex-col justify-evenly w-full h-full py-2 px-4 md:py-0 bg-white rounded-2xl border-2 border-slate-200"
> >
<span class="text-xs md:text-2xl font-bold text-black" <span class="text-xs md:text-2xl font-bold text-black"
>{{ currentStreak >{{ currentStreak
}}{{ }}{{
currentStreak === 1 ? ' utfodring fullført' : ' utfodringer fullført' currentStreak === 1 ? ' utfordring fullført' : ' utfordringer fullført🚀'
}} }}
streak</span </span>
> <p class="text-black text-xs md:text-1xl md:font-bold my-2">
<p class="text-black text-xs md:text-1xl md:font-bold">
{{ {{
currentStreak! > 0 currentStreak! > 0
? 'Bra jobba du har fullført ' + currentStreak + ' utfordringer på rad!' ? 'Bra jobba du har fullført ' +
currentStreak +
' utfordringer på rad! Din neste utfordring utløper om:'
: 'Du har ikke fullført en utfordring det siste. Fullfør en nå for å starte en streak!' : 'Du har ikke fullført en utfordring det siste. Fullfør en nå for å starte en streak!'
}} }}
</p> </p>
...@@ -128,6 +130,8 @@ const display = () => { ...@@ -128,6 +130,8 @@ const display = () => {
userStore.getUserStreak() userStore.getUserStreak()
currentStreak.value = userStore.streak?.streak currentStreak.value = userStore.streak?.streak
deadline.value = userStore.streak?.firstDue deadline.value = userStore.streak?.firstDue
console.log('Streak:', currentStreak.value)
console.log('Deadline:', deadline.value)
} }
const hide = () => { const hide = () => {
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="flex flex-col flex-nowrap self-center"> <div class="flex flex-col flex-nowrap self-center">
<!-- Check Icon --> <!-- Check Icon -->
<div <div
v-if="challenge.completion !== undefined && challenge.completion >= 100" v-if="challenge.completion !== undefined && challenge.completedOn !== null"
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" 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="" /> <img src="@/assets/completed.png" alt="" />
...@@ -33,19 +33,19 @@ ...@@ -33,19 +33,19 @@
@click="editChallenge(challenge)" @click="editChallenge(challenge)"
:data-cy="'challenge-icon-' + challenge.id" :data-cy="'challenge-icon-' + challenge.id"
:src="challengeImageUrl" :src="challengeImageUrl"
class="max-w-12 max-h-12 md:max-h-8 md:max-w-8 lg:max-w-10 lg:max-h-10 cursor-pointer hover:scale-125 rounded-sm" class="max-w-8 max-h-12 md:max-h-8 md:max-w-8 lg:max-w-10 lg:max-h-10 cursor-pointer hover:scale-125 rounded-sm"
:alt="challenge.title" :alt="challenge.title"
/> />
<!-- Progress Bar, if the challenge is not complete --> <!-- Progress Bar, if the challenge is not complete -->
<div <div
v-if="challenge.completion != undefined && challenge.completion < 100" v-if="challenge.completion != undefined && challenge.completedOn === null"
class="flex-grow w-full mt-2" class="flex-grow w-full mt-2"
> >
<div class="flex flex-row ml-5 md:ml-10 justify-center"> <div class="flex flex-row ml-5 md:ml-10 justify-center">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="bg-gray-200 rounded-full h-2.5 dark:bg-gray-700"> <div class="bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div <div
class="bg-green-600 h-2.5 rounded-full" class="bg-lime-400 h-2.5 rounded-full"
data-cy="challenge-progress" data-cy="challenge-progress"
:style="{ :style="{
width: (challenge.saved / challenge.target) * 100 + '%' width: (challenge.saved / challenge.target) * 100 + '%'
...@@ -55,19 +55,20 @@ ...@@ -55,19 +55,20 @@
<div class="text-center text-nowrap text-xs md:text-base"> <div class="text-center text-nowrap text-xs md:text-base">
{{ challenge.saved }}kr / {{ challenge.target }}kr {{ challenge.saved }}kr / {{ challenge.target }}kr
</div> </div>
<button
@click="incrementSaved(challenge)"
:data-cy="'increment-challenge' + challenge.id"
type="button"
class="primary text-xs ml-2 z-10 relative"
>
+ {{ challenge.perPurchase }}kr på {{ challenge.title }}
</button>
</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>
</div> </div>
<span v-else class="text-center text-xs md:text-base" <span
v-else-if="challenge.completedOn !== null"
class="text-center text-xs md:text-base"
>Ferdig: {{ challenge.saved }}</span >Ferdig: {{ challenge.saved }}</span
> >
</div> </div>
...@@ -112,7 +113,6 @@ const getChallengeIcon = async (challengeId: number) => { ...@@ -112,7 +113,6 @@ const getChallengeIcon = async (challengeId: number) => {
}) })
challengeImageUrl.value = URL.createObjectURL(imageResponse.data) challengeImageUrl.value = URL.createObjectURL(imageResponse.data)
} catch (error) { } catch (error) {
console.error('Failed to load challenge icon:', error)
challengeImageUrl.value = '/src/assets/star.png' // Fallback on error challengeImageUrl.value = '/src/assets/star.png' // Fallback on error
} }
} }
......
...@@ -84,7 +84,7 @@ watch( ...@@ -84,7 +84,7 @@ watch(
placeholder="Skriv inn passord" placeholder="Skriv inn passord"
/> />
<button <button
class="absolute right-0 top-1 bg-transparent hover:bg-transparent" class="absolute right-0 top-1 bg-transparent hover:bg-transparent mr-4 mt-1"
@click="toggleShowPassword" @click="toggleShowPassword"
> >
{{ showPassword ? '🔓' : '🔒' }} {{ showPassword ? '🔓' : '🔒' }}
......
...@@ -12,6 +12,7 @@ const confirm = ref<string>('') ...@@ -12,6 +12,7 @@ const confirm = ref<string>('')
const showPassword = ref<boolean>(false) const showPassword = ref<boolean>(false)
const errorMessage = ref<string>('') const errorMessage = ref<string>('')
const passwordValidations = ref<string[]>([])
const userStore = useUserStore() const userStore = useUserStore()
...@@ -42,6 +43,39 @@ const toggleShowPassword = () => { ...@@ -42,6 +43,39 @@ const toggleShowPassword = () => {
showPassword.value = !showPassword.value showPassword.value = !showPassword.value
} }
const validatePassword = () => {
const messages = []
const lengthValid = password.value.length >= 8 && password.value.length <= 30
const numberValid = /[0-9]/.test(password.value)
const lowercaseValid = /[a-zæøå]/.test(password.value)
const uppercaseValid = /[ÆØÅA-Z]/.test(password.value)
const specialCharacterValid = /[@#$%^&+=!]/.test(password.value)
const noSpacesValid = !/\s/.test(password.value)
if (!lengthValid) {
messages.push('Må være mellom 8 og 30 karakterer. ')
}
if (!numberValid) {
messages.push('Må inneholde minst ett tall. ')
}
if (!lowercaseValid) {
messages.push('Må inneholde minst én liten bokstav. ')
}
if (!uppercaseValid) {
messages.push('Må inneholde minst én stor bokstav. ')
}
if (!specialCharacterValid) {
messages.push('Må inneholde minst ett spesialtegn (@#$%^&+=!). ')
}
if (!noSpacesValid) {
messages.push('Må ikke inneholde mellomrom. ')
}
passwordValidations.value = messages
}
watch(password, validatePassword)
watch( watch(
() => userStore.errorMessage, () => userStore.errorMessage,
(newValue: string) => { (newValue: string) => {
...@@ -56,7 +90,7 @@ watch( ...@@ -56,7 +90,7 @@ watch(
<div class="flex flex-row justify-between mx-4"> <div class="flex flex-row justify-between mx-4">
<p>Fornavn*</p> <p>Fornavn*</p>
<ToolTip <ToolTip
:message="'Must include only letters, spaces, commas, apostrophes, periods, and hyphens. 1-30 characters long'" :message="'Må kun inneholde bokstaver, mellomrom, komma, apostrof, punktum, og bindestrek. 1-30 karakterer langt'"
/> />
</div> </div>
<input <input
...@@ -71,7 +105,7 @@ watch( ...@@ -71,7 +105,7 @@ watch(
<div class="flex flex-row justify-between mx-4"> <div class="flex flex-row justify-between mx-4">
<p>Etternavn*</p> <p>Etternavn*</p>
<ToolTip <ToolTip
:message="'Must include only letters, spaces, commas, apostrophes, periods, and hyphens. 1-30 characters long'" :message="'Må kun inneholde bokstaver, mellomrom, komma, apostrof, punktum, og bindestrek. 1-30 karakterer langt'"
/> />
</div> </div>
<input <input
...@@ -86,7 +120,7 @@ watch( ...@@ -86,7 +120,7 @@ watch(
<div class="flex flex-row justify-between mx-4"> <div class="flex flex-row justify-between mx-4">
<p>E-post*</p> <p>E-post*</p>
<ToolTip <ToolTip
:message="'Valid email: Starts with Norwegian letters, numbers, or special characters. Includes \@\ followed by a domain. Ends with 2-7 letters.'" :message="'Gyldig email: Må starte med norske bokstaver, tall, eller spesielle karakterer. Inkluderer \@\ fulgt av et domene. Ender med 2-7 bokstaver.'"
/> />
</div> </div>
<input <input
...@@ -101,7 +135,7 @@ watch( ...@@ -101,7 +135,7 @@ watch(
<div class="flex flex-row justify-between mx-4"> <div class="flex flex-row justify-between mx-4">
<p>Brukernavn*</p> <p>Brukernavn*</p>
<ToolTip <ToolTip
:message="'Must start with a letter and can include numbers and underscores. 3-30 characters long.'" :message="'Må starte med en bokstav og kan inneholde tall og understrek. 3-30 karakterer langt.'"
/> />
</div> </div>
<input <input
...@@ -116,7 +150,7 @@ watch( ...@@ -116,7 +150,7 @@ watch(
<div class="flex flex-row justify-between mx-4"> <div class="flex flex-row justify-between mx-4">
<p>Passord*</p> <p>Passord*</p>
<ToolTip <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.'" :message="'Må være minst 8 karakterer, inkludert et tall, en liten bokstav, en stor bokstav, et spesialtegn (@#$%^&+=!), og ingen mellomrom.'"
/> />
</div> </div>
<div class="relative"> <div class="relative">
...@@ -129,7 +163,7 @@ watch( ...@@ -129,7 +163,7 @@ watch(
:class="{ 'border-2 border-lime-400': isPasswordValid }" :class="{ 'border-2 border-lime-400': isPasswordValid }"
/> />
<button <button
class="absolute right-0 top-1 bg-transparent hover:bg-transparent" class="absolute right-0 top-1 bg-transparent hover:bg-transparent mr-4 mt-1"
@click="toggleShowPassword" @click="toggleShowPassword"
> >
{{ showPassword ? '🔓' : '🔒' }} {{ showPassword ? '🔓' : '🔒' }}
...@@ -145,6 +179,11 @@ watch( ...@@ -145,6 +179,11 @@ watch(
placeholder="Bekreft passord" placeholder="Bekreft passord"
type="password" type="password"
/> />
<div class="ml-4">
<p class="text-sm">
<span v-for="message in passwordValidations" :key="message">{{ message }}</span>
</p>
</div>
</div> </div>
<div class="flex flex-row gap-5"> <div class="flex flex-row gap-5">
<button <button
...@@ -155,7 +194,6 @@ watch( ...@@ -155,7 +194,6 @@ watch(
> >
Registrer deg Registrer deg
</button> </button>
<p>{{ errorMessage }}</p>
</div> </div>
</div> </div>
</template> </template>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
> >
<div class="relative bg-white pt-10 p-4 rounded-lg shadow-xl" style="width: 40rem"> <div class="relative bg-white pt-10 p-4 rounded-lg shadow-xl" style="width: 40rem">
<button @click="closeModal" class="absolute top-0 right-0 m-2 text-white"> <button @click="closeModal" class="absolute top-0 right-0 m-2 primary">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
> >
<button <button
@click="acceptChallenge(challenge)" @click="acceptChallenge(challenge)"
class="text-white font-bold py-1 px-4 mt-[-14px] sm:mt-0" class="font-bold py-1 px-4 mt-[-14px] sm:mt-0 primary"
> >
Godta Godta
</button> </button>
...@@ -80,6 +80,7 @@ ...@@ -80,6 +80,7 @@
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref } from 'vue'
import authInterceptor from '@/services/authInterceptor' import authInterceptor from '@/services/authInterceptor'
import type { AxiosResponse } from 'axios' import type { AxiosResponse } from 'axios'
import { useChallengeStore } from '@/stores/challengeStore'
interface Challenge { interface Challenge {
title: string title: string
...@@ -94,6 +95,8 @@ interface Challenge { ...@@ -94,6 +95,8 @@ interface Challenge {
const showModal = ref(true) const showModal = ref(true)
const generatedChallenges = reactive<Challenge[]>([]) const generatedChallenges = reactive<Challenge[]>([])
const emit = defineEmits(['update-challenges'])
const challengeStore = useChallengeStore()
async function fetchGeneratedChallenges() { async function fetchGeneratedChallenges() {
try { try {
...@@ -122,7 +125,7 @@ onMounted(() => { ...@@ -122,7 +125,7 @@ onMounted(() => {
localStorage.setItem('lastModalShow', Date.now().toString()) localStorage.setItem('lastModalShow', Date.now().toString())
}) })
function acceptChallenge(challenge: Challenge) { async function acceptChallenge(challenge: Challenge) {
if (!challenge) { if (!challenge) {
console.error('No challenge data provided to acceptChallenge function.') console.error('No challenge data provided to acceptChallenge function.')
return return
...@@ -136,7 +139,7 @@ function acceptChallenge(challenge: Challenge) { ...@@ -136,7 +139,7 @@ function acceptChallenge(challenge: Challenge) {
due: challenge.dueFull, due: challenge.dueFull,
type: challenge.type type: challenge.type
} }
authInterceptor await authInterceptor
.post('/challenges', postData) .post('/challenges', postData)
.then((response: AxiosResponse) => { .then((response: AxiosResponse) => {
challenge.isAccepted = true challenge.isAccepted = true
...@@ -144,6 +147,9 @@ function acceptChallenge(challenge: Challenge) { ...@@ -144,6 +147,9 @@ function acceptChallenge(challenge: Challenge) {
.catch((error) => { .catch((error) => {
console.error('Failed to save challenge:', error) console.error('Failed to save challenge:', error)
}) })
await challengeStore.getUserChallenges()
const challenges = challengeStore.challenges
emit('update-challenges', challenges)
} }
const closeModal = () => { const closeModal = () => {
......
<template> <template>
<div class="fixed bottom-5 right-5 hover:cursor-pointer z-50" @click="isModalOpen = true"> <div class="fixed bottom-10 right-10 hover:cursor-pointer z-50" @click="isModalOpen = true">
<img <img
alt="Hjelp" alt="Hjelp"
class="h-10 transition-transform duration-300 ease-in-out hover:scale-110" class="h-12 transition-transform duration-300 ease-in-out hover:scale-110"
src="@/assets/hjelp.png" src="@/assets/hjelp.png"
/> />
</div> </div>
<ModalComponent v-if="isModalOpen" @close="isModalOpen = false"> <ModalComponent v-if="isModalOpen" @close="isModalOpen = false">
<InteractiveSpare <InteractiveSpare
:speech="speech" :speech="speech"
:png-size="15" :png-size="12"
direction="right" direction="right"
@emit:close="isModalOpen = false" @emit:close="isModalOpen = false"
/> />
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
v-if="index % 6 === modValue" v-if="index % 6 === modValue"
:src="url" :src="url"
alt="could not load" alt="could not load"
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" 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 rounded-lg border-stale-400 shadow-md shadow-gray-500"
/> />
</div> </div>
</template> </template>
......
...@@ -22,13 +22,9 @@ ...@@ -22,13 +22,9 @@
</div> </div>
<button @click="cycleArray('next')"></button> <button @click="cycleArray('next')"></button>
</div> </div>
<div class="flex flex-row items-center gap-4 mx-auto"> <div class="flex flex-row items-center justify-center mx-auto">
<button @click="saveAvatar" class="primary save-button basis-1/2">Lagre</button> <button @click="saveAvatar" class="primary save-button">Lagre</button>
<button @click="openFileExplorer" class="primary basis-1/2">
Upload New Avatar
</button>
</div> </div>
<input type="file" ref="fileInput" @change="handleFileUpload" hidden />
</div> </div>
</div> </div>
</template> </template>
...@@ -77,8 +73,7 @@ const openModal = () => { ...@@ -77,8 +73,7 @@ const openModal = () => {
const urlProfilePicture = userStore.profilePicture const urlProfilePicture = userStore.profilePicture
// Check if a profile picture URL exists and append it to the avatars list // Check if a profile picture URL exists and append it to the avatars list
const img = localStorage.getItem('profilePicture') as string const img = localStorage.getItem('profilePicture') as string
console.log(state.avatars)
console.log(img)
if (state.avatars.includes(state.selectedPublicImg) || state.avatars.includes(img)) { if (state.avatars.includes(state.selectedPublicImg) || state.avatars.includes(img)) {
// Remove the public asset from the list if it's already selected // Remove the public asset from the list if it's already selected
state.avatars = state.avatars.filter((avatar) => avatar !== state.selectedPublicImg) state.avatars = state.avatars.filter((avatar) => avatar !== state.selectedPublicImg)
...@@ -113,23 +108,11 @@ const cycleArray = (direction: string) => { ...@@ -113,23 +108,11 @@ const cycleArray = (direction: string) => {
} }
} }
const handleFileUpload = async (event: any) => {
const input = event.target
if (input.files && input.files[0]) {
const file = input.files[0]
// Clear any existing temporary blob URLs
state.avatars = state.avatars.filter((avatar) => !avatar.startsWith('blob:'))
state.newFile = file // Save the new file object for later upload
state.avatars.push(URL.createObjectURL(file)) // Add the blob URL for preview
state.currentAvatarIndex = state.avatars.length - 1 // Set this new avatar as current
}
}
const saveAvatar = async () => { const saveAvatar = async () => {
if (state.newFile && currentAvatar.value.startsWith('blob:')) { if (currentAvatar.value.startsWith('blob:')) {
// If there's a new file selected, upload it // If there's a new file selected, upload it
const formData = new FormData() const formData = new FormData()
formData.append('file', state.newFile) formData.append('file', currentAvatar.value)
await userStore.uploadProfilePicture(formData) await userStore.uploadProfilePicture(formData)
} else if (currentAvatar.value.startsWith('/')) { } else if (currentAvatar.value.startsWith('/')) {
// If it's a public asset, fetch it as a blob and upload // If it's a public asset, fetch it as a blob and upload
......
<template> <template>
<nav class="flex justify-between items-center min-h-32 text-xl w-full px-3 my-0"> <nav class="flex justify-between items-center min-h-32 text-xl w-full px-3 my-0">
<div class="order-first basis-1/5"> <div class="order-first md:basis-1/5 basis-2/5">
<router-link to="/hjem" @click="hamburgerOpen = false"> <router-link to="/hjem" @click="hamburgerOpen = false">
<img <img
alt="logo" alt="logo"
...@@ -18,22 +18,28 @@ ...@@ -18,22 +18,28 @@
<router-link active-class="border-b-2" to="/profil">🤭Profil</router-link> <router-link active-class="border-b-2" to="/profil">🤭Profil</router-link>
</div> </div>
<div v-if="!isHamburger" class="flex-row flex gap-2 justify-end w-auto h-14 basis-1/5"> <div
v-if="!isHamburger"
class="flex-row flex gap-2 justify-end w-auto h-14 basis-1/5 px-10"
>
<ButtonDisplayStreak /> <ButtonDisplayStreak />
<button <button
class="primary basis-1/2 bg-[#95e35d] logout focus:ring focus:ring-black-300 text-nowrap" class="primary basis-1/3 h-10 mt-1 bg-[#95e35d] logout focus:ring focus:ring-black-300 text-nowrap"
@click="openModal" @click="openModal"
> >
Logg ut Logg ut
</button> </button>
</div> </div>
<div class="flex flex-row gap-2"> <div v-if="isHamburger" class="flex flex-row gap-2 md:basis-2/5 justify-end">
<ButtonDisplayStreak v-if="isHamburger" /> <ButtonDisplayStreak />
<button class="primary logout" v-if="isHamburger" @click="toggleMenu"></button> <button class="primary logout" @click="toggleMenu"></button>
</div> </div>
</nav> </nav>
<div v-if="hamburgerOpen" class="flex flex-col bg-white border border-slate-300 z-50"> <div
v-if="hamburgerOpen"
class="flex flex-col absolute w-full bg-white border border-slate-300 z-50"
>
<router-link to="/hjem" @click="hamburgerOpen = false">🏠Hjem</router-link> <router-link to="/hjem" @click="hamburgerOpen = false">🏠Hjem</router-link>
<router-link to="/sparemaal" @click="hamburgerOpen = false">🎯Sparemål</router-link> <router-link to="/sparemaal" @click="hamburgerOpen = false">🎯Sparemål</router-link>
<router-link to="/spareutfordringer" @click="hamburgerOpen = false" <router-link to="/spareutfordringer" @click="hamburgerOpen = false"
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
</div> </div>
<button <button
v-if="!allChallengesCompleted()" 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" class="h-auto w-auto absolute flex text-center self-end mr-10 md:mr-20 text-wrap border-2 border-gray-200 rounded-xl 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 hover:scale-105"
@click="scrollToFirstUncompleted" @click="scrollToFirstUncompleted"
v-show="!isAtFirstUncompleted" v-show="!isAtFirstUncompleted"
> >
...@@ -22,11 +22,11 @@ ...@@ -22,11 +22,11 @@
<div <div
v-if="challengesLocal" v-if="challengesLocal"
ref="containerRef" 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" 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-transparent rounded-lg bg-white shadow-md shadow-slate-400"
style="background-image: url('src/assets/backgroundSavingsPath.png')" style="background-image: url('src/assets/bakgrunn.png')"
> >
<div> <div>
<img src="@/assets/start.png" alt="Spare" class="md:w-1/6 md:h-auto h-20" /> <img src="@/assets/start-sign.png" alt="Spare" class="md:w-1/6 md:h-auto h-20" />
</div> </div>
<div <div
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
'justify-center mx-auto md:justify-between': index % 2 === 1, 'justify-center mx-auto md:justify-between': index % 2 === 1,
'justify-center md:justify-between mx-auto': index % 2 === 0 'justify-center md:justify-between mx-auto': index % 2 === 0
}" }"
class="flex flex-row w-full md:w-4/5 justify-start gap-4 md:gap-8" class="flex flex-row w-full md:w-4/5 justify-start gap-4 md:gap-8 h-auto"
> >
<div class="flex"> <div class="flex">
<img-gif-template <img-gif-template
...@@ -97,46 +97,61 @@ ...@@ -97,46 +97,61 @@
v-if="index === challengesLocal.length - 1 && index % 2 === 0" v-if="index === challengesLocal.length - 1 && index % 2 === 0"
class="flex flex-row mt-2" class="flex flex-row mt-2"
> >
<button class="text-2xl ml-48" @click="addSpareUtfordring">+</button> <button class="text-2xl ml-48 mr-2 primary" @click="addSpareUtfordring">
+
</button>
<p class="">Legg til <br />Spareutfordring</p> <p class="">Legg til <br />Spareutfordring</p>
</div> </div>
<div <div
v-else-if="index === challengesLocal.length - 1 && index % 2 !== 0" v-else-if="index === challengesLocal.length - 1 && index % 2 !== 0"
class="mr-20 flex flex-row" class="mr-20 flex flex-row"
> >
<button class="text-2xl ml-10 rounded-full" @click="addSpareUtfordring"> <button class="text-2xl ml-10 rounded-full primary" @click="addSpareUtfordring">
+ +
</button> </button>
<p class="">Legg til <br />Spareutfordring</p> <p class="pl-2">Legg til <br />Spareutfordring</p>
</div> </div>
<!-- Finish line --> <!-- Finish line -->
</div> </div>
<img <img
src="@/assets/finishLine.png" src="@/assets/finishline2.png"
class="w-full max-h-auto mx-auto mt-4" class="w-full max-h-auto mx-auto mt-4"
alt="Finish Line" alt="Finish Line"
/> />
</div> </div>
<!-- Goal --> <!-- Goal -->
<div v-if="goalLocal" class="flex flex-row justify-around m-t-2 pt-6 w-full mx-auto"> <div
v-if="goalLocal"
class="flex flex-row md:justify-between justify-around m-t-2 pt-6 w-[80%] mx-auto"
>
<div class="grid grid-rows-2 grid-flow-col gap 4"> <div class="grid grid-rows-2 grid-flow-col gap 4">
<div class="row-span-3 cursor-pointer" @click="editGoal(goalLocal)"> <p
class="md:mr-20 md:text-xl mt-4 font-bold text-sm md:text-nowrap h-auto w-32 mr-0"
>
Ditt neste sparemål🤩:
</p>
<div
class="row-span-3 cursor-pointer md:ml-10 text-center"
@click="editGoal(goalLocal)"
>
<img <img
:src="goalImageUrl" :src="goalImageUrl"
class="w-12 h-12 mx-auto rounded-sm" class="w-12 h-12 mx-auto rounded-sm"
:alt="goalLocal.title" :alt="goalLocal.title"
/> />
<div class="text-lg font-bold" data-cy="goal-title">{{ goalLocal.title }}</div> <div class="md:text-lg text-xs font-bold" data-cy="goal-title">
{{ goalLocal.title }}
</div>
</div> </div>
</div> </div>
<div class="flex flex-col items-end"> <div class="flex flex-col items-end gap-2">
<div @click="goToEditGoal" class="cursor-pointer"> <button class="primary secondary md:text-lg text-xs" @click="goToEditGoal">
<h3 class="text-blue-500 text-base">Endre mål</h3> Endre mål
</div> </button>
<div <div
:key="componentKey" :key="componentKey"
ref="targetRef" ref="targetRef"
class="bg-yellow-400 px-4 py-1 rounded-full text-black font-bold" class="bg-yellow-300 px-4 py-1 rounded-2xl text-black font-bold md:text-lg text-xs text-nowrap"
> >
{{ goalLocal.saved }}kr / {{ goalLocal.target }}kr {{ goalLocal.saved }}kr / {{ goalLocal.target }}kr
</div> </div>
...@@ -182,6 +197,8 @@ import authInterceptor from '@/services/authInterceptor' ...@@ -182,6 +197,8 @@ import authInterceptor from '@/services/authInterceptor'
const router = useRouter() const router = useRouter()
const goalStore = useGoalStore() const goalStore = useGoalStore()
const emit = defineEmits(['complete-challenge'])
interface Props { interface Props {
challenges: Challenge[] challenges: Challenge[]
goal: Goal | null | undefined goal: Goal | null | undefined
...@@ -262,6 +279,7 @@ const handleChallengeUpdate = (updatedChallenge: Challenge) => { ...@@ -262,6 +279,7 @@ const handleChallengeUpdate = (updatedChallenge: Challenge) => {
) { ) {
animateChallenge(updatedChallenge) animateChallenge(updatedChallenge)
saveAnimatedStateChallenge(updatedChallenge) saveAnimatedStateChallenge(updatedChallenge)
emit('complete-challenge')
} }
if (goalLocal) { if (goalLocal) {
...@@ -626,9 +644,9 @@ const sortChallenges = () => { ...@@ -626,9 +644,9 @@ const sortChallenges = () => {
if (challengesLocal.value) { if (challengesLocal.value) {
challengesLocal.value.sort((a, b) => { challengesLocal.value.sort((a, b) => {
// First, sort by completion status: non-completed (less than 100) before completed (100) // First, sort by completion status: non-completed (less than 100) before completed (100)
if (a.completion !== 100 && b.completion === 100) { if (a.completedOn === null && b.completedOn !== null) {
return 1 // 'a' is not completed and 'b' is completed, 'a' should come first return 1 // 'a' is not completed and 'b' is completed, 'a' should come first
} else if (a.completion === 100 && b.completion !== 100) { } else if (a.completion !== null && b.completion === null) {
return -1 // 'a' is completed and 'b' is not, 'b' should come first return -1 // 'a' is completed and 'b' is not, 'b' should come first
} else { } else {
// Explicitly convert dates to numbers for subtraction // Explicitly convert dates to numbers for subtraction
......
...@@ -8,8 +8,18 @@ ...@@ -8,8 +8,18 @@
'flex-row-reverse': imageDirection === 'left' 'flex-row-reverse': imageDirection === 'left'
}" }"
> >
<a @click="isModalOpen = true" class="hover:bg-transparent z-20"> <a
@click="isModalOpen = true"
class="hover:bg-transparent hover:p-0 hover:scale-105 z-20"
>
<img <img
v-if="profilePicture && isMounted"
alt="Spare"
class="md:h-5/6 md:w-5/6 w-2/3 h-2/3 cursor-pointer ml-14 md:ml-10"
:src="profilePicture"
/>
<img
v-else
alt="Spare" alt="Spare"
class="md:h-5/6 md:w-5/6 w-2/3 h-2/3 cursor-pointer ml-14 md:ml-10" class="md:h-5/6 md:w-5/6 w-2/3 h-2/3 cursor-pointer ml-14 md:ml-10"
src="@/assets/spare.png" src="@/assets/spare.png"
...@@ -41,10 +51,14 @@ ...@@ -41,10 +51,14 @@
<script setup lang="ts"> <script setup lang="ts">
import InteractiveSpare from '@/components/InteractiveSpare.vue' import InteractiveSpare from '@/components/InteractiveSpare.vue'
import { defineProps, ref, watchEffect } from 'vue' import { defineProps, onMounted, ref, watchEffect } from 'vue'
import ModalComponent from '@/components/ModalComponent.vue' import ModalComponent from '@/components/ModalComponent.vue'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const isModalOpen = ref(false) const isModalOpen = ref(false)
const profilePicture = ref<string>()
const isMounted = ref(false)
const props = defineProps({ const props = defineProps({
speech: Array<string>, speech: Array<string>,
...@@ -60,4 +74,10 @@ const props = defineProps({ ...@@ -60,4 +74,10 @@ const props = defineProps({
watchEffect(() => { watchEffect(() => {
isModalOpen.value = props.show isModalOpen.value = props.show
}) })
onMounted(async () => {
await userStore.getProfilePicture()
profilePicture.value = userStore.profilePicture
isMounted.value = true
})
</script> </script>
import { describe, expect, it, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import ButtonComponent from '@/components/ButtonDisplayStreak.vue'
describe('ButtonComponent', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('renders correctly', () => {
const wrapper = mount(ButtonComponent, {
props: {
buttonText: 'Click me',
type: 'goal'
}
})
expect(wrapper.exists()).toBe(true)
})
})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment