pukaad-ui-lib 1.346.0 → 1.348.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.346.0",
4
+ "version": "1.348.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -0,0 +1,33 @@
1
+ export interface NotificationActor {
2
+ id: string;
3
+ name: string;
4
+ avatar: string | null;
5
+ }
6
+ export interface NotificationRef {
7
+ type: string;
8
+ id: string;
9
+ }
10
+ export interface NotificationItem {
11
+ id: string;
12
+ category: string;
13
+ action: string;
14
+ message: string;
15
+ ref?: NotificationRef;
16
+ is_read: boolean;
17
+ actor?: NotificationActor;
18
+ grouped_count?: number;
19
+ created_at: string;
20
+ }
21
+ declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
22
+ type __VLS_ModelProps = {
23
+ modelValue?: typeof modelValue['value'];
24
+ };
25
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
+ select: (item: NotificationItem) => any;
27
+ "update:modelValue": (value: boolean | undefined) => any;
28
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
29
+ onSelect?: ((item: NotificationItem) => any) | undefined;
30
+ "onUpdate:modelValue"?: ((value: boolean | undefined) => any) | undefined;
31
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
+ declare const _default: typeof __VLS_export;
33
+ export default _default;
@@ -0,0 +1,261 @@
1
+ <template>
2
+ <Drawer v-model="modelValue" title="การแจ้งเตือน" class="w-[748px]">
3
+ <div class="flex justify-between items-center mb-[16px]">
4
+ <ShadTabs v-model="selectedTab" class="w-full">
5
+ <div class="flex justify-between items-center w-full">
6
+ <ShadTabsList>
7
+ <ShadTabsTrigger
8
+ v-for="tab in tabs"
9
+ :key="tab.value"
10
+ :value="tab.value"
11
+ class="px-4 py-2"
12
+ >
13
+ <Icon name="lucide:layers" />
14
+ <div class="flex">
15
+ {{ tab.label }}
16
+ <div
17
+ v-if="
18
+ tab.value === 'general' ? hasUnreadGeneral : hasUnreadSystem
19
+ "
20
+ class="bg-destructive rounded-full w-[10px] h-[10px] m-[2px] shrink-0"
21
+ />
22
+ </div>
23
+ </ShadTabsTrigger>
24
+ </ShadTabsList>
25
+
26
+ <div
27
+ v-if="notifications.length > 0"
28
+ class="text-primary font-body-medium cursor-pointer"
29
+ @click="markAllRead"
30
+ >
31
+ อ่านทั้งหมด
32
+ </div>
33
+ </div>
34
+
35
+ <ShadTabsContent value="general" class="flex flex-col gap-[8px]">
36
+ <template v-if="isLoading && notifications.length === 0">
37
+ <div class="flex justify-center py-[40px]">
38
+ <Icon
39
+ name="lucide:loader-2"
40
+ size="24"
41
+ class="animate-spin text-gray"
42
+ />
43
+ </div>
44
+ </template>
45
+
46
+ <template v-else-if="notifications.length === 0">
47
+ <div
48
+ class="flex flex-col items-center justify-center py-[40px] gap-[8px]"
49
+ >
50
+ <Icon name="lucide:bell-off" size="40" class="text-gray" />
51
+ <div class="font-body-medium text-gray">ยังไม่มีการแจ้งเตือน</div>
52
+ </div>
53
+ </template>
54
+
55
+ <template v-else>
56
+ <Card
57
+ v-for="item in notifications"
58
+ :key="item.id"
59
+ variant="outline"
60
+ class="cursor-pointer transition-colors"
61
+ :class="{ 'bg-bright': !item.is_read }"
62
+ @click="onClickNotification(item)"
63
+ >
64
+ <div class="flex gap-[8px] items-center">
65
+ <div class="flex gap-[8px] items-center w-full">
66
+ <Avatar :size="30" :src="item.actor?.avatar ?? void 0" />
67
+ <div class="flex flex-col gap-[4px]">
68
+ <div class="font-body-medium">
69
+ <span
70
+ v-if="item.actor"
71
+ class="font-body-medium-prominent"
72
+ >
73
+ {{ item.actor.name }}
74
+ </span>
75
+ {{ item.message }}
76
+ <span
77
+ v-if="item.grouped_count && item.grouped_count > 1"
78
+ class="text-gray font-body-small"
79
+ >
80
+ ({{ item.grouped_count }} คน)
81
+ </span>
82
+ </div>
83
+ <div class="text-gray font-body-small">
84
+ {{ formatRelativeTime(item.created_at) }}
85
+ </div>
86
+ </div>
87
+ </div>
88
+ <div
89
+ v-if="!item.is_read"
90
+ class="bg-primary rounded-full w-[10px] h-[10px] shrink-0"
91
+ />
92
+ </div>
93
+ </Card>
94
+ </template>
95
+ </ShadTabsContent>
96
+
97
+ <ShadTabsContent value="system" class="flex flex-col gap-[8px]">
98
+ <template v-if="isLoading && notifications.length === 0">
99
+ <div class="flex justify-center py-[40px]">
100
+ <Icon
101
+ name="lucide:loader-2"
102
+ size="24"
103
+ class="animate-spin text-gray"
104
+ />
105
+ </div>
106
+ </template>
107
+
108
+ <template v-else-if="notifications.length === 0">
109
+ <div
110
+ class="flex flex-col items-center justify-center py-[40px] gap-[8px]"
111
+ >
112
+ <Icon name="lucide:bell-off" size="40" class="text-gray" />
113
+ <div class="font-body-medium text-gray">ยังไม่มีการแจ้งเตือนระบบ</div>
114
+ </div>
115
+ </template>
116
+
117
+ <template v-else>
118
+ <Card
119
+ v-for="item in notifications"
120
+ :key="item.id"
121
+ variant="outline"
122
+ class="cursor-pointer transition-colors"
123
+ :class="{ 'bg-bright': !item.is_read }"
124
+ @click="onClickNotification(item)"
125
+ >
126
+ <div class="flex gap-[8px] items-center">
127
+ <div class="flex gap-[8px] items-center w-full">
128
+ <Icon
129
+ name="pukaad:memsg-square"
130
+ class="w-[40px] h-[40px] shrink-0"
131
+ />
132
+ <div class="flex flex-col gap-[4px]">
133
+ <div class="font-body-medium">{{ item.message }}</div>
134
+ <div class="text-gray font-body-small">
135
+ {{ formatRelativeTime(item.created_at) }}
136
+ </div>
137
+ </div>
138
+ </div>
139
+ <div
140
+ v-if="!item.is_read"
141
+ class="bg-primary rounded-full w-[10px] h-[10px] shrink-0"
142
+ />
143
+ </div>
144
+ </Card>
145
+ </template>
146
+ </ShadTabsContent>
147
+ </ShadTabs>
148
+ </div>
149
+ </Drawer>
150
+ </template>
151
+
152
+ <script setup>
153
+ import { ref, computed, watch } from "vue";
154
+ import { useApi } from "../../composables/useApi";
155
+ import { useNotificationStore } from "../../composables/store-notification";
156
+ const api = useApi();
157
+ const modelValue = defineModel({ type: Boolean });
158
+ const notificationStore = useNotificationStore();
159
+ const emit = defineEmits(["select"]);
160
+ const selectedTab = ref("general");
161
+ const tabs = [
162
+ { value: "general", label: "\u0E17\u0E31\u0E48\u0E27\u0E44\u0E1B" },
163
+ { value: "system", label: "\u0E23\u0E30\u0E1A\u0E1A" }
164
+ ];
165
+ const generalNotifications = ref([]);
166
+ const systemNotifications = ref([]);
167
+ const isLoading = ref(false);
168
+ const notifications = computed(
169
+ () => selectedTab.value === "system" ? systemNotifications.value : generalNotifications.value
170
+ );
171
+ const hasUnreadGeneral = computed(
172
+ () => generalNotifications.value.some((n) => !n.is_read)
173
+ );
174
+ const hasUnreadSystem = computed(
175
+ () => systemNotifications.value.some((n) => !n.is_read)
176
+ );
177
+ const fetchNotifications = async () => {
178
+ try {
179
+ isLoading.value = true;
180
+ if (selectedTab.value === "system") {
181
+ const response = await api(`/notifications`, {
182
+ params: { page: 1, page_size: 50, category: "system" }
183
+ });
184
+ systemNotifications.value = response.data;
185
+ } else {
186
+ const response = await api(`/notifications`, {
187
+ params: { page: 1, page_size: 50 }
188
+ });
189
+ generalNotifications.value = response.data.filter(
190
+ (n) => n.category !== "system"
191
+ );
192
+ }
193
+ } catch (err) {
194
+ console.error("Fetch notifications error:", err);
195
+ } finally {
196
+ isLoading.value = false;
197
+ }
198
+ };
199
+ const markAsRead = async (item) => {
200
+ if (item.is_read) return;
201
+ try {
202
+ await api(`/notifications/${item.id}/read`, { method: "PATCH" });
203
+ item.is_read = true;
204
+ const totalUnread = generalNotifications.value.filter((n) => !n.is_read).length + systemNotifications.value.filter((n) => !n.is_read).length;
205
+ notificationStore.setUnreadCount(totalUnread);
206
+ } catch {
207
+ }
208
+ };
209
+ const markAllRead = async () => {
210
+ try {
211
+ await api(`/notifications/read-all`, { method: "PATCH" });
212
+ generalNotifications.value.forEach((n) => n.is_read = true);
213
+ systemNotifications.value.forEach((n) => n.is_read = true);
214
+ notificationStore.setUnreadCount(0);
215
+ } catch {
216
+ }
217
+ };
218
+ const onClickNotification = (item) => {
219
+ markAsRead(item);
220
+ emit("select", item);
221
+ };
222
+ const formatRelativeTime = (dateStr) => {
223
+ const now = /* @__PURE__ */ new Date();
224
+ const date = new Date(dateStr);
225
+ const diffMs = now.getTime() - date.getTime();
226
+ const diffSec = Math.floor(diffMs / 1e3);
227
+ const diffMin = Math.floor(diffSec / 60);
228
+ const diffHour = Math.floor(diffMin / 60);
229
+ const diffDay = Math.floor(diffHour / 24);
230
+ if (diffSec < 60) return "\u0E40\u0E21\u0E37\u0E48\u0E2D\u0E0B\u0E31\u0E01\u0E04\u0E23\u0E39\u0E48";
231
+ if (diffMin < 60) return `${diffMin} \u0E19\u0E32\u0E17\u0E35\u0E17\u0E35\u0E48\u0E41\u0E25\u0E49\u0E27`;
232
+ if (diffHour < 24) return `${diffHour} \u0E0A\u0E31\u0E48\u0E27\u0E42\u0E21\u0E07\u0E17\u0E35\u0E48\u0E41\u0E25\u0E49\u0E27`;
233
+ if (diffDay < 7) return `${diffDay} \u0E27\u0E31\u0E19\u0E17\u0E35\u0E48\u0E41\u0E25\u0E49\u0E27`;
234
+ return date.toLocaleDateString("th-TH", {
235
+ day: "numeric",
236
+ month: "short",
237
+ year: "numeric"
238
+ });
239
+ };
240
+ watch(
241
+ () => notificationStore.isOpenDrawer,
242
+ (val) => {
243
+ if (val) {
244
+ fetchNotifications();
245
+ }
246
+ }
247
+ );
248
+ watch(selectedTab, () => {
249
+ if (notificationStore.isOpenDrawer) {
250
+ fetchNotifications();
251
+ }
252
+ });
253
+ watch(
254
+ () => notificationStore.unreadCount,
255
+ () => {
256
+ if (notificationStore.isOpenDrawer) {
257
+ fetchNotifications();
258
+ }
259
+ }
260
+ );
261
+ </script>
@@ -0,0 +1,33 @@
1
+ export interface NotificationActor {
2
+ id: string;
3
+ name: string;
4
+ avatar: string | null;
5
+ }
6
+ export interface NotificationRef {
7
+ type: string;
8
+ id: string;
9
+ }
10
+ export interface NotificationItem {
11
+ id: string;
12
+ category: string;
13
+ action: string;
14
+ message: string;
15
+ ref?: NotificationRef;
16
+ is_read: boolean;
17
+ actor?: NotificationActor;
18
+ grouped_count?: number;
19
+ created_at: string;
20
+ }
21
+ declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
22
+ type __VLS_ModelProps = {
23
+ modelValue?: typeof modelValue['value'];
24
+ };
25
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
+ select: (item: NotificationItem) => any;
27
+ "update:modelValue": (value: boolean | undefined) => any;
28
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
29
+ onSelect?: ((item: NotificationItem) => any) | undefined;
30
+ "onUpdate:modelValue"?: ((value: boolean | undefined) => any) | undefined;
31
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
+ declare const _default: typeof __VLS_export;
33
+ export default _default;
@@ -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" | "enter" | "go" | "previous" | "send" | undefined;
17
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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" | "enter" | "go" | "previous" | "send" | undefined;
211
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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" | "enter" | "go" | "previous" | "send" | undefined;
17
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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" | "enter" | "go" | "previous" | "send" | undefined;
211
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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" | "enter" | "go" | "previous" | "send" | undefined;
19
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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" | "enter" | "go" | "previous" | "send" | undefined;
215
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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" | "enter" | "go" | "previous" | "send" | undefined;
19
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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" | "enter" | "go" | "previous" | "send" | undefined;
215
+ enterKeyHint?: "search" | "done" | "next" | "send" | "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;
@@ -0,0 +1,31 @@
1
+ export declare const useNotificationStore: import("pinia").StoreDefinition<"notification", Pick<{
2
+ unreadCount: import("vue").Ref<number, number>;
3
+ isOpenDrawer: import("vue").Ref<boolean, boolean>;
4
+ fetchUnreadCount: () => Promise<void>;
5
+ initRealtime: () => void;
6
+ closeRealtime: () => void;
7
+ setUnreadCount: (count: number) => void;
8
+ openDrawer: () => void;
9
+ closeDrawer: () => void;
10
+ toggleDrawer: () => void;
11
+ }, "unreadCount" | "isOpenDrawer">, Pick<{
12
+ unreadCount: import("vue").Ref<number, number>;
13
+ isOpenDrawer: import("vue").Ref<boolean, boolean>;
14
+ fetchUnreadCount: () => Promise<void>;
15
+ initRealtime: () => void;
16
+ closeRealtime: () => void;
17
+ setUnreadCount: (count: number) => void;
18
+ openDrawer: () => void;
19
+ closeDrawer: () => void;
20
+ toggleDrawer: () => void;
21
+ }, never>, Pick<{
22
+ unreadCount: import("vue").Ref<number, number>;
23
+ isOpenDrawer: import("vue").Ref<boolean, boolean>;
24
+ fetchUnreadCount: () => Promise<void>;
25
+ initRealtime: () => void;
26
+ closeRealtime: () => void;
27
+ setUnreadCount: (count: number) => void;
28
+ openDrawer: () => void;
29
+ closeDrawer: () => void;
30
+ toggleDrawer: () => void;
31
+ }, "fetchUnreadCount" | "initRealtime" | "closeRealtime" | "setUnreadCount" | "openDrawer" | "closeDrawer" | "toggleDrawer">>;
@@ -0,0 +1,61 @@
1
+ import { defineStore } from "pinia";
2
+ import { ref } from "vue";
3
+ import { useApi } from "./useApi.js";
4
+ import { useRealtime } from "./useRealtime.js";
5
+ export const useNotificationStore = defineStore("notification", () => {
6
+ const api = useApi();
7
+ const { onEvent, offEvent } = useRealtime();
8
+ const unreadCount = ref(0);
9
+ const isOpenDrawer = ref(false);
10
+ const fetchUnreadCount = async () => {
11
+ try {
12
+ const response = await api(
13
+ `/notifications/unread-count`
14
+ );
15
+ unreadCount.value = response.data.count;
16
+ } catch (err) {
17
+ console.error("Failed to fetch unread count", err);
18
+ }
19
+ };
20
+ const onNotificationEvent = (_payload) => {
21
+ const payload = _payload;
22
+ if (typeof payload.unread_count === "number") {
23
+ unreadCount.value = payload.unread_count;
24
+ return;
25
+ }
26
+ if (payload?.action === "delete") {
27
+ unreadCount.value = Math.max(0, unreadCount.value - 1);
28
+ return;
29
+ }
30
+ unreadCount.value++;
31
+ };
32
+ const initRealtime = () => {
33
+ onEvent("notification", onNotificationEvent);
34
+ };
35
+ const closeRealtime = () => {
36
+ offEvent("notification", onNotificationEvent);
37
+ };
38
+ const setUnreadCount = (count) => {
39
+ unreadCount.value = count;
40
+ };
41
+ const openDrawer = () => {
42
+ isOpenDrawer.value = true;
43
+ };
44
+ const closeDrawer = () => {
45
+ isOpenDrawer.value = false;
46
+ };
47
+ const toggleDrawer = () => {
48
+ isOpenDrawer.value = !isOpenDrawer.value;
49
+ };
50
+ return {
51
+ unreadCount,
52
+ isOpenDrawer,
53
+ fetchUnreadCount,
54
+ initRealtime,
55
+ closeRealtime,
56
+ setUnreadCount,
57
+ openDrawer,
58
+ closeDrawer,
59
+ toggleDrawer
60
+ };
61
+ });
@@ -0,0 +1,10 @@
1
+ type EventCallback = (payload: unknown) => void;
2
+ export declare const useRealtime: () => {
3
+ connect: () => void;
4
+ disconnect: () => void;
5
+ send: (event: string, payload: unknown) => void;
6
+ onEvent: (event: string, callback: EventCallback) => void;
7
+ offEvent: (event: string, callback: EventCallback) => void;
8
+ isConnected: import("vue").Ref<boolean, boolean>;
9
+ };
10
+ export {};
@@ -0,0 +1,127 @@
1
+ import { ref, getCurrentInstance, onBeforeUnmount } from "vue";
2
+ import { useRuntimeConfig } from "nuxt/app";
3
+ import { createAuthenticatedWebSocket } from "../utils/websocket.js";
4
+ const socket = ref(null);
5
+ const isConnected = ref(false);
6
+ const reconnectAttempts = ref(0);
7
+ const maxReconnectDelay = 3e4;
8
+ const baseReconnectDelay = 1e3;
9
+ let reconnectTimeout = null;
10
+ let isManuallyClosed = false;
11
+ const eventListeners = /* @__PURE__ */ new Map();
12
+ export const useRealtime = () => {
13
+ const { BASE_URL_API } = useRuntimeConfig().public;
14
+ const onEvent = (event, callback) => {
15
+ if (!eventListeners.has(event)) {
16
+ eventListeners.set(event, /* @__PURE__ */ new Set());
17
+ }
18
+ eventListeners.get(event).add(callback);
19
+ };
20
+ const offEvent = (event, callback) => {
21
+ eventListeners.get(event)?.delete(callback);
22
+ };
23
+ const getWsUrl = () => {
24
+ let url = BASE_URL_API;
25
+ if (typeof window !== "undefined" && window.location.hostname !== "localhost") {
26
+ url = url.replace("localhost", window.location.hostname).replace("127.0.0.1", window.location.hostname);
27
+ }
28
+ if (url.startsWith("http://")) {
29
+ url = url.replace("http://", "ws://");
30
+ } else if (url.startsWith("https://")) {
31
+ url = url.replace("https://", "wss://");
32
+ }
33
+ return `${url}/ws`;
34
+ };
35
+ const connect = () => {
36
+ if (socket.value?.readyState === WebSocket.OPEN || socket.value?.readyState === WebSocket.CONNECTING) {
37
+ return;
38
+ }
39
+ isManuallyClosed = false;
40
+ const wsUrl = getWsUrl();
41
+ console.log("Connecting to WS:", wsUrl);
42
+ const ws = createAuthenticatedWebSocket(wsUrl);
43
+ if (!ws) {
44
+ return;
45
+ }
46
+ socket.value = ws;
47
+ socket.value.onopen = () => {
48
+ console.log("WS Connected");
49
+ isConnected.value = true;
50
+ reconnectAttempts.value = 0;
51
+ };
52
+ socket.value.onmessage = (event) => {
53
+ try {
54
+ const data = JSON.parse(event.data);
55
+ console.log("WS Event:", data);
56
+ handleEvent(data);
57
+ } catch (e) {
58
+ console.error("WS Parse Error:", e);
59
+ }
60
+ };
61
+ socket.value.onclose = () => {
62
+ console.log("WS Closed");
63
+ isConnected.value = false;
64
+ socket.value = null;
65
+ if (!isManuallyClosed) {
66
+ attemptReconnect();
67
+ }
68
+ };
69
+ socket.value.onerror = (error) => {
70
+ console.error("WS Error:", error);
71
+ };
72
+ };
73
+ const attemptReconnect = () => {
74
+ if (reconnectTimeout) clearTimeout(reconnectTimeout);
75
+ const delay = Math.min(
76
+ baseReconnectDelay * Math.pow(2, reconnectAttempts.value),
77
+ maxReconnectDelay
78
+ );
79
+ const jitter = Math.random() * 1e3;
80
+ console.log(
81
+ `Reconnecting in ${delay + jitter}ms... (Attempt ${reconnectAttempts.value + 1})`
82
+ );
83
+ reconnectTimeout = setTimeout(() => {
84
+ reconnectAttempts.value++;
85
+ connect();
86
+ }, delay + jitter);
87
+ };
88
+ const disconnect = () => {
89
+ isManuallyClosed = true;
90
+ if (socket.value) {
91
+ socket.value.close();
92
+ socket.value = null;
93
+ }
94
+ if (reconnectTimeout) {
95
+ clearTimeout(reconnectTimeout);
96
+ reconnectTimeout = null;
97
+ }
98
+ isConnected.value = false;
99
+ };
100
+ const send = (event, payload) => {
101
+ if (socket.value?.readyState === WebSocket.OPEN) {
102
+ socket.value.send(JSON.stringify({ event, payload }));
103
+ } else {
104
+ console.warn("WS: Cannot send, socket is not connected.");
105
+ }
106
+ };
107
+ const handleEvent = (data) => {
108
+ const { event, payload } = data;
109
+ const listeners = eventListeners.get(event);
110
+ if (listeners) {
111
+ listeners.forEach((cb) => cb(payload));
112
+ }
113
+ };
114
+ if (getCurrentInstance()) {
115
+ onBeforeUnmount(() => {
116
+ disconnect();
117
+ });
118
+ }
119
+ return {
120
+ connect,
121
+ disconnect,
122
+ send,
123
+ onEvent,
124
+ offEvent,
125
+ isConnected
126
+ };
127
+ };
@@ -0,0 +1 @@
1
+ export declare const createAuthenticatedWebSocket: (url: string) => WebSocket | null;
@@ -0,0 +1,15 @@
1
+ import { useRuntimeConfig, useCookie } from "nuxt/app";
2
+ export const createAuthenticatedWebSocket = (url) => {
3
+ const config = useRuntimeConfig();
4
+ const { APP_TYPE } = config.public;
5
+ const secId = useCookie(APP_TYPE === "OFFICE" ? "OFFICE_SEC_ID" : "SEC_ID");
6
+ if (!secId.value) {
7
+ console.warn(
8
+ "WS: No auth token found, cannot create authenticated connection."
9
+ );
10
+ return null;
11
+ }
12
+ const wsUrl = new URL(url);
13
+ wsUrl.searchParams.append("token", secId.value);
14
+ return new WebSocket(wsUrl.toString());
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.346.0",
3
+ "version": "1.348.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",