create-bdpa-react-scaffold 1.3.2 ā 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/create-ui-lib.js +936 -932
- package/package.json +2 -2
package/create-ui-lib.js
CHANGED
|
@@ -1,933 +1,937 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// create-ui-lib.js
|
|
3
|
-
// Full upgraded scaffolding script for a complete React + Tailwind
|
|
4
|
-
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const { spawnSync } = require("child_process");
|
|
8
|
-
|
|
9
|
-
// -------------------------------
|
|
10
|
-
// CLI args parsing
|
|
11
|
-
// -------------------------------
|
|
12
|
-
const argv = process.argv.slice(2);
|
|
13
|
-
let targetArg = ".";
|
|
14
|
-
let packageManager = "npm";
|
|
15
|
-
let doInstall = true;
|
|
16
|
-
let forceWrite = false;
|
|
17
|
-
|
|
18
|
-
for (let i = 0; i < argv.length; i++) {
|
|
19
|
-
const a = argv[i];
|
|
20
|
-
if (!a) continue;
|
|
21
|
-
if (a === "--pm" && i + 1 < argv.length) {
|
|
22
|
-
packageManager = argv[i + 1];
|
|
23
|
-
i++;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (a === "--no-install") {
|
|
27
|
-
doInstall = false;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (a === "--force") {
|
|
31
|
-
forceWrite = true;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (!a.startsWith("-")) {
|
|
35
|
-
targetArg = a;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const BASE_DIR = path.resolve(process.cwd(), targetArg);
|
|
41
|
-
|
|
42
|
-
function ensureTargetDir(dir, { force } = { force: false }) {
|
|
43
|
-
if (!fs.existsSync(dir)) {
|
|
44
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const entries = fs.readdirSync(dir).filter((e) => e !== ".git");
|
|
48
|
-
if (entries.length > 0 && !force) {
|
|
49
|
-
console.error(`\nDirectory ${dir} is not empty. Use --force to continue.`);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function write(filePath, content) {
|
|
55
|
-
const fullPath = path.join(BASE_DIR, filePath);
|
|
56
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
57
|
-
fs.writeFileSync(fullPath, content.trimStart(), "utf8");
|
|
58
|
-
console.log("ā Created:", filePath);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function installDependencies(pm, cwd) {
|
|
62
|
-
const pmCmd = pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm" : "npm";
|
|
63
|
-
console.log(`\nš¦ Installing dependencies with ${pmCmd}...`);
|
|
64
|
-
const args = pmCmd === "npm" ? ["install"] : ["install"];
|
|
65
|
-
const result = spawnSync(pmCmd, args, { stdio: "inherit", cwd });
|
|
66
|
-
if (result.status !== 0) {
|
|
67
|
-
console.error(`\nā ${pmCmd} install failed.`);
|
|
68
|
-
process.exit(result.status || 1);
|
|
69
|
-
}
|
|
70
|
-
console.log("\nā
Dependencies installed.");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Prepare target directory
|
|
74
|
-
ensureTargetDir(BASE_DIR, { force: forceWrite });
|
|
75
|
-
|
|
76
|
-
// -------------------------------
|
|
77
|
-
// Root files
|
|
78
|
-
// -------------------------------
|
|
79
|
-
|
|
80
|
-
write("package.json", `
|
|
81
|
-
{
|
|
82
|
-
"name": "
|
|
83
|
-
"version": "2.0.0",
|
|
84
|
-
"private": true,
|
|
85
|
-
"scripts": {
|
|
86
|
-
"dev": "vite",
|
|
87
|
-
"build": "vite build",
|
|
88
|
-
"preview": "vite preview"
|
|
89
|
-
},
|
|
90
|
-
"dependencies": {
|
|
91
|
-
"axios": "^1.6.8",
|
|
92
|
-
"react": "^18.2.0",
|
|
93
|
-
"react-dom": "^18.2.0",
|
|
94
|
-
"react-router-dom": "^6.20.0",
|
|
95
|
-
"lucide-react": "^0.344.0",
|
|
96
|
-
"bcryptjs": "^2.4.3"
|
|
97
|
-
},
|
|
98
|
-
"devDependencies": {
|
|
99
|
-
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
100
|
-
"autoprefixer": "^10.4.20",
|
|
101
|
-
"postcss": "^8.4.47",
|
|
102
|
-
"tailwindcss": "^3.4.0",
|
|
103
|
-
"vite": "^5.0.0"
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
`);
|
|
107
|
-
|
|
108
|
-
write("postcss.config.cjs", `
|
|
109
|
-
module.exports = {
|
|
110
|
-
plugins: {
|
|
111
|
-
tailwindcss: {},
|
|
112
|
-
autoprefixer: {}
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
`);
|
|
116
|
-
|
|
117
|
-
write("tailwind.config.cjs", `
|
|
118
|
-
module.exports = {
|
|
119
|
-
content: [
|
|
120
|
-
"./index.html",
|
|
121
|
-
"./src/**/*.{js,jsx,ts,tsx}"
|
|
122
|
-
],
|
|
123
|
-
theme: {
|
|
124
|
-
extend: {
|
|
125
|
-
colors: {
|
|
126
|
-
milwaukeeBlue: "#2563eb",
|
|
127
|
-
milwaukeeGold: "#fbbf24"
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
plugins: []
|
|
132
|
-
};
|
|
133
|
-
`);
|
|
134
|
-
|
|
135
|
-
write("vite.config.mts", `
|
|
136
|
-
import { defineConfig } from "vite";
|
|
137
|
-
import react from "@vitejs/plugin-react-swc";
|
|
138
|
-
|
|
139
|
-
export default defineConfig({
|
|
140
|
-
plugins: [react()]
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
</
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
@apply
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
import "
|
|
183
|
-
import
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
export { default as
|
|
202
|
-
export { default as
|
|
203
|
-
export { default as
|
|
204
|
-
export { default as
|
|
205
|
-
export { default as
|
|
206
|
-
export {
|
|
207
|
-
|
|
208
|
-
export { default as
|
|
209
|
-
export { default as
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
export { default as
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
export {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const [
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
<FormField label="
|
|
314
|
-
<Input
|
|
315
|
-
</FormField>
|
|
316
|
-
|
|
317
|
-
<FormField label="
|
|
318
|
-
<Input
|
|
319
|
-
</FormField>
|
|
320
|
-
|
|
321
|
-
<
|
|
322
|
-
<
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
</
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"bg-
|
|
444
|
-
|
|
445
|
-
"
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
{error && (
|
|
506
|
-
<p className="text-xs text-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
export function
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
<
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
<
|
|
745
|
-
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
<
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
async
|
|
835
|
-
return this.request({ url, method: "GET", ...config });
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
async
|
|
839
|
-
return this.request({ url, method: "
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
async
|
|
843
|
-
return this.request({ url, method: "
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
async
|
|
847
|
-
return this.request({ url, method: "
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
async
|
|
851
|
-
return this.request({ url, method: "
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (!
|
|
886
|
-
throw new Error("
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
if (password
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
console.log("
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
2
|
+
// create-ui-lib.js
|
|
3
|
+
// Full upgraded scaffolding script for a complete React + Tailwind scaffold and demo
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
// -------------------------------
|
|
10
|
+
// CLI args parsing
|
|
11
|
+
// -------------------------------
|
|
12
|
+
const argv = process.argv.slice(2);
|
|
13
|
+
let targetArg = ".";
|
|
14
|
+
let packageManager = "npm";
|
|
15
|
+
let doInstall = true;
|
|
16
|
+
let forceWrite = false;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
if (!a) continue;
|
|
21
|
+
if (a === "--pm" && i + 1 < argv.length) {
|
|
22
|
+
packageManager = argv[i + 1];
|
|
23
|
+
i++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (a === "--no-install") {
|
|
27
|
+
doInstall = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (a === "--force") {
|
|
31
|
+
forceWrite = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!a.startsWith("-")) {
|
|
35
|
+
targetArg = a;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BASE_DIR = path.resolve(process.cwd(), targetArg);
|
|
41
|
+
|
|
42
|
+
function ensureTargetDir(dir, { force } = { force: false }) {
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const entries = fs.readdirSync(dir).filter((e) => e !== ".git");
|
|
48
|
+
if (entries.length > 0 && !force) {
|
|
49
|
+
console.error(`\nDirectory ${dir} is not empty. Use --force to continue.`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function write(filePath, content) {
|
|
55
|
+
const fullPath = path.join(BASE_DIR, filePath);
|
|
56
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
57
|
+
fs.writeFileSync(fullPath, content.trimStart(), "utf8");
|
|
58
|
+
console.log("ā Created:", filePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function installDependencies(pm, cwd) {
|
|
62
|
+
const pmCmd = pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm" : "npm";
|
|
63
|
+
console.log(`\nš¦ Installing dependencies with ${pmCmd}...`);
|
|
64
|
+
const args = pmCmd === "npm" ? ["install"] : ["install"];
|
|
65
|
+
const result = spawnSync(pmCmd, args, { stdio: "inherit", cwd });
|
|
66
|
+
if (result.status !== 0) {
|
|
67
|
+
console.error(`\nā ${pmCmd} install failed.`);
|
|
68
|
+
process.exit(result.status || 1);
|
|
69
|
+
}
|
|
70
|
+
console.log("\nā
Dependencies installed.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Prepare target directory
|
|
74
|
+
ensureTargetDir(BASE_DIR, { force: forceWrite });
|
|
75
|
+
|
|
76
|
+
// -------------------------------
|
|
77
|
+
// Root files
|
|
78
|
+
// -------------------------------
|
|
79
|
+
|
|
80
|
+
write("package.json", `
|
|
81
|
+
{
|
|
82
|
+
"name": "BDPA-react-scaffold",
|
|
83
|
+
"version": "2.0.0",
|
|
84
|
+
"private": true,
|
|
85
|
+
"scripts": {
|
|
86
|
+
"dev": "vite",
|
|
87
|
+
"build": "vite build",
|
|
88
|
+
"preview": "vite preview"
|
|
89
|
+
},
|
|
90
|
+
"dependencies": {
|
|
91
|
+
"axios": "^1.6.8",
|
|
92
|
+
"react": "^18.2.0",
|
|
93
|
+
"react-dom": "^18.2.0",
|
|
94
|
+
"react-router-dom": "^6.20.0",
|
|
95
|
+
"lucide-react": "^0.344.0",
|
|
96
|
+
"bcryptjs": "^2.4.3"
|
|
97
|
+
},
|
|
98
|
+
"devDependencies": {
|
|
99
|
+
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
100
|
+
"autoprefixer": "^10.4.20",
|
|
101
|
+
"postcss": "^8.4.47",
|
|
102
|
+
"tailwindcss": "^3.4.0",
|
|
103
|
+
"vite": "^5.0.0"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
write("postcss.config.cjs", `
|
|
109
|
+
module.exports = {
|
|
110
|
+
plugins: {
|
|
111
|
+
tailwindcss: {},
|
|
112
|
+
autoprefixer: {}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
`);
|
|
116
|
+
|
|
117
|
+
write("tailwind.config.cjs", `
|
|
118
|
+
module.exports = {
|
|
119
|
+
content: [
|
|
120
|
+
"./index.html",
|
|
121
|
+
"./src/**/*.{js,jsx,ts,tsx}"
|
|
122
|
+
],
|
|
123
|
+
theme: {
|
|
124
|
+
extend: {
|
|
125
|
+
colors: {
|
|
126
|
+
milwaukeeBlue: "#2563eb",
|
|
127
|
+
milwaukeeGold: "#fbbf24"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
plugins: []
|
|
132
|
+
};
|
|
133
|
+
`);
|
|
134
|
+
|
|
135
|
+
write("vite.config.mts", `
|
|
136
|
+
import { defineConfig } from "vite";
|
|
137
|
+
import react from "@vitejs/plugin-react-swc";
|
|
138
|
+
|
|
139
|
+
export default defineConfig({
|
|
140
|
+
plugins: [react()],
|
|
141
|
+
server: {
|
|
142
|
+
host: true,
|
|
143
|
+
port: 3000
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
`);
|
|
147
|
+
|
|
148
|
+
write("index.html", `
|
|
149
|
+
<!doctype html>
|
|
150
|
+
<html lang="en">
|
|
151
|
+
<head>
|
|
152
|
+
<meta charset="UTF-8" />
|
|
153
|
+
<title>BDPA React Scaffold and Demo</title>
|
|
154
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
155
|
+
</head>
|
|
156
|
+
<body class="bg-gray-100">
|
|
157
|
+
<div id="root"></div>
|
|
158
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
// -------------------------------
|
|
164
|
+
// src root
|
|
165
|
+
// -------------------------------
|
|
166
|
+
|
|
167
|
+
write("src/index.css", `
|
|
168
|
+
@tailwind base;
|
|
169
|
+
@tailwind components;
|
|
170
|
+
@tailwind utilities;
|
|
171
|
+
|
|
172
|
+
body {
|
|
173
|
+
@apply bg-gray-100 text-gray-900;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
h1, h2, h3, h4 {
|
|
177
|
+
@apply font-semibold;
|
|
178
|
+
}
|
|
179
|
+
`);
|
|
180
|
+
|
|
181
|
+
write("src/main.jsx", `
|
|
182
|
+
import React from "react";
|
|
183
|
+
import ReactDOM from "react-dom/client";
|
|
184
|
+
import { BrowserRouter } from "react-router-dom";
|
|
185
|
+
import App from "./App.jsx";
|
|
186
|
+
import "./index.css";
|
|
187
|
+
import { ToastProvider } from "./components/ui/ToastProvider.jsx";
|
|
188
|
+
|
|
189
|
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
|
190
|
+
<React.StrictMode>
|
|
191
|
+
<BrowserRouter>
|
|
192
|
+
<ToastProvider>
|
|
193
|
+
<App />
|
|
194
|
+
</ToastProvider>
|
|
195
|
+
</BrowserRouter>
|
|
196
|
+
</React.StrictMode>
|
|
197
|
+
);
|
|
198
|
+
`);
|
|
199
|
+
|
|
200
|
+
write("src/index.js", `
|
|
201
|
+
export { default as Button } from "./components/ui/Button.jsx";
|
|
202
|
+
export { default as Card } from "./components/ui/Card.jsx";
|
|
203
|
+
export { default as Input } from "./components/ui/Input.jsx";
|
|
204
|
+
export { default as FormField } from "./components/ui/FormField.jsx";
|
|
205
|
+
export { default as Table } from "./components/ui/Table.jsx";
|
|
206
|
+
export { default as Navbar } from "./components/ui/Navbar.jsx";
|
|
207
|
+
export { default as Sidebar } from "./components/ui/Sidebar.jsx";
|
|
208
|
+
export { default as Modal } from "./components/ui/Modal.jsx";
|
|
209
|
+
export { default as Tabs } from "./components/ui/Tabs.jsx";
|
|
210
|
+
export { ToastProvider, useToast } from "./components/ui/ToastProvider.jsx";
|
|
211
|
+
|
|
212
|
+
export { default as Login } from "./pages/auth/Login.jsx";
|
|
213
|
+
export { default as Register } from "./pages/auth/Register.jsx";
|
|
214
|
+
|
|
215
|
+
export { default as Container } from "./components/layout/Container.jsx";
|
|
216
|
+
export { default as Section } from "./components/layout/Section.jsx";
|
|
217
|
+
|
|
218
|
+
export { default as api, ApiClient } from "./utils/api.js";
|
|
219
|
+
export { hashPassword, verifyPassword, getPasswordStrength, getPasswordStrengthLabel } from "./utils/password.js";
|
|
220
|
+
`);
|
|
221
|
+
|
|
222
|
+
write("src/App.jsx", `
|
|
223
|
+
import { useState } from "react";
|
|
224
|
+
import { Routes, Route, useNavigate } from "react-router-dom";
|
|
225
|
+
import {
|
|
226
|
+
Button,
|
|
227
|
+
Card,
|
|
228
|
+
Input,
|
|
229
|
+
FormField,
|
|
230
|
+
Table,
|
|
231
|
+
Navbar,
|
|
232
|
+
Sidebar,
|
|
233
|
+
Modal,
|
|
234
|
+
Tabs,
|
|
235
|
+
ApiClient,
|
|
236
|
+
useToast,
|
|
237
|
+
Login,
|
|
238
|
+
Register
|
|
239
|
+
} from "./index.js";
|
|
240
|
+
|
|
241
|
+
const columns = [
|
|
242
|
+
{ key: "name", label: "Student" },
|
|
243
|
+
{ key: "course", label: "Course" },
|
|
244
|
+
{ key: "status", label: "Status" }
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const data = [
|
|
248
|
+
{ name: "Alex", course: "Web Design Fundamentals", status: "Enrolled" },
|
|
249
|
+
{ name: "Jordan", course: "Advanced Web App Design", status: "Waitlisted" },
|
|
250
|
+
{ name: "Taylor", course: "eSports Strategy", status: "Enrolled" }
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
function Dashboard() {
|
|
254
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
255
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
256
|
+
const [posts, setPosts] = useState([]);
|
|
257
|
+
const [loadingPosts, setLoadingPosts] = useState(false);
|
|
258
|
+
const [postsError, setPostsError] = useState("");
|
|
259
|
+
const toast = useToast();
|
|
260
|
+
const navigate = useNavigate();
|
|
261
|
+
const client = new ApiClient("https://jsonplaceholder.typicode.com");
|
|
262
|
+
|
|
263
|
+
const fetchPosts = async () => {
|
|
264
|
+
setLoadingPosts(true);
|
|
265
|
+
setPostsError("");
|
|
266
|
+
const res = await client.getAll("/posts?_limit=5");
|
|
267
|
+
if (res.success) {
|
|
268
|
+
setPosts(res.data);
|
|
269
|
+
} else {
|
|
270
|
+
setPostsError(res.error || "Failed to load posts");
|
|
271
|
+
}
|
|
272
|
+
setLoadingPosts(false);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const tabs = [
|
|
276
|
+
{ label: "Overview", content: <p>Welcome to the BDPA React Scaffold and Demo.</p> },
|
|
277
|
+
{ label: "Components", content: <p>Buttons, Cards, Inputs, Tables, and more.</p> },
|
|
278
|
+
{ label: "Auth", content: <p>Login + Registration pages included.</p> }
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div className="flex h-screen overflow-hidden">
|
|
283
|
+
|
|
284
|
+
{/* Sidebar */}
|
|
285
|
+
<Sidebar
|
|
286
|
+
open={sidebarOpen}
|
|
287
|
+
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
|
288
|
+
links={[
|
|
289
|
+
{ label: "Home", href: "/" },
|
|
290
|
+
{ label: "Login", href: "/login" },
|
|
291
|
+
{ label: "Register", href: "/register" }
|
|
292
|
+
]}
|
|
293
|
+
/>
|
|
294
|
+
|
|
295
|
+
{/* Main content */}
|
|
296
|
+
<div className="flex-1 flex flex-col">
|
|
297
|
+
|
|
298
|
+
{/* Navbar */}
|
|
299
|
+
<Navbar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
|
|
300
|
+
|
|
301
|
+
{/* Page content */}
|
|
302
|
+
<div className="p-6 space-y-6 overflow-auto">
|
|
303
|
+
|
|
304
|
+
<Tabs tabs={tabs} />
|
|
305
|
+
|
|
306
|
+
<div className="grid md:grid-cols-2 gap-6">
|
|
307
|
+
|
|
308
|
+
{/* Form/Card example */}
|
|
309
|
+
<Card>
|
|
310
|
+
<h2 className="text-lg font-semibold mb-4">Sample Form</h2>
|
|
311
|
+
|
|
312
|
+
<div className="space-y-4">
|
|
313
|
+
<FormField label="Student Name">
|
|
314
|
+
<Input placeholder="e.g. Alex Johnson" />
|
|
315
|
+
</FormField>
|
|
316
|
+
|
|
317
|
+
<FormField label="Email">
|
|
318
|
+
<Input type="email" placeholder="student@example.com" />
|
|
319
|
+
</FormField>
|
|
320
|
+
|
|
321
|
+
<FormField label="Course">
|
|
322
|
+
<Input placeholder="Web Design Fundamentals" />
|
|
323
|
+
</FormField>
|
|
324
|
+
|
|
325
|
+
<div className="flex gap-2 pt-2">
|
|
326
|
+
<Button variant="primary">Save</Button>
|
|
327
|
+
<Button variant="secondary">Cancel</Button>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</Card>
|
|
331
|
+
|
|
332
|
+
{/* Table example */}
|
|
333
|
+
<Card>
|
|
334
|
+
<h2 className="text-lg font-semibold mb-4">Enrollment Overview</h2>
|
|
335
|
+
<Table columns={columns} data={data} />
|
|
336
|
+
</Card>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* Buttons */}
|
|
340
|
+
<Card>
|
|
341
|
+
<h2 className="text-lg font-semibold mb-4">Button Variants</h2>
|
|
342
|
+
<div className="flex flex-wrap gap-3">
|
|
343
|
+
<Button variant="primary">Primary</Button>
|
|
344
|
+
<Button variant="secondary">Secondary</Button>
|
|
345
|
+
<Button variant="danger">Danger</Button>
|
|
346
|
+
<Button variant="outline">Outline</Button>
|
|
347
|
+
</div>
|
|
348
|
+
</Card>
|
|
349
|
+
|
|
350
|
+
{/* Live API Demo */}
|
|
351
|
+
<Card>
|
|
352
|
+
<h2 className="text-lg font-semibold mb-4">Live API Demo (JSONPlaceholder)</h2>
|
|
353
|
+
<div className="flex items-center gap-3 mb-3">
|
|
354
|
+
<Button onClick={fetchPosts} disabled={loadingPosts}>
|
|
355
|
+
{loadingPosts ? "Loading..." : "Fetch Posts"}
|
|
356
|
+
</Button>
|
|
357
|
+
{postsError && (
|
|
358
|
+
<span className="text-sm text-red-600">{postsError}</span>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
{posts.length > 0 && (
|
|
362
|
+
<ul className="list-disc pl-6 space-y-1">
|
|
363
|
+
{posts.map((p) => (
|
|
364
|
+
<li key={p.id} className="text-sm">
|
|
365
|
+
<span className="font-medium">#{p.id}</span> {p.title}
|
|
366
|
+
</li>
|
|
367
|
+
))}
|
|
368
|
+
</ul>
|
|
369
|
+
)}
|
|
370
|
+
</Card>
|
|
371
|
+
|
|
372
|
+
{/* Modal + Toast */}
|
|
373
|
+
<div className="flex gap-4">
|
|
374
|
+
<Button onClick={() => setModalOpen(true)}>Open Modal</Button>
|
|
375
|
+
<Button onClick={() => toast.show("This is a toast!", "success")}>
|
|
376
|
+
Show Toast
|
|
377
|
+
</Button>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<Modal open={modalOpen} onClose={() => setModalOpen(false)}>
|
|
381
|
+
<h2 className="text-lg font-semibold mb-4">Modal Title</h2>
|
|
382
|
+
<p>This is a modal example.</p>
|
|
383
|
+
<Button className="mt-4" onClick={() => setModalOpen(false)}>
|
|
384
|
+
Close
|
|
385
|
+
</Button>
|
|
386
|
+
</Modal>
|
|
387
|
+
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export default function App() {
|
|
395
|
+
const navigate = useNavigate();
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<Routes>
|
|
399
|
+
<Route path="/" element={<Dashboard />} />
|
|
400
|
+
<Route
|
|
401
|
+
path="/login"
|
|
402
|
+
element={
|
|
403
|
+
<Login
|
|
404
|
+
onSubmit={() => {
|
|
405
|
+
alert("Login submitted!");
|
|
406
|
+
navigate("/");
|
|
407
|
+
}}
|
|
408
|
+
/>
|
|
409
|
+
}
|
|
410
|
+
/>
|
|
411
|
+
<Route
|
|
412
|
+
path="/register"
|
|
413
|
+
element={
|
|
414
|
+
<Register
|
|
415
|
+
onSubmit={() => {
|
|
416
|
+
alert("Registration submitted!");
|
|
417
|
+
navigate("/");
|
|
418
|
+
}}
|
|
419
|
+
/>
|
|
420
|
+
}
|
|
421
|
+
/>
|
|
422
|
+
</Routes>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
`);
|
|
426
|
+
|
|
427
|
+
// -------------------------------
|
|
428
|
+
// UI Components
|
|
429
|
+
// -------------------------------
|
|
430
|
+
|
|
431
|
+
write("src/components/ui/Button.jsx", `
|
|
432
|
+
export default function Button({
|
|
433
|
+
variant = "primary",
|
|
434
|
+
children,
|
|
435
|
+
className = "",
|
|
436
|
+
...props
|
|
437
|
+
}) {
|
|
438
|
+
const base =
|
|
439
|
+
"inline-flex items-center justify-center px-4 py-2 rounded-md font-semibold text-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2";
|
|
440
|
+
|
|
441
|
+
const variants = {
|
|
442
|
+
primary:
|
|
443
|
+
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
|
444
|
+
secondary:
|
|
445
|
+
"bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400",
|
|
446
|
+
danger:
|
|
447
|
+
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
|
|
448
|
+
outline:
|
|
449
|
+
"border border-gray-300 text-gray-800 hover:bg-gray-100 focus:ring-gray-400"
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<button
|
|
454
|
+
className={\`\${base} \${variants[variant]} \${className}\`}
|
|
455
|
+
{...props}
|
|
456
|
+
>
|
|
457
|
+
{children}
|
|
458
|
+
</button>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
`);
|
|
462
|
+
|
|
463
|
+
write("src/components/ui/Card.jsx", `
|
|
464
|
+
export default function Card({ children, className = "" }) {
|
|
465
|
+
return (
|
|
466
|
+
<div
|
|
467
|
+
className={\`bg-white shadow-sm rounded-lg p-4 md:p-6 border border-gray-200 \${className}\`}
|
|
468
|
+
>
|
|
469
|
+
{children}
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
`);
|
|
474
|
+
|
|
475
|
+
write("src/components/ui/Input.jsx", `
|
|
476
|
+
export default function Input({ label, className = "", ...props }) {
|
|
477
|
+
return (
|
|
478
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
479
|
+
{label && (
|
|
480
|
+
<span className="font-medium text-gray-700">
|
|
481
|
+
{label}
|
|
482
|
+
</span>
|
|
483
|
+
)}
|
|
484
|
+
<input
|
|
485
|
+
className={\`border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm \${className}\`}
|
|
486
|
+
{...props}
|
|
487
|
+
/>
|
|
488
|
+
</label>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
`);
|
|
492
|
+
|
|
493
|
+
write("src/components/ui/FormField.jsx", `
|
|
494
|
+
export default function FormField({ label, error, children, helperText }) {
|
|
495
|
+
return (
|
|
496
|
+
<div className="flex flex-col gap-1 text-sm">
|
|
497
|
+
{label && (
|
|
498
|
+
<label className="font-medium text-gray-700">
|
|
499
|
+
{label}
|
|
500
|
+
</label>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
{children}
|
|
504
|
+
|
|
505
|
+
{helperText && !error && (
|
|
506
|
+
<p className="text-xs text-gray-500">{helperText}</p>
|
|
507
|
+
)}
|
|
508
|
+
|
|
509
|
+
{error && (
|
|
510
|
+
<p className="text-xs text-red-600">
|
|
511
|
+
{error}
|
|
512
|
+
</p>
|
|
513
|
+
)}
|
|
514
|
+
</div>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
`);
|
|
518
|
+
|
|
519
|
+
write("src/components/ui/Table.jsx", `
|
|
520
|
+
export default function Table({ columns, data }) {
|
|
521
|
+
return (
|
|
522
|
+
<div className="overflow-x-auto">
|
|
523
|
+
<table className="min-w-full border border-gray-200 bg-white rounded-lg overflow-hidden">
|
|
524
|
+
<thead className="bg-gray-100">
|
|
525
|
+
<tr>
|
|
526
|
+
{columns.map((col) => (
|
|
527
|
+
<th
|
|
528
|
+
key={col.key}
|
|
529
|
+
className="px-4 py-2 text-left text-xs font-semibold text-gray-700 border-b border-gray-200"
|
|
530
|
+
>
|
|
531
|
+
{col.label}
|
|
532
|
+
</th>
|
|
533
|
+
))}
|
|
534
|
+
</tr>
|
|
535
|
+
</thead>
|
|
536
|
+
|
|
537
|
+
<tbody>
|
|
538
|
+
{data.map((row, i) => (
|
|
539
|
+
<tr
|
|
540
|
+
key={i}
|
|
541
|
+
className={i % 2 === 0 ? "bg-white" : "bg-gray-50"}
|
|
542
|
+
>
|
|
543
|
+
{columns.map((col) => (
|
|
544
|
+
<td
|
|
545
|
+
key={col.key}
|
|
546
|
+
className="px-4 py-2 text-sm text-gray-800 border-b border-gray-100"
|
|
547
|
+
>
|
|
548
|
+
{row[col.key]}
|
|
549
|
+
</td>
|
|
550
|
+
))}
|
|
551
|
+
</tr>
|
|
552
|
+
))}
|
|
553
|
+
</tbody>
|
|
554
|
+
</table>
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
`);
|
|
559
|
+
|
|
560
|
+
write("src/components/ui/Navbar.jsx", `
|
|
561
|
+
import { Menu } from "lucide-react";
|
|
562
|
+
|
|
563
|
+
export default function Navbar({ onMenuClick }) {
|
|
564
|
+
return (
|
|
565
|
+
<nav className="bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between">
|
|
566
|
+
<button className="md:hidden" onClick={onMenuClick}>
|
|
567
|
+
<Menu />
|
|
568
|
+
</button>
|
|
569
|
+
<h1 className="text-xl font-bold">BDPA React Scaffold and Demo</h1>
|
|
570
|
+
</nav>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
`);
|
|
574
|
+
|
|
575
|
+
write("src/components/ui/Sidebar.jsx", `
|
|
576
|
+
import { Link } from "react-router-dom";
|
|
577
|
+
|
|
578
|
+
export default function Sidebar({ open, onToggle, links }) {
|
|
579
|
+
return (
|
|
580
|
+
<div
|
|
581
|
+
className={\`
|
|
582
|
+
fixed md:static inset-y-0 left-0 z-40
|
|
583
|
+
bg-white border-r border-gray-200
|
|
584
|
+
h-full w-64 transform
|
|
585
|
+
transition-transform duration-200
|
|
586
|
+
\${open ? "translate-x-0" : "-translate-x-full md:translate-x-0"}
|
|
587
|
+
\`}
|
|
588
|
+
>
|
|
589
|
+
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
|
590
|
+
<h2 className="font-semibold">Menu</h2>
|
|
591
|
+
<button className="md:hidden" onClick={onToggle}>ā</button>
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
<ul className="p-4 space-y-2">
|
|
595
|
+
{links.map((l) => (
|
|
596
|
+
<li key={l.label}>
|
|
597
|
+
<Link to={l.href} className="block px-2 py-2 rounded hover:bg-gray-100">
|
|
598
|
+
{l.label}
|
|
599
|
+
</Link>
|
|
600
|
+
</li>
|
|
601
|
+
))}
|
|
602
|
+
</ul>
|
|
603
|
+
</div>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
`);
|
|
607
|
+
|
|
608
|
+
write("src/components/ui/Modal.jsx", `
|
|
609
|
+
export default function Modal({ open, onClose, children }) {
|
|
610
|
+
if (!open) return null;
|
|
611
|
+
|
|
612
|
+
return (
|
|
613
|
+
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
|
614
|
+
<div className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative">
|
|
615
|
+
<button
|
|
616
|
+
className="absolute top-3 right-3 text-gray-500 hover:text-gray-700"
|
|
617
|
+
onClick={onClose}
|
|
618
|
+
>
|
|
619
|
+
ā
|
|
620
|
+
</button>
|
|
621
|
+
|
|
622
|
+
{children}
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
`);
|
|
628
|
+
|
|
629
|
+
write("src/components/ui/Tabs.jsx", `
|
|
630
|
+
import { useState } from "react";
|
|
631
|
+
|
|
632
|
+
export default function Tabs({ tabs }) {
|
|
633
|
+
const [active, setActive] = useState(0);
|
|
634
|
+
|
|
635
|
+
return (
|
|
636
|
+
<div>
|
|
637
|
+
<div className="flex gap-4 border-b border-gray-200">
|
|
638
|
+
{tabs.map((t, i) => (
|
|
639
|
+
<button
|
|
640
|
+
key={i}
|
|
641
|
+
onClick={() => setActive(i)}
|
|
642
|
+
className={\`pb-2 text-sm font-medium \${active === i
|
|
643
|
+
? "border-b-2 border-blue-600 text-blue-600"
|
|
644
|
+
: "text-gray-600 hover:text-gray-800"
|
|
645
|
+
}\`}
|
|
646
|
+
>
|
|
647
|
+
{t.label}
|
|
648
|
+
</button>
|
|
649
|
+
))}
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
<div className="mt-4">{tabs[active].content}</div>
|
|
653
|
+
</div>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
`);
|
|
657
|
+
|
|
658
|
+
write("src/components/ui/ToastProvider.jsx", `
|
|
659
|
+
import { createContext, useContext, useState } from "react";
|
|
660
|
+
|
|
661
|
+
const ToastContext = createContext();
|
|
662
|
+
|
|
663
|
+
export function useToast() {
|
|
664
|
+
return useContext(ToastContext);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function ToastProvider({ children }) {
|
|
668
|
+
const [toasts, setToasts] = useState([]);
|
|
669
|
+
|
|
670
|
+
const show = (message, type = "info") => {
|
|
671
|
+
const id = Date.now();
|
|
672
|
+
setToasts((t) => [...t, { id, message, type }]);
|
|
673
|
+
setTimeout(() => {
|
|
674
|
+
setToasts((t) => t.filter((toast) => toast.id !== id));
|
|
675
|
+
}, 3000);
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
return (
|
|
679
|
+
<ToastContext.Provider value={{ show }}>
|
|
680
|
+
{children}
|
|
681
|
+
|
|
682
|
+
<div className="fixed bottom-4 right-4 space-y-3 z-50">
|
|
683
|
+
{toasts.map((t) => (
|
|
684
|
+
<div
|
|
685
|
+
key={t.id}
|
|
686
|
+
className={\`px-4 py-2 rounded-md shadow text-white \${t.type === "success"
|
|
687
|
+
? "bg-green-600"
|
|
688
|
+
: t.type === "error"
|
|
689
|
+
? "bg-red-600"
|
|
690
|
+
: "bg-gray-800"
|
|
691
|
+
}\`}
|
|
692
|
+
>
|
|
693
|
+
{t.message}
|
|
694
|
+
</div>
|
|
695
|
+
))}
|
|
696
|
+
</div>
|
|
697
|
+
</ToastContext.Provider>
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
`);
|
|
701
|
+
|
|
702
|
+
// -------------------------------
|
|
703
|
+
// Auth Components
|
|
704
|
+
// -------------------------------
|
|
705
|
+
|
|
706
|
+
write("src/pages/auth/Login.jsx", `
|
|
707
|
+
import Button from "../../components/ui/Button.jsx";
|
|
708
|
+
import Input from "../../components/ui/Input.jsx";
|
|
709
|
+
import Card from "../../components/ui/Card.jsx";
|
|
710
|
+
|
|
711
|
+
export default function Login({ onSubmit }) {
|
|
712
|
+
return (
|
|
713
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
|
714
|
+
<Card className="w-full max-w-sm space-y-4">
|
|
715
|
+
<h2 className="text-xl font-bold">Sign In</h2>
|
|
716
|
+
|
|
717
|
+
<Input label="Email" type="email" placeholder="you@example.com" />
|
|
718
|
+
<Input label="Password" type="password" placeholder="ā¢ā¢ā¢ā¢ā¢ā¢ā¢ā¢" />
|
|
719
|
+
|
|
720
|
+
<Button className="w-full" onClick={onSubmit}>
|
|
721
|
+
Sign In
|
|
722
|
+
</Button>
|
|
723
|
+
|
|
724
|
+
<p className="text-sm text-center text-gray-600">
|
|
725
|
+
Donāt have an account?{" "}
|
|
726
|
+
<a href="/register" className="text-blue-600 hover:underline">
|
|
727
|
+
Create one
|
|
728
|
+
</a>
|
|
729
|
+
</p>
|
|
730
|
+
</Card>
|
|
731
|
+
</div>
|
|
732
|
+
);
|
|
733
|
+
}`);
|
|
734
|
+
|
|
735
|
+
write("src/pages/auth/Register.jsx", `
|
|
736
|
+
import Button from "../../components/ui/Button.jsx";
|
|
737
|
+
import Input from "../../components/ui/Input.jsx";
|
|
738
|
+
import Card from "../../components/ui/Card.jsx";
|
|
739
|
+
|
|
740
|
+
export default function Register({ onSubmit }) {
|
|
741
|
+
return (
|
|
742
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
|
743
|
+
<Card className="w-full max-w-sm space-y-4">
|
|
744
|
+
<h2 className="text-xl font-bold">Create Account</h2>
|
|
745
|
+
|
|
746
|
+
<Input label="Full Name" placeholder="Your Name" />
|
|
747
|
+
<Input label="Email" type="email" placeholder="you@example.com" />
|
|
748
|
+
<Input label="Password" type="password" placeholder="" />
|
|
749
|
+
|
|
750
|
+
<Button className="w-full" onClick={onSubmit}>
|
|
751
|
+
Register
|
|
752
|
+
</Button>
|
|
753
|
+
|
|
754
|
+
<p className="text-sm text-center text-gray-600">
|
|
755
|
+
Already have an account?{" "}
|
|
756
|
+
<a href="/login" className="text-blue-600 hover:underline">
|
|
757
|
+
Sign in
|
|
758
|
+
</a>
|
|
759
|
+
</p>
|
|
760
|
+
</Card>
|
|
761
|
+
</div>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
`);
|
|
765
|
+
|
|
766
|
+
// -------------------------------
|
|
767
|
+
// Layout Components
|
|
768
|
+
// -------------------------------
|
|
769
|
+
|
|
770
|
+
write("src/components/layout/Container.jsx", `
|
|
771
|
+
export default function Container({ children, className = "" }) {
|
|
772
|
+
return (
|
|
773
|
+
<div className={\`max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 \${className}\`}>
|
|
774
|
+
{children}
|
|
775
|
+
</div>
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
`);
|
|
779
|
+
|
|
780
|
+
write("src/components/layout/Section.jsx", `
|
|
781
|
+
export default function Section({ children, className = "" }) {
|
|
782
|
+
return (
|
|
783
|
+
<section className={\`py-12 \${className}\`}>
|
|
784
|
+
{children}
|
|
785
|
+
</section>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
`);
|
|
789
|
+
|
|
790
|
+
// -------------------------------
|
|
791
|
+
// API Utility
|
|
792
|
+
// -------------------------------
|
|
793
|
+
|
|
794
|
+
write("src/utils/api.js", `
|
|
795
|
+
import axios from "axios";
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Axios-powered API client with CRUD helpers.
|
|
799
|
+
*/
|
|
800
|
+
|
|
801
|
+
export class ApiClient {
|
|
802
|
+
constructor(baseURL = "") {
|
|
803
|
+
this.instance = axios.create({
|
|
804
|
+
baseURL,
|
|
805
|
+
headers: { "Content-Type": "application/json" }
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
this.instance.interceptors.response.use(
|
|
809
|
+
(res) => res,
|
|
810
|
+
(err) => {
|
|
811
|
+
const message = err?.response?.data?.message || err?.message || "Request failed";
|
|
812
|
+
return Promise.reject(new Error(message));
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
setToken(token) {
|
|
818
|
+
if (token) {
|
|
819
|
+
this.instance.defaults.headers.common["Authorization"] = "Bearer " + token;
|
|
820
|
+
} else {
|
|
821
|
+
delete this.instance.defaults.headers.common["Authorization"];
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async request(config) {
|
|
826
|
+
try {
|
|
827
|
+
const res = await this.instance.request(config);
|
|
828
|
+
return { success: true, data: res.data };
|
|
829
|
+
} catch (error) {
|
|
830
|
+
return { success: false, error: error.message };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async getAll(url, config = {}) {
|
|
835
|
+
return this.request({ url, method: "GET", ...config });
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async getOne(url, config = {}) {
|
|
839
|
+
return this.request({ url, method: "GET", ...config });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async create(url, data, config = {}) {
|
|
843
|
+
return this.request({ url, method: "POST", data, ...config });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async update(url, data, config = {}) {
|
|
847
|
+
return this.request({ url, method: "PUT", data, ...config });
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async patch(url, data, config = {}) {
|
|
851
|
+
return this.request({ url, method: "PATCH", data, ...config });
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async delete(url, config = {}) {
|
|
855
|
+
return this.request({ url, method: "DELETE", ...config });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export default new ApiClient(import.meta.env.VITE_API_BASE_URL || "");
|
|
860
|
+
`);
|
|
861
|
+
|
|
862
|
+
// -------------------------------
|
|
863
|
+
// Password Utility
|
|
864
|
+
// -------------------------------
|
|
865
|
+
|
|
866
|
+
write("src/utils/password.js", `
|
|
867
|
+
import bcrypt from "bcryptjs";
|
|
868
|
+
|
|
869
|
+
const SALT_ROUNDS = 10;
|
|
870
|
+
|
|
871
|
+
export async function hashPassword(password) {
|
|
872
|
+
if (!password || typeof password !== "string" || password.trim().length === 0) {
|
|
873
|
+
throw new Error("Password must be a non-empty string");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
const hash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
878
|
+
return hash;
|
|
879
|
+
} catch (error) {
|
|
880
|
+
throw new Error("Error hashing password: " + error.message);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export async function verifyPassword(password, hash) {
|
|
885
|
+
if (!password || typeof password !== "string") {
|
|
886
|
+
throw new Error("Password must be a non-empty string");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (!hash || typeof hash !== "string") {
|
|
890
|
+
throw new Error("Hash must be a valid string");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
try {
|
|
894
|
+
const isValid = await bcrypt.compare(password, hash);
|
|
895
|
+
return isValid;
|
|
896
|
+
} catch (error) {
|
|
897
|
+
throw new Error("Error verifying password: " + error.message);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function getPasswordStrength(password) {
|
|
902
|
+
if (!password) return 0;
|
|
903
|
+
|
|
904
|
+
let strength = 0;
|
|
905
|
+
|
|
906
|
+
if (password.length >= 8) strength++;
|
|
907
|
+
if (password.length >= 12) strength++;
|
|
908
|
+
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
|
909
|
+
if (/\d/.test(password)) strength++;
|
|
910
|
+
if (/[!@#\$%^&*(),.?":{}|<>]/.test(password)) strength++;
|
|
911
|
+
|
|
912
|
+
return Math.min(strength, 4);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export function getPasswordStrengthLabel(password) {
|
|
916
|
+
const levels = ["Weak", "Fair", "Good", "Strong", "Very Strong"];
|
|
917
|
+
const strength = getPasswordStrength(password);
|
|
918
|
+
return levels[strength];
|
|
919
|
+
}
|
|
920
|
+
`);
|
|
921
|
+
|
|
922
|
+
console.log("\nā
React scaffold and demo complete!");
|
|
923
|
+
console.log("\nNext steps:");
|
|
924
|
+
console.log(" 1. npm run dev");
|
|
925
|
+
console.log(" 3. Open http://localhost:3000 in your browser\n");
|
|
926
|
+
|
|
927
|
+
// Create assets folder structure
|
|
928
|
+
fs.mkdirSync(path.join(BASE_DIR, "src/assets/images"), { recursive: true });
|
|
929
|
+
console.log("ā Created: src/assets/images\n");
|
|
930
|
+
|
|
931
|
+
if (doInstall) {
|
|
932
|
+
installDependencies(packageManager, BASE_DIR);
|
|
933
|
+
} else {
|
|
934
|
+
console.log("\nā¹ļø Skipping install (flag --no-install). Run manually later.");
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|