4bnode 4.0.0 → 4.0.2
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 +10 -0
- package/add-login.js +2 -2
- package/index.js +57 -5
- package/lib/indexFile.js +60 -13
- package/package.json +1 -1
- package/skeleton/.4bnode/assets/js/App.js +115 -0
- package/skeleton/.4bnode/assets/js/Root.js +44 -0
- package/skeleton/.4bnode/assets/js/components/Modal.js +52 -0
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +68 -0
- package/skeleton/.4bnode/assets/js/components/ToastContainer.js +15 -0
- package/skeleton/.4bnode/assets/js/context.js +7 -0
- package/skeleton/.4bnode/assets/js/lib.js +83 -0
- package/skeleton/.4bnode/assets/js/main.js +5 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +1594 -0
- package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +435 -0
- package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +461 -0
- package/skeleton/.4bnode/assets/js/pages/EnvironmentPage.js +141 -0
- package/skeleton/.4bnode/assets/js/pages/RealtimePage.js +186 -0
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1656 -0
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +901 -0
- package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
- package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
- package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +66 -0
- package/skeleton/{dev-api.js → .4bnode/dev-api.js} +259 -114
- package/skeleton/.4bnode/ui.html +1556 -0
- package/skeleton/index.js +62 -16
- package/skeleton/package.json +1 -1
- package/skeleton/public/index.html +290 -7586
- /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Bold.woff2 +0 -0
- /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Light.woff2 +0 -0
- /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Medium.woff2 +0 -0
- /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Regular.woff2 +0 -0
- /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-SemiBold.woff2 +0 -0
- /package/skeleton/{public → .4bnode/assets}/images/mongo.png +0 -0
- /package/skeleton/{public → .4bnode/assets}/images/mysql.png +0 -0
- /package/skeleton/{public → .4bnode/assets}/images/postgresql.png +0 -0
- /package/skeleton/{public → .4bnode/assets}/images/sqlite.png +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { LuZap, LuRocket } from "react-icons/lu";
|
|
3
|
+
import { html, Icons } from "../lib.js";
|
|
4
|
+
import { useApp } from "../context.js";
|
|
5
|
+
|
|
6
|
+
export function DashboardPage() {
|
|
7
|
+
const { data, setPage, status } = useApp();
|
|
8
|
+
const modelCount = data.models?.length || 0;
|
|
9
|
+
const routeCount = data.routes?.length || 0;
|
|
10
|
+
const dbOk = data.mongoConfigured;
|
|
11
|
+
const totalFields =
|
|
12
|
+
data.models?.reduce((sum, m) => sum + (m.fields?.length || 0), 0) ||
|
|
13
|
+
0;
|
|
14
|
+
const featuresActive = [
|
|
15
|
+
status?.socketConfigured,
|
|
16
|
+
status?.websocketConfigured,
|
|
17
|
+
status?.serialportConfigured,
|
|
18
|
+
].filter(Boolean);
|
|
19
|
+
|
|
20
|
+
const steps = [
|
|
21
|
+
{
|
|
22
|
+
label: "Database",
|
|
23
|
+
done: dbOk,
|
|
24
|
+
page: "database",
|
|
25
|
+
icon: Icons.database,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "Schemas",
|
|
29
|
+
done: modelCount > 0,
|
|
30
|
+
page: "schemas",
|
|
31
|
+
icon: Icons.schema,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "Routes",
|
|
35
|
+
done: routeCount > 0,
|
|
36
|
+
page: "routes",
|
|
37
|
+
icon: Icons.route,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
const firstIncomplete = steps.findIndex((s) => !s.done);
|
|
41
|
+
const allDone = steps.every((s) => s.done);
|
|
42
|
+
|
|
43
|
+
const nextAction = !dbOk
|
|
44
|
+
? {
|
|
45
|
+
label: "Connect Database",
|
|
46
|
+
page: "database",
|
|
47
|
+
desc: "Set up your database connection to get started",
|
|
48
|
+
}
|
|
49
|
+
: modelCount === 0
|
|
50
|
+
? {
|
|
51
|
+
label: "Create First Schema",
|
|
52
|
+
page: "schemas",
|
|
53
|
+
desc: "Define your data models to structure your application",
|
|
54
|
+
}
|
|
55
|
+
: routeCount === 0
|
|
56
|
+
? {
|
|
57
|
+
label: "Create First Route",
|
|
58
|
+
page: "routes",
|
|
59
|
+
desc: "Add API endpoints to expose your data",
|
|
60
|
+
}
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
return html`<div>
|
|
64
|
+
<div className="pipeline">
|
|
65
|
+
${steps.map(
|
|
66
|
+
(step, i) =>
|
|
67
|
+
html`<${React.Fragment} key=${i}>
|
|
68
|
+
${i > 0 &&
|
|
69
|
+
html`<div
|
|
70
|
+
className=${`pipeline-connector ${steps[i - 1].done ? "done" : ""}`}
|
|
71
|
+
/>`}
|
|
72
|
+
<div
|
|
73
|
+
className="pipeline-step"
|
|
74
|
+
onClick=${() => setPage(step.page)}
|
|
75
|
+
style=${{ cursor: "pointer" }}
|
|
76
|
+
>
|
|
77
|
+
<div
|
|
78
|
+
className=${`pipeline-dot ${step.done ? "done" : i === firstIncomplete ? "current" : ""}`}
|
|
79
|
+
>
|
|
80
|
+
${step.done ? "\u2713" : i + 1}
|
|
81
|
+
</div>
|
|
82
|
+
<div
|
|
83
|
+
className=${`pipeline-label ${step.done ? "done" : i === firstIncomplete ? "current" : ""}`}
|
|
84
|
+
>
|
|
85
|
+
${step.label}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<//>`,
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
${nextAction &&
|
|
93
|
+
html`<div
|
|
94
|
+
className="card mb-6"
|
|
95
|
+
style=${{
|
|
96
|
+
borderColor: "var(--accent)",
|
|
97
|
+
borderWidth: "1px",
|
|
98
|
+
borderStyle: "solid",
|
|
99
|
+
background:
|
|
100
|
+
"linear-gradient(135deg, rgba(124,58,237,0.06), rgba(124,58,237,0.02))",
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<div className="flex items-center gap-4">
|
|
104
|
+
<div
|
|
105
|
+
style=${{
|
|
106
|
+
width: "44px",
|
|
107
|
+
height: "44px",
|
|
108
|
+
borderRadius: "12px",
|
|
109
|
+
background: "var(--accent-glow)",
|
|
110
|
+
display: "flex",
|
|
111
|
+
alignItems: "center",
|
|
112
|
+
justifyContent: "center",
|
|
113
|
+
flexShrink: 0,
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<${LuZap} size=${20} color="var(--accent-light)" />
|
|
117
|
+
</div>
|
|
118
|
+
<div style=${{ flex: 1 }}>
|
|
119
|
+
<div
|
|
120
|
+
style=${{
|
|
121
|
+
fontWeight: 600,
|
|
122
|
+
fontSize: "14px",
|
|
123
|
+
marginBottom: "2px",
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
Next Step: ${nextAction.label}
|
|
127
|
+
</div>
|
|
128
|
+
<div className="text-sm text-muted">${nextAction.desc}</div>
|
|
129
|
+
</div>
|
|
130
|
+
<button
|
|
131
|
+
className="btn btn-primary"
|
|
132
|
+
onClick=${() => setPage(nextAction.page)}
|
|
133
|
+
>
|
|
134
|
+
${nextAction.label}
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>`}
|
|
138
|
+
${allDone &&
|
|
139
|
+
html`<div
|
|
140
|
+
className="card mb-6"
|
|
141
|
+
style=${{
|
|
142
|
+
borderColor: "rgba(34,197,94,0.3)",
|
|
143
|
+
background:
|
|
144
|
+
"linear-gradient(135deg, rgba(34,197,94,0.06), rgba(34,197,94,0.02))",
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<div className="flex items-center gap-4">
|
|
148
|
+
<div
|
|
149
|
+
style=${{
|
|
150
|
+
width: "44px",
|
|
151
|
+
height: "44px",
|
|
152
|
+
borderRadius: "12px",
|
|
153
|
+
background: "var(--success-glow)",
|
|
154
|
+
display: "flex",
|
|
155
|
+
alignItems: "center",
|
|
156
|
+
justifyContent: "center",
|
|
157
|
+
flexShrink: 0,
|
|
158
|
+
fontSize: "20px",
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
\u2713
|
|
162
|
+
</div>
|
|
163
|
+
<div style=${{ flex: 1 }}>
|
|
164
|
+
<div
|
|
165
|
+
style=${{
|
|
166
|
+
fontWeight: 600,
|
|
167
|
+
fontSize: "14px",
|
|
168
|
+
marginBottom: "2px",
|
|
169
|
+
color: "var(--success)",
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
Project Ready
|
|
173
|
+
</div>
|
|
174
|
+
<div className="text-sm text-muted">
|
|
175
|
+
Database, schemas, and routes are configured. Add features or
|
|
176
|
+
refine your API.
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>`}
|
|
181
|
+
|
|
182
|
+
<div
|
|
183
|
+
className="stats-grid"
|
|
184
|
+
style=${{ gridTemplateColumns: "repeat(4, 1fr)" }}
|
|
185
|
+
>
|
|
186
|
+
<div
|
|
187
|
+
className="stat-card"
|
|
188
|
+
onClick=${() => setPage("database")}
|
|
189
|
+
style=${{ cursor: "pointer" }}
|
|
190
|
+
>
|
|
191
|
+
<div
|
|
192
|
+
className="flex items-center gap-2"
|
|
193
|
+
style=${{ marginBottom: "10px" }}
|
|
194
|
+
>
|
|
195
|
+
<span className="stat-label" style=${{ margin: 0 }}
|
|
196
|
+
>Database</span
|
|
197
|
+
>
|
|
198
|
+
<span
|
|
199
|
+
className=${`stat-badge ${dbOk ? "success" : "warning"}`}
|
|
200
|
+
style=${{ marginLeft: "auto" }}
|
|
201
|
+
>${dbOk ? "Connected" : "Pending"}</span
|
|
202
|
+
>
|
|
203
|
+
</div>
|
|
204
|
+
<span
|
|
205
|
+
className="stat-value"
|
|
206
|
+
style=${{ fontSize: "18px", fontWeight: 600 }}
|
|
207
|
+
>${dbOk ? data.dbType || "MongoDB" : "\u2014"}</span
|
|
208
|
+
>
|
|
209
|
+
</div>
|
|
210
|
+
<div
|
|
211
|
+
className="stat-card"
|
|
212
|
+
onClick=${() => setPage("schemas")}
|
|
213
|
+
style=${{ cursor: "pointer" }}
|
|
214
|
+
>
|
|
215
|
+
<span className="stat-label">Models</span>
|
|
216
|
+
<div className="flex items-center gap-2">
|
|
217
|
+
<span className="stat-value">${modelCount}</span>
|
|
218
|
+
<span
|
|
219
|
+
className="text-sm text-muted"
|
|
220
|
+
style=${{ marginTop: "6px" }}
|
|
221
|
+
>${totalFields} field${totalFields !== 1 ? "s" : ""}</span
|
|
222
|
+
>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div
|
|
226
|
+
className="stat-card"
|
|
227
|
+
onClick=${() => setPage("routes")}
|
|
228
|
+
style=${{ cursor: "pointer" }}
|
|
229
|
+
>
|
|
230
|
+
<span className="stat-label">Routes</span>
|
|
231
|
+
<span className="stat-value">${routeCount}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="stat-card">
|
|
234
|
+
<span className="stat-label">Features</span>
|
|
235
|
+
<span className="stat-value">${featuresActive.length}</span>
|
|
236
|
+
<div
|
|
237
|
+
className="flex gap-1"
|
|
238
|
+
style=${{ flexWrap: "wrap", marginTop: "4px" }}
|
|
239
|
+
>
|
|
240
|
+
${status?.socketConfigured &&
|
|
241
|
+
html`<span className="stat-badge info">Socket.io</span>`}
|
|
242
|
+
${status?.websocketConfigured &&
|
|
243
|
+
html`<span className="stat-badge info">WebSocket</span>`}
|
|
244
|
+
${status?.serialportConfigured &&
|
|
245
|
+
html`<span className="stat-badge info">Serial</span>`}
|
|
246
|
+
${featuresActive.length === 0 &&
|
|
247
|
+
html`<span className="stat-badge info">None yet</span>`}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
${(modelCount > 0 || routeCount > 0) &&
|
|
253
|
+
html`<div className="grid-2">
|
|
254
|
+
${modelCount > 0 &&
|
|
255
|
+
html`<div className="card">
|
|
256
|
+
<div className="flex items-center justify-between mb-3">
|
|
257
|
+
<div
|
|
258
|
+
className="card-title"
|
|
259
|
+
style=${{ margin: 0, fontSize: "13px" }}
|
|
260
|
+
>
|
|
261
|
+
Models
|
|
262
|
+
</div>
|
|
263
|
+
<button
|
|
264
|
+
className="btn btn-ghost btn-sm"
|
|
265
|
+
onClick=${() => setPage("schemas")}
|
|
266
|
+
style=${{ fontSize: "11px" }}
|
|
267
|
+
>
|
|
268
|
+
View All
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
${data.models.slice(0, 5).map(
|
|
272
|
+
(m) =>
|
|
273
|
+
html`<div
|
|
274
|
+
key=${m.name}
|
|
275
|
+
className="flex items-center gap-3"
|
|
276
|
+
style=${{
|
|
277
|
+
padding: "8px 0",
|
|
278
|
+
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
<div
|
|
282
|
+
style=${{
|
|
283
|
+
width: "28px",
|
|
284
|
+
height: "28px",
|
|
285
|
+
borderRadius: "6px",
|
|
286
|
+
background: "var(--accent-glow)",
|
|
287
|
+
display: "flex",
|
|
288
|
+
alignItems: "center",
|
|
289
|
+
justifyContent: "center",
|
|
290
|
+
flexShrink: 0,
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
<span
|
|
294
|
+
style=${{
|
|
295
|
+
fontSize: "11px",
|
|
296
|
+
fontWeight: 700,
|
|
297
|
+
color: "var(--accent-light)",
|
|
298
|
+
}}
|
|
299
|
+
>${m.name.charAt(0).toUpperCase()}</span
|
|
300
|
+
>
|
|
301
|
+
</div>
|
|
302
|
+
<div style=${{ flex: 1, minWidth: 0 }}>
|
|
303
|
+
<div style=${{ fontWeight: 500, fontSize: "13px" }}>
|
|
304
|
+
${m.name}
|
|
305
|
+
</div>
|
|
306
|
+
<div
|
|
307
|
+
className="text-sm text-muted"
|
|
308
|
+
style=${{ fontSize: "11px" }}
|
|
309
|
+
>
|
|
310
|
+
${m.fields?.length || 0} fields
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div className="flex gap-1">
|
|
314
|
+
${(m.fields || []).filter((f) => f.required).length > 0 &&
|
|
315
|
+
html`<span
|
|
316
|
+
style=${{
|
|
317
|
+
fontSize: "9px",
|
|
318
|
+
padding: "2px 6px",
|
|
319
|
+
borderRadius: "4px",
|
|
320
|
+
background: "var(--success-glow)",
|
|
321
|
+
color: "var(--success)",
|
|
322
|
+
fontWeight: 600,
|
|
323
|
+
}}
|
|
324
|
+
>${(m.fields || []).filter((f) => f.required).length}
|
|
325
|
+
REQ</span
|
|
326
|
+
>`}
|
|
327
|
+
${(m.fields || []).filter((f) => f.unique).length > 0 &&
|
|
328
|
+
html`<span
|
|
329
|
+
style=${{
|
|
330
|
+
fontSize: "9px",
|
|
331
|
+
padding: "2px 6px",
|
|
332
|
+
borderRadius: "4px",
|
|
333
|
+
background: "var(--warning-glow)",
|
|
334
|
+
color: "var(--warning)",
|
|
335
|
+
fontWeight: 600,
|
|
336
|
+
}}
|
|
337
|
+
>${(m.fields || []).filter((f) => f.unique).length}
|
|
338
|
+
UNQ</span
|
|
339
|
+
>`}
|
|
340
|
+
</div>
|
|
341
|
+
</div>`,
|
|
342
|
+
)}
|
|
343
|
+
${modelCount > 5 &&
|
|
344
|
+
html`<div
|
|
345
|
+
className="text-sm text-muted"
|
|
346
|
+
style=${{ padding: "8px 0", textAlign: "center" }}
|
|
347
|
+
>
|
|
348
|
+
+${modelCount - 5} more
|
|
349
|
+
</div>`}
|
|
350
|
+
</div>`}
|
|
351
|
+
${routeCount > 0 &&
|
|
352
|
+
html`<div className="card">
|
|
353
|
+
<div className="flex items-center justify-between mb-3">
|
|
354
|
+
<div
|
|
355
|
+
className="card-title"
|
|
356
|
+
style=${{ margin: 0, fontSize: "13px" }}
|
|
357
|
+
>
|
|
358
|
+
Routes
|
|
359
|
+
</div>
|
|
360
|
+
<button
|
|
361
|
+
className="btn btn-ghost btn-sm"
|
|
362
|
+
onClick=${() => setPage("routes")}
|
|
363
|
+
style=${{ fontSize: "11px" }}
|
|
364
|
+
>
|
|
365
|
+
View All
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
${data.routes.slice(0, 5).map((r) => {
|
|
369
|
+
const rName = r.replace(".js", "");
|
|
370
|
+
return html`<div
|
|
371
|
+
key=${r}
|
|
372
|
+
className="flex items-center gap-3"
|
|
373
|
+
style=${{
|
|
374
|
+
padding: "8px 0",
|
|
375
|
+
borderBottom: "1px solid rgba(255,255,255,0.04)",
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
<div
|
|
379
|
+
style=${{
|
|
380
|
+
width: "28px",
|
|
381
|
+
height: "28px",
|
|
382
|
+
borderRadius: "6px",
|
|
383
|
+
background: "rgba(59,130,246,0.1)",
|
|
384
|
+
display: "flex",
|
|
385
|
+
alignItems: "center",
|
|
386
|
+
justifyContent: "center",
|
|
387
|
+
flexShrink: 0,
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
<span
|
|
391
|
+
style=${{
|
|
392
|
+
fontSize: "11px",
|
|
393
|
+
fontWeight: 700,
|
|
394
|
+
color: "#60a5fa",
|
|
395
|
+
}}
|
|
396
|
+
>${rName.charAt(0).toUpperCase()}</span
|
|
397
|
+
>
|
|
398
|
+
</div>
|
|
399
|
+
<div style=${{ flex: 1, minWidth: 0 }}>
|
|
400
|
+
<div style=${{ fontWeight: 500, fontSize: "13px" }}>
|
|
401
|
+
${rName}
|
|
402
|
+
</div>
|
|
403
|
+
<div
|
|
404
|
+
className="text-sm text-muted"
|
|
405
|
+
style=${{
|
|
406
|
+
fontSize: "11px",
|
|
407
|
+
fontFamily: "'SF Mono', monospace",
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
src/routes/${r}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>`;
|
|
414
|
+
})}
|
|
415
|
+
${routeCount > 5 &&
|
|
416
|
+
html`<div
|
|
417
|
+
className="text-sm text-muted"
|
|
418
|
+
style=${{ padding: "8px 0", textAlign: "center" }}
|
|
419
|
+
>
|
|
420
|
+
+${routeCount - 5} more
|
|
421
|
+
</div>`}
|
|
422
|
+
</div>`}
|
|
423
|
+
</div>`}
|
|
424
|
+
${modelCount === 0 &&
|
|
425
|
+
routeCount === 0 &&
|
|
426
|
+
!nextAction &&
|
|
427
|
+
html`<div className="card">
|
|
428
|
+
<div className="empty-state">
|
|
429
|
+
<div className="empty-icon"><${LuRocket} size=${36} /></div>
|
|
430
|
+
<h3>Ready to Build</h3>
|
|
431
|
+
<p>Start by creating schemas and routes for your API</p>
|
|
432
|
+
</div>
|
|
433
|
+
</div>`}
|
|
434
|
+
</div>`;
|
|
435
|
+
}
|