Versie module 1 van 26-7

This commit is contained in:
2026-07-26 22:18:52 +02:00
commit 26a820b41a
39 changed files with 3663 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# PantryHub
Een moderne boodschappenapp gebouwd met:
- Node.js
- Express
- SQLite
- Socket.IO
- Bootstrap (Tabler)
- EJS
- PWA
## Installatie
```bash
npm install
npm run dev
```
Open daarna:
http://localhost:3000
## Features
- Huishoudens
- Gedeelde boodschappenlijsten
- Slimme productdatabase
- Realtime synchronisatie
- Offline ondersteuning
- Pushnotificaties
- Barcode scanner
+25
View File
@@ -0,0 +1,25 @@
require("dotenv").config();
const http = require("http");
const createApp = require("./config/express");
const app = createApp();
const server = http.createServer(app);
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log("");
console.log("===================================");
console.log("🚀 PantryHub gestart");
console.log(`🌍 http://localhost:${PORT}`);
console.log("===================================");
});
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
appName: "PantryHub",
port: process.env.PORT || 3000,
env: process.env.NODE_ENV,
database: process.env.DATABASE,
sessionSecret: process.env.SESSION_SECRET
};
+15
View File
@@ -0,0 +1,15 @@
const Database = require("better-sqlite3");
const path = require("path");
const databaseFile = path.join(
__dirname,
"..",
process.env.DATABASE || "database.sqlite"
);
const db = new Database(databaseFile);
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
module.exports = db;
+93
View File
@@ -0,0 +1,93 @@
const express = require("express");
const helmet = require("helmet");
const compression = require("compression");
const morgan = require("morgan");
const cookieParser = require("cookie-parser");
const path = require("path");
const expressLayouts = require("express-ejs-layouts");
const flash = require("connect-flash");
const userMiddleware = require("../middleware/user");
const session = require("./session");
module.exports = () => {
const app = express();
app.set("view engine", "ejs");
app.set(
"views",
path.join(__dirname, "../views")
);
app.use(expressLayouts);
app.set(
"layout",
"layouts/main"
);
app.use(
helmet({
contentSecurityPolicy: false
})
);
app.use(compression());
app.use(morgan("dev"));
app.use(cookieParser());
app.use(
express.urlencoded({
extended: true
})
);
app.use(express.json());
app.use(session);
app.use(userMiddleware);
app.use(flash());
app.use(
express.static(
path.join(__dirname, "../public")
)
);
app.use(
"/",
require("../routes/auth")
);
app.use(
"/",
require("../routes/web")
);
app.use((req, res) => {
res.status(404)
.render("errors/404");
});
return app;
};
+27
View File
@@ -0,0 +1,27 @@
const session = require("express-session");
const BetterSqlite3Store = require("better-sqlite3-session-store")(session);
const Database = require("better-sqlite3");
const db = new Database("sessions.sqlite");
module.exports = session({
store: new BetterSqlite3Store({
client: db,
expired: {
clear: true,
intervalMs: 900000
}
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 30
}
});
+165
View File
@@ -0,0 +1,165 @@
const bcrypt = require("bcrypt");
const db = require("../config/database");
const HouseholdService = require("../services/HouseholdService");
class AuthController {
registerForm(req, res) {
res.render("auth/register", {
title: "Registreren"
});
}
async register(req, res) {
const {
name,
email,
password
} = req.body;
const existingUser = db.prepare(
"SELECT id FROM users WHERE email = ?"
).get(email);
if (existingUser) {
req.flash(
"error",
"E-mailadres bestaat al"
);
return res.redirect("/register");
}
const passwordHash = await bcrypt.hash(
password,
12
);
const result = db.prepare(`
INSERT INTO users
(
name,
email,
password
)
VALUES (?, ?, ?)
`).run(
name,
email,
passwordHash
);
const userId = result.lastInsertRowid;
HouseholdService.createForUser(
userId,
`${name} huishouden`
);
req.session.userId = userId;
res.redirect("/");
}
loginForm(req, res) {
res.render("auth/login", {
title: "Inloggen"
});
}
async login(req, res) {
const {
email,
password
} = req.body;
const user = db.prepare(
"SELECT * FROM users WHERE email = ?"
).get(email);
if (!user) {
req.flash(
"error",
"Ongeldige gegevens"
);
return res.redirect("/login");
}
const valid = await bcrypt.compare(
password,
user.password
);
if (!valid) {
req.flash(
"error",
"Ongeldige gegevens"
);
return res.redirect("/login");
}
req.session.userId = user.id;
res.redirect("/");
}
logout(req, res) {
req.session.destroy(() => {
res.redirect("/login");
});
}
}
module.exports = new AuthController();
+25
View File
@@ -0,0 +1,25 @@
class DashboardController {
index(req, res) {
res.render(
"dashboard/index",
{
title: "Dashboard",
user: req.user
}
);
}
}
module.exports = new DashboardController();
+15
View File
@@ -0,0 +1,15 @@
class HomeController {
index(req, res) {
res.render("home/index", {
title: "PantryHub"
});
}
}
module.exports = new HomeController();
+21
View File
@@ -0,0 +1,21 @@
class HouseholdController {
index(req,res){
res.render(
"household/index",
{
title:"Huishouden",
user:req.user
}
);
}
}
module.exports =
new HouseholdController();
+35
View File
@@ -0,0 +1,35 @@
const ProductService =
require("../services/ProductService");
class ProductController {
index(req,res) {
const products =
ProductService.getProducts();
res.render(
"products/index",
{
title:"Producten",
products
}
);
}
}
module.exports =
new ProductController();
Binary file not shown.
View File
+214
View File
@@ -0,0 +1,214 @@
require("dotenv").config();
const db = require("../config/database");
console.log("Database migratie gestart...");
db.exec(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS households (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS household_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
household_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (household_id)
REFERENCES households(id)
ON DELETE CASCADE,
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
icon TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category_id INTEGER,
barcode TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(category_id)
REFERENCES categories(id)
);
CREATE TABLE IF NOT EXISTS shopping_lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
household_id INTEGER NOT NULL,
name TEXT NOT NULL,
archived INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(household_id)
REFERENCES households(id)
);
CREATE TABLE IF NOT EXISTS shopping_list_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
list_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
amount REAL DEFAULT 1,
checked INTEGER DEFAULT 0,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(list_id)
REFERENCES shopping_lists(id)
ON DELETE CASCADE,
FOREIGN KEY(product_id)
REFERENCES products(id)
);
CREATE TABLE IF NOT EXISTS stores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS product_favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, product_id),
FOREIGN KEY(user_id)
REFERENCES users(id)
ON DELETE CASCADE,
FOREIGN KEY(product_id)
REFERENCES products(id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS product_stores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL,
store_id INTEGER NOT NULL,
aisle TEXT,
FOREIGN KEY(product_id)
REFERENCES products(id)
ON DELETE CASCADE,
FOREIGN KEY(store_id)
REFERENCES stores(id)
ON DELETE CASCADE
);
`);
console.log("Database migratie voltooid.");
+107
View File
@@ -0,0 +1,107 @@
require("dotenv").config();
const db = require("../config/database");
const categories = [
["Groente", "🥬"],
["Fruit", "🍎"],
["Zuivel", "🥛"],
["Brood", "🍞"],
["Dranken", "🥤"],
["Huishouden", "🧻"]
];
const products = [
[
"Melk",
3
],
[
"Bananen",
2
],
[
"Brood",
4
],
[
"Koffie",
5
],
[
"WC papier",
6
]
];
const insertProduct = db.prepare(`
INSERT OR IGNORE INTO products
(
name,
category_id
)
VALUES (?,?)
`);
const seedProducts = db.transaction(() => {
for(const product of products){
insertProduct.run(product);
}
});
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.");
+13
View File
@@ -0,0 +1,13 @@
module.exports = function(req, res, next) {
if (!req.session.userId) {
return res.redirect("/login");
}
next();
};
+20
View File
@@ -0,0 +1,20 @@
const UserRepository =
require("../repositories/UserRepository");
module.exports = function userMiddleware(req, res, next) {
if (req.session.userId) {
req.user =
UserRepository.findById(
req.session.userId
);
}
next();
};
+2012
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
{
"name": "pantryhub",
"version": "1.0.0",
"description": "PantryHub - Realtime boodschappenapp",
"private": true,
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"db:migrate": "node database/migrate.js",
"db:seed": "node database/seed.js"
},
"keywords": [
"shopping",
"groceries",
"pwa",
"sqlite",
"express",
"socketio"
],
"author": "Jurgen",
"license": "MIT",
"dependencies": {
"bcrypt": "^6.0.0",
"better-sqlite3": "^13.0.1",
"better-sqlite3-session-store": "^0.1.0",
"compression": "^1.8.1",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.7",
"dotenv": "^17.4.2",
"ejs": "^6.0.1",
"express": "^5.2.1",
"express-ejs-layouts": "^2.5.1",
"express-session": "^1.19.0",
"express-validator": "^7.3.2",
"helmet": "^8.3.0",
"morgan": "^1.11.0",
"multer": "^2.2.0",
"socket.io": "^4.8.3",
"uuid": "^14.0.1"
},
"devDependencies": {
"nodemon": "^3.1.14"
}
}
+12
View File
@@ -0,0 +1,12 @@
body {
background-color: #f6f8fb;
}
.navbar-brand {
font-weight: 700;
}
+85
View File
@@ -0,0 +1,85 @@
const db = require("../config/database");
class ProductRepository {
getAll() {
return db.prepare(`
SELECT
products.*,
categories.name AS category_name,
categories.icon AS category_icon
FROM products
LEFT JOIN categories
ON categories.id = products.category_id
ORDER BY products.name
`).all();
}
findById(id) {
return db.prepare(`
SELECT *
FROM products
WHERE id = ?
`).get(id);
}
create(data) {
return db.prepare(`
INSERT INTO products
(
name,
category_id,
barcode
)
VALUES (?, ?, ?)
`).run(
data.name,
data.category_id || null,
data.barcode || null
);
}
}
module.exports = new ProductRepository();
+35
View File
@@ -0,0 +1,35 @@
const db = require("../config/database");
class UserRepository {
findById(id) {
return db.prepare(`
SELECT
users.*,
household_members.household_id,
household_members.role,
households.name AS household_name
FROM users
LEFT JOIN household_members
ON users.id = household_members.user_id
LEFT JOIN households
ON households.id = household_members.household_id
WHERE users.id = ?
`).get(id);
}
}
module.exports = new UserRepository();
+40
View File
@@ -0,0 +1,40 @@
const router = require("express").Router();
const AuthController =
require("../controllers/AuthController");
router.get(
"/register",
AuthController.registerForm
);
router.post(
"/register",
AuthController.register
);
router.get(
"/login",
AuthController.loginForm
);
router.post(
"/login",
AuthController.login
);
router.get(
"/logout",
AuthController.logout
);
module.exports = router;
+38
View File
@@ -0,0 +1,38 @@
const router = require("express").Router();
const auth = require("../middleware/auth");
const DashboardController =
require("../controllers/DashboardController");
const HouseholdController =
require("../controllers/HouseholdController");
const ProductController =
require("../controllers/ProductController");
console.log("AUTH:", auth);
console.log("DASHBOARD INDEX:", DashboardController.index);
router.get(
"/",
auth,
DashboardController.index
);
router.get(
"/household",
auth,
HouseholdController.index.bind(HouseholdController)
);
router.get(
"/products",
auth,
ProductController.index.bind(ProductController)
);
module.exports = router;
+59
View File
@@ -0,0 +1,59 @@
const db = require("../config/database");
class HouseholdService {
createForUser(userId, name) {
const create =
db.transaction(() => {
const household =
db.prepare(`
INSERT INTO households
(name)
VALUES (?)
`).run(name);
db.prepare(`
INSERT INTO household_members
(
household_id,
user_id,
role
)
VALUES (?, ?, ?)
`).run(
household.lastInsertRowid,
userId,
"OWNER"
);
return household.lastInsertRowid;
});
return create();
}
}
module.exports = new HouseholdService();
+26
View File
@@ -0,0 +1,26 @@
const ProductRepository =
require("../repositories/ProductRepository");
class ProductService {
getProducts() {
return ProductRepository.getAll();
}
createProduct(data) {
return ProductRepository.create(data);
}
}
module.exports =
new ProductService();
BIN
View File
Binary file not shown.
+58
View File
@@ -0,0 +1,58 @@
<div class="card">
<div class="card-body">
<h2>
Inloggen
</h2>
<form method="post">
<div class="mb-3">
<label class="form-label">
Email
</label>
<input
class="form-control"
type="email"
name="email"
required>
</div>
<div class="mb-3">
<label class="form-label">
Wachtwoord
</label>
<input
class="form-control"
type="password"
name="password"
required>
</div>
<button class="btn btn-primary">
Inloggen
</button>
</form>
</div>
</div>
+73
View File
@@ -0,0 +1,73 @@
<div class="card">
<div class="card-body">
<h2>
Account maken
</h2>
<form method="post">
<div class="mb-3">
<label class="form-label">
Naam
</label>
<input
class="form-control"
name="name"
required>
</div>
<div class="mb-3">
<label class="form-label">
Email
</label>
<input
class="form-control"
type="email"
name="email"
required>
</div>
<div class="mb-3">
<label class="form-label">
Wachtwoord
</label>
<input
class="form-control"
type="password"
name="password"
required>
</div>
<button class="btn btn-primary">
Registreren
</button>
</form>
</div>
</div>
+17
View File
@@ -0,0 +1,17 @@
<div class="mb-4">
<h1>
Welkom <%= user.name %> 👋
</h1>
<p class="text-secondary">
Huishouden:
<strong>
<%= user.household_name || "Geen huishouden" %>
</strong>
</p>
</div>
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<title>404</title>
<link
href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css"
rel="stylesheet">
</head>
<body>
<div class="container py-5">
<h1>404</h1>
<p>Pagina niet gevonden.</p>
<a
href="/"
class="btn btn-primary">
Terug
</a>
</div>
</body>
</html>
+69
View File
@@ -0,0 +1,69 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link
href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css"
rel="stylesheet">
</head>
<body>
<div class="page">
<div class="page-wrapper">
<div class="container-xl py-5">
<div class="text-center">
<h1 class="display-3">
🛒 PantryHub
</h1>
<p class="text-secondary">
De slimme boodschappenapp
</p>
<hr>
<a
href="/login"
class="btn btn-primary">
Inloggen
</a>
<a
href="/register"
class="btn btn-success">
Registreren
</a>
</div>
</div>
</div>
</div>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
<div class="card">
<div class="card-body">
<h2>
<%= user.household_name %>
</h2>
<p>
Rol:
<strong>
<%= user.role %>
</strong>
</p>
<button class="btn btn-primary">
Lid uitnodigen
</button>
</div>
</div>
+43
View File
@@ -0,0 +1,43 @@
<!doctype html>
<html lang="nl">
<head>
<%- include("../partials/head") %>
</head>
<body>
<div class="page">
<%- include("../partials/navbar") %>
<div class="page-wrapper">
<div class="container-xl">
<%- body %>
</div>
<%- include("../partials/footer") %>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js"></script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<footer class="footer footer-transparent">
<div class="container-xl">
<div class="text-center">
PantryHub © <%= new Date().getFullYear() %>
</div>
</div>
</footer>
+18
View File
@@ -0,0 +1,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
<%= title || "PantryHub" %>
</title>
<link
href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css"
rel="stylesheet">
<link
href="/css/style.css"
rel="stylesheet">
+14
View File
@@ -0,0 +1,14 @@
<header class="navbar navbar-expand-md navbar-light d-print-none">
<div class="container-xl">
<a class="navbar-brand" href="/">
🛒 PantryHub
</a>
</div>
</header>
+49
View File
@@ -0,0 +1,49 @@
<aside class="navbar navbar-vertical navbar-expand-lg">
<div class="container-fluid">
<div class="navbar-nav">
<a class="nav-link" href="/">
🏠 Dashboard
</a>
<a class="nav-link" href="/lists">
🛒 Lijsten
</a>
<a class="nav-link" href="/products">
📦 Producten
</a>
<a class="nav-link" href="/household">
👨‍👩‍👧 Huishouden
</a>
<a class="nav-link" href="/settings">
⚙ Instellingen
</a>
</div>
</div>
</aside>
+71
View File
@@ -0,0 +1,71 @@
<div class="page-header mb-4">
<h1>
📦 Producten
</h1>
</div>
<div class="card">
<div class="card-body">
<% if(products.length === 0) { %>
<p>
Nog geen producten.
</p>
<% } %>
<div class="list-group">
<% products.forEach(product => { %>
<div class="list-group-item">
<strong>
<%= product.name %>
</strong>
<br>
<span class="text-secondary">
<%= product.category_icon || "" %>
<%= product.category_name || "Geen categorie" %>
</span>
</div>
<% }) %>
</div>
</div>
</div>