pukaad-ui-lib 1.453.0 → 1.454.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 (40) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/card/card-blog.d.vue.ts +1 -1
  3. package/dist/runtime/components/card/card-blog.vue.d.ts +1 -1
  4. package/dist/runtime/components/card/card-profile/card-profile-header.d.vue.ts +1 -1
  5. package/dist/runtime/components/card/card-profile/card-profile-header.vue.d.ts +1 -1
  6. package/dist/runtime/components/card/card-reaction.d.vue.ts +1 -1
  7. package/dist/runtime/components/card/card-reaction.vue.d.ts +1 -1
  8. package/dist/runtime/components/chat/chat-conversation-item.d.vue.ts +1 -1
  9. package/dist/runtime/components/chat/chat-conversation-item.vue.d.ts +1 -1
  10. package/dist/runtime/components/display/display-video.d.vue.ts +5 -5
  11. package/dist/runtime/components/display/display-video.vue.d.ts +5 -5
  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-combobox.d.vue.ts +1 -1
  15. package/dist/runtime/components/input/input-combobox.vue.d.ts +1 -1
  16. package/dist/runtime/components/input/input-comment.d.vue.ts +1 -1
  17. package/dist/runtime/components/input/input-comment.vue.d.ts +1 -1
  18. package/dist/runtime/components/input/input-file.d.vue.ts +1 -1
  19. package/dist/runtime/components/input/input-file.vue.d.ts +1 -1
  20. package/dist/runtime/components/input/input-localized-name.d.vue.ts +1 -1
  21. package/dist/runtime/components/input/input-localized-name.vue.d.ts +1 -1
  22. package/dist/runtime/components/input/input-suggest.d.vue.ts +1 -1
  23. package/dist/runtime/components/input/input-suggest.vue.d.ts +1 -1
  24. package/dist/runtime/components/input/input-tag.d.vue.ts +1 -1
  25. package/dist/runtime/components/input/input-tag.vue.d.ts +1 -1
  26. package/dist/runtime/components/input/input-textarea.d.vue.ts +1 -1
  27. package/dist/runtime/components/input/input-textarea.vue.d.ts +1 -1
  28. package/dist/runtime/components/preview/preview-content.d.vue.ts +54 -0
  29. package/dist/runtime/components/preview/preview-content.vue +219 -0
  30. package/dist/runtime/components/preview/preview-content.vue.d.ts +54 -0
  31. package/dist/runtime/components/preview/preview-place.d.vue.ts +57 -0
  32. package/dist/runtime/components/preview/preview-place.vue +342 -0
  33. package/dist/runtime/components/preview/preview-place.vue.d.ts +57 -0
  34. package/dist/runtime/components/ui/chart/ChartTooltipContent.d.vue.ts +1 -1
  35. package/dist/runtime/components/ui/chart/ChartTooltipContent.vue.d.ts +1 -1
  36. package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.d.vue.ts +2 -2
  37. package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.vue.d.ts +2 -2
  38. package/dist/runtime/components/ui/native-select/NativeSelectOption.d.vue.ts +2 -2
  39. package/dist/runtime/components/ui/native-select/NativeSelectOption.vue.d.ts +2 -2
  40. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.453.0",
4
+ "version": "1.454.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -38,8 +38,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
38
38
  "onBlog-updated"?: ((id: string, data: any) => any) | undefined;
39
39
  "onBlog-deleted"?: ((id: string) => any) | undefined;
