diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-07-27 18:31:26 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-07-27 18:31:26 +0200 |
| commit | e14717b2e7853705469275d64023bb902b195ebb (patch) | |
| tree | 2e450f7b4e864e232d06df43ad1aba1b62be4f4c /src/routes/categories | |
| parent | 8224c7d6572db6ec76c4224eec1df8a538b47ad4 (diff) | |
feat: separate categories table + /categories management page
- Categories table (id, name unique) with FK from groceries.category_id
- /categories page for add/delete categories
- Main page uses native <select> dropdown for category
- Categories grouped by name in list/catalog views
- Regenerated drizzle migrations from scratch
Diffstat (limited to 'src/routes/categories')
| -rw-r--r-- | src/routes/categories/+page.server.ts | 38 | ||||
| -rw-r--r-- | src/routes/categories/+page.svelte | 134 |
2 files changed, 172 insertions, 0 deletions
diff --git a/src/routes/categories/+page.server.ts b/src/routes/categories/+page.server.ts new file mode 100644 index 0000000..241370e --- /dev/null +++ b/src/routes/categories/+page.server.ts @@ -0,0 +1,38 @@ +import { fail } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { categories } from '$lib/server/db/schema'; +import { eq, asc } from 'drizzle-orm'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + const items = await db + .select() + .from(categories) + .orderBy(asc(categories.name)); + + return { categories: items }; +}; + +export const actions: Actions = { + add: async ({ request }) => { + const data = await request.formData(); + const name = data.get('name')?.toString().trim(); + + if (!name) { + return fail(400, { error: 'Name is required' }); + } + + await db.insert(categories).values({ name }); + + return { success: true }; + }, + + delete: async ({ request }) => { + const data = await request.formData(); + const id = Number(data.get('id')); + + await db.delete(categories).where(eq(categories.id, id)); + + return { success: true }; + }, +}; diff --git a/src/routes/categories/+page.svelte b/src/routes/categories/+page.svelte new file mode 100644 index 0000000..0dfc96c --- /dev/null +++ b/src/routes/categories/+page.svelte @@ -0,0 +1,134 @@ +<script lang="ts"> + import '../../app.css'; + import { enhance } from '$app/forms'; + + let { data, form } = $props(); + let name = $state(''); +</script> + +<h1>Categories</h1> + +<form class="add-form" method="POST" action="?/add" use:enhance> + <label> + <span class="label">New category</span> + <input + type="text" + name="name" + bind:value={name} + placeholder="e.g. Boissons" + autocomplete="off" + /> + </label> + <button type="submit">Add</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?.categories ?? [] as cat (cat.id)} + <li class="catalog-item"> + <span class="cat-name">{cat.name}</span> + <button class="delete-btn" name="id" value={cat.id} type="submit"> + × + </button> + </li> + {/each} + </ul> +</form> + +{#if (data?.categories?.length ?? 0) === 0} + <p class="empty">No categories yet</p> +{/if} + +<style> + .add-form { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; + } + + .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 { + padding: 0.75rem; + border: 1px solid #ccc; + border-radius: 0.5rem; + font-size: 1rem; + } + + .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; + } + + .item-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .catalog-item { + display: flex; + gap: 0.25rem; + height: 2rem; + align-items: center; + } + + .cat-name { + flex-grow: 1; + padding: 0 0.75rem; + } + + .delete-btn { + border: 1px solid #e0e0e0; + border-radius: 0.5rem; + background: #fff; + color: #d00; + transition: background 0.15s; + width: 2rem; + aspect-ratio: 1; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + cursor: pointer; + } + + .delete-btn:hover { + background: #fee; + } + + .empty { + color: #999; + font-size: 0.875rem; + margin-top: 0.5rem; + } +</style> |
