pukaad-ui-lib 1.243.0 → 1.245.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.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.243.0",
4
+ "version": "1.245.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,40 +1,19 @@
1
1
  export interface User {
2
- id: string;
3
- profile_image: {
4
- image_url: string;
5
- };
6
- profile_name: string;
7
- profile_verified?: boolean;
8
- profile_followers?: number;
9
- profile_path: {
10
- path_name: string;
11
- };
12
- profile_posts: number;
13
- profile_bio: string | null;
2
+ id?: string;
3
+ avatar?: string;
4
+ name?: string;
5
+ follower_count?: number;
6
+ public_count?: number;
7
+ description?: string;
14
8
  is_following?: boolean;
15
- is_blocked?: boolean;
16
9
  }
17
- type __VLS_Props = {
18
- disabledPaddingHorizontal?: boolean;
19
- hiddenInfo?: boolean;
20
- variant?: "normal" | "search";
21
- ellipsis?: boolean;
22
- showBlockOption?: boolean;
23
- };
24
10
  type __VLS_ModelProps = {
25
- modelValue: User;
11
+ modelValue?: User;
26
12
  };
27
- type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
28
- declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
29
14
  "update:modelValue": (value: User) => any;
30
- }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
31
16
  "onUpdate:modelValue"?: ((value: User) => any) | undefined;
32
- }>, {
33
- variant: "normal" | "search";
34
- ellipsis: boolean;
35
- disabledPaddingHorizontal: boolean;
36
- hiddenInfo: boolean;
37
- showBlockOption: boolean;
38
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
39
18
  declare const _default: typeof __VLS_export;
40
19
  export default _default;
@@ -1,156 +1,79 @@
1
1
  <template>
2
- <div
3
- :class="[
4
- 'flex gap-[16px] items-center border-b border-mercury w-full',
5
- props.disabledPaddingHorizontal ? 'py-[16px]' : 'p-[16px]'
6
- ]"
7
- >
8
- <div class="flex w-full gap-[16px]">
9
- <!-- <Avatar
10
- :src="user.profile_image.image_url"
11
- size="48"
12
- class="cursor-pointer"
13
- /> -->
2
+ <div class="flex gap-[16px] items-center justify-between cursor-pointer transition-colors" @click="onClickCard">
3
+ <div class="flex gap-[8px]">
4
+ <Avatar :src="modelValue?.avatar" :alt="modelValue?.name" :size="40" />
14
5
  <div class="flex flex-col gap-[4px]">
15
- <div class="flex gap-[4px] items-center">
16
- <div
17
- :class="[
18
- props.hiddenInfo ? 'font-body-large-prominent' : 'font-body-small-prominent'
19
- ]"
20
- >
21
- {{ user.profile_name }}
22
- </div>
23
- <Icon
24
- v-if="user.profile_verified === true"
25
- name="pukaad-verify"
26
- :size="props.hiddenInfo ? 20 : 16"
27
- />
28
- </div>
29
- <div
30
- v-if="!props.hiddenInfo"
31
- :class="[
32
- 'flex gap-[4px] text-gray items-center',
33
- props.variant === 'search' ? 'font-body-large' : 'font-body-small'
34
- ]"
35
- >
36
- <div>ผู้ติดตาม {{ user.profile_followers }}</div>
37
- <div>•</div>
38
- <div>กำลังเผยแพร่ {{ user.profile_posts }}</div>
6
+ <div class="font-body-large-prominent flex items-center gap-[4px]">
7
+ {{ modelValue?.name }}
39
8
  </div>
40
- <div
41
- v-if="!props.hiddenInfo"
42
- :class="[
43
- 'text-gray',
44
- props.variant === 'search' ? 'font-body-large' : 'font-body-small'
45
- ]"
46
- >
47
- {{ user.profile_bio }}
9
+ <div class="flex flex-col gap-[4px] font-body-small text-gray">
10
+ <div class="flex items-center gap-[4px]">
11
+ <div>ผู้ติดตาม {{ $convert.convertNumber(modelValue?.follower_count || 0) }}</div>
12
+ <div>•</div>
13
+ <div>กำลังเผยแพร่ {{ $convert.convertNumber(modelValue?.public_count || 0) }}</div>
14
+ </div>
15
+ <div v-if="modelValue?.description" class="line-clamp-2">
16
+ {{ modelValue?.description }}
17
+ </div>
48
18
  </div>
