diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b5ee22b..3b1acda 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: env: IMAGE_NAME: costco-grocery-list - REGISTRY: gitea.nicosaya.com + REGISTRY: git.nicosaya.com jobs: build: diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fff71a..c39bd96 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,65 +1,65 @@ -{ - // ============================ - // Editor basics - // ============================ - "editor.tabSize": 2, - "editor.insertSpaces": true, - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", - - // ============================ - // JS / TS behavior - // ============================ - "javascript.updateImportsOnFileMove.enabled": "always", - "typescript.updateImportsOnFileMove.enabled": "always", - "javascript.suggest.autoImports": true, - "typescript.suggest.autoImports": true, - - // Fix annoying "JSX not allowed" issues in js files - "javascript.validate.enable": false, - - // ============================ - // React specific - // ============================ - "emmet.includeLanguages": { - "javascript": "javascriptreact", - "typescript": "typescriptreact" - }, - "emmet.triggerExpansionOnTab": true, - - // ============================ - // Terminal - // ============================ - "terminal.integrated.defaultProfile.windows": "Git Bash", - - // ============================ - // Prettier - // ============================ - "[javascript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, - "[javascriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, - "[typescript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, - "[typescriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, - - // ============================ - // File Icons - // ============================ - "workbench.iconTheme": "material-icon-theme", - "material-icon-theme.folders.associations": { - "gitea/workflows": "repository", - }, - - // ============================ - // Explorer cleanup - // ============================ - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/node_modules": false, - "**/*.log": true, - "**/dist": true - } +{ + // ============================ + // Editor basics + // ============================ + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + + // ============================ + // JS / TS behavior + // ============================ + "javascript.updateImportsOnFileMove.enabled": "always", + "typescript.updateImportsOnFileMove.enabled": "always", + "javascript.suggest.autoImports": true, + "typescript.suggest.autoImports": true, + + // Fix annoying "JSX not allowed" issues in js files + "javascript.validate.enable": false, + + // ============================ + // React specific + // ============================ + "emmet.includeLanguages": { + "javascript": "javascriptreact", + "typescript": "typescriptreact" + }, + "emmet.triggerExpansionOnTab": true, + + // ============================ + // Terminal + // ============================ + "terminal.integrated.defaultProfile.windows": "Git Bash", + + // ============================ + // Prettier + // ============================ + "[javascript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, + "[javascriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, + "[typescript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, + "[typescriptreact]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, + + // ============================ + // File Icons + // ============================ + "workbench.iconTheme": "material-icon-theme", + "material-icon-theme.folders.associations": { + "gitea/workflows": "repository", + }, + + // ============================ + // Explorer cleanup + // ============================ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/node_modules": false, + "**/*.log": true, + "**/dist": true + } } \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index 10e0306..752aee7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,43 +1,43 @@ -const express = require("express"); -const cors = require("cors"); -const User = require("./models/user.model"); - -const app = express(); -app.use(express.json()); - -const allowedOrigins = process.env.ALLOWED_ORIGINS.split(",").map(origin => origin.trim()); -console.log("Allowed Origins:", allowedOrigins); -app.use( - cors({ - origin: function (origin, callback) { - if (!origin) return callback(null, true); - if (allowedOrigins.includes(origin)) return callback(null, true); - if (/^http:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true); - if (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true); - callback(new Error("Not allowed by CORS")); - }, - methods: ["GET", "POST", "PUT", "DELETE"], - }) -); - -app.get('/', async (req, res) => { - resText = `Grocery List API is running.\n` + - `Roles available: ${Object.values(User.ROLES).join(', ')}` - - res.status(200).type("text/plain").send(resText); -}); - - -const authRoutes = require("./routes/auth.routes"); -app.use("/auth", authRoutes); - -const listRoutes = require("./routes/list.routes"); -app.use("/list", listRoutes); - -const adminRoutes = require("./routes/admin.routes"); -app.use("/admin", adminRoutes); - -const usersRoutes = require("./routes/users.routes"); -app.use("/users", usersRoutes); - +const express = require("express"); +const cors = require("cors"); +const User = require("./models/user.model"); + +const app = express(); +app.use(express.json()); + +const allowedOrigins = process.env.ALLOWED_ORIGINS.split(",").map(origin => origin.trim()); +console.log("Allowed Origins:", allowedOrigins); +app.use( + cors({ + origin: function (origin, callback) { + if (!origin) return callback(null, true); + if (allowedOrigins.includes(origin)) return callback(null, true); + if (/^http:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true); + if (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true); + callback(new Error("Not allowed by CORS")); + }, + methods: ["GET", "POST", "PUT", "DELETE"], + }) +); + +app.get('/', async (req, res) => { + resText = `Grocery List API is running.\n` + + `Roles available: ${Object.values(User.ROLES).join(', ')}` + + res.status(200).type("text/plain").send(resText); +}); + + +const authRoutes = require("./routes/auth.routes"); +app.use("/auth", authRoutes); + +const listRoutes = require("./routes/list.routes"); +app.use("/list", listRoutes); + +const adminRoutes = require("./routes/admin.routes"); +app.use("/admin", adminRoutes); + +const usersRoutes = require("./routes/users.routes"); +app.use("/users", usersRoutes); + module.exports = app; \ No newline at end of file diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js index 222acd8..bc52c48 100644 --- a/backend/controllers/auth.controller.js +++ b/backend/controllers/auth.controller.js @@ -1,44 +1,44 @@ -const bcrypt = require("bcryptjs"); -const jwt = require("jsonwebtoken"); -const User = require("../models/user.model"); - -exports.register = async (req, res) => { - let { username, password, name } = req.body; - username = username.toLowerCase(); - console.log(`🆕 Registration attempt for ${name} => username:${username}, password:${password}`); - - try { - const hash = await bcrypt.hash(password, 10); - const user = await User.createUser(username, hash, name); - console.log(`✅ User registered: ${username}`); - - res.json({ message: "User registered", user }); - } catch (err) { - res.status(400).json({ message: "Registration failed", error: err }); - } -}; - -exports.login = async (req, res) => { - let { username, password } = req.body; - - username = username.toLowerCase(); - const user = await User.findByUsername(username); - if (!user) { - console.log(`⚠️ Login attempt -> No user found: ${username}`); - return res.status(401).json({ message: "User not found" }); - } - - const valid = await bcrypt.compare(password, user.password); - if (!valid) { - console.log(`⛔ Login attempt for user ${username} with password ${password}`); - return res.status(401).json({ message: "Invalid credentials" }); - } - - const token = jwt.sign( - { id: user.id, role: user.role }, - process.env.JWT_SECRET, - { expiresIn: "1d" } - ); - - res.json({ token, username, role: user.role }); -}; +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); +const User = require("../models/user.model"); + +exports.register = async (req, res) => { + let { username, password, name } = req.body; + username = username.toLowerCase(); + console.log(`🆕 Registration attempt for ${name} => username:${username}, password:${password}`); + + try { + const hash = await bcrypt.hash(password, 10); + const user = await User.createUser(username, hash, name); + console.log(`✅ User registered: ${username}`); + + res.json({ message: "User registered", user }); + } catch (err) { + res.status(400).json({ message: "Registration failed", error: err }); + } +}; + +exports.login = async (req, res) => { + let { username, password } = req.body; + + username = username.toLowerCase(); + const user = await User.findByUsername(username); + if (!user) { + console.log(`⚠️ Login attempt -> No user found: ${username}`); + return res.status(401).json({ message: "User not found" }); + } + + const valid = await bcrypt.compare(password, user.password); + if (!valid) { + console.log(`⛔ Login attempt for user ${username} with password ${password}`); + return res.status(401).json({ message: "Invalid credentials" }); + } + + const token = jwt.sign( + { id: user.id, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: "1d" } + ); + + res.json({ token, username, role: user.role }); +}; diff --git a/backend/controllers/lists.controller.js b/backend/controllers/lists.controller.js index 348f0f8..3667c2d 100644 --- a/backend/controllers/lists.controller.js +++ b/backend/controllers/lists.controller.js @@ -1,37 +1,37 @@ -const List = require("../models/list.model"); - - -exports.getList = async (req, res) => { - const items = await List.getUnboughtItems(); - res.json(items); -}; - -exports.getItemByName = async (req, res) => { - const { itemName } = req.query; - const item = await List.getItemByName(itemName); - res.json(item); -} - - -exports.addItem = async (req, res) => { - const { itemName, quantity } = req.body; - - const id = await List.addOrUpdateItem(itemName, quantity); - - await List.addHistoryRecord(id, quantity); - - res.json({ message: "Item added/updated" }); -}; - - -exports.markBought = async (req, res) => { - await List.setBought(req.body.id); - res.json({ message: "Item marked bought" }); -}; - - -exports.getSuggestions = async (req, res) => { - const { query } = req.query || ""; - const suggestions = await List.getSuggestions(query); - res.json(suggestions); +const List = require("../models/list.model"); + + +exports.getList = async (req, res) => { + const items = await List.getUnboughtItems(); + res.json(items); +}; + +exports.getItemByName = async (req, res) => { + const { itemName } = req.query; + const item = await List.getItemByName(itemName); + res.json(item); +} + + +exports.addItem = async (req, res) => { + const { itemName, quantity } = req.body; + + const id = await List.addOrUpdateItem(itemName, quantity); + + await List.addHistoryRecord(id, quantity); + + res.json({ message: "Item added/updated" }); +}; + + +exports.markBought = async (req, res) => { + await List.setBought(req.body.id); + res.json({ message: "Item marked bought" }); +}; + + +exports.getSuggestions = async (req, res) => { + const { query } = req.query || ""; + const suggestions = await List.getSuggestions(query); + res.json(suggestions); }; \ No newline at end of file diff --git a/backend/controllers/users.controller.js b/backend/controllers/users.controller.js index ef9bc1d..0ef8399 100644 --- a/backend/controllers/users.controller.js +++ b/backend/controllers/users.controller.js @@ -1,55 +1,55 @@ -const User = require("../models/user.model"); - -exports.test = async (req, res) => { - console.log("User route is working"); - res.json({ message: "User route is working" }); -}; - -exports.getAllUsers = async (req, res) => { - console.log(req); - const users = await User.getAllUsers(); - res.json(users); -}; - - -exports.updateUserRole = async (req, res) => { - try { - const { id, role } = req.body; - - console.log(`Updating user ${id} to role ${role}`); - if (!Object.values(User.ROLES).includes(role)) - return res.status(400).json({ error: "Invalid role" }); - - const updated = await User.updateUserRole(id, role); - if (!updated) - return res.status(404).json({ error: "User not found" }); - - res.json({ message: "Role updated", id, role }); - } catch (err) { - res.status(500).json({ error: "Failed to update role" }); - } -}; - -exports.deleteUser = async (req, res) => { - try { - const { id } = req.params; - - const deleted = await User.deleteUser(id); - if (!deleted) - return res.status(404).json({ error: "User not found" }); - - - res.json({ message: "User deleted", id }); - } catch (err) { - res.status(500).json({ error: "Failed to delete user" }); - } -}; - - - - -exports.checkIfUserExists = async (req, res) => { - const { username } = req.query; - const users = await User.checkIfUserExists(username); - res.json(users); -}; +const User = require("../models/user.model"); + +exports.test = async (req, res) => { + console.log("User route is working"); + res.json({ message: "User route is working" }); +}; + +exports.getAllUsers = async (req, res) => { + console.log(req); + const users = await User.getAllUsers(); + res.json(users); +}; + + +exports.updateUserRole = async (req, res) => { + try { + const { id, role } = req.body; + + console.log(`Updating user ${id} to role ${role}`); + if (!Object.values(User.ROLES).includes(role)) + return res.status(400).json({ error: "Invalid role" }); + + const updated = await User.updateUserRole(id, role); + if (!updated) + return res.status(404).json({ error: "User not found" }); + + res.json({ message: "Role updated", id, role }); + } catch (err) { + res.status(500).json({ error: "Failed to update role" }); + } +}; + +exports.deleteUser = async (req, res) => { + try { + const { id } = req.params; + + const deleted = await User.deleteUser(id); + if (!deleted) + return res.status(404).json({ error: "User not found" }); + + + res.json({ message: "User deleted", id }); + } catch (err) { + res.status(500).json({ error: "Failed to delete user" }); + } +}; + + + + +exports.checkIfUserExists = async (req, res) => { + const { username } = req.query; + const users = await User.checkIfUserExists(username); + res.json(users); +}; diff --git a/backend/models/list.model.js b/backend/models/list.model.js index 565d73b..328b383 100644 --- a/backend/models/list.model.js +++ b/backend/models/list.model.js @@ -1,73 +1,73 @@ -const pool = require("../db/pool"); - - -exports.getUnboughtItems = async () => { - const result = await pool.query( - "SELECT * FROM grocery_list WHERE bought = FALSE ORDER BY id ASC" - ); - return result.rows; -}; - -exports.getItemByName = async (itemName) => { - const result = await pool.query( - "SELECT * FROM grocery_list WHERE item_name ILIKE $1", - [itemName] - ); - - return result.rows[0] || null; -}; - - -exports.addOrUpdateItem = async (itemName, quantity) => { - const result = await pool.query( - "SELECT id, bought FROM grocery_list WHERE item_name ILIKE $1", - [itemName] - ); - - if (result.rowCount > 0) { - await pool.query( - `UPDATE grocery_list - SET quantity = $1, - bought = FALSE - WHERE id = $2`, - [quantity, result.rows[0].id] - ); - return result.rows[0].id; - } else { - const insert = await pool.query( - `INSERT INTO grocery_list - (item_name, quantity) - VALUES ($1, $2) RETURNING id`, - [itemName, quantity] - ); - return insert.rows[0].id; - } -}; - - -exports.setBought = async (id) => { - await pool.query("UPDATE grocery_list SET bought = TRUE WHERE id = $1", [id]); -}; - - -exports.addHistoryRecord = async (itemId, quantity) => { - await pool.query( - `INSERT INTO grocery_history (list_item_id, quantity, added_on) - VALUES ($1, $2, NOW())`, - [itemId, quantity] - ); -}; - - -exports.getSuggestions = async (query) => { - const result = await pool.query( - `SELECT DISTINCT item_name - FROM grocery_list - WHERE item_name ILIKE $1 - LIMIT 10`, - [`%${query}%`] - ); - res = result.rows; - return result.rows; -}; - +const pool = require("../db/pool"); + + +exports.getUnboughtItems = async () => { + const result = await pool.query( + "SELECT * FROM grocery_list WHERE bought = FALSE ORDER BY id ASC" + ); + return result.rows; +}; + +exports.getItemByName = async (itemName) => { + const result = await pool.query( + "SELECT * FROM grocery_list WHERE item_name ILIKE $1", + [itemName] + ); + + return result.rows[0] || null; +}; + + +exports.addOrUpdateItem = async (itemName, quantity) => { + const result = await pool.query( + "SELECT id, bought FROM grocery_list WHERE item_name ILIKE $1", + [itemName] + ); + + if (result.rowCount > 0) { + await pool.query( + `UPDATE grocery_list + SET quantity = $1, + bought = FALSE + WHERE id = $2`, + [quantity, result.rows[0].id] + ); + return result.rows[0].id; + } else { + const insert = await pool.query( + `INSERT INTO grocery_list + (item_name, quantity) + VALUES ($1, $2) RETURNING id`, + [itemName, quantity] + ); + return insert.rows[0].id; + } +}; + + +exports.setBought = async (id) => { + await pool.query("UPDATE grocery_list SET bought = TRUE WHERE id = $1", [id]); +}; + + +exports.addHistoryRecord = async (itemId, quantity) => { + await pool.query( + `INSERT INTO grocery_history (list_item_id, quantity, added_on) + VALUES ($1, $2, NOW())`, + [itemId, quantity] + ); +}; + + +exports.getSuggestions = async (query) => { + const result = await pool.query( + `SELECT DISTINCT item_name + FROM grocery_list + WHERE item_name ILIKE $1 + LIMIT 10`, + [`%${query}%`] + ); + res = result.rows; + return result.rows; +}; + diff --git a/backend/models/user.model.js b/backend/models/user.model.js index bb212fe..0340cc4 100644 --- a/backend/models/user.model.js +++ b/backend/models/user.model.js @@ -1,61 +1,61 @@ -const pool = require("../db/pool"); - -exports.ROLES = { - VIEWER: "viewer", - EDITOR: "editor", - ADMIN: "admin", -} - -exports.findByUsername = async (username) => { - query = `SELECT * FROM users WHERE username = ${username}`; - const result = await pool.query("SELECT * FROM users WHERE username = $1", [username]); - console.log(query); - return result.rows[0]; -}; - -exports.createUser = async (username, hashedPassword, name) => { - const result = await pool.query( - `INSERT INTO users (username, password, name, role) - VALUES ($1, $2, $3, $4)`, - [username, hashedPassword, name, this.ROLES.VIEWER] - ); - return result.rows[0]; -}; - - -exports.getAllUsers = async () => { - const result = await pool.query("SELECT id, username, name, role FROM users ORDER BY id ASC"); - return result.rows; -}; - - -exports.updateUserRole = async (id, role) => { - const result = await pool.query( - `UPDATE users - SET role = $1 - WHERE id = $2 - RETURNING id, username, name, role`, - [role, id] - ); - return result.rows[0]; -}; - - -exports.deleteUser = async (id) => { - const result = await pool.query( - `DELETE FROM users WHERE id = $1 RETURNING id`, - [id] - ); - return result.rowCount; -}; - - -exports.checkIfUserExists = async (username) => { - const result = await pool.query( - "SELECT COUNT(*) FROM users WHERE username = $1", - [username] - ); - return result.rows[0].count > 0; -} - - +const pool = require("../db/pool"); + +exports.ROLES = { + VIEWER: "viewer", + EDITOR: "editor", + ADMIN: "admin", +} + +exports.findByUsername = async (username) => { + query = `SELECT * FROM users WHERE username = ${username}`; + const result = await pool.query("SELECT * FROM users WHERE username = $1", [username]); + console.log(query); + return result.rows[0]; +}; + +exports.createUser = async (username, hashedPassword, name) => { + const result = await pool.query( + `INSERT INTO users (username, password, name, role) + VALUES ($1, $2, $3, $4)`, + [username, hashedPassword, name, this.ROLES.VIEWER] + ); + return result.rows[0]; +}; + + +exports.getAllUsers = async () => { + const result = await pool.query("SELECT id, username, name, role FROM users ORDER BY id ASC"); + return result.rows; +}; + + +exports.updateUserRole = async (id, role) => { + const result = await pool.query( + `UPDATE users + SET role = $1 + WHERE id = $2 + RETURNING id, username, name, role`, + [role, id] + ); + return result.rows[0]; +}; + + +exports.deleteUser = async (id) => { + const result = await pool.query( + `DELETE FROM users WHERE id = $1 RETURNING id`, + [id] + ); + return result.rowCount; +}; + + +exports.checkIfUserExists = async (username) => { + const result = await pool.query( + "SELECT COUNT(*) FROM users WHERE username = $1", + [username] + ); + return result.rows[0].count > 0; +} + + diff --git a/backend/routes/admin.routes.js b/backend/routes/admin.routes.js index 5a15e05..3dc9bd8 100644 --- a/backend/routes/admin.routes.js +++ b/backend/routes/admin.routes.js @@ -1,11 +1,11 @@ -const router = require("express").Router(); -const auth = require("../middleware/auth"); -const requireRole = require("../middleware/rbac"); -const usersController = require("../controllers/users.controller"); -const { ROLES } = require("../models/user.model"); - -router.get("/users", auth, requireRole(ROLES.ADMIN), usersController.getAllUsers); -router.put("/users", auth, requireRole(ROLES.ADMIN), usersController.updateUserRole); -router.delete("/users", auth, requireRole(ROLES.ADMIN), usersController.deleteUser); - -module.exports = router; +const router = require("express").Router(); +const auth = require("../middleware/auth"); +const requireRole = require("../middleware/rbac"); +const usersController = require("../controllers/users.controller"); +const { ROLES } = require("../models/user.model"); + +router.get("/users", auth, requireRole(ROLES.ADMIN), usersController.getAllUsers); +router.put("/users", auth, requireRole(ROLES.ADMIN), usersController.updateUserRole); +router.delete("/users", auth, requireRole(ROLES.ADMIN), usersController.deleteUser); + +module.exports = router; diff --git a/backend/routes/auth.routes.js b/backend/routes/auth.routes.js index dba137e..75db9de 100644 --- a/backend/routes/auth.routes.js +++ b/backend/routes/auth.routes.js @@ -1,13 +1,13 @@ -const router = require("express").Router(); -const controller = require("../controllers/auth.controller"); - -router.post("/register", controller.register); -router.post("/login", controller.login); -router.post("/", async (req, res) => { - resText = `Grocery List API is running.\n` + - `Roles available: ${Object.values(User.ROLES).join(', ')}` - - res.status(200).type("text/plain").send(resText); -}); - -module.exports = router; +const router = require("express").Router(); +const controller = require("../controllers/auth.controller"); + +router.post("/register", controller.register); +router.post("/login", controller.login); +router.post("/", async (req, res) => { + resText = `Grocery List API is running.\n` + + `Roles available: ${Object.values(User.ROLES).join(', ')}` + + res.status(200).type("text/plain").send(resText); +}); + +module.exports = router; diff --git a/backend/routes/list.routes.js b/backend/routes/list.routes.js index e73b2b8..63e6026 100644 --- a/backend/routes/list.routes.js +++ b/backend/routes/list.routes.js @@ -1,19 +1,19 @@ -const router = require("express").Router(); -const controller = require("../controllers/lists.controller"); -const auth = require("../middleware/auth"); -const requireRole = require("../middleware/rbac"); -const { ROLES } = require("../models/user.model"); -const User = require("../models/user.model"); - - - -router.get("/", auth, requireRole(...Object.values(ROLES)), controller.getList); -router.get("/item-by-name", auth, requireRole(...Object.values(ROLES)), controller.getItemByName); -router.get("/suggest", auth, requireRole(...Object.values(ROLES)), controller.getSuggestions); - - -router.post("/add", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.addItem); -router.post("/mark-bought", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.markBought); - - -module.exports = router; +const router = require("express").Router(); +const controller = require("../controllers/lists.controller"); +const auth = require("../middleware/auth"); +const requireRole = require("../middleware/rbac"); +const { ROLES } = require("../models/user.model"); +const User = require("../models/user.model"); + + + +router.get("/", auth, requireRole(...Object.values(ROLES)), controller.getList); +router.get("/item-by-name", auth, requireRole(...Object.values(ROLES)), controller.getItemByName); +router.get("/suggest", auth, requireRole(...Object.values(ROLES)), controller.getSuggestions); + + +router.post("/add", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.addItem); +router.post("/mark-bought", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), controller.markBought); + + +module.exports = router; diff --git a/backend/routes/users.routes.js b/backend/routes/users.routes.js index e89edbf..c13c5be 100644 --- a/backend/routes/users.routes.js +++ b/backend/routes/users.routes.js @@ -1,10 +1,10 @@ -const router = require("express").Router(); -const auth = require("../middleware/auth"); -const requireRole = require("../middleware/rbac"); -const usersController = require("../controllers/users.controller"); -const { ROLES } = require("../models/user.model"); - -router.get("/exists", usersController.checkIfUserExists); -router.get("/test", usersController.test); - -module.exports = router; +const router = require("express").Router(); +const auth = require("../middleware/auth"); +const requireRole = require("../middleware/rbac"); +const usersController = require("../controllers/users.controller"); +const { ROLES } = require("../models/user.model"); + +router.get("/exists", usersController.checkIfUserExists); +router.get("/test", usersController.test); + +module.exports = router; diff --git a/docker-compose.yml b/docker-compose.yml index de6750f..259cc37 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,29 @@ -services: - frontend: - build: - context: ./frontend - dockerfile: Dockerfile.dev - environment: - - NODE_ENV=development - volumes: - - ./frontend:/app - - /app/node_modules - ports: - - "3000:5173" - depends_on: - - backend - restart: always - - - backend: - build: - context: ./backend - command: npm run dev - volumes: - - ./backend:/app - - /app/node_modules - ports: - - "5000:5000" - env_file: - - ./backend/.env - restart: always +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + environment: + - NODE_ENV=development + volumes: + - ./frontend:/app + - /app/node_modules + ports: + - "3000:5173" + depends_on: + - backend + restart: always + + + backend: + build: + context: ./backend + command: npm run dev + volumes: + - ./backend:/app + - /app/node_modules + ports: + - "5000:5000" + env_file: + - ./backend/.env + restart: always diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d897821..f89c9c9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,53 +1,53 @@ -import { BrowserRouter, Route, Routes } from "react-router-dom"; -import { ROLES } from "./constants/roles"; -import { AuthProvider } from "./context/AuthContext.jsx"; - -import AdminPanel from "./pages/AdminPanel.jsx"; -import GroceryList from "./pages/GroceryList.jsx"; -import Login from "./pages/Login.jsx"; -import Register from "./pages/Register.jsx"; - -import AppLayout from "./components/AppLayout.jsx"; -import PrivateRoute from "./utils/PrivateRoute.jsx"; - -import RoleGuard from "./utils/RoleGuard.jsx"; - -console.log("VITE_ALLOWED_HOSTS:", import.meta.env.VITE_ALLOWED_HOSTS); - -function App() { - return ( - - - - - {/* Public route */} - } /> - } /> - - {/* Private routes with layout */} - - - - } - > - } /> - - - - - } - /> - - - - - - ); -} - +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { ROLES } from "./constants/roles"; +import { AuthProvider } from "./context/AuthContext.jsx"; + +import AdminPanel from "./pages/AdminPanel.jsx"; +import GroceryList from "./pages/GroceryList.jsx"; +import Login from "./pages/Login.jsx"; +import Register from "./pages/Register.jsx"; + +import AppLayout from "./components/AppLayout.jsx"; +import PrivateRoute from "./utils/PrivateRoute.jsx"; + +import RoleGuard from "./utils/RoleGuard.jsx"; + +console.log("VITE_ALLOWED_HOSTS:", import.meta.env.VITE_ALLOWED_HOSTS); + +function App() { + return ( + + + + + {/* Public route */} + } /> + } /> + + {/* Private routes with layout */} + + + + } + > + } /> + + + + + } + /> + + + + + + ); +} + export default App; \ No newline at end of file diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js index 258b775..2b271c7 100644 --- a/frontend/src/api/auth.js +++ b/frontend/src/api/auth.js @@ -1,11 +1,11 @@ -import api from "./axios"; - -export const loginRequest = async (username, password) => { - const res = await api.post("/auth/login", { username, password }); - return res.data; -}; - -export const registerRequest = async (username, password, name) => { - const res = await api.post("/auth/register", { username, password, name }); - return res.data; +import api from "./axios"; + +export const loginRequest = async (username, password) => { + const res = await api.post("/auth/login", { username, password }); + return res.data; +}; + +export const registerRequest = async (username, password, name) => { + const res = await api.post("/auth/register", { username, password, name }); + return res.data; }; \ No newline at end of file diff --git a/frontend/src/api/axios.js b/frontend/src/api/axios.js index dcc8dbe..bee1254 100644 --- a/frontend/src/api/axios.js +++ b/frontend/src/api/axios.js @@ -1,32 +1,32 @@ -import axios from "axios"; -import { API_BASE_URL } from "../config"; - -const api = axios.create({ - baseURL: API_BASE_URL, - headers: { - "Content-Type": "application/json", - }, -}); - -api.interceptors.request.use((config => { - const token = localStorage.getItem("token"); - if (token) { - config.headers["Authorization"] = `Bearer ${token}`; - } - return config; -})); - -api.interceptors.response.use( - response => response, - error => { - if (error.response?.status === 401 && - error.response?.data?.message === "Invalid or expired token") { - localStorage.removeItem("token"); - window.location.href = "/login"; - alert("Your session has expired. Please log in again."); - } - return Promise.reject(error); - } -); - +import axios from "axios"; +import { API_BASE_URL } from "../config"; + +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +api.interceptors.request.use((config => { + const token = localStorage.getItem("token"); + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; + } + return config; +})); + +api.interceptors.response.use( + response => response, + error => { + if (error.response?.status === 401 && + error.response?.data?.message === "Invalid or expired token") { + localStorage.removeItem("token"); + window.location.href = "/login"; + alert("Your session has expired. Please log in again."); + } + return Promise.reject(error); + } +); + export default api; \ No newline at end of file diff --git a/frontend/src/api/list.js b/frontend/src/api/list.js index 9f01cd6..675a82e 100644 --- a/frontend/src/api/list.js +++ b/frontend/src/api/list.js @@ -1,7 +1,7 @@ -import api from "./axios"; - -export const getList = () => api.get("/list"); -export const getItemByName = (itemName) => api.get("/list/item-by-name", { params: { itemName: itemName } }); -export const addItem = (itemName, quantity) => api.post("/list/add", { itemName, quantity }); -export const markBought = (id) => api.post("/list/mark-bought", { id }); +import api from "./axios"; + +export const getList = () => api.get("/list"); +export const getItemByName = (itemName) => api.get("/list/item-by-name", { params: { itemName: itemName } }); +export const addItem = (itemName, quantity) => api.post("/list/add", { itemName, quantity }); +export const markBought = (id) => api.post("/list/mark-bought", { id }); export const getSuggestions = (query) => api.get("/list/suggest", { params: { query: query } }); \ No newline at end of file diff --git a/frontend/src/api/users.js b/frontend/src/api/users.js index bbadb6c..c4d13d1 100644 --- a/frontend/src/api/users.js +++ b/frontend/src/api/users.js @@ -1,6 +1,6 @@ -import api from "./axios"; - -export const getAllUsers = () => api.get("/admin/users"); -export const updateRole = (id, role) => api.put(`/admin/users`, { id, role }); -export const deleteUser = (id) => api.delete(`/admin/users/${id}`); +import api from "./axios"; + +export const getAllUsers = () => api.get("/admin/users"); +export const updateRole = (id, role) => api.put(`/admin/users`, { id, role }); +export const deleteUser = (id) => api.delete(`/admin/users/${id}`); export const checkIfUserExists = (username) => api.get(`/users/exists`, { params: { username: username } }); \ No newline at end of file diff --git a/frontend/src/components/AppLayout.jsx b/frontend/src/components/AppLayout.jsx index 028174a..b2d8b07 100644 --- a/frontend/src/components/AppLayout.jsx +++ b/frontend/src/components/AppLayout.jsx @@ -1,11 +1,11 @@ -import { Outlet } from "react-router-dom"; -import Navbar from "./Navbar"; - -export default function AppLayout() { - return ( -
- - -
- ); -} +import { Outlet } from "react-router-dom"; +import Navbar from "./Navbar"; + +export default function AppLayout() { + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index c4845cb..c9d6331 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,30 +1,30 @@ -import "../styles/Navbar.css"; - -import { useContext } from "react"; -import { Link } from "react-router-dom"; -import { AuthContext } from "../context/AuthContext"; - -export default function Navbar() { - const { role, logout, username } = useContext(AuthContext); - - return ( - - ); +import "../styles/Navbar.css"; + +import { useContext } from "react"; +import { Link } from "react-router-dom"; +import { AuthContext } from "../context/AuthContext"; + +export default function Navbar() { + const { role, logout, username } = useContext(AuthContext); + + return ( + + ); } \ No newline at end of file diff --git a/frontend/src/constants/roles.js b/frontend/src/constants/roles.js index c108f79..e5ef6db 100644 --- a/frontend/src/constants/roles.js +++ b/frontend/src/constants/roles.js @@ -1,6 +1,6 @@ -export const ROLES = { - VIEWER: "viewer", - EDITOR: "editor", - ADMIN: "admin", - UP_TO_ADMIN: ["viewer", "editor", "admin"], +export const ROLES = { + VIEWER: "viewer", + EDITOR: "editor", + ADMIN: "admin", + UP_TO_ADMIN: ["viewer", "editor", "admin"], }; \ No newline at end of file diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 3d5ad97..e660f4b 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,46 +1,46 @@ -import { createContext, useState } from 'react'; - -export const AuthContext = createContext({ - token: null, - role: null, - username: null, - login: () => { }, - logout: () => { }, -}); - -export const AuthProvider = ({ children }) => { - const [token, setToken] = useState(localStorage.getItem('token') || null); - const [role, setRole] = useState(localStorage.getItem('role') || null); - const [username, setUsername] = useState(localStorage.getItem('username') || null); - - const login = (data) => { - localStorage.setItem('token', data.token); - localStorage.setItem('role', data.role); - localStorage.setItem('username', data.username); - setToken(data.token); - setRole(data.role); - setUsername(data.username); - }; - - const logout = () => { - localStorage.clear(); - - setToken(null); - setRole(null); - setUsername(null); - }; - - const value = { - token, - role, - username, - login, - logout - }; - - return ( - - {children} - - ); +import { createContext, useState } from 'react'; + +export const AuthContext = createContext({ + token: null, + role: null, + username: null, + login: () => { }, + logout: () => { }, +}); + +export const AuthProvider = ({ children }) => { + const [token, setToken] = useState(localStorage.getItem('token') || null); + const [role, setRole] = useState(localStorage.getItem('role') || null); + const [username, setUsername] = useState(localStorage.getItem('username') || null); + + const login = (data) => { + localStorage.setItem('token', data.token); + localStorage.setItem('role', data.role); + localStorage.setItem('username', data.username); + setToken(data.token); + setRole(data.role); + setUsername(data.username); + }; + + const logout = () => { + localStorage.clear(); + + setToken(null); + setRole(null); + setUsername(null); + }; + + const value = { + token, + role, + username, + login, + logout + }; + + return ( + + {children} + + ); }; \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d172030..1a03893 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.jsx' -import './index.css' - -createRoot(document.getElementById('root')!).render( - - - , -) +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/AdminPanel.jsx b/frontend/src/pages/AdminPanel.jsx index de7aff0..0bbd540 100644 --- a/frontend/src/pages/AdminPanel.jsx +++ b/frontend/src/pages/AdminPanel.jsx @@ -1,40 +1,40 @@ -import { useEffect, useState } from "react"; -import { getAllUsers, updateRole } from "../api/users"; -import { ROLES } from "../constants/roles"; - -export default function AdminPanel() { - const [users, setUsers] = useState([]); - - async function loadUsers() { - const allUsers = await getAllUsers(); - console.log(allUsers); - setUsers(allUsers.data); - } - - useEffect(() => { - loadUsers(); - }, []); - - const changeRole = async (id, role) => { - const updated = await updateRole(id, role); - if (updated.status !== 200) return; - loadUsers(); - } - - return ( -
-

Admin Panel

- {users.map((user) => ( -
- {user.username} - {user.role} - -
- )) - } -
- ) +import { useEffect, useState } from "react"; +import { getAllUsers, updateRole } from "../api/users"; +import { ROLES } from "../constants/roles"; + +export default function AdminPanel() { + const [users, setUsers] = useState([]); + + async function loadUsers() { + const allUsers = await getAllUsers(); + console.log(allUsers); + setUsers(allUsers.data); + } + + useEffect(() => { + loadUsers(); + }, []); + + const changeRole = async (id, role) => { + const updated = await updateRole(id, role); + if (updated.status !== 200) return; + loadUsers(); + } + + return ( +
+

Admin Panel

+ {users.map((user) => ( +
+ {user.username} - {user.role} + +
+ )) + } +
+ ) } \ No newline at end of file diff --git a/frontend/src/pages/GroceryList.jsx b/frontend/src/pages/GroceryList.jsx index 0f3a23c..e4ae9c0 100644 --- a/frontend/src/pages/GroceryList.jsx +++ b/frontend/src/pages/GroceryList.jsx @@ -1,193 +1,193 @@ -import { useContext, useEffect, useState } from "react"; -import { addItem, getItemByName, getList, getSuggestions, markBought } from "../api/list"; -import { ROLES } from "../constants/roles"; -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 [showSuggestions, setShowSuggestions] = useState(false); - const [itemName, setItemName] = useState(""); - const [quantity, setQuantity] = useState(1); - const [suggestions, setSuggestions] = useState([]); - - const [showAddForm, setShowAddForm] = useState(true); - const [loading, setLoading] = useState(true); - - const loadItems = async () => { - setLoading(true); - const res = await getList(); - setItems(res.data); - setLoading(false); - }; - - useEffect(() => { - loadItems(); - }, []); - - 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 { - let suggestions = await getSuggestions(text); - suggestions = suggestions.data.map(s => s.item_name); - setSuggestions(suggestions); - } catch { - setSuggestions([]); - } - }; - - const handleAdd = async (e) => { - e.preventDefault(); - if (!itemName.trim()) return; - let newQuantity = quantity; - - const item = await getItemByName(itemName); - if (item.data && item.data.bought === false) { - console.log("Item exists:", item.data); - let currentQuantity = item.data.quantity; - const yes = window.confirm( - `Item "${itemName}" already exists in the list. Do you want to update its quantity from ${currentQuantity} to ${currentQuantity + newQuantity}?` - ); - if (!yes) return; - - newQuantity += currentQuantity; - } - - await addItem(itemName, newQuantity); - - setItemName(""); - setQuantity(1); - setSuggestions([]); - - loadItems(); - }; - - const handleBought = async (id) => { - const yes = window.confirm("Mark this item as bought?"); - if (!yes) return; - - await markBought(id); - loadItems(); - }; - - if (loading) return

Loading...

; - - return ( -
-
-

Costco Grocery List

- - {/* Sorting dropdown */} - - - {/* Add Item form (editor/admin only) */} - {[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && ( - <> - handleSuggest(e.target.value)} - onBlur={() => setTimeout(() => setShowSuggestions(false), 150)} - onClick={() => setShowSuggestions(true)} - /> - - {showSuggestions && suggestions.length > 0 && ( - - )} - - setQuantity(Number(e.target.value))} - /> - - - - )} - - {/* Grocery list */} - -
- - {/* Floating Button (editor/admin only) */} - {[ROLES.ADMIN, ROLES.EDITOR].includes(role) && ( - - )} -
- ); -} +import { useContext, useEffect, useState } from "react"; +import { addItem, getItemByName, getList, getSuggestions, markBought } from "../api/list"; +import { ROLES } from "../constants/roles"; +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 [showSuggestions, setShowSuggestions] = useState(false); + const [itemName, setItemName] = useState(""); + const [quantity, setQuantity] = useState(1); + const [suggestions, setSuggestions] = useState([]); + + const [showAddForm, setShowAddForm] = useState(true); + const [loading, setLoading] = useState(true); + + const loadItems = async () => { + setLoading(true); + const res = await getList(); + setItems(res.data); + setLoading(false); + }; + + useEffect(() => { + loadItems(); + }, []); + + 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 { + let suggestions = await getSuggestions(text); + suggestions = suggestions.data.map(s => s.item_name); + setSuggestions(suggestions); + } catch { + setSuggestions([]); + } + }; + + const handleAdd = async (e) => { + e.preventDefault(); + if (!itemName.trim()) return; + let newQuantity = quantity; + + const item = await getItemByName(itemName); + if (item.data && item.data.bought === false) { + console.log("Item exists:", item.data); + let currentQuantity = item.data.quantity; + const yes = window.confirm( + `Item "${itemName}" already exists in the list. Do you want to update its quantity from ${currentQuantity} to ${currentQuantity + newQuantity}?` + ); + if (!yes) return; + + newQuantity += currentQuantity; + } + + await addItem(itemName, newQuantity); + + setItemName(""); + setQuantity(1); + setSuggestions([]); + + loadItems(); + }; + + const handleBought = async (id) => { + const yes = window.confirm("Mark this item as bought?"); + if (!yes) return; + + await markBought(id); + loadItems(); + }; + + if (loading) return

