@neevjs/client 0.0.1
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/README.md +48 -0
- package/dist/index.d.mts +91 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +699 -0
- package/dist/index.mjs +656 -0
- package/package.json +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
// src/core/AuthClient.ts
|
|
2
|
+
var AuthClient = class {
|
|
3
|
+
client;
|
|
4
|
+
userData = null;
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
async login(email, password) {
|
|
9
|
+
const res = await this.client.request("/auth/login", {
|
|
10
|
+
method: "POST",
|
|
11
|
+
body: JSON.stringify({ email, password })
|
|
12
|
+
});
|
|
13
|
+
localStorage.setItem("neev_token", res.data.token);
|
|
14
|
+
this.userData = res.data.user;
|
|
15
|
+
}
|
|
16
|
+
async register(email, password, name) {
|
|
17
|
+
const res = await this.client.request("/auth/register", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
body: JSON.stringify({ email, password, name })
|
|
20
|
+
});
|
|
21
|
+
localStorage.setItem("neev_token", res.data.token);
|
|
22
|
+
this.userData = res.data.user;
|
|
23
|
+
}
|
|
24
|
+
logout() {
|
|
25
|
+
localStorage.removeItem("neev_token");
|
|
26
|
+
this.userData = null;
|
|
27
|
+
}
|
|
28
|
+
async user() {
|
|
29
|
+
if (!this.isAuthenticated()) return null;
|
|
30
|
+
if (this.userData) return this.userData;
|
|
31
|
+
const res = await this.client.request("/auth/me");
|
|
32
|
+
this.userData = res.data;
|
|
33
|
+
return this.userData;
|
|
34
|
+
}
|
|
35
|
+
isAuthenticated() {
|
|
36
|
+
return !!localStorage.getItem("neev_token");
|
|
37
|
+
}
|
|
38
|
+
getToken() {
|
|
39
|
+
return localStorage.getItem("neev_token");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/core/createClient.ts
|
|
44
|
+
function createClient(config = {}) {
|
|
45
|
+
const plugins = [];
|
|
46
|
+
const baseURL = config.baseURL ?? "";
|
|
47
|
+
async function request(url, options = {}) {
|
|
48
|
+
let req = { url, options };
|
|
49
|
+
for (const plugin of plugins) {
|
|
50
|
+
if (plugin.onRequest) {
|
|
51
|
+
req = await plugin.onRequest(req);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const headers = {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
...req.options.headers ?? {}
|
|
57
|
+
};
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await fetch(baseURL + req.url, {
|
|
61
|
+
method: req.options.method ?? "GET",
|
|
62
|
+
body: req.options.body,
|
|
63
|
+
headers
|
|
64
|
+
});
|
|
65
|
+
let finalResponse = response;
|
|
66
|
+
for (const plugin of plugins) {
|
|
67
|
+
if (plugin.onResponse) {
|
|
68
|
+
finalResponse = await plugin.onResponse(finalResponse);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!finalResponse.ok) {
|
|
72
|
+
const errorBody = await finalResponse.json().catch(() => ({ error: "Unknown error" }));
|
|
73
|
+
throw new Error(
|
|
74
|
+
typeof errorBody.error === "string" ? errorBody.error : `HTTP ${finalResponse.status}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return finalResponse.json();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
80
|
+
for (const plugin of plugins) {
|
|
81
|
+
plugin.onError?.(error);
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function use(plugin) {
|
|
87
|
+
plugins.push(plugin);
|
|
88
|
+
plugin.setup?.(client);
|
|
89
|
+
}
|
|
90
|
+
const client = {
|
|
91
|
+
request,
|
|
92
|
+
use,
|
|
93
|
+
auth: null
|
|
94
|
+
// set below after client is defined
|
|
95
|
+
};
|
|
96
|
+
client.auth = new AuthClient(client);
|
|
97
|
+
return client;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/core/NeevProvider.tsx
|
|
101
|
+
import { createContext, useContext } from "react";
|
|
102
|
+
import { jsx } from "react/jsx-runtime";
|
|
103
|
+
var NeevContext = createContext(null);
|
|
104
|
+
function NeevProvider({ client, children }) {
|
|
105
|
+
return /* @__PURE__ */ jsx(NeevContext.Provider, { value: client, children });
|
|
106
|
+
}
|
|
107
|
+
function useNeevClient() {
|
|
108
|
+
const client = useContext(NeevContext);
|
|
109
|
+
if (!client) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"[NeevJS] useNeevClient must be used inside <NeevProvider>. Make sure you have wrapped your app with <NeevProvider client={client}>."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return client;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/hooks/useModel.ts
|
|
118
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
119
|
+
var cache = /* @__PURE__ */ new Map();
|
|
120
|
+
function useModel(name) {
|
|
121
|
+
const client = useNeevClient();
|
|
122
|
+
const url = `/${name}`;
|
|
123
|
+
const [data, setData] = useState(() => cache.get(url) ?? []);
|
|
124
|
+
const [loading, setLoading] = useState(!cache.has(url));
|
|
125
|
+
const [error, setError] = useState(null);
|
|
126
|
+
const mountedRef = useRef(true);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
mountedRef.current = true;
|
|
129
|
+
return () => {
|
|
130
|
+
mountedRef.current = false;
|
|
131
|
+
};
|
|
132
|
+
}, []);
|
|
133
|
+
const fetchData = useCallback(async () => {
|
|
134
|
+
if (mountedRef.current) setLoading(true);
|
|
135
|
+
try {
|
|
136
|
+
const res = await client.request(url);
|
|
137
|
+
const rows = Array.isArray(res) ? res : res.data ?? [];
|
|
138
|
+
cache.set(url, rows);
|
|
139
|
+
if (mountedRef.current) {
|
|
140
|
+
setData(rows);
|
|
141
|
+
setError(null);
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (mountedRef.current) {
|
|
145
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
if (mountedRef.current) setLoading(false);
|
|
149
|
+
}
|
|
150
|
+
}, [client, url]);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
void fetchData();
|
|
153
|
+
}, [fetchData]);
|
|
154
|
+
const create = useCallback(async (payload) => {
|
|
155
|
+
await client.request(url, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
body: JSON.stringify(payload)
|
|
158
|
+
});
|
|
159
|
+
cache.delete(url);
|
|
160
|
+
await fetchData();
|
|
161
|
+
}, [client, url, fetchData]);
|
|
162
|
+
const update = useCallback(async (id, payload) => {
|
|
163
|
+
await client.request(`${url}/${id}`, {
|
|
164
|
+
method: "PUT",
|
|
165
|
+
body: JSON.stringify(payload)
|
|
166
|
+
});
|
|
167
|
+
cache.delete(url);
|
|
168
|
+
await fetchData();
|
|
169
|
+
}, [client, url, fetchData]);
|
|
170
|
+
const remove = useCallback(async (id) => {
|
|
171
|
+
await client.request(`${url}/${id}`, {
|
|
172
|
+
method: "DELETE"
|
|
173
|
+
});
|
|
174
|
+
cache.delete(url);
|
|
175
|
+
await fetchData();
|
|
176
|
+
}, [client, url, fetchData]);
|
|
177
|
+
return { data, loading, error, create, update, remove, refresh: fetchData };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/hooks/useAuth.ts
|
|
181
|
+
function useAuth() {
|
|
182
|
+
const client = useNeevClient();
|
|
183
|
+
const auth = client.auth;
|
|
184
|
+
return {
|
|
185
|
+
login: (email, password) => auth.login(email, password),
|
|
186
|
+
register: (email, password, name) => auth.register(email, password, name),
|
|
187
|
+
logout: () => auth.logout(),
|
|
188
|
+
user: () => auth.user(),
|
|
189
|
+
isAuthenticated: () => auth.isAuthenticated()
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/hooks/useSyncStatus.ts
|
|
194
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
195
|
+
var syncState = {
|
|
196
|
+
pendingCount: 0,
|
|
197
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
198
|
+
notify() {
|
|
199
|
+
this.listeners.forEach((fn) => fn());
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function useSyncStatus() {
|
|
203
|
+
const [isOffline, setIsOffline] = useState2(!navigator.onLine);
|
|
204
|
+
const [pending, setPending] = useState2(syncState.pendingCount);
|
|
205
|
+
const [syncing, setSyncing] = useState2(false);
|
|
206
|
+
useEffect2(() => {
|
|
207
|
+
function onOnline() {
|
|
208
|
+
setIsOffline(false);
|
|
209
|
+
setSyncing(true);
|
|
210
|
+
setTimeout(() => setSyncing(false), 2e3);
|
|
211
|
+
}
|
|
212
|
+
function onOffline() {
|
|
213
|
+
setIsOffline(true);
|
|
214
|
+
}
|
|
215
|
+
function onSyncUpdate() {
|
|
216
|
+
setPending(syncState.pendingCount);
|
|
217
|
+
}
|
|
218
|
+
window.addEventListener("online", onOnline);
|
|
219
|
+
window.addEventListener("offline", onOffline);
|
|
220
|
+
syncState.listeners.add(onSyncUpdate);
|
|
221
|
+
return () => {
|
|
222
|
+
window.removeEventListener("online", onOnline);
|
|
223
|
+
window.removeEventListener("offline", onOffline);
|
|
224
|
+
syncState.listeners.delete(onSyncUpdate);
|
|
225
|
+
};
|
|
226
|
+
}, []);
|
|
227
|
+
return { isOffline, pending, syncing };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/components/Table.tsx
|
|
231
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
232
|
+
function Table({
|
|
233
|
+
model,
|
|
234
|
+
columns,
|
|
235
|
+
onEdit,
|
|
236
|
+
onDelete,
|
|
237
|
+
emptyMessage = "No records found."
|
|
238
|
+
}) {
|
|
239
|
+
const { data, loading, error } = useModel(model);
|
|
240
|
+
if (loading) {
|
|
241
|
+
return /* @__PURE__ */ jsx2("div", { className: "neev-table-loading", children: /* @__PURE__ */ jsx2("p", { children: "Loading..." }) });
|
|
242
|
+
}
|
|
243
|
+
if (error) {
|
|
244
|
+
return /* @__PURE__ */ jsx2("div", { className: "neev-table-error", children: /* @__PURE__ */ jsxs("p", { children: [
|
|
245
|
+
"Error: ",
|
|
246
|
+
error.message
|
|
247
|
+
] }) });
|
|
248
|
+
}
|
|
249
|
+
if (data.length === 0) {
|
|
250
|
+
return /* @__PURE__ */ jsx2("div", { className: "neev-table-empty", children: /* @__PURE__ */ jsx2("p", { children: emptyMessage }) });
|
|
251
|
+
}
|
|
252
|
+
const resolvedColumns = columns ?? Object.keys(data[0]).map((key) => ({
|
|
253
|
+
key,
|
|
254
|
+
label: key.charAt(0).toUpperCase() + key.slice(1)
|
|
255
|
+
}));
|
|
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: [
|
|
258
|
+
resolvedColumns.map((col) => /* @__PURE__ */ jsx2(
|
|
259
|
+
"th",
|
|
260
|
+
{
|
|
261
|
+
style: {
|
|
262
|
+
textAlign: "left",
|
|
263
|
+
padding: "10px 12px",
|
|
264
|
+
borderBottom: "2px solid #e5e7eb",
|
|
265
|
+
fontWeight: 600,
|
|
266
|
+
color: "#374151",
|
|
267
|
+
background: "#f9fafb"
|
|
268
|
+
},
|
|
269
|
+
children: col.label ?? col.key
|
|
270
|
+
},
|
|
271
|
+
col.key
|
|
272
|
+
)),
|
|
273
|
+
(onEdit || onDelete) && /* @__PURE__ */ jsx2(
|
|
274
|
+
"th",
|
|
275
|
+
{
|
|
276
|
+
style: {
|
|
277
|
+
textAlign: "left",
|
|
278
|
+
padding: "10px 12px",
|
|
279
|
+
borderBottom: "2px solid #e5e7eb",
|
|
280
|
+
fontWeight: 600,
|
|
281
|
+
color: "#374151",
|
|
282
|
+
background: "#f9fafb"
|
|
283
|
+
},
|
|
284
|
+
children: "Actions"
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
] }) }),
|
|
288
|
+
/* @__PURE__ */ jsx2("tbody", { children: data.map((row, rowIndex) => /* @__PURE__ */ jsxs(
|
|
289
|
+
"tr",
|
|
290
|
+
{
|
|
291
|
+
style: { borderBottom: "1px solid #e5e7eb" },
|
|
292
|
+
children: [
|
|
293
|
+
resolvedColumns.map((col) => /* @__PURE__ */ jsx2(
|
|
294
|
+
"td",
|
|
295
|
+
{
|
|
296
|
+
style: { padding: "10px 12px", color: "#1f2937", verticalAlign: "middle" },
|
|
297
|
+
children: col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")
|
|
298
|
+
},
|
|
299
|
+
col.key
|
|
300
|
+
)),
|
|
301
|
+
(onEdit || onDelete) && /* @__PURE__ */ jsxs("td", { style: { padding: "10px 12px", verticalAlign: "middle" }, children: [
|
|
302
|
+
onEdit && /* @__PURE__ */ jsx2(
|
|
303
|
+
"button",
|
|
304
|
+
{
|
|
305
|
+
onClick: () => onEdit(row),
|
|
306
|
+
style: {
|
|
307
|
+
marginRight: 8,
|
|
308
|
+
padding: "4px 10px",
|
|
309
|
+
fontSize: 13,
|
|
310
|
+
cursor: "pointer",
|
|
311
|
+
background: "#3b82f6",
|
|
312
|
+
color: "#fff",
|
|
313
|
+
border: "none",
|
|
314
|
+
borderRadius: 4
|
|
315
|
+
},
|
|
316
|
+
children: "Edit"
|
|
317
|
+
}
|
|
318
|
+
),
|
|
319
|
+
onDelete && /* @__PURE__ */ jsx2(
|
|
320
|
+
"button",
|
|
321
|
+
{
|
|
322
|
+
onClick: () => onDelete(row),
|
|
323
|
+
style: {
|
|
324
|
+
padding: "4px 10px",
|
|
325
|
+
fontSize: 13,
|
|
326
|
+
cursor: "pointer",
|
|
327
|
+
background: "#ef4444",
|
|
328
|
+
color: "#fff",
|
|
329
|
+
border: "none",
|
|
330
|
+
borderRadius: 4
|
|
331
|
+
},
|
|
332
|
+
children: "Delete"
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
] })
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
String(row.id ?? rowIndex)
|
|
339
|
+
)) })
|
|
340
|
+
] }) });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/components/Form.tsx
|
|
344
|
+
import { useState as useState3 } from "react";
|
|
345
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
346
|
+
function Form({
|
|
347
|
+
model,
|
|
348
|
+
fields,
|
|
349
|
+
initialValues = {},
|
|
350
|
+
editId,
|
|
351
|
+
onSuccess,
|
|
352
|
+
onError,
|
|
353
|
+
submitLabel = "Submit",
|
|
354
|
+
children
|
|
355
|
+
}) {
|
|
356
|
+
const { create, update } = useModel(model);
|
|
357
|
+
const [submitting, setSubmitting] = useState3(false);
|
|
358
|
+
const [formError, setFormError] = useState3(null);
|
|
359
|
+
async function handleSubmit(e) {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
setSubmitting(true);
|
|
362
|
+
setFormError(null);
|
|
363
|
+
const formData = new FormData(e.currentTarget);
|
|
364
|
+
const payload = Object.fromEntries(formData.entries());
|
|
365
|
+
try {
|
|
366
|
+
if (editId !== void 0) {
|
|
367
|
+
await update(editId, payload);
|
|
368
|
+
} else {
|
|
369
|
+
await create(payload);
|
|
370
|
+
}
|
|
371
|
+
onSuccess?.();
|
|
372
|
+
} catch (err) {
|
|
373
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
374
|
+
setFormError(error.message);
|
|
375
|
+
onError?.(error);
|
|
376
|
+
} finally {
|
|
377
|
+
setSubmitting(false);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const inputStyle = {
|
|
381
|
+
display: "block",
|
|
382
|
+
width: "100%",
|
|
383
|
+
padding: "8px 10px",
|
|
384
|
+
fontSize: 14,
|
|
385
|
+
border: "1px solid #d1d5db",
|
|
386
|
+
borderRadius: 6,
|
|
387
|
+
outline: "none",
|
|
388
|
+
boxSizing: "border-box",
|
|
389
|
+
marginTop: 4
|
|
390
|
+
};
|
|
391
|
+
const labelStyle = {
|
|
392
|
+
display: "block",
|
|
393
|
+
fontSize: 13,
|
|
394
|
+
fontWeight: 500,
|
|
395
|
+
color: "#374151",
|
|
396
|
+
marginBottom: 12
|
|
397
|
+
};
|
|
398
|
+
return /* @__PURE__ */ jsxs2("form", { onSubmit: (e) => void handleSubmit(e), style: { width: "100%" }, children: [
|
|
399
|
+
formError && /* @__PURE__ */ jsx3(
|
|
400
|
+
"div",
|
|
401
|
+
{
|
|
402
|
+
style: {
|
|
403
|
+
padding: "8px 12px",
|
|
404
|
+
marginBottom: 12,
|
|
405
|
+
background: "#fee2e2",
|
|
406
|
+
color: "#dc2626",
|
|
407
|
+
borderRadius: 6,
|
|
408
|
+
fontSize: 13
|
|
409
|
+
},
|
|
410
|
+
children: formError
|
|
411
|
+
}
|
|
412
|
+
),
|
|
413
|
+
fields?.map((field) => /* @__PURE__ */ jsxs2("label", { style: labelStyle, children: [
|
|
414
|
+
field.label ?? field.name,
|
|
415
|
+
field.required && /* @__PURE__ */ jsx3("span", { style: { color: "#ef4444", marginLeft: 2 }, children: "*" }),
|
|
416
|
+
field.type === "textarea" ? /* @__PURE__ */ jsx3(
|
|
417
|
+
"textarea",
|
|
418
|
+
{
|
|
419
|
+
name: field.name,
|
|
420
|
+
placeholder: field.placeholder,
|
|
421
|
+
required: field.required,
|
|
422
|
+
defaultValue: String(initialValues[field.name] ?? ""),
|
|
423
|
+
style: { ...inputStyle, minHeight: 80, resize: "vertical" }
|
|
424
|
+
}
|
|
425
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxs2(
|
|
426
|
+
"select",
|
|
427
|
+
{
|
|
428
|
+
name: field.name,
|
|
429
|
+
required: field.required,
|
|
430
|
+
defaultValue: String(initialValues[field.name] ?? ""),
|
|
431
|
+
style: inputStyle,
|
|
432
|
+
children: [
|
|
433
|
+
/* @__PURE__ */ jsx3("option", { value: "", children: "Select..." }),
|
|
434
|
+
field.options?.map((opt) => /* @__PURE__ */ jsx3("option", { value: opt.value, children: opt.label }, opt.value))
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
) : /* @__PURE__ */ jsx3(
|
|
438
|
+
"input",
|
|
439
|
+
{
|
|
440
|
+
type: field.type ?? "text",
|
|
441
|
+
name: field.name,
|
|
442
|
+
placeholder: field.placeholder,
|
|
443
|
+
required: field.required,
|
|
444
|
+
defaultValue: String(initialValues[field.name] ?? ""),
|
|
445
|
+
style: inputStyle
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
] }, field.name)),
|
|
449
|
+
children,
|
|
450
|
+
/* @__PURE__ */ jsx3(
|
|
451
|
+
"button",
|
|
452
|
+
{
|
|
453
|
+
type: "submit",
|
|
454
|
+
disabled: submitting,
|
|
455
|
+
style: {
|
|
456
|
+
marginTop: 8,
|
|
457
|
+
padding: "9px 20px",
|
|
458
|
+
background: submitting ? "#9ca3af" : "#111827",
|
|
459
|
+
color: "#fff",
|
|
460
|
+
border: "none",
|
|
461
|
+
borderRadius: 6,
|
|
462
|
+
fontSize: 14,
|
|
463
|
+
fontWeight: 600,
|
|
464
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
465
|
+
width: "100%"
|
|
466
|
+
},
|
|
467
|
+
children: submitting ? "Saving..." : submitLabel
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
] });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/components/Protected.tsx
|
|
474
|
+
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
475
|
+
import { Fragment, jsx as jsx4 } from "react/jsx-runtime";
|
|
476
|
+
function Protected({ children, fallback, role }) {
|
|
477
|
+
const { isAuthenticated, user } = useAuth();
|
|
478
|
+
const [checking, setChecking] = useState4(true);
|
|
479
|
+
const [allowed, setAllowed] = useState4(false);
|
|
480
|
+
useEffect3(() => {
|
|
481
|
+
async function check() {
|
|
482
|
+
if (!isAuthenticated()) {
|
|
483
|
+
setAllowed(false);
|
|
484
|
+
setChecking(false);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (role) {
|
|
488
|
+
const u = await user();
|
|
489
|
+
setAllowed(u?.role === role);
|
|
490
|
+
} else {
|
|
491
|
+
setAllowed(true);
|
|
492
|
+
}
|
|
493
|
+
setChecking(false);
|
|
494
|
+
}
|
|
495
|
+
void check();
|
|
496
|
+
}, [isAuthenticated, role, user]);
|
|
497
|
+
if (checking) return /* @__PURE__ */ jsx4(Fragment, {});
|
|
498
|
+
if (!allowed) {
|
|
499
|
+
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
|
+
}
|
|
501
|
+
return /* @__PURE__ */ jsx4(Fragment, { children });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/plugins/AuthPlugin.ts
|
|
505
|
+
var AuthPlugin = {
|
|
506
|
+
name: "auth",
|
|
507
|
+
onRequest(req) {
|
|
508
|
+
const token = localStorage.getItem("neev_token");
|
|
509
|
+
return {
|
|
510
|
+
...req,
|
|
511
|
+
options: {
|
|
512
|
+
...req.options,
|
|
513
|
+
headers: {
|
|
514
|
+
...req.options.headers ?? {},
|
|
515
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// src/plugins/LoggerPlugin.ts
|
|
523
|
+
var LoggerPlugin = {
|
|
524
|
+
name: "logger",
|
|
525
|
+
onRequest(req) {
|
|
526
|
+
console.log(`[NeevJS] \u2192 ${req.options.method ?? "GET"} ${req.url}`);
|
|
527
|
+
return req;
|
|
528
|
+
},
|
|
529
|
+
onResponse(res) {
|
|
530
|
+
console.log(`[NeevJS] \u2190 ${res.status} ${res.url}`);
|
|
531
|
+
return res;
|
|
532
|
+
},
|
|
533
|
+
onError(err) {
|
|
534
|
+
console.error("[NeevJS] \u2717 Error:", err.message);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/plugins/CachePlugin.ts
|
|
539
|
+
function createCachePlugin(options = {}) {
|
|
540
|
+
const ttl = options.ttl ?? 6e4;
|
|
541
|
+
const store = /* @__PURE__ */ new Map();
|
|
542
|
+
return {
|
|
543
|
+
name: "cache",
|
|
544
|
+
onRequest(req) {
|
|
545
|
+
const method = (req.options.method ?? "GET").toUpperCase();
|
|
546
|
+
if (method !== "GET") return req;
|
|
547
|
+
const entry = store.get(req.url);
|
|
548
|
+
if (entry && Date.now() < entry.expiresAt) {
|
|
549
|
+
const cached = new Response(JSON.stringify(entry.data), {
|
|
550
|
+
status: 200,
|
|
551
|
+
headers: { "Content-Type": "application/json", "X-Neev-Cache": "HIT" }
|
|
552
|
+
});
|
|
553
|
+
return { ...req, _cachedResponse: cached };
|
|
554
|
+
}
|
|
555
|
+
return req;
|
|
556
|
+
},
|
|
557
|
+
async onResponse(res) {
|
|
558
|
+
if (res.headers.get("X-Neev-Cache") === "HIT") return res;
|
|
559
|
+
const url = res.url;
|
|
560
|
+
if (!url) return res;
|
|
561
|
+
try {
|
|
562
|
+
const clone = res.clone();
|
|
563
|
+
const data = await clone.json();
|
|
564
|
+
store.set(url, { data, expiresAt: Date.now() + ttl });
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
return res;
|
|
568
|
+
},
|
|
569
|
+
onError() {
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
var CachePlugin = createCachePlugin();
|
|
574
|
+
|
|
575
|
+
// src/plugins/OfflinePlugin.ts
|
|
576
|
+
var QUEUE_STORAGE_KEY = "neev_offline_queue";
|
|
577
|
+
function generateId() {
|
|
578
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
579
|
+
}
|
|
580
|
+
function loadQueue() {
|
|
581
|
+
try {
|
|
582
|
+
const raw = localStorage.getItem(QUEUE_STORAGE_KEY);
|
|
583
|
+
return raw ? JSON.parse(raw) : [];
|
|
584
|
+
} catch {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function saveQueue(queue) {
|
|
589
|
+
localStorage.setItem(QUEUE_STORAGE_KEY, JSON.stringify(queue));
|
|
590
|
+
syncState.pendingCount = queue.length;
|
|
591
|
+
syncState.notify();
|
|
592
|
+
}
|
|
593
|
+
function createOfflinePlugin() {
|
|
594
|
+
let client;
|
|
595
|
+
async function processQueue() {
|
|
596
|
+
if (!navigator.onLine) return;
|
|
597
|
+
const queue = loadQueue();
|
|
598
|
+
if (queue.length === 0) return;
|
|
599
|
+
const remaining = [];
|
|
600
|
+
for (const action of queue) {
|
|
601
|
+
try {
|
|
602
|
+
await client.request(action.url, action.options);
|
|
603
|
+
} catch {
|
|
604
|
+
remaining.push({ ...action, retries: action.retries + 1 });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
saveQueue(remaining);
|
|
608
|
+
}
|
|
609
|
+
window.addEventListener("online", () => {
|
|
610
|
+
void processQueue();
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
name: "offline",
|
|
614
|
+
setup(c) {
|
|
615
|
+
client = c;
|
|
616
|
+
},
|
|
617
|
+
onRequest(req) {
|
|
618
|
+
const method = (req.options.method ?? "GET").toUpperCase();
|
|
619
|
+
const isMutation = method !== "GET";
|
|
620
|
+
if (!navigator.onLine && isMutation) {
|
|
621
|
+
const queue = loadQueue();
|
|
622
|
+
const action = {
|
|
623
|
+
id: generateId(),
|
|
624
|
+
url: req.url,
|
|
625
|
+
options: req.options,
|
|
626
|
+
timestamp: Date.now(),
|
|
627
|
+
retries: 0
|
|
628
|
+
};
|
|
629
|
+
queue.push(action);
|
|
630
|
+
saveQueue(queue);
|
|
631
|
+
throw new Error("[NeevJS] Offline \u2014 action queued for sync when back online.");
|
|
632
|
+
}
|
|
633
|
+
return req;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
var OfflinePlugin = createOfflinePlugin();
|
|
638
|
+
export {
|
|
639
|
+
AuthClient,
|
|
640
|
+
AuthPlugin,
|
|
641
|
+
CachePlugin,
|
|
642
|
+
Form,
|
|
643
|
+
LoggerPlugin,
|
|
644
|
+
NeevContext,
|
|
645
|
+
NeevProvider,
|
|
646
|
+
OfflinePlugin,
|
|
647
|
+
Protected,
|
|
648
|
+
Table,
|
|
649
|
+
createCachePlugin,
|
|
650
|
+
createClient,
|
|
651
|
+
createOfflinePlugin,
|
|
652
|
+
useAuth,
|
|
653
|
+
useModel,
|
|
654
|
+
useNeevClient,
|
|
655
|
+
useSyncStatus
|
|
656
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neevjs/client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "NeevJS core React framework — plugin-driven, offline-first, built for business apps",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"framework",
|
|
8
|
+
"crud",
|
|
9
|
+
"business",
|
|
10
|
+
"offline-first",
|
|
11
|
+
"plugin",
|
|
12
|
+
"neevjs"
|
|
13
|
+
],
|
|
14
|
+
"author": "Rahul Raj Kushwaha <rahulkraj.dev@gmail.com>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/rahulkraj/neevjs"
|
|
19
|
+
},
|
|
20
|
+
"source": "src/index.ts",
|
|
21
|
+
"main": "dist/index.cjs",
|
|
22
|
+
"module": "dist/index.js",
|
|
23
|
+
"types": "dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@neevjs/shared": "0.0.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "^18.2.0",
|
|
43
|
+
"react": "^18.2.0",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"typescript": "^5.3.3"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
49
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
50
|
+
"clean": "rm -rf dist"
|
|
51
|
+
}
|
|
52
|
+
}
|