diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-07-28 22:22:12 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-07-28 22:25:37 +0200 |
| commit | 7e5535f9834fca2594c5bade425ecee00c32bdb8 (patch) | |
| tree | ef0d22ef1e3926250e96e28c6add377593e43add | |
| parent | 7d55b5d859e8339205494f12f4b34b6e44a44f50 (diff) | |
feat: autocomplete search with datalist to prevent duplicate items
| -rw-r--r-- | README.md | 10 | ||||
| -rw-r--r-- | src/routes/+page.server.ts | 10 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 28 | ||||
| -rw-r--r-- | src/routes/catalog/+page.svelte | 3 |
4 files changed, 38 insertions, 13 deletions
@@ -30,8 +30,9 @@ on the list for the next shopping trip — there is only one persistent list. **Three modes of use:** 1. **Quick add** — during the week, you run out of milk or need an ingredient. - Open the app, type the name, tap to add. As frictionless as possible so you - build the habit. + Open the app, type the name, tap to add. An autocomplete dropdown surfaces + existing catalog items as you type, so "citron" does not become "citrons". + Tapping a suggestion re-adds the existing item; typing a new name creates it. 2. **Planning** — before shopping, sit down with your partner and browse the catalog. Tap unchecked items to add them back to the list. The page @@ -60,13 +61,12 @@ worker precaches assets; works offline after first visit. ### Bugs / Polish -- [ ] Live updates from server -- [ ] Search/filter in list and catalog - [ ] Fixed category order in both lists (drag to reorder aisles) ### Features -- [ ] From catalog, multi-select and add in batch to the list +- [ ] Live updates from server +- [ ] From catalog, multi-select and bulk-update category - [ ] Update item name/category on blur, no explicit save button - [ ] Auto-categorize new items (LLM-assisted) - [ ] Chores module (intro already promises "groceries, chores") diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index b0ddff0..1e6f60b 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,6 +1,7 @@ import { fail, redirect, error } from '@sveltejs/kit'; import { loadUnchecked, + loadAll, toggle, addOrUncheck, } from '$lib/server/services/groceries'; @@ -16,10 +17,17 @@ import type { Actions, PageServerLoad } from './$types'; export interface HomeOutput { items: EnrichedGrocery[]; categories: Category[]; + catalog: EnrichedGrocery[]; } export const load: PageServerLoad = async () => { - return loadUnchecked(); + const unchecked = await loadUnchecked(); + const all = await loadAll(); + return { + items: unchecked.items, + categories: unchecked.categories, + catalog: all.items, + }; }; export const actions: Actions = { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e4f78c5..a151a9b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -9,7 +9,6 @@ // eslint-disable-next-line svelte/prefer-writable-derived let localItems = $state(data?.items ?? []); let addError = $state(''); - // Sync local list with server data when it changes externally (toggle, navigation) $effect(() => { localItems = data?.items ?? []; @@ -81,13 +80,29 @@ bind:value={name} placeholder="Ajouter un article..." autocomplete="off" + list="item-suggestions" onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAdd(false); } }} + oninput={(e) => { + const val = e.currentTarget.value.trim().toLowerCase(); + const match = data.catalog.find( + (item) => item.name.toLowerCase() === val, + ); + if (match) { + name = match.name; + handleAdd(false); + } + }} /> + <datalist id="item-suggestions"> + {#each data.catalog as item (item.id)} + <option value={item.name}></option> + {/each} + </datalist> <button type="button" aria-label="Ajouter" onclick={() => handleAdd(false)} >+</button > @@ -123,7 +138,8 @@ <span class="best-ref" >{item.bestRef.reference} {#if item.bestRef.price && item.bestRef.unit} - {item.bestRef.price.toFixed(2)} {item.bestRef.unit} + {item.bestRef.price.toFixed(2)} + {item.bestRef.unit} {/if} {#if item.bestRef.qualityScore != null} ({item.bestRef.qualityScore}/100) @@ -175,25 +191,25 @@ transition: all 0.2s ease; } - .add-bar button:nth-child(2) { + .add-bar > button:nth-of-type(1) { background: var(--accent); color: var(--white); box-shadow: 0 1px 2px var(--shadow); } - .add-bar button:nth-child(2):hover { + .add-bar > button:nth-of-type(1):hover { background: var(--accent-hover); transform: translateY(-1px); box-shadow: 0 4px 8px var(--shadow-accent-lg); } - .add-bar button:nth-child(3) { + .add-bar > button:nth-of-type(2) { background: var(--surface); color: var(--text-secondary); border: 1.5px solid var(--border); } - .add-bar button:nth-child(3):hover { + .add-bar > button:nth-of-type(2):hover { border-color: var(--accent); color: var(--text); } diff --git a/src/routes/catalog/+page.svelte b/src/routes/catalog/+page.svelte index 525c2ed..7d3ebbe 100644 --- a/src/routes/catalog/+page.svelte +++ b/src/routes/catalog/+page.svelte @@ -38,7 +38,8 @@ <span class="best-ref" >{item.bestRef.reference} {#if item.bestRef.price && item.bestRef.unit} - {item.bestRef.price.toFixed(2)} {item.bestRef.unit} + {item.bestRef.price.toFixed(2)} + {item.bestRef.unit} {/if} {#if item.bestRef.qualityScore != null} ({item.bestRef.qualityScore}/100) |
