create-bdpa-react-scaffold 1.2.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 +83 -141
- 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,6 +88,7 @@ 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",
|
|
@@ -152,7 +153,7 @@ write("index.html", `
|
|
|
152
153
|
<div id="root"></div>
|
|
153
154
|
<script type="module" src="/src/main.jsx"></script>
|
|
154
155
|
</body>
|
|
155
|
-
</html>
|
|
156
|
+
</html>
|
|
156
157
|
`);
|
|
157
158
|
|
|
158
159
|
// -------------------------------
|
|
@@ -214,10 +215,6 @@ export { default as api, ApiClient } from "./utils/api.js";
|
|
|
214
215
|
export { hashPassword, verifyPassword, getPasswordStrength, getPasswordStrengthLabel } from "./utils/password.js";
|
|
215
216
|
`);
|
|
216
217
|
|
|
217
|
-
// -------------------------------
|
|
218
|
-
// App.jsx (FULL DEMO)
|
|
219
|
-
// -------------------------------
|
|
220
|
-
|
|
221
218
|
write("src/App.jsx", `
|
|
222
219
|
import { useState } from "react";
|
|
223
220
|
import { Routes, Route, useNavigate } from "react-router-dom";
|
|
@@ -231,6 +228,7 @@ import {
|
|
|
231
228
|
Sidebar,
|
|
232
229
|
Modal,
|
|
233
230
|
Tabs,
|
|
231
|
+
ApiClient,
|
|
234
232
|
useToast,
|
|
235
233
|
Login,
|
|
236
234
|
Register
|
|
@@ -251,8 +249,24 @@ const data = [
|
|
|
251
249
|
function Dashboard() {
|
|
252
250
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
253
251
|
const [modalOpen, setModalOpen] = useState(false);
|
|
252
|
+
const [posts, setPosts] = useState([]);
|
|
253
|
+
const [loadingPosts, setLoadingPosts] = useState(false);
|
|
254
|
+
const [postsError, setPostsError] = useState("");
|
|
254
255
|
const toast = useToast();
|
|
255
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
|
+
};
|
|
256
270
|
|
|
257
271
|
const tabs = [
|
|
258
272
|
{ label: "Overview", content: <p>Welcome to the UI Library demo.</p> },
|
|
@@ -329,6 +343,28 @@ function Dashboard() {
|
|
|
329
343
|
</div>
|
|
330
344
|
</Card>
|
|
331
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
|
+
|
|
332
368
|
{/* Modal + Toast */}
|
|
333
369
|
<div className="flex gap-4">
|
|
334
370
|
<Button onClick={() => setModalOpen(true)}>Open Modal</Button>
|
|
@@ -752,125 +788,70 @@ export default function Section({ children, className = "" }) {
|
|
|
752
788
|
// -------------------------------
|
|
753
789
|
|
|
754
790
|
write("src/utils/api.js", `
|
|
791
|
+
import axios from "axios";
|
|
792
|
+
|
|
755
793
|
/**
|
|
756
|
-
* API
|
|
757
|
-
*
|
|
758
|
-
* Usage:
|
|
759
|
-
* const api = new ApiClient("https://api.example.com");
|
|
760
|
-
*
|
|
761
|
-
* // Create
|
|
762
|
-
* await api.create("/students", { name: "John", email: "john@example.com" });
|
|
763
|
-
*
|
|
764
|
-
* // Read
|
|
765
|
-
* await api.getAll("/students");
|
|
766
|
-
* await api.getOne("/students/1");
|
|
767
|
-
*
|
|
768
|
-
* // Update
|
|
769
|
-
* await api.update("/students/1", { name: "Jane" });
|
|
770
|
-
*
|
|
771
|
-
* // Delete
|
|
772
|
-
* await api.delete("/students/1");
|
|
794
|
+
* Axios-powered API client with CRUD helpers.
|
|
773
795
|
*/
|
|
774
796
|
|
|
775
797
|
export class ApiClient {
|
|
776
798
|
constructor(baseURL = "") {
|
|
777
|
-
this.
|
|
778
|
-
|
|
799
|
+
this.instance = axios.create({
|
|
800
|
+
baseURL,
|
|
801
|
+
headers: { "Content-Type": "application/json" }
|
|
802
|
+
});
|
|
779
803
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
+
}
|
|
788
812
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
}
|
|
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
|
+
}
|
|
793
820
|
|
|
821
|
+
async request(config) {
|
|
794
822
|
try {
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
if (!response.ok) {
|
|
798
|
-
throw new Error(\`API Error: \${response.status} \${response.statusText}\`);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const data = await response.json();
|
|
802
|
-
return { success: true, data };
|
|
823
|
+
const res = await this.instance.request(config);
|
|
824
|
+
return { success: true, data: res.data };
|
|
803
825
|
} catch (error) {
|
|
804
|
-
console.error("API Request Error:", error);
|
|
805
826
|
return { success: false, error: error.message };
|
|
806
827
|
}
|
|
807
828
|
}
|
|
808
829
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
*/
|
|
812
|
-
async getAll(endpoint, options = {}) {
|
|
813
|
-
return this.request(endpoint, {
|
|
814
|
-
method: "GET",
|
|
815
|
-
...options
|
|
816
|
-
});
|
|
830
|
+
async getAll(url, config = {}) {
|
|
831
|
+
return this.request({ url, method: "GET", ...config });
|
|
817
832
|
}
|
|
818
833
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
*/
|
|
822
|
-
async getOne(endpoint, options = {}) {
|
|
823
|
-
return this.request(endpoint, {
|
|
824
|
-
method: "GET",
|
|
825
|
-
...options
|
|
826
|
-
});
|
|
834
|
+
async getOne(url, config = {}) {
|
|
835
|
+
return this.request({ url, method: "GET", ...config });
|
|
827
836
|
}
|
|
828
837
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
*/
|
|
832
|
-
async create(endpoint, data, options = {}) {
|
|
833
|
-
return this.request(endpoint, {
|
|
834
|
-
method: "POST",
|
|
835
|
-
body: JSON.stringify(data),
|
|
836
|
-
...options
|
|
837
|
-
});
|
|
838
|
+
async create(url, data, config = {}) {
|
|
839
|
+
return this.request({ url, method: "POST", data, ...config });
|
|
838
840
|
}
|
|
839
841
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
*/
|
|
843
|
-
async update(endpoint, data, options = {}) {
|
|
844
|
-
return this.request(endpoint, {
|
|
845
|
-
method: "PUT",
|
|
846
|
-
body: JSON.stringify(data),
|
|
847
|
-
...options
|
|
848
|
-
});
|
|
842
|
+
async update(url, data, config = {}) {
|
|
843
|
+
return this.request({ url, method: "PUT", data, ...config });
|
|
849
844
|
}
|
|
850
845
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
*/
|
|
854
|
-
async patch(endpoint, data, options = {}) {
|
|
855
|
-
return this.request(endpoint, {
|
|
856
|
-
method: "PATCH",
|
|
857
|
-
body: JSON.stringify(data),
|
|
858
|
-
...options
|
|
859
|
-
});
|
|
846
|
+
async patch(url, data, config = {}) {
|
|
847
|
+
return this.request({ url, method: "PATCH", data, ...config });
|
|
860
848
|
}
|
|
861
849
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
*/
|
|
865
|
-
async delete(endpoint, options = {}) {
|
|
866
|
-
return this.request(endpoint, {
|
|
867
|
-
method: "DELETE",
|
|
868
|
-
...options
|
|
869
|
-
});
|
|
850
|
+
async delete(url, config = {}) {
|
|
851
|
+
return this.request({ url, method: "DELETE", ...config });
|
|
870
852
|
}
|
|
871
853
|
}
|
|
872
854
|
|
|
873
|
-
// Export a default instance (optional - users can create their own)
|
|
874
855
|
export default new ApiClient(import.meta.env.VITE_API_BASE_URL || "");
|
|
875
856
|
`);
|
|
876
857
|
|
|
@@ -881,28 +862,8 @@ export default new ApiClient(import.meta.env.VITE_API_BASE_URL || "");
|
|
|
881
862
|
write("src/utils/password.js", `
|
|
882
863
|
import bcrypt from "bcryptjs";
|
|
883
864
|
|
|
884
|
-
/**
|
|
885
|
-
* Password Utility for secure password hashing and verification
|
|
886
|
-
* Uses bcryptjs with salt for secure password storage
|
|
887
|
-
*
|
|
888
|
-
* Usage:
|
|
889
|
-
* import { hashPassword, verifyPassword } from "./utils/password.js";
|
|
890
|
-
*
|
|
891
|
-
* // Hash a password
|
|
892
|
-
* const hash = await hashPassword("myPassword123");
|
|
893
|
-
*
|
|
894
|
-
* // Verify a password
|
|
895
|
-
* const isValid = await verifyPassword("myPassword123", hash);
|
|
896
|
-
*/
|
|
897
|
-
|
|
898
865
|
const SALT_ROUNDS = 10;
|
|
899
866
|
|
|
900
|
-
/**
|
|
901
|
-
* Hash a password with bcryptjs
|
|
902
|
-
* @param {string} password - The plain text password to hash
|
|
903
|
-
* @returns {Promise<string>} The hashed password
|
|
904
|
-
* @throws {Error} If password is empty or invalid
|
|
905
|
-
*/
|
|
906
867
|
export async function hashPassword(password) {
|
|
907
868
|
if (!password || typeof password !== "string" || password.trim().length === 0) {
|
|
908
869
|
throw new Error("Password must be a non-empty string");
|
|
@@ -912,17 +873,10 @@ export async function hashPassword(password) {
|
|
|
912
873
|
const hash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
913
874
|
return hash;
|
|
914
875
|
} catch (error) {
|
|
915
|
-
throw new Error(
|
|
876
|
+
throw new Error("Error hashing password: " + error.message);
|
|
916
877
|
}
|
|
917
878
|
}
|
|
918
879
|
|
|
919
|
-
/**
|
|
920
|
-
* Verify a password against a hash
|
|
921
|
-
* @param {string} password - The plain text password to verify
|
|
922
|
-
* @param {string} hash - The hash to compare against
|
|
923
|
-
* @returns {Promise<boolean>} True if password matches the hash, false otherwise
|
|
924
|
-
* @throws {Error} If inputs are invalid
|
|
925
|
-
*/
|
|
926
880
|
export async function verifyPassword(password, hash) {
|
|
927
881
|
if (!password || typeof password !== "string") {
|
|
928
882
|
throw new Error("Password must be a non-empty string");
|
|
@@ -936,37 +890,24 @@ export async function verifyPassword(password, hash) {
|
|
|
936
890
|
const isValid = await bcrypt.compare(password, hash);
|
|
937
891
|
return isValid;
|
|
938
892
|
} catch (error) {
|
|
939
|
-
throw new Error(
|
|
893
|
+
throw new Error("Error verifying password: " + error.message);
|
|
940
894
|
}
|
|
941
895
|
}
|
|
942
896
|
|
|
943
|
-
/**
|
|
944
|
-
* Get password strength indicator (0-4)
|
|
945
|
-
* @param {string} password - The password to evaluate
|
|
946
|
-
* @returns {number} Strength level: 0=weak, 1=fair, 2=good, 3=strong, 4=very strong
|
|
947
|
-
*/
|
|
948
897
|
export function getPasswordStrength(password) {
|
|
949
898
|
if (!password) return 0;
|
|
950
899
|
|
|
951
900
|
let strength = 0;
|
|
952
901
|
|
|
953
|
-
// Length check
|
|
954
902
|
if (password.length >= 8) strength++;
|
|
955
903
|
if (password.length >= 12) strength++;
|
|
956
|
-
|
|
957
|
-
// Character variety checks
|
|
958
904
|
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
|
959
|
-
if (
|
|
905
|
+
if (/\d/.test(password)) strength++;
|
|
960
906
|
if (/[!@#\$%^&*(),.?":{}|<>]/.test(password)) strength++;
|
|
961
907
|
|
|
962
908
|
return Math.min(strength, 4);
|
|
963
909
|
}
|
|
964
910
|
|
|
965
|
-
/**
|
|
966
|
-
* Get password strength label
|
|
967
|
-
* @param {string} password - The password to evaluate
|
|
968
|
-
* @returns {string} Human-readable strength label
|
|
969
|
-
*/
|
|
970
911
|
export function getPasswordStrengthLabel(password) {
|
|
971
912
|
const levels = ["Weak", "Fair", "Good", "Strong", "Very Strong"];
|
|
972
913
|
const strength = getPasswordStrength(password);
|
|
@@ -979,9 +920,10 @@ console.log("\nNext steps:");
|
|
|
979
920
|
console.log(" 1. npm run dev");
|
|
980
921
|
console.log(" 3. Open http://localhost:5173 in your browser\n");
|
|
981
922
|
|
|
982
|
-
// Install dependencies unless disabled
|
|
983
923
|
if (doInstall) {
|
|
984
924
|
installDependencies(packageManager, BASE_DIR);
|
|
985
925
|
} else {
|
|
986
926
|
console.log("\nℹ️ Skipping install (flag --no-install). Run manually later.");
|
|
987
927
|
}
|
|
928
|
+
|
|
929
|
+
|