Loading...

; + + return ( +
+
+

Costco Grocery List

+ + {/* Sorting dropdown */} + + + {/* Add Item form (editor/admin only) */} + {[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && ( + <> + handleSuggest(e.target.value)} + onBlur={() => setTimeout(() => setShowSuggestions(false), 150)} + onClick={() => setShowSuggestions(true)} + /> + + {showSuggestions && suggestions.length > 0 && ( + + )} + + setQuantity(Number(e.target.value))} + /> + + + + )} + + {/* Grocery list */} + +
+ + {/* Floating Button (editor/admin only) */} + {[ROLES.ADMIN, ROLES.EDITOR].includes(role) && ( + + )} +
+ ); +} diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 7d6132a..64feb16 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,57 +1,57 @@ -import { useContext, useState } from "react"; -import { Link } from "react-router-dom"; -import { loginRequest } from "../api/auth"; -import { AuthContext } from "../context/AuthContext"; -import "../styles/Login.css"; - -export default function Login() { - const { login } = useContext(AuthContext); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); - - const submit = async (e) => { - e.preventDefault(); - setError(""); - - try { - const data = await loginRequest(username, password); - login(data); - window.location.href = "/"; - } catch (err) { - setError(err.response?.data?.message || "Login failed"); - } - }; - - return ( -
-
-

Login

- - {error &&

{error}

} - -
- setUsername(e.target.value)} - /> - - setPassword(e.target.value)} - /> - - -
- -

