From e6dc682215ff275574a3a7ceb8b1cac4bae6adc9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laczk=C3=B3=20Csongor=20Lor=C3=A1nd?=
 <laczko.csongor.lorand@hallgato.ppke.hu>
Date: Tue, 14 May 2024 23:42:58 +0200
Subject: [PATCH] feat: Add file upload to AddDog and make picture sending
 optional

- Implement file upload in AddDog
- Make picture sending optional in both frontend and backend
- Fix token sending issue in delete operation

style: Perform minor UI adjustments in AllDogs
---
 .../hu/pazmany/controller/Controller.java     | 25 +++++++--
 .../java/hu/pazmany/service/DogService.java   | 25 ++++++---
 frontend/src/components/dogs/AddDog.vue       | 56 ++++++++++++++++++-
 frontend/src/components/dogs/AllDogs.vue      |  6 +-
 frontend/src/components/dogs/EditDog.vue      | 15 ++---
 frontend/src/components/dogs/SingleDog.vue    | 19 +++++--
 frontend/src/index.css                        |  2 +-
 7 files changed, 116 insertions(+), 32 deletions(-)

diff --git a/backend/src/main/java/hu/pazmany/controller/Controller.java b/backend/src/main/java/hu/pazmany/controller/Controller.java
index 5c858b9..bf9396b 100644
--- a/backend/src/main/java/hu/pazmany/controller/Controller.java
+++ b/backend/src/main/java/hu/pazmany/controller/Controller.java
@@ -51,13 +51,24 @@ public class Controller {
                 .orElse(ResponseEntity.notFound().build());
     }
 
