Versie module 1 van 26-7
This commit is contained in:
@@ -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
|
||||||
@@ -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("===================================");
|
||||||
|
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class DashboardController {
|
||||||
|
|
||||||
|
|
||||||
|
index(req, res) {
|
||||||
|
|
||||||
|
|
||||||
|
res.render(
|
||||||
|
"dashboard/index",
|
||||||
|
{
|
||||||
|
|
||||||
|
title: "Dashboard",
|
||||||
|
|
||||||
|
user: req.user
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = new DashboardController();
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
class HomeController {
|
||||||
|
|
||||||
|
index(req, res) {
|
||||||
|
|
||||||
|
res.render("home/index", {
|
||||||
|
|
||||||
|
title: "PantryHub"
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new HomeController();
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
class HouseholdController {
|
||||||
|
|
||||||
|
|
||||||
|
index(req,res){
|
||||||
|
|
||||||
|
res.render(
|
||||||
|
"household/index",
|
||||||
|
{
|
||||||
|
title:"Huishouden",
|
||||||
|
user:req.user
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
new HouseholdController();
|
||||||
@@ -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.
@@ -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.");
|
||||||
@@ -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.");
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = function(req, res, next) {
|
||||||
|
|
||||||
|
|
||||||
|
if (!req.session.userId) {
|
||||||
|
|
||||||
|
return res.redirect("/login");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
next();
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
};
|
||||||
Generated
+2012
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
body {
|
||||||
|
|
||||||
|
background-color: #f6f8fb;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
Binary file not shown.
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<footer class="footer footer-transparent">
|
||||||
|
|
||||||
|
<div class="container-xl">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
|
||||||
|
PantryHub © <%= new Date().getFullYear() %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user