feat(recipe): Pulse-Animation beim Aktivieren Favorit/Wunschliste
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m15s
Kurzer Scale-Bounce plus ausklingender Ring in der Aktionsfarbe (rot für Favorit, grün für Wunschliste), sobald der Button eine Markierung setzt. Beim Wieder-Abwählen bleibt es ruhig — hilft die Bestätigung visuell abzusetzen. Die Animation wird per tick()-Zwischenschritt (false → tick → true) gestartet, damit mehrfache Klicks innerhalb weniger hundert ms die Animation neu triggern. prefers-reduced-motion schaltet den Effekt aus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,24 @@
|
|||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let recipeState = $state(data.recipe);
|
let recipeState = $state(data.recipe);
|
||||||
|
|
||||||
|
// Einmalige Pulse-Animation beim Aktivieren (nicht beim Wieder-Abwählen).
|
||||||
|
// Per tick()-Zwischenschritt "aus → an" erzwingen, damit die Animation
|
||||||
|
// auch bei mehrmaligem Klick innerhalb weniger hundert ms neu startet.
|
||||||
|
let pulseFav = $state(false);
|
||||||
|
let pulseWish = $state(false);
|
||||||
|
|
||||||
|
async function firePulse(which: 'fav' | 'wish') {
|
||||||
|
if (which === 'fav') {
|
||||||
|
pulseFav = false;
|
||||||
|
await tick();
|
||||||
|
pulseFav = true;
|
||||||
|
} else {
|
||||||
|
pulseWish = false;
|
||||||
|
await tick();
|
||||||
|
pulseWish = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveRecipe(patch: {
|
async function saveRecipe(patch: {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
@@ -128,15 +146,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const profileId = profileStore.active.id;
|
const profileId = profileStore.active.id;
|
||||||
const method = isFav ? 'DELETE' : 'PUT';
|
const wasFav = isFav;
|
||||||
|
const method = wasFav ? 'DELETE' : 'PUT';
|
||||||
await fetch(`/api/recipes/${data.recipe.id}/favorite`, {
|
await fetch(`/api/recipes/${data.recipe.id}/favorite`, {
|
||||||
method,
|
method,
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({ profile_id: profileId })
|
body: JSON.stringify({ profile_id: profileId })
|
||||||
});
|
});
|
||||||
favoriteProfileIds = isFav
|
favoriteProfileIds = wasFav
|
||||||
? favoriteProfileIds.filter((id) => id !== profileId)
|
? favoriteProfileIds.filter((id) => id !== profileId)
|
||||||
: [...favoriteProfileIds, profileId];
|
: [...favoriteProfileIds, profileId];
|
||||||
|
if (!wasFav) void firePulse('fav');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logCooked() {
|
async function logCooked() {
|
||||||
@@ -258,7 +278,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const profileId = profileStore.active.id;
|
const profileId = profileStore.active.id;
|
||||||
if (onMyWishlist) {
|
const wasOn = onMyWishlist;
|
||||||
|
if (wasOn) {
|
||||||
await fetch(`/api/wishlist/${data.recipe.id}?profile_id=${profileId}`, {
|
await fetch(`/api/wishlist/${data.recipe.id}?profile_id=${profileId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
@@ -272,6 +293,7 @@
|
|||||||
wishlistProfileIds = [...wishlistProfileIds, profileId];
|
wishlistProfileIds = [...wishlistProfileIds, profileId];
|
||||||
}
|
}
|
||||||
void wishlistStore.refresh();
|
void wishlistStore.refresh();
|
||||||
|
if (!wasOn) void firePulse('wish');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wake-Lock — Bildschirm beim Kochen nicht dimmen lassen.
|
// Wake-Lock — Bildschirm beim Kochen nicht dimmen lassen.
|
||||||
@@ -387,11 +409,23 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn" class:heart={isFav} onclick={toggleFavorite}>
|
<button
|
||||||
|
class="btn"
|
||||||
|
class:heart={isFav}
|
||||||
|
class:pulse={pulseFav}
|
||||||
|
onclick={toggleFavorite}
|
||||||
|
onanimationend={() => (pulseFav = false)}
|
||||||
|
>
|
||||||
<Heart size={18} strokeWidth={2} fill={isFav ? 'currentColor' : 'none'} />
|
<Heart size={18} strokeWidth={2} fill={isFav ? 'currentColor' : 'none'} />
|
||||||
<span>Favorit</span>
|
<span>Favorit</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn" class:wish={onMyWishlist} onclick={toggleWishlist}>
|
<button
|
||||||
|
class="btn"
|
||||||
|
class:wish={onMyWishlist}
|
||||||
|
class:pulse={pulseWish}
|
||||||
|
onclick={toggleWishlist}
|
||||||
|
onanimationend={() => (pulseWish = false)}
|
||||||
|
>
|
||||||
{#if onMyWishlist}
|
{#if onMyWishlist}
|
||||||
<Check size={18} strokeWidth={2.5} />
|
<Check size={18} strokeWidth={2.5} />
|
||||||
<span>Auf Wunschliste</span>
|
<span>Auf Wunschliste</span>
|
||||||
@@ -590,11 +624,38 @@
|
|||||||
color: #c53030;
|
color: #c53030;
|
||||||
border-color: #f1b4b4;
|
border-color: #f1b4b4;
|
||||||
background: #fdf3f3;
|
background: #fdf3f3;
|
||||||
|
--pulse-color: rgba(197, 48, 48, 0.45);
|
||||||
}
|
}
|
||||||
.btn.wish {
|
.btn.wish {
|
||||||
color: #2b6a3d;
|
color: #2b6a3d;
|
||||||
border-color: #b7d6c2;
|
border-color: #b7d6c2;
|
||||||
background: #eaf4ed;
|
background: #eaf4ed;
|
||||||
|
--pulse-color: rgba(43, 106, 61, 0.45);
|
||||||
|
}
|
||||||
|
/* Einmalige Bestätigung beim Aktivieren der Aktion — kurzer Scale-Bounce
|
||||||
|
plus ausklingender Ring in der Aktionsfarbe (siehe --pulse-color).
|
||||||
|
prefers-reduced-motion: Ring aus, kein Scale. */
|
||||||
|
.btn.pulse {
|
||||||
|
animation: btnPulse 0.5s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes btnPulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 var(--pulse-color, rgba(43, 106, 61, 0.45));
|
||||||
|
}
|
||||||
|
55% {
|
||||||
|
transform: scale(1.07);
|
||||||
|
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.btn.pulse {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.btn.screen-on {
|
.btn.screen-on {
|
||||||
color: #b07e00;
|
color: #b07e00;
|
||||||
|
|||||||
Reference in New Issue
Block a user