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,14 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import {
|
|
3
|
+
register,
|
|
4
|
+
login,
|
|
5
|
+
logout
|
|
6
|
+
} from "../controllers/authController.js";
|
|
7
|
+
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
|
|
10
|
+
router.post("/register", register);
|
|
11
|
+
router.post("/login", login);
|
|
12
|
+
router.post("/logout", logout);
|
|
13
|
+
|
|
14
|
+
export default router;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Parachute</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"scripts": {
|
|
4
|
+
"dev": "vite",
|
|
5
|
+
"build": "vite build",
|
|
6
|
+
"preview": "vite preview"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"axios": "^1.7.2",
|
|
10
|
+
"react": "^18.3.1",
|
|
11
|
+
"react-dom": "^18.3.1",
|
|
12
|
+
"react-router-dom": "^6.30.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
16
|
+
"autoprefixer": "^10.4.20",
|
|
17
|
+
"postcss": "^8.4.41",
|
|
18
|
+
"tailwindcss": "^3.4.10",
|
|
19
|
+
"vite": "^5.3.4"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NavLink, useNavigate } from "react-router-dom";
|
|
2
|
+
import { api } from "../api/client";
|
|
3
|
+
import { clearUser, getUser } from "../utils/auth";
|
|
4
|
+
|
|
5
|
+
const links = [
|
|
6
|
+
{ to: "/", label: "Dashboard" },
|
|
7
|
+
{ to: "/page1", label: "Form" },
|
|
8
|
+
{ to: "/page2", label: "Table" },
|
|
9
|
+
{ to: "/page3", label: "Cards" }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export default function Navbar() {
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
const user = getUser();
|
|
15
|
+
|
|
16
|
+
const logout = async () => {
|
|
17
|
+
try {
|
|
18
|
+
await api.post("/auth/logout");
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
clearUser();
|
|
22
|
+
navigate("/login");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white/95 backdrop-blur">
|
|
27
|
+
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-4 py-4 sm:px-6 lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
|
28
|
+
<div className="flex items-center justify-between gap-4">
|
|
29
|
+
<div>
|
|
30
|
+
<p className="text-lg font-black tracking-tight text-slate-950">Parachute</p>
|
|
31
|
+
<p className="text-xs text-slate-500">Full-stack starter</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
onClick={logout}
|
|
36
|
+
className="rounded-xl bg-slate-950 px-4 py-2 text-sm font-bold text-white lg:hidden"
|
|
37
|
+
>
|
|
38
|
+
Logout
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<nav className="flex gap-2 overflow-x-auto rounded-2xl border border-slate-200 bg-slate-50 p-1">
|
|
43
|
+
{links.map((link) => (
|
|
44
|
+
<NavLink
|
|
45
|
+
key={link.to}
|
|
46
|
+
to={link.to}
|
|
47
|
+
end={link.to === "/"}
|
|
48
|
+
className={({ isActive }) =>
|
|
49
|
+
`whitespace-nowrap rounded-xl px-4 py-2 text-sm font-bold transition ${
|
|
50
|
+
isActive
|
|
51
|
+
? "bg-white text-slate-950 shadow-sm"
|
|
52
|
+
: "text-slate-500 hover:bg-white hover:text-slate-900"
|
|
53
|
+
}`
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
{link.label}
|
|
57
|
+
</NavLink>
|
|
58
|
+
))}
|
|
59
|
+
</nav>
|
|
60
|
+
|
|
61
|
+
<div className="hidden items-center gap-3 lg:flex">
|
|
62
|
+
<span className="max-w-[180px] truncate text-sm font-semibold text-slate-500">
|
|
63
|
+
{user?.Username || "User"}
|
|
64
|
+
</span>
|
|
65
|
+
<button
|
|
66
|
+
onClick={logout}
|
|
67
|
+
className="rounded-xl bg-slate-950 px-4 py-2 text-sm font-bold text-white transition hover:bg-slate-800"
|
|
68
|
+
>
|
|
69
|
+
Logout
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
min-width: 320px;
|
|
12
|
+
background: #f8fafc;
|
|
13
|
+
color: #111827;
|
|
14
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
a {
|
|
18
|
+
color: inherit;
|
|
19
|
+
text-decoration: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
input, select, textarea, button {
|
|
23
|
+
font: inherit;
|
|
24
|
+
}
|
|
@@ -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
|
+
}
|