refactor: simplify Card HTML and extract search filter composable
All checks were successful
CI / update (push) Successful in 1m23s
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:
@@ -39,13 +39,6 @@ const img_alt = $derived(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.card_anchor{
|
|
||||||
border-radius: var(--radius-card);
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.card-main-link {
|
.card-main-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -76,11 +69,12 @@ const img_alt = $derived(
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
background-color: var(--blue);
|
background-color: var(--blue);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
/* Position/size overrides for global g-icon-badge */
|
/* Position/size overrides for global g-icon-badge */
|
||||||
.icon{
|
.icon{
|
||||||
@@ -106,9 +100,11 @@ const img_alt = $derived(
|
|||||||
.backdrop_blur{
|
.backdrop_blur{
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
.div_image,
|
.card-image{
|
||||||
.div_div_image{
|
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
height: 255px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -116,12 +112,6 @@ const img_alt = $derived(
|
|||||||
border-top-left-radius: inherit;
|
border-top-left-radius: inherit;
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
}
|
}
|
||||||
.div_div_image{
|
|
||||||
height: 255px;
|
|
||||||
position: absolute;
|
|
||||||
width: 300px;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover,
|
.card:hover,
|
||||||
.card:focus-within{
|
.card:focus-within{
|
||||||
@@ -238,18 +228,15 @@ const img_alt = $derived(
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class=card_anchor class:search_me={search} data-tags="[{recipe.tags}]">
|
<div class="card" class:search_me={search} class:margin_right={do_margin_right} data-tags="[{recipe.tags}]">
|
||||||
<div class="card" class:margin_right={do_margin_right}>
|
|
||||||
<a href="{routePrefix}/{recipe.short_name}" class="card-main-link" aria-label="View recipe: {recipe.name}">
|
<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>
|
<span class="visually-hidden">View recipe: {recipe.name}</span>
|
||||||
</a>
|
</a>
|
||||||
<div class=div_div_image >
|
<div class="card-image" style="background-image:url(https://bocken.org/static/rezepte/placeholder/{img_name})">
|
||||||
<div class=div_image style="background-image:url(https://bocken.org/static/rezepte/placeholder/{img_name})">
|
<noscript>
|
||||||
<noscript>
|
<img class="image backdrop_blur" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{img_alt}"/>
|
||||||
<img class="image backdrop_blur" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{img_alt}"/>
|
</noscript>
|
||||||
</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}/>
|
||||||
<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>
|
</div>
|
||||||
{#if showFavoriteIndicator && isFavorite}
|
{#if showFavoriteIndicator && isFavorite}
|
||||||
<div class="favorite-indicator">❤️</div>
|
<div class="favorite-indicator">❤️</div>
|
||||||
@@ -281,4 +268,3 @@ const img_alt = $derived(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|||||||
27
src/lib/js/searchFilter.svelte.ts
Normal file
27
src/lib/js/searchFilter.svelte.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,32 +2,18 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import Recipes from '$lib/components/Recipes.svelte';
|
import Recipes from '$lib/components/Recipes.svelte';
|
||||||
import Search from '$lib/components/Search.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 { data } = $props<{ data: PageData }>();
|
||||||
let current_month = new Date().getMonth() + 1;
|
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 isEnglish = $derived(data.lang === 'en');
|
||||||
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
|
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
|
||||||
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
|
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
|
||||||
|
|
||||||
// Search state
|
const { filtered: filteredRecipes, handleSearchResults } = createSearchFilter(() => data.recipes);
|
||||||
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));
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import Recipes from '$lib/components/Recipes.svelte';
|
import Recipes from '$lib/components/Recipes.svelte';
|
||||||
import Card from '$lib/components/Card.svelte';
|
import Card from '$lib/components/Card.svelte';
|
||||||
import Search from '$lib/components/Search.svelte';
|
import Search from '$lib/components/Search.svelte';
|
||||||
|
import { createSearchFilter } from '$lib/js/searchFilter.svelte';
|
||||||
|
|
||||||
let { data } = $props<{ data: PageData }>();
|
let { data } = $props<{ data: PageData }>();
|
||||||
let current_month = new Date().getMonth() + 1;
|
let current_month = new Date().getMonth() + 1;
|
||||||
|
|
||||||
@@ -28,23 +30,7 @@
|
|||||||
recipesLink: isEnglish ? 'recipe' : 'Rezept'
|
recipesLink: isEnglish ? 'recipe' : 'Rezept'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search state
|
const { filtered: filteredFavorites, handleSearchResults } = createSearchFilter(() => data.favorites);
|
||||||
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));
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,33 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import Recipes from '$lib/components/Recipes.svelte';
|
import Recipes from '$lib/components/Recipes.svelte';
|
||||||
let { data } = $props<{ data: PageData }>();
|
import Card from '$lib/components/Card.svelte';
|
||||||
let current_month = new Date().getMonth() + 1;
|
|
||||||
import Card from '$lib/components/Card.svelte'
|
|
||||||
import Search from '$lib/components/Search.svelte';
|
import Search from '$lib/components/Search.svelte';
|
||||||
import { rand_array } from '$lib/js/randomize';
|
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 isEnglish = $derived(data.lang === 'en');
|
||||||
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
|
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
|
||||||
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
|
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
|
||||||
|
|
||||||
// Search state
|
const { filtered: filteredRecipes, handleSearchResults } = createSearchFilter(() => data.recipes);
|
||||||
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));
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user