@localzet/data-connector 1.0.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 (51) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +52 -0
  3. package/dist/api/index.d.ts +4 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +3 -0
  6. package/dist/api/mixIdApi.d.ts +76 -0
  7. package/dist/api/mixIdApi.d.ts.map +1 -0
  8. package/dist/api/mixIdApi.js +275 -0
  9. package/dist/api/offlineQueue.d.ts +24 -0
  10. package/dist/api/offlineQueue.d.ts.map +1 -0
  11. package/dist/api/offlineQueue.js +137 -0
  12. package/dist/api/websocket.d.ts +28 -0
  13. package/dist/api/websocket.d.ts.map +1 -0
  14. package/dist/api/websocket.js +201 -0
  15. package/dist/components/MixIdCallbackPage.d.ts +6 -0
  16. package/dist/components/MixIdCallbackPage.d.ts.map +1 -0
  17. package/dist/components/MixIdCallbackPage.js +38 -0
  18. package/dist/components/MixIdConnection.d.ts +18 -0
  19. package/dist/components/MixIdConnection.d.ts.map +1 -0
  20. package/dist/components/MixIdConnection.js +197 -0
  21. package/dist/components/index.d.ts +5 -0
  22. package/dist/components/index.d.ts.map +1 -0
  23. package/dist/components/index.js +2 -0
  24. package/dist/hooks/index.d.ts +5 -0
  25. package/dist/hooks/index.d.ts.map +1 -0
  26. package/dist/hooks/index.js +4 -0
  27. package/dist/hooks/useMixIdSession.d.ts +19 -0
  28. package/dist/hooks/useMixIdSession.d.ts.map +1 -0
  29. package/dist/hooks/useMixIdSession.js +124 -0
  30. package/dist/hooks/useMixIdStatus.d.ts +9 -0
  31. package/dist/hooks/useMixIdStatus.d.ts.map +1 -0
  32. package/dist/hooks/useMixIdStatus.js +81 -0
  33. package/dist/hooks/useMixIdSync.d.ts +16 -0
  34. package/dist/hooks/useMixIdSync.d.ts.map +1 -0
  35. package/dist/hooks/useMixIdSync.js +263 -0
  36. package/dist/hooks/useNotifications.d.ts +17 -0
  37. package/dist/hooks/useNotifications.d.ts.map +1 -0
  38. package/dist/hooks/useNotifications.js +144 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +8 -0
  42. package/dist/ui/Button.d.ts +5 -0
  43. package/dist/ui/Button.d.ts.map +1 -0
  44. package/dist/ui/Button.js +7 -0
  45. package/dist/ui/Card.d.ts +5 -0
  46. package/dist/ui/Card.d.ts.map +1 -0
  47. package/dist/ui/Card.js +7 -0
  48. package/dist/ui/index.d.ts +3 -0
  49. package/dist/ui/index.d.ts.map +1 -0
  50. package/dist/ui/index.js +2 -0
  51. package/package.json +69 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMixIdStatus.d.ts","sourceRoot":"","sources":["../../src/hooks/useMixIdStatus.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,gBAAgB,GAAG,cAAc,GAAG,UAAU,CAAA;AAE7F,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,OAAO,CAAA;IACpB,UAAU,EAAE,eAAe,CAAA;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,wBAAgB,cAAc,IAAI,oBAAoB,CAwFrD"}
