diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-08 09:25:03 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-08 09:25:03 +0200 |
| commit | 3148742b323ab71f1ef768a2f4dc353c00944034 (patch) | |
| tree | 8f88990514fb58d11a8669b58a7265dbd3420fe0 /src | |
| parent | 7d85a18867ca0fe2310218c63cfc696c0f916171 (diff) | |
Share grocery row component and list-page form actions
- GroceryRow.svelte renders the toggle row (checkbox, name, best ref,
arrow link) for both the list and catalog pages
- grocery-actions.ts grows full form actions: addCategoryAction,
addGroceryAction, addGroceryAndEditAction, deleteByIdAction; the
categories/groceries route servers become thin wrappers
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/components/GroceryRow.svelte | 55 | ||||
| -rw-r--r-- | src/lib/server/services/grocery-actions.ts | 76 | ||||
| -rw-r--r-- | src/routes/groceries/+page.server.ts | 42 | ||||
| -rw-r--r-- | src/routes/groceries/+page.svelte | 22 | ||||
| -rw-r--r-- | src/routes/groceries/catalog/+page.svelte | 49 | ||||
| -rw-r--r-- | src/routes/groceries/categories/+page.server.ts | 30 | ||||
| -rw-r--r-- | src/routes/grocery/[id]/+page.server.ts | 13 |
7 files changed, 155 insertions, 132 deletions
diff --git a/src/lib/components/GroceryRow.svelte b/src/lib/components/GroceryRow.svelte new file mode 100644 index 0000000..3305afa --- /dev/null +++ b/src/lib/components/GroceryRow.svelte @@ -0,0 +1,55 @@ +<script lang="ts"> + import BestRef from '$lib/components/BestRef.svelte'; + import type { EnrichedGrocery } from '$lib/server/services/groceries'; + + let { + item, + from, + selectable = false, + checked = false, + oncheck, + }: { + item: EnrichedGrocery; + from: string; + selectable?: boolean; + checked?: boolean; + oncheck?: () => void; + } = $props(); +</script> + +<li class="item-row item-row--fixed"> + {#if selectable} + <input type="checkbox" class="item-check" {checked} onchange={oncheck} /> + {/if} + <button + class="item-btn item-btn--grow" + name="id" + value={item.id} + type="submit" + aria-pressed={item.checked} + > + <span class="item-text"> + <span class:checked={item.checked}>{item.name}</span> + {#if item.bestRef} + <BestRef ref={item.bestRef} /> + {/if} + </span> + </button> + <a class="ref-link" href="/grocery/{item.id}?from={from}">→</a> +</li> + +<style> + .item-check { + width: 1.25rem; + height: 1.25rem; + accent-color: var(--accent); + cursor: pointer; + flex-shrink: 0; + } + + .checked { + text-decoration: line-through; + color: var(--text-secondary); + font-weight: 400; + } +</style> diff --git a/src/lib/server/services/grocery-actions.ts b/src/lib/server/services/grocery-actions.ts index 0966c92..c8974e9 100644 --- a/src/lib/server/services/grocery-actions.ts +++ b/src/lib/server/services/grocery-actions.ts @@ -1,6 +1,7 @@ -import { fail } from '@sveltejs/kit'; -import { toggle } from './groceries'; -import { numericId } from './validation'; +import { fail, redirect, type ActionFailure } from '@sveltejs/kit'; +import { addOrUncheck, loadUnchecked, toggle } from './groceries'; +import { add as addCategory } from './categories'; +import { numericId, parseFormString, parseOptionalNumber } from './validation'; export async function toggleGroceryAction(request: Request) { const data = await request.formData(); @@ -12,3 +13,72 @@ export async function toggleGroceryAction(request: Request) { } return { success: true }; } + +export type DeleteRowResult = + ActionFailure<{ error: string }> | { success: boolean; type?: 'delete' }; + +/** + * Shared 'delete a row by its id form field' action, used by the category + * list and the reference list (both submit one form whose buttons carry the + * row id). + */ +export async function deleteByIdAction( + request: Request, + deleteFn: (id: number) => Promise<unknown>, + extra?: { type: 'delete' }, +): Promise<DeleteRowResult> { + const data = await request.formData(); + try { + await deleteFn(numericId(data.get('id'))); + } catch { + return fail(400, { error: 'ID invalide' }); + } + return { success: true, type: extra?.type }; +} + +/** Parses the required 'name' field of an add form, or returns the failure. */ +function requireFormName( + data: FormData, +): string | ActionFailure<{ error: string }> { + const name = parseFormString(data.get('name')); + if (!name) { + return fail(400, { error: 'Le nom est requis' }); + } + return name; +} + +export async function addCategoryAction(request: Request) { + const data = await request.formData(); + const name = requireFormName(data); + if (typeof name !== 'string') { + return name; + } + + await addCategory(name); + return { success: true }; +} + +export async function addGroceryAction(request: Request) { + const data = await request.formData(); + const name = requireFormName(data); + if (typeof name !== 'string') { + return name; + } + const categoryId = parseOptionalNumber(data.get('category_id')); + + await addOrUncheck(name, categoryId); + const { items } = await loadUnchecked(); + return { success: true, items }; +} + +export async function addGroceryAndEditAction(request: Request) { + const data = await request.formData(); + const name = requireFormName(data); + if (typeof name !== 'string') { + return name; + } + const categoryId = parseOptionalNumber(data.get('category_id')); + + const { item } = await addOrUncheck(name, categoryId); + throw redirect(303, `/grocery/${item.id}`); +} diff --git a/src/routes/groceries/+page.server.ts b/src/routes/groceries/+page.server.ts index d0e0759..1cae275 100644 --- a/src/routes/groceries/+page.server.ts +++ b/src/routes/groceries/+page.server.ts @@ -1,14 +1,9 @@ -import { fail, redirect, error } from '@sveltejs/kit'; +import { loadUnchecked, loadAll } from '$lib/server/services/groceries'; import { - loadUnchecked, - loadAll, - addOrUncheck, -} from '$lib/server/services/groceries'; -import { toggleGroceryAction } from '$lib/server/services/grocery-actions'; -import { - parseFormString, - parseOptionalNumber, -} from '$lib/server/services/validation'; + addGroceryAction, + addGroceryAndEditAction, + toggleGroceryAction, +} from '$lib/server/services/grocery-actions'; import type { EnrichedGrocery } from '$lib/server/services/groceries'; import type { Category } from '$lib/server/services/types'; import type { Actions, PageServerLoad } from './$types'; @@ -30,32 +25,9 @@ export const load: PageServerLoad = async () => { }; export const actions: Actions = { - add: async ({ request }) => { - const data = await request.formData(); - const name = parseFormString(data.get('name')); - const categoryId = parseOptionalNumber(data.get('category_id')); - - if (!name) { - return fail(400, { error: 'Le nom est requis' }); - } - - await addOrUncheck(name, categoryId); - const { items } = await loadUnchecked(); - return { success: true, items }; - }, + add: ({ request }) => addGroceryAction(request), toggle: ({ request }) => toggleGroceryAction(request), - addAndEdit: async ({ request }) => { - const data = await request.formData(); - const name = parseFormString(data.get('name')); - const categoryId = parseOptionalNumber(data.get('category_id')); - - if (!name) { - return fail(400, { error: 'Le nom est requis' }); - } - - const { item, created } = await addOrUncheck(name, categoryId); - throw redirect(303, `/grocery/${item.id}`); - }, + addAndEdit: ({ request }) => addGroceryAndEditAction(request), }; diff --git a/src/routes/groceries/+page.svelte b/src/routes/groceries/+page.svelte index 3646ba7..b134da7 100644 --- a/src/routes/groceries/+page.svelte +++ b/src/routes/groceries/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation'; import { enhance } from '$app/forms'; import FuzzyAutocomplete from '$lib/components/FuzzyAutocomplete.svelte'; - import BestRef from '$lib/components/BestRef.svelte'; + import GroceryRow from '$lib/components/GroceryRow.svelte'; import { groupByCategory } from '$lib/shared/group-by-category'; import type { EnrichedGrocery } from '$lib/server/services/groceries'; @@ -96,25 +96,7 @@ <h3 class="category-name">{cat}</h3> <ul class="item-list"> {#each items as item (item.id)} - <li class="item-row item-row--fixed"> - <button - class="item-btn item-btn--grow" - name="id" - value={item.id} - type="submit" - aria-pressed={item.checked} - > - <span class="item-text"> - {item.name} - {#if item.bestRef} - <BestRef ref={item.bestRef} /> - {/if} - </span> - </button> - <a class="ref-link" href="/grocery/{item.id}?from=/groceries" - >→</a - > - </li> + <GroceryRow {item} from="/groceries" /> {/each} </ul> {/each} diff --git a/src/routes/groceries/catalog/+page.svelte b/src/routes/groceries/catalog/+page.svelte index 59e2018..2b92a35 100644 --- a/src/routes/groceries/catalog/+page.svelte +++ b/src/routes/groceries/catalog/+page.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import { enhance } from '$app/forms'; import { SvelteSet } from 'svelte/reactivity'; - import BestRef from '$lib/components/BestRef.svelte'; + import GroceryRow from '$lib/components/GroceryRow.svelte'; import { groupByCategory } from '$lib/shared/group-by-category'; let { data, form } = $props(); @@ -79,32 +79,13 @@ <h3 class="category-name">{cat}</h3> <ul class="item-list"> {#each items as item (item.id)} - <li class="item-row item-row--fixed"> - <input - type="checkbox" - class="item-check" - checked={selectedIds.has(item.id)} - onchange={() => toggleId(item.id)} - /> - <button - class="item-btn item-btn--grow" - name="id" - value={item.id} - type="submit" - aria-pressed={item.checked} - > - <span class="item-text"> - <span class:checked={item.checked}>{item.name}</span> - {#if item.bestRef} - <BestRef ref={item.bestRef} /> - {/if} - </span> - </button> - <a - class="ref-link" - href="/grocery/{item.id}?from=/groceries/catalog">→</a - > - </li> + <GroceryRow + {item} + from="/groceries/catalog" + selectable + checked={selectedIds.has(item.id)} + oncheck={() => toggleId(item.id)} + /> {/each} </ul> {/each} @@ -150,20 +131,6 @@ box-shadow: none; } - .item-check { - width: 1.25rem; - height: 1.25rem; - accent-color: var(--accent); - cursor: pointer; - flex-shrink: 0; - } - - .checked { - text-decoration: line-through; - color: var(--text-secondary); - font-weight: 400; - } - .error { margin-bottom: 1rem; } diff --git a/src/routes/groceries/categories/+page.server.ts b/src/routes/groceries/categories/+page.server.ts index c97b619..d02b328 100644 --- a/src/routes/groceries/categories/+page.server.ts +++ b/src/routes/groceries/categories/+page.server.ts @@ -1,11 +1,14 @@ import { fail } from '@sveltejs/kit'; import { loadAll, - add, deleteCategory, reorder, } from '$lib/server/services/categories'; -import { numericId, parseFormString } from '$lib/server/services/validation'; +import { + addCategoryAction, + deleteByIdAction, +} from '$lib/server/services/grocery-actions'; +import { numericId } from '$lib/server/services/validation'; import type { Category } from '$lib/server/services/types'; import type { Actions, PageServerLoad } from './$types'; @@ -18,28 +21,9 @@ export const load: PageServerLoad = async () => { }; export const actions: Actions = { - add: async ({ request }) => { - const data = await request.formData(); - const name = parseFormString(data.get('name')); - - if (!name) { - return fail(400, { error: 'Le nom est requis' }); - } + add: ({ request }) => addCategoryAction(request), - await add(name); - return { success: true }; - }, - - delete: async ({ request }) => { - const data = await request.formData(); - try { - const id = numericId(data.get('id')); - await deleteCategory(id); - } catch { - return fail(400, { error: 'ID invalide' }); - } - return { success: true }; - }, + delete: ({ request }) => deleteByIdAction(request, deleteCategory), reorder: async ({ request }) => { const data = await request.formData(); diff --git a/src/routes/grocery/[id]/+page.server.ts b/src/routes/grocery/[id]/+page.server.ts index e1e2751..656e19c 100644 --- a/src/routes/grocery/[id]/+page.server.ts +++ b/src/routes/grocery/[id]/+page.server.ts @@ -9,6 +9,7 @@ import { deleteReference, } from '$lib/server/services/references'; import { updateGrocery, deleteGrocery } from '$lib/server/services/groceries'; +import { deleteByIdAction } from '$lib/server/services/grocery-actions'; import { numericId, parseFormString, @@ -68,16 +69,8 @@ export const actions: Actions = { return { success: true, type: 'add' as const }; }, - delete: async ({ request }) => { - const data = await request.formData(); - try { - const id = numericId(data.get('id')); - await deleteReference(id); - } catch { - return fail(400, { error: 'ID invalide' }); - } - return { success: true, type: 'delete' as const }; - }, + delete: ({ request }) => + deleteByIdAction(request, deleteReference, { type: 'delete' }), update: async ({ request, params }) => { const data = await request.formData(); |
