create-batman 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/bin/index.js +485 -0
- package/package.json +30 -0
- package/templates/mysql/client/index.html +12 -0
- package/templates/mysql/client/package.json +21 -0
- package/templates/mysql/client/postcss.config.js +6 -0
- package/templates/mysql/client/src/App.jsx +5 -0
- package/templates/mysql/client/src/api/client.js +6 -0
- package/templates/mysql/client/src/components/Navbar.jsx +75 -0
- package/templates/mysql/client/src/components/ProtectedRoute.jsx +10 -0
- package/templates/mysql/client/src/index.css +24 -0
- package/templates/mysql/client/src/layouts/DashboardLayout.jsx +12 -0
- package/templates/mysql/client/src/main.jsx +10 -0
- package/templates/mysql/client/src/pages/Dashboard.jsx +44 -0
- package/templates/mysql/client/src/pages/Login.jsx +121 -0
- package/templates/mysql/client/src/pages/Page1.jsx +101 -0
- package/templates/mysql/client/src/pages/Page2.jsx +82 -0
- package/templates/mysql/client/src/pages/Page3.jsx +49 -0
- package/templates/mysql/client/src/router/index.jsx +43 -0
- package/templates/mysql/client/src/utils/auth.js +16 -0
- package/templates/mysql/client/tailwind.config.js +15 -0
- package/templates/mysql/client/vite.config.js +6 -0
- package/templates/mysql/server/config/db.js +13 -0
- package/templates/mysql/server/controllers/authController.js +66 -0
- package/templates/mysql/server/index.js +39 -0
- package/templates/mysql/server/middleware/authMiddleware.js +7 -0
- package/templates/mysql/server/package.json +18 -0
- package/templates/mysql/server/routes/authRoutes.js +14 -0
- package/templates/sequelize/client/index.html +12 -0
- package/templates/sequelize/client/package.json +21 -0
- package/templates/sequelize/client/postcss.config.js +6 -0
- package/templates/sequelize/client/src/App.jsx +5 -0
- package/templates/sequelize/client/src/api/client.js +6 -0
- package/templates/sequelize/client/src/components/Navbar.jsx +75 -0
- package/templates/sequelize/client/src/components/ProtectedRoute.jsx +10 -0
- package/templates/sequelize/client/src/index.css +24 -0
- package/templates/sequelize/client/src/layouts/DashboardLayout.jsx +12 -0
- package/templates/sequelize/client/src/main.jsx +10 -0
- package/templates/sequelize/client/src/pages/Dashboard.jsx +44 -0
- package/templates/sequelize/client/src/pages/Login.jsx +121 -0
- package/templates/sequelize/client/src/pages/Page1.jsx +101 -0
- package/templates/sequelize/client/src/pages/Page2.jsx +82 -0
- package/templates/sequelize/client/src/pages/Page3.jsx +49 -0
- package/templates/sequelize/client/src/router/index.jsx +43 -0
- package/templates/sequelize/client/src/utils/auth.js +16 -0
- package/templates/sequelize/client/tailwind.config.js +15 -0
- package/templates/sequelize/client/vite.config.js +6 -0
- package/templates/sequelize/server/config/db.js +13 -0
- package/templates/sequelize/server/config/sequelize.js +17 -0
- package/templates/sequelize/server/controllers/authController.js +62 -0
- package/templates/sequelize/server/index.js +42 -0
- package/templates/sequelize/server/middleware/authMiddleware.js +7 -0
- package/templates/sequelize/server/models/User.js +24 -0
- package/templates/sequelize/server/package.json +19 -0
- package/templates/sequelize/server/routes/authRoutes.js +14 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Navbar from "../components/Navbar";
|
|
2
|
+
|
|
3
|
+
export default function DashboardLayout({ children }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen bg-slate-50">
|
|
6
|
+
<Navbar />
|
|
7
|
+
<main className="mx-auto w-full max-w-6xl px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
|
|
8
|
+
{children}
|
|
9
|
+
</main>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import DashboardLayout from "../layouts/DashboardLayout";
|
|
2
|
+
|
|
3
|
+
const stats = [
|
|
4
|
+
{ label: "Users", value: "24", note: "Auth ready" },
|
|
5
|
+
{ label: "Pages", value: "3", note: "Templates included" },
|
|
6
|
+
{ label: "API", value: "OK", note: "Axios configured" }
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export default function Dashboard() {
|
|
10
|
+
return (
|
|
11
|
+
<DashboardLayout>
|
|
12
|
+
<section className="grid gap-6">
|
|
13
|
+
<div className="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
|
14
|
+
<p className="text-sm font-black uppercase tracking-[0.18em]" style={{ color: "__THEME__" }}>
|
|
15
|
+
Ready to customize
|
|
16
|
+
</p>
|
|
17
|
+
<div className="mt-3 grid gap-3 lg:grid-cols-[1fr_auto] lg:items-end">
|
|
18
|
+
<div>
|
|
19
|
+
<h1 className="text-3xl font-black tracking-tight text-slate-950 sm:text-4xl">
|
|
20
|
+
Welcome Dashboard
|
|
21
|
+
</h1>
|
|
22
|
+
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-500 sm:text-base">
|
|
23
|
+
This is your protected homepage. Use Page 1 for forms, Page 2 for tables, and Page 3 for cards/layout sections.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
<button className="w-full rounded-2xl bg-slate-950 px-5 py-3 text-sm font-black text-white sm:w-auto">
|
|
27
|
+
Start Building
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
33
|
+
{stats.map((stat) => (
|
|
34
|
+
<article key={stat.label} className="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm">
|
|
35
|
+
<p className="text-sm font-bold text-slate-500">{stat.label}</p>
|
|
36
|
+
<h2 className="mt-2 text-3xl font-black text-slate-950">{stat.value}</h2>
|
|
37
|
+
<p className="mt-2 text-sm text-slate-500">{stat.note}</p>
|
|
38
|
+
</article>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
</DashboardLayout>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { api } from "../api/client";
|
|
4
|
+
import { saveUser } from "../utils/auth";
|
|
5
|
+
|
|
6
|
+
export default function Login() {
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
const [message, setMessage] = useState("");
|
|
9
|
+
|
|
10
|
+
const [form, setForm] = useState({
|
|
11
|
+
Username: "",
|
|
12
|
+
Password: ""
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (localStorage.getItem("user")) {
|
|
17
|
+
navigate("/");
|
|
18
|
+
}
|
|
19
|
+
}, [navigate]);
|
|
20
|
+
|
|
21
|
+
const handleChange = (e) => {
|
|
22
|
+
setForm({ ...form, [e.target.name]: e.target.value });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const register = async () => {
|
|
26
|
+
try {
|
|
27
|
+
await api.post("/auth/register", form);
|
|
28
|
+
setMessage("Registered successfully. Now login.");
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setMessage(err.response?.data?.message || "Registration failed");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const login = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const res = await api.post("/auth/login", form);
|
|
37
|
+
saveUser(res.data);
|
|
38
|
+
navigate("/");
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setMessage(err.response?.data?.message || "Login failed");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<main className="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-8">
|
|
46
|
+
<section className="grid w-full max-w-5xl overflow-hidden rounded-[28px] border border-slate-200 bg-white shadow-sm lg:grid-cols-[1fr_420px]">
|
|
47
|
+
<div className="hidden bg-slate-950 p-10 text-white lg:flex lg:flex-col lg:justify-between">
|
|
48
|
+
<div>
|
|
49
|
+
<p className="text-sm font-black uppercase tracking-[0.2em] text-white/50">Parachute</p>
|
|
50
|
+
<h1 className="mt-5 max-w-md text-4xl font-black tracking-tight">
|
|
51
|
+
Simple exam starter with auth already wired.
|
|
52
|
+
</h1>
|
|
53
|
+
<p className="mt-4 max-w-md text-sm leading-6 text-white/65">
|
|
54
|
+
Login, protected pages, layouts, forms, tables, and responsive Tailwind structure ready to customize.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="rounded-2xl border border-white/10 bg-white/5 p-4 text-sm text-white/70">
|
|
58
|
+
Built for speed. Clean enough for real projects.
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="p-6 sm:p-8 lg:p-10">
|
|
63
|
+
<p className="text-sm font-black uppercase tracking-[0.18em]" style={{ color: "__THEME__" }}>
|
|
64
|
+
Welcome
|
|
65
|
+
</p>
|
|
66
|
+
<h2 className="mt-3 text-3xl font-black tracking-tight text-slate-950">Login or Register</h2>
|
|
67
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">
|
|
68
|
+
Use a username and password. Passwords are handled in the backend.
|
|
69
|
+
</p>
|
|
70
|
+
|
|
71
|
+
<div className="mt-6 grid gap-4">
|
|
72
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
73
|
+
Username
|
|
74
|
+
<input
|
|
75
|
+
name="Username"
|
|
76
|
+
placeholder="example: aime"
|
|
77
|
+
value={form.Username}
|
|
78
|
+
onChange={handleChange}
|
|
79
|
+
className="rounded-2xl border border-slate-200 bg-white px-4 py-3 outline-none transition focus:border-slate-400"
|
|
80
|
+
/>
|
|
81
|
+
</label>
|
|
82
|
+
|
|
83
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
84
|
+
Password
|
|
85
|
+
<input
|
|
86
|
+
name="Password"
|
|
87
|
+
type="password"
|
|
88
|
+
placeholder="••••••••"
|
|
89
|
+
value={form.Password}
|
|
90
|
+
onChange={handleChange}
|
|
91
|
+
className="rounded-2xl border border-slate-200 bg-white px-4 py-3 outline-none transition focus:border-slate-400"
|
|
92
|
+
/>
|
|
93
|
+
</label>
|
|
94
|
+
|
|
95
|
+
{message && (
|
|
96
|
+
<p className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-semibold text-slate-700">
|
|
97
|
+
{message}
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
102
|
+
<button
|
|
103
|
+
onClick={register}
|
|
104
|
+
className="rounded-2xl px-4 py-3 text-sm font-black text-white transition hover:opacity-90"
|
|
105
|
+
style={{ background: "__THEME__" }}
|
|
106
|
+
>
|
|
107
|
+
Register
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
onClick={login}
|
|
111
|
+
className="rounded-2xl bg-slate-950 px-4 py-3 text-sm font-black text-white transition hover:bg-slate-800"
|
|
112
|
+
>
|
|
113
|
+
Login
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</section>
|
|
119
|
+
</main>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import DashboardLayout from "../layouts/DashboardLayout";
|
|
3
|
+
|
|
4
|
+
export default function Page1() {
|
|
5
|
+
const [form, setForm] = useState({
|
|
6
|
+
fullName: "",
|
|
7
|
+
email: "",
|
|
8
|
+
role: "student",
|
|
9
|
+
level: "",
|
|
10
|
+
date: "",
|
|
11
|
+
amount: "",
|
|
12
|
+
description: "",
|
|
13
|
+
active: true
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const handleChange = (e) => {
|
|
17
|
+
const { name, value, type, checked } = e.target;
|
|
18
|
+
setForm({ ...form, [name]: type === "checkbox" ? checked : value });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleSubmit = (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
alert("Form submitted. Connect this to your API.");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<DashboardLayout>
|
|
28
|
+
<section className="grid gap-6">
|
|
29
|
+
<div className="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
|
30
|
+
<p className="text-sm font-black uppercase tracking-[0.18em]" style={{ color: "__THEME__" }}>
|
|
31
|
+
Page 1
|
|
32
|
+
</p>
|
|
33
|
+
<h1 className="mt-3 text-3xl font-black tracking-tight text-slate-950">Form Template</h1>
|
|
34
|
+
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
|
|
35
|
+
Copy this page for create/update screens. It includes text, email, select, number, date, textarea, and checkbox inputs.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<form onSubmit={handleSubmit} className="rounded-[28px] border border-slate-200 bg-white p-5 shadow-sm sm:p-6 lg:p-8">
|
|
40
|
+
<div className="grid gap-5 md:grid-cols-2">
|
|
41
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
42
|
+
Full name
|
|
43
|
+
<input name="fullName" value={form.fullName} onChange={handleChange} placeholder="Enter full name" className="rounded-2xl border border-slate-200 px-4 py-3 outline-none focus:border-slate-400" />
|
|
44
|
+
</label>
|
|
45
|
+
|
|
46
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
47
|
+
Email
|
|
48
|
+
<input name="email" type="email" value={form.email} onChange={handleChange} placeholder="name@example.com" className="rounded-2xl border border-slate-200 px-4 py-3 outline-none focus:border-slate-400" />
|
|
49
|
+
</label>
|
|
50
|
+
|
|
51
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
52
|
+
Role
|
|
53
|
+
<select name="role" value={form.role} onChange={handleChange} className="rounded-2xl border border-slate-200 px-4 py-3 outline-none focus:border-slate-400">
|
|
54
|
+
<option value="student">Student</option>
|
|
55
|
+
<option value="admin">Admin</option>
|
|
56
|
+
<option value="manager">Manager</option>
|
|
57
|
+
<option value="staff">Staff</option>
|
|
58
|
+
</select>
|
|
59
|
+
</label>
|
|
60
|
+
|
|
61
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
62
|
+
Level
|
|
63
|
+
<select name="level" value={form.level} onChange={handleChange} className="rounded-2xl border border-slate-200 px-4 py-3 outline-none focus:border-slate-400">
|
|
64
|
+
<option value="">Select level</option>
|
|
65
|
+
<option value="low">Low</option>
|
|
66
|
+
<option value="medium">Medium</option>
|
|
67
|
+
<option value="high">High</option>
|
|
68
|
+
</select>
|
|
69
|
+
</label>
|
|
70
|
+
|
|
71
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
72
|
+
Date
|
|
73
|
+
<input name="date" type="date" value={form.date} onChange={handleChange} className="rounded-2xl border border-slate-200 px-4 py-3 outline-none focus:border-slate-400" />
|
|
74
|
+
</label>
|
|
75
|
+
|
|
76
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700">
|
|
77
|
+
Amount
|
|
78
|
+
<input name="amount" type="number" value={form.amount} onChange={handleChange} placeholder="0" className="rounded-2xl border border-slate-200 px-4 py-3 outline-none focus:border-slate-400" />
|
|
79
|
+
</label>
|
|
80
|
+
|
|
81
|
+
<label className="grid gap-2 text-sm font-bold text-slate-700 md:col-span-2">
|
|
82
|
+
Description
|
|
83
|
+
<textarea name="description" value={form.description} onChange={handleChange} rows="4" placeholder="Write extra details here..." className="resize-none rounded-2xl border border-slate-200 px-4 py-3 outline-none focus:border-slate-400" />
|
|
84
|
+
</label>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="mt-6 flex flex-col gap-4 border-t border-slate-100 pt-6 sm:flex-row sm:items-center sm:justify-between">
|
|
88
|
+
<label className="flex items-center gap-3 text-sm font-bold text-slate-700">
|
|
89
|
+
<input name="active" type="checkbox" checked={form.active} onChange={handleChange} className="h-4 w-4 rounded" />
|
|
90
|
+
Mark record as active
|
|
91
|
+
</label>
|
|
92
|
+
|
|
93
|
+
<button type="submit" className="rounded-2xl px-6 py-3 text-sm font-black text-white" style={{ background: "__THEME__" }}>
|
|
94
|
+
Save Record
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</form>
|
|
98
|
+
</section>
|
|
99
|
+
</DashboardLayout>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import DashboardLayout from "../layouts/DashboardLayout";
|
|
2
|
+
|
|
3
|
+
const records = [
|
|
4
|
+
{ id: 1, name: "Amoxicillin", category: "Antibiotic", status: "Available", amount: "12,000 RWF" },
|
|
5
|
+
{ id: 2, name: "Paracetamol", category: "Pain relief", status: "Low stock", amount: "4,500 RWF" },
|
|
6
|
+
{ id: 3, name: "Cetirizine", category: "Allergy", status: "Available", amount: "6,800 RWF" },
|
|
7
|
+
{ id: 4, name: "Ibuprofen", category: "Pain relief", status: "Unavailable", amount: "7,200 RWF" }
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const badgeClass = {
|
|
11
|
+
Available: "bg-emerald-50 text-emerald-700 ring-emerald-200",
|
|
12
|
+
"Low stock": "bg-amber-50 text-amber-700 ring-amber-200",
|
|
13
|
+
Unavailable: "bg-rose-50 text-rose-700 ring-rose-200"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function Page2() {
|
|
17
|
+
return (
|
|
18
|
+
<DashboardLayout>
|
|
19
|
+
<section className="grid gap-6">
|
|
20
|
+
<div className="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
|
21
|
+
<p className="text-sm font-black uppercase tracking-[0.18em]" style={{ color: "__THEME__" }}>
|
|
22
|
+
Page 2
|
|
23
|
+
</p>
|
|
24
|
+
<div className="mt-3 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
25
|
+
<div>
|
|
26
|
+
<h1 className="text-3xl font-black tracking-tight text-slate-950">Table Template</h1>
|
|
27
|
+
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
|
|
28
|
+
Use this for list/read screens. It stays clean on phones and tablets with horizontal overflow.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
<button className="rounded-2xl bg-slate-950 px-5 py-3 text-sm font-black text-white">Add New</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="overflow-hidden rounded-[28px] border border-slate-200 bg-white shadow-sm">
|
|
36
|
+
<div className="border-b border-slate-100 p-4 sm:p-5">
|
|
37
|
+
<input
|
|
38
|
+
placeholder="Search records..."
|
|
39
|
+
className="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm outline-none focus:border-slate-400"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="overflow-x-auto">
|
|
44
|
+
<table className="min-w-[760px] w-full text-left text-sm">
|
|
45
|
+
<thead className="bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
|
|
46
|
+
<tr>
|
|
47
|
+
<th className="px-5 py-4 font-black">ID</th>
|
|
48
|
+
<th className="px-5 py-4 font-black">Name</th>
|
|
49
|
+
<th className="px-5 py-4 font-black">Category</th>
|
|
50
|
+
<th className="px-5 py-4 font-black">Status</th>
|
|
51
|
+
<th className="px-5 py-4 font-black">Amount</th>
|
|
52
|
+
<th className="px-5 py-4 text-right font-black">Actions</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody className="divide-y divide-slate-100">
|
|
56
|
+
{records.map((record) => (
|
|
57
|
+
<tr key={record.id} className="hover:bg-slate-50/70">
|
|
58
|
+
<td className="px-5 py-4 font-bold text-slate-500">#{record.id}</td>
|
|
59
|
+
<td className="px-5 py-4 font-black text-slate-950">{record.name}</td>
|
|
60
|
+
<td className="px-5 py-4 text-slate-600">{record.category}</td>
|
|
61
|
+
<td className="px-5 py-4">
|
|
62
|
+
<span className={`inline-flex rounded-full px-3 py-1 text-xs font-black ring-1 ${badgeClass[record.status]}`}>
|
|
63
|
+
{record.status}
|
|
64
|
+
</span>
|
|
65
|
+
</td>
|
|
66
|
+
<td className="px-5 py-4 font-bold text-slate-700">{record.amount}</td>
|
|
67
|
+
<td className="px-5 py-4">
|
|
68
|
+
<div className="flex justify-end gap-2">
|
|
69
|
+
<button className="rounded-xl border border-slate-200 px-3 py-2 text-xs font-black text-slate-700 hover:bg-slate-50">Edit</button>
|
|
70
|
+
<button className="rounded-xl border border-rose-200 px-3 py-2 text-xs font-black text-rose-600 hover:bg-rose-50">Delete</button>
|
|
71
|
+
</div>
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
))}
|
|
75
|
+
</tbody>
|
|
76
|
+
</table>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
</DashboardLayout>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import DashboardLayout from "../layouts/DashboardLayout";
|
|
2
|
+
|
|
3
|
+
const cards = [
|
|
4
|
+
{ title: "Simple card", body: "Use this for summaries, quick actions, or small modules." },
|
|
5
|
+
{ title: "Responsive grid", body: "On mobile it stacks. On tablets and laptops it becomes a clean grid." },
|
|
6
|
+
{ title: "Action section", body: "Good for dashboard shortcuts and small system features." }
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export default function Page3() {
|
|
10
|
+
return (
|
|
11
|
+
<DashboardLayout>
|
|
12
|
+
<section className="grid gap-6">
|
|
13
|
+
<div className="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
|
14
|
+
<p className="text-sm font-black uppercase tracking-[0.18em]" style={{ color: "__THEME__" }}>
|
|
15
|
+
Page 3
|
|
16
|
+
</p>
|
|
17
|
+
<h1 className="mt-3 text-3xl font-black tracking-tight text-slate-950">Cards Template</h1>
|
|
18
|
+
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
|
|
19
|
+
Use this for dashboard sections, action panels, feature cards, or simple report blocks.
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
24
|
+
{cards.map((card, index) => (
|
|
25
|
+
<article key={card.title} className="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm">
|
|
26
|
+
<div className="flex h-11 w-11 items-center justify-center rounded-2xl text-sm font-black text-white" style={{ background: index === 0 ? "__THEME__" : "#0f172a" }}>
|
|
27
|
+
{index + 1}
|
|
28
|
+
</div>
|
|
29
|
+
<h2 className="mt-5 text-xl font-black text-slate-950">{card.title}</h2>
|
|
30
|
+
<p className="mt-2 text-sm leading-6 text-slate-500">{card.body}</p>
|
|
31
|
+
</article>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="rounded-[28px] border border-slate-200 bg-slate-950 p-6 text-white shadow-sm sm:p-8">
|
|
36
|
+
<div className="grid gap-5 lg:grid-cols-[1fr_auto] lg:items-center">
|
|
37
|
+
<div>
|
|
38
|
+
<h2 className="text-2xl font-black tracking-tight">Ready for your real feature</h2>
|
|
39
|
+
<p className="mt-2 max-w-2xl text-sm leading-6 text-white/60">
|
|
40
|
+
Replace this with reports, profile settings, sales summaries, admin tools, or any exam requirement.
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
<button className="rounded-2xl bg-white px-5 py-3 text-sm font-black text-slate-950">Customize</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
</DashboardLayout>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
import ProtectedRoute from "../components/ProtectedRoute";
|
|
4
|
+
|
|
5
|
+
import Login from "../pages/Login";
|
|
6
|
+
import Dashboard from "../pages/Dashboard";
|
|
7
|
+
import Page1 from "../pages/Page1";
|
|
8
|
+
import Page2 from "../pages/Page2";
|
|
9
|
+
import Page3 from "../pages/Page3";
|
|
10
|
+
|
|
11
|
+
export default function Router() {
|
|
12
|
+
return (
|
|
13
|
+
<BrowserRouter>
|
|
14
|
+
<Routes>
|
|
15
|
+
<Route path="/login" element={<Login />} />
|
|
16
|
+
|
|
17
|
+
<Route path="/" element={
|
|
18
|
+
<ProtectedRoute>
|
|
19
|
+
<Dashboard />
|
|
20
|
+
</ProtectedRoute>
|
|
21
|
+
} />
|
|
22
|
+
|
|
23
|
+
<Route path="/page1" element={
|
|
24
|
+
<ProtectedRoute>
|
|
25
|
+
<Page1 />
|
|
26
|
+
</ProtectedRoute>
|
|
27
|
+
} />
|
|
28
|
+
|
|
29
|
+
<Route path="/page2" element={
|
|
30
|
+
<ProtectedRoute>
|
|
31
|
+
<Page2 />
|
|
32
|
+
</ProtectedRoute>
|
|
33
|
+
} />
|
|
34
|
+
|
|
35
|
+
<Route path="/page3" element={
|
|
36
|
+
<ProtectedRoute>
|
|
37
|
+
<Page3 />
|
|
38
|
+
</ProtectedRoute>
|
|
39
|
+
} />
|
|
40
|
+
</Routes>
|
|
41
|
+
</BrowserRouter>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const isAuthenticated = () => {
|
|
2
|
+
return !!localStorage.getItem("user");
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const getUser = () => {
|
|
6
|
+
const user = localStorage.getItem("user");
|
|
7
|
+
return user ? JSON.parse(user) : null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const saveUser = (user) => {
|
|
11
|
+
localStorage.setItem("user", JSON.stringify(user));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const clearUser = () => {
|
|
15
|
+
localStorage.removeItem("user");
|
|
16
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import mysql from "mysql2";
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
const db = mysql.createPool({
|
|
7
|
+
host: process.env.DB_HOST || "localhost",
|
|
8
|
+
user: process.env.DB_USER || "root",
|
|
9
|
+
password: process.env.DB_PASSWORD || "",
|
|
10
|
+
database: process.env.DB_NAME
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default db;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import bcrypt from "bcrypt";
|
|
2
|
+
import db from "../config/db.js";
|
|
3
|
+
|
|
4
|
+
export const register = async (req, res) => {
|
|
5
|
+
const { Username, Password } = req.body;
|
|
6
|
+
|
|
7
|
+
if (!Username || !Password) {
|
|
8
|
+
return res.status(400).json({ message: "Username and Password are required" });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const hashedPassword = await bcrypt.hash(Password, 10);
|
|
12
|
+
|
|
13
|
+
db.query(
|
|
14
|
+
"INSERT INTO Users (Username, Password) VALUES (?, ?)",
|
|
15
|
+
[Username, hashedPassword],
|
|
16
|
+
(err) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
if (err.code === "ER_DUP_ENTRY") {
|
|
19
|
+
return res.status(409).json({ message: "Username already exists" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return res.status(500).json({ message: "Registration failed", error: err.message });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
res.status(201).json({ message: "Registered successfully" });
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const login = (req, res) => {
|
|
31
|
+
const { Username, Password } = req.body;
|
|
32
|
+
|
|
33
|
+
db.query(
|
|
34
|
+
"SELECT * FROM Users WHERE Username = ?",
|
|
35
|
+
[Username],
|
|
36
|
+
async (err, data) => {
|
|
37
|
+
if (err) {
|
|
38
|
+
return res.status(500).json({ message: "Login failed", error: err.message });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (data.length === 0) {
|
|
42
|
+
return res.status(404).json({ message: "User not found" });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const user = data[0];
|
|
46
|
+
const validPassword = await bcrypt.compare(Password, user.Password);
|
|
47
|
+
|
|
48
|
+
if (!validPassword) {
|
|
49
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
req.session.user = {
|
|
53
|
+
UserID: user.UserID,
|
|
54
|
+
Username: user.Username
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
res.json(req.session.user);
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const logout = (req, res) => {
|
|
63
|
+
req.session.destroy(() => {
|
|
64
|
+
res.json({ message: "Logged out" });
|
|
65
|
+
});
|
|
66
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import session from "express-session";
|
|
5
|
+
import authRoutes from "./routes/authRoutes.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
dotenv.config();
|
|
9
|
+
|
|
10
|
+
const app = express();
|
|
11
|
+
|
|
12
|
+
app.use(cors({
|
|
13
|
+
origin: process.env.CLIENT_URL,
|
|
14
|
+
credentials: true
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
|
|
19
|
+
app.use(session({
|
|
20
|
+
secret: process.env.SESSION_SECRET,
|
|
21
|
+
resave: false,
|
|
22
|
+
saveUninitialized: false,
|
|
23
|
+
cookie: {
|
|
24
|
+
httpOnly: true,
|
|
25
|
+
secure: false
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
app.use("/auth", authRoutes);
|
|
30
|
+
|
|
31
|
+
app.get("/", (req, res) => {
|
|
32
|
+
res.json({ message: "Parachute backend running" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
app.listen(process.env.PORT, () => {
|
|
38
|
+
console.log(`Server running on port ${process.env.PORT}`);
|
|
39
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"scripts": {
|
|
4
|
+
"dev": "nodemon index.js",
|
|
5
|
+
"start": "node index.js"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"bcrypt": "^5.1.1",
|
|
9
|
+
"cors": "^2.8.5",
|
|
10
|
+
"dotenv": "^16.4.5",
|
|
11
|
+
"express": "^4.19.2",
|
|
12
|
+
"express-session": "^1.18.0",
|
|
13
|
+
"mysql2": "^3.10.3"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"nodemon": "^3.1.4"
|
|
17
|
+
}
|
|
18
|
+
}
|