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