fix: resolve all 1008 svelte-check type errors across codebase

Add type annotations, JSDoc types, null checks, and proper generics
to eliminate all svelte-check errors. Key changes include:
- Type $state(null) variables to avoid 'never' inference
- Add JSDoc typedefs for plain <script> components
- Fix mongoose model typing with Model<any> to avoid union complexity
- Add App.Error/App.PageState interfaces in app.d.ts
- Fix tuple types to array types in types.ts
- Type catch block errors and API handler params
- Add null safety for DOM queries and optional chaining
- Add standard line-clamp property alongside -webkit- prefix
This commit is contained in:
2026-03-02 08:40:15 +01:00
parent 9c50133dfe
commit d2ac67fb44
125 changed files with 871 additions and 600 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ let {
do_margin_right = false,
isFavorite = false,
showFavoriteIndicator = false,
loading_strat = "lazy",
loading_strat = "lazy" as "lazy" | "eager",
routePrefix = '/rezepte',
translationStatus = undefined
} = $props();
+1 -1
View File
@@ -395,7 +395,7 @@ input::placeholder{
</style>
<div class=card href="" >
<div class=card>
<input class=icon placeholder=🥫 bind:value={card_data.icon}/>
{#if image_preview_url}
@@ -43,6 +43,7 @@
}, 200);
}
/** @param {string} category */
function handleCategorySelect(category) {
if (useAndLogic) {
// AND mode: single select
@@ -62,6 +63,7 @@
}
}
/** @param {KeyboardEvent} event */
function handleKeyDown(event) {
if (event.key === 'Enter') {
event.preventDefault();
@@ -77,6 +79,7 @@
}
}
/** @param {string} category */
function handleRemove(category) {
if (useAndLogic) {
onChange(null);
@@ -7,7 +7,7 @@
icon_override = false,
isFavorite = false,
showFavoriteIndicator = false,
loading_strat = "lazy",
loading_strat = "lazy" as "lazy" | "eager",
routePrefix = '/rezepte'
} = $props();
@@ -24,9 +24,9 @@
const isInSeason = $derived(icon_override || recipe.season?.includes(current_month));
function activateTransitions(event) {
const img = event.currentTarget.querySelector('.img-wrap img');
if (img) img.style.viewTransitionName = `recipe-${recipe.short_name}-img`;
function activateTransitions(event: MouseEvent) {
const img = (event.currentTarget as HTMLElement)?.querySelector('.img-wrap img') as HTMLElement | null;
if (img) (img.style as any).viewTransitionName = `recipe-${recipe.short_name}-img`;
}
</script>
<style>
@@ -12,19 +12,19 @@ import { do_on_key } from '$lib/components/recipes/do_on_key.js'
import { portions } from '$lib/js/portions_store.js'
import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelte'
let portions_local = $state()
portions.subscribe((p) => {
let portions_local = $state<string | undefined>()
portions.subscribe((p: any) => {
portions_local = p
});
export function set_portions(){
portions.update((p) => portions_local)
portions.update((_p: any) => portions_local)
}
let { lang = 'de' as 'de' | 'en', ingredients = $bindable() } = $props<{ lang?: 'de' | 'en', ingredients: any }>();
// Translation strings
const t = {
const t: Record<string, Record<string, string>> = {
de: {
portions: 'Portionen:',
ingredients: 'Zutaten',
@@ -219,7 +219,7 @@ function openAddToReferenceModal(list_index: number, position: 'before' | 'after
}
}
function get_sublist_index(sublist_name, list){
function get_sublist_index(sublist_name: string, list: any[]){
for(var i =0; i < list.length; i++){
if(list[i].name == sublist_name){
return i
@@ -227,16 +227,16 @@ function get_sublist_index(sublist_name, list){
}
return -1
}
export function show_modal_edit_subheading_ingredient(list_index){
export function show_modal_edit_subheading_ingredient(list_index: number){
edit_heading.name = ingredients[list_index].name
edit_heading.list_index = list_index
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`)
el.showModal()
edit_heading.list_index = String(list_index)
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`) as HTMLDialogElement | null;
if (el) el.showModal()
}
export function edit_subheading_and_close_modal(){
ingredients[edit_heading.list_index].name = edit_heading.name
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`)
el.close()
ingredients[Number(edit_heading.list_index)].name = edit_heading.name
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`) as HTMLDialogElement | null;
if (el) el.close()
}
function handleIngredientModalCancel() {
@@ -265,7 +265,7 @@ export function add_new_ingredient(){
ingredients[list_index].list.push({ ...new_ingredient})
ingredients = ingredients //tells svelte to update dom
}
export function remove_list(list_index){
export function remove_list(list_index: number){
if(ingredients[list_index].list.length > 1){
const response = confirm(t[lang].confirmDeleteList);
if(!response){
@@ -275,18 +275,18 @@ export function remove_list(list_index){
ingredients.splice(list_index, 1);
ingredients = ingredients //tells svelte to update dom
}
export function remove_ingredient(list_index, ingredient_index){
export function remove_ingredient(list_index: number, ingredient_index: number){
ingredients[list_index].list.splice(ingredient_index, 1)
ingredients = ingredients //tells svelte to update dom
}
export function show_modal_edit_ingredient(list_index, ingredient_index){
export function show_modal_edit_ingredient(list_index: number, ingredient_index: number){
edit_ingredient = {...ingredients[list_index].list[ingredient_index]}
edit_ingredient.list_index = list_index
edit_ingredient.ingredient_index = ingredient_index
edit_ingredient.list_index = String(list_index)
edit_ingredient.ingredient_index = String(ingredient_index)
edit_ingredient.sublist = ingredients[list_index].name
const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`);
modal_el.showModal();
const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement | null;
if (modal_el) modal_el.showModal();
}
export function edit_ingredient_and_close_modal(){
// Check if we're adding to or editing a reference
@@ -333,12 +333,12 @@ export function edit_ingredient_and_close_modal(){
};
} else {
// Normal edit behavior
ingredients[edit_ingredient.list_index].list[edit_ingredient.ingredient_index] = {
ingredients[Number(edit_ingredient.list_index)].list[Number(edit_ingredient.ingredient_index)] = {
amount: edit_ingredient.amount,
unit: edit_ingredient.unit,
name: edit_ingredient.name,
}
ingredients[edit_ingredient.list_index].name = edit_ingredient.sublist
ingredients[Number(edit_ingredient.list_index)].name = edit_ingredient.sublist
}
const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement;
if (modal_el) {
@@ -346,7 +346,7 @@ export function edit_ingredient_and_close_modal(){
setTimeout(() => modal_el.close(), 0);
}
}
export function update_list_position(list_index, direction){
export function update_list_position(list_index: number, direction: number){
if(direction == 1){
if(list_index == 0){
return
@@ -361,7 +361,7 @@ export function update_list_position(list_index, direction){
}
ingredients = ingredients //tells svelte to update dom
}
export function update_ingredient_position(list_index, ingredient_index, direction){
export function update_ingredient_position(list_index: number, ingredient_index: number, direction: number){
if(direction == 1){
if(ingredient_index == 0){
return
@@ -738,7 +738,7 @@ h3{
<div class=list_wrapper >
<h4>{t[lang].portions}</h4>
<p contenteditable type="text" bind:innerText={portions_local} onblur={set_portions}></p>
<p contenteditable bind:innerText={portions_local} onblur={set_portions}></p>
<h2>{t[lang].ingredients}</h2>
{#each ingredients as list, list_index}
@@ -13,7 +13,7 @@ import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelt
let { lang = 'de' as 'de' | 'en', instructions = $bindable(), add_info = $bindable() } = $props<{ lang?: 'de' | 'en', instructions: any, add_info: any }>();
// Translation strings
const t = {
const t: Record<string, Record<string, string>> = {
de: {
preparation: 'Vorbereitung:',
bulkFermentation: 'Stockgare:',
@@ -211,7 +211,7 @@ function openAddToReferenceModal(list_index: number, position: 'before' | 'after
}
}
function get_sublist_index(sublist_name, list){
function get_sublist_index(sublist_name: string, list: any[]){
for(var i =0; i < list.length; i++){
if(list[i].name == sublist_name){
return i
@@ -219,7 +219,7 @@ function get_sublist_index(sublist_name, list){
}
return -1
}
export function remove_list(list_index){
export function remove_list(list_index: number){
if(instructions[list_index].steps.length > 1){
const response = confirm(t[lang].confirmDeleteList);
if(!response){
@@ -245,13 +245,13 @@ export function add_new_step(){
else{
instructions[list_index].steps.push(new_step.step)
}
const el = document.querySelector("#step")
el.innerHTML = ""
const el = document.querySelector("#step") as HTMLElement | null;
if (el) el.innerHTML = ""
new_step.step = ""
instructions = instructions //tells svelte to update dom
}
export function remove_step(list_index, step_index){
export function remove_step(list_index: number, step_index: number){
instructions[list_index].steps.splice(step_index, 1)
instructions = instructions //tells svelte to update dom
}
@@ -262,15 +262,15 @@ let edit_step = $state({
list_index: 0,
step_index: 0,
});
export function show_modal_edit_step(list_index, step_index){
export function show_modal_edit_step(list_index: number, step_index: number){
edit_step = {
step: instructions[list_index].steps[step_index],
name: instructions[list_index].name,
list_index,
step_index,
}
edit_step.list_index = list_index
edit_step.step_index = step_index
const modal_el = document.querySelector(`#edit_step_modal-${lang}`);
modal_el.showModal();
const modal_el = document.querySelector(`#edit_step_modal-${lang}`) as HTMLDialogElement | null;
if (modal_el) modal_el.showModal();
}
export function edit_step_and_close_modal(){
@@ -321,17 +321,17 @@ export function edit_step_and_close_modal(){
}
}
export function show_modal_edit_subheading_step(list_index){
export function show_modal_edit_subheading_step(list_index: number){
edit_heading.name = instructions[list_index].name
edit_heading.list_index = list_index
const el = document.querySelector(`#edit_subheading_steps_modal-${lang}`)
el.showModal()
edit_heading.list_index = String(list_index)
const el = document.querySelector(`#edit_subheading_steps_modal-${lang}`) as HTMLDialogElement | null;
if (el) el.showModal()
}
export function edit_subheading_steps_and_close_modal(){
instructions[edit_heading.list_index].name = edit_heading.name
const modal_el = document.querySelector("#edit_subheading_steps_modal");
modal_el.close();
instructions[Number(edit_heading.list_index)].name = edit_heading.name
const modal_el = document.querySelector(`#edit_subheading_steps_modal-${lang}`) as HTMLDialogElement | null;
if (modal_el) modal_el.close();
}
function handleStepModalCancel() {
@@ -347,19 +347,19 @@ function handleStepModalCancel() {
export function clear_step(){
const el = document.querySelector("#step")
if(el.innerHTML == step_placeholder){
const el = document.querySelector("#step") as HTMLElement | null;
if(el && el.innerHTML == step_placeholder){
el.innerHTML = ""
}
}
export function add_placeholder(){
const el = document.querySelector("#step")
if(el.innerHTML == ""){
const el = document.querySelector("#step") as HTMLElement | null;
if(el && el.innerHTML == ""){
el.innerHTML = step_placeholder
}
}
export function update_list_position(list_index, direction){
export function update_list_position(list_index: number, direction: number){
if(direction == 1){
if(list_index == 0){
return
@@ -374,7 +374,7 @@ export function update_list_position(list_index, direction){
}
instructions = instructions //tells svelte to update dom
}
export function update_step_position(list_index, step_index, direction){
export function update_step_position(list_index: number, step_index: number, direction: number){
if(direction == 1){
if(step_index == 0){
return
@@ -770,27 +770,27 @@ h3{
<div class=additional_info>
<div><h4>{t[lang].preparation}</h4>
<p contenteditable type="text" bind:innerText={add_info.preparation}></p>
<p contenteditable bind:innerText={add_info.preparation}></p>
</div>
<div><h4>{t[lang].bulkFermentation}</h4>
<p contenteditable type="text" bind:innerText={add_info.fermentation.bulk}></p>
<p contenteditable bind:innerText={add_info.fermentation.bulk}></p>
</div>
<div><h4>{t[lang].finalFermentation}</h4>
<p contenteditable type="text" bind:innerText={add_info.fermentation.final}></p>
<p contenteditable bind:innerText={add_info.fermentation.final}></p>
</div>
<div><h4>{t[lang].baking}</h4>
<div><p type="text" bind:innerText={add_info.baking.length} contenteditable placeholder="40 min..."></p></div> bei <div><p type="text" bind:innerText={add_info.baking.temperature} contenteditable placeholder=200...></p></div> °C <div><p type="text" bind:innerText={add_info.baking.mode} contenteditable placeholder="Ober-/Unterhitze..."></p></div></div>
<div><p bind:innerText={add_info.baking.length} contenteditable placeholder="40 min..."></p></div> bei <div><p bind:innerText={add_info.baking.temperature} contenteditable placeholder=200...></p></div> °C <div><p bind:innerText={add_info.baking.mode} contenteditable placeholder="Ober-/Unterhitze..."></p></div></div>
<div><h4>{t[lang].cooking}</h4>
<p contenteditable type="text" bind:innerText={add_info.cooking}></p>
<p contenteditable bind:innerText={add_info.cooking}></p>
</div>
<div><h4>{t[lang].totalTime}</h4>
<p contenteditable type="text" bind:innerText={add_info.total_time}></p>
<p contenteditable bind:innerText={add_info.total_time}></p>
</div>
</div>
@@ -13,13 +13,14 @@
// Get all current URL parameters to preserve state
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : $page.url.searchParams);
/** @param {Event} event */
function toggleHefe(event) {
// If JavaScript is available, prevent form submission and handle client-side
if (browser) {
event.preventDefault();
// Simply toggle the yeast flag in the URL
const url = new URL(window.location);
const url = new URL(window.location.href);
const yeastParam = `y${yeastId}`;
if (url.searchParams.has(yeastParam)) {
@@ -43,6 +43,7 @@
}, 200);
}
/** @param {string} icon */
function handleIconSelect(icon) {
if (useAndLogic) {
// AND mode: single select
@@ -62,6 +63,7 @@
}
}
/** @param {KeyboardEvent} event */
function handleKeyDown(event) {
if (event.key === 'Escape') {
dropdownOpen = false;
@@ -69,6 +71,7 @@
}
}
/** @param {string} icon */
function handleRemove(icon) {
if (useAndLogic) {
onChange(null);
+1 -1
View File
@@ -19,7 +19,7 @@
lang?: string,
recipes?: any[],
isLoggedIn?: boolean,
onSearchResults?: (ids: any[], categories: any[]) => void,
onSearchResults?: (ids: Set<string>, categories: Set<string>) => void,
recipesSlot?: Snippet
} = $props();
</script>
@@ -7,9 +7,10 @@ import HefeSwapper from './HefeSwapper.svelte';
let { data } = $props();
// Helper function to multiply numbers in ingredient amounts
/** @param {string} amount @param {number} multiplier */
function multiplyIngredientAmount(amount, multiplier) {
if (!amount || multiplier === 1) return amount;
return amount.replace(/(\d+(?:[\.,]\d+)?)/g, match => {
return amount.replace(/(\d+(?:[\.,]\d+)?)/g, (/** @type {string} */ match) => {
const number = match.includes(',') ? match.replace(/\./g, '').replace(',', '.') : match;
const multiplied = (parseFloat(number) * multiplier).toString();
const rounded = parseFloat(multiplied).toFixed(3);
@@ -19,6 +20,7 @@ function multiplyIngredientAmount(amount, multiplier) {
}
// Recursively flatten nested ingredient references
/** @param {any[]} items @param {string} lang @param {Set<string>} [visited] @param {number} [baseMultiplier] */
function flattenIngredientReferences(items, lang, visited = new Set(), baseMultiplier = 1) {
const result = [];
@@ -91,7 +93,7 @@ function flattenIngredientReferences(items, lang, visited = new Set(), baseMulti
} else if (item.type === 'section' || !item.type) {
// Regular section - pass through with multiplier applied to amounts
if (baseMultiplier !== 1 && item.list) {
const adjustedList = item.list.map(ingredient => ({
const adjustedList = item.list.map((/** @type {any} */ ingredient) => ({
...ingredient,
amount: multiplyIngredientAmount(ingredient.amount, baseMultiplier)
}));
@@ -138,6 +140,7 @@ let userFormWidth = $state(data.defaultForm?.width || 20);
let userFormLength = $state(data.defaultForm?.length || 30);
let userFormInnerDiameter = $state(data.defaultForm?.innerDiameter || 8);
/** @param {string} shape @param {number} diameter @param {number} width @param {number} length @param {number} innerDiameter */
function calcArea(shape, diameter, width, length, innerDiameter) {
if (shape === 'round') return Math.PI * (diameter / 2) ** 2;
if (shape === 'gugelhupf') return Math.PI * ((diameter / 2) ** 2 - (innerDiameter / 2) ** 2);
@@ -173,9 +176,10 @@ $effect(() => {
}
});
/** @param {number} value */
function updateUrl(value) {
if (browser) {
const url = new URL(window.location);
const url = new URL(window.location.href);
if (value === 1) {
url.searchParams.delete('multiplier');
} else {
@@ -196,6 +200,7 @@ const multiplierOptions = [
// Calculate yeast IDs for each yeast ingredient
const yeastIds = $derived.by(() => {
/** @type {Record<string, number>} */
const ids = {};
let yeastCounter = 0;
if (data.ingredients) {
@@ -223,17 +228,18 @@ const currentParams = $derived(browser ? new URLSearchParams(window.location.sea
onMount(() => {
if (browser) {
const urlParams = new URLSearchParams(window.location.search);
multiplier = parseFloat(urlParams.get('multiplier')) || 1;
multiplier = parseFloat(urlParams.get('multiplier') || '1') || 1;
}
})
onNavigate(() => {
if (browser) {
const urlParams = new URLSearchParams(window.location.search);
multiplier = parseFloat(urlParams.get('multiplier')) || 1;
multiplier = parseFloat(urlParams.get('multiplier') || '1') || 1;
}
})
/** @param {Event} event @param {number} value */
function handleMultiplierClick(event, value) {
if (browser) {
event.preventDefault();
@@ -244,9 +250,10 @@ function handleMultiplierClick(event, value) {
// If no JS, form will submit normally
}
/** @param {Event} event */
function handleCustomInput(event) {
if (browser) {
const value = parseFloat(event.target.value);
const value = parseFloat(/** @type {HTMLInputElement} */ (event.target).value);
if (!isNaN(value) && value > 0) {
multiplier = value;
formDriven = false;
@@ -255,6 +262,7 @@ function handleCustomInput(event) {
}
}
/** @param {Event} event */
function handleCustomSubmit(event) {
if (browser) {
event.preventDefault();
@@ -264,15 +272,16 @@ function handleCustomSubmit(event) {
}
/** @param {string} inputString */
function convertFloatsToFractions(inputString) {
// Split the input string into individual words
const words = inputString.split(' ');
// Define a helper function to check if a number is close to an integer
const isCloseToInt = (num) => Math.abs(num - Math.round(num)) < 0.001;
const isCloseToInt = (/** @type {number} */ num) => Math.abs(num - Math.round(num)) < 0.001;
// Function to convert a float to a fraction
const floatToFraction = (number) => {
const floatToFraction = (/** @type {number} */ number) => {
let bestNumerator = 0;
let bestDenominator = 1;
let minDifference = Math.abs(number);
@@ -298,11 +307,11 @@ function convertFloatsToFractions(inputString) {
};
// Iterate through the words and convert floats to fractions
const result = words.map((word) => {
const result = words.map((/** @type {string} */ word) => {
// Check if the word contains a range (e.g., "300-400")
if (word.includes('-')) {
const rangeNumbers = word.split('-');
const rangeFractions = rangeNumbers.map((num) => {
const rangeFractions = rangeNumbers.map((/** @type {string} */ num) => {
const number = parseFloat(num);
return !isNaN(number) ? floatToFraction(number) : num;
});
@@ -317,8 +326,9 @@ function convertFloatsToFractions(inputString) {
return result.join(' ');
}
/** @param {string} inputString @param {number} constant */
function multiplyNumbersInString(inputString, constant) {
return inputString.replace(/(\d+(?:[\.,]\d+)?)/g, match => {
return inputString.replace(/(\d+(?:[\.,]\d+)?)/g, (/** @type {string} */ match) => {
const number = match.includes(',') ? match.replace(/\./g, '').replace(',', '.') : match;
const multiplied = (parseFloat(number) * constant).toString();
const rounded = parseFloat(multiplied).toFixed(3);
@@ -328,9 +338,10 @@ function multiplyNumbersInString(inputString, constant) {
}
// "1-2 Kuchen (Durchmesser: 26cm", constant=2 -> "2-4 Kuchen (Durchmesser: 26cm)"
/** @param {string} inputString @param {number} constant */
function multiplyFirstAndSecondNumbers(inputString, constant) {
const regex = /(\d+(?:[\.,]\d+)?)(\s*-\s*\d+(?:[\.,]\d+)?)?/;
return inputString.replace(regex, (match, firstNumber, secondNumber) => {
return inputString.replace(regex, (/** @type {string} */ match, /** @type {string} */ firstNumber, /** @type {string} */ secondNumber) => {
const numbersToMultiply = [firstNumber];
if (secondNumber) {
numbersToMultiply.push(secondNumber.replace(/-\s*/, ''));
@@ -346,6 +357,7 @@ function multiplyFirstAndSecondNumbers(inputString, constant) {
}
/** @param {string} string @param {number} multiplier */
function adjust_amount(string, multiplier){
let temp = multiplyNumbersInString(string, multiplier)
temp = convertFloatsToFractions(temp)
@@ -4,6 +4,11 @@ let { data } = $props();
let multiplier = $state(data.multiplier || 1);
// Recursively flatten nested instruction references
/**
* @param {any[]} items
* @param {string} lang
* @param {Set<string>} visited
*/
function flattenInstructionReferences(items, lang, visited = new Set()) {
const result = [];
@@ -18,10 +18,13 @@
instructions?: any[]
} = $props();
let short_name = $state();
let password = $state();
let short_name = $state('');
let password = $state('');
let datecreated = $state(new Date());
let datemodified = $state(datecreated);
let result = $state('');
let image_preview_url = $state('');
let selected_image_file = $state<File | null>(null);
async function doPost () {
const res = await fetch('/api/add', {
@@ -64,15 +67,15 @@ input.temp{
}
</style>
<CardAdd bind:card_data={card_data}></CardAdd>
<CardAdd bind:card_data={card_data} bind:image_preview_url={image_preview_url} bind:selected_image_file={selected_image_file} {short_name}></CardAdd>
<input class=temp bind:value={short_name} placeholder="Kurzname"/>
<SeasonSelect bind:season={season}></SeasonSelect>
<SeasonSelect></SeasonSelect>
<button onclick={() => console.log(season)}>PRINTOUT season</button>
<h2>Zutaten</h2>
<CreateIngredientList bind:ingredients={ingredients}></CreateIngredientList>
<h2>Zubereitung</h2>
<CreateStepList bind:instructions={instructions} ></CreateStepList>
<CreateStepList bind:instructions={instructions} add_info={{}}></CreateStepList>
<input class=temp type="password" placeholder=Passwort bind:value={password}>
+27 -9
View File
@@ -13,7 +13,7 @@
favoritesOnly = false,
lang = 'de',
recipes = [],
onSearchResults = (matchedIds, matchedCategories) => {},
onSearchResults = (/** @type {Set<any>} */ matchedIds, /** @type {Set<any>} */ matchedCategories) => {},
isLoggedIn = false
} = $props();
@@ -32,13 +32,19 @@
let showFilters = $state(false);
// Filter data loaded from APIs
/** @type {any[]} */
let availableTags = $state([]);
/** @type {any[]} */
let availableIcons = $state([]);
// Selected filters (internal state)
/** @type {any} */
let selectedCategory = $state(null);
/** @type {any[]} */
let selectedTags = $state([]);
/** @type {any} */
let selectedIcon = $state(null);
/** @type {number[]} */
let selectedSeasons = $state([]);
let selectedFavoritesOnly = $state(false);
let useAndLogic = $state(true);
@@ -53,8 +59,9 @@
});
// Apply non-text filters (category, tags, icon, season)
/** @param {any[]} recipeList */
function applyNonTextFilters(recipeList) {
return recipeList.filter(recipe => {
return recipeList.filter((/** @type {any} */ recipe) => {
if (useAndLogic) {
// AND mode: recipe must satisfy ALL active filters
// Category filter (single value in AND mode)
@@ -91,7 +98,9 @@
return true;
} else {
// OR mode: recipe must satisfy AT LEAST ONE active filter
/** @type {any[]} */
const categoryArray = Array.isArray(selectedCategory) ? selectedCategory : (selectedCategory ? [selectedCategory] : []);
/** @type {any[]} */
const iconArray = Array.isArray(selectedIcon) ? selectedIcon : (selectedIcon ? [selectedIcon] : []);
const hasActiveFilters = categoryArray.length > 0 || selectedTags.length > 0 || iconArray.length > 0 || selectedSeasons.length > 0 || selectedFavoritesOnly;
@@ -114,6 +123,7 @@
}
// Perform search directly (no worker)
/** @param {string} query */
function performSearch(query) {
// Apply non-text filters first
const filteredByNonText = applyNonTextFilters(recipes);
@@ -121,8 +131,8 @@
// Empty query = show all (filtered) recipes
if (!query || query.trim().length === 0) {
onSearchResults(
new Set(filteredByNonText.map(r => r._id)),
new Set(filteredByNonText.map(r => r.category))
new Set(filteredByNonText.map((/** @type {any} */ r) => r._id)),
new Set(filteredByNonText.map((/** @type {any} */ r) => r.category))
);
return;
}
@@ -131,10 +141,10 @@
const searchText = query.toLowerCase().trim()
.normalize('NFD')
.replace(/\p{Diacritic}/gu, "");
const searchTerms = searchText.split(" ").filter(term => term.length > 0);
const searchTerms = searchText.split(" ").filter((/** @type {string} */ term) => term.length > 0);
// Filter recipes by text
const matched = filteredByNonText.filter(recipe => {
const matched = filteredByNonText.filter((/** @type {any} */ recipe) => {
// Build searchable string from recipe data
const searchString = [
recipe.name || '',
@@ -147,17 +157,18 @@
.replace(/&shy;|­/g, ''); // Remove soft hyphens
// All search terms must match
return searchTerms.every(term => searchString.includes(term));
return searchTerms.every((/** @type {string} */ term) => searchString.includes(term));
});
// Return matched recipe IDs and categories
onSearchResults(
new Set(matched.map(r => r._id)),
new Set(matched.map(r => r.category))
new Set(matched.map((/** @type {any} */ r) => r._id)),
new Set(matched.map((/** @type {any} */ r) => r.category))
);
}
// Build search URL with current filters
/** @param {string} query */
function buildSearchUrl(query) {
if (browser) {
const url = new URL(searchResultsUrl, window.location.origin);
@@ -185,10 +196,12 @@
}
// Filter change handlers - the effect will automatically trigger search
/** @param {any} newCategory */
function handleCategoryChange(newCategory) {
selectedCategory = newCategory;
}
/** @param {any} tag */
function handleTagToggle(tag) {
if (selectedTags.includes(tag)) {
selectedTags = selectedTags.filter(t => t !== tag);
@@ -197,18 +210,22 @@
}
}
/** @param {any} newIcon */
function handleIconChange(newIcon) {
selectedIcon = newIcon;
}
/** @param {number[]} newSeasons */
function handleSeasonChange(newSeasons) {
selectedSeasons = newSeasons;
}
/** @param {boolean} enabled */
function handleFavoritesToggle(enabled) {
selectedFavoritesOnly = enabled;
}
/** @param {boolean} useAnd */
function handleLogicModeToggle(useAnd) {
useAndLogic = useAnd;
@@ -223,6 +240,7 @@
}
}
/** @param {Event} event */
function handleSubmit(event) {
if (browser) {
// For JS-enabled browsers, prevent default and navigate programmatically
@@ -48,15 +48,18 @@
}, 200);
}
/** @param {number} monthNumber */
function handleMonthSelect(monthNumber) {
onChange([...selectedSeasons, monthNumber]);
inputValue = '';
}
/** @param {number} monthNumber */
function handleMonthRemove(monthNumber) {
onChange(selectedSeasons.filter(m => m !== monthNumber));
}
/** @param {KeyboardEvent} event */
function handleKeyDown(event) {
if (event.key === 'Enter') {
event.preventDefault();
@@ -19,11 +19,11 @@
lang?: string,
recipes?: any[],
isLoggedIn?: boolean,
onSearchResults?: (ids: any[], categories: any[]) => void,
onSearchResults?: (ids: Set<string>, categories: Set<string>) => void,
recipesSlot?: Snippet
} = $props();
let month: number = $state();
let month: number = $state(0);
</script>
<style>
a.month{
+14 -10
View File
@@ -6,32 +6,36 @@ let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "Aug
let season_local
let season_local: number[] = [];
season.subscribe((s) => {
season_local = s
season.subscribe((s: number[]) => {
season_local = s;
});
export function set_season(){
let temp = []
let temp: number[] = [];
const el = document.getElementById("labels");
if (!el) return;
for(var i = 0; i < el.children.length; i++){
if(el.children[i].children[0].children[0].checked){
if((el.children[i].children[0].children[0] as HTMLInputElement).checked){
temp.push(i+1)
}
}
season.update((s) => temp)
season.update(() => temp)
}
function write_season(season){
function write_season(season: number[]){
const el = document.getElementById("labels");
if (!el) return;
for(var i = 0; i < season.length; i++){
el.children[season[i]-1].children[0].children[0].checked = true
(el.children[season[i]-1].children[0].children[0] as HTMLInputElement).checked = true;
}
}
function toggle_checkbox_on_key(event){
event.path[0].children[0].checked = !event.path[0].children[0].checked
function toggle_checkbox_on_key(event: Event){
const target = event.target as HTMLElement;
const checkbox = target.querySelector('input[type="checkbox"]') as HTMLInputElement;
if (checkbox) checkbox.checked = !checkbox.checked;
}
onMount(() => {
write_season(season_local)
@@ -17,6 +17,7 @@
let inputValue = $state('');
let dropdownOpen = $state(false);
/** @type {HTMLDivElement | null} */
let dropdownElement = $state(null);
// Filter tags based on input
@@ -32,6 +33,7 @@
dropdownOpen = true;
}
/** @param {FocusEvent} event */
function handleInputBlur(event) {
// Delay to allow click events on dropdown items
setTimeout(() => {
@@ -40,12 +42,14 @@
}, 200);
}
/** @param {string} tag */
function handleTagSelect(tag) {
onToggle(tag);
inputValue = '';
dropdownOpen = false;
}
/** @param {KeyboardEvent} event */
function handleKeyDown(event) {
if (event.key === 'Enter') {
event.preventDefault();
@@ -16,10 +16,10 @@
if(isredirected){
return
}
document.querySelector("#img_carousel").showModal();
/** @type {HTMLDialogElement} */(document.querySelector("#img_carousel")).showModal();
}
function close_dialog_img(){
document.querySelector("#img_carousel").close();
/** @type {HTMLDialogElement} */(document.querySelector("#img_carousel")).close();
}
import Cross from "$lib/assets/icons/Cross.svelte";
import "$lib/css/action_button.css";
@@ -1,6 +1,7 @@
<script>
let { item, ondelete, onedit, isEnglish = false } = $props();
/** @param {string} url */
function getDomain(url) {
try {
return new URL(url).hostname.replace(/^www\./, '');
@@ -91,6 +92,7 @@
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
@@ -663,7 +663,7 @@ button:disabled {
{#each untranslatedBaseRecipes as baseRecipe}
<li>
<strong>{baseRecipe.name}</strong>
<a href="/de/edit/{baseRecipe.id}" target="_blank" rel="noopener noreferrer" style="margin-left: 0.5rem; color: var(--nord10);">
<a href="/de/edit/{baseRecipe.shortName}" target="_blank" rel="noopener noreferrer" style="margin-left: 0.5rem; color: var(--nord10);">
Open in new tab →
</a>
</li>