@@ -0,0 +1,81 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { mixIdApi } from '../api/mixIdApi';
3
+ import { wsClient } from '../api/websocket';
4
+ export function useMixIdStatus() {
5
+ const [isConnected, setIsConnected] = useState(false);
6
+ const [syncStatus, setSyncStatus] = useState('checking');
7
+ const [hasConfig, setHasConfig] = useState(false);
8
+ const checkStatus = useCallback(async () => {
9
+ try {
10
+ const config = mixIdApi.getConfig();
11
+ const hasConfigValue = !!(config && config.accessToken);
12
+ setHasConfig(hasConfigValue);
13
+ if (!hasConfigValue) {
14
+ setIsConnected(false);
15
+ setSyncStatus('disconnected');
16
+ return;
17
+ }
18
+ // Check WebSocket connection
19
+ const wsConnected = wsClient.isConnected();
20
+ if (wsConnected) {
21
+ setIsConnected(true);
22
+ setSyncStatus('connected-ws');
23
+ }
24
+ else {
25
+ // Check if we can use REST API (try to get sync status)
26
+ try {
27
+ await mixIdApi.getSyncStatus();
28
+ setIsConnected(true);
29
+ setSyncStatus('connected-rest');
30
+ }
31
+ catch (error) {
32
+ setIsConnected(false);
33
+ setSyncStatus('disconnected');
34
+ }
35
+ }
36
+ }
37
+ catch (error) {
38
+ setIsConnected(false);
39
+ setSyncStatus('disconnected');
40
+ }
41
+ }, []);
42
+ useEffect(() => {
43
+ // Initial check
44
+ checkStatus();
45
+ // Check periodically
46
+ const interval = setInterval(checkStatus, 2000); // Check every 2 seconds
47
+ // Listen to storage changes (for cross-tab updates)
48
+ const handleStorageChange = (e) => {
49
+ if (e.key === 'mixId_config' || e.key === 'mixId_accessToken' || e.key === 'mixId_refreshToken') {
50
+ checkStatus();
51
+ }
52
+ };
53
+ if (typeof window !== 'undefined') {
54
+ window.addEventListener('storage', handleStorageChange);
55
+ // Listen to custom events for same-tab updates
56
+ const handleConfigChange = () => {
57
+ checkStatus();
58
+ };
59
+ const handleWsStatusChange = () => {
60
+ checkStatus();
61
+ };
62
+ window.addEventListener('mixid-config-changed', handleConfigChange);
63
+ window.addEventListener('mixid-ws-status-changed', handleWsStatusChange);
64
+ return () => {
65
+ clearInterval(interval);
66
+ window.removeEventListener('storage', handleStorageChange);
67
+ window.removeEventListener('mixid-config-changed', handleConfigChange);
68
+ window.removeEventListener('mixid-ws-status-changed', handleWsStatusChange);
69
+ };
70
+ }
71
+ return () => {
72
+ clearInterval(interval);
73
+ };
74
+ }, [checkStatus]);
75
+ return {
76
+ isConnected,
77
+ syncStatus,
78
+ hasConfig,
79
+ refresh: checkStatus,
80
+ };
81
+ }
@@ -0,0 +1,16 @@
1
+ export interface UseMixIdSyncOptions {
2
+ dataTypes?: string[];
3
+ onSettingsUpdate?: (settings: any) => void;
4
+ onDataUpdate?: (dataType: string, data: Record<string, any>) => void;
5
+ getLocalSettings?: () => any;
6
+ getLocalData?: (dataType: string) => Promise<Record<string, any>>;
7
+ saveLocalSettings?: (settings: any) => void | Promise<void>;
8
+ saveLocalData?: (dataType: string, data: Record<string, any>) => void | Promise<void>;
9
+ mergeStrategy?: 'remote-wins' | 'local-wins' | 'newer-wins';
10
+ }
11
+ export declare function useMixIdSync(options?: UseMixIdSyncOptions): {
12
+ performSync: () => Promise<void>;
13
+ uploadSettings: (settingsToUpload: any, version?: number) => Promise<void>;
14
+ uploadData: (dataType: string, data: Record<string, any>) => Promise<void>;
15
+ };
16
+ //# sourceMappingURL=useMixIdSync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMixIdSync.d.ts","sourceRoot":"","sources":["../../src/hooks/useMixIdSync.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1C,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,CAAA;IACpE,gBAAgB,CAAC,EAAE,MAAM,GAAG,CAAA;IAC5B,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;IACjE,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3D,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrF,aAAa,CAAC,EAAE,aAAa,GAAG,YAAY,GAAG,YAAY,CAAA;CAC5D;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB;;uCA2CjC,GAAG,YAAY,MAAM;2BA6BA,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;EAkOlF"}
@@ -0,0 +1,263 @@
1
+ import { useEffect, useRef, useCallback } from 'react';
2
+ import { mixIdApi } from '../api/mixIdApi';
3
+ import { wsClient } from '../api/websocket';
4
+ import { offlineQueue } from '../api/offlineQueue';
5
+ const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes (fallback HTTP sync)
6
+ const HEARTBEAT_INTERVAL = 30 * 1000; // 30 seconds
7
+ export function useMixIdSync(options = {}) {
8
+ const { dataTypes = [], onSettingsUpdate, onDataUpdate, getLocalSettings, getLocalData, saveLocalSettings, saveLocalData, mergeStrategy = 'newer-wins', } = options;
9
+ const syncIntervalRef = useRef(null);
10
+ const heartbeatIntervalRef = useRef(null);
11
+ const lastSettingsVersionRef = useRef(0);
12
+ const lastSettingsUpdateRef = useRef(0);
13
+ // Handle conflict resolution
14
+ const mergeWithConflictResolution = useCallback((local, remote, remoteUpdatedAt) => {
15
+ const remoteTime = new Date(remoteUpdatedAt).getTime();
16
+ const localTime = lastSettingsUpdateRef.current;
17
+ switch (mergeStrategy) {
18
+ case 'remote-wins':
19
+ lastSettingsUpdateRef.current = remoteTime;
20
+ return { ...local, ...remote };
21
+ case 'local-wins':
22
+ return local;
23
+ case 'newer-wins':
24
+ default:
25
+ if (remoteTime > localTime) {
26
+ lastSettingsUpdateRef.current = remoteTime;
27
+ return { ...local, ...remote };
28
+ }
29
+ return local;
30
+ }
31
+ }, [mergeStrategy]);
32
+ // Upload settings
33
+ const uploadSettings = useCallback(async (settingsToUpload, version) => {
34
+ try {
35
+ const syncStatus = await mixIdApi.getSyncStatus();
36
+ if (!syncStatus.syncSettings)
37
+ return;
38
+ const result = await mixIdApi.uploadSettings(settingsToUpload);
39
+ lastSettingsVersionRef.current = result.version;
40
+ lastSettingsUpdateRef.current = Date.now();
41
+ // Send via WebSocket for real-time sync
42
+ if (wsClient.isConnected()) {
43
+ wsClient.send({
44
+ type: 'sync:settings',
45
+ settings: settingsToUpload,
46
+ version: result.version,
47
+ });
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.error('Failed to upload settings:', error);
52
+ // Queue for offline sync
53
+ if (saveLocalSettings) {
54
+ offlineQueue.enqueue('settings', settingsToUpload);
55
+ }
56
+ }
57
+ }, [saveLocalSettings]);
58
+ // Upload data
59
+ const uploadData = useCallback(async (dataType, data) => {
60
+ try {
61
+ const syncStatus = await mixIdApi.getSyncStatus();
62
+ if (!syncStatus.syncData)
63
+ return;
64
+ await mixIdApi.uploadData(dataType, data);
65
+ // Send via WebSocket for real-time sync
66
+ if (wsClient.isConnected()) {
67
+ wsClient.send({
68
+ type: 'sync:data',
69
+ dataType,
70
+ data,
71
+ });
72
+ }
73
+ }
74
+ catch (error) {
75
+ console.error(`Failed to upload ${dataType}:`, error);
76
+ // Queue for offline sync
77
+ offlineQueue.enqueue('data', data, dataType);
78
+ }
79
+ }, []);
80
+ // Process offline queue
81
+ const processOfflineQueue = useCallback(async () => {
82
+ await offlineQueue.processQueue(async (operation) => {
83
+ if (operation.type === 'settings') {
84
+ await uploadSettings(operation.data);
85
+ }
86
+ else if (operation.type === 'data' && operation.dataType) {
87
+ await uploadData(operation.dataType, operation.data);
88
+ }
89
+ });
90
+ }, [uploadSettings, uploadData]);
91
+ // Perform sync
92
+ const performSync = useCallback(async () => {
93
+ try {
94
+ const config = mixIdApi.getConfig();
95
+ if (!config || !config.accessToken) {
96
+ return;
97
+ }
98
+ // Get sync status
99
+ const syncStatus = await mixIdApi.getSyncStatus();
100
+ // Check for updates
101
+ const updates = await mixIdApi.checkUpdates(lastSettingsVersionRef.current, syncStatus.syncData && dataTypes.length > 0 ? dataTypes : undefined);
102
+ // Download updates if available
103
+ if (updates.hasUpdates) {
104
+ if (updates.updates.settings && syncStatus.syncSettings && getLocalSettings && saveLocalSettings) {
105
+ try {
106
+ const remoteSettings = await mixIdApi.downloadSettings();
107
+ const localSettings = getLocalSettings();
108
+ const merged = mergeWithConflictResolution(localSettings, remoteSettings.settings, remoteSettings.updatedAt);
109
+ await saveLocalSettings(merged);
110
+ lastSettingsVersionRef.current = remoteSettings.version;
111
+ onSettingsUpdate?.(merged);
112
+ }
113
+ catch (error) {
114
+ console.error('Failed to download settings:', error);
115
+ }
116
+ }
117
+ if (updates.updates.data && syncStatus.syncData && dataTypes.length > 0) {
118
+ for (const dataType of dataTypes) {
119
+ if (updates.updates.data[dataType] && getLocalData && saveLocalData) {
120
+ try {
121
+ const remoteData = await mixIdApi.downloadData(dataType);
122
+ const localData = await getLocalData(dataType);
123
+ // Merge with conflict resolution
124
+ const merged = { ...localData, ...remoteData.data };
125
+ await saveLocalData(dataType, merged);
126
+ onDataUpdate?.(dataType, merged);
127
+ }
128
+ catch (error) {
129
+ console.error(`Failed to download ${dataType}:`, error);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ // Upload local changes (only if not already synced via WebSocket)
136
+ if (syncStatus.syncSettings && getLocalSettings) {
137
+ const localSettings = getLocalSettings();
138
+ await uploadSettings(localSettings);
139
+ }
140
+ if (syncStatus.syncData && dataTypes.length > 0 && getLocalData) {
141
+ for (const dataType of dataTypes) {
142
+ try {
143
+ const localData = await getLocalData(dataType);
144
+ if (localData && Object.keys(localData).length > 0) {
145
+ await uploadData(dataType, localData);
146
+ }
147
+ }
148
+ catch (error) {
149
+ console.error(`Failed to upload ${dataType}:`, error);
150
+ }
151
+ }
152
+ }
153
+ // Process offline queue
154
+ await processOfflineQueue();
155
+ }
156
+ catch (error) {
157
+ console.error('Sync error:', error);
158
+ }
159
+ }, [
160
+ dataTypes,
161
+ getLocalSettings,
162
+ getLocalData,
163
+ saveLocalSettings,
164
+ saveLocalData,
165
+ mergeWithConflictResolution,
166
+ uploadSettings,
167
+ uploadData,
168
+ processOfflineQueue,
169
+ onSettingsUpdate,
170
+ onDataUpdate,
171
+ ]);
172
+ useEffect(() => {
173
+ const setupSync = () => {
174
+ const config = mixIdApi.getConfig();
175
+ if (!config || !config.accessToken) {
176
+ // Disconnect WebSocket if config is cleared
177
+ wsClient.disconnect();
178
+ return;
179
+ }
180
+ // Connect WebSocket
181
+ wsClient.connect();
182
+ };
183
+ // Initial setup
184
+ setupSync();
185
+ // Listen for config changes
186
+ const handleConfigChange = () => {
187
+ setupSync();
188
+ };
189
+ if (typeof window !== 'undefined') {
190
+ window.addEventListener('mixid-config-changed', handleConfigChange);
191
+ const config = mixIdApi.getConfig();
192
+ if (!config || !config.accessToken) {
193
+ return () => {
194
+ window.removeEventListener('mixid-config-changed', handleConfigChange);
195
+ };
196
+ }
197
+ // Set up WebSocket event handlers
198
+ const handleSettingsUpdate = (message) => {
199
+ if (message.settings && message.updatedAt && getLocalSettings && saveLocalSettings) {
200
+ const localSettings = getLocalSettings();
201
+ const merged = mergeWithConflictResolution(localSettings, message.settings, message.updatedAt);
202
+ saveLocalSettings(merged);
203
+ lastSettingsVersionRef.current = message.version || lastSettingsVersionRef.current;
204
+ onSettingsUpdate?.(merged);
205
+ }
206
+ };
207
+ const handleDataUpdate = async (message) => {
208
+ if (message.dataType && message.data && getLocalData && saveLocalData) {
209
+ try {
210
+ const localData = await getLocalData(message.dataType);
211
+ const merged = { ...localData, ...message.data };
212
+ await saveLocalData(message.dataType, merged);
213
+ onDataUpdate?.(message.dataType, merged);
214
+ }
215
+ catch (error) {
216
+ console.error(`Error merging ${message.dataType}:`, error);
217
+ }
218
+ }
219
+ };
220
+ wsClient.on('sync:settings:update', handleSettingsUpdate);
221
+ wsClient.on('sync:data:update', handleDataUpdate);
222
+ // Initial sync
223
+ performSync();
224
+ // Set up periodic sync (fallback HTTP sync)
225
+ syncIntervalRef.current = setInterval(performSync, SYNC_INTERVAL);
226
+ // Set up heartbeat
227
+ heartbeatIntervalRef.current = setInterval(() => {
228
+ mixIdApi.heartbeat({
229
+ platform: typeof navigator !== 'undefined' ? navigator.platform : 'unknown',
230
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
231
+ }).catch(console.error);
232
+ }, HEARTBEAT_INTERVAL);
233
+ // Process offline queue when online
234
+ if (typeof navigator !== 'undefined' && navigator.onLine) {
235
+ processOfflineQueue();
236
+ }
237
+ return () => {
238
+ window.removeEventListener('mixid-config-changed', handleConfigChange);
239
+ wsClient.off('sync:settings:update', handleSettingsUpdate);
240
+ wsClient.off('sync:data:update', handleDataUpdate);
241
+ if (syncIntervalRef.current) {
242
+ clearInterval(syncIntervalRef.current);
243
+ }
244
+ if (heartbeatIntervalRef.current) {
245
+ clearInterval(heartbeatIntervalRef.current);
246
+ }
247
+ };
248
+ }
249
+ }, [
250
+ mergeWithConflictResolution,
251
+ uploadSettings,
252
+ uploadData,
253
+ processOfflineQueue,
254
+ performSync,
255
+ getLocalSettings,
256
+ getLocalData,
257
+ saveLocalSettings,
258
+ saveLocalData,
259
+ onSettingsUpdate,
260
+ onDataUpdate,
261
+ ]);
262
+ return { performSync, uploadSettings, uploadData };
263
+ }
@@ -0,0 +1,17 @@
1
+ export interface Notification {
2
+ id: string;
3
+ appId: string | null;
4
+ title: string;
5
+ message: string;
6
+ type: string;
7
+ read: boolean;
8
+ createdAt: string;
9
+ }
10
+ export declare function useNotifications(): {
11
+ notifications: Notification[];
12
+ unreadCount: number;
13
+ markAsRead: (notificationId: string) => Promise<void>;
14
+ markAllAsRead: () => Promise<void>;
15
+ refresh: () => Promise<void>;
16
+ };
17
+ //# sourceMappingURL=useNotifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useNotifications.d.ts","sourceRoot":"","sources":["../../src/hooks/useNotifications.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,wBAAgB,gBAAgB;;;iCAiDL,MAAM;;;EAiHhC"}
@@ -0,0 +1,144 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { mixIdApi } from '../api/mixIdApi';
3
+ import { wsClient } from '../api/websocket';
4
+ const NOTIFICATIONS_STORAGE_KEY = 'mixId_notifications';
5
+ export function useNotifications() {
6
+ const [notifications, setNotifications] = useState(() => {
7
+ if (typeof window === 'undefined' || !window.localStorage) {
8
+ return [];
9
+ }
10
+ try {
11
+ const stored = localStorage.getItem(NOTIFICATIONS_STORAGE_KEY);
12
+ return stored ? JSON.parse(stored) : [];
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ });
18
+ const [unreadCount, setUnreadCount] = useState(0);
19
+ const saveNotifications = useCallback((newNotifications) => {
20
+ setNotifications(newNotifications);
21
+ if (typeof window !== 'undefined' && window.localStorage) {
22
+ localStorage.setItem(NOTIFICATIONS_STORAGE_KEY, JSON.stringify(newNotifications));
23
+ }
24
+ setUnreadCount(newNotifications.filter((n) => !n.read).length);
25
+ }, []);
26
+ const fetchNotifications = useCallback(async () => {
27
+ try {
28
+ const config = mixIdApi.getConfig();
29
+ if (!config || !config.accessToken) {
30
+ return;
31
+ }
32
+ const apiBase = config.apiBase || (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
33
+ ? (import.meta.env?.VITE_MIX_ID_API_BASE || 'http://localhost:3000/api')
34
+ : 'http://localhost:3000/api';
35
+ const response = await fetch(`${apiBase}/notifications`, {
36
+ headers: {
37
+ Authorization: `Bearer ${config.accessToken}`,
38
+ },
39
+ });
40
+ if (response.ok) {
41
+ const data = await response.json();
42
+ saveNotifications(data);
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error('Failed to fetch notifications:', error);
47
+ }
48
+ }, [saveNotifications]);
49
+ const markAsRead = useCallback(async (notificationId) => {
50
+ try {
51
+ const config = mixIdApi.getConfig();
52
+ if (!config || !config.accessToken) {
53
+ return;
54
+ }
55
+ // Update locally first
56
+ const updated = notifications.map((n) => (n.id === notificationId ? { ...n, read: true } : n));
57
+ saveNotifications(updated);
58
+ // Send via WebSocket for real-time sync
59
+ if (wsClient.isConnected()) {
60
+ wsClient.send({
61
+ type: 'notification:read',
62
+ notificationId,
63
+ });
64
+ }
65
+ // Also send via HTTP as fallback
66
+ const apiBase = config.apiBase || (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
67
+ ? (import.meta.env?.VITE_MIX_ID_API_BASE || 'http://localhost:3000/api')
68
+ : 'http://localhost:3000/api';
69
+ await fetch(`${apiBase}/notifications/${notificationId}/read`, {
70
+ method: 'PUT',
71
+ headers: {
72
+ Authorization: `Bearer ${config.accessToken}`,
73
+ },
74
+ });
75
+ }
76
+ catch (error) {
77
+ console.error('Failed to mark notification as read:', error);
78
+ }
79
+ }, [notifications, saveNotifications]);
80
+ const markAllAsRead = useCallback(async () => {
81
+ try {
82
+ const config = mixIdApi.getConfig();
83
+ if (!config || !config.accessToken) {
84
+ return;
85
+ }
86
+ // Update locally first
87
+ const updated = notifications.map((n) => ({ ...n, read: true }));
88
+ saveNotifications(updated);
89
+ // Send via HTTP
90
+ const apiBase = config.apiBase || (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
91
+ ? (import.meta.env?.VITE_MIX_ID_API_BASE || 'http://localhost:3000/api')
92
+ : 'http://localhost:3000/api';
93
+ await fetch(`${apiBase}/notifications/read-all`, {
94
+ method: 'PUT',
95
+ headers: {
96
+ Authorization: `Bearer ${config.accessToken}`,
97
+ },
98
+ });
99
+ }
100
+ catch (error) {
101
+ console.error('Failed to mark all notifications as read:', error);
102
+ }
103
+ }, [notifications, saveNotifications]);
104
+ useEffect(() => {
105
+ const config = mixIdApi.getConfig();
106
+ if (!config || !config.accessToken) {
107
+ // Clear notifications if MIX ID is not connected
108
+ saveNotifications([]);
109
+ return;
110
+ }
111
+ // Fetch initial notifications
112
+ fetchNotifications();
113
+ // Set up WebSocket handlers
114
+ const handleNewNotification = (message) => {
115
+ if (message.notification) {
116
+ const newNotification = message.notification;
117
+ const updated = [newNotification, ...notifications];
118
+ saveNotifications(updated);
119
+ }
120
+ };
121
+ const handleNotificationRead = (message) => {
122
+ if (message.notificationId) {
123
+ const updated = notifications.map((n) => n.id === message.notificationId ? { ...n, read: true } : n);
124
+ saveNotifications(updated);
125
+ }
126
+ };
127
+ wsClient.on('notification:new', handleNewNotification);
128
+ wsClient.on('notification:read', handleNotificationRead);
129
+ // Periodic refresh (every 5 minutes)
130
+ const interval = setInterval(fetchNotifications, 5 * 60 * 1000);
131
+ return () => {
132
+ wsClient.off('notification:new', handleNewNotification);
133
+ wsClient.off('notification:read', handleNotificationRead);
134
+ clearInterval(interval);
135
+ };
136
+ }, [fetchNotifications, notifications, saveNotifications]);
137
+ return {
138
+ notifications,
139
+ unreadCount,
140
+ markAsRead,
141
+ markAllAsRead,
142
+ refresh: fetchNotifications,
143
+ };
144
+ }
@@ -0,0 +1,5 @@
1
+ export * from './api';
2
+ export * from './hooks';
3
+ export * from './components';
4
+ export * from './ui';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,OAAO,CAAA;AAGrB,cAAc,SAAS,CAAA;AAGvB,cAAc,cAAc,CAAA;AAG5B,cAAc,MAAM,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // API
2
+ export * from './api';
3
+ // Hooks
4
+ export * from './hooks';
5
+ // Components
6
+ export * from './components';
7
+ // UI Components (for future framework)
8
+ export * from './ui';
@@ -0,0 +1,5 @@
1
+ import { ButtonProps as MantineButtonProps } from '@mantine/core';
2
+ export interface ButtonProps extends MantineButtonProps {
3
+ }
4
+ export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
5
+ //# sourceMappingURL=Button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/ui/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2B,WAAW,IAAI,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAG1F,MAAM,WAAW,WAAY,SAAQ,kBAAkB;CAEtD;AAED,eAAO,MAAM,MAAM,2GAEjB,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button as MantineButton } from '@mantine/core';
3
+ import { forwardRef } from 'react';
4
+ export const Button = forwardRef((props, ref) => {
5
+ return _jsx(MantineButton, { ref: ref, ...props });
6
+ });
7
+ Button.displayName = 'Button';
@@ -0,0 +1,5 @@
1
+ import { PaperProps } from '@mantine/core';
2
+ export interface CardProps extends PaperProps {
3
+ }
4
+ export declare const Card: import("react").ForwardRefExoticComponent<CardProps & import("react").RefAttributes<HTMLDivElement>>;
5
+ //# sourceMappingURL=Card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../src/ui/Card.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAS,UAAU,EAAE,MAAM,eAAe,CAAA;AAGjD,MAAM,WAAW,SAAU,SAAQ,UAAU;CAE5C;AAED,eAAO,MAAM,IAAI,sGAEf,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Paper } from '@mantine/core';
3
+ import { forwardRef } from 'react';
4
+ export const Card = forwardRef((props, ref) => {
5
+ return _jsx(Paper, { ref: ref, withBorder: true, p: "md", ...props });
6
+ });
7
+ Card.displayName = 'Card';
@@ -0,0 +1,3 @@
1
+ export * from './Button';
2
+ export * from './Card';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,QAAQ,CAAA"}
@@ -0,0 +1,2 @@
1
+ export * from './Button';
2
+ export * from './Card';