pukaad-ui-lib 1.296.0 → 1.297.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/card/card-review.d.vue.ts +4 -2
  3. package/dist/runtime/components/card/card-review.vue +115 -68
  4. package/dist/runtime/components/card/card-review.vue.d.ts +4 -2
  5. package/dist/runtime/components/display/display-rating-summary.d.vue.ts +5 -1
  6. package/dist/runtime/components/display/display-rating-summary.vue +33 -13
  7. package/dist/runtime/components/display/display-rating-summary.vue.d.ts +5 -1
  8. package/dist/runtime/components/drawer/drawer-post-review.d.vue.ts +4 -6
  9. package/dist/runtime/components/drawer/drawer-post-review.vue +98 -100
  10. package/dist/runtime/components/drawer/drawer-post-review.vue.d.ts +4 -6
  11. package/dist/runtime/components/drawer/drawer-suggest-place/drawer-suggest-place.vue +2 -1
  12. package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.d.vue.ts +18 -6
  13. package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.vue +133 -88
  14. package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.vue.d.ts +18 -6
  15. package/dist/runtime/components/image/image-cropper.d.vue.ts +1 -1
  16. package/dist/runtime/components/image/image-cropper.vue.d.ts +1 -1
  17. package/dist/runtime/components/input/input-address.d.vue.ts +1 -1
  18. package/dist/runtime/components/input/input-address.vue.d.ts +1 -1
  19. package/dist/runtime/components/input/input-file.d.vue.ts +1 -1
  20. package/dist/runtime/components/input/input-file.vue.d.ts +1 -1
  21. package/dist/runtime/components/input/input-password.d.vue.ts +1 -1
  22. package/dist/runtime/components/input/input-password.vue.d.ts +1 -1
  23. package/dist/runtime/components/modal/modal-password-confirmed.d.vue.ts +1 -1
  24. package/dist/runtime/components/modal/modal-password-confirmed.vue.d.ts +1 -1
  25. package/dist/runtime/components/modal/modal-password-verify.d.vue.ts +1 -1
  26. package/dist/runtime/components/modal/modal-password-verify.vue.d.ts +1 -1
  27. package/dist/runtime/components/modal/modal-phone-OTP.d.vue.ts +1 -1
  28. package/dist/runtime/components/modal/modal-phone-OTP.vue.d.ts +1 -1
  29. package/package.json +1 -1
  30. /package/dist/runtime/assets/svg/socials/{WhatsApp.svg → Whatsapp.svg} +0 -0
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.296.0",
4
+ "version": "1.297.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -6,11 +6,13 @@ type __VLS_Props = {
6
6
  disabledMenu?: boolean;
7
7
  };
8
8
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ edit: (item: CardReviewProps) => any;
9
10
  "toggle-like": (item: CardReviewProps, liked: boolean) => any;
10
- "select-image": (item: CardReviewProps, index: number, liked: boolean) => any;
11
+ "select-image": (item: CardReviewProps, index: number) => any;
11
12
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ onEdit?: ((item: CardReviewProps) => any) | undefined;
12
14
  "onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
