From 91ede955300a9c30886e997ed5b6b1c5be6eda0b Mon Sep 17 00:00:00 2001 From: Julian Krauser <jkrauser209@gmail.com> Date: Tue, 29 Apr 2025 18:32:08 +0200 Subject: [PATCH] Image Upload --- .../admin/management/setting/BaseSetting.vue | 10 +- .../management/setting/ClubImageSetting.vue | 100 ++++++++++++++---- .../admin/management/setting/MailSetting.vue | 2 +- src/stores/admin/management/setting.ts | 39 +++++-- 4 files changed, 117 insertions(+), 34 deletions(-) diff --git a/src/components/admin/management/setting/BaseSetting.vue b/src/components/admin/management/setting/BaseSetting.vue index 96c1308..a879de1 100644 --- a/src/components/admin/management/setting/BaseSetting.vue +++ b/src/components/admin/management/setting/BaseSetting.vue @@ -9,7 +9,14 @@ <button type="submit" class="!w-fit !h-fit !p-0"> <CheckIcon class="h-5 w-5 cursor-pointer" /> </button> - <button type="reset" class="!w-fit !h-fit !p-0" @click="enableEdit = false"> + <button + type="reset" + class="!w-fit !h-fit !p-0" + @click=" + enableEdit = false; + $emit('reset'); + " + > <XMarkIcon class="h-5 w-5 cursor-pointer" /> </button> </div> @@ -48,6 +55,7 @@ export default defineComponent({ required: true, }, }, + emits: ["reset"], data() { return { enableEdit: false as boolean, diff --git a/src/components/admin/management/setting/ClubImageSetting.vue b/src/components/admin/management/setting/ClubImageSetting.vue index e2ab129..1a1d675 100644 --- a/src/components/admin/management/setting/ClubImageSetting.vue +++ b/src/components/admin/management/setting/ClubImageSetting.vue @@ -1,31 +1,48 @@ <template> - <BaseSetting title="Vereins-Auftritt Einstellungen" :submit-function="submit" v-slot="{ enableEdit }"> + <BaseSetting title="Vereins-Auftritt Einstellungen" :submit-function="submit" v-slot="{ enableEdit }" @reset="reset"> <div class="w-full"> <p>Vereins-Icon</p> - <AppIcon v-if="clubSettings['club.icon'] != '' && !overwriteIcon" class="h-10! max-w-full mx-auto" /> - <img v-else-if="overwriteIcon" ref="icon_img" class="hidden w-full h-20 object-contain" /> - <div - v-else - class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm cursor-pointer" - @click="($refs.icon as HTMLInputElement).click()" - > - Kein eigenes Icon ausgewählt + <div class="flex flex-row gap-2"> + <AppIcon v-if="icon != '' && !overwriteIcon" class="h-10! max-w-full mx-auto" /> + <div + v-else-if="!overwriteIcon" + class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm" + :class="{ 'cursor-pointer': enableEdit }" + @click="enableEdit ? ($refs.icon as HTMLInputElement).click() : null" + > + Kein eigenes Icon ausgewählt + </div> + <img ref="icon_img" class="hidden w-full h-20 object-contain" /> + <XMarkIcon + v-if="enableEdit && (icon != '' || overwriteIcon)" + class="h-5 w-5 cursor-pointer" + @click="resetImage('icon')" + /> </div> <input class="hidden!" type="file" ref="icon" accept="image/*" @change="previewImage('icon')" /> </div> <div class="w-full"> <p>Vereins-Logo</p> - <AppLogo v-if="clubSettings['club.logo'] != '' && !overwriteLogo" class="h-10! max-w-full mx-auto" /> - <img v-else-if="overwriteLogo" ref="logo_img" class="hidden w-full h-20 object-contain" /> - <div - v-else - class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm cursor-pointer" - @click="($refs.logo as HTMLInputElement).click()" - > - Kein eigenes Logo ausgewählt + <div class="flex flex-row gap-2"> + <AppLogo v-if="logo != '' && !overwriteLogo" class="h-10! max-w-full mx-auto" /> + <div + v-else-if="!overwriteLogo" + class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm" + :class="{ 'cursor-pointer': enableEdit }" + @click="enableEdit ? ($refs.logo as HTMLInputElement).click() : null" + > + Kein eigenes Logo ausgewählt + </div> + <img ref="logo_img" class="hidden w-full h-20 object-contain" /> + <XMarkIcon + v-if="enableEdit && (logo != '' || overwriteLogo)" + class="h-5 w-5 cursor-pointer" + @click="resetImage('logo')" + /> </div> - <input class="hidden!" type="file" ref="logo" accept="image/*" @change="previewImage('logo')" /></div - ></BaseSetting> + <input class="hidden!" type="file" ref="logo" accept="image/*" @change="previewImage('logo')" /> + </div> + </BaseSetting> </template> <script setup lang="ts"> @@ -37,12 +54,20 @@ import AppLogo from "@/components/AppLogo.vue"; import { useAbilityStore } from "@/stores/ability"; import type { SettingString } from "@/types/settingTypes"; import BaseSetting from "./BaseSetting.vue"; +import { XMarkIcon } from "@heroicons/vue/24/outline"; </script> <script lang="ts"> export default defineComponent({ + watch: { + clubSettings() { + this.reset(); + }, + }, data() { return { + logo: "", + icon: "", overwriteIcon: false as boolean, overwriteLogo: false as boolean, }; @@ -54,8 +79,35 @@ export default defineComponent({ return this.readByTopic("club"); }, }, + mounted() { + this.reset(); + }, methods: { ...mapActions(useSettingStore, ["updateSettings", "uploadImage"]), + reset() { + this.icon = this.clubSettings["club.icon"]; + this.overwriteIcon = false; + (this.$refs.icon_img as HTMLImageElement).style.display = "none"; + (this.$refs.icon as HTMLInputElement).value = ""; + + this.logo = this.clubSettings["club.logo"]; + this.overwriteLogo = false; + (this.$refs.logo_img as HTMLImageElement).style.display = "none"; + (this.$refs.logo as HTMLInputElement).value = ""; + }, + resetImage(inputname: "icon" | "logo") { + if (inputname == "icon") { + this.icon = ""; + this.overwriteIcon = false; + (this.$refs.icon_img as HTMLImageElement).style.display = "none"; + (this.$refs.icon as HTMLInputElement).value = ""; + } else { + this.logo = ""; + this.overwriteLogo = false; + (this.$refs.logo_img as HTMLImageElement).style.display = "none"; + (this.$refs.logo as HTMLInputElement).value = ""; + } + }, previewImage(inputname: "icon" | "logo") { let input = this.$refs[inputname] as HTMLInputElement; let previewElement = this.$refs[inputname + "_img"] as HTMLImageElement; @@ -68,6 +120,12 @@ export default defineComponent({ }; reader.readAsDataURL(input.files[0]); + + if (inputname == "icon") { + this.overwriteIcon = true; + } else { + this.overwriteLogo = true; + } } else { previewElement.src = ""; previewElement.style.display = "none"; @@ -76,11 +134,11 @@ export default defineComponent({ submit(e: any) { return this.uploadImage([ { - key: "club.icon" as SettingString, + key: "club.icon", value: (this.$refs.icon as HTMLInputElement).files?.[0], }, { - key: "club.logo" as SettingString, + key: "club.logo", value: (this.$refs.logo as HTMLInputElement).files?.[0], }, ]); diff --git a/src/components/admin/management/setting/MailSetting.vue b/src/components/admin/management/setting/MailSetting.vue index 64ca03d..4b09057 100644 --- a/src/components/admin/management/setting/MailSetting.vue +++ b/src/components/admin/management/setting/MailSetting.vue @@ -34,7 +34,7 @@ <label for="secure">Secure-Verbindung (setzen bei Port 465)</label> </div> <div class="w-full"> - <label for="password">Passwort (optional)</label> + <label for="password">Passwort (optional - leeres Feld setzt Passwort nicht zurück)</label> <input id="password" type="password" :readonly="!enableEdit" autocomplete="new-password" /> </div> </BaseSetting> diff --git a/src/stores/admin/management/setting.ts b/src/stores/admin/management/setting.ts index 20c39bc..98bd8a8 100644 --- a/src/stores/admin/management/setting.ts +++ b/src/stores/admin/management/setting.ts @@ -54,26 +54,43 @@ export const useSettingStore = defineStore("setting", { key: K, value: SettingValueMapping[K] ): Promise<AxiosResponse<any, any>> { - return await http.put("/admin/setting", { - setting: key, - value: value, - }); + return await http + .put("/admin/setting", { + setting: key, + value: value, + }) + .then((res) => { + this.settings[key] = value; + return res; + }); }, async updateSettings<K extends SettingString>( data: { key: K; value: SettingValueMapping[K] }[] ): Promise<AxiosResponse<any, any>> { - return await http.put("/admin/setting/multi", data); + return await http.put("/admin/setting/multi", data).then((res) => { + for (const element of data) { + this.settings[element.key] = element.value; + } + return res; + }); }, - async uploadImage(data: { key: SettingString; value?: File }[]): Promise<AxiosResponse<any, any>> { + async uploadImage(data: { key: "club.logo" | "club.icon"; value?: File }[]): Promise<AxiosResponse<any, any>> { const formData = new FormData(); for (let entry of data) { if (entry.value) formData.append(entry.key, entry.value); } - return await http.put("/admin/setting/images", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }); + return await http + .put("/admin/setting/images", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((res) => { + for (const element of data) { + this.settings[element.key] = element.value ? "configured" : ""; + } + return res; + }); }, async resetSetting(key: SettingString): Promise<AxiosResponse<any, any>> { return await http.delete(`/admin/setting/${key}`);