create-bdpa-react-scaffold 1.1.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/create-ui-lib.js +230 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ npx create-bdpa-react-scaffold . --force --no-install
|
|
|
38
38
|
- Example UI components (Button, Card, Input, FormField, Table, Navbar, Sidebar, Modal, Tabs, Toast)
|
|
39
39
|
- Simple Auth pages (Login, Register)
|
|
40
40
|
- Demo app and wiring
|
|
41
|
+
- Axios-based API client with CRUD helpers (`src/utils/api.js`)
|
|
41
42
|
|
|
42
43
|
## Local Development (for this CLI)
|
|
43
44
|
|
|
@@ -81,6 +82,26 @@ After publishing, users can run:
|
|
|
81
82
|
npx create-bdpa-react-scaffold my-app
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
## API (Axios)
|
|
86
|
+
|
|
87
|
+
- Base URL: set `VITE_API_BASE_URL` in a `.env` file or your environment.
|
|
88
|
+
- Default export `api` is a preconfigured instance; `ApiClient` class is also exported.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import api, { ApiClient } from "./src/utils/api";
|
|
94
|
+
|
|
95
|
+
// Using default instance
|
|
96
|
+
await api.create("/students", { name: "Ada" });
|
|
97
|
+
const { data } = await api.getAll("/students");
|
|
98
|
+
|
|
99
|
+
// Or create your own client
|
|
100
|
+
const client = new ApiClient("https://api.example.com");
|
|
101
|
+
client.setToken("<jwt>");
|
|
102
|
+
await client.update("/students/1", { name: "Grace" });
|
|
103
|
+
```
|
|
104
|
+
|
|
84
105
|
## License
|
|
85
106
|
|
|
86
107
|
UNLICENSED (update as desired).
|
package/create-ui-lib.js
CHANGED
|
@@ -88,9 +88,12 @@ write("package.json", `
|
|
|
88
88
|
"preview": "vite preview"
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
+
"axios": "^1.6.8",
|
|
91
92
|
"react": "^18.2.0",
|
|
92
93
|
"react-dom": "^18.2.0",
|
|
93
|
-
"
|
|
94
|
+
"react-router-dom": "^6.20.0",
|
|
95
|
+
"lucide-react": "^0.344.0",
|
|
96
|
+
"bcryptjs": "^2.4.3"
|
|
94
97
|
},
|
|
95
98
|
"devDependencies": {
|
|
96
99
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
@@ -150,7 +153,7 @@ write("index.html", `
|
|
|
150
153
|
<div id="root"></div>
|
|
151
154
|
<script type="module" src="/src/main.jsx"></script>
|
|
152
155
|
</body>
|
|
153
|
-
</html>
|
|
156
|
+
</html>
|
|
154
157
|
`);
|
|
155
158
|
|
|
156
159
|
// -------------------------------
|
|
@@ -174,15 +177,18 @@ h1, h2, h3, h4 {
|
|
|
174
177
|
write("src/main.jsx", `
|
|
175
178
|
import React from "react";
|
|
176
179
|
import ReactDOM from "react-dom/client";
|
|
180
|
+
import { BrowserRouter } from "react-router-dom";
|
|
177
181
|
import App from "./App.jsx";
|
|
178
182
|
import "./index.css";
|
|
179
183
|
import { ToastProvider } from "./components/ui/ToastProvider.jsx";
|
|
180
184
|
|
|
181
185
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
|
182
186
|
<React.StrictMode>
|
|
183
|
-
<
|
|
184
|
-
<
|
|
185
|
-
|
|
187
|
+
<BrowserRouter>
|
|
188
|
+
<ToastProvider>
|
|
189
|
+
<App />
|
|
190
|
+
</ToastProvider>
|
|
191
|
+
</BrowserRouter>
|
|
186
192
|
</React.StrictMode>
|
|
187
193
|
);
|
|
188
194
|
`);
|
|
@@ -204,14 +210,14 @@ export { default as Register } from "./components/auth/Register.jsx";
|
|
|
204
210
|
|
|
205
211
|
export { default as Container } from "./components/layout/Container.jsx";
|
|
206
212
|
export { default as Section } from "./components/layout/Section.jsx";
|
|
207
|
-
`);
|
|
208
213
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
export { default as api, ApiClient } from "./utils/api.js";
|
|
215
|
+
export { hashPassword, verifyPassword, getPasswordStrength, getPasswordStrengthLabel } from "./utils/password.js";
|
|
216
|
+
`);
|
|
212
217
|
|
|
213
218
|
write("src/App.jsx", `
|
|
214
219
|
import { useState } from "react";
|
|
220
|
+
import { Routes, Route, useNavigate } from "react-router-dom";
|
|
215
221
|
import {
|
|
216
222
|
Button,
|
|
217
223
|
Card,
|
|
@@ -222,7 +228,10 @@ import {
|
|
|
222
228
|
Sidebar,
|
|
223
229
|
Modal,
|
|
224
230
|
Tabs,
|
|
225
|
-
|
|
231
|
+
ApiClient,
|
|
232
|
+
useToast,
|
|
233
|
+
Login,
|
|
234
|
+
Register
|
|
226
235
|
} from "./index.js";
|
|
227
236
|
|
|
228
237
|
const columns = [
|
|
@@ -237,10 +246,27 @@ const data = [
|
|
|
237
246
|
{ name: "Taylor", course: "eSports Strategy", status: "Enrolled" }
|
|
238
247
|
];
|
|
239
248
|
|
|
240
|
-
|
|
249
|
+
function Dashboard() {
|
|
241
250
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
242
251
|
const [modalOpen, setModalOpen] = useState(false);
|
|
252
|
+
const [posts, setPosts] = useState([]);
|
|
253
|
+
const [loadingPosts, setLoadingPosts] = useState(false);
|
|
254
|
+
const [postsError, setPostsError] = useState("");
|
|
243
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
|
+
};
|
|
244
270
|
|
|
245
271
|
const tabs = [
|
|
246
272
|
{ label: "Overview", content: <p>Welcome to the UI Library demo.</p> },
|
|
@@ -256,7 +282,7 @@ export default function App() {
|
|
|
256
282
|
open={sidebarOpen}
|
|
257
283
|
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
|
258
284
|
links={[
|
|
259
|
-
{ label: "Home", href: "
|
|
285
|
+
{ label: "Home", href: "/" },
|
|
260
286
|
{ label: "Login", href: "/login" },
|
|
261
287
|
{ label: "Register", href: "/register" }
|
|
262
288
|
]}
|
|
@@ -317,6 +343,28 @@ export default function App() {
|
|
|
317
343
|
</div>
|
|
318
344
|
</Card>
|
|
319
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
|
+
|
|
320
368
|
{/* Modal + Toast */}
|
|
321
369
|
<div className="flex gap-4">
|
|
322
370
|
<Button onClick={() => setModalOpen(true)}>Open Modal</Button>
|
|
@@ -338,6 +386,38 @@ export default function App() {
|
|
|
338
386
|
</div>
|
|
339
387
|
);
|
|
340
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
|
+
}
|
|
341
421
|
`);
|
|
342
422
|
|
|
343
423
|
// -------------------------------
|
|
@@ -489,6 +569,8 @@ export default function Navbar({ onMenuClick }) {
|
|
|
489
569
|
`);
|
|
490
570
|
|
|
491
571
|
write("src/components/ui/Sidebar.jsx", `
|
|
572
|
+
import { Link } from "react-router-dom";
|
|
573
|
+
|
|
492
574
|
export default function Sidebar({ open, onToggle, links }) {
|
|
493
575
|
return (
|
|
494
576
|
<div
|
|
@@ -508,9 +590,9 @@ export default function Sidebar({ open, onToggle, links }) {
|
|
|
508
590
|
<ul className="p-4 space-y-2">
|
|
509
591
|
{links.map((l) => (
|
|
510
592
|
<li key={l.label}>
|
|
511
|
-
<
|
|
593
|
+
<Link to={l.href} className="block px-2 py-2 rounded hover:bg-gray-100">
|
|
512
594
|
{l.label}
|
|
513
|
-
</
|
|
595
|
+
</Link>
|
|
514
596
|
</li>
|
|
515
597
|
))}
|
|
516
598
|
</ul>
|
|
@@ -701,14 +783,147 @@ export default function Section({ children, className = "" }) {
|
|
|
701
783
|
}
|
|
702
784
|
`);
|
|
703
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
|
+
|
|
704
918
|
console.log("\n✅ UI Library scaffolding complete!");
|
|
705
919
|
console.log("\nNext steps:");
|
|
706
920
|
console.log(" 1. npm run dev");
|
|
707
921
|
console.log(" 3. Open http://localhost:5173 in your browser\n");
|
|
708
922
|
|
|
709
|
-
// Install dependencies unless disabled
|
|
710
923
|
if (doInstall) {
|
|
711
924
|
installDependencies(packageManager, BASE_DIR);
|
|
712
925
|
} else {
|
|
713
926
|
console.log("\nℹ️ Skipping install (flag --no-install). Run manually later.");
|
|
714
927
|
}
|
|
928
|
+
|
|
929
|
+
|