main
vue 171 lines 4.55 KB
Raw
1 <template>
2 <n-spin :show="loading" content-class="flex grow flex-col">
3 <div class="flex flex-col gap-10">
4 <n-form-item label="Title" path="title" :show-feedback="false">
5 <n-input v-model:value="model.title" clearable />
6 </n-form-item>
7
8 <div class="flex gap-3">
9 <div v-if="model.logo" class="relative">
10 <div
11 class="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/10 opacity-0 transition-opacity duration-300 hover:opacity-100"
12 @click="model.logo = null"
13 >
14 <Icon :name="RemoveIcon" :size="30" class="text-secondary drop-shadow-md/90" />
15 </div>
16
17 <img :src="model.logo" width="66" height="66" class="object-cover" />
18 </div>
19 <n-form-item label="Logo" path="logo" :show-feedback="false">
20 <ImageCropper v-slot="{ openCropper }" placeholder="Select a Logo" @crop="setCroppedImage">
21 <n-button @click="openCropper()">
22 <template #icon>
23 <Icon :name="EditIcon" />
24 </template>
25 Edit Logo Image
26 </n-button>
27 </ImageCropper>
28 </n-form-item>
29 </div>
30
31 <div>
32 <n-button type="primary" :loading @click="save()">
33 <template #icon>
34 <Icon :name="SaveIcon" />
35 </template>
36 Save Changes
37 </n-button>
38 </div>
39 </div>
40 </n-spin>
41 </template>
42
43 <script setup lang="ts">
44 // TODO-FE: refactor
45 import type { CustomerPortalSettingsPayload } from "@/api/endpoints/customerPortal"
46 import type { ImageCropperResult } from "@/components/common/ImageCropper.vue"
47 import type { CustomerPortalSettings } from "@/types/customerPortal"
48 import _split from "lodash/split"
49 import { NButton, NFormItem, NInput, NSpin, useMessage } from "naive-ui"
50 import { onBeforeMount, ref, watch } from "vue"
51 import Api from "@/api"
52 import Icon from "@/components/common/Icon.vue"
53 import ImageCropper from "@/components/common/ImageCropper.vue"
54
55 export interface SettingsModel {
56 title: string | null
57 logo: string | null
58 }
59
60 const emit = defineEmits<{
61 (e: "update", value: SettingsModel): void
62 (e: "success"): void
63 }>()
64
65 const SaveIcon = "carbon:save"
66 const EditIcon = "uil:image-edit"
67 const RemoveIcon = "carbon:trash-can"
68 const message = useMessage()
69 const loading = ref(false)
70 const settings = ref<CustomerPortalSettings | null>(null)
71 const model = ref<SettingsModel>(getDefaultModel())
72
73 function setCroppedImage(result: ImageCropperResult) {
74 const canvas = result.canvas as HTMLCanvasElement
75 model.value.logo = canvas.toDataURL()
76 }
77
78 function getDefaultModel(entity?: CustomerPortalSettings): SettingsModel {
79 return {
80 title: entity?.title || "",
81 logo:
82 entity?.logo_base64 && entity?.logo_mime_type
83 ? `data:${entity.logo_mime_type};base64,${entity.logo_base64}`
84 : null
85 }
86 }
87
88 const DATA_URL_MIME_REGEX = /data:([^;]+);base64/
89
90 function getLogoMeta(logo?: string | null): { base64: string | null; mime_type: string | null } {
91 // Parse data URL format: data:image/png;base64,iVBORw0KG...
92 const parts = _split(logo, ",")
93 if (!logo || parts.length !== 2) {
94 return {
95 base64: null,
96 mime_type: null
97 }
98 }
99
100 const base64 = parts[1]
101 const mimeMatch = parts[0]?.match(DATA_URL_MIME_REGEX)
102 const mime_type = mimeMatch ? mimeMatch[1] : null
103
104 return {
105 base64: base64 ?? null,
106 mime_type: mime_type ?? null
107 }
108 }
109
110 function save() {
111 loading.value = true
112
113 const payload: CustomerPortalSettingsPayload = {
114 title: model.value.title || null,
115 logo_base64: getLogoMeta(model.value.logo).base64,
116 logo_mime_type: getLogoMeta(model.value.logo).mime_type
117 }
118
119 Api.customerPortal
120 .setSettings(payload)
121 .then(res => {
122 if (res.data.success) {
123 message.success(res.data?.message || "Customer Portal settings updated successfully")
124 // Reload the data to show updated values
125 emit("success")
126 } else {
127 message.warning(res.data?.message || "Failed to update metadata")
128 }
129 })
130 .catch(err => {
131 const errorMsg = err.response?.data?.message || "An error occurred while updating metadata"
132 message.error(errorMsg)
133 })
134 .finally(() => {
135 loading.value = false
136 })
137 }
138
139 function getSettings() {
140 loading.value = true
141
142 Api.customerPortal
143 .getSettings()
144 .then(res => {
145 if (res.data.success) {
146 settings.value = res.data.settings
147 model.value = getDefaultModel(settings.value)
148 } else {
149 message.warning(res.data?.message || "An error occurred. Please try again later.")
150 }
151 })
152 .catch(err => {
153 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
154 })
155 .finally(() => {
156 loading.value = false
157 })
158 }
159
160 watch(
161 model,
162 val => {
163 emit("update", val)
164 },
165 { immediate: true, deep: true }
166 )
167
168 onBeforeMount(() => {
169 getSettings()
170 })
171 </script>