create-butterfly-app-v2 1.0.1 → 2.0.0

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 (31) hide show
  1. package/cli.js +176 -35
  2. package/package.json +1 -1
  3. package/templates/base/backend/db/init.js +14 -0
  4. package/templates/base/backend/middleware/auth.js +18 -0
  5. package/templates/{fullstack → base}/backend/package.json +3 -1
  6. package/templates/base/backend/routes/auth.js +76 -0
  7. package/templates/{fullstack → base}/backend/server.js +12 -7
  8. package/templates/{fullstack → base}/frontend/package.json +3 -2
  9. package/templates/base/frontend/src/App.jsx +32 -0
  10. package/templates/base/frontend/src/context/AuthContext.jsx +33 -0
  11. package/templates/base/frontend/src/pages/Login.jsx +40 -0
  12. package/templates/base/frontend/src/pages/Register.jsx +42 -0
  13. package/templates/types/hotel/backend/db/init.js +44 -0
  14. package/templates/types/hotel/backend/routes/api.js +61 -0
  15. package/templates/types/hotel/frontend/src/pages/Dashboard.jsx +147 -0
  16. package/templates/types/parking/backend/db/init.js +45 -0
  17. package/templates/types/parking/backend/routes/api.js +66 -0
  18. package/templates/types/parking/frontend/src/pages/Dashboard.jsx +141 -0
  19. package/templates/types/student/backend/db/init.js +51 -0
  20. package/templates/types/student/backend/routes/api.js +80 -0
  21. package/templates/types/student/frontend/src/pages/Dashboard.jsx +181 -0
  22. package/templates/fullstack/backend/routes/api.js +0 -26
  23. package/templates/fullstack/frontend/src/App.jsx +0 -86
  24. /package/templates/{fullstack → base}/backend/config/db.js +0 -0
  25. /package/templates/{fullstack → base}/frontend/index.html +0 -0
  26. /package/templates/{fullstack → base}/frontend/postcss.config.js +0 -0
  27. /package/templates/{fullstack → base}/frontend/src/index.css +0 -0
  28. /package/templates/{fullstack → base}/frontend/src/main.jsx +0 -0
  29. /package/templates/{fullstack → base}/frontend/tailwind.config.js +0 -0
  30. /package/templates/{fullstack → base}/frontend/vite.config.js +0 -0
  31. /package/templates/{fullstack → base}/package.json +0 -0
package/cli.js CHANGED
@@ -2,48 +2,189 @@
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
4
  const { execSync } = require("child_process");
5
+ const readline = require("readline");
5
6
 
6
- const projectName = process.argv[2] || "my-fullstack-app";
7
- const targetDir = path.resolve(process.cwd(), projectName);
8
- const templateDir = path.join(__dirname, "templates", "fullstack");
7
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8
+ const ask = (q) => new Promise((r) => rl.question(q, r));
9
9
 
