feat: add favorite toggle to food detail page
Heart button in food header to add/remove favorites via the existing favorite-ingredients API. Checks status on load, toggles optimistically with error handling.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { ChevronDown } from '@lucide/svelte';
|
import { ChevronDown, Heart } from '@lucide/svelte';
|
||||||
|
import { toast } from '$lib/js/toast.svelte';
|
||||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||||
import { NUTRIENT_META } from '$lib/data/dailyReferenceIntake';
|
import { NUTRIENT_META } from '$lib/data/dailyReferenceIntake';
|
||||||
|
|
||||||
@@ -129,6 +130,41 @@
|
|||||||
// --- Expand toggles ---
|
// --- Expand toggles ---
|
||||||
let showMicros = $state(true);
|
let showMicros = $state(true);
|
||||||
let showAminos = $state(false);
|
let showAminos = $state(false);
|
||||||
|
|
||||||
|
// --- Favorite ---
|
||||||
|
let favorited = $state(false);
|
||||||
|
let favLoading = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
fetch('/api/fitness/favorite-ingredients').then(r => r.json()).then(data => {
|
||||||
|
favorited = (data.favorites ?? []).some(f => f.source === food.source && f.sourceId === (food.id ?? food.sourceId));
|
||||||
|
}).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function toggleFavorite() {
|
||||||
|
favLoading = true;
|
||||||
|
const id = food.id ?? food.sourceId;
|
||||||
|
try {
|
||||||
|
if (favorited) {
|
||||||
|
await fetch('/api/fitness/favorite-ingredients', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ source: food.source, sourceId: id }),
|
||||||
|
});
|
||||||
|
favorited = false;
|
||||||
|
} else {
|
||||||
|
await fetch('/api/fitness/favorite-ingredients', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ source: food.source, sourceId: id, name: food.name }),
|
||||||
|
});
|
||||||
|
favorited = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error(isEn ? 'Failed to update favorite' : 'Fehler beim Aktualisieren');
|
||||||
|
}
|
||||||
|
favLoading = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -138,7 +174,12 @@
|
|||||||
<div class="food-detail">
|
<div class="food-detail">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="food-header">
|
<header class="food-header">
|
||||||
|
<div class="food-header-row">
|
||||||
<h1>{food.nameDe ?? food.name}</h1>
|
<h1>{food.nameDe ?? food.name}</h1>
|
||||||
|
<button class="fav-btn" class:active={favorited} disabled={favLoading} onclick={toggleFavorite} aria-label={favorited ? 'Remove favorite' : 'Add favorite'}>
|
||||||
|
<Heart size={20} fill={favorited ? 'var(--nord11)' : 'none'} color={favorited ? 'var(--nord11)' : 'var(--color-text-tertiary)'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{#if food.nameEn && food.nameDe}
|
{#if food.nameEn && food.nameDe}
|
||||||
<p class="name-alt">{food.nameEn}</p>
|
<p class="name-alt">{food.nameEn}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -316,12 +357,34 @@
|
|||||||
.food-header {
|
.food-header {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
.food-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
.food-header h1 {
|
.food-header h1 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
margin: 0 0 0.25rem;
|
margin: 0 0 0.25rem;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
.fav-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.3rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.15s;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.fav-btn:hover {
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
}
|
||||||
|
.fav-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
.name-alt {
|
.name-alt {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user