701 lines
25 KiB
Svelte
701 lines
25 KiB
Svelte
<script lang="ts">
|
||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||
import {
|
||
listPresets,
|
||
createPreset,
|
||
deletePreset,
|
||
renamePreset,
|
||
createPresetEntry,
|
||
updatePresetEntry,
|
||
deletePresetEntry,
|
||
} from "$lib/api/presets";
|
||
import { listProducts } from "$lib/api/products";
|
||
import { createMealFromPreset } from "$lib/api/meals";
|
||
import { today } from "$lib/utils/date";
|
||
import { kcal, g } from "$lib/utils/format";
|
||
import TopBar from "$lib/components/ui/TopBar.svelte";
|
||
import Sheet from "$lib/components/ui/Sheet.svelte";
|
||
import type { Preset, PresetEntry, Product } from "$lib/types/api";
|
||
|
||
const queryClient = useQueryClient();
|
||
|
||
// ── Search / filter ───────────────────────────────────────────────────────
|
||
let q = $state("");
|
||
let debouncedQ = $state("");
|
||
let debounceTimer: ReturnType<typeof setTimeout>;
|
||
|
||
function handleSearch(value: string) {
|
||
q = value;
|
||
clearTimeout(debounceTimer);
|
||
debounceTimer = setTimeout(() => {
|
||
debouncedQ = value;
|
||
}, 300);
|
||
}
|
||
|
||
const presetsQuery = createQuery(() => ({
|
||
queryKey: ["presets"],
|
||
queryFn: () => listPresets(100),
|
||
}));
|
||
|
||
const filteredPresets = $derived(
|
||
(presetsQuery.data ?? []).filter(
|
||
(p) =>
|
||
!debouncedQ || p.name.toLowerCase().includes(debouncedQ.toLowerCase()),
|
||
),
|
||
);
|
||
|
||
// ── Expand/collapse ───────────────────────────────────────────────────────
|
||
let expandedId = $state<number | null>(null);
|
||
|
||
function toggleExpand(preset: Preset) {
|
||
expandedId = expandedId === preset.id ? null : preset.id;
|
||
}
|
||
|
||
// ── Add to today ──────────────────────────────────────────────────────────
|
||
let addingId = $state<number | null>(null);
|
||
let addedId = $state<number | null>(null);
|
||
|
||
async function handleAddToToday(preset: Preset) {
|
||
addingId = preset.id;
|
||
try {
|
||
await createMealFromPreset(today(), preset.id, preset.name);
|
||
queryClient.invalidateQueries({ queryKey: ["diary", today()] });
|
||
addedId = preset.id;
|
||
setTimeout(() => {
|
||
addedId = null;
|
||
}, 1500);
|
||
} finally {
|
||
addingId = null;
|
||
}
|
||
}
|
||
|
||
// ── Create preset ─────────────────────────────────────────────────────────
|
||
let creating = $state<boolean>(false);
|
||
let creatingName = $state("");
|
||
let creatingSaving = $state(false);
|
||
|
||
async function handleCreate() {
|
||
creatingSaving = true;
|
||
try {
|
||
await createPreset(creatingName.trim());
|
||
queryClient.invalidateQueries({ queryKey: ["presets"] });
|
||
creating = false;
|
||
creatingName = "";
|
||
} finally {
|
||
creatingSaving = false;
|
||
}
|
||
}
|
||
|
||
// ── Delete preset ─────────────────────────────────────────────────────────
|
||
let deletingId = $state<number | null>(null);
|
||
|
||
async function handleDelete(preset: Preset) {
|
||
if (!confirm(`Delete preset "${preset.name}"?`)) return;
|
||
deletingId = preset.id;
|
||
try {
|
||
await deletePreset(preset.id);
|
||
if (expandedId === preset.id) expandedId = null;
|
||
queryClient.invalidateQueries({ queryKey: ["presets"] });
|
||
} finally {
|
||
deletingId = null;
|
||
}
|
||
}
|
||
|
||
// ── Rename preset ─────────────────────────────────────────────────────────
|
||
let renamePreset_: Preset | null = $state(null);
|
||
let renameName = $state("");
|
||
let renaming = $state(false);
|
||
|
||
function openRename(preset: Preset) {
|
||
renamePreset_ = preset;
|
||
renameName = preset.name;
|
||
}
|
||
|
||
async function handleRename(e: SubmitEvent) {
|
||
e.preventDefault();
|
||
if (!renamePreset_ || !renameName.trim()) return;
|
||
renaming = true;
|
||
try {
|
||
await renamePreset(renamePreset_.id, renameName.trim());
|
||
queryClient.invalidateQueries({ queryKey: ["presets"] });
|
||
renamePreset_ = null;
|
||
} finally {
|
||
renaming = false;
|
||
}
|
||
}
|
||
|
||
// ── Edit entry grams ──────────────────────────────────────────────────────
|
||
let editingEntry = $state<{ presetId: number; entry: PresetEntry } | null>(
|
||
null,
|
||
);
|
||
let editGrams = $state(0);
|
||
let editSaving = $state(false);
|
||
let editGramsInput = $state<HTMLInputElement | null>(null);
|
||
|
||
function openEditEntry(presetId: number, entry: PresetEntry) {
|
||
editingEntry = { presetId, entry };
|
||
editGrams = entry.grams;
|
||
}
|
||
|
||
async function handleEditEntry(e: SubmitEvent) {
|
||
e.preventDefault();
|
||
if (!editingEntry) return;
|
||
editSaving = true;
|
||
try {
|
||
await updatePresetEntry(
|
||
editingEntry.presetId,
|
||
editingEntry.entry.id,
|
||
editGrams,
|
||
);
|
||
queryClient.invalidateQueries({ queryKey: ["presets"] });
|
||
editingEntry = null;
|
||
} finally {
|
||
editSaving = false;
|
||
}
|
||
}
|
||
|
||
// ── Delete entry ──────────────────────────────────────────────────────────
|
||
async function handleDeleteEntry(presetId: number, entryId: number) {
|
||
await deletePresetEntry(presetId, entryId);
|
||
queryClient.invalidateQueries({ queryKey: ["presets"] });
|
||
}
|
||
|
||
// ── Add entry to preset ───────────────────────────────────────────────────
|
||
let addEntryPresetId = $state<number | null>(null);
|
||
let productQ = $state("");
|
||
let productDebouncedQ = $state("");
|
||
let productDebounceTimer: ReturnType<typeof setTimeout>;
|
||
let selectedProduct = $state<Product | null>(null);
|
||
let addGrams = $state(100);
|
||
let addSaving = $state(false);
|
||
let addGramsInput = $state<HTMLInputElement | null>(null);
|
||
|
||
function openAddEntry(presetId: number) {
|
||
addEntryPresetId = presetId;
|
||
productQ = "";
|
||
productDebouncedQ = "";
|
||
selectedProduct = null;
|
||
addGrams = 100;
|
||
}
|
||
|
||
function handleProductSearch(v: string) {
|
||
productQ = v;
|
||
clearTimeout(productDebounceTimer);
|
||
productDebounceTimer = setTimeout(() => {
|
||
productDebouncedQ = v;
|
||
}, 300);
|
||
}
|
||
|
||
const productsQuery = createQuery(() => ({
|
||
queryKey: ["products", productDebouncedQ],
|
||
queryFn: () => listProducts(productDebouncedQ, 30),
|
||
enabled: addEntryPresetId !== null,
|
||
staleTime: 30_000,
|
||
}));
|
||
|
||
async function handleAddEntry(e: SubmitEvent) {
|
||
e.preventDefault();
|
||
if (!addEntryPresetId || !selectedProduct) return;
|
||
addSaving = true;
|
||
try {
|
||
await createPresetEntry(addEntryPresetId, selectedProduct.id, addGrams);
|
||
queryClient.invalidateQueries({ queryKey: ["presets"] });
|
||
addEntryPresetId = null;
|
||
} finally {
|
||
addSaving = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<div class="flex flex-col h-screen">
|
||
<TopBar title="Presets" back="/diary/{today()}" />
|
||
|
||
<!-- Search -->
|
||
<div class="px-4 py-3 border-b border-zinc-800 bg-zinc-950">
|
||
<div class="relative">
|
||
<svg
|
||
class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||
/>
|
||
</svg>
|
||
<input
|
||
type="search"
|
||
placeholder="Search presets…"
|
||
value={q}
|
||
oninput={(e) => handleSearch(e.currentTarget.value)}
|
||
class="w-full bg-zinc-900 border border-zinc-700 rounded-xl pl-9 pr-4 py-2.5 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-green-500 transition-colors"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<main class="flex-1 overflow-y-auto">
|
||
{#if presetsQuery.isPending}
|
||
<div class="space-y-2 p-4">
|
||
{#each Array(5) as _}
|
||
<div class="h-16 bg-zinc-900 rounded-xl animate-pulse"></div>
|
||
{/each}
|
||
</div>
|
||
{:else if presetsQuery.isError}
|
||
<p class="text-center text-zinc-500 mt-16">Could not load presets</p>
|
||
{:else if !filteredPresets.length}
|
||
<div class="text-center text-zinc-500 mt-16 px-6">
|
||
<p>No presets yet</p>
|
||
<p class="text-sm mt-1">Save a meal as preset from the diary view or</p>
|
||
</div>
|
||
<button
|
||
onclick={() => (creating = true)}
|
||
class="w-full rounded-xl py-4 text-green-500 hover:text-green-300 transition-colors text-sm font-medium"
|
||
>
|
||
Add preset
|
||
</button>
|
||
{:else}
|
||
<ul class="divide-y divide-zinc-800/50">
|
||
{#each filteredPresets as preset (preset.id)}
|
||
<li>
|
||
<!-- Preset header row -->
|
||
<div class="flex items-center gap-2 px-4 py-3">
|
||
<!-- Expand toggle + name -->
|
||
<button
|
||
onclick={() => toggleExpand(preset)}
|
||
class="flex-1 flex items-center gap-2 text-left min-w-0"
|
||
>
|
||
<svg
|
||
class="w-4 h-4 text-zinc-500 shrink-0 transition-transform {expandedId ===
|
||
preset.id
|
||
? ''
|
||
: '-rotate-90'}"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M19 9l-7 7-7-7"
|
||
/>
|
||
</svg>
|
||
<div class="min-w-0">
|
||
<p class="font-medium truncate">{preset.name}</p>
|
||
<p class="text-xs text-zinc-500 mt-0.5">
|
||
{kcal(preset.calories)} kcal · P {g(preset.protein)}g · C {g(
|
||
preset.carb,
|
||
)}g · F {g(preset.fat)}g
|
||
</p>
|
||
</div>
|
||
</button>
|
||
|
||
<!-- Action buttons -->
|
||
<div class="flex items-center gap-1 shrink-0">
|
||
<!-- Add to today -->
|
||
<button
|
||
onclick={() => handleAddToToday(preset)}
|
||
disabled={addingId === preset.id}
|
||
class="flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-colors
|
||
{addedId === preset.id
|
||
? 'bg-green-600/20 text-green-400'
|
||
: 'bg-zinc-800 hover:bg-zinc-700 text-zinc-300 disabled:opacity-50'}"
|
||
>
|
||
{#if addingId === preset.id}
|
||
<svg
|
||
class="w-3 h-3 animate-spin"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||
/>
|
||
</svg>
|
||
{:else if addedId === preset.id}
|
||
<svg
|
||
class="w-3 h-3"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M5 13l4 4L19 7"
|
||
/>
|
||
</svg>
|
||
Added
|
||
{:else}
|
||
<svg
|
||
class="w-3 h-3"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M12 4v16m8-8H4"
|
||
/>
|
||
</svg>
|
||
Today
|
||
{/if}
|
||
</button>
|
||
|
||
<!-- Rename -->
|
||
<button
|
||
onclick={() => openRename(preset)}
|
||
class="w-8 h-8 flex items-center justify-center rounded-full text-zinc-500 hover:text-blue-400 hover:bg-blue-400/10 transition-colors"
|
||
aria-label="Rename preset"
|
||
>
|
||
<svg
|
||
class="w-4 h-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- Delete -->
|
||
<button
|
||
onclick={() => handleDelete(preset)}
|
||
disabled={deletingId === preset.id}
|
||
class="w-8 h-8 flex items-center justify-center rounded-full text-zinc-600 hover:text-red-400 hover:bg-red-400/10 disabled:opacity-30 transition-colors"
|
||
aria-label="Delete preset"
|
||
>
|
||
<svg
|
||
class="w-4 h-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Expanded entries -->
|
||
{#if expandedId === preset.id}
|
||
<div class="px-4 pb-3 border-t border-zinc-800/60">
|
||
{#if preset.entries.length === 0}
|
||
<p class="text-sm text-zinc-600 py-3 text-center">
|
||
No entries yet
|
||
</p>
|
||
{:else}
|
||
<ul class="divide-y divide-zinc-800/50">
|
||
{#each preset.entries as entry (entry.id)}
|
||
<li class="flex items-center justify-between py-2.5">
|
||
<button
|
||
onclick={() => openEditEntry(preset.id, entry)}
|
||
class="flex-1 text-left min-w-0 group"
|
||
>
|
||
<p class="text-sm font-medium truncate">
|
||
{entry.product.name}
|
||
</p>
|
||
<p class="text-xs text-zinc-500">
|
||
{entry.grams}g · {kcal(entry.calories)} kcal
|
||
</p>
|
||
</button>
|
||
<button
|
||
onclick={() => handleDeleteEntry(preset.id, entry.id)}
|
||
class="w-7 h-7 flex items-center justify-center rounded-full text-zinc-600 hover:text-red-400 hover:bg-red-400/10 transition-colors ml-2 shrink-0"
|
||
aria-label="Delete entry"
|
||
>
|
||
<svg
|
||
class="w-4 h-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M6 18L18 6M6 6l12 12"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
{/if}
|
||
|
||
<!-- Add entry button -->
|
||
<button
|
||
onclick={() => openAddEntry(preset.id)}
|
||
class="mt-2 flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-green-400 transition-colors"
|
||
>
|
||
<svg
|
||
class="w-3.5 h-3.5"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2.5"
|
||
d="M12 4v16m8-8H4"
|
||
/>
|
||
</svg>
|
||
Add food
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
</li>
|
||
{/each}
|
||
<li>
|
||
<button
|
||
onclick={() => (creating = true)}
|
||
class="w-full rounded-xl py-4 text-zinc-500 hover:text-zinc-300 transition-colors text-sm font-medium"
|
||
>
|
||
+ Add preset
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
{/if}
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Create preset sheet -->
|
||
<Sheet
|
||
open={creating === true}
|
||
onclose={() => {
|
||
creating = false;
|
||
creatingName = "";
|
||
}}
|
||
title="Create new preset"
|
||
>
|
||
<form onsubmit={handleCreate} class="space-y-4">
|
||
<input
|
||
type="text"
|
||
bind:value={creatingName}
|
||
required
|
||
autofocus
|
||
class="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-3 text-zinc-100 focus:outline-none focus:border-green-500 transition-colors"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
disabled={creatingSaving || !creatingName.trim()}
|
||
class="w-full bg-green-600 hover:bg-green-500 disabled:opacity-50 rounded-xl py-3 font-semibold transition-colors"
|
||
>
|
||
{creatingSaving ? "Saving…" : "Save"}
|
||
</button>
|
||
</form>
|
||
</Sheet>
|
||
|
||
<!-- Rename preset sheet -->
|
||
<Sheet
|
||
open={renamePreset_ !== null}
|
||
onclose={() => (renamePreset_ = null)}
|
||
title="Rename preset"
|
||
>
|
||
<form onsubmit={handleRename} class="space-y-4">
|
||
<input
|
||
type="text"
|
||
bind:value={renameName}
|
||
required
|
||
autofocus
|
||
class="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-3 text-zinc-100 focus:outline-none focus:border-green-500 transition-colors"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
disabled={renaming || !renameName.trim()}
|
||
class="w-full bg-green-600 hover:bg-green-500 disabled:opacity-50 rounded-xl py-3 font-semibold transition-colors"
|
||
>
|
||
{renaming ? "Saving…" : "Save"}
|
||
</button>
|
||
</form>
|
||
</Sheet>
|
||
|
||
<!-- Edit entry grams sheet -->
|
||
<Sheet
|
||
open={editingEntry !== null}
|
||
onclose={() => (editingEntry = null)}
|
||
title="Edit entry"
|
||
>
|
||
{#if editingEntry}
|
||
<form onsubmit={handleEditEntry} class="space-y-4">
|
||
<p class="text-sm text-zinc-400">{editingEntry.entry.product.name}</p>
|
||
<div>
|
||
<label class="block text-sm text-zinc-400 mb-2">Grams</label>
|
||
<div class="flex items-center gap-3">
|
||
<button
|
||
type="button"
|
||
onpointerdown={(e) => e.preventDefault()}
|
||
onclick={() => {
|
||
editGrams = Math.max(1, editGrams - 10);
|
||
}}
|
||
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
|
||
>−</button
|
||
>
|
||
<input
|
||
bind:this={editGramsInput}
|
||
type="number"
|
||
bind:value={editGrams}
|
||
min="1"
|
||
autofocus
|
||
inputmode="decimal"
|
||
onfocus={(e) => e.currentTarget.select()}
|
||
class="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-center text-xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onpointerdown={(e) => e.preventDefault()}
|
||
onclick={() => {
|
||
editGrams = editGrams + 10;
|
||
}}
|
||
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
|
||
>+</button
|
||
>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="submit"
|
||
disabled={editSaving || editGrams < 1}
|
||
class="w-full bg-green-600 hover:bg-green-500 disabled:opacity-50 rounded-xl py-3 font-semibold transition-colors"
|
||
>
|
||
{editSaving ? "Saving…" : "Save"}
|
||
</button>
|
||
</form>
|
||
{/if}
|
||
</Sheet>
|
||
|
||
<!-- Add entry to preset sheet -->
|
||
<Sheet
|
||
open={addEntryPresetId !== null}
|
||
onclose={() => {
|
||
addEntryPresetId = null;
|
||
selectedProduct = null;
|
||
}}
|
||
title="Add food to preset"
|
||
>
|
||
{#if addEntryPresetId !== null}
|
||
{#if selectedProduct === null}
|
||
<!-- Product search -->
|
||
<div class="space-y-3">
|
||
<input
|
||
type="search"
|
||
placeholder="Search foods…"
|
||
value={productQ}
|
||
oninput={(e) => handleProductSearch(e.currentTarget.value)}
|
||
autofocus
|
||
class="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-green-500 transition-colors"
|
||
/>
|
||
{#if productsQuery.isPending && productDebouncedQ}
|
||
<p class="text-sm text-zinc-500 text-center py-4">Searching…</p>
|
||
{:else}
|
||
<ul
|
||
class="max-h-64 overflow-y-auto divide-y divide-zinc-800/50 -mx-4"
|
||
>
|
||
{#each productsQuery.data ?? [] as product (product.id)}
|
||
<li>
|
||
<button
|
||
onclick={() => {
|
||
selectedProduct = product;
|
||
addGrams = 100;
|
||
}}
|
||
class="w-full flex items-center justify-between px-4 py-3 hover:bg-zinc-800 transition-colors text-left"
|
||
>
|
||
<div class="min-w-0">
|
||
<p class="text-sm font-medium truncate capitalize">
|
||
{product.name}
|
||
</p>
|
||
<p class="text-xs text-zinc-500">
|
||
{kcal(product.calories)} kcal · P {g(product.protein)}g ·
|
||
C {g(product.carb)}g · F {g(product.fat)}g
|
||
<span class="text-zinc-600">/ 100g</span>
|
||
</p>
|
||
</div>
|
||
</button>
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
{/if}
|
||
</div>
|
||
{:else}
|
||
<!-- Grams input -->
|
||
<form onsubmit={handleAddEntry} class="space-y-4">
|
||
<div>
|
||
<p class="text-sm font-medium capitalize mb-0.5">
|
||
{selectedProduct.name}
|
||
</p>
|
||
<p class="text-xs text-zinc-500">
|
||
{kcal(selectedProduct.calories)} kcal / 100g
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-zinc-400 mb-2">Grams</label>
|
||
<div class="flex items-center gap-3">
|
||
<button
|
||
type="button"
|
||
onpointerdown={(e) => e.preventDefault()}
|
||
onclick={() => {
|
||
addGrams = Math.max(1, addGrams - 10);
|
||
}}
|
||
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
|
||
>−</button
|
||
>
|
||
<input
|
||
bind:this={addGramsInput}
|
||
type="number"
|
||
bind:value={addGrams}
|
||
min="1"
|
||
autofocus
|
||
inputmode="decimal"
|
||
onfocus={(e) => e.currentTarget.select()}
|
||
class="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-center text-xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onpointerdown={(e) => e.preventDefault()}
|
||
onclick={() => {
|
||
addGrams = addGrams + 10;
|
||
}}
|
||
class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
|
||
>+</button
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button
|
||
type="button"
|
||
onclick={() => (selectedProduct = null)}
|
||
class="flex-1 bg-zinc-800 hover:bg-zinc-700 rounded-xl py-3 text-sm font-medium transition-colors"
|
||
>
|
||
Back
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={addSaving || addGrams < 1}
|
||
class="flex-1 bg-green-600 hover:bg-green-500 disabled:opacity-50 rounded-xl py-3 font-semibold transition-colors"
|
||
>
|
||
{addSaving ? "Adding…" : "Add"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
{/if}
|
||
{/if}
|
||
</Sheet>
|