@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.
- package/LICENSE +674 -0
- package/README.md +52 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +3 -0
- package/dist/api/mixIdApi.d.ts +76 -0
- package/dist/api/mixIdApi.d.ts.map +1 -0
- package/dist/api/mixIdApi.js +275 -0
- package/dist/api/offlineQueue.d.ts +24 -0
- package/dist/api/offlineQueue.d.ts.map +1 -0
- package/dist/api/offlineQueue.js +137 -0
- package/dist/api/websocket.d.ts +28 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +201 -0
- package/dist/components/MixIdCallbackPage.d.ts +6 -0
- package/dist/components/MixIdCallbackPage.d.ts.map +1 -0
- package/dist/components/MixIdCallbackPage.js +38 -0
- package/dist/components/MixIdConnection.d.ts +18 -0
- package/dist/components/MixIdConnection.d.ts.map +1 -0
- package/dist/components/MixIdConnection.js +197 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +2 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useMixIdSession.d.ts +19 -0
- package/dist/hooks/useMixIdSession.d.ts.map +1 -0
- package/dist/hooks/useMixIdSession.js +124 -0
- package/dist/hooks/useMixIdStatus.d.ts +9 -0
- package/dist/hooks/useMixIdStatus.d.ts.map +1 -0
- package/dist/hooks/useMixIdStatus.js +81 -0
- package/dist/hooks/useMixIdSync.d.ts +16 -0
- package/dist/hooks/useMixIdSync.d.ts.map +1 -0
- package/dist/hooks/useMixIdSync.js +263 -0
- package/dist/hooks/useNotifications.d.ts +17 -0
- package/dist/hooks/useNotifications.d.ts.map +1 -0
- package/dist/hooks/useNotifications.js +144 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/ui/Button.d.ts +5 -0
- package/dist/ui/Button.d.ts.map +1 -0
- package/dist/ui/Button.js +7 -0
- package/dist/ui/Card.d.ts +5 -0
- package/dist/ui/Card.d.ts.map +1 -0
- package/dist/ui/Card.js +7 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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 @@
|
|
|
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"}
|
package/dist/ui/Card.js
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,QAAQ,CAAA"}
|
package/dist/ui/index.js
ADDED