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:
@@ -21,8 +21,8 @@
|
||||
oncurrentImageRemoved?: () => void
|
||||
}>();
|
||||
|
||||
function handleImageChange(event) {
|
||||
const file = event.target.files[0];
|
||||
function handleImageChange(event: Event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
onerror?.('File size must be less than 5MB');
|
||||
@@ -38,7 +38,7 @@
|
||||
imageFile = file;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
imagePreview = e.target.result;
|
||||
imagePreview = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
} = $props();
|
||||
|
||||
let isVisible = $state(eager); // If eager=true, render immediately
|
||||
/** @type {HTMLDivElement | null} */
|
||||
let containerRef = $state(null);
|
||||
/** @type {IntersectionObserver | null} */
|
||||
let observer = $state(null);
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
} = $props();
|
||||
|
||||
let shouldLoad = $state(eager);
|
||||
/** @type {HTMLImageElement | null} */
|
||||
let imgElement = $state(null);
|
||||
let isLoaded = $state(false);
|
||||
/** @type {IntersectionObserver | null} */
|
||||
let observer = $state(null);
|
||||
|
||||
// React to eager prop changes
|
||||
@@ -33,6 +35,7 @@
|
||||
}
|
||||
|
||||
// Helper to check if element is actually visible (both horizontal and vertical)
|
||||
/** @param {HTMLElement} el */
|
||||
function isElementInViewport(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
@@ -58,6 +61,7 @@
|
||||
}
|
||||
|
||||
// Listen to both scroll events and intersection
|
||||
/** @type {HTMLElement[]} */
|
||||
let scrollContainers = [];
|
||||
|
||||
// Find parent scroll containers
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
let { data = { labels: [], datasets: [] }, title = '', height = '400px', onFilterChange = null } = $props<{ data?: any, title?: string, height?: string, onFilterChange?: ((categories: string[] | null) => void) | null }>();
|
||||
/**
|
||||
* @type {{
|
||||
* data?: { labels: string[], datasets: Array<{ label: string, data: number[] }> },
|
||||
* title?: string,
|
||||
* height?: string,
|
||||
* onFilterChange?: ((categories: string[] | null) => void) | null
|
||||
* }}
|
||||
*/
|
||||
let { data = { labels: [], datasets: [] }, title = '', height = '400px', onFilterChange = null } = $props();
|
||||
|
||||
let canvas = $state();
|
||||
let chart = $state();
|
||||
let hiddenCategories = $state(new Set()); // Track which categories are hidden
|
||||
/** @type {HTMLCanvasElement | undefined} */
|
||||
let canvas = $state(undefined);
|
||||
/** @type {Chart | null} */
|
||||
let chart = $state(null);
|
||||
|
||||
// Register Chart.js components
|
||||
Chart.register(...registerables);
|
||||
@@ -25,32 +34,39 @@
|
||||
'#ECEFF4', // Nord Light Gray
|
||||
];
|
||||
|
||||
function getCategoryColor(category, index) {
|
||||
const categoryColorMap = {
|
||||
'groceries': '#A3BE8C', // Green
|
||||
'restaurant': '#D08770', // Orange
|
||||
'transport': '#5E81AC', // Blue
|
||||
'entertainment': '#B48EAD', // Purple
|
||||
'shopping': '#EBCB8B', // Yellow
|
||||
'utilities': '#81A1C1', // Light Blue
|
||||
'healthcare': '#BF616A', // Red
|
||||
'education': '#88C0D0', // Cyan
|
||||
'travel': '#8FBCBB', // Light Cyan
|
||||
'other': '#4C566A' // Dark Gray
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
const categoryColorMap = {
|
||||
'groceries': '#A3BE8C', // Green
|
||||
'restaurant': '#D08770', // Orange
|
||||
'transport': '#5E81AC', // Blue
|
||||
'entertainment': '#B48EAD', // Purple
|
||||
'shopping': '#EBCB8B', // Yellow
|
||||
'utilities': '#81A1C1', // Light Blue
|
||||
'healthcare': '#BF616A', // Red
|
||||
'education': '#88C0D0', // Cyan
|
||||
'travel': '#8FBCBB', // Light Cyan
|
||||
'other': '#4C566A' // Dark Gray
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} category
|
||||
* @param {number} index
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCategoryColor(category, index) {
|
||||
return categoryColorMap[category] || nordColors[index % nordColors.length];
|
||||
}
|
||||
|
||||
function emitFilter() {
|
||||
if (!onFilterChange || !chart) return;
|
||||
const allVisible = chart.data.datasets.every((_, idx) => !chart.getDatasetMeta(idx).hidden);
|
||||
const c = chart;
|
||||
const allVisible = c.data.datasets.every((/** @type {any} */ _, /** @type {number} */ idx) => !c.getDatasetMeta(idx).hidden);
|
||||
if (allVisible) {
|
||||
onFilterChange(null);
|
||||
} else {
|
||||
const visible = chart.data.datasets
|
||||
.filter((_, idx) => !chart.getDatasetMeta(idx).hidden)
|
||||
.map(ds => ds.label.toLowerCase());
|
||||
const visible = c.data.datasets
|
||||
.filter((/** @type {any} */ _, /** @type {number} */ idx) => !c.getDatasetMeta(idx).hidden)
|
||||
.map((/** @type {any} */ ds) => /** @type {string} */ (ds.label ?? '').toLowerCase());
|
||||
onFilterChange(visible);
|
||||
}
|
||||
}
|
||||
@@ -64,16 +80,17 @@
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Convert $state proxy to plain arrays to avoid Chart.js property descriptor issues
|
||||
const plainLabels = [...(data.labels || [])];
|
||||
const plainDatasets = (data.datasets || []).map(ds => ({
|
||||
const plainDatasets = (data.datasets || []).map((/** @type {{ label: string, data: number[] }} */ ds) => ({
|
||||
label: ds.label,
|
||||
data: [...(ds.data || [])]
|
||||
}));
|
||||
|
||||
// Process datasets with colors and capitalize labels
|
||||
const processedDatasets = plainDatasets.map((dataset, index) => ({
|
||||
const processedDatasets = plainDatasets.map((/** @type {{ label: string, data: number[] }} */ dataset, /** @type {number} */ index) => ({
|
||||
label: dataset.label.charAt(0).toUpperCase() + dataset.label.slice(1),
|
||||
data: dataset.data,
|
||||
backgroundColor: getCategoryColor(dataset.label, index),
|
||||
@@ -130,7 +147,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
plugins: /** @type {any} */ ({
|
||||
datalabels: {
|
||||
display: false
|
||||
},
|
||||
@@ -146,28 +163,30 @@
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
onClick: (event, legendItem, legend) => {
|
||||
onClick: (/** @type {any} */ event, /** @type {{ datasetIndex?: number }} */ legendItem, /** @type {any} */ legend) => {
|
||||
const datasetIndex = legendItem.datasetIndex;
|
||||
if (datasetIndex == null || !chart) return;
|
||||
const c = chart;
|
||||
|
||||
// Check if only this dataset is currently visible
|
||||
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
|
||||
const meta = chart.getDatasetMeta(idx);
|
||||
const onlyThisVisible = c.data.datasets.every((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||
const meta = c.getDatasetMeta(idx);
|
||||
return idx === datasetIndex ? !meta.hidden : meta.hidden;
|
||||
});
|
||||
|
||||
if (onlyThisVisible) {
|
||||
// Show all categories
|
||||
chart.data.datasets.forEach((dataset, idx) => {
|
||||
chart.getDatasetMeta(idx).hidden = false;
|
||||
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||
c.getDatasetMeta(idx).hidden = false;
|
||||
});
|
||||
} else {
|
||||
// Hide all except the clicked one
|
||||
chart.data.datasets.forEach((dataset, idx) => {
|
||||
chart.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
||||
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||
c.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
||||
});
|
||||
}
|
||||
|
||||
chart.update();
|
||||
c.update();
|
||||
emitFilter();
|
||||
}
|
||||
},
|
||||
@@ -200,57 +219,58 @@
|
||||
bodyFont: {
|
||||
family: 'Inter, system-ui, sans-serif',
|
||||
size: 14,
|
||||
weight: '500'
|
||||
weight: 500
|
||||
},
|
||||
titleMarginBottom: 8,
|
||||
usePointStyle: true,
|
||||
boxPadding: 6,
|
||||
callbacks: {
|
||||
title: function(context) {
|
||||
title: function(/** @type {any} */ context) {
|
||||
return '';
|
||||
},
|
||||
label: function(context) {
|
||||
label: function(/** @type {any} */ context) {
|
||||
return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
interaction: {
|
||||
intersect: true,
|
||||
mode: 'point'
|
||||
},
|
||||
onClick: (event, activeElements) => {
|
||||
if (activeElements.length > 0) {
|
||||
onClick: (/** @type {any} */ event, /** @type {Array<{ datasetIndex: number }>} */ activeElements) => {
|
||||
if (activeElements.length > 0 && chart) {
|
||||
const c = chart;
|
||||
const datasetIndex = activeElements[0].datasetIndex;
|
||||
|
||||
// Check if only this dataset is currently visible
|
||||
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
|
||||
const meta = chart.getDatasetMeta(idx);
|
||||
const onlyThisVisible = c.data.datasets.every((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||
const meta = c.getDatasetMeta(idx);
|
||||
return idx === datasetIndex ? !meta.hidden : meta.hidden;
|
||||
});
|
||||
|
||||
if (onlyThisVisible) {
|
||||
// Show all categories
|
||||
chart.data.datasets.forEach((dataset, idx) => {
|
||||
chart.getDatasetMeta(idx).hidden = false;
|
||||
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||
c.getDatasetMeta(idx).hidden = false;
|
||||
});
|
||||
} else {
|
||||
// Hide all except the clicked one
|
||||
chart.data.datasets.forEach((dataset, idx) => {
|
||||
chart.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
||||
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||
c.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
||||
});
|
||||
}
|
||||
|
||||
chart.update();
|
||||
c.update();
|
||||
emitFilter();
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
id: 'monthlyTotals',
|
||||
afterDatasetsDraw: function(chart) {
|
||||
const ctx = chart.ctx;
|
||||
const chartArea = chart.chartArea;
|
||||
afterDatasetsDraw: function(/** @type {Chart} */ chartInstance) {
|
||||
const ctx = chartInstance.ctx;
|
||||
const chartArea = chartInstance.chartArea;
|
||||
|
||||
ctx.save();
|
||||
ctx.font = 'bold 14px Inter, system-ui, sans-serif';
|
||||
@@ -259,23 +279,26 @@
|
||||
ctx.textBaseline = 'bottom';
|
||||
|
||||
// Calculate and display monthly totals (only for visible categories)
|
||||
chart.data.labels.forEach((label, index) => {
|
||||
const labels = chartInstance.data.labels || [];
|
||||
labels.forEach((/** @type {any} */ label, /** @type {number} */ index) => {
|
||||
let total = 0;
|
||||
chart.data.datasets.forEach((dataset, datasetIndex) => {
|
||||
chartInstance.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ datasetIndex) => {
|
||||
// Only add to total if the dataset is visible
|
||||
const meta = chart.getDatasetMeta(datasetIndex);
|
||||
const meta = chartInstance.getDatasetMeta(datasetIndex);
|
||||
if (meta && !meta.hidden) {
|
||||
total += dataset.data[index] || 0;
|
||||
const val = dataset.data[index];
|
||||
total += (typeof val === 'number' ? val : 0);
|
||||
}
|
||||
});
|
||||
|
||||
if (total > 0) {
|
||||
// Get the x position for this month from any visible dataset
|
||||
/** @type {number | null} */
|
||||
let x = null;
|
||||
let maxY = chartArea.bottom;
|
||||
|
||||
for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
|
||||
const datasetMeta = chart.getDatasetMeta(datasetIndex);
|
||||
for (let datasetIndex = 0; datasetIndex < chartInstance.data.datasets.length; datasetIndex++) {
|
||||
const datasetMeta = chartInstance.getDatasetMeta(datasetIndex);
|
||||
if (datasetMeta && !datasetMeta.hidden && datasetMeta.data[index]) {
|
||||
if (x === null) {
|
||||
x = datasetMeta.data[index].x;
|
||||
@@ -309,7 +332,7 @@
|
||||
mediaQuery.addEventListener('change', handleThemeChange);
|
||||
|
||||
// Also watch for data-theme attribute changes on <html>
|
||||
const themeObserver = new MutationObserver((mutations) => {
|
||||
const themeObserver = new MutationObserver((/** @type {MutationRecord[]} */ mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
|
||||
handleThemeChange();
|
||||
|
||||
@@ -3,16 +3,31 @@
|
||||
import ProfilePicture from './ProfilePicture.svelte';
|
||||
import { formatCurrency } from '$lib/utils/formatters';
|
||||
|
||||
let debtData = {
|
||||
/**
|
||||
* @typedef {{ username: string, netAmount: number, transactions: Array<any> }} DebtEntry
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* whoOwesMe: DebtEntry[],
|
||||
* whoIOwe: DebtEntry[],
|
||||
* totalOwedToMe: number,
|
||||
* totalIOwe: number
|
||||
* }} DebtData
|
||||
*/
|
||||
|
||||
/** @type {DebtData} */
|
||||
let debtData = $state({
|
||||
whoOwesMe: [],
|
||||
whoIOwe: [],
|
||||
totalOwedToMe: 0,
|
||||
totalIOwe: 0
|
||||
};
|
||||
let loading = true;
|
||||
let error = null;
|
||||
});
|
||||
let loading = $state(true);
|
||||
/** @type {string | null} */
|
||||
let error = $state(null);
|
||||
|
||||
$: shouldHide = getShouldHide();
|
||||
let shouldHide = $derived(getShouldHide());
|
||||
|
||||
function getShouldHide() {
|
||||
const totalUsers = debtData.whoOwesMe.length + debtData.whoIOwe.length;
|
||||
@@ -32,7 +47,7 @@
|
||||
}
|
||||
debtData = await response.json();
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -47,7 +62,7 @@
|
||||
{#if !shouldHide}
|
||||
<div class="debt-breakdown">
|
||||
<h2>Debt Overview</h2>
|
||||
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Loading debt breakdown...</div>
|
||||
{:else if error}
|
||||
@@ -60,7 +75,7 @@
|
||||
<div class="total-amount positive">
|
||||
Total: {formatCurrency(debtData.totalOwedToMe, 'CHF', 'de-CH')}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="debt-list">
|
||||
{#each debtData.whoOwesMe as debt}
|
||||
<div class="debt-item">
|
||||
@@ -86,7 +101,7 @@
|
||||
<div class="total-amount negative">
|
||||
Total: {formatCurrency(debtData.totalIOwe, 'CHF', 'de-CH')}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="debt-list">
|
||||
{#each debtData.whoIOwe as debt}
|
||||
<div class="debt-item">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
totalIOwe: 0
|
||||
});
|
||||
let loading = $state(!initialBalance || !initialDebtData);
|
||||
let error = $state(null);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Use $derived instead of $effect for computed values
|
||||
let singleDebtUser = $derived.by(() => {
|
||||
@@ -69,7 +69,7 @@
|
||||
recentSplits: [...(newBalance.recentSplits || [])]
|
||||
};
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,13 +88,13 @@
|
||||
totalIOwe: newDebtData.totalIOwe || 0
|
||||
};
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatCurrency(amount) {
|
||||
function formatCurrency(amount: number) {
|
||||
return formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import ProfilePicture from './ProfilePicture.svelte';
|
||||
import EditButton from '$lib/components/EditButton.svelte';
|
||||
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
|
||||
import { getCategoryEmoji, getCategoryName, PAYMENT_CATEGORIES } from '$lib/utils/categories';
|
||||
import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters';
|
||||
|
||||
let { paymentId, onclose, onpaymentDeleted } = $props();
|
||||
@@ -12,23 +12,48 @@
|
||||
// Get session from page store
|
||||
let session = $derived($page.data?.session);
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* _id?: string,
|
||||
* title: string,
|
||||
* description?: string,
|
||||
* amount: number,
|
||||
* currency: string,
|
||||
* originalAmount?: number,
|
||||
* exchangeRate?: number,
|
||||
* paidBy: string,
|
||||
* date: string,
|
||||
* image?: string,
|
||||
* category: import('$lib/utils/categories').PaymentCategory,
|
||||
* splitMethod: string,
|
||||
* createdBy: string,
|
||||
* splits?: Array<{ username: string, amount: number, settled: boolean }>,
|
||||
* createdAt?: string,
|
||||
* updatedAt?: string
|
||||
* }} PaymentData
|
||||
*/
|
||||
|
||||
/** @type {PaymentData | null} */
|
||||
let payment = $state(null);
|
||||
let loading = $state(true);
|
||||
/** @type {string | null} */
|
||||
let error = $state(null);
|
||||
let modal = $state();
|
||||
/** @type {HTMLDivElement | undefined} */
|
||||
let modal = $state(undefined);
|
||||
|
||||
onMount(() => {
|
||||
loadPayment();
|
||||
|
||||
onMount(async () => {
|
||||
await loadPayment();
|
||||
|
||||
// Handle escape key to close modal
|
||||
/** @param {KeyboardEvent} event */
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
@@ -43,7 +68,7 @@
|
||||
const result = await response.json();
|
||||
payment = result.payment;
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -55,23 +80,27 @@
|
||||
onclose?.();
|
||||
}
|
||||
|
||||
/** @param {MouseEvent} event */
|
||||
function handleBackdropClick(event) {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {number} amount */
|
||||
function formatCurrency(amount) {
|
||||
return formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH');
|
||||
}
|
||||
|
||||
/** @param {string} dateString */
|
||||
function formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('de-CH');
|
||||
}
|
||||
|
||||
/** @param {PaymentData} payment */
|
||||
function getSplitDescription(payment) {
|
||||
if (!payment.splits || payment.splits.length === 0) return 'No splits';
|
||||
|
||||
|
||||
if (payment.splitMethod === 'equal') {
|
||||
return `Split equally among ${payment.splits.length} people`;
|
||||
} else if (payment.splitMethod === 'full') {
|
||||
@@ -103,9 +132,9 @@
|
||||
// Close modal and dispatch event to refresh data
|
||||
onpaymentDeleted?.(paymentId);
|
||||
closeModal();
|
||||
|
||||
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
deleting = false;
|
||||
}
|
||||
@@ -654,7 +683,7 @@
|
||||
.panel-content {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
.panel-header {
|
||||
padding: 1rem;
|
||||
position: sticky;
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
imageError = true;
|
||||
}
|
||||
|
||||
function getInitials(name) {
|
||||
function getInitials(name: string) {
|
||||
if (!name) return '?';
|
||||
return name.split(' ')
|
||||
.map(word => word.charAt(0))
|
||||
.map((word: string) => word.charAt(0))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.substring(0, 2);
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import ProfilePicture from './ProfilePicture.svelte';
|
||||
|
||||
let {
|
||||
splitMethod = $bindable('equal'),
|
||||
users = $bindable([]),
|
||||
amount = $bindable(0),
|
||||
users = $bindable(/** @type {string[]} */ ([])),
|
||||
amount = $bindable(/** @type {number} */ (0)),
|
||||
paidBy = $bindable(''),
|
||||
splitAmounts = $bindable({}),
|
||||
personalAmounts = $bindable({}),
|
||||
splitAmounts = $bindable(/** @type {Record<string, number>} */ ({})),
|
||||
personalAmounts = $bindable(/** @type {Record<string, number>} */ ({})),
|
||||
currentUser = $bindable(''),
|
||||
predefinedMode = $bindable(false),
|
||||
currency = $bindable('CHF')
|
||||
} = $props<{
|
||||
splitMethod?: string,
|
||||
users?: string[],
|
||||
amount?: number,
|
||||
paidBy?: string,
|
||||
splitAmounts?: Record<string, number>,
|
||||
personalAmounts?: Record<string, number>,
|
||||
currentUser?: string,
|
||||
predefinedMode?: boolean,
|
||||
currency?: string
|
||||
}>();
|
||||
} = $props();
|
||||
|
||||
let personalTotalError = $state(false);
|
||||
|
||||
@@ -30,13 +20,13 @@
|
||||
if (!paidBy) {
|
||||
return 'Paid in Full';
|
||||
}
|
||||
|
||||
|
||||
// Special handling for 2-user predefined setup
|
||||
if (predefinedMode && users.length === 2) {
|
||||
const otherUser = users.find(user => user !== paidBy);
|
||||
const otherUser = users.find((/** @type {string} */ user) => user !== paidBy);
|
||||
return otherUser ? `Paid in Full for ${otherUser}` : 'Paid in Full';
|
||||
}
|
||||
|
||||
|
||||
// General case
|
||||
if (paidBy === currentUser) {
|
||||
return 'Paid in Full by You';
|
||||
@@ -44,14 +34,14 @@
|
||||
return `Paid in Full by ${paidBy}`;
|
||||
}
|
||||
})());
|
||||
|
||||
|
||||
function calculateEqualSplits() {
|
||||
if (!amount || users.length === 0) return;
|
||||
|
||||
const amountNum = parseFloat(amount);
|
||||
|
||||
const amountNum = Number(amount);
|
||||
const splitAmount = amountNum / users.length;
|
||||
|
||||
users.forEach(user => {
|
||||
|
||||
users.forEach((/** @type {string} */ user) => {
|
||||
if (user === paidBy) {
|
||||
splitAmounts[user] = splitAmount - amountNum;
|
||||
} else {
|
||||
@@ -62,12 +52,12 @@
|
||||
|
||||
function calculateFullPayment() {
|
||||
if (!amount) return;
|
||||
|
||||
const amountNum = parseFloat(amount);
|
||||
const otherUsers = users.filter(user => user !== paidBy);
|
||||
|
||||
const amountNum = Number(amount);
|
||||
const otherUsers = users.filter((/** @type {string} */ user) => user !== paidBy);
|
||||
const amountPerOtherUser = otherUsers.length > 0 ? amountNum / otherUsers.length : 0;
|
||||
|
||||
users.forEach(user => {
|
||||
|
||||
users.forEach((/** @type {string} */ user) => {
|
||||
if (user === paidBy) {
|
||||
splitAmounts[user] = -amountNum;
|
||||
} else {
|
||||
@@ -78,20 +68,20 @@
|
||||
|
||||
function calculatePersonalEqualSplit() {
|
||||
if (!amount || users.length === 0) return;
|
||||
|
||||
const totalAmount = parseFloat(amount);
|
||||
|
||||
const totalPersonal = users.reduce((sum, user) => {
|
||||
return sum + (parseFloat(personalAmounts[user]) || 0);
|
||||
|
||||
const totalAmount = Number(amount);
|
||||
|
||||
const totalPersonal = users.reduce((/** @type {number} */ sum, /** @type {string} */ user) => {
|
||||
return sum + (Number(personalAmounts[user]) || 0);
|
||||
}, 0);
|
||||
|
||||
|
||||
const remainder = Math.max(0, totalAmount - totalPersonal);
|
||||
const equalShare = remainder / users.length;
|
||||
|
||||
users.forEach(user => {
|
||||
const personalAmount = parseFloat(personalAmounts[user]) || 0;
|
||||
|
||||
users.forEach((/** @type {string} */ user) => {
|
||||
const personalAmount = Number(personalAmounts[user]) || 0;
|
||||
const totalOwed = personalAmount + equalShare;
|
||||
|
||||
|
||||
if (user === paidBy) {
|
||||
splitAmounts[user] = totalOwed - totalAmount;
|
||||
} else {
|
||||
@@ -108,7 +98,7 @@
|
||||
} else if (splitMethod === 'personal_equal') {
|
||||
calculatePersonalEqualSplit();
|
||||
} else if (splitMethod === 'proportional') {
|
||||
users.forEach(user => {
|
||||
users.forEach((/** @type {string} */ user) => {
|
||||
if (!(user in splitAmounts)) {
|
||||
splitAmounts[user] = 0;
|
||||
}
|
||||
@@ -119,8 +109,9 @@
|
||||
// Validate and recalculate when personal amounts change
|
||||
$effect(() => {
|
||||
if (splitMethod === 'personal_equal' && personalAmounts && amount) {
|
||||
const totalPersonal = Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0);
|
||||
const totalAmount = parseFloat(amount);
|
||||
/** @type {number} */
|
||||
const totalPersonal = Object.values(personalAmounts).reduce((/** @type {number} */ sum, /** @type {number} */ val) => sum + (Number(val) || 0), 0);
|
||||
const totalAmount = Number(amount);
|
||||
personalTotalError = totalPersonal > totalAmount;
|
||||
|
||||
if (!personalTotalError) {
|
||||
@@ -138,7 +129,7 @@
|
||||
|
||||
<div class="form-section">
|
||||
<h2>Split Method</h2>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="splitMethod">How should this payment be split?</label>
|
||||
<select id="splitMethod" name="splitMethod" bind:value={splitMethod} required>
|
||||
@@ -187,11 +178,12 @@
|
||||
</div>
|
||||
{/each}
|
||||
{#if amount}
|
||||
{@const personalTotal = Object.values(personalAmounts).reduce((/** @type {number} */ sum, /** @type {number} */ val) => sum + (Number(val) || 0), 0)}
|
||||
<div class="remainder-info" class:error={personalTotalError}>
|
||||
<span>Total Personal: {currency} {Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0).toFixed(2)}</span>
|
||||
<span>Remainder to Split: {currency} {Math.max(0, parseFloat(amount) - Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0)).toFixed(2)}</span>
|
||||
<span>Total Personal: {currency} {personalTotal.toFixed(2)}</span>
|
||||
<span>Remainder to Split: {currency} {Math.max(0, Number(amount) - personalTotal).toFixed(2)}</span>
|
||||
{#if personalTotalError}
|
||||
<div class="error-message">⚠️ Personal amounts exceed total payment amount!</div>
|
||||
<div class="error-message">Warning: Personal amounts exceed total payment amount!</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function removeUser(userToRemove) {
|
||||
function removeUser(userToRemove: string) {
|
||||
if (predefinedMode) return;
|
||||
if (!canRemoveUsers) return;
|
||||
|
||||
|
||||
if (users.length > 1 && userToRemove !== currentUser) {
|
||||
users = users.filter(u => u !== userToRemove);
|
||||
users = users.filter((u: string) => u !== userToRemove);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
export let initialLatin = undefined;
|
||||
export let hasUrlLatin = false;
|
||||
/** @type {string | undefined} */
|
||||
export let href = undefined;
|
||||
|
||||
// Get the language context (must be created by parent page)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @param {boolean} [visible] - whether the PiP should be shown
|
||||
* @param {(e: Event) => void} [onload] - callback when image loads
|
||||
*/
|
||||
let { pip, src, alt = '', visible = false, onload, el = $bindable(null) } = $props();
|
||||
let { pip, src, alt = '', visible = false, onload = undefined, el = $bindable(null) } = $props();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
*/
|
||||
let { src, alt = '', mode = 'layout', children } = $props();
|
||||
|
||||
/** @type {HTMLDivElement | null} */
|
||||
let pipEl = $state(null);
|
||||
/** @type {HTMLDivElement | null} */
|
||||
let contentEl = $state(null);
|
||||
let inView = $state(false);
|
||||
|
||||
@@ -34,7 +36,7 @@
|
||||
} else {
|
||||
pip.hide();
|
||||
}
|
||||
} else {
|
||||
} else if (pipEl) {
|
||||
// Desktop (both modes): CSS handles everything
|
||||
pipEl.style.opacity = '';
|
||||
pipEl.style.transform = '';
|
||||
@@ -60,6 +62,7 @@
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
/** @type {IntersectionObserver | undefined} */
|
||||
let observer;
|
||||
if (contentEl) {
|
||||
observer = new IntersectionObserver(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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(/­|/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{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const season = writable([]);
|
||||
export const season = writable(/** @type {number[]} */ ([]));
|
||||
|
||||
@@ -166,8 +166,8 @@ async function precacheRecipeData(recipes: BriefRecipeType[]): Promise<void> {
|
||||
dataUrls.push(`/rezepte/${recipe.short_name}/__data.json`);
|
||||
|
||||
// English recipe data (if translation exists)
|
||||
if (recipe.translations?.en?.short_name) {
|
||||
dataUrls.push(`/recipes/${recipe.translations.en.short_name}/__data.json`);
|
||||
if ((recipe as any).translations?.en?.short_name) {
|
||||
dataUrls.push(`/recipes/${(recipe as any).translations.en.short_name}/__data.json`);
|
||||
}
|
||||
|
||||
// Collect metadata for subroute caching
|
||||
|
||||
@@ -39,9 +39,9 @@ export async function requireAuth(
|
||||
|
||||
return {
|
||||
nickname: session.user.nickname,
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
image: session.user.image
|
||||
name: session.user.name ?? undefined,
|
||||
email: session.user.email ?? undefined,
|
||||
image: session.user.image ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@ export async function optionalAuth(
|
||||
|
||||
return {
|
||||
nickname: session.user.nickname,
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
image: session.user.image
|
||||
name: session.user.name ?? undefined,
|
||||
email: session.user.email ?? undefined,
|
||||
image: session.user.image ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,9 +44,10 @@ export function toBrief(recipe: any, recipeLang: string): BriefRecipeType {
|
||||
icon: recipe.icon,
|
||||
description: recipe.translations.en.description,
|
||||
season: recipe.season || [],
|
||||
dateCreated: recipe.dateCreated,
|
||||
dateModified: recipe.dateModified,
|
||||
germanShortName: recipe.short_name,
|
||||
} as BriefRecipeType;
|
||||
} as unknown as BriefRecipeType;
|
||||
}
|
||||
return {
|
||||
...recipe,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { calculateNextExecutionDate } from '$lib/utils/recurring';
|
||||
|
||||
class RecurringPaymentScheduler {
|
||||
private isRunning = false;
|
||||
private task: cron.ScheduledTask | null = null;
|
||||
private task: ReturnType<typeof cron.schedule> | null = null;
|
||||
|
||||
// Start the scheduler - runs every minute to check for due payments
|
||||
start() {
|
||||
@@ -27,9 +27,8 @@ class RecurringPaymentScheduler {
|
||||
|
||||
await this.processRecurringPayments();
|
||||
}, {
|
||||
scheduled: true,
|
||||
timezone: 'Europe/Zurich' // Adjust timezone as needed
|
||||
});
|
||||
} as any);
|
||||
|
||||
console.log('[Scheduler] Recurring payments scheduler started (runs every minute)');
|
||||
}
|
||||
@@ -155,7 +154,7 @@ class RecurringPaymentScheduler {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
isScheduled: this.task !== null,
|
||||
nextRun: this.task?.nextDate()?.toISOString()
|
||||
nextRun: (this.task as any)?.nextDate?.()?.toISOString?.()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ export function getSettlementReceiver(payment: any): string {
|
||||
}
|
||||
|
||||
// Find the user who has a positive amount (the receiver)
|
||||
const receiver = payment.splits.find(split => split.amount > 0);
|
||||
const receiver = payment.splits.find((split: any) => split.amount > 0);
|
||||
if (receiver && receiver.username) {
|
||||
return receiver.username;
|
||||
}
|
||||
|
||||
// Fallback: find the user who is not the payer
|
||||
const otherUser = payment.splits.find(split => split.username !== payment.paidBy);
|
||||
const otherUser = payment.splits.find((split: any) => split.username !== payment.paidBy);
|
||||
if (otherUser && otherUser.username) {
|
||||
return otherUser.username;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user