From 24804b35dba40456770aca4cf3a9c76293660876 Mon Sep 17 00:00:00 2001
From: Eline Evje <elineev@stud.ntnu.no>
Date: Thu, 2 May 2024 17:37:33 +0200
Subject: [PATCH] feat: added functionality for adding image to goals and
 challenges

---
 src/views/ManageChallengeView.vue |  82 ++++++++++++-------
 src/views/ManageGoalView.vue      | 130 +++++++++++++++++++++---------
 src/views/ViewChallengeView.vue   |  47 ++++++++---
 src/views/ViewGoalView.vue        |  42 ++++++----
 4 files changed, 205 insertions(+), 96 deletions(-)

diff --git a/src/views/ManageChallengeView.vue b/src/views/ManageChallengeView.vue
index a600840..8cb79dc 100644
--- a/src/views/ManageChallengeView.vue
+++ b/src/views/ManageChallengeView.vue
@@ -7,6 +7,8 @@ import type { Challenge } from '@/types/challenge'
 import ModalComponent from '@/components/ModalComponent.vue'
 
 const router = useRouter()
+const uploadedFile = ref<File | null>(null);
+
 
 const modalTitle = ref('')
 const modalMessage = ref('')
@@ -65,6 +67,15 @@ function validateInputs() {
     return errors
 }
 
