binary change?
This commit is contained in:
parent
2563c1be5c
commit
3ad4b62991
@ -6,7 +6,7 @@ on:
|
||||
|
||||
env:
|
||||
IMAGE_NAME: costco-grocery-list
|
||||
REGISTRY: gitea.nicosaya.com
|
||||
REGISTRY: git.nicosaya.com
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
128
.vscode/settings.json
vendored
128
.vscode/settings.json
vendored
@ -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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
{/* Public route */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
{/* Private routes with layout */}
|
||||
<Route
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<AppLayout />
|
||||
</PrivateRoute>
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<GroceryList />} />
|
||||
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||
<AdminPanel />
|
||||
</RoleGuard>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
{/* Public route */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
{/* Private routes with layout */}
|
||||
<Route
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<AppLayout />
|
||||
</PrivateRoute>
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<GroceryList />} />
|
||||
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||
<AdminPanel />
|
||||
</RoleGuard>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
@ -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 } });
|
||||
@ -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 } });
|
||||
@ -1,11 +1,11 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function AppLayout() {
|
||||
return (
|
||||
<div>
|
||||
<Navbar />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function AppLayout() {
|
||||
return (
|
||||
<div>
|
||||
<Navbar />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<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>
|
||||
);
|
||||
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 (
|
||||
<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,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"],
|
||||
};
|
||||
@ -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 (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
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 (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@ -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 (
|
||||
<div>
|
||||
<h1>Admin Panel</h1>
|
||||
{users.map((user) => (
|
||||
<div key={user.id}>
|
||||
<strong>{user.username}</strong> - {user.role}
|
||||
<select onChange={(e) => changeRole(user.id, e.target.value)} value={user.role}>
|
||||
<option value={ROLES.VIEWER}>Viewer</option>
|
||||
<option value={ROLES.EDITOR}>Editor</option>
|
||||
<option value={ROLES.ADMIN}>Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div >
|
||||
)
|
||||
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 (
|
||||
<div>
|
||||
<h1>Admin Panel</h1>
|
||||
{users.map((user) => (
|
||||
<div key={user.id}>
|
||||
<strong>{user.username}</strong> - {user.role}
|
||||
<select onChange={(e) => changeRole(user.id, e.target.value)} value={user.role}>
|
||||
<option value={ROLES.VIEWER}>Viewer</option>
|
||||
<option value={ROLES.EDITOR}>Editor</option>
|
||||
<option value={ROLES.ADMIN}>Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
@ -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 <p>Loading...</p>;
|
||||
|
||||
return (
|
||||
<div className="glist-body">
|
||||
<div className="glist-container">
|
||||
<h1 className="glist-title">Costco Grocery List</h1>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Add Item form (editor/admin only) */}
|
||||
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className="glist-input"
|
||||
placeholder="Item name"
|
||||
value={itemName}
|
||||
onChange={(e) => handleSuggest(e.target.value)}
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||
onClick={() => setShowSuggestions(true)}
|
||||
/>
|
||||
|
||||
{showSuggestions && 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>
|
||||
)}
|
||||
|
||||
<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={() =>
|
||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(item.id)
|
||||
}
|
||||
>
|
||||
{item.item_name} ({item.quantity})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Floating Button (editor/admin only) */}
|
||||
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && (
|
||||
<button
|
||||
className="glist-fab"
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
>
|
||||
{showAddForm ? "−" : "+"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 <p>Loading...</p>;
|
||||
|
||||
return (
|
||||
<div className="glist-body">
|
||||
<div className="glist-container">
|
||||
<h1 className="glist-title">Costco Grocery List</h1>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Add Item form (editor/admin only) */}
|
||||
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className="glist-input"
|
||||
placeholder="Item name"
|
||||
value={itemName}
|
||||
onChange={(e) => handleSuggest(e.target.value)}
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||
onClick={() => setShowSuggestions(true)}
|
||||
/>
|
||||
|
||||
{showSuggestions && 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>
|
||||
)}
|
||||
|
||||
<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={() =>
|
||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(item.id)
|
||||
}
|
||||
>
|
||||
{item.item_name} ({item.quantity})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Floating Button (editor/admin only) */}
|
||||
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && (
|
||||
<button
|
||||
className="glist-fab"
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
>
|
||||
{showAddForm ? "−" : "+"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className="login-wrapper">
|
||||
<div className="login-box">
|
||||
<h1 className="login-title">Login</h1>
|
||||
|
||||
{error && <p className="login-error">{error}</p>}
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<input
|
||||
type="text"
|
||||
className="login-input"
|
||||
placeholder="Username"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
className="login-input"
|
||||
placeholder="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<button type="submit" className="login-button">Login</button>
|
||||
</form>
|
||||
|
||||
<p className="login-register">
|
||||
Need an account? <Link to="/register">Register here</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div className="login-wrapper">
|
||||
<div className="login-box">
|
||||
<h1 className="login-title">Login</h1>
|
||||
|
||||
{error && <p className="login-error">{error}</p>}
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<input
|
||||
type="text"
|
||||
className="login-input"
|
||||
placeholder="Username"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
className="login-input"
|
||||
placeholder="Password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<button type="submit" className="login-button">Login</button>
|
||||
</form>
|
||||
|
||||
<p className="login-register">
|
||||
Need an account? <Link to="/register">Register here</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 : <Navigate to="/login" />;
|
||||
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 : <Navigate to="/login" />;
|
||||
}
|
||||
@ -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 <Navigate to="/login" />;
|
||||
if (!allowed.includes(role)) return <Navigate to="/" />;
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
|
||||
function usageExample() {
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||
<AdminPanel />
|
||||
</RoleGuard>
|
||||
}
|
||||
/>
|
||||
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 <Navigate to="/login" />;
|
||||
if (!allowed.includes(role)) return <Navigate to="/" />;
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
|
||||
function usageExample() {
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<RoleGuard allowed={[ROLES.ADMIN]}>
|
||||
<AdminPanel />
|
||||
</RoleGuard>
|
||||
}
|
||||
/>
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user