diff --git a/src/assets/boatSpare.gif b/src/assets/boatSpare.gif new file mode 100644 index 0000000000000000000000000000000000000000..f0aaaa991168e07de3f56e5b129ae082094499f3 Binary files /dev/null and b/src/assets/boatSpare.gif differ diff --git a/src/assets/finishLine.png b/src/assets/finishLine.png index f30b5a30213062013638f6e275673d65d0640777..9394bd3c85fed058ab7862e207667d0c9df00ff7 100644 Binary files a/src/assets/finishLine.png and b/src/assets/finishLine.png differ diff --git a/src/assets/savingsPathBg.png b/src/assets/savingsPathBg.png new file mode 100644 index 0000000000000000000000000000000000000000..fb1994e981fffba4df9736cd50ccd71d00e70340 Binary files /dev/null and b/src/assets/savingsPathBg.png differ diff --git a/src/components/ImgGifTemplate.vue b/src/components/ImgGifTemplate.vue index 83144b10dfe1236438f8730e19a23cbbbd610c01..c05457926edbe9b8df4728ac4889ce2e700c3070 100644 --- a/src/components/ImgGifTemplate.vue +++ b/src/components/ImgGifTemplate.vue @@ -3,7 +3,7 @@ v-if="index % 6 === modValue" :src="url" alt="could not load" - class="h-32 w-32 border-2 rounded-lg border-stale-400" + class="h-32 w-32 border-2 rounded-lg border-stale-400 shadow-md shadow-black" /> </template> diff --git a/src/components/SavingsPath.vue b/src/components/SavingsPath.vue index 9bf0500ce61e30f6a9eef946f6bc92f62254b69a..f4dfc006b89b44b56b2762d1c15328edd56d9453 100644 --- a/src/components/SavingsPath.vue +++ b/src/components/SavingsPath.vue @@ -9,18 +9,25 @@ Din Sparesti </span> </div> + <button + 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">Ufullførte utfordringer<br>↓</button> <div class="h-1 w-4/6 mx-auto my-2 opacity-10"></div> <div ref="containerRef" - class="container relative pt-6 w-4/5 mx-auto md:w-4/5 no-scrollbar h-full max-h-[60vh] md:max-h-[60v] md:min-w-2/5 overflow-y-auto border-2 border-slate-300 rounded-lg bg-white shadow-lg" + class="container relative pt-6 w-4/5 mx-auto md:w-4/5 no-scrollbar h-full max-h-[60vh] md:max-h-[60v] md:min-w-2/5 overflow-y-auto border-2 border-slate-300 rounded-lg bg-white shadow-lg" > - <div> - <img src="@/assets/start.png" alt="Spare" class="md:w-1/6 md:h-auto h-20" /> - </div> - <div + <div> + <img src="@/assets/start.png" alt="Spare" class="md:w-1/6 md:h-auto h-20" /> + </div> + + + <div v-for="(challenge, index) in challenges" :key="challenge.id" class="flex flex-col items-center" + :ref="el => assignRef(el, challenge, index)" > <!-- Challenge Row --> <div @@ -48,7 +55,6 @@ ></img-gif-template> </div> <!-- Challenge Icon and Details --> - <card-template> <div class="flex"> <!-- Challenge Icon --> <div class="flex flex-col items-center gap-4"> @@ -129,7 +135,6 @@ <img src="@/assets/pending.png" alt="" />ï¸ </div> </div> - </card-template> <div class=""> <img-gif-template :index="index" @@ -215,7 +220,16 @@ </template> <script setup lang="ts"> -import { nextTick, onMounted, onUnmounted, type Ref, ref, watch } from 'vue' +import { + type ComponentPublicInstance, + nextTick, + onMounted, + onUnmounted, + reactive, + type Ref, + ref, + watch +} from 'vue' import anime from 'animejs' import type { Challenge } from '@/types/challenge' import type { Goal } from '@/types/goal' @@ -224,7 +238,6 @@ import { useRouter } from 'vue-router' import { useGoalStore } from '@/stores/goalStore' import { useChallengeStore } from '@/stores/challengeStore' import DisplayInfoForChallengeOrGoal from '@/components/DisplayInfoForChallengeOrGoal.vue' -import CardTemplate from '@/views/CardTemplate.vue' import ImgGifTemplate from '@/components/ImgGifTemplate.vue' const router = useRouter() @@ -272,6 +285,76 @@ const handleWindowSizeChange = () => { screenSize.value = window.innerWidth } +interface ElementRefs { + [key: string]: HTMLElement | undefined; +} + +const elementRefs = reactive<ElementRefs>({}); + + +const isAtFirstUncompleted = ref(false); // This state tracks visibility of the button +const firstUncompletedRef: Ref<HTMLElement | undefined> = ref(); + +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; + } +} + +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(); +}); + +onUnmounted(() => { + const container = containerRef.value; + if (container) { + container.removeEventListener('scroll', () => { + // Clean up the scroll listener + }); + } +}); + + + +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]; + } + } +}; + + + + // Utilizing watch to specifically monitor for changes in the props watch( () => props.goal,