TICKET-007 (frontend): save-as-meal flow, collapsible meal rows, unpack, meal component editor
This commit is contained in:
@@ -2,13 +2,15 @@
|
||||
// Dashboard — Daily view: progress bar + log entries grouped by meal_slot (spec §4.6).
|
||||
// Every async view handles loading / error / empty states (spec §8.2 rule 4).
|
||||
// Recent foods quick-log section per TICKET-006.
|
||||
// Multi-select "Save as Meal" flow (spec §4.3, TICKET-007).
|
||||
|
||||
import { onMount } from 'svelte'
|
||||
import { api } from '../lib/api.js'
|
||||
import { currentDate, dayData, refreshDayData, appView, addLogEntryToStore } from '../lib/stores.svelte.js'
|
||||
import { currentDate, dayData, refreshDayData, appView, replaceEntriesForMeal } from '../lib/stores.svelte.js'
|
||||
import { formatDate, formatKcal, defaultQuantity } from '../lib/format.js'
|
||||
import ProgressBar from './ProgressBar.svelte'
|
||||
import LogEntry from './LogEntry.svelte'
|
||||
import MealBuilder from './MealBuilder.svelte'
|
||||
|
||||
let { date } = $props()
|
||||
|
||||
@@ -48,7 +50,7 @@
|
||||
meal_slot: getDefaultSlot(),
|
||||
date: currentDate.value,
|
||||
})
|
||||
await addLogEntryToStore(entry)
|
||||
dayData.log = [...dayData.log, entry]
|
||||
} catch {
|
||||
// Silently fail — user can manually log
|
||||
}
|
||||
@@ -86,6 +88,32 @@
|
||||
if (unslotted.length) result.push({ slot: null, entries: unslotted })
|
||||
return result
|
||||
})
|
||||
|
||||
// ── Multi-select for "Save as Meal" (§4.3) ──────────────────────────────
|
||||
let selectedIds = $state(new Set())
|
||||
let selecting = $state(false) // true → checkboxes visible
|
||||
let creatingMeal = $state(false) // true → show MealBuilder overlay
|
||||
|
||||
function toggleSelect(entryId) {
|
||||
const next = new Set(selectedIds)
|
||||
if (next.has(entryId)) {
|
||||
next.delete(entryId)
|
||||
} else {
|
||||
next.add(entryId)
|
||||
}
|
||||
selectedIds = next
|
||||
}
|
||||
|
||||
function cancelSelection() {
|
||||
selecting = false
|
||||
selectedIds = new Set()
|
||||
}
|
||||
|
||||
function closeMealBuilder() {
|
||||
creatingMeal = false
|
||||
selecting = false
|
||||
selectedIds = new Set()
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="dashboard">
|
||||
@@ -126,17 +154,61 @@
|
||||
{#if dayData.log.length === 0}
|
||||
<p class="status empty">Nothing logged yet. Tap "Add Food" to get started.</p>
|
||||
{:else}
|
||||
{#if !selecting}
|
||||
<div class="meal-actions">
|
||||
<button type="button" class="secondary select-btn" onclick={() => selecting = true}>
|
||||
☑ Select entries
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#each groups as group (group.slot ?? 'other')}
|
||||
<div class="meal-group">
|
||||
<h3 class="slot-header">{group.slot || 'Other'}</h3>
|
||||
<ul class="entry-list">
|
||||
{#each group.entries as entry (entry.id)}
|
||||
<LogEntry {entry} />
|
||||
<li class="entry-row" class:selected={selectedIds.has(entry.id)}>
|
||||
{#if selecting || creatingMeal}
|
||||
<!-- Multi-select checkbox -->
|
||||
<label class="select-label" title="Select for meal">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(entry.id)}
|
||||
onchange={() => toggleSelect(entry.id)}
|
||||
class="select-checkbox"
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
<div class="entry-content">
|
||||
<LogEntry {entry} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- Save as Meal button (spec §4.3) — shown when entries are selected -->
|
||||
{#if selecting && !creatingMeal}
|
||||
<div class="meal-actions">
|
||||
<button type="button" class="save-meal-btn" onclick={() => creatingMeal = true} disabled={selectedIds.size < 2}>
|
||||
🍽️ Save as Meal ({selectedIds.size} selected)
|
||||
</button>
|
||||
<button type="button" class="secondary" onclick={cancelSelection}>
|
||||
Cancel selection
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- MealBuilder overlay -->
|
||||
{#if creatingMeal}
|
||||
<MealBuilder
|
||||
entryIds={[...selectedIds]}
|
||||
date={currentDate.value}
|
||||
onComplete={closeMealBuilder}
|
||||
onCancel={closeMealBuilder}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -219,8 +291,64 @@
|
||||
padding-bottom: 0.2rem;
|
||||
border-bottom: 1px solid var(--border, #e5e7eb);
|
||||
}
|
||||
.entry-list {
|
||||
|
||||
/* ── Entry row with multi-select ────────────────────────────────────── */
|
||||
.entry-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
.entry-row.selected {
|
||||
background: var(--bg-muted, #f0f4ff);
|
||||
border-radius: 0.35rem;
|
||||
}
|
||||
.select-label {
|
||||
padding-top: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.select-checkbox {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.entry-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.entry-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* ── Meal actions ───────────────────────────────────────────────────── */
|
||||
.meal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.save-meal-btn {
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
border: 1px solid #7c3aed;
|
||||
border-radius: 0.35rem;
|
||||
font: inherit;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
button.secondary {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border, #d1d5db);
|
||||
border-radius: 0.35rem;
|
||||
background: var(--bg, #fff);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user