Add navbar

Fix username not being passed from the api
This commit is contained in:
Nico 2025-11-22 17:05:21 -08:00
parent ffbaa2a8e0
commit 4501c47849
13 changed files with 429 additions and 89 deletions

View File

@ -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;

View File

@ -30,5 +30,5 @@ exports.login = async (req, res) => {
{ expiresIn: "1d" }
);
res.json({ token, role: user.role });
res.json({ token, username, role: user.role });
};

View 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");
};

View File

@ -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;
};

View 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;

View File

@ -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>

View File

@ -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;
};

View File

@ -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 });
};

View File

@ -0,0 +1,11 @@
import { Outlet } from "react-router-dom";
import Navbar from "./Navbar";
export default function AppLayout() {
return (
<div>
<Navbar />
<Outlet />
</div>
);
}

View File

@ -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>
)
);
}

View File

@ -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>
);
}

View 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;
}
}

View 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;
}