-    @PostMapping("/newdog")
-    public ResponseEntity<?> addNewDog(@RequestBody DetailedDogDTO dto, @RequestHeader("Authorization") String token) {
+    @PostMapping(value = "/newdog")
+    public ResponseEntity<?> addNewDog(@RequestHeader("Authorization") String token, @RequestParam("dog") String stringDogDTO, @RequestParam(value = "picture", required = false) MultipartFile mpf) {
         if (!isValidToken(token)) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
 
+        ObjectMapper objectMapper = new ObjectMapper();
+        DetailedDogDTO dogDTO;
+        try {
+            dogDTO = objectMapper.readValue(stringDogDTO, DetailedDogDTO.class);
+        } catch (IOException e) {
+            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Hibás JSON formátum");
+        }
         // Save the dog and picture
         try {
-            dogService.addNewDog(dto);
+            if (mpf != null && !mpf.isEmpty()) {
+                dogService.addNewDog(dogDTO, mpf);
+            } else {
+                dogService.addNewDog(dogDTO, null);
+            }
         } catch (IOException e) {
             return ResponseEntity.status(HttpStatus.NO_CONTENT).body("Hibás képformátum");
         }
@@ -65,7 +76,7 @@ public class Controller {
     }
 
     @PostMapping(value = "/dogs/{id}/edit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-    public ResponseEntity<?> editDog(@PathVariable Integer id, @RequestHeader("Authorization") String token, @RequestParam("dog") String stringDogDTO, @RequestParam("picture") MultipartFile mpf) {
+    public ResponseEntity<?> editDog(@PathVariable Integer id, @RequestHeader("Authorization") String token, @RequestParam("dog") String stringDogDTO, @RequestParam(value = "picture", required = false) MultipartFile mpf) {
         if (!isValidToken(token)) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
 
         // Retrieve the dog entity from the database
@@ -80,7 +91,11 @@ public class Controller {
             }
             // Save the updated dog entity
             try {
-                dogService.editDog(id, dogDTO, mpf);
+                if (mpf != null && !mpf.isEmpty()) {
+                    dogService.editDog(id, dogDTO, mpf);
+                } else {
+                    dogService.editDog(id, dogDTO, null);
+                }
             } catch (IOException e) {
                 return ResponseEntity.status(HttpStatus.NO_CONTENT).body("Hibás képformátum");
             }
diff --git a/backend/src/main/java/hu/pazmany/service/DogService.java b/backend/src/main/java/hu/pazmany/service/DogService.java
index 51be267..54224f8 100644
--- a/backend/src/main/java/hu/pazmany/service/DogService.java
+++ b/backend/src/main/java/hu/pazmany/service/DogService.java
@@ -32,14 +32,19 @@ public class DogService {
                 dogEntity.getPicture(), dogEntity.getAge(), dogEntity.getBreed()));
     }
 
-    public void addNewDog(DetailedDogDTO dto) throws IOException {
-        DogEntity newDog = new DogEntity();
+    public void addNewDog(DetailedDogDTO newDogRequest, MultipartFile mpf) throws IOException {
+        DogEntity dogEntity = new DogEntity();
 
-        newDog.setName(dto.getName());
-        //newDog.setPicture(pic.getBytes());
-        newDog.setBreed(dto.getBreed());
-        newDog.setAge(dto.getAge());
-        dogRepository.save(newDog);
+        // Set fields from newDogRequest
+        dogEntity.setName(newDogRequest.getName());
+        dogEntity.setBreed(newDogRequest.getBreed());
+        dogEntity.setAge(newDogRequest.getAge());
+        if (mpf != null && !mpf.isEmpty()) {
+            dogEntity.setPicture(mpf.getBytes());
+        }
+
+        // Save the new entity
+        dogRepository.save(dogEntity);
     }
 
     public void editDog(Integer id, DetailedDogDTO editRequest, MultipartFile mpf) throws IOException {
@@ -51,8 +56,10 @@ public class DogService {
             if (editRequest.getName() != null) {
                 dogEntity.setName(editRequest.getName());
             }
-            mpf.getBytes();
-            dogEntity.setPicture(mpf.getBytes());
+            if (mpf != null) {
+                mpf.getBytes();
+                dogEntity.setPicture(mpf.getBytes());
+            }
             if (editRequest.getAge() != null) {
                 dogEntity.setAge(editRequest.getAge());
             }
diff --git a/frontend/src/components/dogs/AddDog.vue b/frontend/src/components/dogs/AddDog.vue
index fb0e972..c9ed691 100644
--- a/frontend/src/components/dogs/AddDog.vue
+++ b/frontend/src/components/dogs/AddDog.vue
@@ -17,6 +17,18 @@
         <label for="age">Kora</label>
         <input id="age" v-model="dog.age" type="number" required>
       </div>
+      <div class="input-group">
+        <label for="dog-image">Kép</label>
+        <file-pond
+            id="dog-image"
+            name="dogPicture"
+            ref="pond"
+            label-idle="Húzza ide a fájlt..."
+            allow-multiple="false"
+            accepted-file-types="image/jpeg, image/png"
+            v-on:init="handleFilePondInit"
+        />
+      </div>
       <button type="submit">Mentés</button>
     </form>
   </div>
@@ -25,8 +37,18 @@
 <script>
 import { axios, apiURL } from '@/axiosConfig.js';
 import { mapState } from 'vuex';
+import vueFilePond from 'vue-filepond';
+import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
+import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
+import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
+import 'filepond/dist/filepond.min.css';
+
+const FilePond = vueFilePond(FilePondPluginFileValidateType, FilePondPluginImagePreview);
 
 export default {
+  components: {
+    FilePond
+  },
   data() {
     return {
       dog: {},
@@ -37,6 +59,9 @@ export default {
     ...mapState(['token']),
   },
   methods: {
+    handleFilePondInit: function() {
+      console.log('FilePond has initialized');
+    },
     validateAndAddDog() {
       if (this.validateForm()) {
         this.addDog();
@@ -67,11 +92,36 @@ export default {
       return true;
     },
     async addDog() {
+      const files = this.$refs.pond.getFiles();
+      const pictureFile = files.length > 0 ? files[0].file : null;
+
+      const dogData = {
+        name: this.dog.name,
+        breed: this.dog.breed,
+        age: this.dog.age
+      };
+
+      const formData = new FormData();
+      formData.append('dog', JSON.stringify(dogData)); // Convert dog data to JSON string
+      if (pictureFile) {
+        formData.append('picture', pictureFile, pictureFile.name); // Append picture as a blob only if it exists
+      } else {
+        formData.append('picture', null); // Append null if picture does not exist
+      }
+
       const config = {
-        headers: { Authorization: `Bearer ${this.token}` },
+        headers: {
+          Authorization: `Bearer ${this.token}`,
+        },
       };
-      await axios.post(apiURL + '/newdog', this.dog, config);
-      this.$router.push(`/dogs`);
+
+      try {
+        await axios.post(apiURL + `/newdog`, formData, config);
+        this.$router.push(`/dogs`);
+      } catch (error) {
+        console.error('Hiba történt a kutya hozzáadása közben:', error);
+        // Handle error
+      }
     },
   },
 };
diff --git a/frontend/src/components/dogs/AllDogs.vue b/frontend/src/components/dogs/AllDogs.vue
index a4e258b..2844349 100644
--- a/frontend/src/components/dogs/AllDogs.vue
+++ b/frontend/src/components/dogs/AllDogs.vue
@@ -5,7 +5,7 @@
       Betöltés...
     </div>
     <div v-else-if="errorMessage || fetchError || !hasDogs" class="alert alert-error">
-      {{ errorMessage || (fetchError ? 'Could not fetch dogs.' : 'No dogs found.') }}
+      {{ errorMessage || (fetchError ? 'Nem sikerült lekérdezni a kutyákat.' : 'Nem találhatók kutyák.') }}
     </div>
     <div v-else class="dog-item-container">
       <div v-for="dog in dogs" :key="dog.id" class="dog-item" @click="viewDog(dog.id)">
@@ -77,11 +77,11 @@ export default {
 }
 
 .dog-item {
-  @apply flex flex-col items-center bg-white m-4 p-4 rounded shadow w-1/4 min-w-64 min-h-64;
+  @apply flex flex-col items-center m-4 p-4 rounded w-1/4 min-w-64 min-h-64;
 }
 
 .dog-image {
-  @apply w-full h-64 object-cover mb-4 rounded min-w-64 min-h-64;
+  @apply w-full h-64 object-cover bg-white mb-4 rounded min-w-64 min-h-64;
 }
 
 .dog-name {
diff --git a/frontend/src/components/dogs/EditDog.vue b/frontend/src/components/dogs/EditDog.vue
index 7d6f757..106a936 100644
--- a/frontend/src/components/dogs/EditDog.vue
+++ b/frontend/src/components/dogs/EditDog.vue
@@ -97,7 +97,8 @@ export default {
       return true;
     },
     async editDog() {
-      const pictureFile = this.$refs.pond.getFiles()[0].file;
+      const files = this.$refs.pond.getFiles();
+      const pictureFile = files.length > 0 ? files[0].file : null;
 
       const dogData = {
         name: this.dog.name,
@@ -107,12 +108,15 @@ export default {
 
       const formData = new FormData();
       formData.append('dog', JSON.stringify(dogData)); // Convert dog data to JSON string
-      formData.append('picture', pictureFile, pictureFile.name); // Append picture as a blob
+      if (pictureFile) {
+        formData.append('picture', pictureFile, pictureFile.name); // Append picture as a blob only if it exists
+      } else {
+        formData.append('picture', null); // Append null if picture does not exist
+      }
 
       const config = {
         headers: {
           Authorization: `Bearer ${this.token}`,
-          // 'Content-Type': 'multipart/form-data'
         },
       };
 
@@ -120,7 +124,7 @@ export default {
         await axios.post(apiURL + `/dogs/${this.$route.params.id}/edit`, formData, config);
         this.$router.push(`/dog/${this.$route.params.id}`);
       } catch (error) {
-        console.error('Error editing dog:', error);
+        console.error('Hiba történt a kutya szerkesztése közben:', error);
         // Handle error
       }
     },
@@ -132,9 +136,6 @@ export default {
 </script>
 
 <style lang="postcss">
-.edit-dog-container {
-  @apply flex flex-col items-center justify-center min-h-screen bg-blue-200;
-}
 
 .input-group {
   @apply mb-4;
diff --git a/frontend/src/components/dogs/SingleDog.vue b/frontend/src/components/dogs/SingleDog.vue
index 002250a..46c025b 100644
--- a/frontend/src/components/dogs/SingleDog.vue
+++ b/frontend/src/components/dogs/SingleDog.vue
@@ -1,7 +1,9 @@
 <template>
   <div v-if="dog" class="global-container">
     <h1 class="text-2xl font-bold mb-4">{{ dog.name }}</h1>
-    <img :src="dog.picture" :alt="`${dog.name} képe`" class="w-64 h-64 object-cover mb-4 rounded shadow"/>
+    <img v-if="dog"
+         :src="dog.picture ? 'data:image/jpeg;base64,' + dog.picture : 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 50 50\'%3E%3Ctext y=\'.9em\' font-size=\'20\'%3E' + dog.id + '%3C/text%3E%3C/svg%3E'"
+         :alt="`Image of ${dog.name}`" class="dog-image"/>
     <p class="text-lg mb-2"><strong>Kor:</strong> {{ dog.age }}</p>
     <p class="text-lg"><strong>Faj:</strong> {{ dog.breed }}</p>
     <router-link :to="`/edit-dog/${dog.id}`" tag="button" class="button">Szerkesztés</router-link>
@@ -12,6 +14,7 @@
 
 <script>
 import { axios, apiURL } from '@/axiosConfig.js';
+import {mapState} from "vuex";
 
 export default {
   name: 'SingleDog',
@@ -30,11 +33,16 @@ export default {
       // Handle error
     }
   },
+  computed: {
+    ...mapState(['token']),
+  },
   methods: {
     async deleteDog() {
       if (window.confirm('Biztosan törölni akarod ezt a kutyát?')) {
         const config = {
-          headers: { Authorization: `Bearer ${this.token}` },
+          headers: {
+            Authorization: `Bearer ${this.token}`,
+          },
         };
         try {
           await axios.delete(apiURL + `/dogs/${this.$route.params.id}`, config);
@@ -42,13 +50,16 @@ export default {
         } catch (error) {
           if (error.response && error.response.status === 401) {
             this.$router.push('/login');
+          } else if (error.response && error.response.status === 404) {
+            // Handle "Not Found" error
+            console.error('Dog not found:', error);
           } else {
-            console.error(error);
             // Handle other types of errors
+            console.error('Error deleting dog:', error);
           }
         }
       }
-    },
+    }
   },
 };
 </script>
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 4a6b44d..a014ef5 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -51,7 +51,7 @@ p {
 }
 
 .global-container {
-    @apply flex flex-col items-center justify-center min-h-screen bg-blue-200;
+    @apply flex flex-col items-center justify-center bg-blue-200;
 }
 
 .input-group {
-- 
GitLab