Add favorite indicators to recipe cards and improve favorites UI
All checks were successful
CI / update (push) Successful in 17s

- Add heart emoji indicators to recipe cards (top-left positioning)
- Show favorites across all recipe list pages (season, category, icon, tag)
- Create favorites utility functions for server-side data merging
- Convert client-side load files to server-side for session access
- Redesign favorite button with emoji hearts (🖤/❤️) and bottom-right positioning
- Fix randomizer array mutation issue causing card display glitches
- Implement consistent favorite indicators with drop shadows for visibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-01 20:45:28 +02:00
parent 6a64a7ddd6
commit b534cd1ddc
23 changed files with 198 additions and 123 deletions

View File

@@ -7,6 +7,8 @@ import "$lib/css/nordtheme.css";
import "$lib/css/shake.css";
import "$lib/css/icon.css";
export let do_margin_right = false;
export let isFavorite = false;
export let showFavoriteIndicator = false;
// to manually override lazy loading for top cards
export let loading_strat : "lazy" | "eager" | undefined;
if(loading_strat === undefined){
@@ -192,6 +194,14 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
scale: 0.9 0.9;
}
.favorite-indicator{
position: absolute;
font-size: 2rem;
top: -0.5em;
left: -0.5em;
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.8));
}
.icon:hover,
.icon:focus-visible
{
@@ -224,6 +234,9 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
<img class:blur={!isloaded} id=image class="backdrop_blur" src={'https://bocken.org/static/rezepte/thumb/' + recipe.short_name + '.webp'} loading={loading_strat} alt="{recipe.alt}" on:load={() => isloaded=true}/>
</div>
</div>
{#if showFavoriteIndicator && isFavorite}
<div class="favorite-indicator">❤️</div>
{/if}
{#if icon_override || recipe.season.includes(current_month)}
<button class=icon on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/icon/${recipe.icon}`}}>{recipe.icon}</button>
{/if}

View File

@@ -1,6 +1,4 @@
<script lang="ts">
import Heart from '$lib/assets/icons/Heart.svelte';
export let recipeId: string;
export let isFavorite: boolean = false;
export let isLoggedIn: boolean = false;
@@ -36,17 +34,13 @@
<style>
.favorite-button {
all: unset;
color: var(--nord0);
font-size: 1.1rem;
background-color: var(--nord5);
border-radius: 10000px;
padding: 0.5em 1em;
transition: 100ms;
box-shadow: 0em 0em 0.5em 0.05em rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 0.5em;
font-size: 1.5rem;
cursor: pointer;
transition: 100ms;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
position: absolute;
bottom: 0.5em;
right: 0.5em;
}
.favorite-button:disabled {
@@ -54,49 +48,19 @@
cursor: not-allowed;
}
.favorite-button.favorited {
background-color: var(--nord11);
color: white;
}
.favorite-button:not(.favorited):hover,
.favorite-button:not(.favorited):focus-visible {
transform: scale(1.1);
background-color: var(--nord11);
color: white;
box-shadow: 0.1em 0.1em 0.5em 0.1em rgba(0,0,0,0.3);
}
.favorite-button.favorited:hover,
.favorite-button.favorited:focus-visible {
transform: scale(1.1);
background-color: var(--nord5);
color: var(--nord0);
box-shadow: 0.1em 0.1em 0.5em 0.1em rgba(0,0,0,0.3);
}
@media (prefers-color-scheme: dark) {
.favorite-button {
background-color: var(--nord0);
color: white;
}
.favorite-button.favorited:hover,
.favorite-button.favorited:focus-visible {
background-color: var(--nord6);
color: var(--nord0);
}
.favorite-button:hover,
.favorite-button:focus-visible {
transform: scale(1.2);
}
</style>
{#if isLoggedIn}
<button
class="favorite-button"
class:favorited={isFavorite}
class="favorite-button"
disabled={isLoading}
on:click={toggleFavorite}
title={isFavorite ? 'Favorit entfernen' : 'Als Favorit speichern'}
>
<Heart />
{isFavorite ? 'Favorit entfernen' : 'Als Favorit speichern'}
{isFavorite ? '❤️' : '🖤'}
</button>
{/if}

View File

@@ -12,6 +12,6 @@ export function rand_array(array){
let time = new Date()
const seed = Math.floor(time.getTime()/MS_PER_DAY)
let rand = mulberry32(seed)
array.sort((a,b) => 0.5 - rand())
return array
// Create a copy to avoid mutating the original array
return [...array].sort((a,b) => 0.5 - rand())
}

View File

@@ -0,0 +1,48 @@
/**
* Utility functions for handling user favorites on the server side
*/
export async function getUserFavorites(fetch: any, locals: any): Promise<string[]> {
const session = await locals.auth();
if (!session?.user?.nickname) {
return [];
}
try {
const favRes = await fetch('/api/rezepte/favorites');
if (favRes.ok) {
const favData = await favRes.json();
return favData.favorites || [];
}
} catch (e) {
// Silently fail if favorites can't be loaded
console.error('Error loading user favorites:', e);
}
return [];
}
export function addFavoriteStatusToRecipes(recipes: any[], userFavorites: string[]): any[] {
return recipes.map(recipe => ({
...recipe,
isFavorite: userFavorites.some(favId => favId.toString() === recipe._id.toString())
}));
}
export async function loadRecipesWithFavorites(
fetch: any,
locals: any,
recipeLoader: () => Promise<any>
): Promise<{ recipes: any[], session: any }> {
const [recipes, userFavorites, session] = await Promise.all([
recipeLoader(),
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
recipes: addFavoriteStatusToRecipes(recipes, userFavorites),
session
};
}

View File

@@ -1,13 +1,22 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export async function load({ fetch }) {
export async function load({ fetch, locals }) {
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
const res_all_brief = await fetch(`/api/rezepte/items/all_brief`);
const item_season = await res_season.json();
const item_all_brief = await res_all_brief.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
season: item_season,
all_brief: item_all_brief,
season: addFavoriteStatusToRecipes(item_season, userFavorites),
all_brief: addFavoriteStatusToRecipes(item_all_brief, userFavorites),
session
};
};

View File

@@ -36,14 +36,14 @@ h1{
<MediaScroller title="In Saison">
{#each data.season as recipe}
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true}></Card>
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</MediaScroller>
{#each categories as category}
<MediaScroller title={category}>
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
<Card {recipe} {current_month} do_margin_right={true}></Card>
<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</MediaScroller>
{/each}

View File

@@ -310,13 +310,11 @@ h4{
{/each}
</div>
<div class="tags center">
<FavoriteButton
recipeId={data.short_name}
isFavorite={data.isFavorite || false}
isLoggedIn={!!data.session?.user}
/>
</div>
<FavoriteButton
recipeId={data.short_name}
isFavorite={data.isFavorite || false}
isLoggedIn={!!data.session?.user}
/>
{#if data.note}
<RecipeNote note={data.note}></RecipeNote>

View File

@@ -0,0 +1,19 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res = await fetch(`/api/rezepte/items/category/${params.category}`);
const items = await res.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
category: params.category,
recipes: addFavoriteStatusToRecipes(items, userFavorites),
session
};
};

View File

@@ -18,7 +18,7 @@
<section>
<Recipes>
{#each rand_array(data.recipes) as recipe}
<Card {recipe} {current_month}></Card>
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</Recipes>
</section>

View File

@@ -1,10 +0,0 @@
import type { PageLoad } from "./$types";
export async function load({ fetch, params }) {
const res = await fetch(`/api/rezepte/items/category/${params.category}`);
const items = await res.json();
return {
category: params.category,
recipes: items
}
};

View File

@@ -47,7 +47,7 @@ h1{
{:else if data.favorites.length > 0}
<Recipes>
{#each data.favorites as recipe}
<Card {recipe} {current_month}></Card>
<Card {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true}></Card>
{/each}
</Recipes>
{:else}

View File

@@ -0,0 +1,22 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_season = await fetch(`/api/rezepte/items/icon/` + params.icon);
const res_icons = await fetch(`/api/rezepte/items/icon`);
const icons = await res_icons.json();
const item_season = await res_season.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
icons: icons,
icon: params.icon,
season: addFavoriteStatusToRecipes(item_season, userFavorites),
session
};
};

View File

@@ -11,7 +11,7 @@
<IconLayout icons={data.icons} active_icon={data.icon} >
<Recipes slot=recipes>
{#each rand_array(data.season) as recipe}
<Card {recipe} icon_override=true></Card>
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</Recipes>
</IconLayout>

View File

@@ -1,13 +0,0 @@
import type { PageLoad } from "./$types";
export async function load({ fetch, params }) {
const res_season = await fetch(`/api/rezepte/items/icon/` + params.icon);
const res_icons = await fetch(`/api/rezepte/items/icon`);
const icons = await res_icons.json();
const item_season = await res_season.json();
return {
icons: icons,
icon: params.icon,
season: item_season,
};
};

View File

@@ -0,0 +1,19 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals }) => {
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
const item_season = await res_season.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
season: addFavoriteStatusToRecipes(item_season, userFavorites),
session
};
};

View File

@@ -14,7 +14,7 @@
<SeasonLayout active_index={current_month-1}>
<Recipes slot=recipes>
{#each rand_array(data.season) as recipe}
<Card {recipe} {current_month}></Card>
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</Recipes>
</SeasonLayout>

View File

@@ -1,10 +0,0 @@
import type { PageLoad } from "./$types";
export async function load({ fetch }) {
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
const item_season = await res_season.json();
return {
season: item_season,
};
};

View File

@@ -0,0 +1,19 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_season = await fetch(`/api/rezepte/items/in_season/` + params.month);
const item_season = await res_season.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
month: params.month,
season: addFavoriteStatusToRecipes(item_season, userFavorites),
session
};
};

View File

@@ -12,7 +12,7 @@
<SeasonLayout active_index={data.month -1}>
<Recipes slot=recipes>
{#each rand_array(data.season) as recipe}
<Card {recipe} icon_override=true></Card>
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</Recipes>
</SeasonLayout>

View File

@@ -1,12 +0,0 @@
import type { PageLoad } from "./$types";
export async function load({ fetch, params }) {
const res_season = await fetch(`/api/rezepte/items/in_season/` + params.month);
const res_all_brief = await fetch(`/api/rezepte/items/all_brief`);
const item_season = await res_season.json();
const item_all_brief = await res_all_brief.json();
return {
month: params.month,
season: item_season,
};
};

View File

@@ -0,0 +1,19 @@
import type { PageServerLoad } from "./$types";
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const res_tag = await fetch(`/api/rezepte/items/tag/${params.tag}`);
const items_tag = await res_tag.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
getUserFavorites(fetch, locals),
locals.auth()
]);
return {
tag: params.tag,
recipes: addFavoriteStatusToRecipes(items_tag, userFavorites),
session
};
};

View File

@@ -18,7 +18,7 @@
<section>
<Recipes>
{#each rand_array(data.recipes) as recipe}
<Card {recipe} {current_month}></Card>
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
{/each}
</Recipes>
</section>

View File

@@ -1,10 +0,0 @@
import type { PageLoad } from "./$types";
export async function load({ fetch, params }) {
const res_tag = await fetch(`/api/rezepte/items/tag/${params.tag}`);
const items_tag = await res_tag.json();
return {
tag: params.tag,
recipes: items_tag
}
};