13
- "onSelect-image"?: ((item: CardReviewProps, index: number, liked: boolean) => any) | undefined;
15
+ "onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
14
16
  }>, {
15
17
  item: CardReviewProps;
16
18
  disabledMenu: boolean;
@@ -1,70 +1,103 @@
1
1
  <template>
2
- <div class="py-[16px] border-b-[1px] border-mercury flex gap-[16px] w-full">
3
- <div v-if="props.item.user.avatar">
4
- <Avatar :src="props.item.user.avatar" alt="profile_myProfile" :size="30" class="cursor-pointer"
5
- @click="NavigateToProfile(props.item.user.path_name)" />
6
- </div>
7
- <div class="flex flex-col gap-[24px] w-full">
8
- <div class="flex flex-col gap-[8px]">
9
- <div class="flex flex-col gap-[6px]">
10
- <div class="flex flex-col gap-[4px]">
11
- <div class="flex justify-between items-center">
12
- <div @click="NavigateToProfile(props.item.user.path_name)" class="font-body-large cursor-pointer">
13
- {{ props.item.user?.name }}
14
- </div>
15
- <div class="flex gap-[8px] items-center">
16
- <Button variant="text" :color="liked ? 'primary' : 'default'" :disabled="props.disabledLike"
17
- :aria-pressed="liked" @click="toggleLike">
18
- <Icon :name="liked ? 'fa6-solid:thumbs-up' : 'fa6-regular:thumbs-up'" :size="20" />
19
- {{ convertNumber(likeCount) }}
20
- </Button>
21
- <PickerOptionMenuUser v-if="!props.disabledMenu" :state="menuType" disabled-padding />
22
- </div>
23
- </div>
24
- <div class="text-gray font-body-small">
25
- {{ convertNumber(props.item.user?.review_count ?? 0) }} รีวิว •
26
- {{ convertNumber(props.item.user?.like_count ?? 0) }} ชื่นชอบรีวิว
27
- </div>
28
- </div>
29
- <div class="flex gap-[8px] items-center">
30
- <InputRating :size="16" readonly :model-value="props.item.review?.rating" />
31
- <div class="text-gray font-body-small">
32
- {{ props.item.review?.created_at }}
33
- </div>
34
- </div>
35
- </div>
36
-
37
- <div v-if="props.item.review?.description" class="font-body-large">
38
- {{ props.item.review?.description }}
39
- </div>
40
-
41
- <div v-if="props.item.review?.images?.length" class="flex gap-[8px]">
42
- <DisplayImageReview :items="props.item.review?.images" @select="selectImage" />
43
- </div>
44
- </div>
45
- <div v-if="props.item.replies">
46
- <div class="p-[16px] rounded-sm bg-bright">
47
- <div class="text-gray font-body-large">
48
- การตอบกลับจาก {{ props.item.replies?.author }}
49
- </div>
50
- <div class="font-body-large">
51
- {{ props.item.replies?.description }}
52
- </div>
53
- </div>
54
- </div>
55
- </div>
56
- </div>
57
-
58
- <ModalMediaView v-model="isOpen" :items="props.item.review?.images" :start-index="startIndex" title="รูปภาพรีวิว" />
2
+ <div class="py-[16px] border-b-[1px] border-mercury flex gap-[16px] w-full">
3
+ <div v-if="props.item.user.avatar">
4
+ <Avatar
5
+ :src="props.item.user.avatar"
6
+ alt="profile_myProfile"
7
+ :size="30"
8
+ class="cursor-pointer"
9
+ @click="navigateToProfile(props.item.user.path_name)"
10
+ />
11
+ </div>
12
+ <div class="flex flex-col gap-[24px] w-full">
13
+ <div class="flex flex-col gap-[8px]">
14
+ <div class="flex flex-col gap-[6px]">
15
+ <div class="flex flex-col gap-[4px]">
16
+ <div class="flex justify-between items-center">
17
+ <div
18
+ class="font-body-large cursor-pointer"
19
+ @click="navigateToProfile(props.item.user.path_name)"
20
+ >
21
+ {{ props.item.user?.name }}
22
+ </div>
23
+ <div class="flex gap-[8px] items-center">
24
+ <Button
25
+ v-if="props.item.user?.is_my_review"
26
+ variant="text"
27
+ color="default"
28
+ disabled-padding
29
+ @click="emit('edit', props.item)"
30
+ >
31
+ <Icon name="lucide:pencil" :size="16" />
32
+ แก้ไข
33
+ </Button>
34
+ <Button
35
+ variant="text"
36
+ :color="liked ? 'primary' : 'default'"
37
+ :disabled="props.disabledLike"
38
+ :aria-pressed="liked"
39
+ @click="toggleLike"
40
+ >
41
+ <Icon
42
+ :name="liked ? 'pukaad:thumbs-up-solid' : 'pukaad:thumbs-up-regular'"
43
+ :size="20"
44
+ />
45
+ {{ convertNumber(likeCount) }}
46
+ </Button>
47
+ <PickerOptionMenuUser
48
+ v-if="!props.disabledMenu"
49
+ :state="menuType"
50
+ disabled-padding
51
+ />
52
+ </div>
53
+ </div>
54
+ <div class="text-gray font-body-small">
55
+ {{ convertNumber(props.item.user?.review_count ?? 0) }} รีวิว •
56
+ {{ convertNumber(props.item.user?.like_count ?? 0) }} ชื่นชอบรีวิว
57
+ </div>
58
+ </div>
59
+ <div class="flex gap-[8px] items-center">
60
+ <InputRating :size="11" readonly :model-value="props.item.review?.rating" />
61
+ <div class="text-gray font-body-small">
62
+ {{ convertDate(props.item.review?.created_at ?? "") }}
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <div v-if="props.item.review?.description" class="font-body-large">
68
+ {{ props.item.review?.description }}
69
+ </div>
70
+
71
+ <div v-if="props.item.review?.images?.length" class="flex gap-[8px]">
72
+ <DisplayImageReview :items="props.item.review?.images" @select="selectImage" />
73
+ </div>
74
+ </div>
75
+
76
+ <div v-if="props.item.replies">
77
+ <div class="p-[16px] rounded-sm bg-bright">
78
+ <div class="text-gray font-body-large">
79
+ การตอบกลับจาก {{ props.item.replies?.author }}
80
+ </div>
81
+ <div class="font-body-large">{{ props.item.replies?.description }}</div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <ModalMediaView
88
+ v-model="isOpen"
89
+ :items="props.item.review?.images"
90
+ :start-index="startIndex"
91
+ title="รูปภาพรีวิว"
92
+ />
59
93
  </template>
60
94
 
61
95
  <script setup>
62
- import { computed, ref } from "vue";
96
+ import { computed, ref, watch } from "vue";
63
97
  import { useRouter } from "vue-router";
64
98
  import { useConvert } from "../../composables/useConvert";
65
99
  const router = useRouter();
66
- const { convertNumber } = useConvert();
67
- const isOpen = ref(false);
100
+ const { convertNumber, convertDate } = useConvert();
68
101
  const props = defineProps({
69
102
  item: { type: Object, required: true, default: () => ({
70
103
  review_id: "0",
@@ -74,7 +107,8 @@ const props = defineProps({
74
107
  avatar: "",
75
108
  path_name: "",
76
109
  review_count: 0,
77
- like_count: 0
110
+ like_count: 0,
111
+ is_my_review: false
78
112
  },
79
113
  review: {
80
114
  rating: 0,
@@ -94,15 +128,29 @@ const props = defineProps({
94
128
  disabledLike: { type: Boolean, required: false, default: false },
95
129
  disabledMenu: { type: Boolean, required: false, default: false }
96
130
  });
97
- const emit = defineEmits(["toggle-like", "select-image"]);
131
+ const emit = defineEmits(["toggle-like", "select-image", "edit"]);
132
+ const isOpen = ref(false);
133
+ const startIndex = ref(0);
98
134
  const liked = ref(props.item.review?.liked ?? false);
99
135
  const likeCount = ref(props.item.review?.like_count ?? 0);
136
+ watch(
137
+ () => props.item.review?.liked,
138
+ (val) => {
139
+ liked.value = val ?? false;
140
+ }
141
+ );
142
+ watch(
143
+ () => props.item.review?.like_count,
144
+ (val) => {
145
+ likeCount.value = val ?? 0;
146
+ }
147
+ );
100
148
  const menuType = computed(() => props.isProfile ? "my-profile" : "profile");
101
- const NavigateToProfile = (pathName) => {
149
+ const navigateToProfile = (pathName) => {
102
150
  if (!pathName) return;
103
- router.push(`/${pathName}`);
151
+ router.push(`/@${pathName}`);
104
152
  };
105
- function toggleLike() {
153
+ const toggleLike = () => {
106
154
  if (liked.value) {
107
155
  likeCount.value -= 1;
108
156
  liked.value = false;
@@ -111,11 +159,10 @@ function toggleLike() {
111
159
  liked.value = true;
112
160
  }
113
161
  emit("toggle-like", props.item, liked.value);
114
- }
115
- const startIndex = ref(0);
162
+ };
116
163
  const selectImage = (index) => {
117
164
  startIndex.value = index;
118
165
  isOpen.value = true;
119
- emit("select-image", props.item, index, liked.value);
166
+ emit("select-image", props.item, index);
120
167
  };
121
168
  </script>
@@ -6,11 +6,13 @@ type __VLS_Props = {
6
6
  disabledMenu?: boolean;
7
7
  };
8
8
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ edit: (item: CardReviewProps) => any;
9
10
  "toggle-like": (item: CardReviewProps, liked: boolean) => any;
10
- "select-image": (item: CardReviewProps, index: number, liked: boolean) => any;
11
+ "select-image": (item: CardReviewProps, index: number) => any;
11
12
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
13
+ onEdit?: ((item: CardReviewProps) => any) | undefined;
12
14
  "onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
13
- "onSelect-image"?: ((item: CardReviewProps, index: number, liked: boolean) => any) | undefined;
15
+ "onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
14
16
  }>, {
15
17
  item: CardReviewProps;
16
18
  disabledMenu: boolean;
@@ -8,7 +8,11 @@ export interface DisplayRatingSummaryProps {
8
8
  distribution?: RatingDistribution[];
9
9
  badge?: number;
10
10
  }
11
- declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<DisplayRatingSummaryProps> & Readonly<{}>, {
11
+ declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "write-review": () => any;
13
+ }, string, import("vue").PublicProps, Readonly<DisplayRatingSummaryProps> & Readonly<{
14
+ "onWrite-review"?: (() => any) | undefined;
15
+ }>, {
12
16
  rating: number | string;
13
17
  totalReviews: number | string;
14
18
  distribution: RatingDistribution[];
@@ -1,30 +1,49 @@
1
1
  <template>
2
2
  <div class="flex gap-[24px] items-center">
3
3
  <!-- Left: คะแนนรวม -->
4
- <div class="flex flex-col items-center text-primary flex-shrink-0 w-[100px]">
5
- <div class="text-[56px] font-bold leading-none">
6
- {{ Number(props.rating).toFixed(1) }}
7
- </div>
8
- <div class="font-body-large mt-[4px]">
9
- {{ convertNumber(Number(props.totalReviews)) }} รีวิว
4
+ <div class="flex flex-col gap-[8px] w-[214px] justify-center items-center">
5
+ <div
6
+ class="flex flex-col items-center text-primary flex-shrink-0 w-[100px]"
7
+ >
8
+ <div class="text-[56px] font-bold leading-none">
9
+ {{ Number(props.rating).toFixed(1) }}
10
+ </div>
11
+ <div class="font-body-large">
12
+ {{ convertNumber(Number(props.totalReviews)) }} รีวิว
13
+ </div>
10
14
  </div>
15
+ <Button color="primary" @click="emit('write-review')">
16
+ <Icon name="pukaad:write-review" /> เขียนรีวิว
17
+ </Button>
11
18
  </div>
12
-
13
19
  <!-- Right: แถบการกระจายคะแนน -->
14
20
  <div class="flex flex-col gap-[8px] flex-1">
15
- <div v-for="(item, idx) in sortedDistribution" :key="item.stars" class="flex items-center gap-[12px]">
21
+ <div
22
+ v-for="(item, idx) in sortedDistribution"
23
+ :key="item.stars"
24
+ class="flex items-center gap-[12px]"
25
+ >
16
26
  <!-- badge แสดงบน row แรก (5 ดาว) -->
17
27
  <div class="relative flex-shrink-0">
18
28
  <InputRating :size="16" readonly :model-value="item.stars" />
19
- <div v-if="idx === 0 && props.badge != null"
20
- class="absolute -top-[10px] -left-[10px] min-w-[22px] h-[22px] px-[4px] rounded-full bg-error flex items-center justify-center">
21
- <span class="text-white text-[11px] font-bold leading-none">{{ props.badge }}</span>
29
+ <div
30
+ v-if="idx === 0 && props.badge != null"
31
+ class="absolute -top-[10px] -left-[10px] min-w-[22px] h-[22px] px-[4px] rounded-full bg-error flex items-center justify-center"
32
+ >
33
+ <span class="text-white text-[11px] font-bold leading-none">{{
34
+ props.badge
35
+ }}</span>
22
36
  </div>
23
37
  </div>
24
38
 
25
39
  <div class="flex-1 max-w-[260px]">
26
- <ProgressBar :value="maxCount > 0 ? item.count / maxCount * 100 : 0" full-width :height="8"
27
- color-active="#FBC02D" color-background="#E0E0E0" />
40
+ <ProgressBar
41
+ :value="maxCount > 0 ? item.count / maxCount * 100 : 0"
42
+ full-width
43
+ :height="8"
44
+ color-active="#FBC02D"
45
+ color-background="#E0E0E0"
46
+ />
28
47
  </div>
29
48
 
30
49
  <div class="text-gray font-body-large w-[72px]">
@@ -39,6 +58,7 @@
39
58
  import { computed } from "vue";
40
59
  import { useConvert } from "../../composables/useConvert";
41
60
  const { convertNumber } = useConvert();
61
+ const emit = defineEmits(["write-review"]);
42
62
  const props = defineProps({
43
63
  rating: { type: [Number, String], required: false, default: 0 },
44
64
  totalReviews: { type: [Number, String], required: false, default: 0 },
@@ -8,7 +8,11 @@ export interface DisplayRatingSummaryProps {
8
8
  distribution?: RatingDistribution[];
9
9
  badge?: number;
10
10
  }
11
- declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<DisplayRatingSummaryProps> & Readonly<{}>, {
11
+ declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "write-review": () => any;
13
+ }, string, import("vue").PublicProps, Readonly<DisplayRatingSummaryProps> & Readonly<{
14
+ "onWrite-review"?: (() => any) | undefined;
15
+ }>, {
12
16
  rating: number | string;
13
17
  totalReviews: number | string;
14
18
  distribution: RatingDistribution[];
@@ -12,20 +12,18 @@ export interface DrawerPostReviewItem {
12
12
  description: string;
13
13
  photos: IFileItem[];
14
14
  }
15
- export interface DrawerPostReviewProps {
15
+ type __VLS_Props = {
16
16
  item?: DrawerPostReviewItem;
17
- }
18
- type __VLS_Props = DrawerPostReviewProps;
17
+ };
19
18
  type __VLS_ModelProps = {
20
19
  modelValue?: boolean;
21
20
  };
22
21
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
23
22
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
23
+ success: (data: unknown) => any;
24
24
  "update:modelValue": (value: boolean) => any;
25
- } & {
26
- success: (data: any) => any;
27
25
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
28
- onSuccess?: ((data: any) => any) | undefined;
26
+ onSuccess?: ((data: unknown) => any) | undefined;
29
27
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
30
28
  }>, {
31
29
  item: DrawerPostReviewItem;
@@ -1,87 +1,82 @@
1
1
  <template>
2
- <Drawer
3
- class="w-[748px]"
4
- :title="drawerTitle"
5
- @close="onClose"
6
- disabled-auto-close
7
- @submit="onSubmit"
8
- v-model="isOpen"
9
- >
10
- <div class="flex flex-col gap-[16px]">
11
- <div class="flex gap-[16px]">
12
- <div
13
- class="w-[178px] h-[100px] rounded-[8px] overflow-hidden flex-shrink-0"
14
- >
15
- <Image
16
- v-if="form.coverImage"
17
- :src="form.coverImage"
18
- width="auto"
19
- height="auto"
20
- fit="cover"
21
- />
22
- </div>
23
- <div class="flex flex-col gap-[4px]">
24
- <div class="font-body-large-prominent">
25
- {{ form.placeName }}
26
- </div>
27
- <div class="font-body-small text-gray">
28
- {{ form.address }}
29
- </div>
30
- </div>
31
- </div>
32
- <div class="flex items-center justify-center py-[16px]">
33
- <InputRating v-model="form.rating" :size="28" />
34
- </div>
35
- <InputTextarea
36
- name="description"
37
- label="คำอธิบาย"
38
- placeholder="เขียนรีวิวของคุณ"
39
- showCounter
40
- v-model="form.description"
41
- class="min-h-[120px]"
42
- />
43
- <div class="flex flex-col gap-[8px]">
44
- <div class="flex flex-col gap-[4px]">
45
- <div class="text-gray font-body-large">เพิ่มภาพถ่าย</div>
46
- <div class="text-gray font-body-small">อัปโหลด 9 รายการ</div>
47
- </div>
48
- <InputFile
49
- :limit="9"
50
- name="photos"
51
- accept="image/jpeg,image/png,image/webp,image/bmp,image/gif"
52
- v-model="form.photos"
53
- />
54
- <div class="flex flex-col text-gray font-body-small">
55
- <div>รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif</div>
56
- <div>ขนาดไฟล์สูงสุด 30 mb</div>
57
- </div>
58
- </div>
59
- </div>
60
- <template #footer="{ meta }">
61
- <div class="flex justify-end gap-[16px] items-center">
62
- <Button variant="outline" @click="onClose" :disabled="isSubmitting"
63
- >ยกเลิก</Button
64
- >
65
- <Button
66
- type="submit"
67
- color="primary"
68
- :disabled="form.rating === 0 || isSubmitting"
69
- :loading="isSubmitting"
70
- >
71
- ยืนยัน
72
- </Button>
73
- </div>
74
- </template>
75
- </Drawer>
2
+ <Drawer
3
+ class="w-[748px]"
4
+ :title="drawerTitle"
5
+ @close="onClose"
6
+ disabled-auto-close
7
+ @submit="onSubmit"
8
+ v-model="isOpen"
9
+ >
10
+ <div class="flex flex-col gap-[16px]">
11
+ <div class="flex gap-[16px]">
12
+ <div class="w-[178px] h-[100px] rounded-[8px] overflow-hidden flex-shrink-0">
13
+ <Image
14
+ v-if="form.coverImage"
15
+ :src="form.coverImage"
16
+ width="auto"
17
+ height="auto"
18
+ fit="cover"
19
+ />
20
+ </div>
21
+ <div class="flex flex-col gap-[4px]">
22
+ <div class="font-body-large-prominent">{{ form.placeName }}</div>
23
+ <div class="font-body-small text-gray">{{ form.address }}</div>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="flex items-center justify-center py-[16px]">
28
+ <InputRating v-model="form.rating" :size="28" />
29
+ </div>
30
+
31
+ <InputTextarea
32
+ name="description"
33
+ label="คำอธิบาย"
34
+ placeholder="เขียนรีวิวของคุณ"
35
+ showCounter
36
+ v-model="form.description"
37
+ class="min-h-[120px]"
38
+ />
39
+
40
+ <div class="flex flex-col gap-[8px]">
41
+ <div class="flex flex-col gap-[4px]">
42
+ <div class="text-gray font-body-large">เพิ่มภาพถ่าย</div>
43
+ <div class="text-gray font-body-small">อัปโหลด 9 รายการ</div>
44
+ </div>
45
+ <InputFile
46
+ :limit="9"
47
+ name="photos"
48
+ accept="image/jpeg,image/png,image/webp,image/bmp,image/gif"
49
+ v-model="form.photos"
50
+ />
51
+ <div class="flex flex-col text-gray font-body-small">
52
+ <div>รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif</div>
53
+ <div>ขนาดไฟล์สูงสุด 30 mb</div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <template #footer>
59
+ <div class="flex justify-end gap-[16px] items-center">
60
+ <Button variant="outline" @click="onClose" :disabled="isSubmitting">ยกเลิก</Button>
61
+ <Button
62
+ type="submit"
63
+ color="primary"
64
+ :disabled="form.rating === 0 || isSubmitting"
65
+ :loading="isSubmitting"
66
+ >
67
+ ยืนยัน
68
+ </Button>
69
+ </div>
70
+ </template>
71
+ </Drawer>
76
72
  </template>
77
73
 
78
74
  <script setup>
79
- import { useApi } from "../../composables/useApi";
80
75
  import { ref, watch, computed } from "vue";
81
76
  import { useNuxtApp } from "nuxt/app";
77
+ import { useApi } from "../../composables/useApi";
82
78
  const { $alert } = useNuxtApp();
83
79
  const api = useApi();
84
- const emit = defineEmits(["success"]);
85
80
  const props = defineProps({
86
81
  item: { type: Object, required: false, default: () => ({
87
82
  placeId: "",
@@ -93,6 +88,7 @@ const props = defineProps({
93
88
  photos: []
94
89
  }) }
95
90
  });
91
+ const emit = defineEmits(["success"]);
96
92
  const isOpen = defineModel({ type: Boolean, ...{ default: false } });
97
93
  const isSubmitting = ref(false);
98
94
  const form = ref({
@@ -104,10 +100,6 @@ const form = ref({
104
100
  description: props.item.description,
105
101
  photos: props.item.photos ? [...props.item.photos] : []
106
102
  });
107
- const isEdit = computed(() => !!(props.item?.rating && props.item.rating > 0));
108
- const drawerTitle = computed(
109
- () => isEdit.value ? "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E23\u0E35\u0E27\u0E34\u0E27" : "\u0E40\u0E02\u0E35\u0E22\u0E19\u0E23\u0E35\u0E27\u0E34\u0E27"
110
- );
111
103
  watch(isOpen, (newVal) => {
112
104
  if (newVal) {
113
105
  form.value = {
@@ -121,6 +113,8 @@ watch(isOpen, (newVal) => {
121
113
  };
122
114
  }
123
115
  });
116
+ const isEdit = computed(() => (props.item?.rating ?? 0) > 0);
117
+ const drawerTitle = computed(() => isEdit.value ? "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E23\u0E35\u0E27\u0E34\u0E27" : "\u0E40\u0E02\u0E35\u0E22\u0E19\u0E23\u0E35\u0E27\u0E34\u0E27");
124
118
  const hasChanges = () => {
125
119
  const orig = props.item;
126
120
  if (form.value.rating !== orig.rating) return true;
@@ -128,29 +122,17 @@ const hasChanges = () => {
128
122
  if (form.value.photos.length !== (orig.photos?.length ?? 0)) return true;
129
123
  return false;
130
124
  };
131
- const onClose = async () => {
132
- if (hasChanges()) {
133
- const { isConfirmed } = await $alert.show({
134
- type: "warning",
135
- title: "\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
136
- description: "\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E23\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01 \u0E04\u0E38\u0E13\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
137
- confirmText: "\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07",
138
- cancelText: "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E15\u0E48\u0E2D",
139
- showCancelBtn: true
140
- });
141
- if (isConfirmed) isOpen.value = false;
142
- } else {
143
- isOpen.value = false;
144
- }
145
- };
146
125
  const uploadNewPhotos = async (items) => {
147
126
  const newItems = items.filter((it) => it.file);
148
127
  if (newItems.length === 0) return [];
149
128
  const fileNames = newItems.map((it) => it.file.name);
150
- const presignedRes = await api("/storage/presigned-urls", {
151
- method: "post",
152
- body: { state: "suggest-place-review", file_name: fileNames }
153
- });
129
+ const presignedRes = await api(
130
+ "/storage/presigned-urls",
131
+ {
132
+ method: "post",
133
+ body: { state: "suggest-place-review", file_name: fileNames }
134
+ }
135
+ );
154
136
  const uploadItems = presignedRes?.data?.items ?? [];
155
137
  if (uploadItems.length !== newItems.length) {
156
138
  throw new Error("\u0E44\u0E21\u0E48\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E2A\u0E23\u0E49\u0E32\u0E07 upload URL \u0E44\u0E14\u0E49 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07");
@@ -168,6 +150,21 @@ const uploadNewPhotos = async (items) => {
168
150
  );
169
151
  return uploadItems.map((u) => u.public_url);
170
152
  };
153
+ const onClose = async () => {
154
+ if (hasChanges()) {
155
+ const { isConfirmed } = await $alert.show({
156
+ type: "warning",
157
+ title: "\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
158
+ description: "\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E23\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01 \u0E04\u0E38\u0E13\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
159
+ confirmText: "\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07",
160
+ cancelText: "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E15\u0E48\u0E2D",
161
+ showCancelBtn: true
162
+ });
163
+ if (isConfirmed) isOpen.value = false;
164
+ } else {
165
+ isOpen.value = false;
166
+ }
167
+ };
171
168
  const onSubmit = async () => {
172
169
  if (isSubmitting.value) return;
173
170
  isSubmitting.value = true;
@@ -189,7 +186,8 @@ const onSubmit = async () => {
189
186
  emit("success", response?.data);
190
187
  isOpen.value = false;
191
188
  } catch (err) {
192
- const msg = err?.data?.message || err?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07";
189
+ const apiErr = err;
190
+ const msg = apiErr?.data?.message || apiErr?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07";
193
191
  await $alert.show({
194
192
  type: "error",
195
193
  title: "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14",
@@ -12,20 +12,18 @@ export interface DrawerPostReviewItem {
12
12
  description: string;
13
13
  photos: IFileItem[];
14
14
  }
15
- export interface DrawerPostReviewProps {
15
+ type __VLS_Props = {
16
16
  item?: DrawerPostReviewItem;
17
- }
18
- type __VLS_Props = DrawerPostReviewProps;
17
+ };
19
18
  type __VLS_ModelProps = {
20
19
  modelValue?: boolean;
21
20
  };
22
21
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
23
22
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
23
+ success: (data: unknown) => any;
24
24
  "update:modelValue": (value: boolean) => any;
25
- } & {
26
- success: (data: any) => any;
27
25
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
28
- onSuccess?: ((data: any) => any) | undefined;
26
+ onSuccess?: ((data: unknown) => any) | undefined;
29
27
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
30
28
  }>, {
31
29
  item: DrawerPostReviewItem;
@@ -201,8 +201,9 @@ const onSubmit = async () => {
201
201
  emit("submit", formData.value);
202
202
  isOpen.value = false;
203
203
  } catch (e) {
204
+ const apiErr = e;
204
205
  $toast?.error?.(
205
- e?.data?.error || e?.data?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E0A\u0E37\u0E48\u0E2D\u0E21\u0E15\u0E48\u0E2D"
206
+ apiErr?.data?.error || apiErr?.data?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E0A\u0E37\u0E48\u0E2D\u0E21\u0E15\u0E48\u0E2D"
206
207
  );
207
208
  } finally {
208
209
  loading.value = false;
@@ -1,4 +1,12 @@
1
1
  import type { InputAddressValue } from "#pukaad-ui/runtime/components/input/input-address.vue";
2
+ interface FileItem {
3
+ file?: File;
4
+ url: string;
5
+ }
6
+ interface ContactChannel {
7
+ type: string;
8
+ value: string;
9
+ }
2
10
  export interface SuggestPlaceData {
3
11
  address?: InputAddressValue;
4
12
  latLng?: {
@@ -8,18 +16,22 @@ export interface SuggestPlaceData {
8
16
  businessName?: string;
9
17
  nameTh?: string;
10
18
  nameEn?: string;
11
- categories?: any[];
19
+ categories?: {
20
+ id?: string;
21
+ value?: string;
22
+ label?: string;
23
+ }[];
12
24
  description?: string;
13
- openingHours?: any;
25
+ openingHours?: Record<string, unknown>;
14
26
  phone?: string;
15
27
  extraPhones?: string[];
16
28
  isReview?: boolean;
17
29
  rating?: number;
18
30
  reviewDescription?: string;
19
- reviewPhotos?: any[];
20
- photos?: any[];
21
- videos?: any[];
22
- contactChannels?: any[];
31
+ reviewPhotos?: FileItem[];
32
+ photos?: FileItem[];
33
+ videos?: FileItem[];
34
+ contactChannels?: ContactChannel[];
23
35
  }
24
36
  type __VLS_Props = {
25
37
  state?: "personal" | "business" | "backoffice";
@@ -1,76 +1,130 @@
1
1
  <template>
2
- <div class="flex gap-[16px] w-full">
3
- <!-- กรอกข้อมูล -->
4
- <div class="flex flex-col gap-[16px] w-[490px]">
5
- <InputAddress name="address" v-model="modelValue.address" :fixed-province-id="props.fixedProvinceId" />
6
- <template v-if="isAddressCompleted">
7
- <div class="font-body-large-prominent">รายละเอียด</div>
8
- <div class="flex flex-col gap-[4px]">
9
- <InputAutocomplete name="businessName" v-model="modelValue.businessName" label="ชื่อสถานที่ธุรกิจ"
10
- placeholder="กรอกชื่อสถานที่" required show-counter :limit="180" :free-text="true" :fetch-fn="fetchApprovedPlaces"
11
- value-key="business_name" label-key="business_name" />
12
- <InputTextField v-if="extraNameCount >= 1" name="nameTh" v-model="modelValue.nameTh" label="ชื่อภาษาไทย"
13
- placeholder="ใส่ในกรณีที่ต่างจากชื่อหลัก" show-counter :limit="180" />
14
- <InputTextField v-if="extraNameCount >= 2" name="nameEn" v-model="modelValue.nameEn" label="ชื่อภาษาอังกฤษ"
15
- placeholder="ใส่ในกรณีที่ต่างจากชื่อหลัก" show-counter :limit="180" />
16
- <Button v-if="extraNameCount < 2" variant="text" color="primary" class="w-[145px]" @click="extraNameCount++">
17
- <Icon name="lucide:plus" />
18
- เพิ่มชื่อสถานที่
19
- </Button>
20
- </div>
21
- <InputCombobox name="categories" v-model="modelValue.categories" label="หมวดหมู่"
22
- placeholder="เพิ่มหมวดหมู่ที่เกี่ยวข้องกับสถานที่" :limit="3" show-counter :options="categoryOptions" required
23
- multiple />
24
- <InputTextarea name="description" v-model="modelValue.description" label="คำอธิบาย"
25
- placeholder="คำอธิบายเกี่ยวกับสถานที่ (สูงสุด 220 ตัวอักษร)" :limit="220" show-counter />
26
- <InputDateOpening name="openingHours" v-model="modelValue.openingHours" />
27
- <div class="flex flex-col gap-[8px]">
28
- <InputTextField name="phone" v-model="modelValue.phone" label="เบอร์โทรศัพท์" placeholder="กรอกเบอร์โทรศัพท์" />
29
- <InputTextField v-for="(item, index) in modelValue.extraPhones" :key="index" :name="`extraPhone-${index}`"
30
- label="เบอร์โทรศัพท์" placeholder="กรอกเบอร์โทรศัพท์" v-model="modelValue.extraPhones[index]" />
31
- <Button variant="text" color="primary" class="w-[145px]" @click="modelValue.extraPhones.push('')">
32
- <Icon name="lucide:plus" />
33
- เพิ่มเบอร์โทรศัพท์
34
- </Button>
35
- <InputLink name="contactChannels" format="contact_channel" default-first
36
- v-model="modelValue.contactChannels" />
37
- </div>
38
- <template v-if="props.state === 'personal'">
39
- <div class="flex flex-col gap-[16px]">
40
- <InputCheckbox name="isReview" v-model="modelValue.isReview" label="คุณต้องการรีวิวสถานที่นี้" />
41
- <template v-if="modelValue.isReview">
42
- <InputRating name="rating" v-model="modelValue.rating" class="flex py-4 justify-center" />
43
- <InputTextarea name="reviewDescription" v-model="modelValue.reviewDescription" label="คำอธิบาย"
44
- placeholder="คำอธิบายเกี่ยวกับสถานที่" show-counter />
45
- <div class="flex flex-col gap-[8px]">
46
- <div class="flex flex-col gap-[4px]">
47
- <div class="font-body-large-prominent text-gray">
48
- เพิ่มภาพถ่าย
49
- </div>
50
- <div class="font-body-small text-gray">สูงสุด 9 รายการ</div>
51
- </div>
52
- <InputFile name="reviewPhotos" v-model="modelValue.reviewPhotos" accept="image/*" :limit="9" />
53
- <div class="font-body-small text-gray w-[250px]">
54
- รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif
55
- ขนาดไฟล์ไม่เกิน 30 mb
56
- </div>
57
- </div>
58
- </template>
59
- </div>
60
- </template>
61
- </template>
62
- </div>
63
-
64
- <!-- แสดง polygon -->
65
- <div class="flex flex-col gap-[16px] w-[334px] sticky top-0 self-start">
66
- <SuggestPlaceMap :province-id="modelValue.address?.province_id" :amphur-id="modelValue.address?.amphur_id"
67
- :tambon-id="modelValue.address?.tambon_id" @update:lat-lng="modelValue.latLng = $event" />
68
- <InputTextField name="latitude-longitude" icon-prepend="lucide:locate-fixed" readonly
69
- placeholder="ละติจูด, ลองจิจูด" :model-value="
2
+ <div class="flex gap-[16px] w-full">
3
+ <!-- กรอกข้อมูล -->
4
+ <div class="flex flex-col gap-[16px] w-[490px]">
5
+ <InputAddress name="address" v-model="modelValue.address" :fixed-province-id="props.fixedProvinceId" />
6
+ <template v-if="isAddressCompleted">
7
+ <div class="font-body-large-prominent">รายละเอียด</div>
8
+ <div class="flex flex-col gap-[4px]">
9
+ <InputAutocomplete
10
+ name="businessName"
11
+ v-model="modelValue.businessName"
12
+ label="ชื่อสถานที่ธุรกิจ"
13
+ placeholder="กรอกชื่อสถานที่"
14
+ required
15
+ show-counter
16
+ :limit="180"
17
+ :free-text="true"
18
+ :fetch-fn="fetchApprovedPlaces"
19
+ value-key="business_name"
20
+ label-key="business_name"
21
+ />
22
+ <InputTextField
23
+ v-if="extraNameCount >= 1"
24
+ name="nameTh"
25
+ v-model="modelValue.nameTh"
26
+ label="ชื่อภาษาไทย"
27
+ placeholder="ใส่ในกรณีที่ต่างจากชื่อหลัก"
28
+ show-counter
29
+ :limit="180"
30
+ />
31
+ <InputTextField
32
+ v-if="extraNameCount >= 2"
33
+ name="nameEn"
34
+ v-model="modelValue.nameEn"
35
+ label="ชื่อภาษาอังกฤษ"
36
+ placeholder="ใส่ในกรณีที่ต่างจากชื่อหลัก"
37
+ show-counter
38
+ :limit="180"
39
+ />
40
+ <Button v-if="extraNameCount < 2" variant="text" color="primary" class="w-[145px]" @click="extraNameCount++">
41
+ <Icon name="lucide:plus" />
42
+ เพิ่มชื่อสถานที่
43
+ </Button>
44
+ </div>
45
+ <InputCombobox
46
+ name="categories"
47
+ v-model="modelValue.categories"
48
+ label="หมวดหมู่"
49
+ placeholder="เพิ่มหมวดหมู่ที่เกี่ยวข้องกับสถานที่"
50
+ :limit="3"
51
+ show-counter
52
+ :options="categoryOptions"
53
+ required
54
+ multiple
55
+ />
56
+ <InputTextarea
57
+ name="description"
58
+ v-model="modelValue.description"
59
+ label="คำอธิบาย"
60
+ placeholder="คำอธิบายเกี่ยวกับสถานที่ (สูงสุด 220 ตัวอักษร)"
61
+ :limit="220"
62
+ show-counter
63
+ />
64
+ <InputDateOpening name="openingHours" v-model="modelValue.openingHours" />
65
+ <div class="flex flex-col gap-[8px]">
66
+ <InputTextField name="phone" v-model="modelValue.phone" label="เบอร์โทรศัพท์" placeholder="กรอกเบอร์โทรศัพท์" />
67
+ <InputTextField
68
+ v-for="(_, index) in modelValue.extraPhones"
69
+ :key="index"
70
+ :name="`extraPhone-${index}`"
71
+ label="เบอร์โทรศัพท์"
72
+ placeholder="กรอกเบอร์โทรศัพท์"
73
+ v-model="modelValue.extraPhones[index]"
74
+ />
75
+ <Button variant="text" color="primary" class="w-[145px]" @click="modelValue.extraPhones.push('')">
76
+ <Icon name="lucide:plus" />
77
+ เพิ่มเบอร์โทรศัพท์
78
+ </Button>
79
+ <InputLink name="contactChannels" format="contact_channel" default-first v-model="modelValue.contactChannels" />
80
+ </div>
81
+ <template v-if="props.state === 'personal'">
82
+ <div class="flex flex-col gap-[16px]">
83
+ <InputCheckbox name="isReview" v-model="modelValue.isReview" label="คุณต้องการรีวิวสถานที่นี้" />
84
+ <template v-if="modelValue.isReview">
85
+ <InputRating name="rating" v-model="modelValue.rating" class="flex py-4 justify-center" />
86
+ <InputTextarea
87
+ name="reviewDescription"
88
+ v-model="modelValue.reviewDescription"
89
+ label="คำอธิบาย"
90
+ placeholder="คำอธิบายเกี่ยวกับสถานที่"
91
+ show-counter
92
+ />
93
+ <div class="flex flex-col gap-[8px]">
94
+ <div class="flex flex-col gap-[4px]">
95
+ <div class="font-body-large-prominent text-gray">เพิ่มภาพถ่าย</div>
96
+ <div class="font-body-small text-gray">สูงสุด 9 รายการ</div>
97
+ </div>
98
+ <InputFile name="reviewPhotos" v-model="modelValue.reviewPhotos" accept="image/*" :limit="9" />
99
+ <div class="font-body-small text-gray w-[250px]">
100
+ รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif ขนาดไฟล์ไม่เกิน 30 mb
101
+ </div>
102
+ </div>
103
+ </template>
104
+ </div>
105
+ </template>
106
+ </template>
107
+ </div>
108
+
109
+ <!-- แสดง polygon -->
110
+ <div class="flex flex-col gap-[16px] w-[334px] sticky top-0 self-start">
111
+ <SuggestPlaceMap
112
+ :province-id="modelValue.address?.province_id"
113
+ :amphur-id="modelValue.address?.amphur_id"
114
+ :tambon-id="modelValue.address?.tambon_id"
115
+ @update:lat-lng="modelValue.latLng = $event"
116
+ />
117
+ <InputTextField
118
+ name="latitude-longitude"
119
+ icon-prepend="lucide:locate-fixed"
120
+ readonly
121
+ placeholder="ละติจูด, ลองจิจูด"
122
+ :model-value="
70
123
  modelValue.latLng ? `${modelValue.latLng.lat.toFixed(6)}, ${modelValue.latLng.lng.toFixed(6)}` : ''
71
- " />
72
- </div>
73
- </div>
124
+ "
125
+ />
126
+ </div>
127
+ </div>
74
128
  </template>
75
129
 
76
130
  <script setup>
@@ -98,9 +152,7 @@ watch(_model, (val) => {
98
152
  watch(modelValue, (val) => {
99
153
  _model.value = { ...val };
100
154
  }, { deep: true });
101
- const extraNameCount = ref(
102
- modelValue.nameEn ? 2 : modelValue.nameTh ? 1 : 0
103
- );
155
+ const extraNameCount = ref(modelValue.nameEn ? 2 : modelValue.nameTh ? 1 : 0);
104
156
  const isAddressCompleted = computed(() => {
105
157
  const v = modelValue.address || {};
106
158
  return !!(v.province_id && v.amphur_id && v.tambon_id && v.zipcode);
@@ -120,15 +172,15 @@ const listEndpointMap = {
120
172
  business: "/business/suggest-places",
121
173
  backoffice: "/office/suggest-places"
122
174
  };
175
+ const categoryEndpointMap = {
176
+ personal: "/personal/suggest-places/categories",
177
+ business: "/business/suggest-places/categories",
178
+ backoffice: "/office/suggest-places/categories"
179
+ };
123
180
  const fetchApprovedPlaces = async (page, pageSize, search) => {
124
181
  const base = listEndpointMap[props.state] ?? "/personal/suggest-places";
125
182
  const res = await api(base, {
126
- query: {
127
- q: search ?? "",
128
- status: "PENDING,APPROVED",
129
- page,
130
- page_size: pageSize
131
- }
183
+ query: { q: search ?? "", status: "PENDING,APPROVED", page, page_size: pageSize }
132
184
  });
133
185
  const data = (res.data ?? []).map((item) => ({
134
186
  ...item,
@@ -140,17 +192,10 @@ const fetchApprovedPlaces = async (page, pageSize, search) => {
140
192
  };
141
193
  };
142
194
  const categoryOptions = ref([]);
143
- const categoryEndpointMap = {
144
- personal: "/personal/suggest-places/categories",
145
- business: "/business/suggest-places/categories",
146
- backoffice: "/office/suggest-places/categories"
147
- };
148
195
  onMounted(async () => {
149
196
  try {
150
197
  const endpoint = categoryEndpointMap[props.state] ?? "/personal/suggest-places/categories";
151
- const res = await api(
152
- endpoint
153
- );
198
+ const res = await api(endpoint);
154
199
  categoryOptions.value = (res.data ?? []).map((c) => ({
155
200
  label: c.category_name,
156
201
  value: c.id
@@ -1,4 +1,12 @@
1
1
  import type { InputAddressValue } from "#pukaad-ui/runtime/components/input/input-address.vue";
2
+ interface FileItem {
3
+ file?: File;
4
+ url: string;
5
+ }
6
+ interface ContactChannel {
7
+ type: string;
8
+ value: string;
9
+ }
2
10
  export interface SuggestPlaceData {
3
11
  address?: InputAddressValue;
4
12
  latLng?: {
@@ -8,18 +16,22 @@ export interface SuggestPlaceData {
8
16
  businessName?: string;
9
17
  nameTh?: string;
10
18
  nameEn?: string;
11
- categories?: any[];
19
+ categories?: {
20
+ id?: string;
21
+ value?: string;
22
+ label?: string;
23
+ }[];
12
24
  description?: string;
13
- openingHours?: any;
25
+ openingHours?: Record<string, unknown>;
14
26
  phone?: string;
15
27
  extraPhones?: string[];
16
28
  isReview?: boolean;
17
29
  rating?: number;
18
30
  reviewDescription?: string;
19
- reviewPhotos?: any[];
20
- photos?: any[];
21
- videos?: any[];
22
- contactChannels?: any[];
31
+ reviewPhotos?: FileItem[];
32
+ photos?: FileItem[];
33
+ videos?: FileItem[];
34
+ contactChannels?: ContactChannel[];
23
35
  }
24
36
  type __VLS_Props = {
25
37
  state?: "personal" | "business" | "backoffice";
@@ -64,12 +64,12 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
64
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
65
65
  src: string;
66
66
  center: boolean;
67
+ modal: boolean;
67
68
  responsive: boolean;
68
69
  restore: boolean;
69
70
  checkCrossOrigin: boolean;
70
71
  checkOrientation: boolean;
71
72
  crossorigin: "" | "anonymous" | "use-credentials";
72
- modal: boolean;
73
73
  guides: boolean;
74
74
  highlight: boolean;
75
75
  background: boolean;
@@ -64,12 +64,12 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
64
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
65
65
  src: string;
66
66
  center: boolean;
67
+ modal: boolean;
67
68
  responsive: boolean;
68
69
  restore: boolean;
69
70
  checkCrossOrigin: boolean;
70
71
  checkOrientation: boolean;
71
72
  crossorigin: "" | "anonymous" | "use-credentials";
72
- modal: boolean;
73
73
  guides: boolean;
74
74
  highlight: boolean;
75
75
  background: boolean;
@@ -49,11 +49,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
49
49
  label: string;
50
50
  required: boolean;
51
51
  name: string;
52
+ gap: string;
52
53
  placeholder: string;
53
54
  labelDetail: string;
54
55
  placeholderDetail: string;
55
56
  requiredDetail: boolean;
56
- gap: string;
57
57
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
58
  declare const _default: typeof __VLS_export;
59
59
  export default _default;
@@ -49,11 +49,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
49
49
  label: string;
50
50
  required: boolean;
51
51
  name: string;
52
+ gap: string;
52
53
  placeholder: string;
53
54
  labelDetail: string;
54
55
  placeholderDetail: string;
55
56
  requiredDetail: boolean;
56
- gap: string;
57
57
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
58
  declare const _default: typeof __VLS_export;
59
59
  export default _default;
@@ -35,8 +35,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
35
35
  fullHeight: boolean;
36
36
  name: string;
37
37
  limit: number;
38
- disabledErrorMessage: boolean;
39
38
  accept: string;
39
+ disabledErrorMessage: boolean;
40
40
  labelIcon: string;
41
41
  disabledDrop: boolean;
42
42
  column: boolean;
@@ -35,8 +35,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
35
35
  fullHeight: boolean;
36
36
  name: string;
37
37
  limit: number;
38
- disabledErrorMessage: boolean;
39
38
  accept: string;
39
+ disabledErrorMessage: boolean;
40
40
  labelIcon: string;
41
41
  disabledDrop: boolean;
42
42
  column: boolean;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
22
22
  }>, {
23
23
  id: string;
24
24
  name: string;
25
- new: boolean;
26
25
  disabledForgotPassword: boolean;
26
+ new: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
22
22
  }>, {
23
23
  id: string;
24
24
  name: string;
25
- new: boolean;
26
25
  disabledForgotPassword: boolean;
26
+ new: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -24,8 +24,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
24
24
  }) => any) | undefined;
25
25
  }>, {
26
26
  title: string;
27
- disabledForgotPassword: boolean;
28
27
  confirmText: string;
28
+ disabledForgotPassword: boolean;
29
29
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
30
  declare const _default: typeof __VLS_export;
31
31
  export default _default;
@@ -24,8 +24,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
24
24
  }) => any) | undefined;
25
25
  }>, {
26
26
  title: string;
27
- disabledForgotPassword: boolean;
28
27
  confirmText: string;
28
+ disabledForgotPassword: boolean;
29
29
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
30
  declare const _default: typeof __VLS_export;
31
31
  export default _default;
@@ -32,8 +32,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
32
32
  }>, {
33
33
  title: string;
34
34
  mode: "login" | "secure";
35
- disabledForgotPassword: boolean;
36
35
  confirmText: string;
36
+ disabledForgotPassword: boolean;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
39
39
  export default _default;
@@ -32,8 +32,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
32
32
  }>, {
33
33
  title: string;
34
34
  mode: "login" | "secure";
35
- disabledForgotPassword: boolean;
36
35
  confirmText: string;
36
+ disabledForgotPassword: boolean;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
39
39
  export default _default;
@@ -14,8 +14,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
14
14
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
15
15
  onComplete?: ((data?: any) => any) | undefined;
16
16
  }>, {
17
- phone: string;
18
17
  confirmedText: string;
18
+ phone: string;
19
19
  phoneLabel: string;
20
20
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
21
  declare const _default: typeof __VLS_export;
@@ -14,8 +14,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
14
14
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
15
15
  onComplete?: ((data?: any) => any) | undefined;
16
16
  }>, {
17
- phone: string;
18
17
  confirmedText: string;
18
+ phone: string;
19
19
  phoneLabel: string;
20
20
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
21
  declare const _default: typeof __VLS_export;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.296.0",
3
+ "version": "1.297.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",