Skip to content
Snippets Groups Projects
Commit c3309340 authored by Ina Martini's avatar Ina Martini
Browse files

Merge branch 'refactor/14/refactor-interactviespare-to-modal' into 'dev'

Refactor interactiveSpare to modal

See merge request !42
parents a189a332 37ef87c0
No related branches found
No related tags found
3 merge requests!66Final merge,!42Refactor interactiveSpare to modal,!4Pipeline fix
Pipeline #281629 passed with warnings
......@@ -111,7 +111,7 @@ describe('Goals and Challenges Page Load', () => {
cy.get('[data-cy=challenge-icon-1]').click();
// Assert that navigation has occurred
cy.url().should('include', '/spareutfordringer/1');
cy.url().should('include', '/spareutfordringer/rediger/1');
});
});
src/assets/hjelp.png

32.9 KiB

src/assets/varsel.png

25.5 KiB

<template>
<div
class="flex items-center mr-10 max-w-[60vh]"
:class="{ 'flex-row': direction === 'right', 'flex-row-reverse': direction === 'left' }"
>
<!-- Image -->
<img
:src="spareImageSrc"
:style="{ width: pngSize + 'rem', height: pngSize + 'rem' }"
:class="['object-contain', ...imageClass]"
alt="Sparemannen"
class="w-dynamic h-dynamic object-contain"
@click="nextSpeech"
/>
<!-- Speech Bubble -->
<div
v-if="currentSpeech"
:class="`mb-40 inline-block relative w-64 bg-white p-4 rounded-3xl border border-gray-600 tri-right round ${bubbleDirection}`"
>
<div class="text-left leading-6">
<p class="m-0">{{ currentSpeech }}</p>
<ModalComponent :is-modal-open="isModalOpen" @close="isModalOpen = false">
<template v-slot:input>
<div
class="spareDiv flex items-center mr-10 max-w-[60vh] cursor-pointer"
:class="{
'flex-row': direction === 'right',
'flex-row-reverse': direction === 'left'
}"
@click="nextSpeech"
>
<!-- Image -->
<img
:src="spareImageSrc"
:style="{ width: pngSize + 'rem', height: pngSize + 'rem' }"
:class="['object-contain', ...imageClass]"
alt="Spare"
class="w-dynamic h-dynamic object-contain"
/>
<!-- Speech Bubble -->
<div
v-if="currentSpeech"
:class="`mb-40 inline-block relative w-64 bg-white p-4 rounded-3xl border border-gray-600 tri-right round ${bubbleDirection}`"
>
<div class="text-left leading-6">
<p class="speech m-0">{{ currentSpeech }}</p>
</div>
</div>
</div>
<div class="-mb-5 mt-8 text-xs text-gray-500">
<p class="justify-center items-center">Trykk for å se hva Spare har å si!</p>
<a
@click="clearSpeeches"
class="underline hover:bg-transparent font-normal text-gray-500 cursor-pointer transition-none hover:transition-none hover:p-0"
>
Skip
</a>
</div>
</div>
</div>
</template>
</ModalComponent>
</template>
<script setup lang="ts">
import { computed, defineProps, ref } from 'vue'
import { computed, defineProps, ref, watch } from 'vue'
import spareImageSrc from '@/assets/spare.png'
import ModalComponent from '@/components/ModalComponent.vue'
interface Props {
speech?: string[] // Using TypeScript's type for speech as an array of strings
direction: 'left' | 'right' // This restricts direction to either 'left' or 'right'
pngSize: number // Just declaring the type directly since it's simple
speech: string[] | null
direction: 'left' | 'right'
pngSize: number
isModalOpen: boolean
}
const props = defineProps<Props>()
const speech = ref<String[]>(props.speech || [])
const speech = ref<string[]>(props.speech || [])
const isModalOpen = ref(props.isModalOpen)
// Watch the speech prop for changes
watch(
() => props.speech,
(newVal) => {
if (newVal) {
// Check if the new value is not null
speech.value = newVal // Update the reactive speech array
currentSpeechIndex.value = 0 // Reset the speech index
isModalOpen.value = true // Open the modal if new speech is available
} else {
speech.value = [] // Clear the speech array if null is received
isModalOpen.value = false // Close the modal if there's no speech
}
}
)
const currentSpeechIndex = ref(0)
const currentSpeech = computed(() => speech.value[currentSpeechIndex.value])
......@@ -54,6 +89,8 @@ const nextSpeech = () => {
} else {
// If no speeches left, reset index to indicate no available speech
currentSpeechIndex.value = -1
// Close the modal if there are no speeches left
modalClosed()
}
}
}
......@@ -68,6 +105,16 @@ const imageClass = computed(() => {
const bubbleDirection = computed(() => {
return props.direction === 'right' ? 'btm-left-in' : 'btm-right-in'
})
const clearSpeeches = () => {
currentSpeechIndex.value = -1
modalClosed()
}
const modalClosed = () => {
isModalOpen.value = false
currentSpeechIndex.value = -1
}
</script>
<style scoped>
/* CSS talk bubble */
......
<template>
<div
v-if="isModalOpen"
class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
class="fixed inset-0 bg-black bg-opacity-30 flex justify-center items-center z-50"
>
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full text-center">
<div class="bg-white p-6 rounded-lg shadow-lg max-w-lg w-full text-center">
<h2 class="title font-bold mb-4">{{ title }}</h2>
<p class="message mb-4">{{ message }}</p>
......
......@@ -361,7 +361,6 @@ watch(
(newGoal, oldGoal) => {
if (newGoal !== oldGoal) {
goal.value = newGoal
console.log('Updated goal:', goal.value)
}
},
{ immediate: true }
......@@ -373,7 +372,6 @@ watch(
if (newChallenges !== oldChallenges) {
challenges.value = newChallenges
sortChallenges()
console.log('Updated challenges:', challenges.value)
}
},
{ immediate: true }
......@@ -404,8 +402,6 @@ const incrementSaved = async (challenge: Challenge) => {
await challengeStore.completeUserChallenge(challenge)
}
console.log('Incrementing saved amount for:', 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
......@@ -432,11 +428,11 @@ const recalculateAndAnimate = () => {
}
const editChallenge = (challenge: Challenge) => {
router.push(`/spareutfordringer/${challenge.id}`)
router.push(`/spareutfordringer/rediger/${challenge.id}`)
}
const editGoal = (goal: Goal) => {
router.push(`/sparemaal/${goal.id}`)
router.push(`/sparemaal/rediger/${goal.id}`)
}
// Declare the ref with a type annotation for an array of strings
......@@ -589,9 +585,8 @@ const getPigStepsIcon = () => {
return 'src/assets/pigSteps.png'
}
// TODO - Change when EditGoal view is created
const goToEditGoal = () => {
router.push({ name: 'EditGoal' })
router.push({ name: 'edit-goal', params: { id: goal.value?.id } })
}
</script>
......
......@@ -8,7 +8,8 @@ describe('SpeechBubbleComponent', () => {
props: {
direction: 'left',
speech: ['Hello', 'World'],
pngSize: 100
pngSize: 100,
isModalOpen: true
}
})
expect(wrapper.exists()).toBeTruthy()
......@@ -19,18 +20,20 @@ describe('SpeechBubbleComponent', () => {
props: {
direction: 'right',
speech: ['Hello', 'World'],
pngSize: 100
pngSize: 100,
isModalOpen: true
}
})
expect(wrapper.find('div').classes()).toContain('flex-row')
expect(wrapper.find('.spareDiv').classes()).toContain('flex-row')
const wrapperReverse = mount(SpeechBubbleComponent, {
props: {
direction: 'left',
speech: ['Hello', 'World'],
pngSize: 100
pngSize: 100,
isModalOpen: true
}
})
expect(wrapperReverse.find('div').classes()).toContain('flex-row-reverse')
expect(wrapperReverse.find('.spareDiv').classes()).toContain('flex-row-reverse')
})
it('image class is computed based on direction', () => {
......@@ -38,22 +41,24 @@ describe('SpeechBubbleComponent', () => {
props: {
direction: 'right',
speech: ['Hello', 'World'],
pngSize: 100
pngSize: 100,
isModalOpen: true
}
})
expect(wrapper.find('img').classes()).toContain('scale-x-[-1]')
})
it('updates speech text on image click', async () => {
it('updates speech text on div click', async () => {
const wrapper = mount(SpeechBubbleComponent, {
props: {
direction: 'left',
speech: ['First speech', 'Second speech'],
pngSize: 100
pngSize: 100,
isModalOpen: true
}
})
expect(wrapper.find('p').text()).toBe('First speech')
await wrapper.find('img').trigger('click')
expect(wrapper.find('p').text()).toBe('Second speech')
expect(wrapper.find('.speech').text()).toBe('First speech')
await wrapper.find('.spareDiv').trigger('click')
expect(wrapper.find('.speech').text()).toBe('Second speech')
})
})
......@@ -72,7 +72,7 @@ async function onButtonClick() {
await accountStore.postAccount('SPENDING', spendingAccountNumber, 0)
await router.push({ name: 'home' })
await router.push({ name: 'home', query: { firstLogin: 'true' } })
}
function restrictToNumbers(event: InputEvent, type: string) {
......
......@@ -27,6 +27,7 @@ const profile = ref<Profile>({
const updatePassword = ref<boolean>(false)
const confirmPassword = ref<string>('')
const errorMessage = ref<string>('')
const isModalOpen = ref(false)
const nameRegex = /^[æÆøØåÅa-zA-Z,.'-][æÆøØåÅa-zA-Z ,.'-]{1,29}$/
const emailRegex =
......@@ -210,6 +211,7 @@ const saveChanges = async () => {
:png-size="10"
:speech="['Her kan du endre på profilen din!']"
direction="left"
:isModalOpen="isModalOpen"
/>
<CardTemplate>
......
<template>
<div class="flex flex-col items-center max-h-[60vh] md:flex-row md:max-h-[80vh] mx-auto">
<div class="flex flex-col basis-1/3 order-last md:order-first md:basis-1/4 md:pl-10">
<InteractiveSpare
:speech="speech"
:direction="'right'"
:pngSize="15"
class="opacity-0 h-0 w-0 md:opacity-100 md:h-auto md:w-auto md:mx-auto md:my-20"
></InteractiveSpare>
<div class="flex flex-col basis-1/3 order-last md:order-first md:basis-1/4 md:pl-1 mt-10">
<img
v-if="newSpeechAvailable"
alt="Varsel"
class="jump scale-x-[-1] w-1/12 h-1/12 ml-52 cursor-pointer z-10"
src="@/assets/varsel.png"
/>
<div class="flex items-center">
<a @click="openInteractiveSpare" class="hover:bg-transparent z-20">
<img
alt="Spare"
class="scale-x-[-1] 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"
/>
</a>
</div>
<div class="flex flex-row gap-2 items-center mx-auto my-4 md:flex-col md:gap-4 md:m-8">
<ButtonAddGoalOrChallenge :buttonText="'Legg til sparemål'" :type="'goal'" />
<ButtonAddGoalOrChallenge
......@@ -17,6 +26,18 @@
</div>
<savings-path :challenges="challenges" :goal="goal"></savings-path>
</div>
<InteractiveSpare
:speech="speech"
:direction="'right'"
:pngSize="15"
:isModalOpen="isModalOpen"
class="opacity-0 h-0 w-0 md:opacity-100 md:h-auto md:w-auto"
></InteractiveSpare>
<div class="fixed bottom-5 left-5">
<div @click="openHelp" class="hover:cursor-pointer">
<img alt="Hjelp" class="w-1/12" src="@/assets/hjelp.png" />
</div>
</div>
</template>
<script setup lang="ts">
......@@ -28,9 +49,13 @@ import type { Goal } from '@/types/goal'
import { useGoalStore } from '@/stores/goalStore'
import { useChallengeStore } from '@/stores/challengeStore'
import SavingsPath from '@/components/SavingsPath.vue'
import router from '@/router'
const goalStore = useGoalStore()
const challengeStore = useChallengeStore()
const isModalOpen = ref(false)
const speech = ref<string[]>([])
const newSpeechAvailable = ref(false)
const challenges = ref<Challenge[]>([])
const goals = ref<Goal[]>([])
......@@ -43,15 +68,53 @@ onMounted(async () => {
challenges.value = challengeStore.challenges
goals.value = goalStore.goals
goal.value = goals.value[0]
console.log('Goals:', goals.value)
firstLoggedInSpeech()
})
// Define your speech array
const speechArray = [
'Hei! Jeg er Sparemannen.',
'Jeg hjelper deg med å spare penger.',
'Klikk på meg for å høre mer.'
]
// Check if the user is logging in for the first time, and display the first login speech
const firstLoggedInSpeech = () => {
const isFirstLogin = router.currentRoute.value.query.firstLogin === 'true'
if (isFirstLogin) {
speech.value = [
'Hei, jeg er Spare!',
'Jeg skal hjelpe deg med å spare penger.',
'Du får varsel når jeg har noe å si!'
]
isModalOpen.value = true
router.replace({ name: 'home', query: { firstLogin: 'false' } })
}
}
const speech = ref(speechArray)
const openInteractiveSpare = () => {
// Check if there's new speech available before opening the modal.
if (newSpeechAvailable.value) {
isModalOpen.value = true // Open the modal
newSpeechAvailable.value = false // Reset the flag since the speech will now be displayed
}
}
const openHelp = () => {
speech.value = [
'Heisann, jeg er Spare!',
'Jeg skal hjelpe deg med å spare penger.',
'Du kan legge til sparemål og spareutfordringer!',
'Sammen kan vi spare penger og nå dine mål!'
]
isModalOpen.value = true
}
</script>
<style>
@keyframes jump {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.jump {
animation: jump 0.6s infinite ease-in-out;
}
</style>
......@@ -99,7 +99,7 @@ const updateGoal = () => {
authInterceptor
.put(`/goals/${goalInstance.value.id}`, goalInstance.value)
.then(() => {
router.push({ name: 'goals' })
router.back()
})
.catch((error) => {
console.error(error)
......
......@@ -12,6 +12,7 @@ import router from '@/router'
const profile = ref<Profile>()
const completedGoals = ref<Goal[]>([])
const completedChallenges = ref<Challenge[]>([])
const isModalOpen = ref(false)
onMounted(async () => {
await authInterceptor('/profile')
......@@ -43,6 +44,10 @@ onMounted(async () => {
const welcome = computed(() => {
return [`Velkommen, ${profile.value?.firstName} ${profile.value?.lastName} !`]
})
const openInteractiveSpare = () => {
isModalOpen.value = true
}
</script>
<template>
......@@ -87,7 +92,21 @@ const welcome = computed(() => {
</div>
<div class="flex flex-col">
<InteractiveSpare :png-size="10" :speech="welcome" direction="left" />
<InteractiveSpare
:png-size="10"
:speech="welcome"
direction="left"
:isModalOpen="isModalOpen"
/>
<div class="flex items-center">
<a @click="openInteractiveSpare" class="hover:bg-transparent z-20">
<img
alt="Spare"
class="scale-x-[-1] 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"
/>
</a>
</div>
<div class="flex flex-row justify-between mx-4">
<p class="font-bold">Fullførte sparemål</p>
<a class="hover:p-0 cursor-pointer" v-text="'Se alle'" />
......
......@@ -29,6 +29,8 @@ const isCompleted = computed(() => challengeInstance.value.completedOn != null)
const motivation = ref<string[]>([])
const isModalOpen = ref(false)
const calculateSpeech = () => {
if (completion.value === 0) {
return motivation.value.push(
......@@ -140,7 +142,12 @@ const completeChallenge = () => {
</button>
</div>
</div>
<InteractiveSpare :png-size="10" :speech="motivation" direction="left" />
<InteractiveSpare
:png-size="10"
:speech="motivation"
direction="left"
:isModalOpen="isModalOpen"
/>
</div>
</template>
......
......@@ -20,6 +20,11 @@ const completion = computed(() => (goalInstance.value.saved / goalInstance.value
const isCompleted = computed(() => goalInstance.value.completedOn != null)
const motivation = ref<string[]>([])
const isModalOpen = ref(false)
const openInteractiveSpare = () => {
isModalOpen.value = true
}
const calculateSpeech = () => {
if (completion.value === 0) {
......@@ -126,7 +131,21 @@ const completeGoal = () => {
</button>
</div>
</div>
<InteractiveSpare :png-size="10" :speech="motivation" direction="left" />
<div class="flex items-center">
<a @click="openInteractiveSpare" class="hover:bg-transparent z-20">
<img
alt="Spare"
class="scale-x-[-1] 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"
/>
</a>
</div>
<InteractiveSpare
:png-size="10"
:speech="motivation"
direction="left"
:isModalOpen="isModalOpen"
/>
</div>
</template>
......
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