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