create-butterfly-app-v2 1.0.1 → 1.1.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 +17 -3
- package/package.json +1 -1
- package/templates/fullstack/backend/middleware/auth.js +18 -0
- package/templates/fullstack/backend/package.json +2 -0
- package/templates/fullstack/backend/routes/api.js +54 -6
- package/templates/fullstack/backend/routes/auth.js +76 -0
- package/templates/fullstack/backend/server.js +1 -0
- package/templates/fullstack/frontend/package.json +2 -1
- package/templates/fullstack/frontend/src/App.jsx +27 -79
- package/templates/fullstack/frontend/src/context/AuthContext.jsx +45 -0
- package/templates/fullstack/frontend/src/pages/Dashboard.jsx +110 -0
- package/templates/fullstack/frontend/src/pages/Login.jsx +40 -0
- package/templates/fullstack/frontend/src/pages/Register.jsx +42 -0
package/cli.js
CHANGED
|
@@ -44,6 +44,20 @@ try {
|
|
|
44
44
|
console.log("\n Run 'npm install' in each folder manually if install failed.");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
console.log(`\n
|
|
48
|
-
console.log(`
|
|
49
|
-
console.log(`
|
|
47
|
+
console.log(`\n Goodluck with your project! 🚀\n`);
|
|
48
|
+
console.log(` ─────────────────────────────`);
|
|
49
|
+
console.log(` cd ${projectName}`);
|
|
50
|
+
console.log(` npm run dev`);
|
|
51
|
+
console.log(` ─────────────────────────────\n`);
|
|
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`);
|
package/package.json
CHANGED
|
@@ -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,23 +1,71 @@
|
|
|
1
1
|
const router = require("express").Router();
|
|
2
2
|
const db = require("../config/db");
|
|
3
|
+
const authenticate = require("../middleware/auth");
|
|
3
4
|
|
|
4
|
-
router.get("/users", async (req, res) => {
|
|
5
|
+
router.get("/users", authenticate, async (req, res) => {
|
|
5
6
|
try {
|
|
6
|
-
const [rows] = await db.query("SELECT
|
|
7
|
+
const [rows] = await db.query("SELECT id, name, email, created_at FROM users");
|
|
7
8
|
res.json(rows);
|
|
8
9
|
} catch (err) {
|
|
9
10
|
res.status(500).json({ error: err.message });
|
|
10
11
|
}
|
|
11
12
|
});
|
|
12
13
|
|
|
13
|
-
router.
|
|
14
|
-
|
|
14
|
+
router.get("/users/:id", authenticate, async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const [rows] = await db.query(
|
|
17
|
+
"SELECT id, name, email, created_at FROM users WHERE id = ?",
|
|
18
|
+
[req.params.id]
|
|
19
|
+
);
|
|
20
|
+
if (rows.length === 0) return res.status(404).json({ error: "User not found" });
|
|
21
|
+
res.json(rows[0]);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
res.status(500).json({ error: err.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
router.post("/users", authenticate, async (req, res) => {
|
|
28
|
+
const { name, email, password } = req.body;
|
|
29
|
+
if (!name || !email) return res.status(400).json({ error: "Name and email required" });
|
|
15
30
|
try {
|
|
31
|
+
const bcrypt = require("bcryptjs");
|
|
32
|
+
const hashed = password ? await bcrypt.hash(password, 10) : "";
|
|
16
33
|
const [result] = await db.query(
|
|
17
|
-
"INSERT INTO users (name, email) VALUES (?, ?)",
|
|
18
|
-
[name, email]
|
|
34
|
+
"INSERT INTO users (name, email, password) VALUES (?, ?, ?)",
|
|
35
|
+
[name, email, hashed]
|
|
19
36
|
);
|
|
20
37
|
res.status(201).json({ id: result.insertId, name, email });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err.code === "ER_DUP_ENTRY") {
|
|
40
|
+
return res.status(409).json({ error: "Email already exists" });
|
|
41
|
+
}
|
|
42
|
+
res.status(500).json({ error: err.message });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
router.put("/users/:id", authenticate, async (req, res) => {
|
|
47
|
+
const { name, email } = req.body;
|
|
48
|
+
try {
|
|
49
|
+
const [result] = await db.query(
|
|
50
|
+
"UPDATE users SET name = ?, email = ? WHERE id = ?",
|
|
51
|
+
[name, email, req.params.id]
|
|
52
|
+
);
|
|
53
|
+
if (result.affectedRows === 0) {
|
|
54
|
+
return res.status(404).json({ error: "User not found" });
|
|
55
|
+
}
|
|
56
|
+
res.json({ id: parseInt(req.params.id), name, email });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
res.status(500).json({ error: err.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
router.delete("/users/:id", authenticate, async (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const [result] = await db.query("DELETE FROM users WHERE id = ?", [req.params.id]);
|
|
65
|
+
if (result.affectedRows === 0) {
|
|
66
|
+
return res.status(404).json({ error: "User not found" });
|
|
67
|
+
}
|
|
68
|
+
res.json({ message: "User deleted" });
|
|
21
69
|
} catch (err) {
|
|
22
70
|
res.status(500).json({ error: err.message });
|
|
23
71
|
}
|
|
@@ -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;
|
|
@@ -1,85 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.then((d) => setStatus(d.status))
|
|
13
|
-
.catch(() => setStatus("no connection"));
|
|
14
|
-
}, []);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
fetch("/api/users")
|
|
18
|
-
.then((r) => r.json())
|
|
19
|
-
.then(setUsers)
|
|
20
|
-
.catch(console.error);
|
|
21
|
-
}, []);
|
|
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
|
+
}
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
body: JSON.stringify({ name, email }),
|
|
29
|
-
});
|
|
30
|
-
if (res.ok) {
|
|
31
|
-
const user = await res.json();
|
|
32
|
-
setUsers((prev) => [...prev, user]);
|
|
33
|
-
setName("");
|
|
34
|
-
setEmail("");
|
|
35
|
-
}
|
|
36
|
-
};
|
|
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
|
+
}
|
|
37
18
|
|
|
19
|
+
function App() {
|
|
38
20
|
return (
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
required
|
|
50
|
-
/>
|
|
51
|
-
<input
|
|
52
|
-
className="border rounded px-3 py-2"
|
|
53
|
-
type="email"
|
|
54
|
-
placeholder="Email"
|
|
55
|
-
value={email}
|
|
56
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
57
|
-
required
|
|
58
|
-
/>
|
|
59
|
-
<button className="bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700">
|
|
60
|
-
Add User
|
|
61
|
-
</button>
|
|
62
|
-
</form>
|
|
63
|
-
|
|
64
|
-
<table className="w-full max-w-lg bg-white rounded shadow">
|
|
65
|
-
<thead>
|
|
66
|
-
<tr className="border-b bg-gray-50">
|
|
67
|
-
<th className="p-2 text-left">ID</th>
|
|
68
|
-
<th className="p-2 text-left">Name</th>
|
|
69
|
-
<th className="p-2 text-left">Email</th>
|
|
70
|
-
</tr>
|
|
71
|
-
</thead>
|
|
72
|
-
<tbody>
|
|
73
|
-
{users.map((u) => (
|
|
74
|
-
<tr key={u.id} className="border-b">
|
|
75
|
-
<td className="p-2">{u.id}</td>
|
|
76
|
-
<td className="p-2">{u.name}</td>
|
|
77
|
-
<td className="p-2">{u.email}</td>
|
|
78
|
-
</tr>
|
|
79
|
-
))}
|
|
80
|
-
</tbody>
|
|
81
|
-
</table>
|
|
82
|
-
</div>
|
|
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>
|
|
83
31
|
);
|
|
84
32
|
}
|
|
85
33
|
|
|
@@ -0,0 +1,45 @@
|
|
|
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) {
|
|
12
|
+
setLoading(false);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
fetch("/api/auth/me", {
|
|
16
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
17
|
+
})
|
|
18
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
19
|
+
.then((u) => {
|
|
20
|
+
if (u) setUser(u);
|
|
21
|
+
else { setToken(null); localStorage.removeItem("token"); }
|
|
22
|
+
})
|
|
23
|
+
.finally(() => setLoading(false));
|
|
24
|
+
}, [token]);
|
|
25
|
+
|
|
26
|
+
const login = (t, u) => {
|
|
27
|
+
localStorage.setItem("token", t);
|
|
28
|
+
setToken(t);
|
|
29
|
+
setUser(u);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const logout = () => {
|
|
33
|
+
localStorage.removeItem("token");
|
|
34
|
+
setToken(null);
|
|
35
|
+
setUser(null);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<AuthContext.Provider value={{ user, token, loading, login, logout }}>
|
|
40
|
+
{children}
|
|
41
|
+
</AuthContext.Provider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const useAuth = () => useContext(AuthContext);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useAuth } from "../context/AuthContext";
|
|
3
|
+
|
|
4
|
+
export default function Dashboard() {
|
|
5
|
+
const { user, logout } = useAuth();
|
|
6
|
+
const [users, setUsers] = useState([]);
|
|
7
|
+
const [name, setName] = useState("");
|
|
8
|
+
const [email, setEmail] = useState("");
|
|
9
|
+
const [editing, setEditing] = useState(null);
|
|
10
|
+
const [editName, setEditName] = useState("");
|
|
11
|
+
const [editEmail, setEditEmail] = useState("");
|
|
12
|
+
|
|
13
|
+
const token = localStorage.getItem("token");
|
|
14
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
|
|
15
|
+
|
|
16
|
+
const fetchUsers = () =>
|
|
17
|
+
fetch("/api/users", { headers })
|
|
18
|
+
.then((r) => r.json())
|
|
19
|
+
.then(setUsers)
|
|
20
|
+
.catch(console.error);
|
|
21
|
+
|
|
22
|
+
useEffect(() => { fetchUsers(); }, []);
|
|
23
|
+
|
|
24
|
+
const addUser = async (e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
const res = await fetch("/api/users", {
|
|
27
|
+
method: "POST", headers,
|
|
28
|
+
body: JSON.stringify({ name, email }),
|
|
29
|
+
});
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
setName(""); setEmail("");
|
|
32
|
+
fetchUsers();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const updateUser = async (id) => {
|
|
37
|
+
await fetch(`/api/users/${id}`, {
|
|
38
|
+
method: "PUT", headers,
|
|
39
|
+
body: JSON.stringify({ name: editName, email: editEmail }),
|
|
40
|
+
});
|
|
41
|
+
setEditing(null);
|
|
42
|
+
fetchUsers();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const deleteUser = async (id) => {
|
|
46
|
+
if (!confirm("Delete this user?")) return;
|
|
47
|
+
await fetch(`/api/users/${id}`, { method: "DELETE", headers });
|
|
48
|
+
fetchUsers();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="min-h-screen bg-gray-100">
|
|
53
|
+
<nav className="bg-white shadow px-6 py-3 flex justify-between items-center">
|
|
54
|
+
<h1 className="text-xl font-bold text-blue-600">__PROJECT_NAME__</h1>
|
|
55
|
+
<div className="flex items-center gap-4">
|
|
56
|
+
<span className="text-gray-600">{user?.name}</span>
|
|
57
|
+
<button onClick={logout} className="bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600">Logout</button>
|
|
58
|
+
</div>
|
|
59
|
+
</nav>
|
|
60
|
+
|
|
61
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
62
|
+
<form onSubmit={addUser} className="flex gap-2 mb-6">
|
|
63
|
+
<input className="border rounded px-3 py-2 flex-1" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} required />
|
|
64
|
+
<input className="border rounded px-3 py-2 flex-1" placeholder="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
|
65
|
+
<button className="bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700">Add</button>
|
|
66
|
+
</form>
|
|
67
|
+
|
|
68
|
+
<div className="bg-white rounded shadow overflow-hidden">
|
|
69
|
+
<table className="w-full">
|
|
70
|
+
<thead>
|
|
71
|
+
<tr className="border-b bg-gray-50">
|
|
72
|
+
<th className="p-3 text-left">ID</th>
|
|
73
|
+
<th className="p-3 text-left">Name</th>
|
|
74
|
+
<th className="p-3 text-left">Email</th>
|
|
75
|
+
<th className="p-3 text-left">Actions</th>
|
|
76
|
+
</tr>
|
|
77
|
+
</thead>
|
|
78
|
+
<tbody>
|
|
79
|
+
{users.map((u) => (
|
|
80
|
+
<tr key={u.id} className="border-b hover:bg-gray-50">
|
|
81
|
+
{editing === u.id ? (
|
|
82
|
+
<>
|
|
83
|
+
<td className="p-2">{u.id}</td>
|
|
84
|
+
<td className="p-2"><input className="border rounded px-2 py-1 w-full" value={editName} onChange={(e) => setEditName(e.target.value)} /></td>
|
|
85
|
+
<td className="p-2"><input className="border rounded px-2 py-1 w-full" value={editEmail} onChange={(e) => setEditEmail(e.target.value)} /></td>
|
|
86
|
+
<td className="p-2 flex gap-1">
|
|
87
|
+
<button onClick={() => updateUser(u.id)} className="bg-green-500 text-white px-2 py-1 rounded text-xs hover:bg-green-600">Save</button>
|
|
88
|
+
<button onClick={() => setEditing(null)} className="bg-gray-500 text-white px-2 py-1 rounded text-xs hover:bg-gray-600">Cancel</button>
|
|
89
|
+
</td>
|
|
90
|
+
</>
|
|
91
|
+
) : (
|
|
92
|
+
<>
|
|
93
|
+
<td className="p-3">{u.id}</td>
|
|
94
|
+
<td className="p-3">{u.name}</td>
|
|
95
|
+
<td className="p-3">{u.email}</td>
|
|
96
|
+
<td className="p-3 flex gap-1">
|
|
97
|
+
<button onClick={() => { setEditing(u.id); setEditName(u.name); setEditEmail(u.email); }} className="bg-yellow-500 text-white px-2 py-1 rounded text-xs hover:bg-yellow-600">Edit</button>
|
|
98
|
+
<button onClick={() => deleteUser(u.id)} className="bg-red-500 text-white px-2 py-1 rounded text-xs hover:bg-red-600">Delete</button>
|
|
99
|
+
</td>
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</tr>
|
|
103
|
+
))}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -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
|
+
}
|