- Need an account? Register here -

-
-
- ); -} +import { useContext, useState } from "react"; +import { Link } from "react-router-dom"; +import { loginRequest } from "../api/auth"; +import { AuthContext } from "../context/AuthContext"; +import "../styles/Login.css"; + +export default function Login() { + const { login } = useContext(AuthContext); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + const submit = async (e) => { + e.preventDefault(); + setError(""); + + try { + const data = await loginRequest(username, password); + login(data); + window.location.href = "/"; + } catch (err) { + setError(err.response?.data?.message || "Login failed"); + } + }; + + return ( +
+
+

Login

+ + {error &&

{error}

} + +
+ setUsername(e.target.value)} + /> + + setPassword(e.target.value)} + /> + + +
+ +

+ Need an account? Register here +

+
+
+ ); +} diff --git a/frontend/src/styles/GroceryList.css b/frontend/src/styles/GroceryList.css index a6861b2..f168931 100644 --- a/frontend/src/styles/GroceryList.css +++ b/frontend/src/styles/GroceryList.css @@ -1,138 +1,138 @@ -/* 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; - border-radius: 8px; - box-shadow: 0 0 10px rgba(0,0,0,0.08); - padding: 1em; - width: calc(100% - 8em); - max-width: 440px; - margin: 0 auto; -} - -.glist-suggest-item { - padding: 0.5em; - padding-inline: 2em; - 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; - } -} +/* 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; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0,0,0,0.08); + padding: 1em; + width: calc(100% - 8em); + max-width: 440px; + margin: 0 auto; +} + +.glist-suggest-item { + padding: 0.5em; + padding-inline: 2em; + 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; + } +} diff --git a/frontend/src/styles/Navbar.css b/frontend/src/styles/Navbar.css index 30d23a2..5bca7aa 100644 --- a/frontend/src/styles/Navbar.css +++ b/frontend/src/styles/Navbar.css @@ -1,58 +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; -} +.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; +} diff --git a/frontend/src/utils/PrivateRoute.jsx b/frontend/src/utils/PrivateRoute.jsx index c5091a5..440f508 100644 --- a/frontend/src/utils/PrivateRoute.jsx +++ b/frontend/src/utils/PrivateRoute.jsx @@ -1,8 +1,8 @@ -import { useContext } from "react"; -import { Navigate } from "react-router-dom"; -import { AuthContext } from "../context/AuthContext"; - -export default function PrivateRoute({ children }) { - const { token } = useContext(AuthContext); - return token ? children : ; +import { useContext } from "react"; +import { Navigate } from "react-router-dom"; +import { AuthContext } from "../context/AuthContext"; + +export default function PrivateRoute({ children }) { + const { token } = useContext(AuthContext); + return token ? children : ; } \ No newline at end of file diff --git a/frontend/src/utils/RoleGuard.jsx b/frontend/src/utils/RoleGuard.jsx index 1ab2f3f..802d2f9 100644 --- a/frontend/src/utils/RoleGuard.jsx +++ b/frontend/src/utils/RoleGuard.jsx @@ -1,24 +1,24 @@ -import { useContext } from "react"; -import { Navigate } from "react-router-dom"; -import { AuthContext } from "../context/AuthContext"; - -export default function RoleGuard({ allowed, children }) { - const { role } = useContext(AuthContext); - - if (!role) return ; - if (!allowed.includes(role)) return ; - - return children; -} - - -function usageExample() { - - - - } - /> +import { useContext } from "react"; +import { Navigate } from "react-router-dom"; +import { AuthContext } from "../context/AuthContext"; + +export default function RoleGuard({ allowed, children }) { + const { role } = useContext(AuthContext); + + if (!role) return ; + if (!allowed.includes(role)) return ; + + return children; +} + + +function usageExample() { + + + + } + /> } \ No newline at end of file