10
- if (fs.existsSync(targetDir)) {
11
- console.error(`Error: Directory "${projectName}" already exists.`);
12
- process.exit(1);
10
+ const C = {
11
+ reset: "\x1b[0m",
12
+ blue: "\x1b[34m",
13
+ cyan: "\x1b[36m",
14
+ green: "\x1b[32m",
15
+ yellow: "\x1b[33m",
16
+ red: "\x1b[31m",
17
+ magenta: "\x1b[35m",
18
+ bold: "\x1b[1m",
19
+ dim: "\x1b[2m",
20
+ };
21
+
22
+ function c(color, text) {
23
+ return `${C[color] || ""}${text}${C.reset}`;
24
+ }
25
+
26
+ function showBanner() {
27
+ console.log(`
28
+ ${c("cyan", " ╔══════════════════════════════════════════╗")}
29
+ ${c("cyan", " ║")} ${c("bold", "CREATE BUTTERFLY APP")} ${c("cyan", "║")}
30
+ ${c("cyan", " ║")} ${c("dim", "Full-Stack Project Generator")} ${c("cyan", "║")}
31
+ ${c("cyan", " ╠══════════════════════════════════════════╣")}
32
+ ${c("cyan", " ║")} ${c("cyan", "║")}
33
+ ${c("cyan", " ║")} ${c("blue", " ,,_")} ${c("cyan", "║")}
34
+ ${c("cyan", " ║")} ${c("blue", " \\\\\'-.")} ${c("cyan", "║")}
35
+ ${c("cyan", " ║")} ${c("blue", " ) -'\\")} ${c("green", "Node.js")} ${c("blue", "+")} ${c("green", "React")} ${c("blue", " +")} ${c("cyan", "║")}
36
+ ${c("cyan", " ║")} ${c("blue", " /_/|_\\\\")} ${c("green", "Tailwind")} ${c("blue", "+")} ${c("green", "MySQL")} ${c("cyan", "║")}
37
+ ${c("cyan", " ║")} ${c("blue", " \\\\__/")} ${c("cyan", "║")}
38
+ ${c("cyan", " ║")} ${c("cyan", "║")}
39
+ ${c("cyan", " ╚══════════════════════════════════════════╝${c("reset", "")}
40
+ `));
41
+ }
42
+
43
+ const PROJECT_TYPES = [
44
+ { id: "hotel", name: "Hotel Reservation", emoji: "🏨" },
45
+ { id: "parking", name: "Car Parking", emoji: "🅿️" },
46
+ { id: "student", name: "Student Management", emoji: "🎓" },
47
+ ];
48
+
49
+ async function selectProjectType() {
50
+ console.log(` ${c("bold", "Select your project type:")}\n`);
51
+ for (let i = 0; i < PROJECT_TYPES.length; i++) {
52
+ const t = PROJECT_TYPES[i];
53
+ console.log(` ${c("yellow", `[${i + 1}]`)} ${t.emoji} ${c("bold", t.name)}`);
54
+ }
55
+ console.log(` ${c("yellow", "[r]")} 🎲 ${c("bold", "Random")} - Surprise me!\n`);
56
+
57
+ let choice;
58
+ while (true) {
59
+ const input = await ask(` ${c("cyan", "?>")} Enter number or ${c("yellow", "r")} for random: `);
60
+ if (input.toLowerCase() === "r") {
61
+ const rand = Math.floor(Math.random() * PROJECT_TYPES.length);
62
+ choice = PROJECT_TYPES[rand];
63
+ console.log(` ${c("green", "✓")} Randomly selected: ${choice.emoji} ${c("bold", choice.name)}\n`);
64
+ break;
65
+ }
66
+ const num = parseInt(input);
67
+ if (num >= 1 && num <= PROJECT_TYPES.length) {
68
+ choice = PROJECT_TYPES[num - 1];
69
+ break;
70
+ }
71
+ console.log(` ${c("red", "✗")} Invalid choice, try again\n`);
72
+ }
73
+ return choice;
13
74
  }
14
75
 
