refactor: simplify Card HTML and extract search filter composable
All checks were successful
CI / update (push) Successful in 1m23s

- Remove unnecessary wrapper divs in Card component (.card_anchor, .div_div_image)
- Flatten Card HTML from 4 levels to 2 levels of nesting
- Create reusable createSearchFilter composable in $lib/js/searchFilter.svelte.ts
- Apply search filter composable to category, tag, and favorites pages
This commit is contained in:
2026-01-25 14:47:26 +01:00
parent 940f9f14a2
commit 5824993b18
5 changed files with 54 additions and 83 deletions

View File

@@ -39,13 +39,6 @@ const img_alt = $derived(
);
</script>
<style>
.card_anchor{
border-radius: var(--radius-card);
cursor: pointer;
display: inline-block;
text-decoration: none;
color: inherit;
}
.card-main-link {
position: absolute;
inset: 0;
@@ -76,11 +69,12 @@ const img_alt = $derived(
width: 300px;
border-radius: var(--radius-card);
background-size: contain;
display: flex;
display: inline-flex;
flex-direction: column;
justify-content: end;
background-color: var(--blue);
box-shadow: var(--shadow-lg);
color: inherit;
}
/* Position/size overrides for global g-icon-badge */
.icon{
@@ -106,9 +100,11 @@ const img_alt = $derived(
.backdrop_blur{
backdrop-filter: blur(10px);
}
.div_image,
.div_div_image{
.card-image{
width: 300px;
height: 255px;
position: absolute;
top: 0;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
@@ -116,12 +112,6 @@ const img_alt = $derived(
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.div_div_image{
height: 255px;
position: absolute;
width: 300px;
top: 0;
}
.card:hover,
.card:focus-within{
@@ -238,19 +228,16 @@ const img_alt = $derived(
}
</style>
<div class=card_anchor class:search_me={search} data-tags="[{recipe.tags}]">
<div class="card" class:margin_right={do_margin_right}>
<div class="card" class:search_me={search} class:margin_right={do_margin_right} data-tags="[{recipe.tags}]">
<a href="{routePrefix}/{recipe.short_name}" class="card-main-link" aria-label="View recipe: {recipe.name}">
<span class="visually-hidden">View recipe: {recipe.name}</span>
</a>
<div class=div_div_image >
<div class=div_image style="background-image:url(https://bocken.org/static/rezepte/placeholder/{img_name})">
<div class="card-image" style="background-image:url(https://bocken.org/static/rezepte/placeholder/{img_name})">
<noscript>
<img class="image backdrop_blur" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{img_alt}"/>
</noscript>
<img class="image backdrop_blur" class:blur={!isloaded} src={'https://bocken.org/static/rezepte/thumb/' + img_name} loading={loading_strat} alt="{img_alt}" onload={() => isloaded=true}/>
</div>
</div>
{#if showFavoriteIndicator && isFavorite}
<div class="favorite-indicator">❤️</div>
{/if}
@@ -281,4 +268,3 @@ const img_alt = $derived(
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,27 @@
/**
* Shared search filter composable for recipe list pages.
* Extracts duplicated search state logic from multiple pages.
*/
type Recipe = { _id: string; [key: string]: any };
export function createSearchFilter<T extends Recipe>(getRecipes: () => T[]) {
let matchedRecipeIds = $state(new Set<string>());
let hasActiveSearch = $state(false);
function handleSearchResults(ids: Set<string>, _categories?: Set<string>) {
matchedRecipeIds = ids;
hasActiveSearch = ids.size < getRecipes().length;
}
const filtered = $derived.by(() => {
if (!hasActiveSearch) return getRecipes();
return getRecipes().filter(r => matchedRecipeIds.has(r._id));
});
return {
get filtered() { return filtered; },
get hasActiveSearch() { return hasActiveSearch; },
handleSearchResults
};
}

View File

@@ -2,32 +2,18 @@
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
import Search from '$lib/components/Search.svelte';
import Card from '$lib/components/Card.svelte';
import { rand_array } from '$lib/js/randomize';
import { createSearchFilter } from '$lib/js/searchFilter.svelte';
let { data } = $props<{ data: PageData }>();
let current_month = new Date().getMonth() + 1;
import Card from '$lib/components/Card.svelte'
import { rand_array } from '$lib/js/randomize';
const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
// Search state
let matchedRecipeIds = $state(new Set());
let hasActiveSearch = $state(false);
// Handle search results from Search component
function handleSearchResults(ids, categories) {
matchedRecipeIds = ids;
hasActiveSearch = ids.size < data.recipes.length;
}
// Filter recipes based on search
const filteredRecipes = $derived.by(() => {
if (!hasActiveSearch) {
return data.recipes;
}
return data.recipes.filter(r => matchedRecipeIds.has(r._id));
});
const { filtered: filteredRecipes, handleSearchResults } = createSearchFilter(() => data.recipes);
</script>
<style>
h1 {

View File

@@ -4,6 +4,8 @@
import Recipes from '$lib/components/Recipes.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
import { createSearchFilter } from '$lib/js/searchFilter.svelte';
let { data } = $props<{ data: PageData }>();
let current_month = new Date().getMonth() + 1;
@@ -28,23 +30,7 @@
recipesLink: isEnglish ? 'recipe' : 'Rezept'
});
// Search state
let matchedRecipeIds = $state(new Set());
let hasActiveSearch = $state(false);
// Handle search results from Search component
function handleSearchResults(ids, categories) {
matchedRecipeIds = ids;
hasActiveSearch = ids.size < data.favorites.length;
}
// Filter recipes based on search
const filteredFavorites = $derived.by(() => {
if (!hasActiveSearch) {
return data.favorites;
}
return data.favorites.filter(r => matchedRecipeIds.has(r._id));
});
const { filtered: filteredFavorites, handleSearchResults } = createSearchFilter(() => data.favorites);
</script>
<style>

View File

@@ -1,33 +1,19 @@
<script lang="ts">
import type { PageData } from './$types';
import Recipes from '$lib/components/Recipes.svelte';
let { data } = $props<{ data: PageData }>();
let current_month = new Date().getMonth() + 1;
import Card from '$lib/components/Card.svelte'
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
import { rand_array } from '$lib/js/randomize';
import { createSearchFilter } from '$lib/js/searchFilter.svelte';
let { data } = $props<{ data: PageData }>();
let current_month = new Date().getMonth() + 1;
const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
// Search state
let matchedRecipeIds = $state(new Set());
let hasActiveSearch = $state(false);
// Handle search results from Search component
function handleSearchResults(ids, categories) {
matchedRecipeIds = ids;
hasActiveSearch = ids.size < data.recipes.length;
}
// Filter recipes based on search
const filteredRecipes = $derived.by(() => {
if (!hasActiveSearch) {
return data.recipes;
}
return data.recipes.filter(r => matchedRecipeIds.has(r._id));
});
const { filtered: filteredRecipes, handleSearchResults } = createSearchFilter(() => data.recipes);
</script>
<style>
h1 {