49
19
  </div>
50
20
  </div>
51
-
52
- <Button
53
- :color="buttonState.variant"
54
- :variant="buttonState.type"
55
- @click="buttonState.action"
56
- >
57
- {{ buttonState.text }}
21
+ <Button :color="!modelValue?.is_following ? 'primary' : void 0" variant="outline" @click.stop="onFollowToggle">
22
+ {{ modelValue?.is_following ? "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E15\u0E34\u0E14\u0E15\u0E32\u0E21" : "\u0E15\u0E34\u0E14\u0E15\u0E32\u0E21" }}
58
23
  </Button>
59
-
60
- <PickerOptionMenuUser
61
- v-if="ellipsis"
62
- state="follower"
63
- circle
64
- variant="text"
65
- />
66
24
  </div>
67
25
  </template>
68
26
 
69
27
  <script setup>
70
- import { computed, ref } from "vue";
71
- import { useNuxtApp } from "nuxt/app";
72
- const { $alert } = useNuxtApp();
73
- const props = defineProps({
74
- disabledPaddingHorizontal: { type: Boolean, required: false, default: false },
75
- hiddenInfo: { type: Boolean, required: false, default: false },
76
- variant: { type: String, required: false, default: "normal" },
77
- ellipsis: { type: Boolean, required: false, default: false },
78
- showBlockOption: { type: Boolean, required: false, default: false }
79
- });
80
- const user = defineModel({ type: Object, ...{ required: true } });
81
- const buttonState = computed(() => {
82
- if (user.value.is_blocked) {
83
- return {
84
- text: "\u0E40\u0E25\u0E34\u0E01\u0E1A\u0E25\u0E47\u0E2D\u0E01",
85
- variant: "default",
86
- type: "solid",
87
- action: toggleBlock
88
- };
89
- } else if (user.value.is_following) {
90
- return {
91
- text: "\u0E15\u0E34\u0E14\u0E15\u0E32\u0E21\u0E41\u0E25\u0E49\u0E27",
92
- variant: "default",
93
- type: "solid",
94
- action: toggleFollow
95
- };
96
- } else if (showBlockOption.value) {
97
- return {
98
- text: "\u0E1A\u0E25\u0E47\u0E2D\u0E01",
99
- variant: "error",
100
- type: "solid",
101
- action: toggleBlock
102
- };
103
- } else {
104
- return {
105
- text: "\u0E15\u0E34\u0E14\u0E15\u0E32\u0E21",
106
- variant: "primary",
107
- type: "flat-outline",
108
- action: toggleFollow
109
- };
28
+ import { ref } from "vue";
29
+ import { useRouter } from "vue-router";
30
+ import { navigateTo, useCookie, useRuntimeConfig } from "nuxt/app";
31
+ import { useApi } from "../../composables/useApi";
32
+ const modelValue = defineModel({ type: Object, ...{
33
+ default: () => ({})
34
+ } });
35
+ const router = useRouter();
36
+ const onClickCard = () => {
37
+ if (modelValue.value?.id) {
38
+ router.push(`/@${modelValue.value.id}`);
110
39
  }
111
- });
112
- const toggleFollow = () => {
113
- user.value = {
114
- ...user.value,
115
- is_following: !user.value.is_following
116
- };
117
40
  };
118
- const showBlockOption = ref(props.showBlockOption);
119
- const toggleBlock = () => {
120
- if (user.value.is_blocked) {
121
- $alert.show({
122
- type: "warning",
123
- title: "\u0E40\u0E25\u0E34\u0E01\u0E1A\u0E25\u0E47\u0E2D\u0E04",
124
- text: `\u0E04\u0E38\u0E13\u0E41\u0E19\u0E48\u0E43\u0E08\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48\u0E27\u0E48\u0E32\u0E04\u0E38\u0E13\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E40\u0E25\u0E34\u0E01\u0E1A\u0E25\u0E47\u0E2D\u0E01 ${user.value.profile_name} \u0E43\u0E0A\u0E48\u0E21\u0E31\u0E49\u0E22?`,
125
- confirmText: "\u0E40\u0E25\u0E34\u0E01\u0E1A\u0E25\u0E47\u0E2D\u0E04",
126
- showCancelBtn: true,
127
- cancelText: "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01"
128
- }).then((result) => {
129
- if (result) {
130
- user.value = {
131
- ...user.value,
132
- is_blocked: false
133
- };
134
- showBlockOption.value = true;
135
- }
136
- });
137
- } else {
138
- $alert.show({
139
- type: "warning",
140
- title: "\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E1A\u0E25\u0E47\u0E2D\u0E01\u0E1C\u0E39\u0E49\u0E43\u0E0A\u0E49\u0E19\u0E35\u0E49\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
141
- text: `${user.value.profile_name} \u0E08\u0E30\u0E44\u0E21\u0E48\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E42\u0E1E\u0E2A\u0E15\u0E4C\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13 \u0E2A\u0E48\u0E07\u0E02\u0E49\u0E2D\u0E04\u0E27\u0E32\u0E21\u0E16\u0E36\u0E07\u0E04\u0E38\u0E13 \u0E21\u0E2D\u0E07\u0E40\u0E2B\u0E47\u0E19\u0E04\u0E27\u0E32\u0E21\u0E04\u0E34\u0E14\u0E40\u0E2B\u0E47\u0E19\u0E2B\u0E23\u0E37\u0E2D\u0E23\u0E35\u0E27\u0E34\u0E27\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13 \u0E41\u0E25\u0E30\u0E08\u0E30\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01\u0E01\u0E32\u0E23\u0E15\u0E34\u0E14\u0E15\u0E32\u0E21\u0E14\u0E49\u0E27\u0E22`,
142
- confirmText: "\u0E1A\u0E25\u0E47\u0E2D\u0E01",
143
- showCancelBtn: true,
144
- cancelText: "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01"
145
- }).then((result) => {
146
- if (result) {
147
- user.value = {
148
- ...user.value,
149
- is_following: false,
150
- is_blocked: true
151
- };
41
+ const api = useApi();
42
+ const config = useRuntimeConfig();
43
+ const isLoading = ref(false);
44
+ const onFollowToggle = async () => {
45
+ const { APP_TYPE } = config.public;
46
+ const secId = useCookie(APP_TYPE === "OFFICE" ? "OFFICE_SEC_ID" : "SEC_ID");
47
+ if (!secId.value) {
48
+ navigateTo("/auth/login");
49
+ return;
50
+ }
51
+ if (!modelValue.value?.id) return;
52
+ if (isLoading.value) return;
53
+ const targetId = modelValue.value.id;
54
+ const currentFollowing = !!modelValue.value.is_following;
55
+ modelValue.value.is_following = !currentFollowing;
56
+ if (modelValue.value.follower_count !== void 0) {
57
+ modelValue.value.follower_count += !currentFollowing ? 1 : -1;
58
+ }
59
+ isLoading.value = true;
60
+ try {
61
+ const method = currentFollowing ? "DELETE" : "POST";
62
+ const response = await api(`/profiles/${targetId}/follow`, { method });
63
+ if (response.data) {
64
+ modelValue.value.is_following = response.data.is_following;
65
+ if (modelValue.value.follower_count !== void 0) {
66
+ modelValue.value.follower_count = response.data.follower_count;
152
67
  }
153
- });
68
+ }
69
+ } catch (error) {
70
+ modelValue.value.is_following = currentFollowing;
71
+ if (modelValue.value.follower_count !== void 0) {
72
+ modelValue.value.follower_count += currentFollowing ? 1 : -1;
73
+ }
74
+ console.error("Follow toggle failed:", error);
75
+ } finally {
76
+ isLoading.value = false;
154
77
  }
155
78
  };
156
79
  </script>
@@ -1,40 +1,19 @@
1
1
  export interface User {
2
- id: string;
3
- profile_image: {
4
- image_url: string;
5
- };
6
- profile_name: string;
7
- profile_verified?: boolean;
8
- profile_followers?: number;
9
- profile_path: {
10
- path_name: string;
11
- };
12
- profile_posts: number;
13
- profile_bio: string | null;
2
+ id?: string;
3
+ avatar?: string;
4
+ name?: string;
5
+ follower_count?: number;
6
+ public_count?: number;
7
+ description?: string;
14
8
  is_following?: boolean;
15
- is_blocked?: boolean;
16
9
  }
17
- type __VLS_Props = {
18
- disabledPaddingHorizontal?: boolean;
19
- hiddenInfo?: boolean;
20
- variant?: "normal" | "search";
21
- ellipsis?: boolean;
22
- showBlockOption?: boolean;
23
- };
24
10
  type __VLS_ModelProps = {
25
- modelValue: User;
11
+ modelValue?: User;
26
12
  };
27
- type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
28
- declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
29
14
  "update:modelValue": (value: User) => any;
30
- }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
31
16
  "onUpdate:modelValue"?: ((value: User) => any) | undefined;
32
- }>, {
33
- variant: "normal" | "search";
34
- ellipsis: boolean;
35
- disabledPaddingHorizontal: boolean;
36
- hiddenInfo: boolean;
37
- showBlockOption: boolean;
38
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
39
18
  declare const _default: typeof __VLS_export;
40
19
  export default _default;
@@ -0,0 +1,30 @@
1
+ export interface ProfileSummary {
2
+ id?: string;
3
+ name: string;
4
+ avatar: string;
5
+ follower_count: number;
6
+ following_count: number;
7
+ favorite_count?: number;
8
+ }
9
+ export interface DrawerProfileNetworkProps {
10
+ profileId: string;
11
+ defaultTab?: "followers" | "following" | "favorites";
12
+ }
13
+ type __VLS_Props = DrawerProfileNetworkProps;
14
+ type __VLS_ModelProps = {
15
+ modelValue?: boolean;
16
+ };
17
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
18
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ "update:modelValue": (value: boolean) => any;
20
+ } & {
21
+ close: () => any;
22
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
23
+ onClose?: (() => any) | undefined;
24
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
25
+ }>, {
26
+ profileId: string;
27
+ defaultTab: "followers" | "following" | "favorites";
28
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
+ declare const _default: typeof __VLS_export;
30
+ export default _default;
@@ -0,0 +1,268 @@
1
+ <template>
2
+ <Drawer class="w-[748px]" v-model="isOpen" @close="onClose">
3
+ <template #header>
4
+ <div class="flex items-center gap-[16px]">
5
+ <Avatar :src="profileData?.avatar" :size="40" />
6
+ <div class="flex flex-col gap-[4px]">
7
+ <div class="font-body-prominent text-black">
8
+ {{ profileData?.name }}
9
+ </div>
10
+ <div class="font-body-small text-gray items-center">
11
+ @{{ profileData?.id }}
12
+ </div>
13
+ </div>
14
+ </div>
15
+ </template>
16
+
17
+ <div class="flex flex-col h-full">
18
+ <ShadTabs v-model="activeTab" class="flex-1 flex flex-col">
19
+ <ShadTabsList>
20
+ <ShadTabsTrigger value="followers" class="px-[16px] py-[8px]">
21
+ ผู้ติดตาม ({{ $convert.convertNumber(profileData?.follower_count || 0) }})
22
+ </ShadTabsTrigger>
23
+ <ShadTabsTrigger value="following" class="px-[16px] py-[8px]">
24
+ กำลังติดตาม ({{ $convert.convertNumber(profileData?.following_count || 0) }})
25
+ </ShadTabsTrigger>
26
+ <ShadTabsTrigger value="favorites" class="px-[16px] py-[8px]">
27
+ สถานที่โปรด ({{ $convert.convertNumber(profileData?.favorite_count || 0) }})
28
+ </ShadTabsTrigger>
29
+ </ShadTabsList>
30
+
31
+ <div class="flex-1 overflow-y-auto" @scroll="onScroll">
32
+ <!-- Followers -->
33
+ <ShadTabsContent value="followers" class="flex flex-col mt-0">
34
+ <template v-if="tabState.followers.loading">
35
+ <div class="flex justify-center py-[40px]">
36
+ <Icon name="lucide:loader-2" size="24" class="animate-spin text-gray" />
37
+ </div>
38
+ </template>
39
+ <template v-else-if="followers.length > 0">
40
+ <div class="flex flex-col gap-[16px]">
41
+ <template v-for="(user, index) in followers" :key="user.id || index">
42
+ <CardUserItem v-model="followers[index]" />
43
+ <Divider v-if="index !== followers.length - 1" />
44
+ </template>
45
+ </div>
46
+ <div v-if="tabState.followers.loadingMore" class="flex justify-center py-[16px]">
47
+ <Icon name="lucide:loader-2" size="20" class="animate-spin text-gray" />
48
+ </div>
49
+ </template>
50
+ <template v-else>
51
+ <div class="flex flex-col items-center justify-center py-[40px] gap-[8px]">
52
+ <Icon name="lucide:users" size="40" class="text-gray" />
53
+ <div class="font-body-medium text-gray">ไม่มีผู้ติดตาม</div>
54
+ </div>
55
+ </template>
56
+ </ShadTabsContent>
57
+
58
+ <!-- Following -->
59
+ <ShadTabsContent value="following" class="flex flex-col mt-0">
60
+ <template v-if="tabState.following.loading">
61
+ <div class="flex justify-center py-[40px]">
62
+ <Icon name="lucide:loader-2" size="24" class="animate-spin text-gray" />
63
+ </div>
64
+ </template>
65
+ <template v-else-if="following.length > 0">
66
+ <div class="flex flex-col gap-[16px]">
67
+ <template v-for="(user, index) in following" :key="user.id || index">
68
+ <CardUserItem v-model="following[index]" />
69
+ <Divider v-if="index !== following.length - 1" />
70
+ </template>
71
+ </div>
72
+ <div v-if="tabState.following.loadingMore" class="flex justify-center py-[16px]">
73
+ <Icon name="lucide:loader-2" size="20" class="animate-spin text-gray" />
74
+ </div>
75
+ </template>
76
+ <template v-else>
77
+ <div class="flex flex-col items-center justify-center py-[40px] gap-[8px]">
78
+ <Icon name="lucide:users" size="40" class="text-gray" />
79
+ <div class="font-body-medium text-gray">ไม่ได้ติดตามใคร</div>
80
+ </div>
81
+ </template>
82
+ </ShadTabsContent>
83
+
84
+ <!-- Favorites -->
85
+ <ShadTabsContent value="favorites" class="flex flex-col mt-0">
86
+ <template v-if="tabState.favorites.loading">
87
+ <div class="flex justify-center py-[40px]">
88
+ <Icon name="lucide:loader-2" size="24" class="animate-spin text-gray" />
89
+ </div>
90
+ </template>
91
+ <template v-else-if="favorites.length > 0">
92
+ <div class="flex flex-col items-center justify-center p-[48px] text-gray font-body-large">
93
+ สถานที่โปรด (อยู่ระหว่างการพัฒนา)
94
+ </div>
95
+ <div v-if="tabState.favorites.loadingMore" class="flex justify-center py-[16px]">
96
+ <Icon name="lucide:loader-2" size="20" class="animate-spin text-gray" />
97
+ </div>
98
+ </template>
99
+ <template v-else>
100
+ <div class="flex flex-col items-center justify-center py-[40px] gap-[8px]">
101
+ <Icon name="lucide:map-pin-off" size="40" class="text-gray" />
102
+ <div class="font-body-medium text-gray">ไม่มีสถานที่โปรด</div>
103
+ </div>
104
+ </template>
105
+ </ShadTabsContent>
106
+ </div>
107
+ </ShadTabs>
108
+ </div>
109
+ </Drawer>
110
+ </template>
111
+
112
+ <script setup>
113
+ import { ref, reactive, watch } from "vue";
114
+ import { useApi } from "../../composables/useApi";
115
+ const createTabState = () => ({
116
+ loading: false,
117
+ loadingMore: false,
118
+ hasMore: true,
119
+ page: 1
120
+ });
121
+ const props = defineProps({
122
+ profileId: { type: String, required: true, default: "" },
123
+ defaultTab: { type: String, required: false, default: "followers" }
124
+ });
125
+ const emit = defineEmits(["close"]);
126
+ const isOpen = defineModel({ type: Boolean, ...{ default: false } });
127
+ const activeTab = ref(props.defaultTab);
128
+ const profileData = ref(null);
129
+ const api = useApi();
130
+ const followers = ref([]);
131
+ const following = ref([]);
132
+ const favorites = ref([]);
133
+ const tabState = reactive({
134
+ followers: createTabState(),
135
+ following: createTabState(),
136
+ favorites: createTabState()
137
+ });
138
+ const PAGE_SIZE = 20;
139
+ const syncProfile = (profile) => {
140
+ profileData.value = { ...profileData.value, ...profile };
141
+ };
142
+ const fetchFollowers = async (isLoadMore = false) => {
143
+ if (!props.profileId) return;
144
+ const s = tabState.followers;
145
+ if (isLoadMore) {
146
+ if (!s.hasMore || s.loadingMore) return;
147
+ s.loadingMore = true;
148
+ } else {
149
+ s.loading = true;
150
+ s.page = 1;
151
+ }
152
+ try {
153
+ const res = await api(`/profiles/${props.profileId}/followers`, {
154
+ query: { page: s.page, page_size: PAGE_SIZE }
155
+ });
156
+ if (res.meta) s.hasMore = s.page < res.meta.total_pages;
157
+ if (res.data?.profile) syncProfile(res.data.profile);
158
+ const items = res.data?.items || [];
159
+ isLoadMore ? followers.value.push(...items) : followers.value = items;
160
+ } catch (e) {
161
+ console.error("Failed to fetch followers", e);
162
+ } finally {
163
+ s.loading = false;
164
+ s.loadingMore = false;
165
+ }
166
+ };
167
+ const fetchFollowing = async (isLoadMore = false) => {
168
+ if (!props.profileId) return;
169
+ const s = tabState.following;
170
+ if (isLoadMore) {
171
+ if (!s.hasMore || s.loadingMore) return;
172
+ s.loadingMore = true;
173
+ } else {
174
+ s.loading = true;
175
+ s.page = 1;
176
+ }
177
+ try {
178
+ const res = await api(`/profiles/${props.profileId}/following`, {
179
+ query: { page: s.page, page_size: PAGE_SIZE }
180
+ });
181
+ if (res.meta) s.hasMore = s.page < res.meta.total_pages;
182
+ if (res.data?.profile) syncProfile(res.data.profile);
183
+ const items = res.data?.items || [];
184
+ isLoadMore ? following.value.push(...items) : following.value = items;
185
+ } catch (e) {
186
+ console.error("Failed to fetch following", e);
187
+ } finally {
188
+ s.loading = false;
189
+ s.loadingMore = false;
190
+ }
191
+ };
192
+ const fetchFavorites = async (isLoadMore = false) => {
193
+ if (!props.profileId) return;
194
+ const s = tabState.favorites;
195
+ if (isLoadMore) {
196
+ if (!s.hasMore || s.loadingMore) return;
197
+ s.loadingMore = true;
198
+ } else {
199
+ s.loading = true;
200
+ s.page = 1;
201
+ }
202
+ try {
203
+ const res = await api(
204
+ `/profiles/${props.profileId}/favorites`,
205
+ { query: { page: s.page, page_size: PAGE_SIZE } }
206
+ );
207
+ if (res.meta) s.hasMore = s.page < res.meta.total_pages;
208
+ if (res.data?.profile) syncProfile(res.data.profile);
209
+ const items = res.data?.items || [];
210
+ isLoadMore ? favorites.value.push(...items) : favorites.value = items;
211
+ } catch (e) {
212
+ console.error("Failed to fetch favorites", e);
213
+ } finally {
214
+ s.loading = false;
215
+ s.loadingMore = false;
216
+ }
217
+ };
218
+ const resetAll = () => {
219
+ followers.value = [];
220
+ following.value = [];
221
+ favorites.value = [];
222
+ profileData.value = null;
223
+ ["followers", "following", "favorites"].forEach((tab) => {
224
+ Object.assign(tabState[tab], createTabState());
225
+ });
226
+ };
227
+ watch(isOpen, (v) => {
228
+ if (v) {
229
+ activeTab.value = props.defaultTab;
230
+ if (activeTab.value === "followers") fetchFollowers();
231
+ else if (activeTab.value === "following") fetchFollowing();
232
+ else if (activeTab.value === "favorites") fetchFavorites();
233
+ } else {
234
+ resetAll();
235
+ }
236
+ });
237
+ watch(activeTab, (v) => {
238
+ if (v === "followers" && followers.value.length === 0) fetchFollowers();
239
+ else if (v === "following" && following.value.length === 0) fetchFollowing();
240
+ else if (v === "favorites" && favorites.value.length === 0) fetchFavorites();
241
+ });
242
+ const onClose = () => {
243
+ isOpen.value = false;
244
+ emit("close");
245
+ };
246
+ const onScroll = (e) => {
247
+ const target = e.target;
248
+ if (!target) return;
249
+ const isBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 50;
250
+ if (!isBottom) return;
251
+ if (activeTab.value === "followers") {
252
+ if (tabState.followers.hasMore) {
253
+ tabState.followers.page++;
254
+ fetchFollowers(true);
255
+ }
256
+ } else if (activeTab.value === "following") {
257
+ if (tabState.following.hasMore) {
258
+ tabState.following.page++;
259
+ fetchFollowing(true);
260
+ }
261
+ } else if (activeTab.value === "favorites") {
262
+ if (tabState.favorites.hasMore) {
263
+ tabState.favorites.page++;
264
+ fetchFavorites(true);
265
+ }
266
+ }
267
+ };
268
+ </script>
@@ -0,0 +1,30 @@
1
+ export interface ProfileSummary {
2
+ id?: string;
3
+ name: string;
4
+ avatar: string;
5
+ follower_count: number;
6
+ following_count: number;
7
+ favorite_count?: number;
8
+ }
9
+ export interface DrawerProfileNetworkProps {
10
+ profileId: string;
11
+ defaultTab?: "followers" | "following" | "favorites";
12
+ }
13
+ type __VLS_Props = DrawerProfileNetworkProps;
14
+ type __VLS_ModelProps = {
15
+ modelValue?: boolean;
16
+ };
17
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
18
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ "update:modelValue": (value: boolean) => any;
20
+ } & {
21
+ close: () => any;
22
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
23
+ onClose?: (() => any) | undefined;
24
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
25
+ }>, {
26
+ profileId: string;
27
+ defaultTab: "followers" | "following" | "favorites";
28
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
+ declare const _default: typeof __VLS_export;
30
+ export default _default;
@@ -100,11 +100,11 @@ const handleSubmit = async () => {
100
100
  logout_all: logoutAll.value
101
101
  }
102
102
  });
103
- if (res.code === 200 && res.data) {
103
+ if (res.data?.sec_id) {
104
104
  emit("complete", { secId: res.data.sec_id });
105
105
  isOpen.value = false;
106
106
  } else {
107
- $toast?.error?.(res.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14");
107
+ $toast?.error?.(res.message?.description || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14");
108
108
  }
109
109
  } catch (e) {
110
110
  $toast?.error?.(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.243.0",
3
+ "version": "1.245.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",