15
- console.log(`\n Creating ${projectName}...\n`);
16
-
17
- function copyRecursive(src, dest) {
18
- fs.mkdirSync(dest, { recursive: true });
19
- const entries = fs.readdirSync(src, { withFileTypes: true });
20
- for (const entry of entries) {
21
- const srcPath = path.join(src, entry.name);
22
- const destPath = path.join(dest, entry.name);
23
- if (entry.isDirectory()) {
24
- copyRecursive(srcPath, destPath);
25
- } else {
26
- let content = fs.readFileSync(srcPath, "utf-8");
27
- content = content.replace(/__PROJECT_NAME__/g, projectName);
28
- fs.writeFileSync(destPath, content);
76
+ async function askProjectName() {
77
+ console.log(` ${c("bold", "Name your project:")}\n`);
78
+ while (true) {
79
+ const name = await ask(` ${c("cyan", "?>")} Project name (e.g. my-hotel-app): `);
80
+ if (name && name.trim()) {
81
+ return name.trim();
29
82
  }
83
+ console.log(` ${c("red", "✗")} Name cannot be empty\n`);
30
84
  }
31
85
  }
32
86
 
33
- copyRecursive(templateDir, targetDir);
34
- console.log(` Created project at: ${targetDir}\n`);
35
-
36
- try {
37
- console.log(" Installing backend dependencies...");
38
- execSync("npm install", { cwd: path.join(targetDir, "backend"), stdio: "inherit" });
39
- console.log(" Installing frontend dependencies...");
40
- execSync("npm install", { cwd: path.join(targetDir, "frontend"), stdio: "inherit" });
41
- console.log(" Installing root dependencies...");
42
- execSync("npm install", { cwd: targetDir, stdio: "inherit" });
43
- } catch {
44
- console.log("\n Run 'npm install' in each folder manually if install failed.");
87
+ function showProgress(msg) {
88
+ process.stdout.write(` ${c("yellow", "→")} ${msg}...`);
89
+ }
90
+
91
+ function done() {
92
+ console.log(` ${c("green", "")}`);
93
+ }
94
+
95
+ async function main() {
96
+ showBanner();
97
+
98
+ const projectType = await selectProjectType();
99
+ const projectName = await askProjectName();
100
+ const targetDir = path.resolve(process.cwd(), projectName);
101
+ const baseDir = path.join(__dirname, "templates", "base");
102
+ const typeDir = path.join(__dirname, "templates", "types", projectType.id);
103
+
104
+ if (fs.existsSync(targetDir)) {
105
+ console.log(`\n ${c("red", "✗")} Error: Directory "${projectName}" already exists.\n`);
106
+ rl.close();
107
+ process.exit(1);
108
+ }
109
+
110
+ console.log(`\n ${c("bold", "Creating project...")}\n ${c("dim", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`);
111
+
112
+ function copyRecursive(src, dest, type) {
113
+ if (!fs.existsSync(src)) return;
114
+ fs.mkdirSync(dest, { recursive: true });
115
+ const entries = fs.readdirSync(src, { withFileTypes: true });
116
+ for (const entry of entries) {
117
+ const srcPath = path.join(src, entry.name);
118
+ const destPath = path.join(dest, entry.name);
119
+ if (entry.isDirectory()) {
120
+ copyRecursive(srcPath, destPath, type);
121
+ } else {
122
+ let content = fs.readFileSync(srcPath, "utf-8");
123
+ content = content.replace(/__PROJECT_NAME__/g, projectName);
124
+ content = content.replace(/__PROJECT_TYPE__/g, type);
125
+ fs.writeFileSync(destPath, content);
126
+ }
127
+ }
128
+ }
129
+
130
+ showProgress("Scaffolding project files");
131
+ copyRecursive(baseDir, targetDir, projectType.id);
132
+ copyRecursive(typeDir, targetDir, projectType.id);
133
+ done();
134
+
135
+ showProgress("Installing backend dependencies");
136
+ try {
137
+ execSync("npm install", { cwd: path.join(targetDir, "backend"), stdio: "pipe" });
138
+ done();
139
+ } catch {
140
+ console.log(` ${c("red", "✗")}`);
141
+ }
142
+
143
+ showProgress("Installing frontend dependencies");
144
+ try {
145
+ execSync("npm install", { cwd: path.join(targetDir, "frontend"), stdio: "pipe" });
146
+ done();
147
+ } catch {
148
+ console.log(` ${c("red", "✗")}`);
149
+ }
150
+
151
+ showProgress("Installing root dependencies");
152
+ try {
153
+ execSync("npm install", { cwd: targetDir, stdio: "pipe" });
154
+ done();
155
+ } catch {
156
+ console.log(` ${c("red", "✗")}`);
157
+ }
158
+
159
+ console.log(`
160
+ ${c("bold", c("green", "✔ Project created successfully!"))}
161
+ ${c("dim", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
162
+
163
+ ${c("bold", "Next steps:")}
164
+
165
+ ${c("cyan", " 1.")} Start XAMPP (Apache + MySQL)
166
+ ${c("cyan", " 2.")} Open phpMyAdmin and create database: ${c("yellow", projectName)}
167
+ ${c("cyan", " 3.")} Run these commands:
168
+
169
+ ${c("bold", " cd " + projectName)}
170
+ ${c("bold", " npm run dev")}
171
+
172
+ ${c("cyan", " 4.")} Open ${c("green", "http://localhost:5173")}
173
+ ${c("cyan", " 5.")} Register a new account
174
+
175
+ ${c("dim", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}
176
+
177
+ ${c("bold", c("magenta", " 🦋 Goodluck with your " + projectType.name + " project! 🦋"))}
178
+
179
+ ${c("dim", "(Tables are auto-created on server start - no manual SQL needed)")}
180
+ ${c("dim", "Sample data is seeded automatically for you!")}
181
+
182
+ `);
183
+ rl.close();
45
184
  }
46
185
 
47
- console.log(`\n Done! Run these commands:\n`);
48
- console.log(` cd ${projectName}`);
49
- console.log(` npm run dev # starts both backend & frontend\n`);
186
+ main().catch((err) => {
187
+ console.error(err);
188
+ rl.close();
189
+ process.exit(1);
190
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-butterfly-app-v2",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "Scaffold a full-stack Node.js + React + Tailwind + MySQL project",
5
5
  "bin": {
6
6
  "create-butterfly-app-v2": "cli.js"
@@ -0,0 +1,14 @@
1
+ const db = require("../config/db");
2
+
3
+ module.exports = async function init() {
4
+ await db.query(`
5
+ CREATE TABLE IF NOT EXISTS users (
6
+ id INT AUTO_INCREMENT PRIMARY KEY,
7
+ name VARCHAR(100) NOT NULL,
8
+ email VARCHAR(100) NOT NULL UNIQUE,
9
+ password VARCHAR(255) DEFAULT '',
10
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
11
+ )
12
+ `);
13
+ console.log("Tables initialized");
14
+ };
@@ -0,0 +1,18 @@
1
+ const jwt = require("jsonwebtoken");
2
+
3
+ function authenticate(req, res, next) {
4
+ const header = req.headers.authorization;
5
+ if (!header || !header.startsWith("Bearer ")) {
6
+ return res.status(401).json({ error: "No token provided" });
7
+ }
8
+ try {
9
+ const token = header.split(" ")[1];
10
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
11
+ req.user = decoded;
12
+ next();
13
+ } catch {
14
+ res.status(401).json({ error: "Invalid token" });
15
+ }
16
+ }
17
+
18
+ module.exports = authenticate;
@@ -1,13 +1,15 @@
1
1
  {
2
- "name": "__PROJECT_NAME__-backend",
2
+ "name": "__PROJECT_TYPE__-backend",
3
3
  "private": true,
4
4
  "scripts": {
5
5
  "dev": "nodemon server.js"
6
6
  },
7
7
  "dependencies": {
8
+ "bcryptjs": "^2.4.3",
8
9
  "cors": "^2.8.5",
9
10
  "dotenv": "^16.4.7",
10
11
  "express": "^4.21.1",
12
+ "jsonwebtoken": "^9.0.2",
11
13
  "mysql2": "^3.11.5"
12
14
  },
13
15
  "devDependencies": {
@@ -0,0 +1,76 @@
1
+ const router = require("express").Router();
2
+ const bcrypt = require("bcryptjs");
3
+ const jwt = require("jsonwebtoken");
4
+ const db = require("../config/db");
5
+
6
+ router.post("/register", async (req, res) => {
7
+ const { name, email, password } = req.body;
8
+ if (!name || !email || !password) {
9
+ return res.status(400).json({ error: "All fields required" });
10
+ }
11
+ try {
12
+ const hashed = await bcrypt.hash(password, 10);
13
+ const [result] = await db.query(
14
+ "INSERT INTO users (name, email, password) VALUES (?, ?, ?)",
15
+ [name, email, hashed]
16
+ );
17
+ const token = jwt.sign(
18
+ { id: result.insertId, name, email },
19
+ process.env.JWT_SECRET,
20
+ { expiresIn: process.env.JWT_EXPIRES_IN || "7d" }
21
+ );
22
+ res.status(201).json({ token, user: { id: result.insertId, name, email } });
23
+ } catch (err) {
24
+ if (err.code === "ER_DUP_ENTRY") {
25
+ return res.status(409).json({ error: "Email already exists" });
26
+ }
27
+ res.status(500).json({ error: err.message });
28
+ }
29
+ });
30
+
31
+ router.post("/login", async (req, res) => {
32
+ const { email, password } = req.body;
33
+ if (!email || !password) {
34
+ return res.status(400).json({ error: "Email and password required" });
35
+ }
36
+ try {
37
+ const [rows] = await db.query("SELECT * FROM users WHERE email = ?", [email]);
38
+ if (rows.length === 0) {
39
+ return res.status(401).json({ error: "Invalid credentials" });
40
+ }
41
+ const user = rows[0];
42
+ const match = await bcrypt.compare(password, user.password);
43
+ if (!match) {
44
+ return res.status(401).json({ error: "Invalid credentials" });
45
+ }
46
+ const token = jwt.sign(
47
+ { id: user.id, name: user.name, email: user.email },
48
+ process.env.JWT_SECRET,
49
+ { expiresIn: process.env.JWT_EXPIRES_IN || "7d" }
50
+ );
51
+ res.json({ token, user: { id: user.id, name: user.name, email: user.email } });
52
+ } catch (err) {
53
+ res.status(500).json({ error: err.message });
54
+ }
55
+ });
56
+
57
+ router.get("/me", async (req, res) => {
58
+ const header = req.headers.authorization;
59
+ if (!header || !header.startsWith("Bearer ")) {
60
+ return res.status(401).json({ error: "No token" });
61
+ }
62
+ try {
63
+ const token = header.split(" ")[1];
64
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
65
+ const [rows] = await db.query(
66
+ "SELECT id, name, email FROM users WHERE id = ?",
67
+ [decoded.id]
68
+ );
69
+ if (rows.length === 0) return res.status(404).json({ error: "User not found" });
70
+ res.json(rows[0]);
71
+ } catch {
72
+ res.status(401).json({ error: "Invalid token" });
73
+ }
74
+ });
75
+
76
+ module.exports = router;
@@ -10,17 +10,22 @@ app.use(cors());
10
10
  app.use(express.json());
11
11
 
12
12
  app.get("/api/health", (req, res) => {
13
- res.json({ status: "ok", project: "__PROJECT_NAME__" });
13
+ res.json({ status: "ok", project: "__PROJECT_NAME__", type: "__PROJECT_TYPE__" });
14
14
  });
15
15
 
16
+ app.use("/api/auth", require("./routes/auth"));
16
17
  app.use("/api", require("./routes/api"));
17
18
 
18
- db.query("SELECT 1")
19
- .then(() => {
19
+ async function start() {
20
+ try {
21
+ await db.query("SELECT 1");
20
22
  console.log("MySQL connected");
23
+ await require("./db/init")();
21
24
  app.listen(PORT, () => console.log(`Backend on http://localhost:${PORT}`));
22
- })
23
- .catch((err) => {
24
- console.error("MySQL connection failed:", err.message);
25
+ } catch (err) {
26
+ console.error("Startup failed:", err.message);
25
27
  process.exit(1);
26
- });
28
+ }
29
+ }
30
+
31
+ start();
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "__PROJECT_NAME__-frontend",
2
+ "name": "__PROJECT_TYPE__-frontend",
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "react": "^19.0.0",
12
- "react-dom": "^19.0.0"
12
+ "react-dom": "^19.0.0",
13
+ "react-router-dom": "^7.1.1"
13
14
  },
14
15
  "devDependencies": {
15
16
  "@vitejs/plugin-react": "^4.3.4",
@@ -0,0 +1,32 @@
1
+ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
2
+ import { AuthProvider, useAuth } from "./context/AuthContext";
3
+ import Login from "./pages/Login";
4
+ import Register from "./pages/Register";
5
+ import Dashboard from "./pages/Dashboard";
6
+
7
+ function Protected({ children }) {
8
+ const { user, loading } = useAuth();
9
+ if (loading) return <div className="min-h-screen flex items-center justify-center text-gray-500">Loading...</div>;
10
+ return user ? children : <Navigate to="/login" />;
11
+ }
12
+
13
+ function Guest({ children }) {
14
+ const { user, loading } = useAuth();
15
+ if (loading) return <div className="min-h-screen flex items-center justify-center text-gray-500">Loading...</div>;
16
+ return user ? <Navigate to="/dashboard" /> : children;
17
+ }
18
+
19
+ export default function App() {
20
+ return (
21
+ <BrowserRouter>
22
+ <AuthProvider>
23
+ <Routes>
24
+ <Route path="/login" element={<Guest><Login /></Guest>} />
25
+ <Route path="/register" element={<Guest><Register /></Guest>} />
26
+ <Route path="/dashboard" element={<Protected><Dashboard /></Protected>} />
27
+ <Route path="*" element={<Navigate to="/login" />} />
28
+ </Routes>
29
+ </AuthProvider>
30
+ </BrowserRouter>
31
+ );
32
+ }
@@ -0,0 +1,33 @@
1
+ import { createContext, useContext, useState, useEffect } from "react";
2
+
3
+ const AuthContext = createContext(null);
4
+
5
+ export function AuthProvider({ children }) {
6
+ const [user, setUser] = useState(null);
7
+ const [token, setToken] = useState(localStorage.getItem("token"));
8
+ const [loading, setLoading] = useState(true);
9
+
10
+ useEffect(() => {
11
+ if (!token) { setLoading(false); return; }
12
+ fetch("/api/auth/me", {
13
+ headers: { Authorization: `Bearer ${token}` },
14
+ })
15
+ .then((r) => (r.ok ? r.json() : null))
16
+ .then((u) => {
17
+ if (u) setUser(u);
18
+ else { setToken(null); localStorage.removeItem("token"); }
19
+ })
20
+ .finally(() => setLoading(false));
21
+ }, [token]);
22
+
23
+ const login = (t, u) => { localStorage.setItem("token", t); setToken(t); setUser(u); };
24
+ const logout = () => { localStorage.removeItem("token"); setToken(null); setUser(null); };
25
+
26
+ return (
27
+ <AuthContext.Provider value={{ user, token, loading, login, logout }}>
28
+ {children}
29
+ </AuthContext.Provider>
30
+ );
31
+ }
32
+
33
+ export const useAuth = () => useContext(AuthContext);
@@ -0,0 +1,40 @@
1
+ import { useState } from "react";
2
+ import { useNavigate, Link } from "react-router-dom";
3
+ import { useAuth } from "../context/AuthContext";
4
+
5
+ export default function Login() {
6
+ const [email, setEmail] = useState("");
7
+ const [password, setPassword] = useState("");
8
+ const [error, setError] = useState("");
9
+ const { login } = useAuth();
10
+ const navigate = useNavigate();
11
+
12
+ const handleSubmit = async (e) => {
13
+ e.preventDefault();
14
+ setError("");
15
+ const res = await fetch("/api/auth/login", {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify({ email, password }),
19
+ });
20
+ const data = await res.json();
21
+ if (!res.ok) return setError(data.error);
22
+ login(data.token, data.user);
23
+ navigate("/dashboard");
24
+ };
25
+
26
+ return (
27
+ <div className="min-h-screen flex items-center justify-center bg-gray-100">
28
+ <form onSubmit={handleSubmit} className="bg-white p-8 rounded shadow w-96">
29
+ <h1 className="text-2xl font-bold mb-6 text-center">Login</h1>
30
+ {error && <p className="text-red-500 mb-4 text-sm">{error}</p>}
31
+ <input className="border rounded w-full px-3 py-2 mb-4" placeholder="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
32
+ <input className="border rounded w-full px-3 py-2 mb-4" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
33
+ <button className="bg-blue-600 text-white rounded w-full py-2 hover:bg-blue-700">Login</button>
34
+ <p className="text-center text-sm mt-4">
35
+ No account? <Link to="/register" className="text-blue-600">Register</Link>
36
+ </p>
37
+ </form>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,42 @@
1
+ import { useState } from "react";
2
+ import { useNavigate, Link } from "react-router-dom";
3
+ import { useAuth } from "../context/AuthContext";
4
+
5
+ export default function Register() {
6
+ const [name, setName] = useState("");
7
+ const [email, setEmail] = useState("");
8
+ const [password, setPassword] = useState("");
9
+ const [error, setError] = useState("");
10
+ const { login } = useAuth();
11
+ const navigate = useNavigate();
12
+
13
+ const handleSubmit = async (e) => {
14
+ e.preventDefault();
15
+ setError("");
16
+ const res = await fetch("/api/auth/register", {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({ name, email, password }),
20
+ });
21
+ const data = await res.json();
22
+ if (!res.ok) return setError(data.error);
23
+ login(data.token, data.user);
24
+ navigate("/dashboard");
25
+ };
26
+
27
+ return (
28
+ <div className="min-h-screen flex items-center justify-center bg-gray-100">
29
+ <form onSubmit={handleSubmit} className="bg-white p-8 rounded shadow w-96">
30
+ <h1 className="text-2xl font-bold mb-6 text-center">Register</h1>
31
+ {error && <p className="text-red-500 mb-4 text-sm">{error}</p>}
32
+ <input className="border rounded w-full px-3 py-2 mb-4" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} required />
33
+ <input className="border rounded w-full px-3 py-2 mb-4" placeholder="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
34
+ <input className="border rounded w-full px-3 py-2 mb-4" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
35
+ <button className="bg-blue-600 text-white rounded w-full py-2 hover:bg-blue-700">Register</button>
36
+ <p className="text-center text-sm mt-4">
37
+ Already have an account? <Link to="/login" className="text-blue-600">Login</Link>
38
+ </p>
39
+ </form>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,44 @@
1
+ const db = require("../config/db");
2
+
3
+ module.exports = async function init() {
4
+ await db.query(`
5
+ CREATE TABLE IF NOT EXISTS users (
6
+ id INT AUTO_INCREMENT PRIMARY KEY,
7
+ name VARCHAR(100) NOT NULL,
8
+ email VARCHAR(100) NOT NULL UNIQUE,
9
+ password VARCHAR(255) DEFAULT '',
10
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
11
+ )
12
+ `);
13
+ await db.query(`
14
+ CREATE TABLE IF NOT EXISTS rooms (
15
+ id INT AUTO_INCREMENT PRIMARY KEY,
16
+ number VARCHAR(10) NOT NULL UNIQUE,
17
+ type VARCHAR(50) NOT NULL DEFAULT 'standard',
18
+ price DECIMAL(10,2) NOT NULL DEFAULT 0,
19
+ capacity INT NOT NULL DEFAULT 2,
20
+ status VARCHAR(20) NOT NULL DEFAULT 'available',
21
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
22
+ )
23
+ `);
24
+ await db.query(`
25
+ CREATE TABLE IF NOT EXISTS reservations (
26
+ id INT AUTO_INCREMENT PRIMARY KEY,
27
+ guest_name VARCHAR(100) NOT NULL,
28
+ guest_email VARCHAR(100) NOT NULL,
29
+ guest_phone VARCHAR(20) DEFAULT '',
30
+ room_id INT NOT NULL,
31
+ check_in DATE NOT NULL,
32
+ check_out DATE NOT NULL,
33
+ status VARCHAR(20) NOT NULL DEFAULT 'confirmed',
34
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
35
+ FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
36
+ )
37
+ `);
38
+ const [rows] = await db.query("SELECT COUNT(*) as c FROM rooms");
39
+ if (rows[0].c === 0) {
40
+ await db.query("INSERT INTO rooms (number, type, price, capacity) VALUES ('101', 'standard', 80, 2), ('102', 'standard', 80, 2), ('201', 'deluxe', 150, 3), ('202', 'deluxe', 150, 3), ('301', 'suite', 250, 4)");
41
+ console.log("Sample rooms created");
42
+ }
43
+ console.log("Hotel tables initialized");
44
+ };