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");
|
const usersRoutes = require("./routes/users.routes");
|
||||||
app.use("/users", usersRoutes);
|
app.use("/users", usersRoutes);
|
||||||
|
|
||||||
|
const suggestController = require("./routes/suggest.routes");
|
||||||
|
app.get("/suggest", suggestController);
|
||||||
|
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
@ -30,5 +30,5 @@ exports.login = async (req, res) => {
|
|||||||
{ expiresIn: "1d" }
|
{ 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 GroceryList from "./pages/GroceryList.jsx";
|
||||||
import Login from "./pages/Login.jsx";
|
import Login from "./pages/Login.jsx";
|
||||||
|
|
||||||
|
import AppLayout from "./components/AppLayout.jsx";
|
||||||
import PrivateRoute from "./utils/PrivateRoute.jsx";
|
import PrivateRoute from "./utils/PrivateRoute.jsx";
|
||||||
|
|
||||||
import RoleGuard from "./utils/RoleGuard.jsx";
|
import RoleGuard from "./utils/RoleGuard.jsx";
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
||||||
|
{/* Public route */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|
||||||
|
{/* Private routes with layout */}
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
|
||||||
element={
|
element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<GroceryList />
|
<AppLayout />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
<Route
|
<Route path="/" element={<GroceryList />} />
|
||||||
path="/admin"
|
|
||||||
element={
|
<Route
|
||||||
<RoleGuard allowed={[ROLES.ADMIN]}>
|
path="/admin"
|
||||||
<AdminPanel />
|
element={
|
||||||
</RoleGuard>
|
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||||
}
|
<AdminPanel />
|
||||||
/>
|
</RoleGuard>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import api from "./axios";
|
|||||||
|
|
||||||
export const loginRequest = async (username, password) => {
|
export const loginRequest = async (username, password) => {
|
||||||
const res = await api.post("/auth/login", { username, password });
|
const res = await api.post("/auth/login", { username, password });
|
||||||
|
alert(`Response data: ${JSON.stringify(res.data)}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,3 +3,7 @@ import api from "./axios";
|
|||||||
export const getList = () => api.get("/list");
|
export const getList = () => api.get("/list");
|
||||||
export const addItem = (itemName, quantiy) => api.post("/list/add", { itemName, quantiy });
|
export const addItem = (itemName, quantiy) => api.post("/list/add", { itemName, quantiy });
|
||||||
export const markBought = (id) => api.post("/list/mark-bought", { id });
|
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 { useContext } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { ROLES } from "../constants/roles";
|
|
||||||
import { AuthContext } from "../context/AuthContext";
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const { role, logout } = useContext(AuthContext);
|
const { role, logout, username } = useContext(AuthContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav>
|
<nav className="navbar">
|
||||||
<Link to="/">List</Link>
|
<div className="navbar-links">
|
||||||
{role === ROLES.ADMIN && <Link to="/admin">Admin Panel</Link>}
|
<Link to="/">Home</Link>
|
||||||
<button onClick={logout}>Logout</button>
|
|
||||||
|
{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>
|
</nav>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@ -1,103 +1,176 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
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 { AuthContext } from "../context/AuthContext";
|
||||||
|
import "../styles/GroceryList.css";
|
||||||
|
|
||||||
export default function GroceryList() {
|
export default function GroceryList() {
|
||||||
const { role, username } = useContext(AuthContext);
|
const { role, username } = useContext(AuthContext);
|
||||||
|
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
|
const [sortedItems, setSortedItems] = useState([]);
|
||||||
|
|
||||||
|
const [sortMode, setSortMode] = useState("az");
|
||||||
|
|
||||||
const [itemName, setItemName] = useState("");
|
const [itemName, setItemName] = useState("");
|
||||||
const [quantity, setQuantity] = useState(1);
|
const [quantity, setQuantity] = useState(1);
|
||||||
const [loading, setLoading] = useState(true);
|
const [suggestions, setSuggestions] = useState([]);
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
const [showAddForm, setShowAddForm] = useState(true);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// Load list
|
|
||||||
const loadItems = async () => {
|
const loadItems = async () => {
|
||||||
try {
|
setLoading(true);
|
||||||
setLoading(true);
|
const res = await getList();
|
||||||
const response = await getList();
|
setItems(res.data);
|
||||||
setItems(response.data);
|
setLoading(false);
|
||||||
setLoading(false);
|
|
||||||
} catch (err) {
|
|
||||||
setError("Failed to load grocery list");
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadItems();
|
loadItems();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Add item (editor/admin)
|
useEffect(() => {
|
||||||
const handleAdd = async (e) => {
|
let sorted = [...items];
|
||||||
e.preventDefault();
|
|
||||||
|
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 {
|
try {
|
||||||
await addItem(itemName, quantity);
|
setSuggestions(suggest(text).data.map((i) => i.item_name));
|
||||||
setItemName("");
|
} catch {
|
||||||
setQuantity(1);
|
setSuggestions([]);
|
||||||
loadItems();
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
setError("Failed to add item");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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) => {
|
const handleBought = async (id) => {
|
||||||
try {
|
const yes = window.confirm("Mark this item as bought?");
|
||||||
await markBought(id);
|
if (!yes) return;
|
||||||
loadItems();
|
|
||||||
} catch (err) {
|
await markBought(id);
|
||||||
setError("Failed to mark item as bought");
|
loadItems();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <p>Loading...</p>;
|
if (loading) return <p>Loading...</p>;
|
||||||
if (error) return <p style={{ color: "red" }}>{error}</p>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: "20px" }}>
|
<div className="glist-body">
|
||||||
<h1>Grocery List</h1>
|
<div className="glist-container">
|
||||||
<p>Logged in as: <strong>{username}</strong> ({role})</p>
|
|
||||||
|
|
||||||
{/* Add Item Section (editor/admin only) */}
|
<h1 className="glist-title">Costco Grocery List - {username}[{role}]</h1>
|
||||||
{(role === "editor" || role === "admin") && (
|
<p><strong>{username}</strong> ({role})</p>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Grocery List */}
|
{/* Sorting dropdown */}
|
||||||
<ul>
|
<select
|
||||||
{items.map((item) => (
|
value={sortMode}
|
||||||
<li key={item.id} style={{ margin: "10px 0" }}>
|
onChange={(e) => setSortMode(e.target.value)}
|
||||||
{item.item_name} — {item.quantity}
|
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") && (
|
{/* Add Item form (editor/admin only) */}
|
||||||
<button
|
{(role === "editor" || role === "admin") && showAddForm && (
|
||||||
onClick={() => handleBought(item.id)}
|
<>
|
||||||
style={{ marginLeft: "15px" }}
|
<input
|
||||||
>
|
type="text"
|
||||||
Mark Bought
|
className="glist-input"
|
||||||
</button>
|
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>
|
|
||||||
))}
|
<input
|
||||||
</ul>
|
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>
|
</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