From 113415ce357a9cea1759d0452ebae6352f8fe36b Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Mon, 7 Sep 2026 10:55:06 +0200 Subject: Split groceries domain out of the app root Move the grocery list, catalog, and categories routes under /groceries/ so the app home can host the projects/routines overview next. Update nav to exact-path matching, login redirect default, and layout title. --- src/routes/groceries/+page.server.ts | 71 ++++++++ src/routes/groceries/+page.svelte | 207 +++++++++++++++++++++++ src/routes/groceries/catalog/+page.server.ts | 49 ++++++ src/routes/groceries/catalog/+page.svelte | 211 ++++++++++++++++++++++++ src/routes/groceries/categories/+page.server.ts | 53 ++++++ src/routes/groceries/categories/+page.svelte | 164 ++++++++++++++++++ 6 files changed, 755 insertions(+) create mode 100644 src/routes/groceries/+page.server.ts create mode 100644 src/routes/groceries/+page.svelte create mode 100644 src/routes/groceries/catalog/+page.server.ts create mode 100644 src/routes/groceries/catalog/+page.svelte create mode 100644 src/routes/groceries/categories/+page.server.ts create mode 100644 src/routes/groceries/categories/+page.svelte (limited to 'src/routes/groceries') diff --git a/src/routes/groceries/+page.server.ts b/src/routes/groceries/+page.server.ts new file mode 100644 index 0000000..e462ab2 --- /dev/null +++ b/src/routes/groceries/+page.server.ts @@ -0,0 +1,71 @@ +import { fail, redirect, error } from '@sveltejs/kit'; +import { + loadUnchecked, + loadAll, + toggle, + addOrUncheck, +} from '$lib/server/services/groceries'; +import { + numericId, + parseFormString, + parseOptionalNumber, +} from '$lib/server/services/validation'; +import type { EnrichedGrocery } from '$lib/server/services/groceries'; +import type { Category } from '$lib/server/services/types'; +import type { Actions, PageServerLoad } from './$types'; + +export interface GroceriesOutput { + items: EnrichedGrocery[]; + categories: Category[]; + catalog: EnrichedGrocery[]; +} + +export const load: PageServerLoad = async () => { + const unchecked = await loadUnchecked(); + const all = await loadAll(); + return { + items: unchecked.items, + categories: unchecked.categories, + catalog: all.items, + }; +}; + +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 }; + }, + + toggle: async ({ request }) => { + const data = await request.formData(); + try { + const id = numericId(data.get('id')); + await toggle(id); + } catch { + return fail(400, { error: 'ID invalide' }); + } + return { success: true }; + }, + + 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}`); + }, +}; diff --git a/src/routes/groceries/+page.svelte b/src/routes/groceries/+page.svelte new file mode 100644 index 0000000..4471738 --- /dev/null +++ b/src/routes/groceries/+page.svelte @@ -0,0 +1,207 @@ + + +
+ { + name = item.name; + handleAdd(false); + }} + /> + + +
+ +
+ {#if addError || form?.error} +

{addError || form?.error}

+ {/if} + + {#if localItems.length === 0} +

Rien à acheter

+ {:else} +
+ {#each groupByCategory(localItems) as [cat, items] (cat)} +

{cat}

+
    + {#each items as item (item.id)} +
  • + + → +
  • + {/each} +
+ {/each} +
+ {/if} +
+ + diff --git a/src/routes/groceries/catalog/+page.server.ts b/src/routes/groceries/catalog/+page.server.ts new file mode 100644 index 0000000..22464dc --- /dev/null +++ b/src/routes/groceries/catalog/+page.server.ts @@ -0,0 +1,49 @@ +import { fail } from '@sveltejs/kit'; +import { + loadAll, + toggle, + bulkUpdateGroceryCategory, +} from '$lib/server/services/groceries'; +import { + numericId, + parseOptionalNumber, +} from '$lib/server/services/validation'; +import type { EnrichedGrocery } from '$lib/server/services/groceries'; +import type { Category } from '$lib/server/services/types'; +import type { Actions, PageServerLoad } from './$types'; + +export interface CatalogOutput { + items: EnrichedGrocery[]; + categories: Category[]; +} + +export const load: PageServerLoad = async () => { + return loadAll(); +}; + +export const actions: Actions = { + toggle: async ({ request }) => { + const data = await request.formData(); + try { + const id = numericId(data.get('id')); + await toggle(id); + } catch { + return fail(400, { error: 'ID invalide' }); + } + return { success: true }; + }, + + bulkUpdateCategory: async ({ request }) => { + const data = await request.formData(); + const ids = data.getAll('ids').map(numericId); + const rawCategoryId = parseOptionalNumber(data.get('category_id')); + const categoryId = rawCategoryId === -1 ? null : rawCategoryId; + + if (ids.length === 0) { + return fail(400, { error: 'Aucun article sélectionné' }); + } + + await bulkUpdateGroceryCategory(ids, categoryId); + return { success: true, type: 'bulkUpdate' as const }; + }, +}; diff --git a/src/routes/groceries/catalog/+page.svelte b/src/routes/groceries/catalog/+page.svelte new file mode 100644 index 0000000..4cbcc27 --- /dev/null +++ b/src/routes/groceries/catalog/+page.svelte @@ -0,0 +1,211 @@ + + +{#if catalogItems.length > 0} +
+
{ + return async ({ update }) => { + await update({ reset: false }); + selectedIds.clear(); + }; + }} + > + {#each Array.from(selectedIds) as id (id)} + + {/each} + + +
+
+{/if} + +
+ {#if form?.error} +

{form.error}

+ {/if} + + {#if catalogItems.length === 0} +

Aucun article

+ {:else} + + + {#if filteredItems.length === 0} +

Aucun résultat

+ {:else} +
+ {#each groupByCategory(filteredItems) as [cat, items] (cat)} +

{cat}

+
    + {#each items as item (item.id)} +
  • + toggleId(item.id)} + /> + + → +
  • + {/each} +
+ {/each} +
+ {/if} + {/if} +
+ + diff --git a/src/routes/groceries/categories/+page.server.ts b/src/routes/groceries/categories/+page.server.ts new file mode 100644 index 0000000..c97b619 --- /dev/null +++ b/src/routes/groceries/categories/+page.server.ts @@ -0,0 +1,53 @@ +import { fail } from '@sveltejs/kit'; +import { + loadAll, + add, + deleteCategory, + reorder, +} from '$lib/server/services/categories'; +import { numericId, parseFormString } from '$lib/server/services/validation'; +import type { Category } from '$lib/server/services/types'; +import type { Actions, PageServerLoad } from './$types'; + +export interface CategoriesOutput { + categories: Category[]; +} + +export const load: PageServerLoad = async () => { + return { categories: await loadAll() }; +}; + +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' }); + } + + 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 }; + }, + + reorder: async ({ request }) => { + const data = await request.formData(); + const ids = data.getAll('orderedIds').map(numericId); + if (ids.length === 0) { + return fail(400, { error: 'Aucune catégorie' }); + } + await reorder(ids); + return { success: true }; + }, +}; diff --git a/src/routes/groceries/categories/+page.svelte b/src/routes/groceries/categories/+page.svelte new file mode 100644 index 0000000..267a287 --- /dev/null +++ b/src/routes/groceries/categories/+page.svelte @@ -0,0 +1,164 @@ + + +
+
+ + +
+ + {#if form?.error} +

{form.error}

+ {/if} + +
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + {#each localCategories as cat (cat.id)} + + {/each} +
+ +
+
    + {#each localCategories as cat, index (cat.id)} +
  • handleDragStart(index)} + ondragover={(e) => handleDragOver(e, index)} + ondrop={handleDrop} + ondragend={() => (draggedIndex = null)} + > + ⋮⋮ + {cat.name} + +
  • + {/each} +
+
+ + {#if (localCategories.length ?? 0) === 0} +

Aucune catégorie

+ {/if} +
+ + -- cgit v1.3