@neevjs/client 0.0.1 → 1.0.0-beta
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 +21 -0
- package/dist/index.d.mts +205 -7
- package/dist/index.d.ts +205 -7
- package/dist/index.js +662 -139
- package/dist/index.mjs +652 -134
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { VERSION as VERSION2 } from "@neevjs/shared";
|
|
3
|
+
|
|
1
4
|
// src/core/AuthClient.ts
|
|
5
|
+
var USER_CACHE_KEY = "neev_user";
|
|
2
6
|
var AuthClient = class {
|
|
3
7
|
client;
|
|
4
8
|
userData = null;
|
|
5
9
|
constructor(client) {
|
|
6
10
|
this.client = client;
|
|
11
|
+
try {
|
|
12
|
+
const cached = sessionStorage.getItem(USER_CACHE_KEY);
|
|
13
|
+
if (cached) this.userData = JSON.parse(cached);
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
7
16
|
}
|
|
8
17
|
async login(email, password) {
|
|
9
18
|
const res = await this.client.request("/auth/login", {
|
|
@@ -12,6 +21,7 @@ var AuthClient = class {
|
|
|
12
21
|
});
|
|
13
22
|
localStorage.setItem("neev_token", res.data.token);
|
|
14
23
|
this.userData = res.data.user;
|
|
24
|
+
sessionStorage.setItem(USER_CACHE_KEY, JSON.stringify(this.userData));
|
|
15
25
|
}
|
|
16
26
|
async register(email, password, name) {
|
|
17
27
|
const res = await this.client.request("/auth/register", {
|
|
@@ -20,9 +30,11 @@ var AuthClient = class {
|
|
|
20
30
|
});
|
|
21
31
|
localStorage.setItem("neev_token", res.data.token);
|
|
22
32
|
this.userData = res.data.user;
|
|
33
|
+
sessionStorage.setItem(USER_CACHE_KEY, JSON.stringify(this.userData));
|
|
23
34
|
}
|
|
24
35
|
logout() {
|
|
25
36
|
localStorage.removeItem("neev_token");
|
|
37
|
+
sessionStorage.removeItem(USER_CACHE_KEY);
|
|
26
38
|
this.userData = null;
|
|
27
39
|
}
|
|
28
40
|
async user() {
|
|
@@ -30,6 +42,7 @@ var AuthClient = class {
|
|
|
30
42
|
if (this.userData) return this.userData;
|
|
31
43
|
const res = await this.client.request("/auth/me");
|
|
32
44
|
this.userData = res.data;
|
|
45
|
+
sessionStorage.setItem(USER_CACHE_KEY, JSON.stringify(this.userData));
|
|
33
46
|
return this.userData;
|
|
34
47
|
}
|
|
35
48
|
isAuthenticated() {
|
|
@@ -51,13 +64,23 @@ function createClient(config = {}) {
|
|
|
51
64
|
req = await plugin.onRequest(req);
|
|
52
65
|
}
|
|
53
66
|
}
|
|
67
|
+
if (req._cachedResponse instanceof Response) {
|
|
68
|
+
let cachedRes = req._cachedResponse;
|
|
69
|
+
for (const plugin of plugins) {
|
|
70
|
+
if (plugin.onResponse) {
|
|
71
|
+
cachedRes = await plugin.onResponse(cachedRes, req);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return cachedRes.json();
|
|
75
|
+
}
|
|
54
76
|
const headers = {
|
|
55
77
|
"Content-Type": "application/json",
|
|
56
78
|
...req.options.headers ?? {}
|
|
57
79
|
};
|
|
58
80
|
let response;
|
|
59
81
|
try {
|
|
60
|
-
|
|
82
|
+
const finalBaseURL = req.options.baseURL ?? baseURL;
|
|
83
|
+
response = await fetch(finalBaseURL + req.url, {
|
|
61
84
|
method: req.options.method ?? "GET",
|
|
62
85
|
body: req.options.body,
|
|
63
86
|
headers
|
|
@@ -65,20 +88,31 @@ function createClient(config = {}) {
|
|
|
65
88
|
let finalResponse = response;
|
|
66
89
|
for (const plugin of plugins) {
|
|
67
90
|
if (plugin.onResponse) {
|
|
68
|
-
finalResponse = await plugin.onResponse(finalResponse);
|
|
91
|
+
finalResponse = await plugin.onResponse(finalResponse, req);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (finalResponse.status === 401) {
|
|
95
|
+
client.auth?.logout();
|
|
96
|
+
const error = new Error("Session expired. Please log in again.");
|
|
97
|
+
error.status = 401;
|
|
98
|
+
for (const plugin of plugins) {
|
|
99
|
+
plugin.onError?.(error, req);
|
|
69
100
|
}
|
|
101
|
+
throw error;
|
|
70
102
|
}
|
|
71
103
|
if (!finalResponse.ok) {
|
|
72
104
|
const errorBody = await finalResponse.json().catch(() => ({ error: "Unknown error" }));
|
|
73
|
-
|
|
105
|
+
const error = new Error(
|
|
74
106
|
typeof errorBody.error === "string" ? errorBody.error : `HTTP ${finalResponse.status}`
|
|
75
107
|
);
|
|
108
|
+
error.status = finalResponse.status;
|
|
109
|
+
throw error;
|
|
76
110
|
}
|
|
77
111
|
return finalResponse.json();
|
|
78
112
|
} catch (err) {
|
|
79
113
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
80
114
|
for (const plugin of plugins) {
|
|
81
|
-
plugin.onError?.(error);
|
|
115
|
+
plugin.onError?.(error, req);
|
|
82
116
|
}
|
|
83
117
|
throw error;
|
|
84
118
|
}
|
|
@@ -91,7 +125,6 @@ function createClient(config = {}) {
|
|
|
91
125
|
request,
|
|
92
126
|
use,
|
|
93
127
|
auth: null
|
|
94
|
-
// set below after client is defined
|
|
95
128
|
};
|
|
96
129
|
client.auth = new AuthClient(client);
|
|
97
130
|
return client;
|
|
@@ -114,15 +147,150 @@ function useNeevClient() {
|
|
|
114
147
|
return client;
|
|
115
148
|
}
|
|
116
149
|
|
|
150
|
+
// src/core/SecureStore.ts
|
|
151
|
+
var SECURE_PREFIX = "neev_secure_";
|
|
152
|
+
var _encryptionKey = null;
|
|
153
|
+
async function getEncryptionKey() {
|
|
154
|
+
if (_encryptionKey) return _encryptionKey;
|
|
155
|
+
if (!window.crypto?.subtle) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"[NeevJS] SecureStore requires Web Crypto API (available in HTTPS or localhost contexts only)."
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
_encryptionKey = await window.crypto.subtle.generateKey(
|
|
161
|
+
{ name: "AES-GCM", length: 256 },
|
|
162
|
+
false,
|
|
163
|
+
// non-extractable — the raw key bytes can NEVER be exported
|
|
164
|
+
["encrypt", "decrypt"]
|
|
165
|
+
);
|
|
166
|
+
return _encryptionKey;
|
|
167
|
+
}
|
|
168
|
+
async function encrypt(data) {
|
|
169
|
+
const key = await getEncryptionKey();
|
|
170
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
171
|
+
const encoded = new TextEncoder().encode(JSON.stringify(data));
|
|
172
|
+
const ciphertext = await window.crypto.subtle.encrypt(
|
|
173
|
+
{ name: "AES-GCM", iv },
|
|
174
|
+
key,
|
|
175
|
+
encoded
|
|
176
|
+
);
|
|
177
|
+
const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
|
|
178
|
+
combined.set(iv, 0);
|
|
179
|
+
combined.set(new Uint8Array(ciphertext), iv.byteLength);
|
|
180
|
+
return btoa(String.fromCharCode(...combined));
|
|
181
|
+
}
|
|
182
|
+
async function decrypt(encoded) {
|
|
183
|
+
const key = await getEncryptionKey();
|
|
184
|
+
const combined = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
|
|
185
|
+
const iv = combined.slice(0, 12);
|
|
186
|
+
const ciphertext = combined.slice(12);
|
|
187
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
188
|
+
{ name: "AES-GCM", iv },
|
|
189
|
+
key,
|
|
190
|
+
ciphertext
|
|
191
|
+
);
|
|
192
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
193
|
+
}
|
|
194
|
+
var SecureStore = {
|
|
195
|
+
/**
|
|
196
|
+
* Encrypt and store a value in localStorage.
|
|
197
|
+
* The value cannot be read without the in-memory session key.
|
|
198
|
+
*/
|
|
199
|
+
async set(key, value) {
|
|
200
|
+
const ciphertext = await encrypt(value);
|
|
201
|
+
localStorage.setItem(SECURE_PREFIX + key, ciphertext);
|
|
202
|
+
},
|
|
203
|
+
/**
|
|
204
|
+
* Retrieve and decrypt a value.
|
|
205
|
+
* Returns null if the key doesn't exist or cannot be decrypted
|
|
206
|
+
* (e.g. after a page refresh when the key is gone).
|
|
207
|
+
*/
|
|
208
|
+
async get(key) {
|
|
209
|
+
const stored = localStorage.getItem(SECURE_PREFIX + key);
|
|
210
|
+
if (!stored) return null;
|
|
211
|
+
try {
|
|
212
|
+
return await decrypt(stored);
|
|
213
|
+
} catch {
|
|
214
|
+
localStorage.removeItem(SECURE_PREFIX + key);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
/**
|
|
219
|
+
* Remove a specific key from SecureStore.
|
|
220
|
+
*/
|
|
221
|
+
remove(key) {
|
|
222
|
+
localStorage.removeItem(SECURE_PREFIX + key);
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* Remove ALL SecureStore entries from localStorage.
|
|
226
|
+
* Call this on logout to clean up all session-bound encrypted data.
|
|
227
|
+
*/
|
|
228
|
+
clear() {
|
|
229
|
+
const keysToRemove = [];
|
|
230
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
231
|
+
const k = localStorage.key(i);
|
|
232
|
+
if (k?.startsWith(SECURE_PREFIX)) keysToRemove.push(k);
|
|
233
|
+
}
|
|
234
|
+
keysToRemove.forEach((k) => localStorage.removeItem(k));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
117
238
|
// src/hooks/useModel.ts
|
|
118
239
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
119
240
|
var cache = /* @__PURE__ */ new Map();
|
|
120
|
-
|
|
241
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
242
|
+
function emitChange(url, exclude) {
|
|
243
|
+
listeners.get(url)?.forEach((listener) => {
|
|
244
|
+
if (listener !== exclude) listener();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function mutateModel(url) {
|
|
248
|
+
cache.delete(url);
|
|
249
|
+
emitChange(url);
|
|
250
|
+
}
|
|
251
|
+
var inFlightRequests = /* @__PURE__ */ new Map();
|
|
252
|
+
var cacheTimestamps = /* @__PURE__ */ new Map();
|
|
253
|
+
var cacheError = /* @__PURE__ */ new Map();
|
|
254
|
+
var STALE_TIME = 60 * 1e3;
|
|
255
|
+
function useModel(name, options) {
|
|
121
256
|
const client = useNeevClient();
|
|
122
|
-
const
|
|
257
|
+
const baseUrl = `/${name}`;
|
|
258
|
+
const url = options?.params ? `${baseUrl}?${new URLSearchParams(
|
|
259
|
+
Object.entries(options.params).filter(([, v]) => v !== void 0).map(([k, v]) => [k, String(v)])
|
|
260
|
+
).toString()}` : baseUrl;
|
|
123
261
|
const [data, setData] = useState(() => cache.get(url) ?? []);
|
|
124
262
|
const [loading, setLoading] = useState(!cache.has(url));
|
|
125
263
|
const [error, setError] = useState(null);
|
|
264
|
+
if (options?.suspense) {
|
|
265
|
+
if (cacheError.has(url)) {
|
|
266
|
+
throw cacheError.get(url);
|
|
267
|
+
}
|
|
268
|
+
const timestamp = cacheTimestamps.get(url);
|
|
269
|
+
const isFresh = timestamp && Date.now() - timestamp < STALE_TIME;
|
|
270
|
+
if (!isFresh && !cache.has(url)) {
|
|
271
|
+
if (!navigator.onLine) {
|
|
272
|
+
throw new Error("[NeevJS] Offline: No cached data available for this model.");
|
|
273
|
+
}
|
|
274
|
+
let requestPromise = inFlightRequests.get(url);
|
|
275
|
+
if (!requestPromise) {
|
|
276
|
+
requestPromise = client.request(url, { baseURL: options?.baseURL }).then((res) => {
|
|
277
|
+
const rows = Array.isArray(res) ? res : res.data ?? [];
|
|
278
|
+
cache.set(url, rows);
|
|
279
|
+
cacheTimestamps.set(url, Date.now());
|
|
280
|
+
cacheError.delete(url);
|
|
281
|
+
}).catch((err) => {
|
|
282
|
+
if (!navigator.onLine && cache.has(url)) return;
|
|
283
|
+
cacheError.set(url, err instanceof Error ? err : new Error(String(err)));
|
|
284
|
+
}).finally(() => {
|
|
285
|
+
if (inFlightRequests.get(url) === requestPromise) {
|
|
286
|
+
inFlightRequests.delete(url);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
inFlightRequests.set(url, requestPromise);
|
|
290
|
+
}
|
|
291
|
+
throw requestPromise;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
126
294
|
const mountedRef = useRef(true);
|
|
127
295
|
useEffect(() => {
|
|
128
296
|
mountedRef.current = true;
|
|
@@ -130,51 +298,138 @@ function useModel(name) {
|
|
|
130
298
|
mountedRef.current = false;
|
|
131
299
|
};
|
|
132
300
|
}, []);
|
|
133
|
-
const fetchData = useCallback(async () => {
|
|
301
|
+
const fetchData = useCallback(async (force = false) => {
|
|
302
|
+
if (!navigator.onLine && cache.has(url)) {
|
|
303
|
+
if (mountedRef.current) {
|
|
304
|
+
setData(cache.get(url));
|
|
305
|
+
setError(null);
|
|
306
|
+
setLoading(false);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!force && cache.has(url)) {
|
|
311
|
+
const timestamp = cacheTimestamps.get(url);
|
|
312
|
+
if (timestamp && Date.now() - timestamp < STALE_TIME) {
|
|
313
|
+
if (mountedRef.current) {
|
|
314
|
+
setData(cache.get(url));
|
|
315
|
+
setError(null);
|
|
316
|
+
setLoading(false);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
134
321
|
if (mountedRef.current) setLoading(true);
|
|
322
|
+
let requestPromise = inFlightRequests.get(url);
|
|
323
|
+
if (!requestPromise) {
|
|
324
|
+
requestPromise = client.request(url, { baseURL: options?.baseURL });
|
|
325
|
+
inFlightRequests.set(url, requestPromise);
|
|
326
|
+
}
|
|
135
327
|
try {
|
|
136
|
-
const res = await
|
|
328
|
+
const res = await requestPromise;
|
|
137
329
|
const rows = Array.isArray(res) ? res : res.data ?? [];
|
|
138
330
|
cache.set(url, rows);
|
|
331
|
+
cacheTimestamps.set(url, Date.now());
|
|
332
|
+
cacheError.delete(url);
|
|
139
333
|
if (mountedRef.current) {
|
|
140
334
|
setData(rows);
|
|
141
335
|
setError(null);
|
|
142
336
|
}
|
|
143
337
|
} catch (err) {
|
|
144
|
-
if (
|
|
338
|
+
if (!navigator.onLine && cache.has(url)) {
|
|
339
|
+
if (mountedRef.current) {
|
|
340
|
+
setData(cache.get(url));
|
|
341
|
+
setError(null);
|
|
342
|
+
}
|
|
343
|
+
} else if (mountedRef.current) {
|
|
145
344
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
146
345
|
}
|
|
147
346
|
} finally {
|
|
347
|
+
if (inFlightRequests.get(url) === requestPromise) {
|
|
348
|
+
inFlightRequests.delete(url);
|
|
349
|
+
}
|
|
148
350
|
if (mountedRef.current) setLoading(false);
|
|
149
351
|
}
|
|
150
352
|
}, [client, url]);
|
|
151
353
|
useEffect(() => {
|
|
152
|
-
void fetchData();
|
|
354
|
+
void fetchData(false);
|
|
153
355
|
}, [fetchData]);
|
|
356
|
+
const handleRemoteChange = useCallback(() => {
|
|
357
|
+
void fetchData(true);
|
|
358
|
+
}, [fetchData]);
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
if (!listeners.has(url)) listeners.set(url, /* @__PURE__ */ new Set());
|
|
361
|
+
listeners.get(url).add(handleRemoteChange);
|
|
362
|
+
return () => {
|
|
363
|
+
listeners.get(url)?.delete(handleRemoteChange);
|
|
364
|
+
};
|
|
365
|
+
}, [url, handleRemoteChange]);
|
|
154
366
|
const create = useCallback(async (payload) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
367
|
+
try {
|
|
368
|
+
await client.request(url, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
body: JSON.stringify(payload),
|
|
371
|
+
baseURL: options?.baseURL
|
|
372
|
+
});
|
|
373
|
+
cache.delete(url);
|
|
374
|
+
await fetchData(true);
|
|
375
|
+
emitChange(url, handleRemoteChange);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (err.message?.includes("[NeevJS] Offline")) {
|
|
378
|
+
const newRecord = { ...payload, id: `temp_${Date.now()}` };
|
|
379
|
+
const currentData = cache.get(url) ?? [];
|
|
380
|
+
const updatedData = [...currentData, newRecord];
|
|
381
|
+
cache.set(url, updatedData);
|
|
382
|
+
setData(updatedData);
|
|
383
|
+
emitChange(url, handleRemoteChange);
|
|
384
|
+
} else {
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}, [client, url, fetchData, handleRemoteChange]);
|
|
162
389
|
const update = useCallback(async (id, payload) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
390
|
+
try {
|
|
391
|
+
await client.request(`${url}/${id}`, {
|
|
392
|
+
method: "PUT",
|
|
393
|
+
body: JSON.stringify(payload),
|
|
394
|
+
baseURL: options?.baseURL
|
|
395
|
+
});
|
|
396
|
+
cache.delete(url);
|
|
397
|
+
await fetchData(true);
|
|
398
|
+
emitChange(url, handleRemoteChange);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (err.message?.includes("[NeevJS] Offline")) {
|
|
401
|
+
const currentData = cache.get(url) ?? [];
|
|
402
|
+
const updatedData = currentData.map((item) => String(item.id) === String(id) ? { ...item, ...payload } : item);
|
|
403
|
+
cache.set(url, updatedData);
|
|
404
|
+
setData(updatedData);
|
|
405
|
+
emitChange(url, handleRemoteChange);
|
|
406
|
+
} else {
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}, [client, url, fetchData, handleRemoteChange]);
|
|
170
411
|
const remove = useCallback(async (id) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
412
|
+
try {
|
|
413
|
+
await client.request(`${url}/${id}`, {
|
|
414
|
+
method: "DELETE",
|
|
415
|
+
baseURL: options?.baseURL
|
|
416
|
+
});
|
|
417
|
+
cache.delete(url);
|
|
418
|
+
await fetchData(true);
|
|
419
|
+
emitChange(url, handleRemoteChange);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
if (err.message?.includes("[NeevJS] Offline")) {
|
|
422
|
+
const currentData = cache.get(url) ?? [];
|
|
423
|
+
const filteredData = currentData.filter((item) => String(item.id) !== String(id));
|
|
424
|
+
cache.set(url, filteredData);
|
|
425
|
+
setData(filteredData);
|
|
426
|
+
emitChange(url, handleRemoteChange);
|
|
427
|
+
} else {
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}, [client, url, fetchData, handleRemoteChange]);
|
|
432
|
+
return { data, loading, error, create, update, remove, refresh: () => fetchData(true) };
|
|
178
433
|
}
|
|
179
434
|
|
|
180
435
|
// src/hooks/useAuth.ts
|
|
@@ -194,6 +449,8 @@ function useAuth() {
|
|
|
194
449
|
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
195
450
|
var syncState = {
|
|
196
451
|
pendingCount: 0,
|
|
452
|
+
syncing: false,
|
|
453
|
+
errors: [],
|
|
197
454
|
listeners: /* @__PURE__ */ new Set(),
|
|
198
455
|
notify() {
|
|
199
456
|
this.listeners.forEach((fn) => fn());
|
|
@@ -202,18 +459,19 @@ var syncState = {
|
|
|
202
459
|
function useSyncStatus() {
|
|
203
460
|
const [isOffline, setIsOffline] = useState2(!navigator.onLine);
|
|
204
461
|
const [pending, setPending] = useState2(syncState.pendingCount);
|
|
205
|
-
const [syncing, setSyncing] = useState2(
|
|
462
|
+
const [syncing, setSyncing] = useState2(syncState.syncing);
|
|
463
|
+
const [errors, setErrors] = useState2(syncState.errors);
|
|
206
464
|
useEffect2(() => {
|
|
207
465
|
function onOnline() {
|
|
208
466
|
setIsOffline(false);
|
|
209
|
-
setSyncing(true);
|
|
210
|
-
setTimeout(() => setSyncing(false), 2e3);
|
|
211
467
|
}
|
|
212
468
|
function onOffline() {
|
|
213
469
|
setIsOffline(true);
|
|
214
470
|
}
|
|
215
471
|
function onSyncUpdate() {
|
|
216
472
|
setPending(syncState.pendingCount);
|
|
473
|
+
setSyncing(syncState.syncing);
|
|
474
|
+
setErrors([...syncState.errors]);
|
|
217
475
|
}
|
|
218
476
|
window.addEventListener("online", onOnline);
|
|
219
477
|
window.addEventListener("offline", onOffline);
|
|
@@ -224,7 +482,95 @@ function useSyncStatus() {
|
|
|
224
482
|
syncState.listeners.delete(onSyncUpdate);
|
|
225
483
|
};
|
|
226
484
|
}, []);
|
|
227
|
-
|
|
485
|
+
function clearErrors() {
|
|
486
|
+
syncState.errors = [];
|
|
487
|
+
syncState.notify();
|
|
488
|
+
}
|
|
489
|
+
return { isOffline, pending, syncing, errors, clearErrors };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/hooks/useStore.ts
|
|
493
|
+
import { useCallback as useCallback2, useSyncExternalStore } from "react";
|
|
494
|
+
var stores = /* @__PURE__ */ new Map();
|
|
495
|
+
var listeners2 = /* @__PURE__ */ new Map();
|
|
496
|
+
function subscribe(key, callback) {
|
|
497
|
+
if (!listeners2.has(key)) listeners2.set(key, /* @__PURE__ */ new Set());
|
|
498
|
+
listeners2.get(key).add(callback);
|
|
499
|
+
return () => {
|
|
500
|
+
listeners2.get(key).delete(callback);
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function emitChange2(key) {
|
|
504
|
+
listeners2.get(key)?.forEach((fn) => fn());
|
|
505
|
+
}
|
|
506
|
+
var PERSIST_PREFIX = "neev_store_";
|
|
507
|
+
var SESSION_PREFIX = "neev_session_";
|
|
508
|
+
function readFromStorage(key, backend, fallback) {
|
|
509
|
+
try {
|
|
510
|
+
const prefix = backend === "sessionStorage" ? SESSION_PREFIX : PERSIST_PREFIX;
|
|
511
|
+
const raw = window[backend].getItem(prefix + key);
|
|
512
|
+
if (raw === null) return fallback;
|
|
513
|
+
const entry = JSON.parse(raw);
|
|
514
|
+
if (entry.expiresAt !== void 0 && Date.now() > entry.expiresAt) {
|
|
515
|
+
window[backend].removeItem(prefix + key);
|
|
516
|
+
return fallback;
|
|
517
|
+
}
|
|
518
|
+
return entry.value;
|
|
519
|
+
} catch {
|
|
520
|
+
return fallback;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function writeToStorage(key, value, backend, ttl) {
|
|
524
|
+
try {
|
|
525
|
+
const prefix = backend === "sessionStorage" ? SESSION_PREFIX : PERSIST_PREFIX;
|
|
526
|
+
const entry = {
|
|
527
|
+
value,
|
|
528
|
+
expiresAt: ttl !== void 0 ? Date.now() + ttl : void 0
|
|
529
|
+
};
|
|
530
|
+
window[backend].setItem(prefix + key, JSON.stringify(entry));
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function useStore(key, initialValue, options = {}) {
|
|
535
|
+
const { persist = false, session = false, ttl } = options;
|
|
536
|
+
const backend = persist ? "localStorage" : session ? "sessionStorage" : null;
|
|
537
|
+
if (!stores.has(key)) {
|
|
538
|
+
const resolved = backend ? readFromStorage(key, backend, initialValue) : initialValue;
|
|
539
|
+
stores.set(key, resolved);
|
|
540
|
+
}
|
|
541
|
+
const snapshot = useSyncExternalStore(
|
|
542
|
+
useCallback2((callback) => subscribe(key, callback), [key]),
|
|
543
|
+
useCallback2(() => stores.get(key), [key]),
|
|
544
|
+
useCallback2(() => initialValue, [])
|
|
545
|
+
// eslint-disable-line react-hooks/exhaustive-deps
|
|
546
|
+
);
|
|
547
|
+
const setValue = useCallback2(
|
|
548
|
+
(updater) => {
|
|
549
|
+
const current = stores.get(key);
|
|
550
|
+
const next = typeof updater === "function" ? updater(current) : updater;
|
|
551
|
+
stores.set(key, next);
|
|
552
|
+
if (backend) {
|
|
553
|
+
writeToStorage(key, next, backend, ttl);
|
|
554
|
+
}
|
|
555
|
+
emitChange2(key);
|
|
556
|
+
},
|
|
557
|
+
[key, backend, ttl]
|
|
558
|
+
);
|
|
559
|
+
return [snapshot, setValue];
|
|
560
|
+
}
|
|
561
|
+
function getStore(key) {
|
|
562
|
+
return stores.get(key);
|
|
563
|
+
}
|
|
564
|
+
function setStore(key, value) {
|
|
565
|
+
stores.set(key, value);
|
|
566
|
+
emitChange2(key);
|
|
567
|
+
}
|
|
568
|
+
function clearPersistedStore(key) {
|
|
569
|
+
try {
|
|
570
|
+
localStorage.removeItem(PERSIST_PREFIX + key);
|
|
571
|
+
sessionStorage.removeItem(SESSION_PREFIX + key);
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
228
574
|
}
|
|
229
575
|
|
|
230
576
|
// src/components/Table.tsx
|
|
@@ -234,37 +580,44 @@ function Table({
|
|
|
234
580
|
columns,
|
|
235
581
|
onEdit,
|
|
236
582
|
onDelete,
|
|
237
|
-
emptyMessage = "No records found."
|
|
583
|
+
emptyMessage = "No records found.",
|
|
584
|
+
classNames = {},
|
|
585
|
+
styles = {},
|
|
586
|
+
unstyled = false
|
|
238
587
|
}) {
|
|
239
588
|
const { data, loading, error } = useModel(model);
|
|
240
589
|
if (loading) {
|
|
241
|
-
return /* @__PURE__ */ jsx2("div", { className: "neev-table-loading", children: /* @__PURE__ */ jsx2("p", { children: "Loading..." }) });
|
|
590
|
+
return /* @__PURE__ */ jsx2("div", { className: classNames.loadingState ?? "neev-table-loading", style: styles.loadingState, children: /* @__PURE__ */ jsx2("p", { children: "Loading..." }) });
|
|
242
591
|
}
|
|
243
592
|
if (error) {
|
|
244
|
-
return /* @__PURE__ */ jsx2("div", { className: "neev-table-error", children: /* @__PURE__ */ jsxs("p", { children: [
|
|
593
|
+
return /* @__PURE__ */ jsx2("div", { className: classNames.errorState ?? "neev-table-error", style: styles.errorState, children: /* @__PURE__ */ jsxs("p", { children: [
|
|
245
594
|
"Error: ",
|
|
246
595
|
error.message
|
|
247
596
|
] }) });
|
|
248
597
|
}
|
|
249
598
|
if (data.length === 0) {
|
|
250
|
-
return /* @__PURE__ */ jsx2("div", { className: "neev-table-empty", children: /* @__PURE__ */ jsx2("p", { children: emptyMessage }) });
|
|
599
|
+
return /* @__PURE__ */ jsx2("div", { className: classNames.emptyState ?? "neev-table-empty", style: styles.emptyState, children: /* @__PURE__ */ jsx2("p", { children: emptyMessage }) });
|
|
251
600
|
}
|
|
252
601
|
const resolvedColumns = columns ?? Object.keys(data[0]).map((key) => ({
|
|
253
602
|
key,
|
|
254
603
|
label: key.charAt(0).toUpperCase() + key.slice(1)
|
|
255
604
|
}));
|
|
256
|
-
return /* @__PURE__ */ jsx2("div", { className: "neev-table-wrapper", style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxs("table", { className: "neev-table", style: { width: "100%", borderCollapse: "collapse" }, children: [
|
|
257
|
-
/* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
605
|
+
return /* @__PURE__ */ jsx2("div", { className: classNames.root ?? "neev-table-wrapper", style: { ...!unstyled ? { overflowX: "auto" } : {}, ...styles.root }, children: /* @__PURE__ */ jsxs("table", { className: classNames.table ?? "neev-table", style: { ...!unstyled ? { width: "100%", borderCollapse: "collapse" } : {}, ...styles.table }, children: [
|
|
606
|
+
/* @__PURE__ */ jsx2("thead", { className: classNames.thead, style: styles.thead, children: /* @__PURE__ */ jsxs("tr", { className: classNames.tr, style: styles.tr, children: [
|
|
258
607
|
resolvedColumns.map((col) => /* @__PURE__ */ jsx2(
|
|
259
608
|
"th",
|
|
260
609
|
{
|
|
610
|
+
className: classNames.th,
|
|
261
611
|
style: {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
612
|
+
...!unstyled ? {
|
|
613
|
+
textAlign: "left",
|
|
614
|
+
padding: "10px 12px",
|
|
615
|
+
borderBottom: "2px solid #e5e7eb",
|
|
616
|
+
fontWeight: 600,
|
|
617
|
+
color: "#374151",
|
|
618
|
+
background: "#f9fafb"
|
|
619
|
+
} : {},
|
|
620
|
+
...styles.th
|
|
268
621
|
},
|
|
269
622
|
children: col.label ?? col.key
|
|
270
623
|
},
|
|
@@ -273,45 +626,55 @@ function Table({
|
|
|
273
626
|
(onEdit || onDelete) && /* @__PURE__ */ jsx2(
|
|
274
627
|
"th",
|
|
275
628
|
{
|
|
629
|
+
className: classNames.th,
|
|
276
630
|
style: {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
631
|
+
...!unstyled ? {
|
|
632
|
+
textAlign: "left",
|
|
633
|
+
padding: "10px 12px",
|
|
634
|
+
borderBottom: "2px solid #e5e7eb",
|
|
635
|
+
fontWeight: 600,
|
|
636
|
+
color: "#374151",
|
|
637
|
+
background: "#f9fafb"
|
|
638
|
+
} : {},
|
|
639
|
+
...styles.th
|
|
283
640
|
},
|
|
284
641
|
children: "Actions"
|
|
285
642
|
}
|
|
286
643
|
)
|
|
287
644
|
] }) }),
|
|
288
|
-
/* @__PURE__ */ jsx2("tbody", { children: data.map((row, rowIndex) => /* @__PURE__ */ jsxs(
|
|
645
|
+
/* @__PURE__ */ jsx2("tbody", { className: classNames.tbody, style: styles.tbody, children: data.map((row, rowIndex) => /* @__PURE__ */ jsxs(
|
|
289
646
|
"tr",
|
|
290
647
|
{
|
|
291
|
-
|
|
648
|
+
className: classNames.tr,
|
|
649
|
+
style: { ...!unstyled ? { borderBottom: "1px solid #e5e7eb" } : {}, ...styles.tr },
|
|
292
650
|
children: [
|
|
293
651
|
resolvedColumns.map((col) => /* @__PURE__ */ jsx2(
|
|
294
652
|
"td",
|
|
295
653
|
{
|
|
296
|
-
|
|
654
|
+
className: classNames.td,
|
|
655
|
+
style: { ...!unstyled ? { padding: "10px 12px", color: "#1f2937", verticalAlign: "middle" } : {}, ...styles.td },
|
|
297
656
|
children: col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")
|
|
298
657
|
},
|
|
299
658
|
col.key
|
|
300
659
|
)),
|
|
301
|
-
(onEdit || onDelete) && /* @__PURE__ */ jsxs("td", { style: { padding: "10px 12px", verticalAlign: "middle" }, children: [
|
|
660
|
+
(onEdit || onDelete) && /* @__PURE__ */ jsxs("td", { className: classNames.actions ?? classNames.td, style: { ...!unstyled ? { padding: "10px 12px", verticalAlign: "middle" } : {}, ...styles.td, ...styles.actions }, children: [
|
|
302
661
|
onEdit && /* @__PURE__ */ jsx2(
|
|
303
662
|
"button",
|
|
304
663
|
{
|
|
305
664
|
onClick: () => onEdit(row),
|
|
665
|
+
className: classNames.editButton,
|
|
306
666
|
style: {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
667
|
+
...!unstyled ? {
|
|
668
|
+
marginRight: 8,
|
|
669
|
+
padding: "4px 10px",
|
|
670
|
+
fontSize: 13,
|
|
671
|
+
cursor: "pointer",
|
|
672
|
+
background: "#3b82f6",
|
|
673
|
+
color: "#fff",
|
|
674
|
+
border: "none",
|
|
675
|
+
borderRadius: 4
|
|
676
|
+
} : {},
|
|
677
|
+
...styles.editButton
|
|
315
678
|
},
|
|
316
679
|
children: "Edit"
|
|
317
680
|
}
|
|
@@ -320,14 +683,18 @@ function Table({
|
|
|
320
683
|
"button",
|
|
321
684
|
{
|
|
322
685
|
onClick: () => onDelete(row),
|
|
686
|
+
className: classNames.deleteButton,
|
|
323
687
|
style: {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
688
|
+
...!unstyled ? {
|
|
689
|
+
padding: "4px 10px",
|
|
690
|
+
fontSize: 13,
|
|
691
|
+
cursor: "pointer",
|
|
692
|
+
background: "#ef4444",
|
|
693
|
+
color: "#fff",
|
|
694
|
+
border: "none",
|
|
695
|
+
borderRadius: 4
|
|
696
|
+
} : {},
|
|
697
|
+
...styles.deleteButton
|
|
331
698
|
},
|
|
332
699
|
children: "Delete"
|
|
333
700
|
}
|
|
@@ -351,22 +718,48 @@ function Form({
|
|
|
351
718
|
onSuccess,
|
|
352
719
|
onError,
|
|
353
720
|
submitLabel = "Submit",
|
|
354
|
-
|
|
721
|
+
classNames = {},
|
|
722
|
+
styles = {},
|
|
723
|
+
unstyled = false,
|
|
724
|
+
transformPayload,
|
|
725
|
+
onSubmitOverride,
|
|
726
|
+
children,
|
|
727
|
+
fieldErrors: externalFieldErrors = {},
|
|
728
|
+
validate
|
|
355
729
|
}) {
|
|
356
730
|
const { create, update } = useModel(model);
|
|
357
731
|
const [submitting, setSubmitting] = useState3(false);
|
|
358
732
|
const [formError, setFormError] = useState3(null);
|
|
733
|
+
const [internalFieldErrors, setInternalFieldErrors] = useState3({});
|
|
734
|
+
const allFieldErrors = { ...internalFieldErrors, ...externalFieldErrors };
|
|
359
735
|
async function handleSubmit(e) {
|
|
360
736
|
e.preventDefault();
|
|
361
737
|
setSubmitting(true);
|
|
362
738
|
setFormError(null);
|
|
739
|
+
setInternalFieldErrors({});
|
|
363
740
|
const formData = new FormData(e.currentTarget);
|
|
364
741
|
const payload = Object.fromEntries(formData.entries());
|
|
365
742
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
743
|
+
let finalPayload = payload;
|
|
744
|
+
if (validate) {
|
|
745
|
+
const validationErrors = validate(finalPayload);
|
|
746
|
+
if (validationErrors && Object.keys(validationErrors).length > 0) {
|
|
747
|
+
setInternalFieldErrors(validationErrors);
|
|
748
|
+
setSubmitting(false);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (transformPayload) {
|
|
753
|
+
finalPayload = await transformPayload(finalPayload);
|
|
754
|
+
}
|
|
755
|
+
if (onSubmitOverride) {
|
|
756
|
+
await onSubmitOverride(finalPayload);
|
|
368
757
|
} else {
|
|
369
|
-
|
|
758
|
+
if (editId !== void 0) {
|
|
759
|
+
await update(editId, finalPayload);
|
|
760
|
+
} else {
|
|
761
|
+
await create(finalPayload);
|
|
762
|
+
}
|
|
370
763
|
}
|
|
371
764
|
onSuccess?.();
|
|
372
765
|
} catch (err) {
|
|
@@ -377,7 +770,7 @@ function Form({
|
|
|
377
770
|
setSubmitting(false);
|
|
378
771
|
}
|
|
379
772
|
}
|
|
380
|
-
const
|
|
773
|
+
const baseInputStyle = unstyled ? {} : {
|
|
381
774
|
display: "block",
|
|
382
775
|
width: "100%",
|
|
383
776
|
padding: "8px 10px",
|
|
@@ -388,29 +781,33 @@ function Form({
|
|
|
388
781
|
boxSizing: "border-box",
|
|
389
782
|
marginTop: 4
|
|
390
783
|
};
|
|
391
|
-
const
|
|
784
|
+
const baseLabelStyle = unstyled ? {} : {
|
|
392
785
|
display: "block",
|
|
393
786
|
fontSize: 13,
|
|
394
787
|
fontWeight: 500,
|
|
395
788
|
color: "#374151",
|
|
396
789
|
marginBottom: 12
|
|
397
790
|
};
|
|
398
|
-
return /* @__PURE__ */ jsxs2("form", { onSubmit: (e) => void handleSubmit(e), style: { width: "100%" }, children: [
|
|
791
|
+
return /* @__PURE__ */ jsxs2("form", { className: classNames.root, onSubmit: (e) => void handleSubmit(e), style: { width: "100%", ...styles.root }, children: [
|
|
399
792
|
formError && /* @__PURE__ */ jsx3(
|
|
400
793
|
"div",
|
|
401
794
|
{
|
|
795
|
+
className: classNames.error,
|
|
402
796
|
style: {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
797
|
+
...!unstyled ? {
|
|
798
|
+
padding: "8px 12px",
|
|
799
|
+
marginBottom: 12,
|
|
800
|
+
background: "#fee2e2",
|
|
801
|
+
color: "#dc2626",
|
|
802
|
+
borderRadius: 6,
|
|
803
|
+
fontSize: 13
|
|
804
|
+
} : {},
|
|
805
|
+
...styles.error
|
|
409
806
|
},
|
|
410
807
|
children: formError
|
|
411
808
|
}
|
|
412
809
|
),
|
|
413
|
-
fields?.map((field) => /* @__PURE__ */ jsxs2("label", { style:
|
|
810
|
+
fields?.map((field) => /* @__PURE__ */ jsxs2("label", { className: classNames.label, style: { ...baseLabelStyle, ...styles.label }, children: [
|
|
414
811
|
field.label ?? field.name,
|
|
415
812
|
field.required && /* @__PURE__ */ jsx3("span", { style: { color: "#ef4444", marginLeft: 2 }, children: "*" }),
|
|
416
813
|
field.type === "textarea" ? /* @__PURE__ */ jsx3(
|
|
@@ -420,7 +817,8 @@ function Form({
|
|
|
420
817
|
placeholder: field.placeholder,
|
|
421
818
|
required: field.required,
|
|
422
819
|
defaultValue: String(initialValues[field.name] ?? ""),
|
|
423
|
-
|
|
820
|
+
className: classNames.textarea ?? classNames.input,
|
|
821
|
+
style: { ...baseInputStyle, minHeight: 80, resize: "vertical", ...styles.input, ...styles.textarea }
|
|
424
822
|
}
|
|
425
823
|
) : field.type === "select" ? /* @__PURE__ */ jsxs2(
|
|
426
824
|
"select",
|
|
@@ -428,7 +826,8 @@ function Form({
|
|
|
428
826
|
name: field.name,
|
|
429
827
|
required: field.required,
|
|
430
828
|
defaultValue: String(initialValues[field.name] ?? ""),
|
|
431
|
-
|
|
829
|
+
className: classNames.select ?? classNames.input,
|
|
830
|
+
style: { ...baseInputStyle, ...styles.input, ...styles.select },
|
|
432
831
|
children: [
|
|
433
832
|
/* @__PURE__ */ jsx3("option", { value: "", children: "Select..." }),
|
|
434
833
|
field.options?.map((opt) => /* @__PURE__ */ jsx3("option", { value: opt.value, children: opt.label }, opt.value))
|
|
@@ -442,7 +841,24 @@ function Form({
|
|
|
442
841
|
placeholder: field.placeholder,
|
|
443
842
|
required: field.required,
|
|
444
843
|
defaultValue: String(initialValues[field.name] ?? ""),
|
|
445
|
-
|
|
844
|
+
className: classNames.input,
|
|
845
|
+
style: { ...baseInputStyle, ...styles.input }
|
|
846
|
+
}
|
|
847
|
+
),
|
|
848
|
+
allFieldErrors[field.name] && /* @__PURE__ */ jsx3(
|
|
849
|
+
"span",
|
|
850
|
+
{
|
|
851
|
+
className: classNames.fieldError,
|
|
852
|
+
style: {
|
|
853
|
+
...!unstyled ? {
|
|
854
|
+
display: "block",
|
|
855
|
+
marginTop: 4,
|
|
856
|
+
fontSize: 12,
|
|
857
|
+
color: "#dc2626"
|
|
858
|
+
} : {},
|
|
859
|
+
...styles.fieldError
|
|
860
|
+
},
|
|
861
|
+
children: allFieldErrors[field.name]
|
|
446
862
|
}
|
|
447
863
|
)
|
|
448
864
|
] }, field.name)),
|
|
@@ -452,17 +868,21 @@ function Form({
|
|
|
452
868
|
{
|
|
453
869
|
type: "submit",
|
|
454
870
|
disabled: submitting,
|
|
871
|
+
className: classNames.submitButton,
|
|
455
872
|
style: {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
873
|
+
...!unstyled ? {
|
|
874
|
+
marginTop: 8,
|
|
875
|
+
padding: "9px 20px",
|
|
876
|
+
background: submitting ? "#9ca3af" : "#111827",
|
|
877
|
+
color: "#fff",
|
|
878
|
+
border: "none",
|
|
879
|
+
borderRadius: 6,
|
|
880
|
+
fontSize: 14,
|
|
881
|
+
fontWeight: 600,
|
|
882
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
883
|
+
width: "100%"
|
|
884
|
+
} : {},
|
|
885
|
+
...styles.submitButton
|
|
466
886
|
},
|
|
467
887
|
children: submitting ? "Saving..." : submitLabel
|
|
468
888
|
}
|
|
@@ -473,34 +893,86 @@ function Form({
|
|
|
473
893
|
// src/components/Protected.tsx
|
|
474
894
|
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
475
895
|
import { Fragment, jsx as jsx4 } from "react/jsx-runtime";
|
|
476
|
-
function Protected({ children, fallback, role }) {
|
|
896
|
+
function Protected({ children, loadingFallback = null, fallback, role }) {
|
|
477
897
|
const { isAuthenticated, user } = useAuth();
|
|
478
|
-
const
|
|
479
|
-
const [
|
|
898
|
+
const hasToken = isAuthenticated();
|
|
899
|
+
const [checking, setChecking] = useState4(hasToken && !!role);
|
|
900
|
+
const [allowed, setAllowed] = useState4(hasToken && !role);
|
|
480
901
|
useEffect3(() => {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
902
|
+
if (!hasToken) {
|
|
903
|
+
setAllowed(false);
|
|
904
|
+
setChecking(false);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (!role) {
|
|
908
|
+
setAllowed(true);
|
|
909
|
+
setChecking(false);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
let cancelled = false;
|
|
913
|
+
async function checkRole() {
|
|
914
|
+
const u = await user();
|
|
915
|
+
if (!cancelled) {
|
|
489
916
|
setAllowed(u?.role === role);
|
|
490
|
-
|
|
491
|
-
setAllowed(true);
|
|
917
|
+
setChecking(false);
|
|
492
918
|
}
|
|
493
|
-
setChecking(false);
|
|
494
919
|
}
|
|
495
|
-
void
|
|
496
|
-
|
|
497
|
-
|
|
920
|
+
void checkRole();
|
|
921
|
+
return () => {
|
|
922
|
+
cancelled = true;
|
|
923
|
+
};
|
|
924
|
+
}, [hasToken, role, user]);
|
|
925
|
+
if (checking) {
|
|
926
|
+
return /* @__PURE__ */ jsx4(Fragment, { children: loadingFallback });
|
|
927
|
+
}
|
|
498
928
|
if (!allowed) {
|
|
499
929
|
return /* @__PURE__ */ jsx4(Fragment, { children: fallback ?? /* @__PURE__ */ jsx4("div", { style: { padding: 24, textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ jsx4("p", { children: "You are not authorized to view this page." }) }) });
|
|
500
930
|
}
|
|
501
931
|
return /* @__PURE__ */ jsx4(Fragment, { children });
|
|
502
932
|
}
|
|
503
933
|
|
|
934
|
+
// src/components/NeevBoundary.tsx
|
|
935
|
+
import { Component, Suspense } from "react";
|
|
936
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
937
|
+
var NeevErrorBoundary = class extends Component {
|
|
938
|
+
constructor(props) {
|
|
939
|
+
super(props);
|
|
940
|
+
this.state = { hasError: false, error: null };
|
|
941
|
+
}
|
|
942
|
+
static getDerivedStateFromError(error) {
|
|
943
|
+
return { hasError: true, error };
|
|
944
|
+
}
|
|
945
|
+
componentDidCatch(error, errorInfo) {
|
|
946
|
+
console.error("NeevBoundary caught an error:", error, errorInfo);
|
|
947
|
+
}
|
|
948
|
+
resetErrorBoundary = () => {
|
|
949
|
+
this.setState({ hasError: false, error: null });
|
|
950
|
+
};
|
|
951
|
+
render() {
|
|
952
|
+
if (this.state.hasError && this.state.error) {
|
|
953
|
+
if (this.props.fallback) {
|
|
954
|
+
return this.props.fallback(this.state.error, this.resetErrorBoundary);
|
|
955
|
+
}
|
|
956
|
+
return /* @__PURE__ */ jsxs3("div", { style: { padding: "1rem", background: "#fee2e2", color: "#b91c1c", borderRadius: "8px", fontFamily: "sans-serif" }, children: [
|
|
957
|
+
/* @__PURE__ */ jsx5("h3", { style: { margin: "0 0 8px 0" }, children: "Data Error" }),
|
|
958
|
+
/* @__PURE__ */ jsx5("p", { style: { margin: 0, fontSize: "14px" }, children: this.state.error.message }),
|
|
959
|
+
/* @__PURE__ */ jsx5(
|
|
960
|
+
"button",
|
|
961
|
+
{
|
|
962
|
+
onClick: this.resetErrorBoundary,
|
|
963
|
+
style: { marginTop: "12px", padding: "6px 12px", background: "#b91c1c", color: "white", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "13px" },
|
|
964
|
+
children: "Try again"
|
|
965
|
+
}
|
|
966
|
+
)
|
|
967
|
+
] });
|
|
968
|
+
}
|
|
969
|
+
return this.props.children;
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
function NeevBoundary({ children, loadingFallback, errorFallback }) {
|
|
973
|
+
return /* @__PURE__ */ jsx5(NeevErrorBoundary, { fallback: errorFallback, children: /* @__PURE__ */ jsx5(Suspense, { fallback: loadingFallback ?? /* @__PURE__ */ jsx5("div", { style: { padding: "1rem", color: "#6b7280", fontFamily: "sans-serif" }, children: "Loading data..." }), children }) });
|
|
974
|
+
}
|
|
975
|
+
|
|
504
976
|
// src/plugins/AuthPlugin.ts
|
|
505
977
|
var AuthPlugin = {
|
|
506
978
|
name: "auth",
|
|
@@ -520,18 +992,19 @@ var AuthPlugin = {
|
|
|
520
992
|
};
|
|
521
993
|
|
|
522
994
|
// src/plugins/LoggerPlugin.ts
|
|
995
|
+
import { VERSION } from "@neevjs/shared";
|
|
523
996
|
var LoggerPlugin = {
|
|
524
997
|
name: "logger",
|
|
525
998
|
onRequest(req) {
|
|
526
|
-
console.log(`[NeevJS] \u2192 ${req.options.method ?? "GET"} ${req.url}`);
|
|
999
|
+
console.log(`[NeevJS v${VERSION}] \u2192 ${req.options.method ?? "GET"} ${req.url}`);
|
|
527
1000
|
return req;
|
|
528
1001
|
},
|
|
529
|
-
onResponse(res) {
|
|
530
|
-
console.log(`[NeevJS] \u2190 ${res.status} ${
|
|
1002
|
+
onResponse(res, req) {
|
|
1003
|
+
console.log(`[NeevJS v${VERSION}] \u2190 ${res.status} ${req.options.method ?? "GET"} ${req.url}`);
|
|
531
1004
|
return res;
|
|
532
1005
|
},
|
|
533
|
-
onError(err) {
|
|
534
|
-
console.error(
|
|
1006
|
+
onError(err, req) {
|
|
1007
|
+
console.error(`[NeevJS v${VERSION}] \u2717 Error on ${req?.url ?? "unknown"}:`, err.message);
|
|
535
1008
|
}
|
|
536
1009
|
};
|
|
537
1010
|
|
|
@@ -543,25 +1016,36 @@ function createCachePlugin(options = {}) {
|
|
|
543
1016
|
name: "cache",
|
|
544
1017
|
onRequest(req) {
|
|
545
1018
|
const method = (req.options.method ?? "GET").toUpperCase();
|
|
546
|
-
if (method !== "GET")
|
|
1019
|
+
if (method !== "GET") {
|
|
1020
|
+
store.delete(req.url);
|
|
1021
|
+
const parts = req.url.split("/");
|
|
1022
|
+
if (parts.length > 2) {
|
|
1023
|
+
const baseUrl = "/" + parts[1];
|
|
1024
|
+
store.delete(baseUrl);
|
|
1025
|
+
}
|
|
1026
|
+
return req;
|
|
1027
|
+
}
|
|
547
1028
|
const entry = store.get(req.url);
|
|
548
1029
|
if (entry && Date.now() < entry.expiresAt) {
|
|
549
1030
|
const cached = new Response(JSON.stringify(entry.data), {
|
|
550
1031
|
status: 200,
|
|
551
|
-
headers: {
|
|
1032
|
+
headers: {
|
|
1033
|
+
"Content-Type": "application/json",
|
|
1034
|
+
"X-Neev-Cache": "HIT"
|
|
1035
|
+
}
|
|
552
1036
|
});
|
|
553
1037
|
return { ...req, _cachedResponse: cached };
|
|
554
1038
|
}
|
|
555
1039
|
return req;
|
|
556
1040
|
},
|
|
557
|
-
async onResponse(res) {
|
|
558
|
-
if (res.headers.get("X-Neev-Cache") === "HIT") return res;
|
|
559
|
-
const
|
|
560
|
-
if (
|
|
1041
|
+
async onResponse(res, req) {
|
|
1042
|
+
if (res.headers.get("X-Neev-Cache") === "HIT" || !res.ok) return res;
|
|
1043
|
+
const method = (req.options.method ?? "GET").toUpperCase();
|
|
1044
|
+
if (method !== "GET") return res;
|
|
561
1045
|
try {
|
|
562
1046
|
const clone = res.clone();
|
|
563
1047
|
const data = await clone.json();
|
|
564
|
-
store.set(url, { data, expiresAt: Date.now() + ttl });
|
|
1048
|
+
store.set(req.url, { data, expiresAt: Date.now() + ttl });
|
|
565
1049
|
} catch {
|
|
566
1050
|
}
|
|
567
1051
|
return res;
|
|
@@ -592,27 +1076,54 @@ function saveQueue(queue) {
|
|
|
592
1076
|
}
|
|
593
1077
|
function createOfflinePlugin() {
|
|
594
1078
|
let client;
|
|
1079
|
+
let onlineHandler = null;
|
|
595
1080
|
async function processQueue() {
|
|
596
1081
|
if (!navigator.onLine) return;
|
|
597
1082
|
const queue = loadQueue();
|
|
598
1083
|
if (queue.length === 0) return;
|
|
1084
|
+
syncState.syncing = true;
|
|
1085
|
+
syncState.notify();
|
|
599
1086
|
const remaining = [];
|
|
1087
|
+
let hasChanges = false;
|
|
1088
|
+
const urlsToMutate = /* @__PURE__ */ new Set();
|
|
600
1089
|
for (const action of queue) {
|
|
601
1090
|
try {
|
|
602
1091
|
await client.request(action.url, action.options);
|
|
603
|
-
|
|
604
|
-
|
|
1092
|
+
hasChanges = true;
|
|
1093
|
+
const baseUrl = "/" + (action.url.split("/")[1] || "");
|
|
1094
|
+
urlsToMutate.add(baseUrl);
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
if (err.status && err.status >= 400 && err.status < 500) {
|
|
1097
|
+
syncState.errors.push(new Error(`Offline action on ${action.url} failed: ${err.message}`));
|
|
1098
|
+
hasChanges = true;
|
|
1099
|
+
const baseUrl = "/" + (action.url.split("/")[1] || "");
|
|
1100
|
+
urlsToMutate.add(baseUrl);
|
|
1101
|
+
} else {
|
|
1102
|
+
remaining.push({ ...action, retries: action.retries + 1 });
|
|
1103
|
+
}
|
|
605
1104
|
}
|
|
606
1105
|
}
|
|
607
1106
|
saveQueue(remaining);
|
|
1107
|
+
syncState.syncing = false;
|
|
1108
|
+
syncState.notify();
|
|
1109
|
+
if (hasChanges) {
|
|
1110
|
+
urlsToMutate.forEach((url) => mutateModel(url));
|
|
1111
|
+
}
|
|
608
1112
|
}
|
|
609
|
-
window.addEventListener("online", () => {
|
|
610
|
-
void processQueue();
|
|
611
|
-
});
|
|
612
1113
|
return {
|
|
613
1114
|
name: "offline",
|
|
614
1115
|
setup(c) {
|
|
615
1116
|
client = c;
|
|
1117
|
+
if (onlineHandler) {
|
|
1118
|
+
window.removeEventListener("online", onlineHandler);
|
|
1119
|
+
}
|
|
1120
|
+
onlineHandler = () => {
|
|
1121
|
+
void processQueue();
|
|
1122
|
+
};
|
|
1123
|
+
window.addEventListener("online", onlineHandler);
|
|
1124
|
+
if (navigator.onLine && loadQueue().length > 0) {
|
|
1125
|
+
setTimeout(() => void processQueue(), 500);
|
|
1126
|
+
}
|
|
616
1127
|
},
|
|
617
1128
|
onRequest(req) {
|
|
618
1129
|
const method = (req.options.method ?? "GET").toUpperCase();
|
|
@@ -641,16 +1152,23 @@ export {
|
|
|
641
1152
|
CachePlugin,
|
|
642
1153
|
Form,
|
|
643
1154
|
LoggerPlugin,
|
|
1155
|
+
NeevBoundary,
|
|
644
1156
|
NeevContext,
|
|
645
1157
|
NeevProvider,
|
|
646
1158
|
OfflinePlugin,
|
|
647
1159
|
Protected,
|
|
1160
|
+
SecureStore,
|
|
648
1161
|
Table,
|
|
1162
|
+
VERSION2 as VERSION,
|
|
1163
|
+
clearPersistedStore,
|
|
649
1164
|
createCachePlugin,
|
|
650
1165
|
createClient,
|
|
651
1166
|
createOfflinePlugin,
|
|
1167
|
+
getStore,
|
|
1168
|
+
setStore,
|
|
652
1169
|
useAuth,
|
|
653
1170
|
useModel,
|
|
654
1171
|
useNeevClient,
|
|
1172
|
+
useStore,
|
|
655
1173
|
useSyncStatus
|
|
656
1174
|
};
|