40
40
  }>, {
41
- isMyProfile: boolean;
42
41
  disabledMenu: boolean;
42
+ isMyProfile: boolean;
43
43
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
44
44
  declare const _default: typeof __VLS_export;
45
45
  export default _default;
@@ -38,8 +38,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
38
38
  "onBlog-updated"?: ((id: string, data: any) => any) | undefined;
39
39
  "onBlog-deleted"?: ((id: string) => any) | undefined;
40
40
  }>, {
41
- isMyProfile: boolean;
42
41
  disabledMenu: boolean;
42
+ isMyProfile: boolean;
43
43
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
44
44
  declare const _default: typeof __VLS_export;
45
45
  export default _default;
@@ -7,8 +7,8 @@ declare const __VLS_export: import("vue").DefineComponent<CardProfileAboutProps,
7
7
  onFollow?: ((isFollowing: boolean) => any) | undefined;
8
8
  }>, {
9
9
  type: "article" | "media" | "sale-info";
10
- disabledPadding: boolean;
11
10
  preview: boolean;
11
+ disabledPadding: boolean;
12
12
  optionMenuButtonType: "solid" | "text";
13
13
  actionsHidden: boolean;
14
14
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -7,8 +7,8 @@ declare const __VLS_export: import("vue").DefineComponent<CardProfileAboutProps,
7
7
  onFollow?: ((isFollowing: boolean) => any) | undefined;
8
8
  }>, {
9
9
  type: "article" | "media" | "sale-info";
10
- disabledPadding: boolean;
11
10
  preview: boolean;
11
+ disabledPadding: boolean;
12
12
  optionMenuButtonType: "solid" | "text";
13
13
  actionsHidden: boolean;
14
14
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -25,6 +25,7 @@ type __VLS_Props = {
25
25
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
26
26
  disabled: boolean;
27
27
  id: string;
28
+ state: string;
28
29
  itemCount: Count;
29
30
  itemActive: Reacted;
30
31
  padding: string | number;
@@ -32,7 +33,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
32
33
  disabledDividerTop: boolean;
33
34
  disabledDividerBottom: boolean;
34
35
  iconSize: number | string;
35
- state: string;
36
36
  loginPath: string;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
@@ -25,6 +25,7 @@ type __VLS_Props = {
25
25
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
26
26
  disabled: boolean;
27
27
  id: string;
28
+ state: string;
28
29
  itemCount: Count;
29
30
  itemActive: Reacted;
30
31
  padding: string | number;
@@ -32,7 +33,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
32
33
  disabledDividerTop: boolean;
33
34
  disabledDividerBottom: boolean;
34
35
  iconSize: number | string;
35
- state: string;
36
36
  loginPath: string;
37
37
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
38
  declare const _default: typeof __VLS_export;
@@ -26,13 +26,13 @@ declare const __VLS_export: import("vue").DefineComponent<ChatConversationItemPr
26
26
  name: string;
27
27
  avatar: string | null;
28
28
  muted: boolean;
29
- disabledMenu: boolean;
30
29
  preview: string;
31
30
  to: string;
32
31
  active: boolean;
33
32
  typing: boolean;
34
33
  unreadCount: number;
35
34
  pinned: boolean;
35
+ disabledMenu: boolean;
36
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
37
37
  declare const _default: typeof __VLS_export;
38
38
  export default _default;
@@ -26,13 +26,13 @@ declare const __VLS_export: import("vue").DefineComponent<ChatConversationItemPr
26
26
  name: string;
27
27
  avatar: string | null;
28
28
  muted: boolean;
29
- disabledMenu: boolean;
30
29
  preview: string;
31
30
  to: string;
32
31
  active: boolean;
33
32
  typing: boolean;
34
33
  unreadCount: number;
35
34
  pinned: boolean;
35
+ disabledMenu: boolean;
36
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
37
37
  declare const _default: typeof __VLS_export;
38
38
  export default _default;
@@ -81,16 +81,16 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
81
81
  toggle: () => void;
82
82
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
83
83
  close: (payload: DisplayVideoAction) => any;
84
- fullscreen: (payload: DisplayVideoAction) => any;
85
- "play-click": (payload: DisplayVideoAction) => any;
86
84
  prev: (payload: DisplayVideoAction) => any;
87
85
  next: (payload: DisplayVideoAction) => any;
86
+ fullscreen: (payload: DisplayVideoAction) => any;
87
+ "play-click": (payload: DisplayVideoAction) => any;
88
88
  }, string, import("vue").PublicProps, Readonly<DisplayVideoProps> & Readonly<{
89
89
  onClose?: ((payload: DisplayVideoAction) => any) | undefined;
90
- onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
91
- "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
92
90
  onPrev?: ((payload: DisplayVideoAction) => any) | undefined;
93
91
  onNext?: ((payload: DisplayVideoAction) => any) | undefined;
92
+ onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
93
+ "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
94
94
  }>, {
95
95
  src: string;
96
96
  fullWidth: boolean;
@@ -102,9 +102,9 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
102
102
  loop: boolean;
103
103
  controls: boolean;
104
104
  preload: "none" | "metadata" | "auto";
105
+ playsinline: boolean;
105
106
  sources: DisplayVideoSource[];
106
107
  tracks: DisplayVideoTrack[];
107
- playsinline: boolean;
108
108
  disablePictureInPicture: boolean;
109
109
  disableRemotePlayback: boolean;
110
110
  volume: number;
@@ -81,16 +81,16 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
81
81
  toggle: () => void;
82
82
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
83
83
  close: (payload: DisplayVideoAction) => any;
84
- fullscreen: (payload: DisplayVideoAction) => any;
85
- "play-click": (payload: DisplayVideoAction) => any;
86
84
  prev: (payload: DisplayVideoAction) => any;
87
85
  next: (payload: DisplayVideoAction) => any;
86
+ fullscreen: (payload: DisplayVideoAction) => any;
87
+ "play-click": (payload: DisplayVideoAction) => any;
88
88
  }, string, import("vue").PublicProps, Readonly<DisplayVideoProps> & Readonly<{
89
89
  onClose?: ((payload: DisplayVideoAction) => any) | undefined;
90
- onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
91
- "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
92
90
  onPrev?: ((payload: DisplayVideoAction) => any) | undefined;
93
91
  onNext?: ((payload: DisplayVideoAction) => any) | undefined;
92
+ onFullscreen?: ((payload: DisplayVideoAction) => any) | undefined;
93
+ "onPlay-click"?: ((payload: DisplayVideoAction) => any) | undefined;
94
94
  }>, {
95
95
  src: string;
96
96
  fullWidth: boolean;
@@ -102,9 +102,9 @@ declare const __VLS_base: import("vue").DefineComponent<DisplayVideoProps, {
102
102
  loop: boolean;
103
103
  controls: boolean;
104
104
  preload: "none" | "metadata" | "auto";
105
+ playsinline: boolean;
105
106
  sources: DisplayVideoSource[];
106
107
  tracks: DisplayVideoTrack[];
107
- playsinline: boolean;
108
108
  disablePictureInPicture: boolean;
109
109
  disableRemotePlayback: boolean;
110
110
  volume: number;
@@ -70,9 +70,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
70
70
  required: boolean;
71
71
  description: string;
72
72
  options: AutocompleteOption[] | string[] | number[];
73
- limit: number;
74
73
  labelKey: string;
75
74
  placeholder: string;
75
+ limit: number;
76
76
  disabledErrorMessage: boolean;
77
77
  disabledBorder: boolean;
78
78
  showCounter: boolean;
@@ -70,9 +70,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
70
70
  required: boolean;
71
71
  description: string;
72
72
  options: AutocompleteOption[] | string[] | number[];
73
- limit: number;
74
73
  labelKey: string;
75
74
  placeholder: string;
75
+ limit: number;
76
76
  disabledErrorMessage: boolean;
77
77
  disabledBorder: boolean;
78
78
  showCounter: boolean;
@@ -60,8 +60,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
60
60
  }>, {
61
61
  pageSize: number;
62
62
  name: string;
63
- limit: number;
64
63
  labelKey: string;
64
+ limit: number;
65
65
  valueKey: string;
66
66
  multiple: boolean;
67
67
  placeholderSearch: string;
@@ -60,8 +60,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
60
60
  }>, {
61
61
  pageSize: number;
62
62
  name: string;
63
- limit: number;
64
63
  labelKey: string;
64
+ limit: number;
65
65
  valueKey: string;
66
66
  multiple: boolean;
67
67
  placeholderSearch: string;
@@ -46,8 +46,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  }>, {
47
47
  loading: boolean;
48
48
  disabled: boolean;
49
- placeholder: string;
50
49
  cancelText: string;
50
+ placeholder: string;
51
51
  autofocus: boolean;
52
52
  disabledAvatar: boolean;
53
53
  submitText: string;
@@ -46,8 +46,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  }>, {
47
47
  loading: boolean;
48
48
  disabled: boolean;
49
- placeholder: string;
50
49
  cancelText: string;
50
+ placeholder: string;
51
51
  autofocus: boolean;
52
52
  disabledAvatar: boolean;
53
53
  submitText: string;
@@ -37,8 +37,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
37
37
  fullWidth: boolean;
38
38
  fullHeight: boolean;
39
39
  limit: number;
40
- accept: string;
41
40
  disabledErrorMessage: boolean;
41
+ accept: string;
42
42
  labelIcon: string;
43
43
  disabledDrop: boolean;
44
44
  column: boolean;
@@ -37,8 +37,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
37
37
  fullWidth: boolean;
38
38
  fullHeight: boolean;
39
39
  limit: number;
40
- accept: string;
41
40
  disabledErrorMessage: boolean;
41
+ accept: string;
42
42
  labelIcon: string;
43
43
  disabledDrop: boolean;
44
44
  column: boolean;
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  name: string;
13
13
  modelValue: LocalizedNameItem[];
14
14
  options: InputSelectItem[];
15
- limit: number;
16
15
  placeholder: string;
16
+ limit: number;
17
17
  removable: boolean;
18
18
  showCounter: boolean;
19
19
  addLabel: string;
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  name: string;
13
13
  modelValue: LocalizedNameItem[];
14
14
  options: InputSelectItem[];
15
- limit: number;
16
15
  placeholder: string;
16
+ limit: number;
17
17
  removable: boolean;
18
18
  showCounter: boolean;
19
19
  addLabel: string;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
20
20
  name: string;
21
21
  required: boolean;
22
22
  description: string;
23
- limit: number;
24
23
  labelKey: string;
25
24
  placeholder: string;
25
+ limit: number;
26
26
  disabledErrorMessage: boolean;
27
27
  disabledBorder: boolean;
28
28
  showCounter: boolean;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
20
20
  name: string;
21
21
  required: boolean;
22
22
  description: string;
23
- limit: number;
24
23
  labelKey: string;
25
24
  placeholder: string;
25
+ limit: number;
26
26
  disabledErrorMessage: boolean;
27
27
  disabledBorder: boolean;
28
28
  showCounter: boolean;
@@ -27,8 +27,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
27
27
  }>, {
28
28
  name: string;
29
29
  state: "user" | "admin";
30
- limit: number;
31
30
  placeholder: string;
31
+ limit: number;
32
32
  profileScope: "personal" | "business";
33
33
  ignore: string[];
34
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -27,8 +27,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
27
27
  }>, {
28
28
  name: string;
29
29
  state: "user" | "admin";
30
- limit: number;
31
30
  placeholder: string;
31
+ limit: number;
32
32
  profileScope: "personal" | "business";
33
33
  ignore: string[];
34
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -46,8 +46,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  resize: "none" | "both" | "horizontal" | "vertical";
47
47
  fullWidth: boolean;
48
48
  fullHeight: boolean;
49
- limit: number;
50
49
  rows: number;
50
+ limit: number;
51
51
  readonly: boolean;
52
52
  disabledErrorMessage: boolean;
53
53
  disabledBorder: boolean;
@@ -46,8 +46,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
46
46
  resize: "none" | "both" | "horizontal" | "vertical";
47
47
  fullWidth: boolean;
48
48
  fullHeight: boolean;
49
- limit: number;
50
49
  rows: number;
50
+ limit: number;
51
51
  readonly: boolean;
52
52
  disabledErrorMessage: boolean;
53
53
  disabledBorder: boolean;
@@ -0,0 +1,54 @@
1
+ import type { CardReferItem } from "#pukaad-ui/runtime/components/card/card-refer.vue";
2
+ export type PreviewContentState = "personal" | "business";
3
+ export interface PreviewContentData {
4
+ id?: string;
5
+ content_type?: "post_it" | "image" | "video";
6
+ labels?: {
7
+ id: string;
8
+ name: string;
9
+ }[];
10
+ cover_text?: string;
11
+ cover_color?: string;
12
+ video_poster_url?: string;
13
+ description?: string;
14
+ media?: {
15
+ media_type: string;
16
+ url: string;
17
+ description?: string;
18
+ }[];
19
+ tags?: {
20
+ id: string;
21
+ name: string;
22
+ }[];
23
+ mentions?: CardReferItem[];
24
+ places?: {
25
+ id: string;
26
+ name: string;
27
+ address?: string;
28
+ }[];
29
+ like_count?: number;
30
+ bookmark_count?: number;
31
+ share_count?: number;
32
+ author?: {
33
+ id: string;
34
+ name: string;
35
+ avatar: string | null;
36
+ follower_count?: number;
37
+ published_count?: number;
38
+ response_rate?: number;
39
+ };
40
+ }
41
+ type __VLS_Props = {
42
+ contentId: string | number;
43
+ /** เลือก endpoint ตามแอปที่เปิดพรีวิว */
44
+ state?: PreviewContentState;
45
+ };
46
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
47
+ exit: () => any;
48
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
49
+ onExit?: (() => any) | undefined;
50
+ }>, {
51
+ state: PreviewContentState;
52
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
53
+ declare const _default: typeof __VLS_export;
54
+ export default _default;
@@ -0,0 +1,219 @@
1
+ <template>
2
+ <!-- Figma: Preview - Video (2743:66169) — post_it / image ใช้โครงเดียวกัน -->
3
+ <div class="min-h-screen w-full bg-white">
4
+ <!-- Figma: Navbar Preview (new tab) — Property 1=Admin -->
5
+ <!-- ใช้ scroll ของหน้า (ไม่ใช่ scroll ใน div) เพราะ Comment โหลดหน้าถัดไปจาก window scroll -->
6
+ <div class="sticky top-0 z-overlay w-full bg-cloud">
7
+ <div class="max-w-[1280px] mx-auto flex justify-end p-[16px]">
8
+ <Button variant="outline" class="rounded-[6px]" @click="emit('exit')">
9
+ ออกจากพรีวิว
10
+ </Button>
11
+ </div>
12
+ </div>
13
+
14
+ <!-- Figma: เนื้อหาเริ่มห่างจากแถบ 22px (ช่องว่าง 6 + padding 16) -->
15
+ <div class="flex justify-center px-[16px] pt-[22px] pb-[16px]">
16
+ <!-- กว้าง 844 เท่าวิดีโอ — DisplayVideo ยืดตามความกว้างกล่องนี้ ปุ่มเล่นจะได้อยู่กลางวิดีโอ -->
17
+ <div class="w-[844px] flex flex-col gap-[16px]">
18
+ <Label v-if="labelItems.length" :items="labelItems" />
19
+
20
+ <!-- Post-it -->
21
+ <div
22
+ v-if="content.content_type === 'post_it'"
23
+ class="flex gap-[16px] py-[16px] items-center"
24
+ >
25
+ <div
26
+ class="w-[6px] h-full shrink-0"
27
+ :style="{ backgroundColor: content.cover_color }"
28
+ />
29
+ <div class="font-headline-large-prominent min-w-0 break-words">
30
+ {{ content.cover_text }}
31
+ </div>
32
+ </div>
33
+ <!-- Image -->
34
+ <DisplayImage
35
+ v-else-if="content.content_type === 'image'"
36
+ :items="content.media"
37
+ item-src="url"
38
+ />
39
+ <!-- Video -->
40
+ <DisplayVideo
41
+ v-else-if="content.content_type === 'video'"
42
+ :src="content.media?.[0]?.url"
43
+ :poster="content.video_poster_url"
44
+ play-button
45
+ width="844"
46
+ height="475"
47
+ preload="metadata"
48
+ class="bg-black"
49
+ />
50
+
51
+ <CardRefer v-if="content.mentions?.[0]" :item="content.mentions[0]" />
52
+
53
+ <!-- Figma: Announce Description -->
54
+ <div
55
+ v-if="content.author"
56
+ class="flex items-center justify-between gap-[16px]"
57
+ >
58
+ <div class="flex items-center gap-[8px] min-w-0">
59
+ <Avatar :src="content.author.avatar ?? ''" :size="40" />
60
+ <div class="flex flex-col gap-[4px] min-w-0">
61
+ <div class="font-body-large-prominent truncate">
62
+ {{ content.author.name }}
63
+ </div>
64
+ <div class="flex gap-[8px] font-body-large">
65
+ <div class="flex gap-[4px]">
66
+ <span class="text-gray">ผู้ติดตาม</span>
67
+ {{ convertNumber(content.author.follower_count ?? 0) }}
68
+ </div>
69
+ <div class="flex gap-[4px]">
70
+ <span class="text-gray">กำลังเผยแพร่</span>
71
+ {{ convertNumber(content.author.published_count ?? 0) }}
72
+ </div>
73
+ <div class="flex gap-[4px]">
74
+ <span class="text-gray">อัตราตอบแชท</span>
75
+ {{ content.author.response_rate ?? 0 }}%
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <!-- พรีวิว = ดูหน้าตาได้แต่กดไม่ได้ -->
81
+ <Button circle size="icon-sm" class="shrink-0" inert>
82
+ <Icon name="lucide:ellipsis-vertical" />
83
+ </Button>
84
+ </div>
85
+
86
+ <div
87
+ v-if="content.description"
88
+ class="font-body-large whitespace-pre-wrap"
89
+ >
90
+ {{ content.description }}
91
+ </div>
92
+
93
+ <div v-if="place" class="flex gap-[8px] font-body-large">
94
+ <span class="text-gray">อยู่ที่</span>
95
+ <span class="text-primary">{{ place.name }}</span>
96
+ </div>
97
+
98
+ <div v-if="content.tags?.length" class="flex flex-wrap gap-[8px]">
99
+ <Chip v-for="tag in content.tags" :key="tag.id" color="bright">
100
+ {{ tag.name }}
101
+ </Chip>
102
+ </div>
103
+
104
+ <CardReaction
105
+ :item-count="{
106
+ liked: content.like_count ?? 0,
107
+ bookmarked: content.bookmark_count ?? 0,
108
+ shared: content.share_count ?? 0
109
+ }"
110
+ disabled
111
+ />
112
+
113
+ <!-- Figma: Spotlight -->
114
+ <div v-if="recommendedContents.length" inert>
115
+ <CardContentRecommend :items="recommendedContents" />
116
+ </div>
117
+
118
+ <div inert>
119
+ <Comment
120
+ :key="String(props.contentId)"
121
+ :fetch-fn="commentFetchFn"
122
+ :current-user="previewUser"
123
+ />
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </template>
129
+
130
+ <script setup>
131
+ import { computed, onMounted, ref, watch } from "vue";
132
+ import { useApi } from "#pukaad-ui/runtime/composables/useApi";
133
+ import { useConvert } from "#pukaad-ui/runtime/composables/useConvert";
134
+ import { mapContentToCard } from "#pukaad-ui/runtime/composables/useContentCard";
135
+ const props = defineProps({
136
+ contentId: { type: [String, Number], required: true },
137
+ state: { type: String, required: false, default: "personal" }
138
+ });
139
+ const emit = defineEmits(["exit"]);
140
+ const api = useApi();
141
+ const { convertNumber } = useConvert();
142
+ const endpointMap = {
143
+ personal: "/contents",
144
+ business: "/business/contents"
145
+ };
146
+ const content = ref({});
147
+ const recommendedContents = ref([]);
148
+ const previewUser = { id: "", name: "", avatar: "" };
149
+ const labelItems = computed(
150
+ () => (content.value.labels ?? []).map((label) => ({
151
+ id: label.id,
152
+ label: label.name
153
+ }))
154
+ );
155
+ const place = computed(() => content.value.places?.[0]);
156
+ const fetchContent = async () => {
157
+ try {
158
+ const res = await api(
159
+ `${endpointMap[props.state]}/${props.contentId}`
160
+ );
161
+ content.value = res.data ?? {};
162
+ } catch (err) {
163
+ console.error(err);
164
+ }
165
+ };
166
+ const fetchRecommendations = async () => {
167
+ try {
168
+ const res = await api(
169
+ `/contents/${props.contentId}/recommendations`,
170
+ { query: { limit: 5, contents_limit: 8 } }
171
+ );
172
+ recommendedContents.value = (res.data?.contents ?? []).map(
173
+ (raw) => mapContentToCard(raw)
174
+ );
175
+ } catch (err) {
176
+ console.error(err);
177
+ recommendedContents.value = [];
178
+ }
179
+ };
180
+ const toCommentUser = (profile) => ({
181
+ id: profile?.profile_path?.path_name ?? "",
182
+ name: profile?.profile_name ?? "\u0E44\u0E21\u0E48\u0E17\u0E23\u0E32\u0E1A\u0E0A\u0E37\u0E48\u0E2D",
183
+ avatar: profile?.profile_image?.image_url ?? ""
184
+ });
185
+ const toCommentItem = (raw) => ({
186
+ id: String(raw.id ?? ""),
187
+ user: toCommentUser(raw.profileData),
188
+ content: raw.content ?? "",
189
+ created_at: raw.created_at ?? "",
190
+ like_count: raw.like_count ?? 0,
191
+ is_liked: raw.is_liked ?? false,
192
+ reply_count: raw.reply_count ?? 0,
193
+ reply: (raw.comment_reply ?? []).map((r) => ({
194
+ id: String(r.id ?? ""),
195
+ user: toCommentUser(r.profileData),
196
+ reply_to: r.reply_to ? toCommentUser(r.reply_to) : void 0,
197
+ content: r.content ?? "",
198
+ created_at: r.created_at ?? "",
199
+ like_count: r.like_count ?? 0,
200
+ is_liked: r.is_liked ?? false
201
+ }))
202
+ });
203
+ const commentFetchFn = async (page, pageSize) => {
204
+ const res = await api(`/contents/${props.contentId}/comments`, {
205
+ query: { limit: pageSize, offset: (page - 1) * pageSize }
206
+ });
207
+ return {
208
+ data: (res.data ?? []).map(toCommentItem),
209
+ totalComments: res.meta?.total_comments ?? 0,
210
+ meta: { total_pages: res.meta?.total_pages ?? 1 }
211
+ };
212
+ };
213
+ const loadAll = () => {
214
+ fetchContent();
215
+ fetchRecommendations();
216
+ };
217
+ watch(() => [props.contentId, props.state], loadAll);
218
+ onMounted(loadAll);
219
+ </script>
@@ -0,0 +1,54 @@
1
+ import type { CardReferItem } from "#pukaad-ui/runtime/components/card/card-refer.vue";
2
+ export type PreviewContentState = "personal" | "business";
3
+ export interface PreviewContentData {
4
+ id?: string;
5
+ content_type?: "post_it" | "image" | "video";
6
+ labels?: {
7
+ id: string;
8
+ name: string;
9
+ }[];
10
+ cover_text?: string;
11
+ cover_color?: string;
12
+ video_poster_url?: string;
13
+ description?: string;
14
+ media?: {
15
+ media_type: string;
16
+ url: string;
17
+ description?: string;
18
+ }[];
19
+ tags?: {
20
+ id: string;
21
+ name: string;
22
+ }[];
23
+ mentions?: CardReferItem[];
24
+ places?: {
25
+ id: string;
26
+ name: string;
27
+ address?: string;
28
+ }[];
29
+ like_count?: number;
30
+ bookmark_count?: number;
31
+ share_count?: number;
32
+ author?: {
33
+ id: string;
34
+ name: string;
35
+ avatar: string | null;
36
+ follower_count?: number;
37
+ published_count?: number;
38
+ response_rate?: number;
39
+ };
40
+ }
41
+ type __VLS_Props = {
42
+ contentId: string | number;
43
+ /** เลือก endpoint ตามแอปที่เปิดพรีวิว */
44
+ state?: PreviewContentState;
45
+ };
46
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
47
+ exit: () => any;
48
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
49
+ onExit?: (() => any) | undefined;
50
+ }>, {
51
+ state: PreviewContentState;
52
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
53
+ declare const _default: typeof __VLS_export;
54
+ export default _default;
@@ -0,0 +1,57 @@
1
+ import type { Category, ContactChannel, OpeningHour, SpecialDay } from "#pukaad-ui/types/models/place";
2
+ export type PreviewPlaceState = "personal" | "office" | "business";
3
+ export interface PreviewPlaceMedia {
4
+ id?: string | number;
5
+ url: string;
6
+ description?: string;
7
+ }
8
+ export interface PreviewPlaceData {
9
+ id?: string;
10
+ business_name?: string;
11
+ description?: string;
12
+ address?: {
13
+ full_address?: string;
14
+ };
15
+ photos?: PreviewPlaceMedia[];
16
+ videos?: PreviewPlaceMedia[];
17
+ rating?: number;
18
+ review_count?: number;
19
+ like_count?: number;
20
+ share_count?: number;
21
+ categories?: Category[];
22
+ is_review?: boolean;
23
+ open_status?: string;
24
+ lat_lng?: {
25
+ lat: number;
26
+ lng: number;
27
+ };
28
+ owner_profile_id?: string | null;
29
+ phone?: string;
30
+ contact_channels?: ContactChannel[];
31
+ opening_hours?: OpeningHour[];
32
+ special_days?: SpecialDay[];
33
+ }
34
+ export interface PreviewPlaceRecommendItem {
35
+ id: string | number;
36
+ title: string;
37
+ image: string;
38
+ rating: number;
39
+ review_count: number;
40
+ }
41
+ type __VLS_Props = {
42
+ placeId: string | number;
43
+ /** เลือก endpoint ตามแอปที่เปิดพรีวิว */
44
+ state?: PreviewPlaceState;
45
+ /** "ที่ไหนน่าสนใจ" — ยังไม่มี API ให้แอปส่งมาเอง ไม่ส่ง = ซ่อนการ์ด */
46
+ recommendedPlaces?: PreviewPlaceRecommendItem[];
47
+ };
48
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
49
+ exit: () => any;
50
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
51
+ onExit?: (() => any) | undefined;
52
+ }>, {
53
+ state: PreviewPlaceState;
54
+ recommendedPlaces: PreviewPlaceRecommendItem[];
55
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
56
+ declare const _default: typeof __VLS_export;
57
+ export default _default;
@@ -0,0 +1,342 @@
1
+ <template>
2
+ <!-- Figma: Preview - Display Place (2884:29368) -->
3
+ <div class="h-screen w-full overflow-hidden bg-white flex flex-col">
4
+ <!-- Figma: Navbar Preview (new tab) — Property 1=Admin -->
5
+ <div class="shrink-0 w-full bg-cloud">
6
+ <div class="max-w-[1280px] mx-auto flex justify-end p-[16px]">
7
+ <Button variant="outline" @click="emit('exit')">ออกจากพรีวิว</Button>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="flex-1 overflow-y-auto">
12
+ <div class="max-w-[1280px] mx-auto p-[16px] flex flex-col gap-[16px]">
13
+ <!-- Place Cover -->
14
+ <DisplayImagePlace
15
+ v-if="photos.length > 0 || videos.length > 0"
16
+ :photos="photos"
17
+ :videos="videos"
18
+ variant="featured"
19
+ :state="props.state"
20
+ :title="placeData.business_name"
21
+ />
22
+
23
+ <!-- Place Title -->
24
+ <div class="flex flex-col gap-[8px]">
25
+ <div class="flex gap-[8px] justify-between items-center">
26
+ <div class="font-headline-medium-prominent">
27
+ {{ placeData.business_name || "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E0A\u0E37\u0E48\u0E2D\u0E2A\u0E16\u0E32\u0E19\u0E17\u0E35\u0E48" }}
28
+ </div>
29
+ <!-- พรีวิว = ดูหน้าตาได้แต่กดไม่ได้ -->
30
+ <div class="flex gap-[8px] items-center" inert>
31
+ <Button size="sm" class="rounded-full font-body-small">
32
+ <Icon name="lucide:heart" :size="16" />
33
+ ถูกใจ
34
+ </Button>
35
+ <Button circle size="icon-sm">
36
+ <Icon name="uil:share" />
37
+ </Button>
38
+ <Button circle size="icon-sm">
39
+ <Icon name="lucide:ellipsis-vertical" />
40
+ </Button>
41
+ </div>
42
+ </div>
43
+
44
+ <Chip v-if="closedStatus" color="error" class="gap-[4px] self-start">
45
+ <Icon :name="closedStatus.icon" class="!size-[16px]" />
46
+ <span class="font-body-medium-prominent">{{
47
+ closedStatus.label
48
+ }}</span>
49
+ </Chip>
50
+
51
+ <div class="flex gap-[16px] font-body-large">
52
+ <div class="flex gap-[8px] items-center">
53
+ <InputRating
54
+ :model-value="placeData.rating ?? 0"
55
+ readonly
56
+ :size="16"
57
+ />
58
+ {{ convertNumber(placeData.review_count || 0) }}
59
+ </div>
60
+ <Divider alignment="vertical" />
61
+ <div class="flex gap-[4px]">
62
+ <span class="text-gray">ถูกใจ</span>
63
+ {{ convertNumber(placeData.like_count || 0) }}
64
+ </div>
65
+ <Divider alignment="vertical" />
66
+ <div class="flex gap-[4px]">
67
+ <span class="text-gray">แชร์</span>
68
+ {{ convertNumber(placeData.share_count || 0) }}
69
+ </div>
70
+ <Divider alignment="vertical" />
71
+ <div>
72
+ {{
73
+ placeData.categories?.[0]?.category_name || "\u0E44\u0E21\u0E48\u0E23\u0E30\u0E1A\u0E38\u0E2B\u0E21\u0E27\u0E14\u0E2B\u0E21\u0E39\u0E48"
74
+ }}
75
+ </div>
76
+ </div>
77
+
78
+ <div class="flex gap-[16px] items-center">
79
+ <div class="flex gap-[8px] items-center">
80
+ <Icon name="lucide:map-pin" :size="20" class="text-primary" />
81
+ <div class="font-body-large text-gray">
82
+ {{ placeData.address?.full_address || "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E17\u0E35\u0E48\u0E2D\u0E22\u0E39\u0E48" }}
83
+ </div>
84
+ </div>
85
+ <Button
86
+ v-if="placeData.lat_lng"
87
+ variant="text"
88
+ color="primary"
89
+ class="font-body-large"
90
+ @click="openMap"
91
+ >
92
+ ดูแผนที่
93
+ </Button>
94
+ </div>
95
+ </div>
96
+
97
+ <div class="flex justify-between gap-[24px]">
98
+ <!-- ซ้าย: รีวิว -->
99
+ <div class="w-[824px] shrink-0">
100
+ <ShadTabs
101
+ v-if="placeData.is_review"
102
+ v-model="activeTab"
103
+ class="gap-[16px]"
104
+ >
105
+ <ShadTabsList>
106
+ <ShadTabsTrigger
107
+ v-for="tab in mainTabs"
108
+ :key="tab.value"
109
+ :value="tab.value"
110
+ >
111
+ {{ tab.label }}
112
+ </ShadTabsTrigger>
113
+ </ShadTabsList>
114
+
115
+ <ShadTabsContent value="review" class="flex flex-col gap-[16px]">
116
+ <div inert>
117
+ <DisplayRatingSummary
118
+ :rating="placeData.rating ?? 0"
119
+ :total-reviews="placeData.review_count ?? 0"
120
+ :distribution="ratingDistribution"
121
+ />
122
+ </div>
123
+
124
+ <ShadTabs v-model="activeReviewTab">
125
+ <ShadTabsList>
126
+ <ShadTabsTrigger
127
+ v-for="tab in reviewFilterTabs"
128
+ :key="tab.value"
129
+ :value="tab.value"
130
+ :disabled="reviewCounts[tab.value] === 0"
131
+ >
132
+ {{ tab.label }}
133
+ </ShadTabsTrigger>
134
+ </ShadTabsList>
135
+ </ShadTabs>
136
+
137
+ <div v-if="reviews.length > 0" class="flex flex-col">
138
+ <CardReview
139
+ v-for="review in reviews"
140
+ :key="review.review_id"
141
+ :item="review"
142
+ :is-profile="false"
143
+ disabled-like
144
+ disabled-menu
145
+ />
146
+ </div>
147
+ <div
148
+ v-else-if="!isLoadingReviews"
149
+ class="flex flex-col items-center gap-[8px] py-[32px]"
150
+ >
151
+ <Icon name="pukaad:page-not-found" :size="120" />
152
+ <div class="font-title-small-prominent">ยังไม่มีรีวิว</div>
153
+ </div>
154
+
155
+ <div ref="sentinel" class="h-[1px]" />
156
+ <div
157
+ v-if="isLoadingReviews"
158
+ class="flex justify-center py-[16px]"
159
+ >
160
+ <Icon
161
+ name="lucide:loader-circle"
162
+ :size="40"
163
+ class="animate-spin text-cloud"
164
+ />
165
+ </div>
166
+ </ShadTabsContent>
167
+
168
+ <ShadTabsContent value="aboutPost">
169
+ <div class="text-gray py-[32px] text-center">
170
+ ยังไม่มีโพสต์ที่เกี่ยวข้อง
171
+ </div>
172
+ </ShadTabsContent>
173
+ </ShadTabs>
174
+
175
+ <div
176
+ v-else
177
+ class="flex flex-col items-center justify-center gap-[16px] py-[48px]"
178
+ >
179
+ <Icon name="pukaad:page-not-found" :size="145" />
180
+ <div>ยังไม่มีรีวิว</div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- ขวา: Place About + Reccommended Places -->
185
+ <div class="w-[380px] shrink-0 flex flex-col gap-[16px]">
186
+ <CardPlaceDetail :item="placeDetailItem" />
187
+ <div v-if="props.recommendedPlaces.length > 0" inert>
188
+ <CardRecommendPlace :items="props.recommendedPlaces" />
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </template>
196
+
197
+ <script setup>
198
+ import { computed, onMounted, ref, useTemplateRef, watch } from "vue";
199
+ import { useElementVisibility } from "@vueuse/core";
200
+ import { useApi } from "#pukaad-ui/runtime/composables/useApi";
201
+ import { useConvert } from "#pukaad-ui/runtime/composables/useConvert";
202
+ const props = defineProps({
203
+ placeId: { type: [String, Number], required: true },
204
+ state: { type: String, required: false, default: "personal" },
205
+ recommendedPlaces: { type: Array, required: false, default: () => [] }
206
+ });
207
+ const emit = defineEmits(["exit"]);
208
+ const api = useApi();
209
+ const { convertNumber } = useConvert();
210
+ const endpointMap = {
211
+ personal: { place: "/suggest-places", reviews: "/suggest-places" },
212
+ office: {
213
+ place: "/office/suggest-places",
214
+ reviews: "/office/suggest-places"
215
+ },
216
+ business: { place: "/business/suggest-places", reviews: "/suggest-places" }
217
+ };
218
+ const endpoint = computed(() => endpointMap[props.state]);
219
+ const closedStatusMap = {
220
+ temporarily_closed: { label: "\u0E1B\u0E34\u0E14\u0E0A\u0E31\u0E48\u0E27\u0E04\u0E23\u0E32\u0E27", icon: "lucide:circle-pause" },
221
+ // ── MOCK ──
222
+ // TODO: ชื่อค่ายังไม่ยืนยันกับ backend (ยังไม่มี endpoint ปิดถาวร)
223
+ permanently_closed: { label: "\u0E1B\u0E34\u0E14\u0E16\u0E32\u0E27\u0E23", icon: "lucide:circle-x" }
224
+ // ── /MOCK ──
225
+ };
226
+ const mainTabs = [
227
+ { label: "\u0E23\u0E35\u0E27\u0E34\u0E27", value: "review" },
228
+ { label: "\u0E42\u0E1E\u0E2A\u0E15\u0E4C\u0E17\u0E35\u0E48\u0E40\u0E01\u0E35\u0E48\u0E22\u0E27\u0E02\u0E49\u0E2D\u0E07", value: "aboutPost" }
229
+ ];
230
+ const reviewFilterTabs = [
231
+ { label: "\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14", value: "all" },
232
+ { label: "\u0E21\u0E35\u0E23\u0E39\u0E1B\u0E20\u0E32\u0E1E", value: "img" },
233
+ { label: "\u0E04\u0E27\u0E32\u0E21\u0E04\u0E34\u0E14\u0E40\u0E2B\u0E47\u0E19", value: "comment" },
234
+ { label: "5 \u0E14\u0E32\u0E27", value: "rating_5" },
235
+ { label: "4 \u0E14\u0E32\u0E27", value: "rating_4" },
236
+ { label: "3 \u0E14\u0E32\u0E27", value: "rating_3" },
237
+ { label: "2 \u0E14\u0E32\u0E27", value: "rating_2" },
238
+ { label: "1 \u0E14\u0E32\u0E27", value: "rating_1" }
239
+ ];
240
+ const placeData = ref({});
241
+ const reviews = ref([]);
242
+ const reviewTotal = ref(0);
243
+ const reviewPage = ref(1);
244
+ const reviewPageSize = 10;
245
+ const isLoadingReviews = ref(false);
246
+ const activeTab = ref("review");
247
+ const activeReviewTab = ref("all");
248
+ const reviewCounts = ref({
249
+ all: 0,
250
+ img: 0,
251
+ comment: 0,
252
+ rating_5: 0,
253
+ rating_4: 0,
254
+ rating_3: 0,
255
+ rating_2: 0,
256
+ rating_1: 0
257
+ });
258
+ const hasMoreReviews = computed(() => reviews.value.length < reviewTotal.value);
259
+ const closedStatus = computed(() => {
260
+ const status = placeData.value.open_status;
261
+ return status ? closedStatusMap[status] : void 0;
262
+ });
263
+ const photos = computed(() => placeData.value.photos ?? []);
264
+ const videos = computed(() => placeData.value.videos ?? []);
265
+ const ratingDistribution = computed(
266
+ () => [5, 4, 3, 2, 1].map((stars) => ({
267
+ stars,
268
+ count: reviewCounts.value[`rating_${stars}`] ?? 0
269
+ }))
270
+ );
271
+ const placeDetailItem = computed(() => ({
272
+ id: placeData.value.id ?? String(props.placeId),
273
+ place_name: placeData.value.business_name ?? "",
274
+ description: placeData.value.description ?? "",
275
+ address: placeData.value.address?.full_address ?? "",
276
+ categories: placeData.value.categories ?? [],
277
+ open_status: placeData.value.open_status,
278
+ opening_hours: placeData.value.opening_hours,
279
+ special_days: placeData.value.special_days,
280
+ phone: placeData.value.phone,
281
+ contact_channels: placeData.value.contact_channels,
282
+ owner_profile_id: placeData.value.owner_profile_id
283
+ }));
284
+ const fetchPlaceData = async () => {
285
+ try {
286
+ const res = await api(
287
+ `${endpoint.value.place}/${props.placeId}`
288
+ );
289
+ placeData.value = res.data ?? {};
290
+ } catch (err) {
291
+ console.error(err);
292
+ }
293
+ };
294
+ let reviewRequestId = 0;
295
+ const fetchReviews = async (page = 1) => {
296
+ if (page > 1 && isLoadingReviews.value) return;
297
+ const requestId = ++reviewRequestId;
298
+ isLoadingReviews.value = true;
299
+ try {
300
+ const res = await api(`${endpoint.value.reviews}/${props.placeId}/reviews`, {
301
+ query: {
302
+ tab: activeReviewTab.value,
303
+ page,
304
+ page_size: reviewPageSize
305
+ }
306
+ });
307
+ if (requestId !== reviewRequestId) return;
308
+ const d = res?.data;
309
+ const items = d?.items ?? [];
310
+ reviews.value = page === 1 ? items : [...reviews.value, ...items];
311
+ reviewTotal.value = d?.total ?? 0;
312
+ reviewPage.value = d?.page ?? page;
313
+ if (d?.counts) reviewCounts.value = d.counts;
314
+ } catch (err) {
315
+ console.error(err);
316
+ if (requestId === reviewRequestId && page === 1) reviews.value = [];
317
+ } finally {
318
+ if (requestId === reviewRequestId) isLoadingReviews.value = false;
319
+ }
320
+ };
321
+ const loadAll = () => {
322
+ fetchPlaceData();
323
+ if (activeReviewTab.value !== "all") activeReviewTab.value = "all";
324
+ else fetchReviews(1);
325
+ };
326
+ const openMap = () => {
327
+ const ll = placeData.value.lat_lng;
328
+ if (!ll) return;
329
+ window.open(`https://www.google.com/maps?q=${ll.lat},${ll.lng}`, "_blank");
330
+ };
331
+ const sentinel = useTemplateRef("sentinel");
332
+ const isSentinelVisible = useElementVisibility(sentinel);
333
+ watch(
334
+ [isSentinelVisible, hasMoreReviews, isLoadingReviews],
335
+ ([visible, more, loading]) => {
336
+ if (visible && more && !loading) fetchReviews(reviewPage.value + 1);
337
+ }
338
+ );
339
+ watch(activeReviewTab, () => fetchReviews(1));
340
+ watch(() => [props.placeId, props.state], loadAll);
341
+ onMounted(loadAll);
342
+ </script>
@@ -0,0 +1,57 @@
1
+ import type { Category, ContactChannel, OpeningHour, SpecialDay } from "#pukaad-ui/types/models/place";
2
+ export type PreviewPlaceState = "personal" | "office" | "business";
3
+ export interface PreviewPlaceMedia {
4
+ id?: string | number;
5
+ url: string;
6
+ description?: string;
7
+ }
8
+ export interface PreviewPlaceData {
9
+ id?: string;
10
+ business_name?: string;
11
+ description?: string;
12
+ address?: {
13
+ full_address?: string;
14
+ };
15
+ photos?: PreviewPlaceMedia[];
16
+ videos?: PreviewPlaceMedia[];
17
+ rating?: number;
18
+ review_count?: number;
19
+ like_count?: number;
20
+ share_count?: number;
21
+ categories?: Category[];
22
+ is_review?: boolean;
23
+ open_status?: string;
24
+ lat_lng?: {
25
+ lat: number;
26
+ lng: number;
27
+ };
28
+ owner_profile_id?: string | null;
29
+ phone?: string;
30
+ contact_channels?: ContactChannel[];
31
+ opening_hours?: OpeningHour[];
32
+ special_days?: SpecialDay[];
33
+ }
34
+ export interface PreviewPlaceRecommendItem {
35
+ id: string | number;
36
+ title: string;
37
+ image: string;
38
+ rating: number;
39
+ review_count: number;
40
+ }
41
+ type __VLS_Props = {
42
+ placeId: string | number;
43
+ /** เลือก endpoint ตามแอปที่เปิดพรีวิว */
44
+ state?: PreviewPlaceState;
45
+ /** "ที่ไหนน่าสนใจ" — ยังไม่มี API ให้แอปส่งมาเอง ไม่ส่ง = ซ่อนการ์ด */
46
+ recommendedPlaces?: PreviewPlaceRecommendItem[];
47
+ };
48
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
49
+ exit: () => any;
50
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
51
+ onExit?: (() => any) | undefined;
52
+ }>, {
53
+ state: PreviewPlaceState;
54
+ recommendedPlaces: PreviewPlaceRecommendItem[];
55
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
56
+ declare const _default: typeof __VLS_export;
57
+ export default _default;
@@ -18,8 +18,8 @@ type __VLS_Slots = {} & {
18
18
  default?: (props: typeof __VLS_1) => any;
19
19
  };
20
20
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
- config: ChartConfig;
22
21
  payload: Record<string, any>;
22
+ config: ChartConfig;
23
23
  indicator: "line" | "dot" | "dashed";
24
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
25
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -18,8 +18,8 @@ type __VLS_Slots = {} & {
18
18
  default?: (props: typeof __VLS_1) => any;
19
19
  };
20
20
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
- config: ChartConfig;
22
21
  payload: Record<string, any>;
22
+ config: ChartConfig;
23
23
  indicator: "line" | "dot" | "dashed";
24
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
25
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -14,7 +14,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
14
14
  dir?: string | undefined | undefined;
15
15
  draggable?: (boolean | "true" | "false") | undefined;
16
16
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
17
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
17
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
18
18
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
19
19
  id?: string | undefined | undefined;
20
20
  inert?: (boolean | "true" | "false") | undefined;
@@ -208,7 +208,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
208
208
  dir?: string | undefined | undefined;
209
209
  draggable?: (boolean | "true" | "false") | undefined;
210
210
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
211
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
211
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
212
212
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
213
213
  id?: string | undefined | undefined;
214
214
  inert?: (boolean | "true" | "false") | undefined;
@@ -14,7 +14,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
14
14
  dir?: string | undefined | undefined;
15
15
  draggable?: (boolean | "true" | "false") | undefined;
16
16
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
17
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
17
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
18
18
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
19
19
  id?: string | undefined | undefined;
20
20
  inert?: (boolean | "true" | "false") | undefined;
@@ -208,7 +208,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
208
208
  dir?: string | undefined | undefined;
209
209
  draggable?: (boolean | "true" | "false") | undefined;
210
210
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
211
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
211
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
212
212
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
213
213
  id?: string | undefined | undefined;
214
214
  inert?: (boolean | "true" | "false") | undefined;
@@ -16,7 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
16
16
  dir?: string | undefined | undefined;
17
17
  draggable?: (boolean | "true" | "false") | undefined;
18
18
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
19
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
19
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
20
20
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
21
21
  id?: string | undefined | undefined;
22
22
  inert?: (boolean | "true" | "false") | undefined;
@@ -212,7 +212,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
212
212
  dir?: string | undefined | undefined;
213
213
  draggable?: (boolean | "true" | "false") | undefined;
214
214
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
215
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
215
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
216
216
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
217
217
  id?: string | undefined | undefined;
218
218
  inert?: (boolean | "true" | "false") | undefined;
@@ -16,7 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
16
16
  dir?: string | undefined | undefined;
17
17
  draggable?: (boolean | "true" | "false") | undefined;
18
18
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
19
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
19
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
20
20
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
21
21
  id?: string | undefined | undefined;
22
22
  inert?: (boolean | "true" | "false") | undefined;
@@ -212,7 +212,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
212
212
  dir?: string | undefined | undefined;
213
213
  draggable?: (boolean | "true" | "false") | undefined;
214
214
  enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
215
- enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
215
+ enterKeyHint?: "search" | "done" | "send" | "next" | "enter" | "go" | "previous" | undefined;
216
216
  hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
217
217
  id?: string | undefined | undefined;
218
218
  inert?: (boolean | "true" | "false") | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.453.0",
3
+ "version": "1.454.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",