Compare commits
2 Commits
aeec3b4865
...
2dc871c50f
Author | SHA1 | Date | |
---|---|---|---|
2dc871c50f
|
|||
88f9531a6f
|
@@ -72,7 +72,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<Search></Search>
|
<Search icon={active_icon}></Search>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<slot name=recipes></slot>
|
<slot name=recipes></slot>
|
||||||
|
@@ -1,21 +1,70 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { onNavigate } from "$app/navigation";
|
import { onNavigate } from "$app/navigation";
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import HefeSwapper from './HefeSwapper.svelte';
|
import HefeSwapper from './HefeSwapper.svelte';
|
||||||
export let data
|
export let data
|
||||||
let multiplier;
|
let multiplier = data.multiplier || 1;
|
||||||
let custom_mul = "…"
|
|
||||||
|
|
||||||
|
// Progressive enhancement - use JS if available
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Apply multiplier from URL
|
if (browser) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
multiplier = urlParams.get('multiplier') || 1;
|
multiplier = parseFloat(urlParams.get('multiplier')) || 1;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onNavigate(() => {
|
onNavigate(() => {
|
||||||
|
if (browser) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
multiplier = urlParams.get('multiplier') || 1;
|
multiplier = parseFloat(urlParams.get('multiplier')) || 1;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function handleMultiplierClick(event, value) {
|
||||||
|
if (browser) {
|
||||||
|
event.preventDefault();
|
||||||
|
multiplier = value;
|
||||||
|
|
||||||
|
// Update URL without reloading
|
||||||
|
const url = new URL(window.location);
|
||||||
|
if (value === 1) {
|
||||||
|
url.searchParams.delete('multiplier');
|
||||||
|
} else {
|
||||||
|
url.searchParams.set('multiplier', value);
|
||||||
|
}
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
|
}
|
||||||
|
// If no JS, form will submit normally
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCustomInput(event) {
|
||||||
|
if (browser) {
|
||||||
|
const value = parseFloat(event.target.value);
|
||||||
|
if (!isNaN(value) && value > 0) {
|
||||||
|
multiplier = value;
|
||||||
|
|
||||||
|
// Update URL without reloading
|
||||||
|
const url = new URL(window.location);
|
||||||
|
if (value === 1) {
|
||||||
|
url.searchParams.delete('multiplier');
|
||||||
|
} else {
|
||||||
|
url.searchParams.set('multiplier', value);
|
||||||
|
}
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCustomSubmit(event) {
|
||||||
|
if (browser) {
|
||||||
|
event.preventDefault();
|
||||||
|
// Value already updated by handleCustomInput
|
||||||
|
}
|
||||||
|
// If no JS, form will submit normally
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function convertFloatsToFractions(inputString) {
|
function convertFloatsToFractions(inputString) {
|
||||||
// Split the input string into individual words
|
// Split the input string into individual words
|
||||||
const words = inputString.split(' ');
|
const words = inputString.split(' ');
|
||||||
@@ -104,22 +153,6 @@ function adjust_amount(string, multiplier){
|
|||||||
return temp
|
return temp
|
||||||
}
|
}
|
||||||
|
|
||||||
function apply_if_not_NaN(custom){
|
|
||||||
const multipliers = [0.5, 1, 1.5, 2, 3]
|
|
||||||
if((!isNaN(custom * 1)) && custom != ""){
|
|
||||||
if(multipliers.includes(parseFloat(custom))){
|
|
||||||
multiplier = custom
|
|
||||||
custom_mul = "…"
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
custom_mul = convertFloatsToFractions(custom)
|
|
||||||
multiplier = custom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
custom_mul = "…"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleHefeToggle(event, item) {
|
function handleHefeToggle(event, item) {
|
||||||
item.name = event.detail.name;
|
item.name = event.detail.name;
|
||||||
@@ -202,9 +235,67 @@ span
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.multipliers button:last-child{
|
.custom-multiplier {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 2em;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 100ms;
|
||||||
|
color: var(--nord0);
|
||||||
|
background-color: var(--nord5);
|
||||||
|
box-shadow: 0px 0px 0.4em 0.05em rgba(0,0,0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-input {
|
||||||
|
width: 3em;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: center;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove number input arrows */
|
||||||
|
.custom-input::-webkit-outer-spin-button,
|
||||||
|
.custom-input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-input[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark){
|
||||||
|
.custom-multiplier {
|
||||||
|
color: var(--tag-font);
|
||||||
|
background-color: var(--nord6-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-multiplier:hover,
|
||||||
|
.custom-multiplier:focus-within {
|
||||||
|
scale: 1.2;
|
||||||
|
background-color: var(--orange);
|
||||||
|
box-shadow: 0px 0px 0.5em 0.1em rgba(0,0,0, 0.3);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{#if data.ingredients}
|
{#if data.ingredients}
|
||||||
@@ -216,23 +307,39 @@ span
|
|||||||
|
|
||||||
<h3>Menge anpassen:</h3>
|
<h3>Menge anpassen:</h3>
|
||||||
<div class=multipliers>
|
<div class=multipliers>
|
||||||
<button class:selected={multiplier==0.5} on:click={() => multiplier=0.5}><sup>1</sup>⁄<sub>2</sub>x</button>
|
<form method="get" style="display: inline;">
|
||||||
<button class:selected={multiplier==1} on:click={() => {multiplier=1; custom_mul="…"}}>1x</button>
|
<input type="hidden" name="multiplier" value="0.5" />
|
||||||
<button class:selected={multiplier==1.5} on:click={() => {multiplier=1.5; custom_mul="…"}}><sup>3</sup>⁄<sub>2</sub>x</button>
|
<button type="submit" class:selected={multiplier==0.5} on:click={(e) => handleMultiplierClick(e, 0.5)}>{@html "<sup>1</sup>/<sub>2</sub>x"}</button>
|
||||||
<button class:selected={multiplier==2} on:click="{() => {multiplier=2; custom_mul="…"}}">2x</button>
|
</form>
|
||||||
<button class:selected={multiplier==3} on:click="{() => {multiplier=3; custom_mul="…"}}">3x</button>
|
<form method="get" style="display: inline;">
|
||||||
<button class:selected={multiplier==custom_mul} on:click={(e) => { const el = e.composedPath()[0].children[0]; if(el){ el.focus()}}}>
|
<input type="hidden" name="multiplier" value="1" />
|
||||||
<span class:selected={multiplier==custom_mul}
|
<button type="submit" class:selected={multiplier==1} on:click={(e) => handleMultiplierClick(e, 1)}>1x</button>
|
||||||
on:focus={() => { custom_mul="" }
|
</form>
|
||||||
}
|
<form method="get" style="display: inline;">
|
||||||
on:blur="{() => { apply_if_not_NaN(custom_mul);
|
<input type="hidden" name="multiplier" value="1.5" />
|
||||||
if(custom_mul == "")
|
<button type="submit" class:selected={multiplier==1.5} on:click={(e) => handleMultiplierClick(e, 1.5)}>{@html "<sup>3</sup>/<sub>2</sub>x"}</button>
|
||||||
{custom_mul = "…"}
|
</form>
|
||||||
}}"
|
<form method="get" style="display: inline;">
|
||||||
bind:innerHTML={custom_mul}
|
<input type="hidden" name="multiplier" value="2" />
|
||||||
contenteditable > </span>
|
<button type="submit" class:selected={multiplier==2} on:click={(e) => handleMultiplierClick(e, 2)}>2x</button>
|
||||||
x
|
</form>
|
||||||
</button>
|
<form method="get" style="display: inline;">
|
||||||
|
<input type="hidden" name="multiplier" value="3" />
|
||||||
|
<button type="submit" class:selected={multiplier==3} on:click={(e) => handleMultiplierClick(e, 3)}>3x</button>
|
||||||
|
</form>
|
||||||
|
<form method="get" style="display: inline;" class="custom-multiplier" on:submit={handleCustomSubmit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="multiplier"
|
||||||
|
pattern="[0-9]+(\.[0-9]*)?"
|
||||||
|
title="Enter a positive number (e.g., 2.5, 0.75, 3.14)"
|
||||||
|
placeholder="…"
|
||||||
|
class="custom-input"
|
||||||
|
value={multiplier != 0.5 && multiplier != 1 && multiplier != 1.5 && multiplier != 2 && multiplier != 3 ? multiplier : ''}
|
||||||
|
on:input={handleCustomInput}
|
||||||
|
/>
|
||||||
|
<button type="submit" class="custom-button">x</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Zutaten</h2>
|
<h2>Zutaten</h2>
|
||||||
|
@@ -1,21 +1,90 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import "$lib/css/nordtheme.css";
|
import "$lib/css/nordtheme.css";
|
||||||
|
|
||||||
onMount(() => {
|
// Filter props for different contexts
|
||||||
|
export let category = null;
|
||||||
|
export let tag = null;
|
||||||
|
export let icon = null;
|
||||||
|
export let season = null;
|
||||||
|
export let favoritesOnly = false;
|
||||||
|
export let searchResultsUrl = '/rezepte/search';
|
||||||
|
|
||||||
|
let searchQuery = '';
|
||||||
|
|
||||||
|
// Build search URL with current filters
|
||||||
|
function buildSearchUrl(query) {
|
||||||
|
if (browser) {
|
||||||
|
const url = new URL(searchResultsUrl, window.location.origin);
|
||||||
|
if (query) url.searchParams.set('q', query);
|
||||||
|
if (category) url.searchParams.set('category', category);
|
||||||
|
if (tag) url.searchParams.set('tag', tag);
|
||||||
|
if (icon) url.searchParams.set('icon', icon);
|
||||||
|
if (season) url.searchParams.set('season', season);
|
||||||
|
if (favoritesOnly) url.searchParams.set('favorites', 'true');
|
||||||
|
return url.toString();
|
||||||
|
} else {
|
||||||
|
// Server-side fallback - return just the base path
|
||||||
|
return searchResultsUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event) {
|
||||||
|
if (browser) {
|
||||||
|
// For JS-enabled browsers, prevent default and navigate programmatically
|
||||||
|
// This allows for future enhancements like instant search
|
||||||
|
const url = buildSearchUrl(searchQuery);
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
// If no JS, form will submit normally
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
searchQuery = '';
|
||||||
|
if (browser) {
|
||||||
|
// Reset any client-side filtering if present
|
||||||
|
const recipes = document.querySelectorAll(".search_me");
|
||||||
|
recipes.forEach(recipe => {
|
||||||
|
recipe.style.display = 'flex';
|
||||||
|
recipe.classList.remove("matched-recipe");
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".media_scroller_wrapper").forEach( scroller => {
|
||||||
|
scroller.style.display= 'block'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Swap buttons for JS-enabled experience
|
||||||
|
const submitButton = document.getElementById('submit-search');
|
||||||
|
const clearButton = document.getElementById('clear-search');
|
||||||
|
|
||||||
|
if (submitButton && clearButton) {
|
||||||
|
submitButton.style.display = 'none';
|
||||||
|
clearButton.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial search value from URL if present
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const urlQuery = urlParams.get('q');
|
||||||
|
if (urlQuery) {
|
||||||
|
searchQuery = urlQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced client-side filtering (existing functionality)
|
||||||
const recipes = document.querySelectorAll(".search_me");
|
const recipes = document.querySelectorAll(".search_me");
|
||||||
const search = document.getElementById("search");
|
const search = document.getElementById("search");
|
||||||
const clearSearch = document.getElementById("clear-search");
|
|
||||||
|
|
||||||
|
if (recipes.length > 0 && search) {
|
||||||
function do_search(click_only_result=false){
|
function do_search(click_only_result=false){
|
||||||
// grab search input value
|
|
||||||
const searchText = search.value.toLowerCase().trim().normalize('NFD').replace(/\p{Diacritic}/gu, "");
|
const searchText = search.value.toLowerCase().trim().normalize('NFD').replace(/\p{Diacritic}/gu, "");
|
||||||
const searchTerms = searchText.split(" ");
|
const searchTerms = searchText.split(" ");
|
||||||
const hasFilter = searchText.length > 0;
|
const hasFilter = searchText.length > 0;
|
||||||
|
|
||||||
let scrollers_with_results = [];
|
let scrollers_with_results = [];
|
||||||
let scrollers = [];
|
let scrollers = [];
|
||||||
// for each recipe hide all but matched
|
|
||||||
recipes.forEach(recipe => {
|
recipes.forEach(recipe => {
|
||||||
const searchString = `${recipe.textContent} ${recipe.dataset.tags}`.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/­|/g, '');
|
const searchString = `${recipe.textContent} ${recipe.dataset.tags}`.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/­|/g, '');
|
||||||
const isMatch = searchTerms.every(term => searchString.includes(term));
|
const isMatch = searchTerms.every(term => searchString.includes(term));
|
||||||
@@ -35,47 +104,25 @@ onMount(() => {
|
|||||||
scrollers.filter(item => !scrollers_with_results.includes(item)).forEach( scroller => {
|
scrollers.filter(item => !scrollers_with_results.includes(item)).forEach( scroller => {
|
||||||
scroller.parentNode.style.display= 'none'
|
scroller.parentNode.style.display= 'none'
|
||||||
})
|
})
|
||||||
scroll
|
|
||||||
let items = document.querySelectorAll(".matched-recipe");
|
let items = document.querySelectorAll(".matched-recipe");
|
||||||
items = [...new Set(items)] // make unique as seasonal mediascroller can lead to duplicates
|
items = [...new Set(items)]
|
||||||
// if only one result and click_only_result is true, click it
|
|
||||||
if(click_only_result && scrollers_with_results.length == 1 && items.length == 1){
|
if(click_only_result && scrollers_with_results.length == 1 && items.length == 1){
|
||||||
// add '/rezepte' to history to not force-redirect back to recipe if going back
|
|
||||||
items[0].click();
|
items[0].click();
|
||||||
}
|
}
|
||||||
// if scrollers with results are presenet scroll first result into view
|
|
||||||
/*if(scrollers_with_results.length > 0){
|
|
||||||
scrollers_with_results[0].scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
|
|
||||||
}*/ // For now disabled because it is annoying on mobile
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
search.addEventListener("input", () => {
|
search.addEventListener("input", () => {
|
||||||
|
searchQuery = search.value;
|
||||||
do_search();
|
do_search();
|
||||||
})
|
})
|
||||||
|
|
||||||
clearSearch.addEventListener("click", () => {
|
// Initial search if URL had query
|
||||||
search.value = "";
|
if (urlQuery) {
|
||||||
recipes.forEach(recipe => {
|
|
||||||
recipe.style.display = 'flex';
|
|
||||||
recipe.classList.remove("matched-recipe");
|
|
||||||
})
|
|
||||||
document.querySelectorAll(".media_scroller_wrapper").forEach( scroller => {
|
|
||||||
scroller.style.display= 'block'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
let paramString = window.location.href.split('?')[1];
|
|
||||||
let queryString = new URLSearchParams(paramString);
|
|
||||||
|
|
||||||
for (let pair of queryString.entries()) {
|
|
||||||
if(pair[0] == 'q'){
|
|
||||||
const search = document.getElementById("search");
|
|
||||||
search.value=pair[1];
|
|
||||||
do_search(true);
|
do_search(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@@ -110,7 +157,7 @@ input::placeholder{
|
|||||||
scale: 1.02 1.02;
|
scale: 1.02 1.02;
|
||||||
filter: drop-shadow(0.4em 0.5em 1em rgba(0,0,0,0.6))
|
filter: drop-shadow(0.4em 0.5em 1em rgba(0,0,0,0.6))
|
||||||
}
|
}
|
||||||
button#clear-search {
|
.search-button {
|
||||||
all: unset;
|
all: unset;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -123,17 +170,35 @@ button#clear-search {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 180ms ease-in-out;
|
transition: color 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
button#clear-search:hover {
|
.search-button:hover {
|
||||||
color: white;
|
color: white;
|
||||||
scale: 1.1 1.1;
|
scale: 1.1 1.1;
|
||||||
}
|
}
|
||||||
button#clear-search:active{
|
.search-button:active{
|
||||||
transition: 50ms;
|
transition: 50ms;
|
||||||
scale: 0.8 0.8;
|
scale: 0.8 0.8;
|
||||||
}
|
}
|
||||||
|
.search-button svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="search js-only">
|
<form class="search" method="get" action={buildSearchUrl('')} on:submit|preventDefault={handleSubmit}>
|
||||||
<input type="text" id="search" placeholder="Suche...">
|
{#if category}<input type="hidden" name="category" value={category} />{/if}
|
||||||
<button id="clear-search">
|
{#if tag}<input type="hidden" name="tag" value={tag} />{/if}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Sucheintrag löschen</title><path d="M135.19 390.14a28.79 28.79 0 0021.68 9.86h246.26A29 29 0 00432 371.13V140.87A29 29 0 00403.13 112H156.87a28.84 28.84 0 00-21.67 9.84v0L46.33 256l88.86 134.11z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M336.67 192.33L206.66 322.34M336.67 322.34L206.66 192.33M336.67 192.33L206.66 322.34M336.67 322.34L206.66 192.33"></path></svg></button>
|
{#if icon}<input type="hidden" name="icon" value={icon} />{/if}
|
||||||
</div>
|
{#if season}<input type="hidden" name="season" value={season} />{/if}
|
||||||
|
{#if favoritesOnly}<input type="hidden" name="favorites" value="true" />{/if}
|
||||||
|
|
||||||
|
<input type="text" id="search" name="q" placeholder="Suche..." bind:value={searchQuery}>
|
||||||
|
|
||||||
|
<!-- Submit button (visible by default, hidden when JS loads) -->
|
||||||
|
<button type="submit" id="submit-search" class="search-button" style="display: flex;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" style="width: 100%; height: 100%;"><title>Suchen</title><path d="M221.09 64a157.09 157.09 0 10157.09 157.09A157.1 157.1 0 00221.09 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="m338.29 338.29 105.25 105.25"></path></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Clear button (hidden by default, shown when JS loads) -->
|
||||||
|
<button type="button" id="clear-search" class="search-button js-only" style="display: none;" on:click={clearSearch}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Sucheintrag löschen</title><path d="M135.19 390.14a28.79 28.79 0 0021.68 9.86h246.26A29 29 0 00432 371.13V140.87A29 29 0 00403.13 112H156.87a28.84 28.84 0 00-21.67 9.84v0L46.33 256l88.86 134.11z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M336.67 192.33L206.66 322.34M336.67 322.34L206.66 192.33M336.67 192.33L206.66 322.34M336.67 322.34L206.66 192.33"></path></svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@@ -41,7 +41,7 @@ a.month:hover,
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<Search></Search>
|
<Search season={active_index + 1}></Search>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<slot name=recipes></slot>
|
<slot name=recipes></slot>
|
||||||
|
74
src/routes/api/rezepte/search/+server.ts
Normal file
74
src/routes/api/rezepte/search/+server.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
import type { BriefRecipeType } from '../../../../types/types';
|
||||||
|
import { Recipe } from '../../../../models/Recipe';
|
||||||
|
import { dbConnect, dbDisconnect } from '../../../../utils/db';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||||
|
await dbConnect();
|
||||||
|
|
||||||
|
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
|
||||||
|
const category = url.searchParams.get('category');
|
||||||
|
const tag = url.searchParams.get('tag');
|
||||||
|
const icon = url.searchParams.get('icon');
|
||||||
|
const season = url.searchParams.get('season');
|
||||||
|
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build base query
|
||||||
|
let dbQuery: any = {};
|
||||||
|
|
||||||
|
// Apply filters based on context
|
||||||
|
if (category) {
|
||||||
|
dbQuery.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
dbQuery.tags = { $in: [tag] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
dbQuery.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (season) {
|
||||||
|
const seasonNum = parseInt(season);
|
||||||
|
if (!isNaN(seasonNum)) {
|
||||||
|
dbQuery.season = { $in: [seasonNum] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all recipes matching base filters
|
||||||
|
let recipes = await Recipe.find(dbQuery, 'name short_name tags category icon description season dateModified').lean() as BriefRecipeType[];
|
||||||
|
|
||||||
|
// Handle favorites filter
|
||||||
|
if (favoritesOnly && locals.session?.user) {
|
||||||
|
const User = (await import('../../../../models/User')).User;
|
||||||
|
const user = await User.findById(locals.session.user.id);
|
||||||
|
if (user && user.favoriteRecipes) {
|
||||||
|
const favoriteShortNames = user.favoriteRecipes;
|
||||||
|
recipes = recipes.filter(recipe => favoriteShortNames.includes(recipe.short_name));
|
||||||
|
} else {
|
||||||
|
recipes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply text search if query provided
|
||||||
|
if (query) {
|
||||||
|
const searchTerms = query.normalize('NFD').replace(/\p{Diacritic}/gu, "").split(" ");
|
||||||
|
|
||||||
|
recipes = recipes.filter(recipe => {
|
||||||
|
const searchString = `${recipe.name} ${recipe.description || ''} ${recipe.tags?.join(' ') || ''}`.toLowerCase()
|
||||||
|
.normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/­|/g, '');
|
||||||
|
|
||||||
|
return searchTerms.every(term => searchString.includes(term));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbDisconnect();
|
||||||
|
return json(JSON.parse(JSON.stringify(recipes)));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await dbDisconnect();
|
||||||
|
return json({ error: 'Search failed' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
@@ -1,6 +1,6 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
export async function load({ fetch, params}) {
|
export async function load({ fetch, params, url}) {
|
||||||
const res = await fetch(`/api/rezepte/items/${params.name}`);
|
const res = await fetch(`/api/rezepte/items/${params.name}`);
|
||||||
let item = await res.json();
|
let item = await res.json();
|
||||||
if(!res.ok){
|
if(!res.ok){
|
||||||
@@ -19,8 +19,12 @@ export async function load({ fetch, params}) {
|
|||||||
// Silently fail if not authenticated or other error
|
// Silently fail if not authenticated or other error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get multiplier from URL parameters
|
||||||
|
const multiplier = parseFloat(url.searchParams.get('multiplier') || '1');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
isFavorite
|
isFavorite,
|
||||||
|
multiplier
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<h1>Rezepte in Kategorie <q>{data.category}</q>:</h1>
|
<h1>Rezepte in Kategorie <q>{data.category}</q>:</h1>
|
||||||
<Search></Search>
|
<Search category={data.category}></Search>
|
||||||
<section>
|
<section>
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each rand_array(data.recipes) as recipe}
|
{#each rand_array(data.recipes) as recipe}
|
||||||
|
@@ -40,7 +40,7 @@ h1{
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Search></Search>
|
<Search favoritesOnly={true}></Search>
|
||||||
|
|
||||||
{#if data.error}
|
{#if data.error}
|
||||||
<p class="empty-state">Fehler beim Laden der Favoriten: {data.error}</p>
|
<p class="empty-state">Fehler beim Laden der Favoriten: {data.error}</p>
|
||||||
|
50
src/routes/rezepte/search/+page.server.ts
Normal file
50
src/routes/rezepte/search/+page.server.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||||
|
const query = url.searchParams.get('q') || '';
|
||||||
|
const category = url.searchParams.get('category');
|
||||||
|
const tag = url.searchParams.get('tag');
|
||||||
|
const icon = url.searchParams.get('icon');
|
||||||
|
const season = url.searchParams.get('season');
|
||||||
|
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
||||||
|
|
||||||
|
// Build API URL with filters
|
||||||
|
const apiUrl = new URL('/api/rezepte/search', url.origin);
|
||||||
|
if (query) apiUrl.searchParams.set('q', query);
|
||||||
|
if (category) apiUrl.searchParams.set('category', category);
|
||||||
|
if (tag) apiUrl.searchParams.set('tag', tag);
|
||||||
|
if (icon) apiUrl.searchParams.set('icon', icon);
|
||||||
|
if (season) apiUrl.searchParams.set('season', season);
|
||||||
|
if (favoritesOnly) apiUrl.searchParams.set('favorites', 'true');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl.toString());
|
||||||
|
const results = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
results: response.ok ? results : [],
|
||||||
|
error: response.ok ? null : results.error || 'Search failed',
|
||||||
|
filters: {
|
||||||
|
category,
|
||||||
|
tag,
|
||||||
|
icon,
|
||||||
|
season,
|
||||||
|
favoritesOnly
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
results: [],
|
||||||
|
error: 'Search failed',
|
||||||
|
filters: {
|
||||||
|
category,
|
||||||
|
tag,
|
||||||
|
icon,
|
||||||
|
season,
|
||||||
|
favoritesOnly
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
75
src/routes/rezepte/search/+page.svelte
Normal file
75
src/routes/rezepte/search/+page.svelte
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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';
|
||||||
|
export let data: PageData;
|
||||||
|
export let current_month = new Date().getMonth() + 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
.search-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--nord3);
|
||||||
|
}
|
||||||
|
.filter-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--nord2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Suchergebnisse{data.query ? ` für "${data.query}"` : ''} - Bocken Rezepte</title>
|
||||||
|
<meta name="description" content="Suchergebnisse in den Bockenschen Rezepten." />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1>Suchergebnisse</h1>
|
||||||
|
|
||||||
|
{#if data.filters.category || data.filters.tag || data.filters.icon || data.filters.season || data.filters.favoritesOnly}
|
||||||
|
<div class="filter-info">
|
||||||
|
Gefiltert nach:
|
||||||
|
{#if data.filters.category}Kategorie "{data.filters.category}"{/if}
|
||||||
|
{#if data.filters.tag}Stichwort "{data.filters.tag}"{/if}
|
||||||
|
{#if data.filters.icon}Icon "{data.filters.icon}"{/if}
|
||||||
|
{#if data.filters.season}Saison "{data.filters.season}"{/if}
|
||||||
|
{#if data.filters.favoritesOnly}Nur Favoriten{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Search
|
||||||
|
category={data.filters.category}
|
||||||
|
tag={data.filters.tag}
|
||||||
|
icon={data.filters.icon}
|
||||||
|
season={data.filters.season}
|
||||||
|
favoritesOnly={data.filters.favoritesOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if data.error}
|
||||||
|
<div class="search-info">
|
||||||
|
<p>Fehler bei der Suche: {data.error}</p>
|
||||||
|
</div>
|
||||||
|
{:else if data.query}
|
||||||
|
<div class="search-info">
|
||||||
|
<p>{data.results.length} Ergebnisse für "{data.query}"</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if data.results.length > 0}
|
||||||
|
<Recipes>
|
||||||
|
{#each data.results as recipe}
|
||||||
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true}></Card>
|
||||||
|
{/each}
|
||||||
|
</Recipes>
|
||||||
|
{:else if data.query && !data.error}
|
||||||
|
<div class="search-info">
|
||||||
|
<p>Keine Rezepte gefunden.</p>
|
||||||
|
<p>Versuche es mit anderen Suchbegriffen.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<h1>Rezepte mit Stichwort <q>{data.tag}</q>:</h1>
|
<h1>Rezepte mit Stichwort <q>{data.tag}</q>:</h1>
|
||||||
<Search></Search>
|
<Search tag={data.tag}></Search>
|
||||||
<section>
|
<section>
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each rand_array(data.recipes) as recipe}
|
{#each rand_array(data.recipes) as recipe}
|
||||||
|
Reference in New Issue
Block a user