swipe, zoeken op producten, beter toevoegen in lijsten

This commit is contained in:
2026-09-06 20:44:04 +02:00
parent ce07575d5e
commit a4d61ca26c
13 changed files with 437 additions and 157 deletions
+21 -2
View File
@@ -4,12 +4,30 @@
<div class="card">
<div class="card-body">
<div class="row g-3 mb-4" data-product-filter>
<div class="col-md-8">
<label class="form-label" for="favorite-search">Zoek op naam</label>
<input id="favorite-search" class="form-control" type="search" placeholder="Bijv. melk" data-product-search>
</div>
<div class="col-md-4">
<label class="form-label" for="favorite-category-filter">Filter op categorie</label>
<select id="favorite-category-filter" class="form-select" data-product-category>
<option value="">Alle categorieën</option>
<% (categories || []).forEach(category => { %>
<option value="<%= category.id %>"><%= category.icon || "" %> <%= category.name %></option>
<% }); %>
</select>
</div>
</div>
<% if (favorites.length === 0) { %>
<p class="text-secondary">Je hebt nog geen favoriete producten.</p>
<% } else { %>
<div class="list-group">
<div class="list-group" data-product-list>
<% favorites.forEach(product => { %>
<div class="list-group-item d-flex justify-content-between align-items-center gap-3">
<div class="list-group-item d-flex justify-content-between align-items-center gap-3"
data-product-row
data-product-name="<%= product.name.toLowerCase() %>"
data-product-category="<%= product.category_id || '' %>">
<div>
<strong><%= product.category_icon || "" %> <%= product.name %></strong>
<div class="text-secondary"><%= product.category_name || "Geen categorie" %></div>
@@ -21,6 +39,7 @@
</div>
<% }); %>
</div>
<p class="text-secondary mt-3 d-none" data-product-empty>Geen favorieten gevonden.</p>
<% } %>
</div>
</div>
+1 -1
View File
@@ -30,7 +30,7 @@
<div class="text-secondary small"><%= member.email %></div>
</div>
<span class="badge bg-primary"><%= member.role %></span>
<span class="badge bg-primary text-white"><%= member.role %></span>
</div>
+1 -1
View File
@@ -38,7 +38,7 @@
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/js/app.js"></script>
<script src="/js/app.js?v=10"></script>
</body>
+77 -72
View File
@@ -3,85 +3,90 @@
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
<h1 class="mb-0"><%= list.name %></h1>
<div class="d-flex flex-wrap gap-2">
<form method="post" action="/lists/<%= list.id %>/favorites/add"
onsubmit="return confirm('Alle favorieten toevoegen aan deze lijst?');">
<button class="btn btn-outline-primary" type="submit">Alle favorieten toevoegen</button>
</form>
<form method="post" action="/lists/<%= list.id %>/items/reset"
onsubmit="return confirm('Alle producten weer openzetten?');">
<button class="btn btn-outline-primary" type="submit">Alles weer openzetten</button>
</form>
<form method="post" action="/lists/<%= list.id %>/items/clear"
onsubmit="return confirm('Weet je zeker dat je alle producten uit deze lijst wilt verwijderen?');">
<button class="btn btn-outline-danger" type="submit">Alles verwijderen</button>
</form>
<form method="post" action="/lists/<%= list.id %>/favorites/add" onsubmit="return confirm('Alle favorieten toevoegen aan deze lijst?');"><button class="btn btn-outline-primary" type="submit">Alle favorieten toevoegen</button></form>
<form method="post" action="/lists/<%= list.id %>/items/reset" onsubmit="return confirm('Alle producten weer openzetten?');"><button class="btn btn-outline-primary" type="submit">Alles weer openzetten</button></form>
<form method="post" action="/lists/<%= list.id %>/items/clear" onsubmit="return confirm('Weet je zeker dat je alle producten uit deze lijst wilt verwijderen?');"><button class="btn btn-outline-danger" type="submit">Alles verwijderen</button></form>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<% if (items.length === 0) { %>
<p class="text-secondary">Nog geen producten op de lijst.</p>
<% } else { %>
<div class="list-group">
<% items.forEach(item => { %>
<div class="list-group-item d-flex align-items-center">
<form method="post" action="/lists/<%= list.id %>/items/<%= item.id %>/toggle">
<button class="btn btn-sm me-2 list-check-button" type="submit"
aria-label="Product <%= item.checked ? 'weer openzetten' : 'afvinken' %>"
<% if (item.checked) { %>onclick="return confirm('Product weer openzetten?');"<% } %>>
<%= item.checked ? "☑" : "☐" %>
</button>
</form>
<div class="flex-grow-1">
<strong<% if (item.checked) { %> style="text-decoration: line-through"<% } %>>
<%= item.product_name %>
</strong>
<% if (item.amount !== 1) { %><span class="badge bg-info text-white"><%= item.amount %></span><% } %>
<div class="small text-secondary"><%= item.category_icon || "" %> <%= item.category_name || "Geen categorie" %></div>
</div>
<form class="delete-item-form" method="post" action="/lists/<%= list.id %>/items/<%= item.id %>">
<button class="btn btn-sm btn-link text-danger" type="submit">Verwijder</button>
</form>
<div class="card mb-4">
<div class="card-header"><h3 class="card-title">Product toevoegen</h3></div>
<div class="card-body">
<form method="post" action="/lists/<%= list.id %>/items" class="row g-3 align-items-end" data-product-filter>
<div class="col-md-5">
<label class="form-label" for="product-search">Zoek op naam</label>
<input id="product-search" class="form-control" type="search" placeholder="Bijv. melk" data-product-search>
</div>
<div class="col-md-3">
<label class="form-label" for="product-category-filter">Filter op categorie</label>
<select id="product-category-filter" class="form-select" data-product-category>
<option value="">Alle categorieën</option>
<% (categories || []).forEach(category => { %><option value="<%= category.id %>"><%= category.icon || "" %> <%= category.name %></option><% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="product-id">Product</label>
<select id="product-id" class="form-select" name="product_id" required data-product-select>
<option value="">Kies een product</option>
<% (products || []).forEach(product => { %><option value="<%= product.id %>" data-product-option data-product-name="<%= product.name.toLowerCase() %>" data-product-category="<%= product.category_id || '' %>"><%= product.category_icon || "" %> <%= product.name %></option><% }); %>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="amount">Hoeveelheid</label>
<input id="amount" class="form-control" type="number" name="amount" value="1" min="0.5" step="0.5" required>
</div>
<div class="col-md-3"><button class="btn btn-primary w-100" type="submit">Toevoegen</button></div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<% if (items.length === 0) { %><p class="text-secondary">Nog geen producten op de lijst.</p><% } else { %>
<div class="list-group">
<% items.forEach(item => { %>
<div class="shopping-list-swipe">
<div class="swipe-action swipe-action-delete" aria-label="Product verwijderen"><span class="swipe-action-icon">🗑</span><span>Verwijder</span></div>
<div class="swipe-action swipe-action-check" aria-label="Product afvinken"><span class="swipe-action-icon">☑</span><span>Afvinken</span></div>
<div class="list-group-item d-flex align-items-center shopping-list-item"
data-item-id="<%= item.id %>"
data-product-name="<%= item.product_name %>"
data-item-amount="<%= item.amount %>"
data-toggle-url="/lists/<%= list.id %>/items/<%= item.id %>/toggle"
data-delete-url="/lists/<%= list.id %>/items/<%= item.id %>"
role="button" tabindex="0">
<div class="flex-grow-1">
<strong<% if (item.checked) { %> style="text-decoration: line-through"<% } %>><%= item.product_name %></strong>
<div class="small text-secondary"><%= item.category_icon || "" %> <%= item.category_name || "Geen categorie" %></div>
</div>
<% }); %>
<span class="badge bg-info text-white list-item-amount"><%= item.amount %></span>
</div>
</div>
<% } %>
<% }); %>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header"><h3 class="card-title">Product toevoegen</h3></div>
<div class="card-body">
<form method="post" action="/lists/<%= list.id %>/items">
<label class="form-label" for="product-id">Product</label>
<select id="product-id" class="form-select mb-3" name="product_id" required>
<option value="">Kies een product</option>
<% (products || []).forEach(product => { %>
<option value="<%= product.id %>"><%= product.name %></option>
<% }); %>
</select>
<label class="form-label" for="amount">Hoeveelheid</label>
<input id="amount" class="form-control mb-3" type="number" name="amount"
value="1" min="0.5" step="0.5" required>
<button class="btn btn-primary w-100" type="submit">Toevoegen</button>
</form>
</div>
</div>
<% } %>
</div>
</div>
<script>
document.querySelectorAll(".delete-item-form").forEach((form) => {
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (!confirm("Product verwijderen?")) return;
const response = await fetch(form.action, { method: "DELETE" });
if (response.ok) window.location.reload();
});
});
</script>
<div class="modal fade" id="amount-modal" tabindex="-1" aria-labelledby="amount-modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="amount-form" method="post">
<div class="modal-header">
<h2 class="modal-title fs-3" id="amount-modal-title">Hoeveelheid wijzigen</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Sluiten"></button>
</div>
<div class="modal-body">
<p class="text-secondary mb-3" id="amount-product-name"></p>
<label class="form-label" for="modal-amount">Hoeveelheid</label>
<input id="modal-amount" class="form-control" type="number" name="amount" min="0.5" step="0.5" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" class="btn btn-primary">Opslaan</button>
</div>
</form>
</div>
</div>
</div>
+3 -1
View File
@@ -12,6 +12,8 @@
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg">
<link rel="apple-touch-icon" href="/icons/icon.svg">
@@ -32,5 +34,5 @@ if ("serviceWorker" in navigator) {
<link
href="/css/style.css"
href="/css/style.css?v=4"
rel="stylesheet">
+72 -72
View File
@@ -1,82 +1,82 @@
<div class="page-header mb-4">
<h1>
📦 Producten
</h1>
<h1>📦 Producten</h1>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<% if (products.length === 0) { %>
<p class="text-secondary">Nog geen producten.</p>
<% } else { %>
<div class="list-group">
<% products.forEach(product => { %>
<div class="list-group-item">
<div class="d-flex align-items-start justify-content-between gap-3">
<div>
<strong><%= product.category_icon || "" %> <%= product.name %></strong>
<div class="text-secondary"><%= product.category_name || "Geen categorie" %></div>
</div>
<div class="d-flex align-items-center gap-2">
<form method="post" action="/products/<%= product.id %>/favorite">
<button class="btn btn-sm <%= product.is_favorite ? 'btn-danger' : 'btn-outline-secondary' %>"
type="submit"
aria-label="<%= product.is_favorite ? 'Verwijder uit favorieten' : 'Toevoegen aan favorieten' %>">
<%= product.is_favorite ? '♥' : '♡' %>
</button>
</form>
<a class="btn btn-sm btn-outline-primary" href="/products/<%= product.id %>/edit">
Wijzigen
</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h3 class="card-title">Nieuw product</h3></div>
<div class="card-body">
<form method="post" action="/products" class="row g-3 align-items-end">
<div class="col-md-5">
<label class="form-label" for="product-name">Productnaam</label>
<input id="product-name" class="form-control" type="text" name="name" placeholder="bijv. Melk" maxlength="120" required>
</div>
<div class="col-md-4">
<label class="form-label" for="category-id">Categorie</label>
<select id="category-id" class="form-select" name="category_id">
<option value="">Geen categorie</option>
<% (categories || []).forEach(category => { %>
<option value="<%= category.id %>"><%= category.icon || "" %> <%= category.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-3"><button class="btn btn-primary w-100" type="submit">Product toevoegen</button></div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="row g-3 mb-4" data-product-filter>
<div class="col-md-8">
<label class="form-label" for="product-search">Zoek op naam</label>
<input id="product-search" class="form-control" type="search" placeholder="Bijv. melk" data-product-search>
</div>
<div class="col-md-4">
<label class="form-label" for="product-category-filter">Filter op categorie</label>
<select id="product-category-filter" class="form-select" data-product-category>
<option value="">Alle categorieën</option>
<% (categories || []).forEach(category => { %>
<option value="<%= category.id %>"><%= category.icon || "" %> <%= category.name %></option>
<% }); %>
</select>
</div>
</div>
<% if (products.length === 0) { %>
<p class="text-secondary">Nog geen producten.</p>
<% } else { %>
<div class="list-group" data-product-list>
<% products.forEach(product => { %>
<div class="list-group-item" data-product-row data-product-name="<%= product.name.toLowerCase() %>" data-product-category="<%= product.category_id || '' %>">
<div class="d-flex align-items-start justify-content-between gap-3">
<div>
<strong><%= product.category_icon || "" %> <%= product.name %></strong>
<div class="text-secondary"><%= product.category_name || "Geen categorie" %></div>
</div>
<% }); %>
<div class="d-flex align-items-center gap-2">
<form method="post" action="/products/<%= product.id %>/favorite">
<button class="btn btn-sm <%= product.is_favorite ? 'btn-danger' : 'btn-outline-secondary' %>" type="submit" aria-label="<%= product.is_favorite ? 'Verwijder uit favorieten' : 'Toevoegen aan favorieten' %>"><%= product.is_favorite ? '♥' : '♡' %></button>
</form>
<a class="btn btn-sm btn-outline-primary" href="/products/<%= product.id %>/edit">Wijzigen</a>
</div>
</div>
</div>
<% } %>
<% }); %>
</div>
</div>
<p class="text-secondary mt-3 d-none" data-product-empty>Geen producten gevonden.</p>
<% } %>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header"><h3 class="card-title">Nieuw product</h3></div>
<div class="card-body">
<form method="post" action="/products">
<label class="form-label" for="product-name">Productnaam</label>
<input id="product-name" class="form-control mb-3" type="text" name="name"
placeholder="bijv. Melk" maxlength="120" required>
<label class="form-label" for="category-id">Categorie</label>
<select id="category-id" class="form-select mb-3" name="category_id">
<option value="">Geen categorie</option>
<% (categories || []).forEach(category => { %>
<option value="<%= category.id %>"><%= category.icon || "" %> <%= category.name %></option>
<% }); %>
</select>
<button class="btn btn-primary w-100" type="submit">Product toevoegen</button>
</form>
<div class="card mt-4">
<div class="card-header"><h3 class="card-title">Producten in bulk laden</h3></div>
<div class="card-body">
<form method="post" action="/products/import" enctype="multipart/form-data" class="row g-3 align-items-end">
<div class="col-md-9">
<label class="form-label" for="product-import-file">CSV-bestand</label>
<input id="product-import-file" class="form-control" type="file" name="file" accept=".csv,text/csv" required>
</div>
</div>
<div class="card mt-3">
<div class="card-header"><h3 class="card-title">Producten in bulk laden</h3></div>
<div class="card-body">
<form method="post" action="/products/import" enctype="multipart/form-data">
<label class="form-label" for="product-import-file">CSV-bestand</label>
<input id="product-import-file" class="form-control mb-3" type="file" name="file"
accept=".csv,text/csv" required>
<div class="text-secondary small mb-3">Kolommen: name, category, barcode. Alleen name is verplicht.</div>
<button class="btn btn-outline-primary w-100" type="submit">CSV importeren</button>
</form>
</div>
</div>
<div class="col-md-3"><button class="btn btn-outline-primary w-100" type="submit">CSV importeren</button></div>
</form>
<div class="text-secondary small mt-2">Kolommen: name, category, barcode. Alleen name is verplicht.</div>
</div>
</div>
</div>