From 3148742b323ab71f1ef768a2f4dc353c00944034 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Tue, 8 Sep 2026 09:25:03 +0200 Subject: 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 --- src/lib/components/GroceryRow.svelte | 55 ++++++++++++++++++ src/lib/server/services/grocery-actions.ts | 76 ++++++++++++++++++++++++- src/routes/groceries/+page.server.ts | 42 +++----------- src/routes/groceries/+page.svelte | 22 +------ src/routes/groceries/catalog/+page.svelte | 49 +++------------- src/routes/groceries/categories/+page.server.ts | 30 +++------- src/routes/grocery/[id]/+page.server.ts | 13 +---- 7 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 src/lib/components/GroceryRow.svelte (limited to 'src') 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 @@ + + +
  • + {#if selectable} + + {/if} + + → +
  • + + 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, + extra?: { type: 'delete' }, +): Promise { + 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 @@

    {cat}

      {#each items as item (item.id)} -
    • - - → -
    • + {/each}
    {/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 @@