From e075698891d653330b5d5d0f80c2c3495aee6262 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Mon, 27 Jul 2026 19:53:11 +0200 Subject: feat: product references + grocery detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - references table (brand, price, unit, quality_score) with FK to groceries - /grocery/[id] page for managing references per item - Unit dropdown (€/kg, €/L, €/u) defaulting to €/kg - → link on all items navigates to detail page - Delete item moved from catalog to detail page --- src/lib/server/db/schema.ts | 13 +- src/routes/+page.server.ts | 9 -- src/routes/+page.svelte | 32 ++-- src/routes/grocery/[id]/+page.server.ts | 71 +++++++++ src/routes/grocery/[id]/+page.svelte | 253 ++++++++++++++++++++++++++++++++ 5 files changed, 350 insertions(+), 28 deletions(-) create mode 100644 src/routes/grocery/[id]/+page.server.ts create mode 100644 src/routes/grocery/[id]/+page.svelte (limited to 'src') diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 1d5fd9e..9cd6af8 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,4 +1,4 @@ -import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'; export const categories = sqliteTable('categories', { id: integer('id').primaryKey({ autoIncrement: true }), @@ -12,3 +12,14 @@ export const groceries = sqliteTable('groceries', { createdAt: text('created_at').notNull(), categoryId: integer('category_id').references(() => categories.id), }); + +export const references = sqliteTable('references', { + id: integer('id').primaryKey({ autoIncrement: true }), + groceryId: integer('grocery_id') + .notNull() + .references(() => groceries.id, { onDelete: 'cascade' }), + brand: text('brand').notNull(), + price: real('price'), + unit: text('unit'), + qualityScore: integer('quality_score'), +}); diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index e77b27f..6d547e6 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -75,13 +75,4 @@ export const actions: Actions = { return { success: true }; }, - - delete: async ({ request }) => { - const data = await request.formData(); - const id = Number(data.get('id')); - - await db.delete(groceries).where(eq(groceries.id, id)); - - return { success: true }; - }, }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9490a51..64c8410 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -57,10 +57,11 @@

{cat}

@@ -84,15 +85,7 @@ - + → {/each} @@ -191,20 +184,23 @@ align-items: center; } - .catalog-item .delete-btn { - border: 1px solid #e0e0e0; - border-radius: 0.5rem; - background: #fff; - color: #d00; - transition: background 0.15s; + .ref-link { width: 2rem; + aspect-ratio: 1; display: flex; align-items: center; justify-content: center; + border: 1px solid #e0e0e0; + border-radius: 0.5rem; + background: #fff; + color: #333; + text-decoration: none; + font-size: 1rem; + transition: background 0.15s; } - .catalog-item .delete-btn:hover { - background: #fee; + .ref-link:hover { + background: #f5f5f5; } .empty { 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 @@ + + +

{data.item.name}

+ +{#if data.item.categoryName} +

{data.item.categoryName}

+{/if} + +
+ +

References

+ +
+ +
+ + + +
+ +
+ +{#if form?.error} +

{form.error}

+{/if} + +
+ +
+ +{#if (data?.references?.length ?? 0) === 0} +

No references yet

+{/if} + +
+ +
+ +← Back to list + + -- cgit v1.3