Add navbar
Fix username not being passed from the api
This commit is contained in:
parent
ffbaa2a8e0
commit
4501c47849
@ -44,5 +44,8 @@ app.use("/admin", adminRoutes);
|
||||
const usersRoutes = require("./routes/users.routes");
|
||||
app.use("/users", usersRoutes);
|
||||
|
||||
const suggestController = require("./routes/suggest.routes");
|
||||
app.get("/suggest", suggestController);
|
||||
|
||||
|
||||
module.exports = app;
|
||||
@ -30,5 +30,5 @@ exports.login = async (req, res) => {
|
||||
{ expiresIn: "1d" }
|
||||
);
|
||||
|
||||
res.json({ token, role: user.role });
|
||||
res.json({ token, username, role: user.role });
|
||||
};
|
||||
|
||||
9
backend/controllers/suggest.controller.js
Normal file
9
backend/controllers/suggest.controller.js
Normal file
@ -0,0 +1,9 @@
|
||||
const List = require("../models/list.model");
|
||||
|
||||
|
||||
exports.getHistory = async (req, res) => {
|
||||
console.log("GET /suggest called");
|
||||
const { query } = req.query;
|
||||
const items = await List.getHistory(query);
|
||||
res.json("asdf");
|
||||
};
|
||||
@ -44,3 +44,17 @@ exports.addHistoryRecord = async (itemId, quantity) => {
|
||||
);
|
||||
};
|
||||
|
||||
exports.getHistory = async (query) => {
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT item_name
|
||||
FROM grocery_list
|
||||
WHERE item_name ILIKE $1
|
||||
LIMIT 10`,
|
||||
[`%${query}%`]
|
||||
);
|
||||
console.log("QUERY:");
|
||||
console.log(result.query);
|
||||
return result.rows;
|
||||
|
||||
};
|
||||
|
||||
|
||||
9
backend/routes/suggest.routes.js
Normal file
9
backend/routes/suggest.routes.js
Normal file
@ -0,0 +1,9 @@
|
||||
const router = require("express").Router();
|
||||
const controller = require("../controllers/suggest.controller");
|
||||
const auth = require("../middleware/auth");
|
||||
const requireRole = require("../middleware/rbac");
|
||||
const { ROLES } = require("../models/user.model");
|
||||
|
||||
router.get("/", auth, requireRole(ROLES.VIEWER, ROLES.EDITOR, ROLES.ADMIN), controller.getHistory);
|
||||
|
||||
module.exports = router;
|
||||
@ -6,31 +6,41 @@ import AdminPanel from "./pages/AdminPanel.jsx";
|
||||
import GroceryList from "./pages/GroceryList.jsx";
|
||||
import Login from "./pages/Login.jsx";
|
||||
|
||||
import AppLayout from "./components/AppLayout.jsx";
|
||||
import PrivateRoute from "./utils/PrivateRoute.jsx";
|
||||
|
||||
import RoleGuard from "./utils/RoleGuard.jsx";
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
{/* Public route */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* Private routes with layout */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<GroceryList />
|
||||
<AppLayout />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||
<AdminPanel />
|
||||
</RoleGuard>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Route path="/" element={<GroceryList />} />
|
||||
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||
<AdminPanel />
|
||||
</RoleGuard>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
|
||||
@ -2,6 +2,7 @@ import api from "./axios";
|
||||
|
||||
export const loginRequest = async (username, password) => {
|
||||
const res = await api.post("/auth/login", { username, password });
|
||||
alert(`Response data: ${JSON.stringify(res.data)}`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
|
||||
@ -3,3 +3,7 @@ import api from "./axios";
|
||||
export const getList = () => api.get("/list");
|
||||
export const addItem = (itemName, quantiy) => api.post("/list/add", { itemName, quantiy });
|
||||
export const markBought = (id) => api.post("/list/mark-bought", { id });
|
||||
export const suggest = (query) => {
|
||||
console.log("API SUGGEST QUERY:", query);
|
||||
api.get("/suggest", { query });
|
||||
};
|
||||
11
frontend/src/components/AppLayout.jsx
Normal file
11
frontend/src/components/AppLayout.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function AppLayout() {
|
||||
return (
|
||||
<div>
|
||||
<Navbar />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,16 +1,30 @@
|
||||
import "../styles/Navbar.css";
|
||||
|
||||
import { useContext } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ROLES } from "../constants/roles";
|
||||
import { AuthContext } from "../context/AuthContext";
|
||||
|
||||
export default function Navbar() {
|
||||
const { role, logout } = useContext(AuthContext);
|
||||
const { role, logout, username } = useContext(AuthContext);
|
||||
|
||||
return (
|
||||
<nav>
|
||||
<Link to="/">List</Link>
|
||||
{role === ROLES.ADMIN && <Link to="/admin">Admin Panel</Link>}
|
||||
<button onClick={logout}>Logout</button>
|
||||
<nav className="navbar">
|
||||
<div className="navbar-links">
|
||||
<Link to="/">Home</Link>
|
||||
|
||||
{role === "admin" && <Link to="/admin">Admin</Link>}
|
||||
</div>
|
||||
|
||||
<div className="navbar-idcard">
|
||||
<div className="navbar-idinfo">
|
||||
<span className="navbar-username">{username}</span>
|
||||
<span className="navbar-role">{role}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="navbar-logout" onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</nav>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -1,103 +1,176 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { addItem, getList, markBought } from "../api/list";
|
||||
import { addItem, getList, markBought, suggest } from "../api/list";
|
||||
import { AuthContext } from "../context/AuthContext";
|
||||
import "../styles/GroceryList.css";
|
||||
|
||||
export default function GroceryList() {
|
||||
const { role, username } = useContext(AuthContext);
|
||||
|
||||
const [items, setItems] = useState([]);
|
||||
const [sortedItems, setSortedItems] = useState([]);
|
||||
|
||||
const [sortMode, setSortMode] = useState("az");
|
||||
|
||||
const [itemName, setItemName] = useState("");
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
|
||||
const [showAddForm, setShowAddForm] = useState(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Load list
|
||||
const loadItems = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getList();
|
||||
setItems(response.data);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError("Failed to load grocery list");
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await getList();
|
||||
setItems(res.data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadItems();
|
||||
}, []);
|
||||
|
||||
// Add item (editor/admin)
|
||||
const handleAdd = async (e) => {
|
||||
e.preventDefault();
|
||||
useEffect(() => {
|
||||
let sorted = [...items];
|
||||
|
||||
if (sortMode === "az")
|
||||
sorted.sort((a, b) => a.item_name.localeCompare(b.item_name));
|
||||
|
||||
if (sortMode === "za")
|
||||
sorted.sort((a, b) => b.item_name.localeCompare(a.item_name));
|
||||
|
||||
if (sortMode === "qty-high")
|
||||
sorted.sort((a, b) => b.quantity - a.quantity);
|
||||
|
||||
if (sortMode === "qty-low")
|
||||
sorted.sort((a, b) => a.quantity - b.quantity);
|
||||
|
||||
setSortedItems(sorted);
|
||||
}, [items, sortMode]);
|
||||
|
||||
const handleSuggest = async (text) => {
|
||||
setItemName(text);
|
||||
|
||||
if (!text.trim()) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await addItem(itemName, quantity);
|
||||
setItemName("");
|
||||
setQuantity(1);
|
||||
loadItems();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setError("Failed to add item");
|
||||
setSuggestions(suggest(text).data.map((i) => i.item_name));
|
||||
} catch {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Mark bought (editor/admin)
|
||||
const handleAdd = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!itemName.trim()) return;
|
||||
|
||||
await addItem(itemName, quantity);
|
||||
|
||||
setItemName("");
|
||||
setQuantity(1);
|
||||
setSuggestions([]);
|
||||
|
||||
loadItems();
|
||||
};
|
||||
|
||||
const handleBought = async (id) => {
|
||||
try {
|
||||
await markBought(id);
|
||||
loadItems();
|
||||
} catch (err) {
|
||||
setError("Failed to mark item as bought");
|
||||
}
|
||||
const yes = window.confirm("Mark this item as bought?");
|
||||
if (!yes) return;
|
||||
|
||||
await markBought(id);
|
||||
loadItems();
|
||||
};
|
||||
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p style={{ color: "red" }}>{error}</p>;
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px" }}>
|
||||
<h1>Grocery List</h1>
|
||||
<p>Logged in as: <strong>{username}</strong> ({role})</p>
|
||||
<div className="glist-body">
|
||||
<div className="glist-container">
|
||||
|
||||
{/* Add Item Section (editor/admin only) */}
|
||||
{(role === "editor" || role === "admin") && (
|
||||
<form onSubmit={handleAdd} style={{ margin: "20px 0" }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Item name"
|
||||
value={itemName}
|
||||
onChange={(e) => setItemName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(e.target.value)}
|
||||
style={{ width: "70px", marginLeft: "10px" }}
|
||||
/>
|
||||
<button style={{ marginLeft: "10px" }}>Add</button>
|
||||
</form>
|
||||
)}
|
||||
<h1 className="glist-title">Costco Grocery List - {username}[{role}]</h1>
|
||||
<p><strong>{username}</strong> ({role})</p>
|
||||
|
||||
{/* Grocery List */}
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<li key={item.id} style={{ margin: "10px 0" }}>
|
||||
{item.item_name} — {item.quantity}
|
||||
{/* Sorting dropdown */}
|
||||
<select
|
||||
value={sortMode}
|
||||
onChange={(e) => setSortMode(e.target.value)}
|
||||
className="glist-sort"
|
||||
>
|
||||
<option value="az">A → Z</option>
|
||||
<option value="za">Z → A</option>
|
||||
<option value="qty-high">Quantity: High → Low</option>
|
||||
<option value="qty-low">Quantity: Low → High</option>
|
||||
</select>
|
||||
|
||||
{(role === "editor" || role === "admin") && (
|
||||
<button
|
||||
onClick={() => handleBought(item.id)}
|
||||
style={{ marginLeft: "15px" }}
|
||||
>
|
||||
Mark Bought
|
||||
</button>
|
||||
{/* Add Item form (editor/admin only) */}
|
||||
{(role === "editor" || role === "admin") && showAddForm && (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className="glist-input"
|
||||
placeholder="Item name"
|
||||
value={itemName}
|
||||
onChange={(e) => handleSuggest(e.target.value)}
|
||||
/>
|
||||
|
||||
{suggestions.length > 0 && (
|
||||
<ul className="glist-suggest-box">
|
||||
{suggestions.map((s, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="glist-suggest-item"
|
||||
onClick={() => {
|
||||
setItemName(s);
|
||||
setSuggestions([]);
|
||||
}}
|
||||
>
|
||||
{s}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className="glist-input"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Number(e.target.value))}
|
||||
/>
|
||||
|
||||
<button className="glist-btn" onClick={handleAdd}>
|
||||
Add Item
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Grocery list */}
|
||||
<ul className="glist-ul">
|
||||
{sortedItems.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="glist-li"
|
||||
onClick={() =>
|
||||
(role === "editor" || role === "admin") && handleBought(item.id)
|
||||
}
|
||||
>
|
||||
{item.item_name} ({item.quantity})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Floating Button (editor/admin only) */}
|
||||
{(role === "editor" || role === "admin") && (
|
||||
<button
|
||||
className="glist-fab"
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
>
|
||||
{showAddForm ? "−" : "+"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
134
frontend/src/styles/GroceryList.css
Normal file
134
frontend/src/styles/GroceryList.css
Normal file
@ -0,0 +1,134 @@
|
||||
/* Container */
|
||||
.glist-body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 1em;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.glist-container {
|
||||
max-width: 480px;
|
||||
margin: auto;
|
||||
background: white;
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.glist-title {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
.glist-input {
|
||||
font-size: 1em;
|
||||
padding: 0.5em;
|
||||
margin: 0.3em 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.glist-btn {
|
||||
font-size: 1em;
|
||||
padding: 0.55em;
|
||||
width: 100%;
|
||||
margin-top: 0.4em;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.glist-btn:hover {
|
||||
background: #0067d8;
|
||||
}
|
||||
|
||||
/* Suggestion dropdown */
|
||||
.glist-suggest-box {
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
width: calc(100% - 2em);
|
||||
left: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.glist-suggest-item {
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.glist-suggest-item:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/* Grocery list items */
|
||||
.glist-ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.glist-li {
|
||||
padding: 0.7em;
|
||||
background: #e9ecef;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 0.6em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.glist-li:hover {
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
/* Sorting dropdown */
|
||||
.glist-sort {
|
||||
width: 100%;
|
||||
margin: 0.3em 0;
|
||||
padding: 0.5em;
|
||||
font-size: 1em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Floating Action Button (FAB) */
|
||||
.glist-fab {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
font-size: 2em;
|
||||
line-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.glist-fab:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
/* Mobile tweaks */
|
||||
@media (max-width: 480px) {
|
||||
.glist-container {
|
||||
padding: 1em 0.8em;
|
||||
}
|
||||
|
||||
.glist-fab {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
58
frontend/src/styles/Navbar.css
Normal file
58
frontend/src/styles/Navbar.css
Normal file
@ -0,0 +1,58 @@
|
||||
.navbar {
|
||||
background: #343a40;
|
||||
color: white;
|
||||
padding: 0.6em 1em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
color: white;
|
||||
margin-right: 1em;
|
||||
text-decoration: none;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.navbar-logout {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.navbar-idcard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
margin-right: 1em;
|
||||
padding: 0.3em 0.6em;
|
||||
background: #495057;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar-idinfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.navbar-username {
|
||||
font-size: 0.95em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-role {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user