aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/components/GroceryRow.svelte55
-rw-r--r--src/lib/server/services/grocery-actions.ts76
-rw-r--r--src/routes/groceries/+page.server.ts42
-rw-r--r--src/routes/groceries/+page.svelte22
-rw-r--r--src/routes/groceries/catalog/+page.svelte49
-rw-r--r--src/routes/groceries/categories/+page.server.ts30
-rw-r--r--src/routes/grocery/[id]/+page.server.ts13
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}">&rarr;</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"
- >&rarr;</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">&rarr;</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();