diff options
| -rw-r--r-- | src/lib/components/Nav.svelte | 15 | ||||
| -rw-r--r-- | src/routes/+layout.svelte | 2 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 209 | ||||
| -rw-r--r-- | src/routes/groceries/+page.server.ts (renamed from src/routes/+page.server.ts) | 2 | ||||
| -rw-r--r-- | src/routes/groceries/+page.svelte | 207 | ||||
| -rw-r--r-- | src/routes/groceries/catalog/+page.server.ts (renamed from src/routes/catalog/+page.server.ts) | 0 | ||||
| -rw-r--r-- | src/routes/groceries/catalog/+page.svelte (renamed from src/routes/catalog/+page.svelte) | 5 | ||||
| -rw-r--r-- | src/routes/groceries/categories/+page.server.ts (renamed from src/routes/categories/+page.server.ts) | 0 | ||||
| -rw-r--r-- | src/routes/groceries/categories/+page.svelte (renamed from src/routes/categories/+page.svelte) | 0 | ||||
| -rw-r--r-- | src/routes/login/+page.server.ts | 4 |
10 files changed, 229 insertions, 215 deletions
diff --git a/src/lib/components/Nav.svelte b/src/lib/components/Nav.svelte index 8f2e5c2..aa42ace 100644 --- a/src/lib/components/Nav.svelte +++ b/src/lib/components/Nav.svelte @@ -3,12 +3,17 @@ </script> <nav class="navbar"> - <a href="/" class:active={$page.url.pathname === '/'}>Liste</a> - <a href="/catalog" class:active={$page.url.pathname === '/catalog'} - >Catalogue</a + <a href="/" class:active={$page.url.pathname === '/'}>Projets</a> + <a href="/groceries" class:active={$page.url.pathname === '/groceries'} + >Liste</a > - <a href="/categories" class:active={$page.url.pathname === '/categories'} - >Catégories</a + <a + href="/groceries/catalog" + class:active={$page.url.pathname === '/groceries/catalog'}>Catalogue</a + > + <a + href="/groceries/categories" + class:active={$page.url.pathname === '/groceries/categories'}>Catégories</a > </nav> diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b2803cc..add22ee 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,7 +7,7 @@ </script> <svelte:head> - <title>Courses</title> + <title>Maison</title> </svelte:head> <div class="app"> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d07b055..5bdee46 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,205 +1,4 @@ -<script lang="ts"> - import { deserialize } from '$app/forms'; - import { goto } from '$app/navigation'; - import { enhance } from '$app/forms'; - import FuzzyAutocomplete from '$lib/components/FuzzyAutocomplete.svelte'; - import type { EnrichedGrocery } from '$lib/server/services/groceries'; - - let { data, form } = $props(); - let name = $state(''); - // eslint-disable-next-line svelte/prefer-writable-derived - let localItems = $state<EnrichedGrocery[]>([]); - let addError = $state(''); - // Sync local list with server data when it changes externally (toggle, navigation) - $effect(() => { - localItems = data?.items ?? []; - }); - - function groupByCategory(items: typeof localItems) { - const groups: Record<string, typeof localItems> = {}; - for (const item of items) { - const key = item.categoryName ?? 'Non catégorisé'; - (groups[key] ??= []).push(item); - } - const result: [string, typeof localItems][] = data.categories - .filter((cat) => groups[cat.name]?.length > 0) - .map((cat) => [cat.name, groups[cat.name]]); - if (groups['Non catégorisé']?.length > 0) { - result.unshift(['Non catégorisé', groups['Non catégorisé']]); - } - return result; - } - - async function handleAdd(redirectToEdit: boolean) { - if (!name.trim()) { - addError = 'Le nom est requis'; - return; - } - addError = ''; - - const formData = new FormData(); - formData.append('name', name); - - const action = redirectToEdit ? '?/addAndEdit' : '?/add'; - name = ''; - - try { - const response = await fetch(action, { - method: 'POST', - headers: { - accept: 'application/json', - 'x-sveltekit-action': 'true', - }, - body: formData, - }); - - const result = deserialize(await response.text()); - - if (result.type === 'error') { - addError = - typeof result.error === 'string' - ? result.error - : result.error?.message || 'Erreur'; - return; - } - - if (result.type === 'redirect') { - goto(result.location); - return; - } - - if (result.type === 'success' && result.data?.items) { - localItems = result.data.items as EnrichedGrocery[]; - } - } catch (err) { - addError = 'Erreur réseau'; - console.error(err); - } - } -</script> - -<div class="add-bar"> - <FuzzyAutocomplete - items={data.catalog} - bind:value={name} - placeholder="Ajouter un article..." - onselect={(item) => { - name = item.name; - handleAdd(false); - }} - /> - <button type="button" aria-label="Ajouter" onclick={() => handleAdd(false)} - >+</button - > - <button type="button" aria-label="Modifier" onclick={() => handleAdd(true)} - >→</button - > -</div> - -<main> - {#if addError || form?.error} - <p class="error">{addError || form?.error}</p> - {/if} - - {#if localItems.length === 0} - <p class="empty">Rien à acheter</p> - {:else} - <form method="POST" action="?/toggle" use:enhance> - {#each groupByCategory(localItems) as [cat, items] (cat)} - <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} - <span class="best-ref" - >{item.bestRef.reference} - {#if item.bestRef.price && item.bestRef.unit} - {item.bestRef.price.toFixed(2)} - {item.bestRef.unit} - {/if} - {#if item.bestRef.qualityScore != null} - ({item.bestRef.qualityScore}/100) - {/if} - </span> - {/if} - </span> - </button> - <a class="ref-link" href="/grocery/{item.id}?from=/">→</a> - </li> - {/each} - </ul> - {/each} - </form> - {/if} -</main> - -<style> - .add-bar { - display: flex; - gap: 0.5rem; - padding: 0.5rem 1rem; - position: sticky; - top: var(--nav-height); - background: var(--bg); - border-bottom: 1px solid var(--border); - z-index: 10; - } - - .add-bar button { - width: 3rem; - height: 3rem; - flex-shrink: 0; - padding: 0; - border: none; - border-radius: 0.75rem; - font-size: 1.25rem; - font-weight: 500; - font-family: inherit; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - } - - .add-bar > button:nth-of-type(1) { - background: var(--accent); - color: var(--white); - box-shadow: 0 1px 2px var(--shadow); - } - - .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-of-type(2) { - background: var(--surface); - color: var(--text-secondary); - border: 1.5px solid var(--border); - } - - .add-bar > button:nth-of-type(2):hover { - border-color: var(--accent); - color: var(--text); - } - - .error { - margin-bottom: 1rem; - } - - .empty { - margin-top: 1rem; - padding: 2rem; - } -</style> +<p>Accueil — à venir : projets et routines.</p> +<ul> + <li><a href="/groceries">Courses</a></li> +</ul> diff --git a/src/routes/+page.server.ts b/src/routes/groceries/+page.server.ts index 1e6f60b..e462ab2 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/groceries/+page.server.ts @@ -14,7 +14,7 @@ import type { EnrichedGrocery } from '$lib/server/services/groceries'; import type { Category } from '$lib/server/services/types'; import type { Actions, PageServerLoad } from './$types'; -export interface HomeOutput { +export interface GroceriesOutput { items: EnrichedGrocery[]; categories: Category[]; catalog: EnrichedGrocery[]; 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 @@ +<script lang="ts"> + import { deserialize } from '$app/forms'; + import { goto } from '$app/navigation'; + import { enhance } from '$app/forms'; + import FuzzyAutocomplete from '$lib/components/FuzzyAutocomplete.svelte'; + import type { EnrichedGrocery } from '$lib/server/services/groceries'; + + let { data, form } = $props(); + let name = $state(''); + // eslint-disable-next-line svelte/prefer-writable-derived + let localItems = $state<EnrichedGrocery[]>([]); + let addError = $state(''); + // Sync local list with server data when it changes externally (toggle, navigation) + $effect(() => { + localItems = data?.items ?? []; + }); + + function groupByCategory(items: typeof localItems) { + const groups: Record<string, typeof localItems> = {}; + for (const item of items) { + const key = item.categoryName ?? 'Non catégorisé'; + (groups[key] ??= []).push(item); + } + const result: [string, typeof localItems][] = data.categories + .filter((cat) => groups[cat.name]?.length > 0) + .map((cat) => [cat.name, groups[cat.name]]); + if (groups['Non catégorisé']?.length > 0) { + result.unshift(['Non catégorisé', groups['Non catégorisé']]); + } + return result; + } + + async function handleAdd(redirectToEdit: boolean) { + if (!name.trim()) { + addError = 'Le nom est requis'; + return; + } + addError = ''; + + const formData = new FormData(); + formData.append('name', name); + + const action = redirectToEdit ? '?/addAndEdit' : '?/add'; + name = ''; + + try { + const response = await fetch(action, { + method: 'POST', + headers: { + accept: 'application/json', + 'x-sveltekit-action': 'true', + }, + body: formData, + }); + + const result = deserialize(await response.text()); + + if (result.type === 'error') { + addError = + typeof result.error === 'string' + ? result.error + : result.error?.message || 'Erreur'; + return; + } + + if (result.type === 'redirect') { + goto(result.location); + return; + } + + if (result.type === 'success' && result.data?.items) { + localItems = result.data.items as EnrichedGrocery[]; + } + } catch (err) { + addError = 'Erreur réseau'; + console.error(err); + } + } +</script> + +<div class="add-bar"> + <FuzzyAutocomplete + items={data.catalog} + bind:value={name} + placeholder="Ajouter un article..." + onselect={(item) => { + name = item.name; + handleAdd(false); + }} + /> + <button type="button" aria-label="Ajouter" onclick={() => handleAdd(false)} + >+</button + > + <button type="button" aria-label="Modifier" onclick={() => handleAdd(true)} + >→</button + > +</div> + +<main> + {#if addError || form?.error} + <p class="error">{addError || form?.error}</p> + {/if} + + {#if localItems.length === 0} + <p class="empty">Rien à acheter</p> + {:else} + <form method="POST" action="?/toggle" use:enhance> + {#each groupByCategory(localItems) as [cat, items] (cat)} + <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} + <span class="best-ref" + >{item.bestRef.reference} + {#if item.bestRef.price && item.bestRef.unit} + {item.bestRef.price.toFixed(2)} + {item.bestRef.unit} + {/if} + {#if item.bestRef.qualityScore != null} + ({item.bestRef.qualityScore}/100) + {/if} + </span> + {/if} + </span> + </button> + <a class="ref-link" href="/grocery/{item.id}?from=/groceries" + >→</a + > + </li> + {/each} + </ul> + {/each} + </form> + {/if} +</main> + +<style> + .add-bar { + display: flex; + gap: 0.5rem; + padding: 0.5rem 1rem; + position: sticky; + top: var(--nav-height); + background: var(--bg); + border-bottom: 1px solid var(--border); + z-index: 10; + } + + .add-bar button { + width: 3rem; + height: 3rem; + flex-shrink: 0; + padding: 0; + border: none; + border-radius: 0.75rem; + font-size: 1.25rem; + font-weight: 500; + font-family: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + } + + .add-bar > button:nth-of-type(1) { + background: var(--accent); + color: var(--white); + box-shadow: 0 1px 2px var(--shadow); + } + + .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-of-type(2) { + background: var(--surface); + color: var(--text-secondary); + border: 1.5px solid var(--border); + } + + .add-bar > button:nth-of-type(2):hover { + border-color: var(--accent); + color: var(--text); + } + + .error { + margin-bottom: 1rem; + } + + .empty { + margin-top: 1rem; + padding: 2rem; + } +</style> diff --git a/src/routes/catalog/+page.server.ts b/src/routes/groceries/catalog/+page.server.ts index 22464dc..22464dc 100644 --- a/src/routes/catalog/+page.server.ts +++ b/src/routes/groceries/catalog/+page.server.ts diff --git a/src/routes/catalog/+page.svelte b/src/routes/groceries/catalog/+page.svelte index ebc1819..4cbcc27 100644 --- a/src/routes/catalog/+page.svelte +++ b/src/routes/groceries/catalog/+page.svelte @@ -125,8 +125,9 @@ {/if} </span> </button> - <a class="ref-link" href="/grocery/{item.id}?from=/catalog" - >→</a + <a + class="ref-link" + href="/grocery/{item.id}?from=/groceries/catalog">→</a > </li> {/each} diff --git a/src/routes/categories/+page.server.ts b/src/routes/groceries/categories/+page.server.ts index c97b619..c97b619 100644 --- a/src/routes/categories/+page.server.ts +++ b/src/routes/groceries/categories/+page.server.ts diff --git a/src/routes/categories/+page.svelte b/src/routes/groceries/categories/+page.svelte index 267a287..267a287 100644 --- a/src/routes/categories/+page.svelte +++ b/src/routes/groceries/categories/+page.svelte diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index 88c5f2d..b43c97e 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -3,7 +3,9 @@ import { setSession, verifyPassword } from '$lib/server/auth'; import type { Actions, PageServerLoad } from './$types'; function safeNext(value: string | null): string { - return value?.startsWith('/') && !value.startsWith('//') ? value : '/'; + return value?.startsWith('/') && !value.startsWith('//') + ? value + : '/groceries'; } export const load: PageServerLoad = ({ url }) => ({ |
