From c07d503d835327c3aa3b9501c0cea831a72ae5f6 Mon Sep 17 00:00:00 2001 From: Jurgen Rentinck Date: Fri, 7 Aug 2026 20:45:36 +0200 Subject: [PATCH] Werk hoofdapplicatie bij Legt de bestaande wijzigingen in de hoofdworktree vast voordat de laatste worktree wordt gemerged.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .env.example | 7 + .gitignore | 14 ++ .open | 0 app.js | 21 ++- config/database.js | 11 +- config/express.js | 26 ++- config/session.js | 9 +- controllers/AuthController.js | 56 +++++-- controllers/DashboardController.js | 21 ++- controllers/HomeController.js | 6 +- controllers/HouseholdController.js | 14 +- controllers/ProductController.js | 40 +++-- controllers/ShoppingListController.js | 179 ++++++++++++++++++++ database.sqlite-shm | Bin 32768 -> 32768 bytes database.sqlite-wal | Bin 0 -> 222512 bytes database/seed.js | 56 +++---- middleware/auth.js | 6 +- open | 0 public/css/style.css | 37 ++++- public/icons/icon.svg | 5 + public/js/app.js | 48 ++++++ public/manifest.json | 43 +++++ public/service-worker.js | 98 +++++++++++ repositories/ShoppingListRepository.js | 108 ++++++++++++ repositories/UserRepository.js | 14 +- routes/api.js | 43 +++++ routes/auth.js | 26 ++- routes/web.js | 51 +++++- sessions.sqlite | Bin 12288 -> 20480 bytes sockets/handlers.js | 55 ++++++ views/auth/login.ejs | 102 +++++++----- views/auth/register.ejs | 139 +++++++++------- views/dashboard/index.ejs | 119 +++++++++++-- views/household/index.ejs | 109 +++++++++--- views/layouts/main.ejs | 37 ++--- views/lists/index.ejs | 96 +++++++++++ views/lists/show.ejs | 221 +++++++++++++++++++++++++ views/partials/flash.ejs | 66 ++++++++ views/partials/head.ejs | 24 ++- views/partials/navbar.ejs | 97 ++++++++++- views/products/index.ejs | 146 ++++++++++++---- 41 files changed, 1837 insertions(+), 313 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .open create mode 100644 controllers/ShoppingListController.js create mode 100644 open create mode 100644 public/icons/icon.svg create mode 100644 public/js/app.js create mode 100644 public/manifest.json create mode 100644 public/service-worker.js create mode 100644 repositories/ShoppingListRepository.js create mode 100644 routes/api.js create mode 100644 sockets/handlers.js create mode 100644 views/lists/index.ejs create mode 100644 views/lists/show.ejs create mode 100644 views/partials/flash.ejs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d6a15f3 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +PORT=3000 + +NODE_ENV=development + +SESSION_SECRET=change_this_secret + +DATABASE=database.sqlite \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..391c439 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +node_modules/ + +.env + +database.sqlite + +storage/logs/* +storage/backups/* + +public/uploads/* + +.vscode + +.idea \ No newline at end of file diff --git a/.open b/.open new file mode 100644 index 0000000..e69de29 diff --git a/app.js b/app.js index 979f0c5..3c51a4c 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,30 @@ require("dotenv").config(); const http = require("http"); +const { Server } = require("socket.io"); const createApp = require("./config/express"); +const session = require("./config/session"); +const socketHandlers = require("./sockets/handlers"); const app = createApp(); const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"] + } +}); + +io.use((socket, next) => session(socket.request, {}, next)); + +socketHandlers(io); + +// Make io available in routes +app.locals.io = io; + const PORT = process.env.PORT || 3000; server.listen(PORT, () => { @@ -22,4 +39,6 @@ server.listen(PORT, () => { console.log("==================================="); -}); \ No newline at end of file + console.log(""); + +}); diff --git a/config/database.js b/config/database.js index 41e35b3..ffb170d 100644 --- a/config/database.js +++ b/config/database.js @@ -1,15 +1,14 @@ const Database = require("better-sqlite3"); const path = require("path"); -const databaseFile = path.join( - __dirname, - "..", - process.env.DATABASE || "database.sqlite" -); +const configuredDatabase = process.env.DATABASE || "database.sqlite"; +const databaseFile = path.isAbsolute(configuredDatabase) + ? configuredDatabase + : path.join(__dirname, "..", configuredDatabase); const db = new Database(databaseFile); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); -module.exports = db; \ No newline at end of file +module.exports = db; diff --git a/config/express.js b/config/express.js index 35ebbe3..e0267d9 100644 --- a/config/express.js +++ b/config/express.js @@ -57,9 +57,24 @@ module.exports = () => { app.use(session); -app.use(userMiddleware); + app.use(flash()); -app.use(flash()); + app.use(userMiddleware); + + app.use((req, res, next) => { + + res.locals.user = req.user || null; + + res.locals.success = req.flash("success"); + res.locals.error = req.flash("error"); + res.locals.info = req.flash("info"); + res.locals.warning = req.flash("warning"); + + res.locals.title = res.locals.title || "PantryHub"; + + next(); + + }); app.use( @@ -69,6 +84,13 @@ app.use(flash()); ); + // API Routes + app.use( + "/api", + require("../routes/api") + ); + + // Web Routes app.use( "/", require("../routes/auth") diff --git a/config/session.js b/config/session.js index 0c44709..ad93068 100644 --- a/config/session.js +++ b/config/session.js @@ -14,14 +14,17 @@ module.exports = session({ } }), - secret: process.env.SESSION_SECRET, + secret: process.env.SESSION_SECRET || "change-this-development-secret", resave: false, saveUninitialized: false, cookie: { - maxAge: 1000 * 60 * 60 * 24 * 30 + maxAge: 1000 * 60 * 60 * 24 * 30, + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production" } -}); \ No newline at end of file +}); diff --git a/controllers/AuthController.js b/controllers/AuthController.js index dd1f9cd..b29406c 100644 --- a/controllers/AuthController.js +++ b/controllers/AuthController.js @@ -10,20 +10,23 @@ class AuthController { registerForm(req, res) { - res.render("auth/register", { - title: "Registreren" - }); + res.locals.title = "Registreren"; + + res.render("auth/register"); } async register(req, res) { - const { - name, - email, - password - } = req.body; + const name = (req.body.name || "").trim(); + const email = (req.body.email || "").trim().toLowerCase(); + const password = req.body.password || ""; + + if (name.length < 2 || !/^\S+@\S+\.\S+$/.test(email) || password.length < 8) { + req.flash("error", "Vul een naam, geldig e-mailadres en een wachtwoord van minimaal 8 tekens in."); + return res.redirect("/register"); + } const existingUser = db.prepare( @@ -67,7 +70,7 @@ class AuthController { ); - const userId = result.lastInsertRowid; + const userId = result.lastInsertRowid; HouseholdService.createForUser( @@ -78,6 +81,10 @@ class AuthController { req.session.userId = userId; + req.flash( + "success", + "Welkom bij PantryHub!" + ); res.redirect("/"); @@ -87,9 +94,18 @@ class AuthController { loginForm(req, res) { - res.render("auth/login", { - title: "Inloggen" - }); + res.locals.title = "Inloggen"; + + if (req.query.logout) { + + req.flash( + "info", + "Je bent uitgelogd." + ); + + } + + res.render("auth/login"); } @@ -97,10 +113,8 @@ class AuthController { async login(req, res) { - const { - email, - password - } = req.body; + const email = (req.body.email || "").trim().toLowerCase(); + const password = req.body.password || ""; const user = db.prepare( @@ -141,6 +155,10 @@ class AuthController { req.session.userId = user.id; + req.flash( + "success", + `Welkom terug ${user.name}!` + ); res.redirect("/"); @@ -152,7 +170,9 @@ class AuthController { req.session.destroy(() => { - res.redirect("/login"); + res.clearCookie("connect.sid"); + + res.redirect("/login?logout=1"); }); @@ -162,4 +182,4 @@ class AuthController { } -module.exports = new AuthController(); \ No newline at end of file +module.exports = new AuthController(); diff --git a/controllers/DashboardController.js b/controllers/DashboardController.js index 686d599..cb9cb30 100644 --- a/controllers/DashboardController.js +++ b/controllers/DashboardController.js @@ -1,25 +1,28 @@ -class DashboardController { +const ShoppingListRepository = require("../repositories/ShoppingListRepository"); +class DashboardController { index(req, res) { + const lists = ShoppingListRepository.getByHousehold( + req.user.household_id + ); + + const recentLists = lists.slice(0, 5); + + res.locals.title = "Dashboard"; res.render( "dashboard/index", { - - title: "Dashboard", - - user: req.user - + user: req.user, + lists: recentLists, + totalLists: lists.length } ); - } - } - module.exports = new DashboardController(); \ No newline at end of file diff --git a/controllers/HomeController.js b/controllers/HomeController.js index 6750343..274e61f 100644 --- a/controllers/HomeController.js +++ b/controllers/HomeController.js @@ -2,11 +2,9 @@ class HomeController { index(req, res) { - res.render("home/index", { + res.locals.title = "PantryHub"; - title: "PantryHub" - - }); + res.render("home/index"); } diff --git a/controllers/HouseholdController.js b/controllers/HouseholdController.js index 66cb980..5e8448f 100644 --- a/controllers/HouseholdController.js +++ b/controllers/HouseholdController.js @@ -1,21 +1,21 @@ +const UserRepository = require("../repositories/UserRepository"); + class HouseholdController { + index(req, res) { - index(req,res){ + res.locals.title = "Huishouden"; res.render( "household/index", { - title:"Huishouden", - user:req.user + user: req.user, + members: UserRepository.getHouseholdMembers(req.user.household_id) } ); } - } - -module.exports = -new HouseholdController(); \ No newline at end of file +module.exports = new HouseholdController(); diff --git a/controllers/ProductController.js b/controllers/ProductController.js index c39bf1a..2e39d0f 100644 --- a/controllers/ProductController.js +++ b/controllers/ProductController.js @@ -1,35 +1,53 @@ const ProductService = require("../services/ProductService"); +const db = require("../config/database"); + class ProductController { - - index(req,res) { - + index(req, res) { const products = ProductService.getProducts(); + const categories = db.prepare(` + SELECT * FROM categories ORDER BY name + `).all(); + + res.locals.title = "Producten"; res.render( "products/index", { - - title:"Producten", - - products - + products, + categories, + user: req.user } ); + } + + create(req, res) { + + const { name, category_id } = req.body; + + if (!name || name.trim().length === 0) { + req.flash("error", "Geef een productnaam op"); + return res.redirect("/products"); + } + + ProductService.createProduct({ + name: name.trim(), + category_id: category_id || null + }); + + req.flash("success", "Product toegevoegd"); + res.redirect("/products"); } - - } - module.exports = new ProductController(); \ No newline at end of file diff --git a/controllers/ShoppingListController.js b/controllers/ShoppingListController.js new file mode 100644 index 0000000..6b23544 --- /dev/null +++ b/controllers/ShoppingListController.js @@ -0,0 +1,179 @@ +const ShoppingListRepository = require("../repositories/ShoppingListRepository"); +const ProductRepository = require("../repositories/ProductRepository"); + +class ShoppingListController { + + index(req, res) { + + const lists = ShoppingListRepository.getByHousehold( + req.user.household_id + ); + + res.locals.title = "Boodschappenlijsten"; + + res.render("lists/index", { + lists, + user: req.user + }); + + } + + create(req, res) { + + const { name } = req.body; + + if (!name || name.trim().length === 0) { + req.flash("error", "Geef een naam op"); + return res.redirect("/lists"); + } + + // Check if user has a household + if (!req.user || !req.user.household_id) { + req.flash("error", "Je bent niet aan een huishouden gekoppeld"); + return res.redirect("/lists"); + } + + ShoppingListRepository.create({ + household_id: req.user.household_id, + name: name.trim() + }); + + req.flash("success", "Boodschappenlijst aangemaakt"); + res.redirect("/lists"); + + } + + show(req, res) { + + const { id } = req.params; + + const list = ShoppingListRepository.getById(id); + + if (!list) { + return res.status(404).render("errors/404"); + } + + if (list.household_id !== req.user.household_id) { + return res.status(403).render("errors/404"); + } + + const items = ShoppingListRepository.getItems(id); + + res.locals.title = list.name; + + res.render("lists/show", { + list, + items, + user: req.user + }); + + } + + addItem(req, res) { + + const { id } = req.params; + const { product_id, amount } = req.body; + + const list = ShoppingListRepository.getById(id); + + if (!list) { + return res.status(404).json({ error: "Lijst niet gevonden" }); + } + + if (list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Niet gemachtigd" }); + } + + const product = ProductRepository.findById(product_id); + + if (!product) { + return res.status(404).json({ error: "Product niet gevonden" }); + } + + const quantity = Number(amount); + if (!Number.isFinite(quantity) || quantity <= 0 || quantity > 999) { + req.flash("error", "Vul een geldige hoeveelheid in"); + return res.redirect(`/lists/${id}`); + } + + const item = ShoppingListRepository.addItem({ + list_id: id, + product_id, + amount: quantity + }); + + req.app.locals.io.to(`household:${req.user.household_id}`).emit("item:added", { + listId: Number(id) + }); + + req.flash("success", "Product toegevoegd"); + + if (req.xhr || req.headers.accept?.includes("application/json")) { + return res.json({ success: true, item }); + } + + res.redirect(`/lists/${id}`); + + } + + toggleItem(req, res) { + + const { id, itemId } = req.params; + + const list = ShoppingListRepository.getById(id); + + if (!list || list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Niet gemachtigd" }); + } + + if (!ShoppingListRepository.findItem(id, itemId)) { + return res.status(404).json({ error: "Item niet gevonden" }); + } + + ShoppingListRepository.toggleItem(itemId); + + req.app.locals.io.to(`household:${req.user.household_id}`).emit("item:toggled", { + listId: Number(id), itemId: Number(itemId) + }); + + if (req.xhr || req.headers.accept?.includes("application/json")) { + return res.json({ success: true }); + } + + res.redirect(`/lists/${id}`); + + } + + deleteItem(req, res) { + + const { id, itemId } = req.params; + + const list = ShoppingListRepository.getById(id); + + if (!list || list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Niet gemachtigd" }); + } + + if (!ShoppingListRepository.findItem(id, itemId)) { + return res.status(404).json({ error: "Item niet gevonden" }); + } + + ShoppingListRepository.deleteItem(itemId); + + req.app.locals.io.to(`household:${req.user.household_id}`).emit("item:removed", { + listId: Number(id), itemId: Number(itemId) + }); + + req.flash("success", "Product verwijderd"); + + if (req.method === "DELETE" || req.xhr || req.headers.accept?.includes("application/json")) { + return res.json({ success: true }); + } + + res.redirect(`/lists/${id}`); + + } + +} + +module.exports = new ShoppingListController(); diff --git a/database.sqlite-shm b/database.sqlite-shm index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..2039af5a084fb2e96f58012379b43fed627186f4 100644 GIT binary patch literal 32768 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 R%tnE7`^5qQo@+BN@PGLQrn&$C literal 0 HcmV?d00001 diff --git a/database/seed.js b/database/seed.js index f4326ad..91c1f4d 100644 --- a/database/seed.js +++ b/database/seed.js @@ -49,16 +49,29 @@ const products = [ ]; +const insertCategory = db.prepare(` + +INSERT INTO categories (name, icon) +SELECT ?, ? +WHERE NOT EXISTS (SELECT 1 FROM categories WHERE name = ?) + +`); + +const seedCategories = db.transaction(() => { + + for (const category of categories) { + insertCategory.run(category[0], category[1], category[0]); + } + +}); + +seedCategories(); + const insertProduct = db.prepare(` -INSERT OR IGNORE INTO products - -( - name, - category_id -) - -VALUES (?,?) +INSERT INTO products (name, category_id) +SELECT ?, ? +WHERE NOT EXISTS (SELECT 1 FROM products WHERE name = ?) `); @@ -70,7 +83,7 @@ const seedProducts = db.transaction(() => { for(const product of products){ - insertProduct.run(product); + insertProduct.run(product[0], product[1], product[0]); } @@ -80,28 +93,5 @@ const seedProducts = db.transaction(() => { seedProducts(); -const insert = db.prepare(` -INSERT OR IGNORE INTO categories -(name, icon) - -VALUES (?,?) - -`); - - -const seed = db.transaction(() => { - - for (const category of categories) { - - insert.run(category); - - } - -}); - - -seed(); - - -console.log("Seed voltooid."); \ No newline at end of file +console.log("Seed voltooid."); diff --git a/middleware/auth.js b/middleware/auth.js index 01c7f78..2b957d4 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -3,6 +3,10 @@ module.exports = function(req, res, next) { if (!req.session.userId) { + if (req.originalUrl.startsWith("/api/")) { + return res.status(401).json({ error: "Je bent niet ingelogd" }); + } + return res.redirect("/login"); } @@ -10,4 +14,4 @@ module.exports = function(req, res, next) { next(); -}; \ No newline at end of file +}; diff --git a/open b/open new file mode 100644 index 0000000..e69de29 diff --git a/public/css/style.css b/public/css/style.css index ec192fa..c16a7da 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,12 +1,43 @@ body { + background: #f5f7fb; +} - background-color: #f6f8fb; +.navbar { + margin-bottom: 25px; +} + +.card { + border: 0; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,.06); +} + +.page-title { + font-weight: 700; +} + +.alert { + + animation: fadeIn .3s ease; } +@keyframes fadeIn { -.navbar-brand { + from{ - font-weight: 700; + opacity:0; + + transform:translateY(-10px); + + } + + to{ + + opacity:1; + + transform:translateY(0); + + } } \ No newline at end of file diff --git a/public/icons/icon.svg b/public/icons/icon.svg new file mode 100644 index 0000000..1f6ea9a --- /dev/null +++ b/public/icons/icon.svg @@ -0,0 +1,5 @@ + + PantryHub + + + diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..71b89d7 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,48 @@ +// Socket.IO Client +const socket = typeof io === 'function' ? io() : null; + +// Join household room if available +const householdId = document.querySelector('[data-household-id]')?.dataset.householdId; +if (socket && householdId) { + socket.emit('join:household', householdId); +} + +// Real-time list updates +socket?.on('list:updated', (data) => { + console.log('📝 Lijst bijgewerkt:', data); + window.location.reload(); +}); + +socket?.on('item:added', (data) => { + console.log('➕ Item toegevoegd:', data); + window.location.reload(); +}); + +socket?.on('item:toggled', (data) => { + console.log('✓ Item afgevinkt:', data); + window.location.reload(); +}); + +socket?.on('item:removed', (data) => { + console.log('✕ Item verwijderd:', data); + window.location.reload(); +}); + +// Auto-remove alerts +document.addEventListener("DOMContentLoaded", () => { + + setTimeout(() => { + + document + .querySelectorAll(".alert") + .forEach(alert => { + + alert.remove(); + + }); + + }, 4000); + +}); + +console.log('✅ PantryHub app geladen'); diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..08d60c5 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,43 @@ +{ + "name": "PantryHub", + "short_name": "PantryHub", + "description": "Realtime boodschappenapp", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#0066cc", + "icons": [ + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ], + "shortcuts": [ + { + "name": "Lijsten", + "short_name": "Lijsten", + "description": "Bekijk je boodschappenlijsten", + "url": "/lists", + "icons": [ + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml" + } + ] + } + ], + "categories": [ + "shopping", + "lifestyle" + ] +} diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..7e1d1d7 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,98 @@ +const CACHE_NAME = 'pantryhub-v2'; +const urlsToCache = [ + '/', + '/css/style.css', + '/js/app.js', + '/manifest.json', + '/icons/icon.svg' +]; + +// Install event +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(urlsToCache).catch(err => { + console.log('Cache addAll error:', err); + }); + }) + ); +}); + +// Activate event +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); + +// Fetch event +self.addEventListener('fetch', (event) => { + // Skip non-GET requests + if (event.request.method !== 'GET') { + return; + } + + // Browser extensions and third-party resources cannot be stored by Cache API. + // Only handle same-origin HTTP(S) requests belonging to PantryHub. + const url = new URL(event.request.url); + if (!['http:', 'https:'].includes(url.protocol) || url.origin !== self.location.origin) { + return; + } + + // For API calls, use network-first strategy + if (event.request.url.includes('/api/')) { + event.respondWith( + fetch(event.request) + .then(response => { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then(cache => { + cache.put(event.request, responseClone); + }); + return response; + }) + .catch(() => { + return caches.match(event.request); + }) + ); + return; + } + + // For other requests, use cache-first strategy + event.respondWith( + caches.match(event.request) + .then(response => { + if (response) { + return response; + } + return fetch(event.request).then(response => { + if (!response || response.status !== 200 || response.type !== 'basic') { + return response; + } + const responseClone = response.clone(); + caches.open(CACHE_NAME) + .then(cache => { + cache.put(event.request, responseClone); + }); + return response; + }); + }) + .catch(() => { + // Return offline page or empty response + return new Response('Offline - pagina niet beschikbaar', { + status: 503, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/plain' + }) + }); + }) + ); +}); diff --git a/repositories/ShoppingListRepository.js b/repositories/ShoppingListRepository.js new file mode 100644 index 0000000..f58f80f --- /dev/null +++ b/repositories/ShoppingListRepository.js @@ -0,0 +1,108 @@ +const db = require("../config/database"); + +class ShoppingListRepository { + + getByHousehold(householdId) { + + return db.prepare(` + SELECT + shopping_lists.*, + COUNT(shopping_list_items.id) as item_count, + SUM(CASE WHEN shopping_list_items.checked = 1 THEN 1 ELSE 0 END) as checked_count + FROM shopping_lists + LEFT JOIN shopping_list_items + ON shopping_lists.id = shopping_list_items.list_id + WHERE shopping_lists.household_id = ? + AND shopping_lists.archived = 0 + GROUP BY shopping_lists.id + ORDER BY shopping_lists.created_at DESC + `).all(householdId); + + } + + getById(id) { + + return db.prepare(` + SELECT * FROM shopping_lists WHERE id = ? + `).get(id); + + } + + create(data) { + + return db.prepare(` + INSERT INTO shopping_lists + (household_id, name) + VALUES (?, ?) + `).run(data.household_id, data.name); + + } + + getItems(listId) { + + return db.prepare(` + SELECT + shopping_list_items.*, + products.name as product_name, + categories.name as category_name, + categories.icon as category_icon + FROM shopping_list_items + JOIN products ON products.id = shopping_list_items.product_id + LEFT JOIN categories ON categories.id = products.category_id + WHERE shopping_list_items.list_id = ? + ORDER BY shopping_list_items.checked ASC, shopping_list_items.sort_order ASC + `).all(listId); + + } + + addItem(data) { + + return db.prepare(` + INSERT INTO shopping_list_items + (list_id, product_id, amount) + VALUES (?, ?, ?) + `).run(data.list_id, data.product_id, data.amount); + + } + + findItem(listId, itemId) { + + return db.prepare(` + SELECT id FROM shopping_list_items + WHERE id = ? AND list_id = ? + `).get(itemId, listId); + + } + + toggleItem(itemId) { + + return db.prepare(` + UPDATE shopping_list_items + SET checked = CASE WHEN checked = 1 THEN 0 ELSE 1 END + WHERE id = ? + `).run(itemId); + + } + + deleteItem(itemId) { + + return db.prepare(` + DELETE FROM shopping_list_items + WHERE id = ? + `).run(itemId); + + } + + archiveList(listId) { + + return db.prepare(` + UPDATE shopping_lists + SET archived = 1 + WHERE id = ? + `).run(listId); + + } + +} + +module.exports = new ShoppingListRepository(); diff --git a/repositories/UserRepository.js b/repositories/UserRepository.js index 8a38e32..67be4c7 100644 --- a/repositories/UserRepository.js +++ b/repositories/UserRepository.js @@ -28,8 +28,20 @@ class UserRepository { } + getHouseholdMembers(householdId) { + + return db.prepare(` + SELECT users.id, users.name, users.email, household_members.role + FROM household_members + JOIN users ON users.id = household_members.user_id + WHERE household_members.household_id = ? + ORDER BY CASE household_members.role WHEN 'OWNER' THEN 0 ELSE 1 END, users.name + `).all(householdId); + + } + } -module.exports = new UserRepository(); \ No newline at end of file +module.exports = new UserRepository(); diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..ae7f8f3 --- /dev/null +++ b/routes/api.js @@ -0,0 +1,43 @@ +const router = require("express").Router(); + +const auth = require("../middleware/auth"); + +const ProductRepository = require("../repositories/ProductRepository"); +const ShoppingListRepository = require("../repositories/ShoppingListRepository"); + +// Get all products +router.get("/products", auth, (req, res) => { + + const products = ProductRepository.getAll(); + + res.json(products); + +}); + +// Get shopping lists for household +router.get("/lists", auth, (req, res) => { + + const lists = ShoppingListRepository.getByHousehold( + req.user.household_id + ); + + res.json(lists); + +}); + +// Get shopping list items +router.get("/lists/:id/items", auth, (req, res) => { + + const list = ShoppingListRepository.getById(req.params.id); + + if (!list || list.household_id !== req.user.household_id) { + return res.status(403).json({ error: "Not authorized" }); + } + + const items = ShoppingListRepository.getItems(req.params.id); + + res.json(items); + +}); + +module.exports = router; diff --git a/routes/auth.js b/routes/auth.js index 2fb4382..3a4737b 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -4,9 +4,25 @@ const router = require("express").Router(); const AuthController = require("../controllers/AuthController"); +const HomeController = +require("../controllers/HomeController"); + +// Home page +router.get( + "/home", + HomeController.index +); + +// Register router.get( "/register", + (req, res, next) => { + if (req.user) { + return res.redirect("/"); + } + next(); + }, AuthController.registerForm ); @@ -17,9 +33,15 @@ router.post( ); - +// Login router.get( "/login", + (req, res, next) => { + if (req.user) { + return res.redirect("/"); + } + next(); + }, AuthController.loginForm ); @@ -30,7 +52,7 @@ router.post( ); - +// Logout router.get( "/logout", AuthController.logout diff --git a/routes/web.js b/routes/web.js index 22d6ebf..0702f36 100644 --- a/routes/web.js +++ b/routes/web.js @@ -11,8 +11,8 @@ require("../controllers/HouseholdController"); const ProductController = require("../controllers/ProductController"); -console.log("AUTH:", auth); -console.log("DASHBOARD INDEX:", DashboardController.index); +const ShoppingListController = +require("../controllers/ShoppingListController"); router.get( "/", @@ -20,6 +20,42 @@ router.get( DashboardController.index ); +router.get( + "/lists", + auth, + ShoppingListController.index.bind(ShoppingListController) +); + +router.post( + "/lists", + auth, + ShoppingListController.create.bind(ShoppingListController) +); + +router.get( + "/lists/:id", + auth, + ShoppingListController.show.bind(ShoppingListController) +); + +router.post( + "/lists/:id/items", + auth, + ShoppingListController.addItem.bind(ShoppingListController) +); + +router.post( + "/lists/:id/items/:itemId/toggle", + auth, + ShoppingListController.toggleItem.bind(ShoppingListController) +); + +router.delete( + "/lists/:id/items/:itemId", + auth, + ShoppingListController.deleteItem.bind(ShoppingListController) +); + router.get( "/household", auth, @@ -27,12 +63,15 @@ router.get( ); router.get( - "/products", - auth, - ProductController.index.bind(ProductController) - ); + +router.post( + "/products", + auth, + ProductController.create.bind(ProductController) +); + module.exports = router; \ No newline at end of file diff --git a/sessions.sqlite b/sessions.sqlite index c3805c5cdd4e3022303ff32b3ce07d8681127d82..a62a3afb022f334d618c84ed4d6be1933fcf4240 100644 GIT binary patch literal 20480 zcmeHO+lwPv8Bh0g&!wl+8Qh0K*x3m|ScOha-7i^soFtw5rK>AdxeZIKq>@hMo=U26 z8Rnt%K~Y@T2VeXj1VK^R1z&yeO%W8Id=M9WT~J&E htuhaC@Zg)kT-3sJV$(Qqe zzu!4k=hXN6PT@GKPMiMLQ0vQ6KjFFO+1T(r@%ufV&D$Q2XB#|Mj}7o)YjptL;jzsF zw1NESw_e{rJ)1t-;CP{?)fTe*f3O)9;_CieHSCMSuPX zJf&>b&kLzsSg80jvC96b324!uE*$00Y)-##&PS@&{O^B*KYZiS=FR)j2OA!Bpfu-` zaSz%Hxd zZhpAM+)y^(-`?5ywde29?6rNEe0n?gZQ@{VEYxMPxu}lXshTv<269UqNx@}uoGmhi zy6`N}7!JE?GZ1Kq!EtI2}Z}AWDlE8^X8{ z$srs;R0I2gNwcAA;Eyf2H)-w%+SBRiXwX{(LQ_o#$0K>#1`RxdmU6#Y0R09Id-6Q+ zd_U0Y$rJGFv**t*b71l*3lfLxGn%SmJexX-8hV*%%@gDDOfAG&-ca<2Gmt2fS}!E% zz~ti=Bt60@NA+oT#2liEK~5X_)VL6CmHDcscJs-;1CVeAAu)Q5kX|@2`IrTXlxOXJ zmu(fW2_ZwAG*};+@b))m5fEq@DGLmCf?K#k4 zIEAp>T2HG}2jqp|O*X}OWBeX2fiM)7-aeAg7M-6r%*RBVN2~kW4kVsR` zUC%EYgO)E0MOc&tWAJUO!}^C}M8(KXzPR!fMqCrqo7f<176>}`?1ISQ}4gLfA9r%8{XFLPj>%t_v78)?Wuc*zQpeL_WtdAfA{g;&-VW4 zeRJ=juebN!?pJ-k@cr5Q18?}X%?7xEy9~GtxD2=qxD2=qxD2=qxD0$r82H+a+(vlD zRu)U9taih?EG7rJz9P-b6s*m3mY~r{=~rWTJw$?1~Sayj|9mgR2f zQoaa{q*;DZka%(tRoRZ%pC*M$BRmWRG|wQ66`Cr7kN~>dT3o z8Kz^$f)QJeMQD`KM$2q$KJF^X`dsgXi9$n854t5)YY(a8T(AI*!bEq`nXyW?86+3g zj-JW|OSwWTJJ4pssxHa`G)j$@jT62`Vf|7#wq%Ee>Y}4ex-Bw)#4+>cFlV*Y8pO+s zD$7T3g=;VKWTGSJ`bp0y<+_I25wg&zHSWulzy*gLhR>7)md~y3UG_q4>4y~4KQs+@yPi=N1{(D%@McOQ4oEU?y{%=#FcG(zo^?z6Yhb61$Yl@YgN}kE{4wp$@ zbl~d$S1e|A_5UEc8cw*Y|6gAFydnuy9Ju=bfnB=bswb-e`awE`iX~0Iv-dy)pGqhs~3w0bZ2AyP%xxvWjLH3#irs zv6y_xD(bV#WMB1UTit@fsx<;(sI`^ zWTz5b7ZMI89t=TItBT8sG5YeRn{x*yFItf7G>vOR!a|^UALSf7?Jht%b71lX3zD4z z;}9ftZQD6;3lc?xV)Lg0fRsA0(~BH{gt7<$%G;f~jd{hz53I*=gk|6^DR(d1wYKN| zZ*tjBE7lma6UVMQ22UqNnsx3<@rp4BCSVq!C^!MTJN2$U20>ce-KqD=wD!e~UoWJ9 zFXF~8K4(q9c7+Sqy;6X^$rOg*^x77)=MGFhYe5Rzlvy|d3G6SU*S4mBkWL+#e8z%g TR}|t5B$8U^CQ55xC{z6pTfGEC delta 250 zcmZozz}S#5L5ht*i-CcGd7^?H6NA>qga!Ouyg)u9zb^y7@5aKTd=nciG#blTSbdx7 zZKDFxy`4gmeL^zQ3 { + + const userId = socket.request.session?.userId; + const user = userId && UserRepository.findById(userId); + + if (!user || !user.household_id) { + socket.disconnect(true); + return; + } + + socket.join(`household:${user.household_id}`); + + console.log("🔌 Gebruiker verbonden:", socket.id); + + socket.on("join:household", () => { + // The room is derived from the authenticated session above. + }); + + socket.on("list:updated", (data) => { + + io.to(`household:${data.householdId}`).emit("list:updated", data); + + }); + + socket.on("item:added", (data) => { + + io.to(`household:${data.householdId}`).emit("item:added", data); + + }); + + socket.on("item:toggled", (data) => { + + io.to(`household:${data.householdId}`).emit("item:toggled", data); + + }); + + socket.on("item:removed", (data) => { + + io.to(`household:${data.householdId}`).emit("item:removed", data); + + }); + + socket.on("disconnect", () => { + + console.log("❌ Gebruiker verbroken:", socket.id); + + }); + + }); + +}; diff --git a/views/auth/login.ejs b/views/auth/login.ejs index 0835a5d..ed12ea3 100644 --- a/views/auth/login.ejs +++ b/views/auth/login.ejs @@ -1,55 +1,81 @@ -
+
-
+
+
-

-Inloggen -

+
+
-
+

🛒 PantryHub

+

Inloggen op je account

-
+
- + - +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ +
+ +

+ + Nog geen account? + + Registreren + +

+ +
+ +
+ +
+ +
-
- - - - - -
- - - - - - diff --git a/views/auth/register.ejs b/views/auth/register.ejs index 35d113b..db86ba1 100644 --- a/views/auth/register.ejs +++ b/views/auth/register.ejs @@ -1,73 +1,88 @@ -
+
-
+
+
-

-Account maken -

+
+
-
+

🛒 PantryHub

+

Account aanmaken

-
+
- + - +
+ + + + + +
+ +
+ + + + + +
+ +
+ + + + + +
+ + + +
+ +
+ +

+ + Al een account? + + Inloggen + +

+ +
+ +
+ +
+ +
- - - -
- - - - - -
- - - -
- - - - - -
- - - - - - - - - -
- -
\ No newline at end of file diff --git a/views/dashboard/index.ejs b/views/dashboard/index.ejs index 2a5a74c..d895908 100644 --- a/views/dashboard/index.ejs +++ b/views/dashboard/index.ejs @@ -1,17 +1,116 @@
-

-Welkom <%= user.name %> 👋 -

+

Welkom <%= user.name %> 👋

+

+ Huishouden: <%= user.household_name || "Geen huishouden" %> +

-

+

-Huishouden: - -<%= user.household_name || "Geen huishouden" %> - +
-

+
-
\ No newline at end of file +
+ +
+ +

📊 Statistieken

+ +
+ +
Boodschappenlijsten: <%= totalLists %>
+ + <% if(lists.length > 0) { %> + +
Gemiddeld progress: + <% + const avg = lists.length > 0 + ? Math.round(lists.reduce((sum, l) => sum + (l.checked_count || 0) / (l.item_count || 1), 0) / lists.length * 100) + : 0; + %> + <%= avg %>% +
+ + <% } %> + +
+ +
+ +
+ +
+ + + +
+ +<% if(lists.length > 0) { %> + +
+ +
+ +
+ +
+ +

📝 Recente lijsten

+ +
+ + + +
+ +
+ +
+ +<% } %> diff --git a/views/household/index.ejs b/views/household/index.ejs index b595bb0..67f6762 100644 --- a/views/household/index.ejs +++ b/views/household/index.ejs @@ -1,31 +1,88 @@ -
+ \ No newline at end of file +
+ +
+ +
+ +
+ +

Leden

+ +
+ +
+ + <% members.forEach(member => { %> +
+ +
+ +
+ <%= member.name %> +
<%= member.email %>
+
+ + <%= member.role %> + +
+ +
+ <% }) %> + +
+ +
+ +
+ +
+ +
+ +
+ +

Instellingen

+ +
+ +
+ + <% if(user.role === 'OWNER') { %> + +
+ + + +

Delen van deze code met anderen:

+ +
+ + + + + +
+ +
+ + <% } %> + +
+ +
+ +
+ +
diff --git a/views/layouts/main.ejs b/views/layouts/main.ejs index f19d0e1..8c6ec7c 100644 --- a/views/layouts/main.ejs +++ b/views/layouts/main.ejs @@ -1,5 +1,5 @@ - - + + @@ -7,37 +7,26 @@ - - -
- <%- include("../partials/navbar") %> +
-
+ <%- include("../partials/flash") %> + <%- body %> -
+
- <%- body %> - -
- - - <%- include("../partials/footer") %> - - -
- - -
- - - + <%- include("../partials/footer") %> + + + + + - \ No newline at end of file + diff --git a/views/lists/index.ejs b/views/lists/index.ejs new file mode 100644 index 0000000..c6c2d19 --- /dev/null +++ b/views/lists/index.ejs @@ -0,0 +1,96 @@ + + +
+ +
+ +
+ +
+ + <% if(lists.length === 0) { %> + +

Je hebt nog geen boodschappenlijsten. Maak er een aan!

+ + <% } else { %> + + + + <% } %> + +
+ +
+ +
+ +
+ +
+ +
+ +

Nieuwe lijst

+ +
+ +
+ +
+ +
+ + + + + +
+ + + +
+ +
+ +
+ +
+ +
diff --git a/views/lists/show.ejs b/views/lists/show.ejs new file mode 100644 index 0000000..d75c036 --- /dev/null +++ b/views/lists/show.ejs @@ -0,0 +1,221 @@ + + +
+ +
+ +
+ +
+ + <% if(items.length === 0) { %> + +

Nog geen producten op de lijst.

+ + <% } else { %> + +
+ + <% items.forEach(item => { %> + +
+ +
+ +
+ + + + + +
+ +
+ + style="text-decoration: line-through; color: #999;"<% } %>> + + <%= item.product_name %> + + + + <% if(item.amount && item.amount !== 1) { %> + + <%= item.amount %> + + <% } %> + +
+ + + + <%= item.category_icon || "" %> <%= item.category_name || "Geen categorie" %> + + + +
+ +
+ + + +
+ +
+ +
+ + <% }) %> + +
+ + <% } %> + +
+ +
+ +
+ +
+ +
+ +
+ +

Product toevoegen

+ +
+ +
+ +
+ +
+ + + + + + + + + + + +
+ +
+ + + + + +
+ + + +
+ +
+ +
+ +
+ +
+ + diff --git a/views/partials/flash.ejs b/views/partials/flash.ejs new file mode 100644 index 0000000..d0c3177 --- /dev/null +++ b/views/partials/flash.ejs @@ -0,0 +1,66 @@ +<% if(success.length) { %> + + + +<% } %> + + +<% if(error.length) { %> + + + +<% } %> + + +<% if(info.length) { %> + + + +<% } %> + + +<% if(warning.length) { %> + + + +<% } %> \ No newline at end of file diff --git a/views/partials/head.ejs b/views/partials/head.ejs index 2f3417a..77a32a5 100644 --- a/views/partials/head.ejs +++ b/views/partials/head.ejs @@ -2,17 +2,29 @@ + + + <%= title || "PantryHub" %> + - + + - \ No newline at end of file + + + + + diff --git a/views/partials/navbar.ejs b/views/partials/navbar.ejs index 347f89e..8ee021c 100644 --- a/views/partials/navbar.ejs +++ b/views/partials/navbar.ejs @@ -1,14 +1,99 @@ -