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.
Files changed (55) hide show
  1. package/README.md +27 -0
  2. package/bin/index.js +485 -0
  3. package/package.json +30 -0
  4. package/templates/mysql/client/index.html +12 -0
  5. package/templates/mysql/client/package.json +21 -0
  6. package/templates/mysql/client/postcss.config.js +6 -0
  7. package/templates/mysql/client/src/App.jsx +5 -0
  8. package/templates/mysql/client/src/api/client.js +6 -0
  9. package/templates/mysql/client/src/components/Navbar.jsx +75 -0
  10. package/templates/mysql/client/src/components/ProtectedRoute.jsx +10 -0
  11. package/templates/mysql/client/src/index.css +24 -0
  12. package/templates/mysql/client/src/layouts/DashboardLayout.jsx +12 -0
  13. package/templates/mysql/client/src/main.jsx +10 -0
  14. package/templates/mysql/client/src/pages/Dashboard.jsx +44 -0
  15. package/templates/mysql/client/src/pages/Login.jsx +121 -0
  16. package/templates/mysql/client/src/pages/Page1.jsx +101 -0
  17. package/templates/mysql/client/src/pages/Page2.jsx +82 -0
  18. package/templates/mysql/client/src/pages/Page3.jsx +49 -0
  19. package/templates/mysql/client/src/router/index.jsx +43 -0
  20. package/templates/mysql/client/src/utils/auth.js +16 -0
  21. package/templates/mysql/client/tailwind.config.js +15 -0
  22. package/templates/mysql/client/vite.config.js +6 -0
  23. package/templates/mysql/server/config/db.js +13 -0
  24. package/templates/mysql/server/controllers/authController.js +66 -0
  25. package/templates/mysql/server/index.js +39 -0
  26. package/templates/mysql/server/middleware/authMiddleware.js +7 -0
  27. package/templates/mysql/server/package.json +18 -0
  28. package/templates/mysql/server/routes/authRoutes.js +14 -0
  29. package/templates/sequelize/client/index.html +12 -0
  30. package/templates/sequelize/client/package.json +21 -0
  31. package/templates/sequelize/client/postcss.config.js +6 -0
  32. package/templates/sequelize/client/src/App.jsx +5 -0
  33. package/templates/sequelize/client/src/api/client.js +6 -0
  34. package/templates/sequelize/client/src/components/Navbar.jsx +75 -0
  35. package/templates/sequelize/client/src/components/ProtectedRoute.jsx +10 -0
  36. package/templates/sequelize/client/src/index.css +24 -0
  37. package/templates/sequelize/client/src/layouts/DashboardLayout.jsx +12 -0
  38. package/templates/sequelize/client/src/main.jsx +10 -0
  39. package/templates/sequelize/client/src/pages/Dashboard.jsx +44 -0
  40. package/templates/sequelize/client/src/pages/Login.jsx +121 -0
  41. package/templates/sequelize/client/src/pages/Page1.jsx +101 -0
  42. package/templates/sequelize/client/src/pages/Page2.jsx +82 -0
  43. package/templates/sequelize/client/src/pages/Page3.jsx +49 -0
  44. package/templates/sequelize/client/src/router/index.jsx +43 -0
  45. package/templates/sequelize/client/src/utils/auth.js +16 -0
  46. package/templates/sequelize/client/tailwind.config.js +15 -0
  47. package/templates/sequelize/client/vite.config.js +6 -0
  48. package/templates/sequelize/server/config/db.js +13 -0
  49. package/templates/sequelize/server/config/sequelize.js +17 -0
  50. package/templates/sequelize/server/controllers/authController.js +62 -0
  51. package/templates/sequelize/server/index.js +42 -0
  52. package/templates/sequelize/server/middleware/authMiddleware.js +7 -0
  53. package/templates/sequelize/server/models/User.js +24 -0
  54. package/templates/sequelize/server/package.json +19 -0
  55. 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,10 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+ import "./index.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
@@ -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,15 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,jsx}"
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ brand: "__THEME__"
11
+ }
12
+ }
13
+ },
14
+ plugins: []
15
+ };
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()]
6
+ });
@@ -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,7 @@
1
+ export const protect = (req, res, next) => {
2
+ if (!req.session.user) {
3
+ return res.status(401).json({ message: "Unauthorized" });
4
+ }
5
+
6
+ next();
7
+ };
@@ -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
+ }