+const handleFileChange = (event: Event) => {
+    const target = event.target as HTMLInputElement;
+    if (target.files && target.files.length > 0) {
+        uploadedFile.value = target.files[0];
+    } else {
+        uploadedFile.value = null;
+    }
+};
+
 const submitAction = async () => {
     const errors = validateInputs()
     if (errors.length > 0) {
@@ -75,18 +86,33 @@ const submitAction = async () => {
         return
     }
     try {
+        let response;
         if (isEdit.value) {
-            updateChallenge()
+            response = await updateChallenge();
         } else {
-            createChallenge()
+            response = await createChallenge();
+        }
+
+        const challengeId = isEdit.value ? challengeInstance.value.id : response.id;
+
+        if (uploadedFile.value && challengeId) {
+            const formData = new FormData();
+            formData.append('file', uploadedFile.value);
+            formData.append('id', challengeId.toString());
+
+            await authInterceptor.post('/challenges/picture', formData, {
+                headers: { 'Content-Type': 'multipart/form-data' },
+            });
         }
+
+        await router.push({ name: 'challenges' });
     } catch (error) {
-        console.error(error)
-        modalTitle.value = 'Systemfeil'
-        modalMessage.value = 'En feil oppstod under lagring av utfordringen.'
-        errorModalOpen.value = true
+        console.error('Error during challenge submission:', error);
+        modalTitle.value = 'Systemfeil';
+        modalMessage.value = 'En feil oppstod under lagring av utfordringen.';
+        errorModalOpen.value = true;
     }
-}
+};
 
 onMounted(async () => {
     if (isEdit.value) {
@@ -109,26 +135,14 @@ onMounted(async () => {
     }
 })
 
-const createChallenge = () => {
-    authInterceptor
-        .post('/challenges', challengeInstance.value, {})
-        .then(() => {
-            return router.push({ name: 'challenges' })
-        })
-        .catch((error) => {
-            console.error(error)
-        })
+const createChallenge = async () => {
+    const response = await authInterceptor.post('/challenges', challengeInstance.value);
+    return response.data;
 }
 
-const updateChallenge = () => {
-    authInterceptor
-        .put(`/challenges/${challengeInstance.value.id}`, challengeInstance.value)
-        .then(() => {
-            router.push({ name: 'challenges' })
-        })
-        .catch((error) => {
-            console.error(error)
-        })
+const updateChallenge = async () => {
+    const response = await authInterceptor.put(`/challenges/${challengeInstance.value.id}`, challengeInstance.value);
+    return response.data;
 }
 
 const cancelCreation = () => {
@@ -151,6 +165,11 @@ const confirmCancel = () => {
     router.push({ name: 'challenges' })
     confirmModalOpen.value = false
 }
+
+const removeUploadedFile = () => {
+    uploadedFile.value = null;
+};
+
 </script>
 
 <template>
@@ -222,13 +241,16 @@ const confirmCancel = () => {
                     />
                 </div>
 
-                <div class="flex flex-col">
+                <div class="flex flex-col items-center">
                     <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"
-                    >
+                    <label for="fileUpload" class="bg-white text-black text-lg p-1 mt-2 rounded cursor-pointer leading-none">
                         💾
-                    </button>
+                    </label>
+                    <input id="fileUpload" type="file" accept=".jpg" hidden @change="handleFileChange" />
+                    <div v-if="uploadedFile" class="flex justify-center items-center mt-2">
+                        <p class="text-sm">{{ uploadedFile.name }}</p>
+                        <button @click="removeUploadedFile" class="ml-2 text-xs font-bold border-2 p-1 rounded text-red-500">Fjern fil</button>
+                    </div>
                 </div>
             </div>
             <div class="flex flex-row justify-between w-full">
diff --git a/src/views/ManageGoalView.vue b/src/views/ManageGoalView.vue
index d2d8e75..cc46ff4 100644
--- a/src/views/ManageGoalView.vue
+++ b/src/views/ManageGoalView.vue
@@ -1,12 +1,14 @@
 <script lang="ts" setup>
 import { useRouter } from 'vue-router'
-import { computed, onMounted, ref, watch } from 'vue'
+import { computed, onMounted, type Ref, ref, watch } from 'vue'
 import type { Goal } from '@/types/goal'
 import ProgressBar from '@/components/ProgressBar.vue'
 import authInterceptor from '@/services/authInterceptor'
 import ModalComponent from '@/components/ModalComponent.vue'
 
 const router = useRouter()
+const uploadedFile: Ref<File | null> = ref(null);
+
 
 const minDate = new Date(new Date().setDate(new Date().getDate() + 1)).toISOString().slice(0, 10)
 const selectedDate = ref<string>(minDate)
@@ -62,28 +64,47 @@ function validateInputs() {
 
     return errors
 }
+
 const submitAction = async () => {
-    const errors = validateInputs()
+    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 formatErrors = errors.join('<br>');
+        modalTitle.value = 'Oops! Noe er feil med det du har fylt ut🚨';
+        modalMessage.value = formatErrors;
+        errorModalOpen.value = true;
+        return;
     }
+
     try {
+        let response;
+
         if (isEdit.value) {
-            updateGoal()
+            response = await updateGoal();
         } else {
-            createGoal()
+            response = await createGoal();
+        }
+
+        const goalId = isEdit.value ? goalInstance.value.id : response.id; // Adjusted to handle the returned data
+
+        if (uploadedFile.value && goalId) {
+            const formData = new FormData();
+            formData.append('file', uploadedFile.value);
+            formData.append('id', goalId.toString());
+
+            await authInterceptor.post('/goals/picture', formData, {
+                headers: { 'Content-Type': 'multipart/form-data' },
+            });
         }
+
+        await router.push({ name: 'goals' });
     } catch (error) {
-        console.error(error)
-        modalTitle.value = 'Systemfeil'
-        modalMessage.value = 'En feil oppstod under lagring av utfordringen.'
-        errorModalOpen.value = true
+        console.error('Error during goal submission:', error);
+        modalTitle.value = 'Systemfeil';
+        modalMessage.value = 'En feil oppstod under lagring av utfordringen.';
+        errorModalOpen.value = true;
     }
-}
+};
+
 
 watch(selectedDate, (newDate) => {
     console.log(newDate)
@@ -108,27 +129,25 @@ onMounted(async () => {
     }
 })
 
-const createGoal = () => {
-    authInterceptor
-        .post('/goals', goalInstance.value, {})
-        .then(() => {
-            return router.push({ name: 'goals' })
-        })
-        .catch((error) => {
-            console.error(error)
-        })
-}
+const createGoal = async (): Promise<any> => {
+    try {
+        const response = await authInterceptor.post('/goals', goalInstance.value);
+        return response.data; // Ensure the response data is returned
+    } catch (error) {
+        console.error('Failed to create goal:', error);
+        throw error; // Rethrow the error to handle it in the submitAction method
+    }
+};
 
-const updateGoal = () => {
-    authInterceptor
-        .put(`/goals/${goalInstance.value.id}`, goalInstance.value)
-        .then(() => {
-            router.back()
-        })
-        .catch((error) => {
-            console.error(error)
-        })
-}
+const updateGoal = async (): Promise<any> => {
+    try {
+        const response = await authInterceptor.put(`/goals/${goalInstance.value.id}`, goalInstance.value);
+        return response.data; // Ensure the response data is returned
+    } catch (error) {
+        console.error('Failed to update goal:', error);
+        throw error; // Rethrow the error to handle it in the submitAction method
+    }
+};
 
 const deleteGoal = () => {
     authInterceptor
@@ -160,6 +179,36 @@ const confirmCancel = () => {
     router.push({ name: 'goals' })
     confirmModalOpen.value = false
 }
+
+const handleFileChange = (event: Event) => {
+    const target = event.target as HTMLInputElement;
+    if (target.files && target.files.length > 0) {
+        uploadedFile.value = target.files[0]; // Save the first selected file
+    } else {
+        uploadedFile.value = null;
+    }
+};
+
+const removeUploadedFile = () => {
+    uploadedFile.value = null;
+};
+
+onMounted(async () => {
+    if (isEdit.value) {
+        const goalId = router.currentRoute.value.params.id
+        if (!goalId) return router.push({ name: 'goals' })
+
+        await authInterceptor(`/goals/${goalId}`)
+            .then((response) => {
+                goalInstance.value = response.data
+                selectedDate.value = response.data.due.slice(0, 16)
+            })
+            .catch((error) => {
+                console.error(error)
+                router.push({ name: 'goals' })
+            })
+    }
+})
 </script>
 
 <template>
@@ -213,13 +262,16 @@ const confirmCancel = () => {
                         type="date"
                     />
                 </div>
-                <div class="flex flex-col">
+                <div class="flex flex-col items-center">
                     <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"
-                    >
+                    <label for="fileUpload" class="bg-white text-black text-lg p-1 mt-2 rounded cursor-pointer leading-none">
                         💾
-                    </button>
+                    </label>
+                    <input id="fileUpload" type="file" accept=".jpg" hidden @change="handleFileChange" />
+                    <div v-if="uploadedFile" class="flex justify-center items-center mt-2">
+                        <p class="text-sm">{{ uploadedFile.name }}</p>
+                        <button @click="removeUploadedFile" class="ml-2 text-xs font-bold border-2 p-1 rounded text-red-500">Fjern fil</button>
+                    </div>
                 </div>
             </div>
 
diff --git a/src/views/ViewChallengeView.vue b/src/views/ViewChallengeView.vue
index 924d5ae..9547641 100644
--- a/src/views/ViewChallengeView.vue
+++ b/src/views/ViewChallengeView.vue
@@ -5,8 +5,11 @@ import ProgressBar from '@/components/ProgressBar.vue'
 import authInterceptor from '@/services/authInterceptor'
 import type { Challenge } from '@/types/challenge'
 import SpareComponent from '@/components/SpareComponent.vue'
+import starImage from '@/assets/star.png';
 
 const router = useRouter()
+const challengeImageUrl = ref(starImage);
+const isImageLoaded = ref(false);
 
 const challengeInstance = ref<Challenge>({
     title: 'Tittel',
@@ -53,17 +56,27 @@ const calculateSpeech = () => {
     }
 }
 
-onMounted(() => {
-    const challengeId = router.currentRoute.value.params.id
-    if (!challengeId) return router.push({ name: 'challenges' })
+onMounted(async () => {
+    const challengeId = router.currentRoute.value.params.id;
+    if (!challengeId) return router.push({ name: 'challenges' });
 
-    authInterceptor(`/challenges/${challengeId}`)
-        .then((response) => {
-            challengeInstance.value = response.data
-            calculateSpeech()
-        })
-        .catch(() => router.push({ name: 'challenges' }))
-})
+    try {
+        const challengeResponse = await authInterceptor.get(`/challenges/${challengeId}`);
+        challengeInstance.value = challengeResponse.data;
+        calculateSpeech();
+
+        try {
+            const imageResponse = await authInterceptor.get(`/challenges/picture?id=${challengeId}`, { responseType: 'blob' });
+            challengeImageUrl.value = URL.createObjectURL(imageResponse.data);
+        } catch (imageError) {
+            console.error("Failed to load image:", imageError);
+        }
+        isImageLoaded.value = true;
+    } catch (error) {
+        console.error("Failed to load challenge details:", error);
+        await router.push({ name: 'challenges' });
+    }
+});
 
 const completeChallenge = () => {
     authInterceptor
@@ -80,6 +93,9 @@ const completeChallenge = () => {
 <template>
     <div class="flex flex-row flex-wrap items-center justify-center gap-10">
         <div class="flex flex-col gap-5 max-w-96">
+            <div class="flex flex-col items-center">
+            </div>
+
             <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"
                 @click="router.push({ name: 'challenges', params: { id: challengeInstance.id } })"
@@ -92,15 +108,22 @@ const completeChallenge = () => {
             >
                 <h2 class="my-0">Spareutfordring:</h2>
                 <h2 class="font-light">
-                    {{ challengeInstance.title + ' ' + challengeInstance.type }}
+                    {{ challengeInstance.title }}
                 </h2>
+                <div class="flex flex-row gap-4 justify-center">
+                    <p class="text-wrap break-words">{{ challengeInstance.description }}</p>
+                    <div>
+                        <img v-if="isImageLoaded" :src="challengeImageUrl || '@/assets/star.png'" alt="Goal Image" class="w-full h-40 object-cover rounded-lg">
+                    </div>
+                </div>
+                <br />
                 <p class="text-center">
                     Du har spart {{ timesSaved }} ganger som er {{ challengeInstance.saved }}kr av
                     {{ challengeInstance.target }}kr
                 </p>
                 <ProgressBar :completion="completion" />
                 <br />
-                <p class="text-wrap break-words">{{ challengeInstance.description }}</p>
+
                 <br />
                 <p>
                     Du sparer {{ challengeInstance.perPurchase }}kr hver gang du dropper å bruke
diff --git a/src/views/ViewGoalView.vue b/src/views/ViewGoalView.vue
index 04f4b42..523c843 100644
--- a/src/views/ViewGoalView.vue
+++ b/src/views/ViewGoalView.vue
@@ -5,8 +5,13 @@ import ProgressBar from '@/components/ProgressBar.vue'
 import authInterceptor from '@/services/authInterceptor'
 import type { Goal } from '@/types/goal'
 import SpareComponent from '@/components/SpareComponent.vue'
+import starImage from '@/assets/star.png';
+
 
 const router = useRouter()
+const goalImageUrl = ref(starImage);
+const isImageLoaded = ref(false);
+
 
 const goalInstance = ref<Goal>({
     title: 'Test tittel',
@@ -45,17 +50,28 @@ const calculateSpeech = () => {
     }
 }
 
-onMounted(() => {
-    const goalId = router.currentRoute.value.params.id
-    if (!goalId) return router.push({ name: 'goals' })
+onMounted(async () => {
+    const goalId = router.currentRoute.value.params.id;
+    if (!goalId) return router.push({ name: 'goals' });
+
+    try {
+        const goalResponse = await authInterceptor.get(`/goals/${goalId}`);
+        goalInstance.value = goalResponse.data;
+        calculateSpeech();
+
+        try {
+            const imageResponse = await authInterceptor.get(`/goals/picture?id=${goalId}`, { responseType: 'blob' });
+            goalImageUrl.value = URL.createObjectURL(imageResponse.data);
+        } catch (imageError) {
+            console.error("Failed to load image:", imageError);
+        }
+        isImageLoaded.value = true;
+    } catch (error) {
+        console.error("Failed to load goal details:", error);
+        await router.push({ name: 'goals' });
+    }
+});
 
-    authInterceptor(`/goals/${goalId}`)
-        .then((response) => {
-            goalInstance.value = response.data
-            calculateSpeech()
-        })
-        .catch(() => router.push({ name: 'goals' }))
-})
 
 const completeGoal = () => {
     authInterceptor
@@ -89,11 +105,7 @@ const completeGoal = () => {
                 <div class="flex flex-row gap-4 justify-center">
                     <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"
-                        />
+                        <img v-if="isImageLoaded" :src="goalImageUrl || '@/assets/star.png'" alt="Goal Image" class="w-full h-40 object-cover rounded-lg">
                     </div>
                 </div>
                 <br />
-- 
GitLab