aboutsummaryrefslogtreecommitdiff
path: root/src/routes/grocery
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes/grocery')
-rw-r--r--src/routes/grocery/[id]/+page.server.ts71
-rw-r--r--src/routes/grocery/[id]/+page.svelte253
2 files changed, 324 insertions, 0 deletions
diff --git a/src/routes/grocery/[id]/+page.server.ts b/src/routes/grocery/[id]/+page.server.ts
new file mode 100644
index 0000000..88aa137
--- /dev/null
+++ b/src/routes/grocery/[id]/+page.server.ts
@@ -0,0 +1,71 @@
+import { error, fail, redirect } from '@sveltejs/kit';
+import { db } from '$lib/server/db';
+import { groceries, categories, references } from '$lib/server/db/schema';
+import { eq } from 'drizzle-orm';
+import type { Actions, PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ params }) => {
+ const id = Number(params.id);
+
+ const [item] = await db.select().from(groceries).where(eq(groceries.id, id));
+ if (!item) throw error(404, 'Item not found');
+
+ const [cat] = item.categoryId
+ ? await db
+ .select()
+ .from(categories)
+ .where(eq(categories.id, item.categoryId))
+ : [null];
+
+ const refs = await db
+ .select()
+ .from(references)
+ .where(eq(references.groceryId, id));
+
+ return {
+ item: {
+ ...item,
+ categoryName: cat?.name ?? null,
+ },
+ references: refs,
+ };
+};
+
+export const actions: Actions = {
+ add: async ({ request, params }) => {
+ const data = await request.formData();
+ const brand = data.get('brand')?.toString().trim();
+ const price = data.get('price')?.toString().trim();
+ const unit = data.get('unit')?.toString().trim();
+ const qualityScore = data.get('quality_score')?.toString().trim();
+
+ if (!brand) {
+ return fail(400, { error: 'Brand is required' });
+ }
+
+ await db.insert(references).values({
+ groceryId: Number(params.id),
+ brand,
+ price: price ? Number(price) : null,
+ unit: unit || null,
+ qualityScore: qualityScore ? Number(qualityScore) : null,
+ });
+
+ return { success: true };
+ },
+
+ delete: async ({ request }) => {
+ const data = await request.formData();
+ const id = Number(data.get('id'));
+
+ await db.delete(references).where(eq(references.id, id));
+
+ return { success: true };
+ },
+
+ deleteItem: async ({ params }) => {
+ await db.delete(groceries).where(eq(groceries.id, Number(params.id)));
+
+ throw redirect(303, '/');
+ },
+};
diff --git a/src/routes/grocery/[id]/+page.svelte b/src/routes/grocery/[id]/+page.svelte
new file mode 100644
index 0000000..15c4e8b
--- /dev/null
+++ b/src/routes/grocery/[id]/+page.svelte
@@ -0,0 +1,253 @@
+<script lang="ts">
+ import '../../../app.css';
+ import { enhance } from '$app/forms';
+ import { page } from '$app/stores';
+
+ let { data, form } = $props();
+ let brand = $state('');
+ let price = $state('');
+ let unit = $state('€/kg');
+ let qualityScore = $state('');
+</script>
+
+<h1>{data.item.name}</h1>
+
+{#if data.item.categoryName}
+ <p class="category">{data.item.categoryName}</p>
+{/if}
+
+<hr />
+
+<h2>References</h2>
+
+<form class="add-form" method="POST" action="?/add" use:enhance>
+ <label>
+ <span class="label">Brand</span>
+ <input
+ type="text"
+ name="brand"
+ bind:value={brand}
+ placeholder="e.g. Carte Noire"
+ autocomplete="off"
+ />
+ </label>
+ <div class="ref-row">
+ <label>
+ <span class="label">Price</span>
+ <input
+ type="number"
+ name="price"
+ bind:value={price}
+ placeholder="5.99"
+ step="0.01"
+ />
+ </label>
+ <label>
+ <span class="label">Unit</span>
+ <select name="unit" bind:value={unit}>
+ <option value="€/kg">€/kg</option>
+ <option value="€/L">€/L</option>
+ <option value="€/u">€/u</option>
+ </select>
+ </label>
+ <label>
+ <span class="label">Score</span>
+ <input
+ type="number"
+ name="quality_score"
+ bind:value={qualityScore}
+ placeholder="0-100"
+ min="0"
+ max="100"
+ />
+ </label>
+ </div>
+ <button type="submit">Add reference</button>
+</form>
+
+{#if form?.error}
+ <p class="error">{form.error}</p>
+{/if}
+
+<form method="POST" action="?/delete" use:enhance>
+ <ul class="item-list">
+ {#each data?.references ?? [] as ref (ref.id)}
+ <li class="ref-item">
+ <div class="ref-info">
+ <strong>{ref.brand}</strong>
+ {#if ref.price}
+ <span class="ref-detail"
+ >{ref.price.toFixed(2)}{ref.unit ? ` ${ref.unit}` : ''}</span
+ >
+ {/if}
+ {#if ref.qualityScore != null}
+ <span class="ref-detail">Score: {ref.qualityScore}/100</span>
+ {/if}
+ </div>
+ <button class="delete-btn" name="id" value={ref.id} type="submit">
+ &times;
+ </button>
+ </li>
+ {/each}
+ </ul>
+</form>
+
+{#if (data?.references?.length ?? 0) === 0}
+ <p class="empty">No references yet</p>
+{/if}
+
+<form method="POST" action="?/deleteItem" use:enhance>
+ <button type="submit" class="delete-item-btn">Delete item</button>
+</form>
+
+<a href="/" class="back-link">&larr; Back to list</a>
+
+<style>
+ .category {
+ color: #999;
+ font-size: 0.875rem;
+ }
+
+ .add-form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ margin-top: 0.5rem;
+ }
+
+ .add-form label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ }
+
+ .label {
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: #999;
+ }
+
+ input,
+ select {
+ padding: 0.75rem;
+ border: 1px solid #ccc;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-family: inherit;
+ background: #fff;
+ }
+
+ .ref-row {
+ display: flex;
+ gap: 0.5rem;
+ }
+
+ .ref-row label {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .add-form button[type='submit'] {
+ padding: 0.75rem 1.25rem;
+ border: none;
+ border-radius: 0.5rem;
+ background: #333;
+ color: #fff;
+ font-size: 1rem;
+ align-self: flex-start;
+ }
+
+ .error {
+ color: #d00;
+ margin-top: -0.5rem;
+ margin-bottom: 1rem;
+ }
+
+ hr {
+ border: none;
+ border-top: 1px solid #ddd;
+ margin: 1.5rem 0;
+ }
+
+ .item-list {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ }
+
+ .ref-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ border: 1px solid #e0e0e0;
+ border-radius: 0.5rem;
+ background: #fff;
+ }
+
+ .ref-info {
+ flex-grow: 1;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ align-items: baseline;
+ }
+
+ .ref-detail {
+ color: #666;
+ font-size: 0.875rem;
+ }
+
+ .delete-btn {
+ flex-shrink: 0;
+ width: 2rem;
+ aspect-ratio: 1;
+ padding: 0;
+ border: 1px solid #e0e0e0;
+ border-radius: 0.5rem;
+ background: #fff;
+ color: #d00;
+ font-size: 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: background 0.15s;
+ }
+
+ .delete-btn:hover {
+ background: #fee;
+ }
+
+ .empty {
+ color: #999;
+ font-size: 0.875rem;
+ margin-top: 0.5rem;
+ }
+
+ .back-link {
+ display: inline-block;
+ margin-top: 1.5rem;
+ color: #333;
+ text-decoration: none;
+ font-size: 0.875rem;
+ }
+
+ .delete-item-btn {
+ margin-top: 1.5rem;
+ padding: 0.5rem 1rem;
+ border: 1px solid #d00;
+ border-radius: 0.5rem;
+ background: #fff;
+ color: #d00;
+ font-size: 0.875rem;
+ cursor: pointer;
+ }
+
+ .delete-item-btn:hover {
+ background: #fee;
+ }
+</style>