aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/server/db/schema.ts1
-rw-r--r--src/lib/server/services/categories.ts27
-rw-r--r--src/lib/server/services/groceries.ts12
-rw-r--r--src/routes/categories/+page.server.ts17
-rw-r--r--src/routes/categories/+page.svelte67
5 files changed, 114 insertions, 10 deletions
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
index fae002d..31c2f65 100644
--- a/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -3,6 +3,7 @@ import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
export const categories = sqliteTable('categories', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(),
+ order: integer('order').notNull().default(0),
});
export const groceries = sqliteTable('groceries', {
diff --git a/src/lib/server/services/categories.ts b/src/lib/server/services/categories.ts
index f8b420f..d6fa2c5 100644
--- a/src/lib/server/services/categories.ts
+++ b/src/lib/server/services/categories.ts
@@ -1,18 +1,39 @@
import { db as defaultDb } from '$lib/server/db';
import { categories } from '$lib/server/db/schema';
-import { eq, asc } from 'drizzle-orm';
+import { eq, asc, sql } from 'drizzle-orm';
import type { AppDatabase } from './groceries';
import type { Category } from './types';
export async function loadAll(db = defaultDb): Promise<Category[]> {
- return db.select().from(categories).orderBy(asc(categories.name));
+ return db
+ .select()
+ .from(categories)
+ .orderBy(asc(categories.order), asc(categories.name));
}
export async function add(name: string, db = defaultDb): Promise<Category> {
- const [inserted] = await db.insert(categories).values({ name }).returning();
+ const [{ maxOrder }] = await db
+ .select({ maxOrder: sql<number>`COALESCE(MAX(${categories.order}), 0)` })
+ .from(categories);
+ const [inserted] = await db
+ .insert(categories)
+ .values({ name, order: (maxOrder ?? 0) + 1 })
+ .returning();
return inserted;
}
+export async function reorder(
+ orderedIds: number[],
+ db = defaultDb,
+): Promise<void> {
+ for (let i = 0; i < orderedIds.length; i++) {
+ await db
+ .update(categories)
+ .set({ order: i + 1 })
+ .where(eq(categories.id, orderedIds[i]));
+ }
+}
+
export async function deleteCategory(
id: number,
db = defaultDb,
diff --git a/src/lib/server/services/groceries.ts b/src/lib/server/services/groceries.ts
index 3c6de70..8ffcde3 100644
--- a/src/lib/server/services/groceries.ts
+++ b/src/lib/server/services/groceries.ts
@@ -1,6 +1,6 @@
import { db as defaultDb } from '$lib/server/db';
import { groceries, categories, references } from '$lib/server/db/schema';
-import { eq, desc, inArray } from 'drizzle-orm';
+import { eq, desc, inArray, asc } from 'drizzle-orm';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import type * as schema from '$lib/server/db/schema';
import type { Category, Grocery, Reference } from './types';
@@ -21,7 +21,10 @@ export async function loadUnchecked(
.from(groceries)
.where(eq(groceries.checked, false))
.orderBy(desc(groceries.createdAt)),
- db.select().from(categories).orderBy(desc(categories.name)),
+ db
+ .select()
+ .from(categories)
+ .orderBy(asc(categories.order), asc(categories.name)),
db.select().from(references).orderBy(desc(references.qualityScore)),
]);
@@ -36,7 +39,10 @@ export async function loadAll(
): Promise<{ items: EnrichedGrocery[]; categories: Category[] }> {
const [items, cats, refs] = await Promise.all([
db.select().from(groceries).orderBy(desc(groceries.createdAt)),
- db.select().from(categories).orderBy(desc(categories.name)),
+ db
+ .select()
+ .from(categories)
+ .orderBy(asc(categories.order), asc(categories.name)),
db.select().from(references).orderBy(desc(references.qualityScore)),
]);
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;