create-butterfly-app-v2 1.1.0 → 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.
- package/cli.js +176 -49
- package/package.json +1 -1
- package/templates/base/backend/db/init.js +14 -0
- package/templates/{fullstack → base}/backend/package.json +1 -1
- package/templates/{fullstack → base}/backend/server.js +11 -7
- package/templates/{fullstack → base}/frontend/package.json +1 -1
- package/templates/{fullstack → base}/frontend/src/App.jsx +1 -3
- package/templates/{fullstack → base}/frontend/src/context/AuthContext.jsx +3 -15
- package/templates/types/hotel/backend/db/init.js +44 -0
- package/templates/types/hotel/backend/routes/api.js +61 -0
- package/templates/types/hotel/frontend/src/pages/Dashboard.jsx +147 -0
- package/templates/types/parking/backend/db/init.js +45 -0
- package/templates/types/parking/backend/routes/api.js +66 -0
- package/templates/types/parking/frontend/src/pages/Dashboard.jsx +141 -0
- package/templates/types/student/backend/db/init.js +51 -0
- package/templates/types/student/backend/routes/api.js +80 -0
- package/templates/types/student/frontend/src/pages/Dashboard.jsx +181 -0
- package/templates/fullstack/backend/routes/api.js +0 -74
- package/templates/fullstack/frontend/src/pages/Dashboard.jsx +0 -110
- /package/templates/{fullstack → base}/backend/config/db.js +0 -0
- /package/templates/{fullstack → base}/backend/middleware/auth.js +0 -0
- /package/templates/{fullstack → base}/backend/routes/auth.js +0 -0
- /package/templates/{fullstack → base}/frontend/index.html +0 -0
- /package/templates/{fullstack → base}/frontend/postcss.config.js +0 -0
- /package/templates/{fullstack → base}/frontend/src/index.css +0 -0
- /package/templates/{fullstack → base}/frontend/src/main.jsx +0 -0
- /package/templates/{fullstack → base}/frontend/src/pages/Login.jsx +0 -0
- /package/templates/{fullstack → base}/frontend/src/pages/Register.jsx +0 -0
- /package/templates/{fullstack → base}/frontend/tailwind.config.js +0 -0
- /package/templates/{fullstack → base}/frontend/vite.config.js +0 -0
- /package/templates/{fullstack → base}/package.json +0 -0
package/cli.js
CHANGED
|
@@ -2,62 +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
|
|
7
|
-
const
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
console.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.log(` 1. Start XAMPP (Apache + MySQL)`);
|
|
53
|
-
console.log(` 2. Create database "${projectName}" in phpMyAdmin`);
|
|
54
|
-
console.log(` 3. Run this SQL to create tables:\n`);
|
|
55
|
-
console.log(` CREATE TABLE users (`);
|
|
56
|
-
console.log(` id INT AUTO_INCREMENT PRIMARY KEY,`);
|
|
57
|
-
console.log(` name VARCHAR(100) NOT NULL,`);
|
|
58
|
-
console.log(` email VARCHAR(100) NOT NULL UNIQUE,`);
|
|
59
|
-
console.log(` password VARCHAR(255) DEFAULT '',`);
|
|
60
|
-
console.log(` created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`);
|
|
61
|
-
console.log(` );\n`);
|
|
62
|
-
console.log(` 4. Open http://localhost:5173`);
|
|
63
|
-
console.log(` 5. Register a new account and start using the app!\n`);
|
|
186
|
+
main().catch((err) => {
|
|
187
|
+
console.error(err);
|
|
188
|
+
rl.close();
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
package/package.json
CHANGED
|
@@ -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
|
+
};
|
|
@@ -10,18 +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
16
|
app.use("/api/auth", require("./routes/auth"));
|
|
17
17
|
app.use("/api", require("./routes/api"));
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
async function start() {
|
|
20
|
+
try {
|
|
21
|
+
await db.query("SELECT 1");
|
|
21
22
|
console.log("MySQL connected");
|
|
23
|
+
await require("./db/init")();
|
|
22
24
|
app.listen(PORT, () => console.log(`Backend on http://localhost:${PORT}`));
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
console.error("MySQL connection failed:", err.message);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error("Startup failed:", err.message);
|
|
26
27
|
process.exit(1);
|
|
27
|
-
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
start();
|
|
@@ -16,7 +16,7 @@ function Guest({ children }) {
|
|
|
16
16
|
return user ? <Navigate to="/dashboard" /> : children;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function App() {
|
|
19
|
+
export default function App() {
|
|
20
20
|
return (
|
|
21
21
|
<BrowserRouter>
|
|
22
22
|
<AuthProvider>
|
|
@@ -30,5 +30,3 @@ function App() {
|
|
|
30
30
|
</BrowserRouter>
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
export default App;
|
|
@@ -8,10 +8,7 @@ export function AuthProvider({ children }) {
|
|
|
8
8
|
const [loading, setLoading] = useState(true);
|
|
9
9
|
|
|
10
10
|
useEffect(() => {
|
|
11
|
-
if (!token) {
|
|
12
|
-
setLoading(false);
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
11
|
+
if (!token) { setLoading(false); return; }
|
|
15
12
|
fetch("/api/auth/me", {
|
|
16
13
|
headers: { Authorization: `Bearer ${token}` },
|
|
17
14
|
})
|
|
@@ -23,17 +20,8 @@ export function AuthProvider({ children }) {
|
|
|
23
20
|
.finally(() => setLoading(false));
|
|
24
21
|
}, [token]);
|
|
25
22
|
|
|
26
|
-
const login = (t, u) => {
|
|
27
|
-
|
|
28
|
-
setToken(t);
|
|
29
|
-
setUser(u);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const logout = () => {
|
|
33
|
-
localStorage.removeItem("token");
|
|
34
|
-
setToken(null);
|
|
35
|
-
setUser(null);
|
|
36
|
-
};
|
|
23
|
+
const login = (t, u) => { localStorage.setItem("token", t); setToken(t); setUser(u); };
|
|
24
|
+
const logout = () => { localStorage.removeItem("token"); setToken(null); setUser(null); };
|
|
37
25
|
|
|
38
26
|
return (
|
|
39
27
|
<AuthContext.Provider value={{ user, token, loading, login, logout }}>
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const db = require("../config/db");
|
|
3
|
+
const authenticate = require("../middleware/auth");
|
|
4
|
+
|
|
5
|
+
router.get("/rooms", authenticate, async (req, res) => {
|
|
6
|
+
const [rows] = await db.query("SELECT * FROM rooms ORDER BY number");
|
|
7
|
+
res.json(rows);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
router.post("/rooms", authenticate, async (req, res) => {
|
|
11
|
+
const { number, type, price, capacity } = req.body;
|
|
12
|
+
try {
|
|
13
|
+
const [r] = await db.query("INSERT INTO rooms (number, type, price, capacity) VALUES (?, ?, ?, ?)", [number, type || "standard", price || 0, capacity || 2]);
|
|
14
|
+
res.status(201).json({ id: r.insertId, number, type: type || "standard", price: price || 0, capacity: capacity || 2, status: "available" });
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (err.code === "ER_DUP_ENTRY") return res.status(409).json({ error: "Room number exists" });
|
|
17
|
+
res.status(500).json({ error: err.message });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.put("/rooms/:id", authenticate, async (req, res) => {
|
|
22
|
+
const { number, type, price, capacity, status } = req.body;
|
|
23
|
+
await db.query("UPDATE rooms SET number=?, type=?, price=?, capacity=?, status=? WHERE id=?", [number, type, price, capacity, status, req.params.id]);
|
|
24
|
+
res.json({ id: parseInt(req.params.id), number, type, price, capacity, status });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
router.delete("/rooms/:id", authenticate, async (req, res) => {
|
|
28
|
+
await db.query("DELETE FROM rooms WHERE id=?", [req.params.id]);
|
|
29
|
+
res.json({ message: "Room deleted" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.get("/reservations", authenticate, async (req, res) => {
|
|
33
|
+
const [rows] = await db.query("SELECT r.*, rm.number as room_number, rm.type as room_type FROM reservations r JOIN rooms rm ON r.room_id = rm.id ORDER BY r.check_in DESC");
|
|
34
|
+
res.json(rows);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
router.post("/reservations", authenticate, async (req, res) => {
|
|
38
|
+
const { guest_name, guest_email, guest_phone, room_id, check_in, check_out } = req.body;
|
|
39
|
+
const [r] = await db.query("INSERT INTO reservations (guest_name, guest_email, guest_phone, room_id, check_in, check_out) VALUES (?, ?, ?, ?, ?, ?)", [guest_name, guest_email, guest_phone || "", room_id, check_in, check_out]);
|
|
40
|
+
await db.query("UPDATE rooms SET status='occupied' WHERE id=?", [room_id]);
|
|
41
|
+
res.status(201).json({ id: r.insertId, guest_name, guest_email, room_id, check_in, check_out, status: "confirmed" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
router.put("/reservations/:id", authenticate, async (req, res) => {
|
|
45
|
+
const { guest_name, guest_email, guest_phone, check_in, check_out, status } = req.body;
|
|
46
|
+
await db.query("UPDATE reservations SET guest_name=?, guest_email=?, guest_phone=?, check_in=?, check_out=?, status=? WHERE id=?", [guest_name, guest_email, guest_phone, check_in, check_out, status, req.params.id]);
|
|
47
|
+
if (status === "cancelled" || status === "checked_out") {
|
|
48
|
+
const [prev] = await db.query("SELECT room_id FROM reservations WHERE id=?", [req.params.id]);
|
|
49
|
+
if (prev.length) await db.query("UPDATE rooms SET status='available' WHERE id=?", [prev[0].room_id]);
|
|
50
|
+
}
|
|
51
|
+
res.json({ id: parseInt(req.params.id), guest_name, guest_email, check_in, check_out, status });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
router.delete("/reservations/:id", authenticate, async (req, res) => {
|
|
55
|
+
const [prev] = await db.query("SELECT room_id FROM reservations WHERE id=?", [req.params.id]);
|
|
56
|
+
await db.query("DELETE FROM reservations WHERE id=?", [req.params.id]);
|
|
57
|
+
if (prev.length) await db.query("UPDATE rooms SET status='available' WHERE id=?", [prev[0].room_id]);
|
|
58
|
+
res.json({ message: "Reservation deleted" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
module.exports = router;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useAuth } from "../../context/AuthContext";
|
|
3
|
+
|
|
4
|
+
const token = () => localStorage.getItem("token");
|
|
5
|
+
const headers = () => ({ "Content-Type": "application/json", Authorization: `Bearer ${token()}` });
|
|
6
|
+
|
|
7
|
+
export default function Dashboard() {
|
|
8
|
+
const { user, logout } = useAuth();
|
|
9
|
+
const [rooms, setRooms] = useState([]);
|
|
10
|
+
const [reservations, setReservations] = useState([]);
|
|
11
|
+
const [tab, setTab] = useState("rooms");
|
|
12
|
+
const [form, setForm] = useState({ number: "", type: "standard", price: "", capacity: "2" });
|
|
13
|
+
const [resForm, setResForm] = useState({ guest_name: "", guest_email: "", guest_phone: "", room_id: "", check_in: "", check_out: "" });
|
|
14
|
+
const [editId, setEditId] = useState(null);
|
|
15
|
+
|
|
16
|
+
const fetchRooms = () => fetch("/api/rooms", { headers: headers() }).then(r => r.json()).then(setRooms);
|
|
17
|
+
const fetchReservations = () => fetch("/api/reservations", { headers: headers() }).then(r => r.json()).then(setReservations);
|
|
18
|
+
|
|
19
|
+
useEffect(() => { fetchRooms(); fetchReservations(); }, []);
|
|
20
|
+
|
|
21
|
+
const handleRoom = async (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
const method = editId ? "PUT" : "POST";
|
|
24
|
+
const url = editId ? `/api/rooms/${editId}` : "/api/rooms";
|
|
25
|
+
await fetch(url, { method, headers: headers(), body: JSON.stringify(form) });
|
|
26
|
+
setForm({ number: "", type: "standard", price: "", capacity: "2" });
|
|
27
|
+
setEditId(null);
|
|
28
|
+
fetchRooms();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const deleteRoom = async (id) => {
|
|
32
|
+
if (!confirm("Delete this room?")) return;
|
|
33
|
+
await fetch(`/api/rooms/${id}`, { method: "DELETE", headers: headers() });
|
|
34
|
+
fetchRooms();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const editRoom = (r) => { setForm({ number: r.number, type: r.type, price: r.price.toString(), capacity: r.capacity.toString() }); setEditId(r.id); setTab("rooms"); };
|
|
38
|
+
|
|
39
|
+
const handleReservation = async (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
await fetch("/api/reservations", { method: "POST", headers: headers(), body: JSON.stringify(resForm) });
|
|
42
|
+
setResForm({ guest_name: "", guest_email: "", guest_phone: "", room_id: "", check_in: "", check_out: "" });
|
|
43
|
+
fetchReservations(); fetchRooms();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const cancelReservation = async (id) => {
|
|
47
|
+
if (!confirm("Cancel this reservation?")) return;
|
|
48
|
+
await fetch(`/api/reservations/${id}`, { method: "PUT", headers: headers(), body: JSON.stringify({ status: "cancelled" }) });
|
|
49
|
+
fetchReservations(); fetchRooms();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const statusColor = (s) => ({ available: "text-green-600 bg-green-100", occupied: "text-red-600 bg-red-100", maintenance: "text-yellow-600 bg-yellow-100" }[s] || "");
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="min-h-screen bg-gray-100">
|
|
56
|
+
<nav className="bg-white shadow px-6 py-3 flex justify-between items-center">
|
|
57
|
+
<h1 className="text-xl font-bold text-blue-600">Hotel Reservation</h1>
|
|
58
|
+
<div className="flex items-center gap-4">
|
|
59
|
+
<span className="text-gray-600">{user?.name}</span>
|
|
60
|
+
<button onClick={logout} className="bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600">Logout</button>
|
|
61
|
+
</div>
|
|
62
|
+
</nav>
|
|
63
|
+
|
|
64
|
+
<div className="max-w-6xl mx-auto p-6">
|
|
65
|
+
<div className="flex gap-2 mb-6">
|
|
66
|
+
<button onClick={() => setTab("rooms")} className={`px-4 py-2 rounded ${tab === "rooms" ? "bg-blue-600 text-white" : "bg-white"}`}>Rooms</button>
|
|
67
|
+
<button onClick={() => setTab("reservations")} className={`px-4 py-2 rounded ${tab === "reservations" ? "bg-blue-600 text-white" : "bg-white"}`}>Reservations</button>
|
|
68
|
+
<button onClick={() => setTab("add")} className={`px-4 py-2 rounded ${tab === "add" ? "bg-blue-600 text-white" : "bg-white"}`}>+ New Room</button>
|
|
69
|
+
<button onClick={() => setTab("book")} className={`px-4 py-2 rounded ${tab === "book" ? "bg-blue-600 text-white" : "bg-white"}`}>+ Book Room</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{tab === "rooms" && (
|
|
73
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
74
|
+
{rooms.map(r => (
|
|
75
|
+
<div key={r.id} className="bg-white rounded shadow p-4">
|
|
76
|
+
<div className="flex justify-between items-start">
|
|
77
|
+
<h3 className="text-lg font-bold">Room {r.number}</h3>
|
|
78
|
+
<span className={`px-2 py-1 rounded text-xs font-semibold ${statusColor(r.status)}`}>{r.status}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<p className="text-gray-500 text-sm">{r.type} - {r.capacity} guests</p>
|
|
81
|
+
<p className="text-xl font-bold text-blue-600 mt-2">${r.price}<span className="text-sm text-gray-500">/night</span></p>
|
|
82
|
+
<div className="flex gap-2 mt-3">
|
|
83
|
+
<button onClick={() => editRoom(r)} className="bg-yellow-500 text-white px-3 py-1 rounded text-xs hover:bg-yellow-600">Edit</button>
|
|
84
|
+
<button onClick={() => deleteRoom(r.id)} className="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">Delete</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{tab === "reservations" && (
|
|
92
|
+
<div className="bg-white rounded shadow overflow-hidden">
|
|
93
|
+
<table className="w-full">
|
|
94
|
+
<thead><tr className="border-b bg-gray-50"><th className="p-3 text-left">Guest</th><th className="p-3 text-left">Room</th><th className="p-3 text-left">Check In</th><th className="p-3 text-left">Check Out</th><th className="p-3 text-left">Status</th><th className="p-3 text-left">Action</th></tr></thead>
|
|
95
|
+
<tbody>
|
|
96
|
+
{reservations.map(r => (
|
|
97
|
+
<tr key={r.id} className="border-b hover:bg-gray-50">
|
|
98
|
+
<td className="p-3">{r.guest_name}<br /><span className="text-xs text-gray-500">{r.guest_email}</span></td>
|
|
99
|
+
<td className="p-3">{r.room_number} ({r.room_type})</td>
|
|
100
|
+
<td className="p-3">{new Date(r.check_in).toLocaleDateString()}</td>
|
|
101
|
+
<td className="p-3">{new Date(r.check_out).toLocaleDateString()}</td>
|
|
102
|
+
<td className="p-3"><span className={`px-2 py-1 rounded text-xs font-semibold ${statusColor(r.status) || "text-blue-600 bg-blue-100"}`}>{r.status}</span></td>
|
|
103
|
+
<td className="p-3">{r.status === "confirmed" && <button onClick={() => cancelReservation(r.id)} className="bg-red-500 text-white px-2 py-1 rounded text-xs hover:bg-red-600">Cancel</button>}</td>
|
|
104
|
+
</tr>
|
|
105
|
+
))}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{tab === "add" && (
|
|
112
|
+
<form onSubmit={handleRoom} className="bg-white p-6 rounded shadow max-w-lg">
|
|
113
|
+
<h2 className="text-lg font-bold mb-4">{editId ? "Edit Room" : "Add Room"}</h2>
|
|
114
|
+
<div className="grid grid-cols-2 gap-4">
|
|
115
|
+
<input className="border rounded px-3 py-2" placeholder="Room Number" value={form.number} onChange={e => setForm({ ...form, number: e.target.value })} required />
|
|
116
|
+
<select className="border rounded px-3 py-2" value={form.type} onChange={e => setForm({ ...form, type: e.target.value })}>
|
|
117
|
+
<option value="standard">Standard</option><option value="deluxe">Deluxe</option><option value="suite">Suite</option>
|
|
118
|
+
</select>
|
|
119
|
+
<input className="border rounded px-3 py-2" type="number" placeholder="Price/night" value={form.price} onChange={e => setForm({ ...form, price: e.target.value })} required />
|
|
120
|
+
<input className="border rounded px-3 py-2" type="number" placeholder="Capacity" value={form.capacity} onChange={e => setForm({ ...form, capacity: e.target.value })} required />
|
|
121
|
+
</div>
|
|
122
|
+
<button className="bg-blue-600 text-white rounded px-4 py-2 mt-4 hover:bg-blue-700">{editId ? "Update" : "Add"} Room</button>
|
|
123
|
+
{editId && <button type="button" onClick={() => { setEditId(null); setForm({ number: "", type: "standard", price: "", capacity: "2" }); }} className="ml-2 bg-gray-500 text-white rounded px-4 py-2 mt-4 hover:bg-gray-600">Cancel</button>}
|
|
124
|
+
</form>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{tab === "book" && (
|
|
128
|
+
<form onSubmit={handleReservation} className="bg-white p-6 rounded shadow max-w-lg">
|
|
129
|
+
<h2 className="text-lg font-bold mb-4">Book a Room</h2>
|
|
130
|
+
<div className="grid grid-cols-2 gap-4">
|
|
131
|
+
<input className="border rounded px-3 py-2 col-span-2" placeholder="Guest Name" value={resForm.guest_name} onChange={e => setResForm({ ...resForm, guest_name: e.target.value })} required />
|
|
132
|
+
<input className="border rounded px-3 py-2" type="email" placeholder="Guest Email" value={resForm.guest_email} onChange={e => setResForm({ ...resForm, guest_email: e.target.value })} required />
|
|
133
|
+
<input className="border rounded px-3 py-2" placeholder="Phone" value={resForm.guest_phone} onChange={e => setResForm({ ...resForm, guest_phone: e.target.value })} />
|
|
134
|
+
<select className="border rounded px-3 py-2" value={resForm.room_id} onChange={e => setResForm({ ...resForm, room_id: e.target.value })} required>
|
|
135
|
+
<option value="">Select Room</option>
|
|
136
|
+
{rooms.filter(r => r.status === "available").map(r => <option key={r.id} value={r.id}>Room {r.number} - ${r.price}/night</option>)}
|
|
137
|
+
</select>
|
|
138
|
+
<input className="border rounded px-3 py-2" type="date" placeholder="Check In" value={resForm.check_in} onChange={e => setResForm({ ...resForm, check_in: e.target.value })} required />
|
|
139
|
+
<input className="border rounded px-3 py-2" type="date" placeholder="Check Out" value={resForm.check_out} onChange={e => setResForm({ ...resForm, check_out: e.target.value })} required />
|
|
140
|
+
</div>
|
|
141
|
+
<button className="bg-green-600 text-white rounded px-4 py-2 mt-4 hover:bg-green-700">Book Now</button>
|
|
142
|
+
</form>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|