pukaad-ui-lib 1.300.0 → 1.302.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 (23) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/card/card-review.d.vue.ts +15 -0
  3. package/dist/runtime/components/card/card-review.vue +155 -102
  4. package/dist/runtime/components/card/card-review.vue.d.ts +15 -0
  5. package/dist/runtime/components/display/display-rating-summary.d.vue.ts +2 -0
  6. package/dist/runtime/components/display/display-rating-summary.vue +3 -2
  7. package/dist/runtime/components/display/display-rating-summary.vue.d.ts +2 -0
  8. package/dist/runtime/components/drawer/drawer-post-review.vue +77 -71
  9. package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.vue +124 -124
  10. package/dist/runtime/components/image/image-cropper.d.vue.ts +2 -2
  11. package/dist/runtime/components/image/image-cropper.vue.d.ts +2 -2
  12. package/dist/runtime/components/input/input-autocomplete.d.vue.ts +1 -1
  13. package/dist/runtime/components/input/input-autocomplete.vue.d.ts +1 -1
  14. package/dist/runtime/components/input/input-file.d.vue.ts +1 -1
  15. package/dist/runtime/components/input/input-file.vue.d.ts +1 -1
  16. package/dist/runtime/components/input/input-tag.d.vue.ts +1 -1
  17. package/dist/runtime/components/input/input-tag.vue.d.ts +1 -1
  18. package/dist/runtime/components/input/input-text-field.d.vue.ts +1 -1
  19. package/dist/runtime/components/input/input-text-field.vue.d.ts +1 -1
  20. package/dist/runtime/components/input/input-textarea.d.vue.ts +1 -1
  21. package/dist/runtime/components/input/input-textarea.vue.d.ts +1 -1
  22. package/package.json +1 -1
  23. /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.300.0",
4
+ "version": "1.302.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -4,22 +4,37 @@ type __VLS_Props = {
4
4
  isProfile: boolean;
5
5
  disabledLike?: boolean;
6
6
  disabledMenu?: boolean;
7
+ /** ถ้ามี place context → like/unlike และ edit จัดการใน component เลย */
8
+ place?: {
9
+ id: string | number;
10
+ name?: string;
11
+ address?: string;
12
+ cover?: string;
13
+ };
7
14
  };
8
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
16
  delete: (item: CardReviewProps) => any;
10
17
  edit: (item: CardReviewProps) => any;
11
18
  "toggle-like": (item: CardReviewProps, liked: boolean) => any;
12
19
  "select-image": (item: CardReviewProps, index: number) => any;
20
+ "review-updated": (data: unknown) => any;
13
21
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
22
  onDelete?: ((item: CardReviewProps) => any) | undefined;
15
23
  onEdit?: ((item: CardReviewProps) => any) | undefined;
16
24
  "onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
17
25
  "onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
