diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-07-29 15:01:38 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-07-29 15:01:38 +0200 |
| commit | 63e003a718782e2ca3ffcb2d11b0be97560a7259 (patch) | |
| tree | e38ed25161981ec33aebc7d76e9303e547d42768 /src/routes | |
| parent | 684d3c954e3577a6488101bf5ee85e36439fd53c (diff) | |
feat: drag-to-reorder categories with persistent order
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/categories/+page.server.ts | 17 | ||||
| -rw-r--r-- | src/routes/categories/+page.svelte | 67 |
2 files changed, 80 insertions, 4 deletions
diff --git a/src/routes/categories/+page.server.ts b/src/routes/categories/+page.server.ts index ac53ca8..c97b619 100644 --- a/src/routes/categories/+page.server.ts +++ b/src/routes/categories/+page.server.ts @@ -1,5 +1,10 @@ import { fail } from '@sveltejs/kit'; -import { loadAll, add, deleteCategory } from '$lib/server/services/categories'; +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'; @@ -35,4 +40,14 @@ export const actions: Actions = { } 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/categories/+page.svelte b/src/routes/categories/+page.svelte index 1060eaa..9c91578 100644 --- a/src/routes/categories/+page.svelte +++ b/src/routes/categories/+page.svelte @@ -3,6 +3,36 @@ let { data, form } = $props(); let name = $state(''); + let draggedIndex = $state<number | null>(null); + // eslint-disable-next-line svelte/prefer-writable-derived + let localCategories = $state(data?.categories ?? []); + + // Sync with server data on navigation + $effect(() => { + localCategories = data?.categories ?? []; + }); + + let reorderFormRef: HTMLFormElement; + + function handleDragStart(index: number) { + draggedIndex = index; + } + + function handleDragOver(e: DragEvent, index: number) { + e.preventDefault(); + if (draggedIndex === null || draggedIndex === index) return; + + const next = [...localCategories]; + const [moved] = next.splice(draggedIndex, 1); + next.splice(index, 0, moved); + localCategories = next; + draggedIndex = index; + } + + function handleDrop() { + draggedIndex = null; + reorderFormRef?.requestSubmit(); + } </script> <main> @@ -30,10 +60,33 @@ <p class="error">{form.error}</p> {/if} + <form + bind:this={reorderFormRef} + method="POST" + action="?/reorder" + use:enhance={() => { + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + {#each localCategories as cat (cat.id)} + <input type="hidden" name="orderedIds" value={cat.id} /> + {/each} + </form> + <form method="POST" action="?/delete" use:enhance> <ul class="item-list"> - {#each data?.categories ?? [] as cat (cat.id)} - <li class="item-row item-row--card"> + {#each localCategories as cat, index (cat.id)} + <li + class="item-row item-row--card" + draggable="true" + ondragstart={() => handleDragStart(index)} + ondragover={(e) => handleDragOver(e, index)} + ondrop={handleDrop} + ondragend={() => (draggedIndex = null)} + > + <span class="drag-handle">⋮⋮</span> <span class="cat-name">{cat.name}</span> <button class="btn-ghost" @@ -49,7 +102,7 @@ </ul> </form> - {#if (data?.categories?.length ?? 0) === 0} + {#if (localCategories.length ?? 0) === 0} <p class="empty">Aucune catégorie</p> {/if} </main> @@ -61,6 +114,14 @@ color: var(--text); } + .drag-handle { + color: var(--text-secondary); + font-size: 0.875rem; + cursor: grab; + padding-right: 0.5rem; + user-select: none; + } + .error { margin-top: -0.25rem; margin-bottom: 0.5rem; |