26
+ "onReview-updated"?: ((data: unknown) => any) | undefined;
18
27
  }>, {
19
28
  item: CardReviewProps;
20
29
  disabledMenu: boolean;
21
30
  isProfile: boolean;
22
31
  disabledLike: boolean;
32
+ place: {
33
+ id: string | number;
34
+ name?: string;
35
+ address?: string;
36
+ cover?: string;
37
+ };
23
38
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
39
  declare const _default: typeof __VLS_export;
25
40
  export default _default;
@@ -1,106 +1,121 @@
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
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
- variant="text"
26
- :color="liked ? 'primary' : 'default'"
27
- :disabled="props.disabledLike"
28
- :aria-pressed="liked"
29
- @click="toggleLike"
30
- >
31
- <Icon
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
+ variant="text"
26
+ :color="liked ? 'primary' : 'default'"
27
+ :disabled="props.disabledLike || isLikeLoading"
28
+ :aria-pressed="liked"
29
+ @click="toggleLike"
30
+ >
31
+ <Icon
32
32
  :name="
33
33
  liked ? 'pukaad:thumbs-up-solid' : 'pukaad:thumbs-up-regular'
34
- "
35
- :size="20"
36
- />
37
- {{ convertNumber(likeCount) }}
38
- </Button>
39
- <PickerOptionMenuUser
40
- v-if="!props.disabledMenu"
41
- :state="menuType"
42
- disabled-padding
43
- @review-edit="emit('edit', props.item)"
44
- @review-delete="emit('delete', props.item)"
45
- />
46
- </div>
47
- </div>
48
- <div class="text-gray font-body-small">
49
- {{ convertNumber(props.item.user?.review_count ?? 0) }} รีวิว •
50
- {{ convertNumber(props.item.user?.like_count ?? 0) }} ชื่นชอบรีวิว
51
- </div>
52
- </div>
53
- <div class="flex gap-[8px] items-center">
54
- <InputRating
55
- :size="11"
56
- readonly
57
- :model-value="props.item.review?.rating"
58
- />
59
- <div class="text-gray font-body-small">
60
- {{ convertDateTime(props.item.review?.created_at ?? "") }}
61
- </div>
62
- </div>
63
- </div>
64
-
65
- <div v-if="props.item.review?.description" class="font-body-large">
66
- {{ props.item.review?.description }}
67
- </div>
68
-
69
- <div v-if="props.item.review?.images?.length" class="flex gap-[8px]">
70
- <DisplayImageReview
71
- :items="props.item.review?.images"
72
- @select="selectImage"
73
- />
74
- </div>
75
- </div>
76
-
77
- <div v-if="props.item.replies">
78
- <div class="p-[16px] rounded-sm bg-bright">
79
- <div class="text-gray font-body-large">
80
- การตอบกลับจาก {{ props.item.replies?.author }}
81
- </div>
82
- <div class="font-body-large">
83
- {{ props.item.replies?.description }}
84
- </div>
85
- </div>
86
- </div>
87
- </div>
88
- </div>
89
-
90
- <ModalMediaView
91
- v-model="isOpen"
92
- :items="props.item.review?.images"
93
- :start-index="startIndex"
94
- title="รูปภาพรีวิว"
95
- />
34
+ "
35
+ :size="20"
36
+ />
37
+ {{ convertNumber(likeCount) }}
38
+ </Button>
39
+ <PickerOptionMenuUser
40
+ v-if="!props.disabledMenu"
41
+ :state="menuType"
42
+ disabled-padding
43
+ @review-edit="onEdit"
44
+ @review-delete="emit('delete', props.item)"
45
+ />
46
+ </div>
47
+ </div>
48
+ <div class="text-gray font-body-small">
49
+ {{ convertNumber(props.item.user?.review_count ?? 0) }} รีวิว •
50
+ {{ convertNumber(props.item.user?.like_count ?? 0) }} ชื่นชอบรีวิว
51
+ </div>
52
+ </div>
53
+ <div class="flex gap-[8px] items-center">
54
+ <InputRating
55
+ :size="11"
56
+ readonly
57
+ :model-value="props.item.review?.rating"
58
+ />
59
+ <div class="text-gray font-body-small">
60
+ {{
61
+ convertDateTorelativeText(props.item.review?.created_at ?? "")
62
+ }}
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
73
+ :items="props.item.review?.images"
74
+ @select="selectImage"
75
+ />
76
+ </div>
77
+ </div>
78
+
79
+ <div v-if="props.item.replies">
80
+ <div class="p-[16px] rounded-sm bg-bright">
81
+ <div class="text-gray font-body-large">
82
+ การตอบกลับจาก {{ props.item.replies?.author }}
83
+ </div>
84
+ <div class="font-body-large">
85
+ {{ props.item.replies?.description }}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <ModalMediaView
93
+ v-model="isOpen"
94
+ :items="props.item.review?.images"
95
+ :start-index="startIndex"
96
+ title="รูปภาพรีวิว"
97
+ />
98
+
99
+ <!-- Drawer แก้ไขรีวิว (ใช้เมื่อมี place context) -->
100
+ <DrawerPostReview
101
+ v-if="props.place?.id"
102
+ v-model="isDrawerOpen"
103
+ :item="drawerItem"
104
+ @success="onReviewSuccess"
105
+ />
96
106
  </template>
97
107
 
98
108
  <script setup>
99
109
  import { computed, ref, watch } from "vue";
100
110
  import { useRouter } from "vue-router";
111
+ import { useNuxtApp } from "nuxt/app";
101
112
  import { useConvert } from "../../composables/useConvert";
113
+ import { useApi } from "../../composables/useApi";
102
114
  const router = useRouter();
103
- const { convertNumber, convertDateTime } = useConvert();
115
+ const { convertNumber, convertDateTorelativeText } = useConvert();
116
+ const api = useApi();
117
+ const nuxtApp = useNuxtApp();
118
+ const $toast = nuxtApp.$toast;
104
119
  const props = defineProps({
105
120
  item: { type: Object, required: true, default: () => ({
106
121
  review_id: "0",
@@ -129,13 +144,15 @@ const props = defineProps({
129
144
  }) },
130
145
  isProfile: { type: Boolean, required: true, default: false },
131
146
  disabledLike: { type: Boolean, required: false, default: false },
132
- disabledMenu: { type: Boolean, required: false, default: false }
147
+ disabledMenu: { type: Boolean, required: false, default: false },
148
+ place: { type: Object, required: false, default: void 0 }
133
149
  });
134
- const emit = defineEmits(["toggle-like", "select-image", "edit", "delete"]);
150
+ const emit = defineEmits(["toggle-like", "select-image", "edit", "delete", "review-updated"]);
135
151
  const isOpen = ref(false);
136
152
  const startIndex = ref(0);
137
153
  const liked = ref(props.item.review?.liked ?? false);
138
154
  const likeCount = ref(props.item.review?.like_count ?? 0);
155
+ const isLikeLoading = ref(false);
139
156
  watch(
140
157
  () => props.item.review?.liked,
141
158
  (val) => {
@@ -148,6 +165,16 @@ watch(
148
165
  likeCount.value = val ?? 0;
149
166
  }
150
167
  );
168
+ const isDrawerOpen = ref(false);
169
+ const drawerItem = computed(() => ({
170
+ placeId: String(props.place?.id ?? ""),
171
+ placeName: props.place?.name ?? "",
172
+ address: props.place?.address ?? "",
173
+ coverImage: props.place?.cover ?? "",
174
+ rating: props.item.review?.rating ?? 0,
175
+ description: props.item.review?.description ?? "",
176
+ photos: (props.item.review?.images ?? []).map((url) => ({ url }))
177
+ }));
151
178
  const menuType = computed(
152
179
  () => props.item.user?.is_my_review ? "my-review" : "report-review"
153
180
  );
@@ -155,15 +182,41 @@ const navigateToProfile = (pathName) => {
155
182
  if (!pathName) return;
156
183
  router.push(`/@${pathName}`);
157
184
  };
158
- const toggleLike = () => {
159
- if (liked.value) {
160
- likeCount.value -= 1;
161
- liked.value = false;
185
+ const toggleLike = async () => {
186
+ if (isLikeLoading.value) return;
187
+ const newLiked = !liked.value;
188
+ liked.value = newLiked;
189
+ likeCount.value += newLiked ? 1 : -1;
190
+ emit("toggle-like", props.item, newLiked);
191
+ if (!props.place?.id) return;
192
+ isLikeLoading.value = true;
193
+ try {
194
+ const method = newLiked ? "post" : "delete";
195
+ const res = await api(
196
+ `/personal/suggest-places/${props.place.id}/reviews/${props.item.review_id}/like`,
197
+ { method }
198
+ );
199
+ if (res?.data) {
200
+ liked.value = res.data.liked ?? newLiked;
201
+ likeCount.value = res.data.like_count ?? likeCount.value;
202
+ }
203
+ } catch {
204
+ liked.value = !newLiked;
205
+ likeCount.value += newLiked ? -1 : 1;
206
+ $toast.error("\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");
207
+ } finally {
208
+ isLikeLoading.value = false;
209
+ }
210
+ };
211
+ const onEdit = () => {
212
+ if (props.place?.id) {
213
+ isDrawerOpen.value = true;
162
214
  } else {
163
- likeCount.value += 1;
164
- liked.value = true;
215
+ emit("edit", props.item);
165
216
  }
166
- emit("toggle-like", props.item, liked.value);
217
+ };
218
+ const onReviewSuccess = (data) => {
219
+ emit("review-updated", data);
167
220
  };
168
221
  const selectImage = (index) => {
169
222
  startIndex.value = index;
@@ -4,22 +4,37 @@ type __VLS_Props = {
4
4
  isProfile: boolean;
5
5
  disabledLike?: boolean;
6
6
  disabledMenu?: boolean;
7
+ /** ถ้ามี place context → like/unlike และ edit จัดการใน component เลย */
8
+ place?: {
9
+ id: string | number;
10
+ name?: string;
11
+ address?: string;
12
+ cover?: string;
13
+ };
7
14
  };
8
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
16
  delete: (item: CardReviewProps) => any;
10
17
  edit: (item: CardReviewProps) => any;
11
18
  "toggle-like": (item: CardReviewProps, liked: boolean) => any;
12
19
  "select-image": (item: CardReviewProps, index: number) => any;
20
+ "review-updated": (data: unknown) => any;
13
21
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
22
  onDelete?: ((item: CardReviewProps) => any) | undefined;
15
23
  onEdit?: ((item: CardReviewProps) => any) | undefined;
16
24
  "onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
17
25
  "onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
26
+ "onReview-updated"?: ((data: unknown) => any) | undefined;
18
27
  }>, {
19
28
  item: CardReviewProps;
20
29
  disabledMenu: boolean;
21
30
  isProfile: boolean;
22
31
  disabledLike: boolean;
32
+ place: {
33
+ id: string | number;
34
+ name?: string;
35
+ address?: string;
36
+ cover?: string;
37
+ };
23
38
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
39
  declare const _default: typeof __VLS_export;
25
40
  export default _default;
@@ -7,6 +7,7 @@ export interface DisplayRatingSummaryProps {
7
7
  totalReviews?: number | string;
8
8
  distribution?: RatingDistribution[];
9
9
  badge?: number;
10
+ hasReviewed?: boolean;
10
11
  }
11
12
  declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
13
  "write-review": () => any;
@@ -16,6 +17,7 @@ declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryPr
16
17
  rating: number | string;
17
18
  totalReviews: number | string;
18
19
  distribution: RatingDistribution[];
20
+ hasReviewed: boolean;
19
21
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
22
  declare const _default: typeof __VLS_export;
21
23
  export default _default;
@@ -12,7 +12,7 @@
12
12
  {{ convertNumber(Number(props.totalReviews)) }} รีวิว
13
13
  </div>
14
14
  </div>
15
- <Button color="primary" @click="emit('write-review')">
15
+ <Button v-if="!props.hasReviewed" color="primary" @click="emit('write-review')">
16
16
  <Icon name="pukaad:write-review" /> เขียนรีวิว
17
17
  </Button>
18
18
  </div>
@@ -63,7 +63,8 @@ const props = defineProps({
63
63
  rating: { type: [Number, String], required: false, default: 0 },
64
64
  totalReviews: { type: [Number, String], required: false, default: 0 },
65
65
  distribution: { type: Array, required: false, default: () => [] },
66
- badge: { type: Number, required: false }
66
+ badge: { type: Number, required: false },
67
+ hasReviewed: { type: Boolean, required: false, default: false }
67
68
  });
68
69
  const sortedDistribution = computed(
69
70
  () => [...props.distribution].sort((a, b) => b.stars - a.stars)
@@ -7,6 +7,7 @@ export interface DisplayRatingSummaryProps {
7
7
  totalReviews?: number | string;
8
8
  distribution?: RatingDistribution[];
9
9
  badge?: number;
10
+ hasReviewed?: boolean;
10
11
  }
11
12
  declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
13
  "write-review": () => any;
@@ -16,6 +17,7 @@ declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryPr
16
17
  rating: number | string;
17
18
  totalReviews: number | string;
18
19
  distribution: RatingDistribution[];
20
+ hasReviewed: boolean;
19
21
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
22
  declare const _default: typeof __VLS_export;
21
23
  export default _default;
@@ -1,74 +1,78 @@
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 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>
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
+ v-if="form.coverImage"
14
+ class="w-[178px] h-[100px] rounded-[8px] overflow-hidden flex-shrink-0"
15
+ >
16
+ <Image
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">{{ form.placeName }}</div>
25
+ <div class="font-body-small text-gray">{{ form.address }}</div>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="flex items-center justify-center py-[16px]">
30
+ <InputRating v-model="form.rating" :size="28" />
31
+ </div>
32
+
33
+ <InputTextarea
34
+ name="description"
35
+ label="คำอธิบาย"
36
+ placeholder="เขียนรีวิวของคุณ"
37
+ showCounter
38
+ v-model="form.description"
39
+ class="min-h-[120px]"
40
+ />
41
+
42
+ <div class="flex flex-col gap-[8px]">
43
+ <div class="flex flex-col gap-[4px]">
44
+ <div class="text-gray font-body-large">เพิ่มภาพถ่าย</div>
45
+ <div class="text-gray font-body-small">อัปโหลด 9 รายการ</div>
46
+ </div>
47
+ <InputFile
48
+ :limit="9"
49
+ name="photos"
50
+ accept="image/jpeg,image/png,image/webp,image/bmp,image/gif"
51
+ v-model="form.photos"
52
+ />
53
+ <div class="flex flex-col text-gray font-body-small">
54
+ <div>รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif</div>
55
+ <div>ขนาดไฟล์สูงสุด 30 mb</div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <template #footer>
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>
72
76
  </template>
73
77
 
74
78
  <script setup>
@@ -114,7 +118,9 @@ watch(isOpen, (newVal) => {
114
118
  }
115
119
  });
116
120
  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");
121
+ const drawerTitle = computed(
122
+ () => isEdit.value ? "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E23\u0E35\u0E27\u0E34\u0E27" : "\u0E40\u0E02\u0E35\u0E22\u0E19\u0E23\u0E35\u0E27\u0E34\u0E27"
123
+ );
118
124
  const hasChanges = () => {
119
125
  const orig = props.item;
120
126
  if (form.value.rating !== orig.rating) return true;
@@ -1,130 +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
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="ละติจูด, ลองจิจูด"
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
122
  :model-value="
123
123
  modelValue.latLng ? `${modelValue.latLng.lat.toFixed(6)}, ${modelValue.latLng.lng.toFixed(6)}` : ''
124
- "
125
- />
126
- </div>
127
- </div>
124
+ "
125
+ />
126
+ </div>
127
+ </div>
128
128
  </template>
129
129
 
130
130
  <script setup>
@@ -64,15 +64,15 @@ 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
+ background: boolean;
68
+ modal: boolean;
67
69
  responsive: boolean;
68
70
  restore: boolean;
69
71
  checkCrossOrigin: boolean;
70
72
  checkOrientation: boolean;
71
73
  crossorigin: "" | "anonymous" | "use-credentials";
72
- modal: boolean;
73
74
  guides: boolean;
74
75
  highlight: boolean;
75
- background: boolean;
76
76
  autoCrop: boolean;
77
77
  movable: boolean;
78
78
  rotatable: boolean;
@@ -64,15 +64,15 @@ 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
+ background: boolean;
68
+ modal: boolean;
67
69
  responsive: boolean;
68
70
  restore: boolean;
69
71
  checkCrossOrigin: boolean;
70
72
  checkOrientation: boolean;
71
73
  crossorigin: "" | "anonymous" | "use-credentials";
72
- modal: boolean;
73
74
  guides: boolean;
74
75
  highlight: boolean;
75
- background: boolean;
76
76
  autoCrop: boolean;
77
77
  movable: boolean;
78
78
  rotatable: boolean;
@@ -50,11 +50,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
50
50
  name: string;
51
51
  options: AutocompleteOption[] | string[] | number[];
52
52
  description: string;
53
- limit: number;
54
53
  placeholder: string;
55
54
  disabledErrorMessage: boolean;
56
55
  disabledBorder: boolean;
57
56
  showCounter: boolean;
57
+ limit: number;
58
58
  returnObject: boolean;
59
59
  freeText: boolean;
60
60
  valueKey: string;
@@ -50,11 +50,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
50
50
  name: string;
51
51
  options: AutocompleteOption[] | string[] | number[];
52
52
  description: string;
53
- limit: number;
54
53
  placeholder: string;
55
54
  disabledErrorMessage: boolean;
56
55
  disabledBorder: boolean;
57
56
  showCounter: boolean;
57
+ limit: number;
58
58
  returnObject: boolean;
59
59
  freeText: boolean;
60
60
  valueKey: string;
@@ -34,8 +34,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
34
34
  fullWidth: boolean;
35
35
  fullHeight: boolean;
36
36
  name: string;
37
- limit: number;
38
37
  disabledErrorMessage: boolean;
38
+ limit: number;
39
39
  accept: string;
40
40
  labelIcon: string;
41
41
  disabledDrop: boolean;
@@ -34,8 +34,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
34
34
  fullWidth: boolean;
35
35
  fullHeight: boolean;
36
36
  name: string;
37
- limit: number;
38
37
  disabledErrorMessage: boolean;
38
+ limit: number;
39
39
  accept: string;
40
40
  labelIcon: string;
41
41
  disabledDrop: boolean;
@@ -26,8 +26,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
26
26
  }>, {
27
27
  name: string;
28
28
  state: "user" | "admin";
29
- limit: number;
30
29
  placeholder: string;
30
+ limit: number;
31
31
  ignore: string[];
32
32
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
33
  declare const _default: typeof __VLS_export;
@@ -26,8 +26,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
26
26
  }>, {
27
27
  name: string;
28
28
  state: "user" | "admin";
29
- limit: number;
30
29
  placeholder: string;
30
+ limit: number;
31
31
  ignore: string[];
32
32
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
33
  declare const _default: typeof __VLS_export;
@@ -59,10 +59,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
59
  required: boolean;
60
60
  id: string;
61
61
  name: string;
62
- limit: number;
63
62
  disabledErrorMessage: boolean;
64
63
  disabledBorder: boolean;
65
64
  showCounter: boolean;
65
+ limit: number;
66
66
  readonly: boolean;
67
67
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
68
68
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -59,10 +59,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
59
  required: boolean;
60
60
  id: string;
61
61
  name: string;
62
- limit: number;
63
62
  disabledErrorMessage: boolean;
64
63
  disabledBorder: boolean;
65
64
  showCounter: boolean;
65
+ limit: number;
66
66
  readonly: boolean;
67
67
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
68
68
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -46,10 +46,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  id: string;
47
47
  name: string;
48
48
  resize: "none" | "both" | "horizontal" | "vertical";
49
- limit: number;
50
49
  disabledErrorMessage: boolean;
51
50
  disabledBorder: boolean;
52
51
  showCounter: boolean;
52
+ limit: number;
53
53
  readonly: boolean;
54
54
  rows: number;
55
55
  heightScroll: boolean;
@@ -46,10 +46,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  id: string;
47
47
  name: string;
48
48
  resize: "none" | "both" | "horizontal" | "vertical";
49
- limit: number;
50
49
  disabledErrorMessage: boolean;
51
50
  disabledBorder: boolean;
52
51
  showCounter: boolean;
52
+ limit: number;
53
53
  readonly: boolean;
54
54
  rows: number;
55
55
  heightScroll: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.300.0",
3
+ "version": "1.302.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",