fix: resolve all 1008 svelte-check type errors across codebase
CI / update (push) Successful in 1m54s
CI / update (push) Successful in 1m54s
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:
Vendored
+9
-1
@@ -4,11 +4,19 @@ import type { Session } from "@auth/sveltekit";
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
interface Error {
|
||||||
|
message: string;
|
||||||
|
details?: string;
|
||||||
|
bibleQuote?: any;
|
||||||
|
lang?: string;
|
||||||
|
}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
auth(): Promise<Session | null>;
|
auth(): Promise<Session | null>;
|
||||||
}
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
|
interface PageState {
|
||||||
|
paymentId?: string | null;
|
||||||
|
}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-12
@@ -18,7 +18,7 @@ await dbConnect().then(() => {
|
|||||||
// Don't crash the server - API routes will attempt reconnection
|
// Don't crash the server - API routes will attempt reconnection
|
||||||
});
|
});
|
||||||
|
|
||||||
async function authorization({ event, resolve }) {
|
async function authorization({ event, resolve }: Parameters<Handle>[0]) {
|
||||||
const session = await event.locals.auth();
|
const session = await event.locals.auth();
|
||||||
|
|
||||||
// Protect rezepte routes
|
// Protect rezepte routes
|
||||||
@@ -28,11 +28,10 @@ async function authorization({ event, resolve }) {
|
|||||||
const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
|
const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
|
||||||
redirect(303, `/login?callbackUrl=${callbackUrl}`);
|
redirect(303, `/login?callbackUrl=${callbackUrl}`);
|
||||||
}
|
}
|
||||||
else if (!session.user.groups.includes('rezepte_users')) {
|
else if (!session.user?.groups?.includes('rezepte_users')) {
|
||||||
error(403, {
|
error(403, {
|
||||||
message: 'Zugriff verweigert',
|
message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
||||||
details: 'Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
} as any);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,19 +41,17 @@ async function authorization({ event, resolve }) {
|
|||||||
// For API routes, return 401 instead of redirecting
|
// For API routes, return 401 instead of redirecting
|
||||||
if (event.url.pathname.startsWith('/api/cospend')) {
|
if (event.url.pathname.startsWith('/api/cospend')) {
|
||||||
error(401, {
|
error(401, {
|
||||||
message: 'Anmeldung erforderlich',
|
message: 'Anmeldung erforderlich. Du musst angemeldet sein, um auf diesen Bereich zugreifen zu können.'
|
||||||
details: 'Du musst angemeldet sein, um auf diesen Bereich zugreifen zu können.'
|
} as any);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// For page routes, redirect to login
|
// For page routes, redirect to login
|
||||||
const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
|
const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
|
||||||
redirect(303, `/login?callbackUrl=${callbackUrl}`);
|
redirect(303, `/login?callbackUrl=${callbackUrl}`);
|
||||||
}
|
}
|
||||||
else if (!session.user.groups.includes('cospend')) {
|
else if (!session.user?.groups?.includes('cospend')) {
|
||||||
error(403, {
|
error(403, {
|
||||||
message: 'Zugriff verweigert',
|
message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
||||||
details: 'Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
} as any);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
oncurrentImageRemoved?: () => void
|
oncurrentImageRemoved?: () => void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function handleImageChange(event) {
|
function handleImageChange(event: Event) {
|
||||||
const file = event.target.files[0];
|
const file = (event.target as HTMLInputElement).files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
onerror?.('File size must be less than 5MB');
|
onerror?.('File size must be less than 5MB');
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
imageFile = file;
|
imageFile = file;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
imagePreview = e.target.result;
|
imagePreview = e.target?.result as string;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isVisible = $state(eager); // If eager=true, render immediately
|
let isVisible = $state(eager); // If eager=true, render immediately
|
||||||
|
/** @type {HTMLDivElement | null} */
|
||||||
let containerRef = $state(null);
|
let containerRef = $state(null);
|
||||||
|
/** @type {IntersectionObserver | null} */
|
||||||
let observer = $state(null);
|
let observer = $state(null);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let shouldLoad = $state(eager);
|
let shouldLoad = $state(eager);
|
||||||
|
/** @type {HTMLImageElement | null} */
|
||||||
let imgElement = $state(null);
|
let imgElement = $state(null);
|
||||||
let isLoaded = $state(false);
|
let isLoaded = $state(false);
|
||||||
|
/** @type {IntersectionObserver | null} */
|
||||||
let observer = $state(null);
|
let observer = $state(null);
|
||||||
|
|
||||||
// React to eager prop changes
|
// React to eager prop changes
|
||||||
@@ -33,6 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to check if element is actually visible (both horizontal and vertical)
|
// Helper to check if element is actually visible (both horizontal and vertical)
|
||||||
|
/** @param {HTMLElement} el */
|
||||||
function isElementInViewport(el) {
|
function isElementInViewport(el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||||
@@ -58,6 +61,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Listen to both scroll events and intersection
|
// Listen to both scroll events and intersection
|
||||||
|
/** @type {HTMLElement[]} */
|
||||||
let scrollContainers = [];
|
let scrollContainers = [];
|
||||||
|
|
||||||
// Find parent scroll containers
|
// Find parent scroll containers
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
<script lang="ts">
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Chart, registerables } from 'chart.js';
|
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();
|
/** @type {HTMLCanvasElement | undefined} */
|
||||||
let chart = $state();
|
let canvas = $state(undefined);
|
||||||
let hiddenCategories = $state(new Set()); // Track which categories are hidden
|
/** @type {Chart | null} */
|
||||||
|
let chart = $state(null);
|
||||||
|
|
||||||
// Register Chart.js components
|
// Register Chart.js components
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
@@ -25,32 +34,39 @@
|
|||||||
'#ECEFF4', // Nord Light Gray
|
'#ECEFF4', // Nord Light Gray
|
||||||
];
|
];
|
||||||
|
|
||||||
function getCategoryColor(category, index) {
|
/** @type {Record<string, string>} */
|
||||||
const categoryColorMap = {
|
const categoryColorMap = {
|
||||||
'groceries': '#A3BE8C', // Green
|
'groceries': '#A3BE8C', // Green
|
||||||
'restaurant': '#D08770', // Orange
|
'restaurant': '#D08770', // Orange
|
||||||
'transport': '#5E81AC', // Blue
|
'transport': '#5E81AC', // Blue
|
||||||
'entertainment': '#B48EAD', // Purple
|
'entertainment': '#B48EAD', // Purple
|
||||||
'shopping': '#EBCB8B', // Yellow
|
'shopping': '#EBCB8B', // Yellow
|
||||||
'utilities': '#81A1C1', // Light Blue
|
'utilities': '#81A1C1', // Light Blue
|
||||||
'healthcare': '#BF616A', // Red
|
'healthcare': '#BF616A', // Red
|
||||||
'education': '#88C0D0', // Cyan
|
'education': '#88C0D0', // Cyan
|
||||||
'travel': '#8FBCBB', // Light Cyan
|
'travel': '#8FBCBB', // Light Cyan
|
||||||
'other': '#4C566A' // Dark Gray
|
'other': '#4C566A' // Dark Gray
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} category
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getCategoryColor(category, index) {
|
||||||
return categoryColorMap[category] || nordColors[index % nordColors.length];
|
return categoryColorMap[category] || nordColors[index % nordColors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitFilter() {
|
function emitFilter() {
|
||||||
if (!onFilterChange || !chart) return;
|
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) {
|
if (allVisible) {
|
||||||
onFilterChange(null);
|
onFilterChange(null);
|
||||||
} else {
|
} else {
|
||||||
const visible = chart.data.datasets
|
const visible = c.data.datasets
|
||||||
.filter((_, idx) => !chart.getDatasetMeta(idx).hidden)
|
.filter((/** @type {any} */ _, /** @type {number} */ idx) => !c.getDatasetMeta(idx).hidden)
|
||||||
.map(ds => ds.label.toLowerCase());
|
.map((/** @type {any} */ ds) => /** @type {string} */ (ds.label ?? '').toLowerCase());
|
||||||
onFilterChange(visible);
|
onFilterChange(visible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,16 +80,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
// Convert $state proxy to plain arrays to avoid Chart.js property descriptor issues
|
// Convert $state proxy to plain arrays to avoid Chart.js property descriptor issues
|
||||||
const plainLabels = [...(data.labels || [])];
|
const plainLabels = [...(data.labels || [])];
|
||||||
const plainDatasets = (data.datasets || []).map(ds => ({
|
const plainDatasets = (data.datasets || []).map((/** @type {{ label: string, data: number[] }} */ ds) => ({
|
||||||
label: ds.label,
|
label: ds.label,
|
||||||
data: [...(ds.data || [])]
|
data: [...(ds.data || [])]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Process datasets with colors and capitalize labels
|
// 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),
|
label: dataset.label.charAt(0).toUpperCase() + dataset.label.slice(1),
|
||||||
data: dataset.data,
|
data: dataset.data,
|
||||||
backgroundColor: getCategoryColor(dataset.label, index),
|
backgroundColor: getCategoryColor(dataset.label, index),
|
||||||
@@ -130,7 +147,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: /** @type {any} */ ({
|
||||||
datalabels: {
|
datalabels: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
@@ -146,28 +163,30 @@
|
|||||||
weight: 'bold'
|
weight: 'bold'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick: (event, legendItem, legend) => {
|
onClick: (/** @type {any} */ event, /** @type {{ datasetIndex?: number }} */ legendItem, /** @type {any} */ legend) => {
|
||||||
const datasetIndex = legendItem.datasetIndex;
|
const datasetIndex = legendItem.datasetIndex;
|
||||||
|
if (datasetIndex == null || !chart) return;
|
||||||
|
const c = chart;
|
||||||
|
|
||||||
// Check if only this dataset is currently visible
|
// Check if only this dataset is currently visible
|
||||||
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
|
const onlyThisVisible = c.data.datasets.every((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||||
const meta = chart.getDatasetMeta(idx);
|
const meta = c.getDatasetMeta(idx);
|
||||||
return idx === datasetIndex ? !meta.hidden : meta.hidden;
|
return idx === datasetIndex ? !meta.hidden : meta.hidden;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onlyThisVisible) {
|
if (onlyThisVisible) {
|
||||||
// Show all categories
|
// Show all categories
|
||||||
chart.data.datasets.forEach((dataset, idx) => {
|
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||||
chart.getDatasetMeta(idx).hidden = false;
|
c.getDatasetMeta(idx).hidden = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Hide all except the clicked one
|
// Hide all except the clicked one
|
||||||
chart.data.datasets.forEach((dataset, idx) => {
|
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||||
chart.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
c.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
chart.update();
|
c.update();
|
||||||
emitFilter();
|
emitFilter();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -200,57 +219,58 @@
|
|||||||
bodyFont: {
|
bodyFont: {
|
||||||
family: 'Inter, system-ui, sans-serif',
|
family: 'Inter, system-ui, sans-serif',
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: '500'
|
weight: 500
|
||||||
},
|
},
|
||||||
titleMarginBottom: 8,
|
titleMarginBottom: 8,
|
||||||
usePointStyle: true,
|
usePointStyle: true,
|
||||||
boxPadding: 6,
|
boxPadding: 6,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: function(context) {
|
title: function(/** @type {any} */ context) {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
label: function(context) {
|
label: function(/** @type {any} */ context) {
|
||||||
return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
|
return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
interaction: {
|
interaction: {
|
||||||
intersect: true,
|
intersect: true,
|
||||||
mode: 'point'
|
mode: 'point'
|
||||||
},
|
},
|
||||||
onClick: (event, activeElements) => {
|
onClick: (/** @type {any} */ event, /** @type {Array<{ datasetIndex: number }>} */ activeElements) => {
|
||||||
if (activeElements.length > 0) {
|
if (activeElements.length > 0 && chart) {
|
||||||
|
const c = chart;
|
||||||
const datasetIndex = activeElements[0].datasetIndex;
|
const datasetIndex = activeElements[0].datasetIndex;
|
||||||
|
|
||||||
// Check if only this dataset is currently visible
|
// Check if only this dataset is currently visible
|
||||||
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
|
const onlyThisVisible = c.data.datasets.every((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||||
const meta = chart.getDatasetMeta(idx);
|
const meta = c.getDatasetMeta(idx);
|
||||||
return idx === datasetIndex ? !meta.hidden : meta.hidden;
|
return idx === datasetIndex ? !meta.hidden : meta.hidden;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onlyThisVisible) {
|
if (onlyThisVisible) {
|
||||||
// Show all categories
|
// Show all categories
|
||||||
chart.data.datasets.forEach((dataset, idx) => {
|
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||||
chart.getDatasetMeta(idx).hidden = false;
|
c.getDatasetMeta(idx).hidden = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Hide all except the clicked one
|
// Hide all except the clicked one
|
||||||
chart.data.datasets.forEach((dataset, idx) => {
|
c.data.datasets.forEach((/** @type {any} */ dataset, /** @type {number} */ idx) => {
|
||||||
chart.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
c.getDatasetMeta(idx).hidden = idx !== datasetIndex;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
chart.update();
|
c.update();
|
||||||
emitFilter();
|
emitFilter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [{
|
plugins: [{
|
||||||
id: 'monthlyTotals',
|
id: 'monthlyTotals',
|
||||||
afterDatasetsDraw: function(chart) {
|
afterDatasetsDraw: function(/** @type {Chart} */ chartInstance) {
|
||||||
const ctx = chart.ctx;
|
const ctx = chartInstance.ctx;
|
||||||
const chartArea = chart.chartArea;
|
const chartArea = chartInstance.chartArea;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = 'bold 14px Inter, system-ui, sans-serif';
|
ctx.font = 'bold 14px Inter, system-ui, sans-serif';
|
||||||
@@ -259,23 +279,26 @@
|
|||||||
ctx.textBaseline = 'bottom';
|
ctx.textBaseline = 'bottom';
|
||||||
|
|
||||||
// Calculate and display monthly totals (only for visible categories)
|
// 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;
|
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
|
// Only add to total if the dataset is visible
|
||||||
const meta = chart.getDatasetMeta(datasetIndex);
|
const meta = chartInstance.getDatasetMeta(datasetIndex);
|
||||||
if (meta && !meta.hidden) {
|
if (meta && !meta.hidden) {
|
||||||
total += dataset.data[index] || 0;
|
const val = dataset.data[index];
|
||||||
|
total += (typeof val === 'number' ? val : 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
// Get the x position for this month from any visible dataset
|
// Get the x position for this month from any visible dataset
|
||||||
|
/** @type {number | null} */
|
||||||
let x = null;
|
let x = null;
|
||||||
let maxY = chartArea.bottom;
|
let maxY = chartArea.bottom;
|
||||||
|
|
||||||
for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
|
for (let datasetIndex = 0; datasetIndex < chartInstance.data.datasets.length; datasetIndex++) {
|
||||||
const datasetMeta = chart.getDatasetMeta(datasetIndex);
|
const datasetMeta = chartInstance.getDatasetMeta(datasetIndex);
|
||||||
if (datasetMeta && !datasetMeta.hidden && datasetMeta.data[index]) {
|
if (datasetMeta && !datasetMeta.hidden && datasetMeta.data[index]) {
|
||||||
if (x === null) {
|
if (x === null) {
|
||||||
x = datasetMeta.data[index].x;
|
x = datasetMeta.data[index].x;
|
||||||
@@ -309,7 +332,7 @@
|
|||||||
mediaQuery.addEventListener('change', handleThemeChange);
|
mediaQuery.addEventListener('change', handleThemeChange);
|
||||||
|
|
||||||
// Also watch for data-theme attribute changes on <html>
|
// 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) {
|
for (const mutation of mutations) {
|
||||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
|
if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
|
||||||
handleThemeChange();
|
handleThemeChange();
|
||||||
|
|||||||
@@ -3,16 +3,31 @@
|
|||||||
import ProfilePicture from './ProfilePicture.svelte';
|
import ProfilePicture from './ProfilePicture.svelte';
|
||||||
import { formatCurrency } from '$lib/utils/formatters';
|
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: [],
|
whoOwesMe: [],
|
||||||
whoIOwe: [],
|
whoIOwe: [],
|
||||||
totalOwedToMe: 0,
|
totalOwedToMe: 0,
|
||||||
totalIOwe: 0
|
totalIOwe: 0
|
||||||
};
|
});
|
||||||
let loading = true;
|
let loading = $state(true);
|
||||||
let error = null;
|
/** @type {string | null} */
|
||||||
|
let error = $state(null);
|
||||||
|
|
||||||
$: shouldHide = getShouldHide();
|
let shouldHide = $derived(getShouldHide());
|
||||||
|
|
||||||
function getShouldHide() {
|
function getShouldHide() {
|
||||||
const totalUsers = debtData.whoOwesMe.length + debtData.whoIOwe.length;
|
const totalUsers = debtData.whoOwesMe.length + debtData.whoIOwe.length;
|
||||||
@@ -32,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
debtData = await response.json();
|
debtData = await response.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err.message;
|
error = err instanceof Error ? err.message : String(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -47,7 +62,7 @@
|
|||||||
{#if !shouldHide}
|
{#if !shouldHide}
|
||||||
<div class="debt-breakdown">
|
<div class="debt-breakdown">
|
||||||
<h2>Debt Overview</h2>
|
<h2>Debt Overview</h2>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="loading">Loading debt breakdown...</div>
|
<div class="loading">Loading debt breakdown...</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
@@ -60,7 +75,7 @@
|
|||||||
<div class="total-amount positive">
|
<div class="total-amount positive">
|
||||||
Total: {formatCurrency(debtData.totalOwedToMe, 'CHF', 'de-CH')}
|
Total: {formatCurrency(debtData.totalOwedToMe, 'CHF', 'de-CH')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="debt-list">
|
<div class="debt-list">
|
||||||
{#each debtData.whoOwesMe as debt}
|
{#each debtData.whoOwesMe as debt}
|
||||||
<div class="debt-item">
|
<div class="debt-item">
|
||||||
@@ -86,7 +101,7 @@
|
|||||||
<div class="total-amount negative">
|
<div class="total-amount negative">
|
||||||
Total: {formatCurrency(debtData.totalIOwe, 'CHF', 'de-CH')}
|
Total: {formatCurrency(debtData.totalIOwe, 'CHF', 'de-CH')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="debt-list">
|
<div class="debt-list">
|
||||||
{#each debtData.whoIOwe as debt}
|
{#each debtData.whoIOwe as debt}
|
||||||
<div class="debt-item">
|
<div class="debt-item">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
totalIOwe: 0
|
totalIOwe: 0
|
||||||
});
|
});
|
||||||
let loading = $state(!initialBalance || !initialDebtData);
|
let loading = $state(!initialBalance || !initialDebtData);
|
||||||
let error = $state(null);
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
// Use $derived instead of $effect for computed values
|
// Use $derived instead of $effect for computed values
|
||||||
let singleDebtUser = $derived.by(() => {
|
let singleDebtUser = $derived.by(() => {
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
recentSplits: [...(newBalance.recentSplits || [])]
|
recentSplits: [...(newBalance.recentSplits || [])]
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err.message;
|
error = err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
totalIOwe: newDebtData.totalIOwe || 0
|
totalIOwe: newDebtData.totalIOwe || 0
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err.message;
|
error = err instanceof Error ? err.message : String(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(amount) {
|
function formatCurrency(amount: number) {
|
||||||
return formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH');
|
return formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import ProfilePicture from './ProfilePicture.svelte';
|
import ProfilePicture from './ProfilePicture.svelte';
|
||||||
import EditButton from '$lib/components/EditButton.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';
|
import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters';
|
||||||
|
|
||||||
let { paymentId, onclose, onpaymentDeleted } = $props();
|
let { paymentId, onclose, onpaymentDeleted } = $props();
|
||||||
@@ -12,23 +12,48 @@
|
|||||||
// Get session from page store
|
// Get session from page store
|
||||||
let session = $derived($page.data?.session);
|
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 payment = $state(null);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
/** @type {string | null} */
|
||||||
let error = $state(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
|
// Handle escape key to close modal
|
||||||
|
/** @param {KeyboardEvent} event */
|
||||||
function handleKeydown(event) {
|
function handleKeydown(event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeydown);
|
document.addEventListener('keydown', handleKeydown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeydown);
|
document.removeEventListener('keydown', handleKeydown);
|
||||||
};
|
};
|
||||||
@@ -43,7 +68,7 @@
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
payment = result.payment;
|
payment = result.payment;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err.message;
|
error = err instanceof Error ? err.message : String(err);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -55,23 +80,27 @@
|
|||||||
onclose?.();
|
onclose?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {MouseEvent} event */
|
||||||
function handleBackdropClick(event) {
|
function handleBackdropClick(event) {
|
||||||
if (event.target === event.currentTarget) {
|
if (event.target === event.currentTarget) {
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} amount */
|
||||||
function formatCurrency(amount) {
|
function formatCurrency(amount) {
|
||||||
return formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH');
|
return formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} dateString */
|
||||||
function formatDate(dateString) {
|
function formatDate(dateString) {
|
||||||
return new Date(dateString).toLocaleDateString('de-CH');
|
return new Date(dateString).toLocaleDateString('de-CH');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {PaymentData} payment */
|
||||||
function getSplitDescription(payment) {
|
function getSplitDescription(payment) {
|
||||||
if (!payment.splits || payment.splits.length === 0) return 'No splits';
|
if (!payment.splits || payment.splits.length === 0) return 'No splits';
|
||||||
|
|
||||||
if (payment.splitMethod === 'equal') {
|
if (payment.splitMethod === 'equal') {
|
||||||
return `Split equally among ${payment.splits.length} people`;
|
return `Split equally among ${payment.splits.length} people`;
|
||||||
} else if (payment.splitMethod === 'full') {
|
} else if (payment.splitMethod === 'full') {
|
||||||
@@ -103,9 +132,9 @@
|
|||||||
// Close modal and dispatch event to refresh data
|
// Close modal and dispatch event to refresh data
|
||||||
onpaymentDeleted?.(paymentId);
|
onpaymentDeleted?.(paymentId);
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err.message;
|
error = err instanceof Error ? err.message : String(err);
|
||||||
} finally {
|
} finally {
|
||||||
deleting = false;
|
deleting = false;
|
||||||
}
|
}
|
||||||
@@ -654,7 +683,7 @@
|
|||||||
.panel-content {
|
.panel-content {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
imageError = true;
|
imageError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitials(name) {
|
function getInitials(name: string) {
|
||||||
if (!name) return '?';
|
if (!name) return '?';
|
||||||
return name.split(' ')
|
return name.split(' ')
|
||||||
.map(word => word.charAt(0))
|
.map((word: string) => word.charAt(0))
|
||||||
.join('')
|
.join('')
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.substring(0, 2);
|
.substring(0, 2);
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
<script lang="ts">
|
<script>
|
||||||
import ProfilePicture from './ProfilePicture.svelte';
|
import ProfilePicture from './ProfilePicture.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
splitMethod = $bindable('equal'),
|
splitMethod = $bindable('equal'),
|
||||||
users = $bindable([]),
|
users = $bindable(/** @type {string[]} */ ([])),
|
||||||
amount = $bindable(0),
|
amount = $bindable(/** @type {number} */ (0)),
|
||||||
paidBy = $bindable(''),
|
paidBy = $bindable(''),
|
||||||
splitAmounts = $bindable({}),
|
splitAmounts = $bindable(/** @type {Record<string, number>} */ ({})),
|
||||||
personalAmounts = $bindable({}),
|
personalAmounts = $bindable(/** @type {Record<string, number>} */ ({})),
|
||||||
currentUser = $bindable(''),
|
currentUser = $bindable(''),
|
||||||
predefinedMode = $bindable(false),
|
predefinedMode = $bindable(false),
|
||||||
currency = $bindable('CHF')
|
currency = $bindable('CHF')
|
||||||
} = $props<{
|
} = $props();
|
||||||
splitMethod?: string,
|
|
||||||
users?: string[],
|
|
||||||
amount?: number,
|
|
||||||
paidBy?: string,
|
|
||||||
splitAmounts?: Record<string, number>,
|
|
||||||
personalAmounts?: Record<string, number>,
|
|
||||||
currentUser?: string,
|
|
||||||
predefinedMode?: boolean,
|
|
||||||
currency?: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let personalTotalError = $state(false);
|
let personalTotalError = $state(false);
|
||||||
|
|
||||||
@@ -30,13 +20,13 @@
|
|||||||
if (!paidBy) {
|
if (!paidBy) {
|
||||||
return 'Paid in Full';
|
return 'Paid in Full';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for 2-user predefined setup
|
// Special handling for 2-user predefined setup
|
||||||
if (predefinedMode && users.length === 2) {
|
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';
|
return otherUser ? `Paid in Full for ${otherUser}` : 'Paid in Full';
|
||||||
}
|
}
|
||||||
|
|
||||||
// General case
|
// General case
|
||||||
if (paidBy === currentUser) {
|
if (paidBy === currentUser) {
|
||||||
return 'Paid in Full by You';
|
return 'Paid in Full by You';
|
||||||
@@ -44,14 +34,14 @@
|
|||||||
return `Paid in Full by ${paidBy}`;
|
return `Paid in Full by ${paidBy}`;
|
||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
|
|
||||||
function calculateEqualSplits() {
|
function calculateEqualSplits() {
|
||||||
if (!amount || users.length === 0) return;
|
if (!amount || users.length === 0) return;
|
||||||
|
|
||||||
const amountNum = parseFloat(amount);
|
const amountNum = Number(amount);
|
||||||
const splitAmount = amountNum / users.length;
|
const splitAmount = amountNum / users.length;
|
||||||
|
|
||||||
users.forEach(user => {
|
users.forEach((/** @type {string} */ user) => {
|
||||||
if (user === paidBy) {
|
if (user === paidBy) {
|
||||||
splitAmounts[user] = splitAmount - amountNum;
|
splitAmounts[user] = splitAmount - amountNum;
|
||||||
} else {
|
} else {
|
||||||
@@ -62,12 +52,12 @@
|
|||||||
|
|
||||||
function calculateFullPayment() {
|
function calculateFullPayment() {
|
||||||
if (!amount) return;
|
if (!amount) return;
|
||||||
|
|
||||||
const amountNum = parseFloat(amount);
|
const amountNum = Number(amount);
|
||||||
const otherUsers = users.filter(user => user !== paidBy);
|
const otherUsers = users.filter((/** @type {string} */ user) => user !== paidBy);
|
||||||
const amountPerOtherUser = otherUsers.length > 0 ? amountNum / otherUsers.length : 0;
|
const amountPerOtherUser = otherUsers.length > 0 ? amountNum / otherUsers.length : 0;
|
||||||
|
|
||||||
users.forEach(user => {
|
users.forEach((/** @type {string} */ user) => {
|
||||||
if (user === paidBy) {
|
if (user === paidBy) {
|
||||||
splitAmounts[user] = -amountNum;
|
splitAmounts[user] = -amountNum;
|
||||||
} else {
|
} else {
|
||||||
@@ -78,20 +68,20 @@
|
|||||||
|
|
||||||
function calculatePersonalEqualSplit() {
|
function calculatePersonalEqualSplit() {
|
||||||
if (!amount || users.length === 0) return;
|
if (!amount || users.length === 0) return;
|
||||||
|
|
||||||
const totalAmount = parseFloat(amount);
|
const totalAmount = Number(amount);
|
||||||
|
|
||||||
const totalPersonal = users.reduce((sum, user) => {
|
const totalPersonal = users.reduce((/** @type {number} */ sum, /** @type {string} */ user) => {
|
||||||
return sum + (parseFloat(personalAmounts[user]) || 0);
|
return sum + (Number(personalAmounts[user]) || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const remainder = Math.max(0, totalAmount - totalPersonal);
|
const remainder = Math.max(0, totalAmount - totalPersonal);
|
||||||
const equalShare = remainder / users.length;
|
const equalShare = remainder / users.length;
|
||||||
|
|
||||||
users.forEach(user => {
|
users.forEach((/** @type {string} */ user) => {
|
||||||
const personalAmount = parseFloat(personalAmounts[user]) || 0;
|
const personalAmount = Number(personalAmounts[user]) || 0;
|
||||||
const totalOwed = personalAmount + equalShare;
|
const totalOwed = personalAmount + equalShare;
|
||||||
|
|
||||||
if (user === paidBy) {
|
if (user === paidBy) {
|
||||||
splitAmounts[user] = totalOwed - totalAmount;
|
splitAmounts[user] = totalOwed - totalAmount;
|
||||||
} else {
|
} else {
|
||||||
@@ -108,7 +98,7 @@
|
|||||||
} else if (splitMethod === 'personal_equal') {
|
} else if (splitMethod === 'personal_equal') {
|
||||||
calculatePersonalEqualSplit();
|
calculatePersonalEqualSplit();
|
||||||
} else if (splitMethod === 'proportional') {
|
} else if (splitMethod === 'proportional') {
|
||||||
users.forEach(user => {
|
users.forEach((/** @type {string} */ user) => {
|
||||||
if (!(user in splitAmounts)) {
|
if (!(user in splitAmounts)) {
|
||||||
splitAmounts[user] = 0;
|
splitAmounts[user] = 0;
|
||||||
}
|
}
|
||||||
@@ -119,8 +109,9 @@
|
|||||||
// Validate and recalculate when personal amounts change
|
// Validate and recalculate when personal amounts change
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (splitMethod === 'personal_equal' && personalAmounts && amount) {
|
if (splitMethod === 'personal_equal' && personalAmounts && amount) {
|
||||||
const totalPersonal = Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0);
|
/** @type {number} */
|
||||||
const totalAmount = parseFloat(amount);
|
const totalPersonal = Object.values(personalAmounts).reduce((/** @type {number} */ sum, /** @type {number} */ val) => sum + (Number(val) || 0), 0);
|
||||||
|
const totalAmount = Number(amount);
|
||||||
personalTotalError = totalPersonal > totalAmount;
|
personalTotalError = totalPersonal > totalAmount;
|
||||||
|
|
||||||
if (!personalTotalError) {
|
if (!personalTotalError) {
|
||||||
@@ -138,7 +129,7 @@
|
|||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h2>Split Method</h2>
|
<h2>Split Method</h2>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="splitMethod">How should this payment be split?</label>
|
<label for="splitMethod">How should this payment be split?</label>
|
||||||
<select id="splitMethod" name="splitMethod" bind:value={splitMethod} required>
|
<select id="splitMethod" name="splitMethod" bind:value={splitMethod} required>
|
||||||
@@ -187,11 +178,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if amount}
|
{#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}>
|
<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>Total Personal: {currency} {personalTotal.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>Remainder to Split: {currency} {Math.max(0, Number(amount) - personalTotal).toFixed(2)}</span>
|
||||||
{#if personalTotalError}
|
{#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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeUser(userToRemove) {
|
function removeUser(userToRemove: string) {
|
||||||
if (predefinedMode) return;
|
if (predefinedMode) return;
|
||||||
if (!canRemoveUsers) return;
|
if (!canRemoveUsers) return;
|
||||||
|
|
||||||
if (users.length > 1 && userToRemove !== currentUser) {
|
if (users.length > 1 && userToRemove !== currentUser) {
|
||||||
users = users.filter(u => u !== userToRemove);
|
users = users.filter((u: string) => u !== userToRemove);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
export let initialLatin = undefined;
|
export let initialLatin = undefined;
|
||||||
export let hasUrlLatin = false;
|
export let hasUrlLatin = false;
|
||||||
|
/** @type {string | undefined} */
|
||||||
export let href = undefined;
|
export let href = undefined;
|
||||||
|
|
||||||
// Get the language context (must be created by parent page)
|
// Get the language context (must be created by parent page)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* @param {boolean} [visible] - whether the PiP should be shown
|
* @param {boolean} [visible] - whether the PiP should be shown
|
||||||
* @param {(e: Event) => void} [onload] - callback when image loads
|
* @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>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
*/
|
*/
|
||||||
let { src, alt = '', mode = 'layout', children } = $props();
|
let { src, alt = '', mode = 'layout', children } = $props();
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement | null} */
|
||||||
let pipEl = $state(null);
|
let pipEl = $state(null);
|
||||||
|
/** @type {HTMLDivElement | null} */
|
||||||
let contentEl = $state(null);
|
let contentEl = $state(null);
|
||||||
let inView = $state(false);
|
let inView = $state(false);
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
} else {
|
} else {
|
||||||
pip.hide();
|
pip.hide();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (pipEl) {
|
||||||
// Desktop (both modes): CSS handles everything
|
// Desktop (both modes): CSS handles everything
|
||||||
pipEl.style.opacity = '';
|
pipEl.style.opacity = '';
|
||||||
pipEl.style.transform = '';
|
pipEl.style.transform = '';
|
||||||
@@ -60,6 +62,7 @@
|
|||||||
|
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
|
/** @type {IntersectionObserver | undefined} */
|
||||||
let observer;
|
let observer;
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
observer = new IntersectionObserver(
|
observer = new IntersectionObserver(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ let {
|
|||||||
do_margin_right = false,
|
do_margin_right = false,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
showFavoriteIndicator = false,
|
showFavoriteIndicator = false,
|
||||||
loading_strat = "lazy",
|
loading_strat = "lazy" as "lazy" | "eager",
|
||||||
routePrefix = '/rezepte',
|
routePrefix = '/rezepte',
|
||||||
translationStatus = undefined
|
translationStatus = undefined
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ input::placeholder{
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<div class=card href="" >
|
<div class=card>
|
||||||
|
|
||||||
<input class=icon placeholder=🥫 bind:value={card_data.icon}/>
|
<input class=icon placeholder=🥫 bind:value={card_data.icon}/>
|
||||||
{#if image_preview_url}
|
{#if image_preview_url}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} category */
|
||||||
function handleCategorySelect(category) {
|
function handleCategorySelect(category) {
|
||||||
if (useAndLogic) {
|
if (useAndLogic) {
|
||||||
// AND mode: single select
|
// AND mode: single select
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {KeyboardEvent} event */
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -77,6 +79,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} category */
|
||||||
function handleRemove(category) {
|
function handleRemove(category) {
|
||||||
if (useAndLogic) {
|
if (useAndLogic) {
|
||||||
onChange(null);
|
onChange(null);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
icon_override = false,
|
icon_override = false,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
showFavoriteIndicator = false,
|
showFavoriteIndicator = false,
|
||||||
loading_strat = "lazy",
|
loading_strat = "lazy" as "lazy" | "eager",
|
||||||
routePrefix = '/rezepte'
|
routePrefix = '/rezepte'
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
|
|
||||||
const isInSeason = $derived(icon_override || recipe.season?.includes(current_month));
|
const isInSeason = $derived(icon_override || recipe.season?.includes(current_month));
|
||||||
|
|
||||||
function activateTransitions(event) {
|
function activateTransitions(event: MouseEvent) {
|
||||||
const img = event.currentTarget.querySelector('.img-wrap img');
|
const img = (event.currentTarget as HTMLElement)?.querySelector('.img-wrap img') as HTMLElement | null;
|
||||||
if (img) img.style.viewTransitionName = `recipe-${recipe.short_name}-img`;
|
if (img) (img.style as any).viewTransitionName = `recipe-${recipe.short_name}-img`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<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 { portions } from '$lib/js/portions_store.js'
|
||||||
import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelte'
|
import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelte'
|
||||||
|
|
||||||
let portions_local = $state()
|
let portions_local = $state<string | undefined>()
|
||||||
portions.subscribe((p) => {
|
portions.subscribe((p: any) => {
|
||||||
portions_local = p
|
portions_local = p
|
||||||
});
|
});
|
||||||
|
|
||||||
export function set_portions(){
|
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 }>();
|
let { lang = 'de' as 'de' | 'en', ingredients = $bindable() } = $props<{ lang?: 'de' | 'en', ingredients: any }>();
|
||||||
|
|
||||||
// Translation strings
|
// Translation strings
|
||||||
const t = {
|
const t: Record<string, Record<string, string>> = {
|
||||||
de: {
|
de: {
|
||||||
portions: 'Portionen:',
|
portions: 'Portionen:',
|
||||||
ingredients: 'Zutaten',
|
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++){
|
for(var i =0; i < list.length; i++){
|
||||||
if(list[i].name == sublist_name){
|
if(list[i].name == sublist_name){
|
||||||
return i
|
return i
|
||||||
@@ -227,16 +227,16 @@ function get_sublist_index(sublist_name, list){
|
|||||||
}
|
}
|
||||||
return -1
|
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.name = ingredients[list_index].name
|
||||||
edit_heading.list_index = list_index
|
edit_heading.list_index = String(list_index)
|
||||||
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`)
|
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`) as HTMLDialogElement | null;
|
||||||
el.showModal()
|
if (el) el.showModal()
|
||||||
}
|
}
|
||||||
export function edit_subheading_and_close_modal(){
|
export function edit_subheading_and_close_modal(){
|
||||||
ingredients[edit_heading.list_index].name = edit_heading.name
|
ingredients[Number(edit_heading.list_index)].name = edit_heading.name
|
||||||
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`)
|
const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`) as HTMLDialogElement | null;
|
||||||
el.close()
|
if (el) el.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleIngredientModalCancel() {
|
function handleIngredientModalCancel() {
|
||||||
@@ -265,7 +265,7 @@ export function add_new_ingredient(){
|
|||||||
ingredients[list_index].list.push({ ...new_ingredient})
|
ingredients[list_index].list.push({ ...new_ingredient})
|
||||||
ingredients = ingredients //tells svelte to update dom
|
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){
|
if(ingredients[list_index].list.length > 1){
|
||||||
const response = confirm(t[lang].confirmDeleteList);
|
const response = confirm(t[lang].confirmDeleteList);
|
||||||
if(!response){
|
if(!response){
|
||||||
@@ -275,18 +275,18 @@ export function remove_list(list_index){
|
|||||||
ingredients.splice(list_index, 1);
|
ingredients.splice(list_index, 1);
|
||||||
ingredients = ingredients //tells svelte to update dom
|
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[list_index].list.splice(ingredient_index, 1)
|
||||||
ingredients = ingredients //tells svelte to update dom
|
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 = {...ingredients[list_index].list[ingredient_index]}
|
||||||
edit_ingredient.list_index = list_index
|
edit_ingredient.list_index = String(list_index)
|
||||||
edit_ingredient.ingredient_index = ingredient_index
|
edit_ingredient.ingredient_index = String(ingredient_index)
|
||||||
edit_ingredient.sublist = ingredients[list_index].name
|
edit_ingredient.sublist = ingredients[list_index].name
|
||||||
const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`);
|
const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement | null;
|
||||||
modal_el.showModal();
|
if (modal_el) modal_el.showModal();
|
||||||
}
|
}
|
||||||
export function edit_ingredient_and_close_modal(){
|
export function edit_ingredient_and_close_modal(){
|
||||||
// Check if we're adding to or editing a reference
|
// Check if we're adding to or editing a reference
|
||||||
@@ -333,12 +333,12 @@ export function edit_ingredient_and_close_modal(){
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Normal edit behavior
|
// 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,
|
amount: edit_ingredient.amount,
|
||||||
unit: edit_ingredient.unit,
|
unit: edit_ingredient.unit,
|
||||||
name: edit_ingredient.name,
|
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;
|
const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement;
|
||||||
if (modal_el) {
|
if (modal_el) {
|
||||||
@@ -346,7 +346,7 @@ export function edit_ingredient_and_close_modal(){
|
|||||||
setTimeout(() => modal_el.close(), 0);
|
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(direction == 1){
|
||||||
if(list_index == 0){
|
if(list_index == 0){
|
||||||
return
|
return
|
||||||
@@ -361,7 +361,7 @@ export function update_list_position(list_index, direction){
|
|||||||
}
|
}
|
||||||
ingredients = ingredients //tells svelte to update dom
|
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(direction == 1){
|
||||||
if(ingredient_index == 0){
|
if(ingredient_index == 0){
|
||||||
return
|
return
|
||||||
@@ -738,7 +738,7 @@ h3{
|
|||||||
|
|
||||||
<div class=list_wrapper >
|
<div class=list_wrapper >
|
||||||
<h4>{t[lang].portions}</h4>
|
<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>
|
<h2>{t[lang].ingredients}</h2>
|
||||||
{#each ingredients as list, list_index}
|
{#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 }>();
|
let { lang = 'de' as 'de' | 'en', instructions = $bindable(), add_info = $bindable() } = $props<{ lang?: 'de' | 'en', instructions: any, add_info: any }>();
|
||||||
|
|
||||||
// Translation strings
|
// Translation strings
|
||||||
const t = {
|
const t: Record<string, Record<string, string>> = {
|
||||||
de: {
|
de: {
|
||||||
preparation: 'Vorbereitung:',
|
preparation: 'Vorbereitung:',
|
||||||
bulkFermentation: 'Stockgare:',
|
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++){
|
for(var i =0; i < list.length; i++){
|
||||||
if(list[i].name == sublist_name){
|
if(list[i].name == sublist_name){
|
||||||
return i
|
return i
|
||||||
@@ -219,7 +219,7 @@ function get_sublist_index(sublist_name, list){
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
export function remove_list(list_index){
|
export function remove_list(list_index: number){
|
||||||
if(instructions[list_index].steps.length > 1){
|
if(instructions[list_index].steps.length > 1){
|
||||||
const response = confirm(t[lang].confirmDeleteList);
|
const response = confirm(t[lang].confirmDeleteList);
|
||||||
if(!response){
|
if(!response){
|
||||||
@@ -245,13 +245,13 @@ export function add_new_step(){
|
|||||||
else{
|
else{
|
||||||
instructions[list_index].steps.push(new_step.step)
|
instructions[list_index].steps.push(new_step.step)
|
||||||
}
|
}
|
||||||
const el = document.querySelector("#step")
|
const el = document.querySelector("#step") as HTMLElement | null;
|
||||||
el.innerHTML = ""
|
if (el) el.innerHTML = ""
|
||||||
new_step.step = ""
|
new_step.step = ""
|
||||||
instructions = instructions //tells svelte to update dom
|
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[list_index].steps.splice(step_index, 1)
|
||||||
instructions = instructions //tells svelte to update dom
|
instructions = instructions //tells svelte to update dom
|
||||||
}
|
}
|
||||||
@@ -262,15 +262,15 @@ let edit_step = $state({
|
|||||||
list_index: 0,
|
list_index: 0,
|
||||||
step_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 = {
|
edit_step = {
|
||||||
step: instructions[list_index].steps[step_index],
|
step: instructions[list_index].steps[step_index],
|
||||||
name: instructions[list_index].name,
|
name: instructions[list_index].name,
|
||||||
|
list_index,
|
||||||
|
step_index,
|
||||||
}
|
}
|
||||||
edit_step.list_index = list_index
|
const modal_el = document.querySelector(`#edit_step_modal-${lang}`) as HTMLDialogElement | null;
|
||||||
edit_step.step_index = step_index
|
if (modal_el) modal_el.showModal();
|
||||||
const modal_el = document.querySelector(`#edit_step_modal-${lang}`);
|
|
||||||
modal_el.showModal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function edit_step_and_close_modal(){
|
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.name = instructions[list_index].name
|
||||||
edit_heading.list_index = list_index
|
edit_heading.list_index = String(list_index)
|
||||||
const el = document.querySelector(`#edit_subheading_steps_modal-${lang}`)
|
const el = document.querySelector(`#edit_subheading_steps_modal-${lang}`) as HTMLDialogElement | null;
|
||||||
el.showModal()
|
if (el) el.showModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function edit_subheading_steps_and_close_modal(){
|
export function edit_subheading_steps_and_close_modal(){
|
||||||
instructions[edit_heading.list_index].name = edit_heading.name
|
instructions[Number(edit_heading.list_index)].name = edit_heading.name
|
||||||
const modal_el = document.querySelector("#edit_subheading_steps_modal");
|
const modal_el = document.querySelector(`#edit_subheading_steps_modal-${lang}`) as HTMLDialogElement | null;
|
||||||
modal_el.close();
|
if (modal_el) modal_el.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStepModalCancel() {
|
function handleStepModalCancel() {
|
||||||
@@ -347,19 +347,19 @@ function handleStepModalCancel() {
|
|||||||
|
|
||||||
|
|
||||||
export function clear_step(){
|
export function clear_step(){
|
||||||
const el = document.querySelector("#step")
|
const el = document.querySelector("#step") as HTMLElement | null;
|
||||||
if(el.innerHTML == step_placeholder){
|
if(el && el.innerHTML == step_placeholder){
|
||||||
el.innerHTML = ""
|
el.innerHTML = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function add_placeholder(){
|
export function add_placeholder(){
|
||||||
const el = document.querySelector("#step")
|
const el = document.querySelector("#step") as HTMLElement | null;
|
||||||
if(el.innerHTML == ""){
|
if(el && el.innerHTML == ""){
|
||||||
el.innerHTML = step_placeholder
|
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(direction == 1){
|
||||||
if(list_index == 0){
|
if(list_index == 0){
|
||||||
return
|
return
|
||||||
@@ -374,7 +374,7 @@ export function update_list_position(list_index, direction){
|
|||||||
}
|
}
|
||||||
instructions = instructions //tells svelte to update dom
|
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(direction == 1){
|
||||||
if(step_index == 0){
|
if(step_index == 0){
|
||||||
return
|
return
|
||||||
@@ -770,27 +770,27 @@ h3{
|
|||||||
<div class=additional_info>
|
<div class=additional_info>
|
||||||
|
|
||||||
<div><h4>{t[lang].preparation}</h4>
|
<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>
|
||||||
|
|
||||||
|
|
||||||
<div><h4>{t[lang].bulkFermentation}</h4>
|
<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>
|
||||||
|
|
||||||
<div><h4>{t[lang].finalFermentation}</h4>
|
<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>
|
||||||
|
|
||||||
<div><h4>{t[lang].baking}</h4>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div><h4>{t[lang].totalTime}</h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,14 @@
|
|||||||
// Get all current URL parameters to preserve state
|
// Get all current URL parameters to preserve state
|
||||||
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : $page.url.searchParams);
|
const currentParams = $derived(browser ? new URLSearchParams(window.location.search) : $page.url.searchParams);
|
||||||
|
|
||||||
|
/** @param {Event} event */
|
||||||
function toggleHefe(event) {
|
function toggleHefe(event) {
|
||||||
// If JavaScript is available, prevent form submission and handle client-side
|
// If JavaScript is available, prevent form submission and handle client-side
|
||||||
if (browser) {
|
if (browser) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Simply toggle the yeast flag in the URL
|
// 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}`;
|
const yeastParam = `y${yeastId}`;
|
||||||
|
|
||||||
if (url.searchParams.has(yeastParam)) {
|
if (url.searchParams.has(yeastParam)) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} icon */
|
||||||
function handleIconSelect(icon) {
|
function handleIconSelect(icon) {
|
||||||
if (useAndLogic) {
|
if (useAndLogic) {
|
||||||
// AND mode: single select
|
// AND mode: single select
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {KeyboardEvent} event */
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
dropdownOpen = false;
|
dropdownOpen = false;
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} icon */
|
||||||
function handleRemove(icon) {
|
function handleRemove(icon) {
|
||||||
if (useAndLogic) {
|
if (useAndLogic) {
|
||||||
onChange(null);
|
onChange(null);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
lang?: string,
|
lang?: string,
|
||||||
recipes?: any[],
|
recipes?: any[],
|
||||||
isLoggedIn?: boolean,
|
isLoggedIn?: boolean,
|
||||||
onSearchResults?: (ids: any[], categories: any[]) => void,
|
onSearchResults?: (ids: Set<string>, categories: Set<string>) => void,
|
||||||
recipesSlot?: Snippet
|
recipesSlot?: Snippet
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import HefeSwapper from './HefeSwapper.svelte';
|
|||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
// Helper function to multiply numbers in ingredient amounts
|
// Helper function to multiply numbers in ingredient amounts
|
||||||
|
/** @param {string} amount @param {number} multiplier */
|
||||||
function multiplyIngredientAmount(amount, multiplier) {
|
function multiplyIngredientAmount(amount, multiplier) {
|
||||||
if (!amount || multiplier === 1) return amount;
|
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 number = match.includes(',') ? match.replace(/\./g, '').replace(',', '.') : match;
|
||||||
const multiplied = (parseFloat(number) * multiplier).toString();
|
const multiplied = (parseFloat(number) * multiplier).toString();
|
||||||
const rounded = parseFloat(multiplied).toFixed(3);
|
const rounded = parseFloat(multiplied).toFixed(3);
|
||||||
@@ -19,6 +20,7 @@ function multiplyIngredientAmount(amount, multiplier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recursively flatten nested ingredient references
|
// 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) {
|
function flattenIngredientReferences(items, lang, visited = new Set(), baseMultiplier = 1) {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ function flattenIngredientReferences(items, lang, visited = new Set(), baseMulti
|
|||||||
} else if (item.type === 'section' || !item.type) {
|
} else if (item.type === 'section' || !item.type) {
|
||||||
// Regular section - pass through with multiplier applied to amounts
|
// Regular section - pass through with multiplier applied to amounts
|
||||||
if (baseMultiplier !== 1 && item.list) {
|
if (baseMultiplier !== 1 && item.list) {
|
||||||
const adjustedList = item.list.map(ingredient => ({
|
const adjustedList = item.list.map((/** @type {any} */ ingredient) => ({
|
||||||
...ingredient,
|
...ingredient,
|
||||||
amount: multiplyIngredientAmount(ingredient.amount, baseMultiplier)
|
amount: multiplyIngredientAmount(ingredient.amount, baseMultiplier)
|
||||||
}));
|
}));
|
||||||
@@ -138,6 +140,7 @@ let userFormWidth = $state(data.defaultForm?.width || 20);
|
|||||||
let userFormLength = $state(data.defaultForm?.length || 30);
|
let userFormLength = $state(data.defaultForm?.length || 30);
|
||||||
let userFormInnerDiameter = $state(data.defaultForm?.innerDiameter || 8);
|
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) {
|
function calcArea(shape, diameter, width, length, innerDiameter) {
|
||||||
if (shape === 'round') return Math.PI * (diameter / 2) ** 2;
|
if (shape === 'round') return Math.PI * (diameter / 2) ** 2;
|
||||||
if (shape === 'gugelhupf') return Math.PI * ((diameter / 2) ** 2 - (innerDiameter / 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) {
|
function updateUrl(value) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const url = new URL(window.location);
|
const url = new URL(window.location.href);
|
||||||
if (value === 1) {
|
if (value === 1) {
|
||||||
url.searchParams.delete('multiplier');
|
url.searchParams.delete('multiplier');
|
||||||
} else {
|
} else {
|
||||||
@@ -196,6 +200,7 @@ const multiplierOptions = [
|
|||||||
|
|
||||||
// Calculate yeast IDs for each yeast ingredient
|
// Calculate yeast IDs for each yeast ingredient
|
||||||
const yeastIds = $derived.by(() => {
|
const yeastIds = $derived.by(() => {
|
||||||
|
/** @type {Record<string, number>} */
|
||||||
const ids = {};
|
const ids = {};
|
||||||
let yeastCounter = 0;
|
let yeastCounter = 0;
|
||||||
if (data.ingredients) {
|
if (data.ingredients) {
|
||||||
@@ -223,17 +228,18 @@ const currentParams = $derived(browser ? new URLSearchParams(window.location.sea
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
multiplier = parseFloat(urlParams.get('multiplier')) || 1;
|
multiplier = parseFloat(urlParams.get('multiplier') || '1') || 1;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onNavigate(() => {
|
onNavigate(() => {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
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) {
|
function handleMultiplierClick(event, value) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -244,9 +250,10 @@ function handleMultiplierClick(event, value) {
|
|||||||
// If no JS, form will submit normally
|
// If no JS, form will submit normally
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Event} event */
|
||||||
function handleCustomInput(event) {
|
function handleCustomInput(event) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const value = parseFloat(event.target.value);
|
const value = parseFloat(/** @type {HTMLInputElement} */ (event.target).value);
|
||||||
if (!isNaN(value) && value > 0) {
|
if (!isNaN(value) && value > 0) {
|
||||||
multiplier = value;
|
multiplier = value;
|
||||||
formDriven = false;
|
formDriven = false;
|
||||||
@@ -255,6 +262,7 @@ function handleCustomInput(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Event} event */
|
||||||
function handleCustomSubmit(event) {
|
function handleCustomSubmit(event) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -264,15 +272,16 @@ function handleCustomSubmit(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @param {string} inputString */
|
||||||
function convertFloatsToFractions(inputString) {
|
function convertFloatsToFractions(inputString) {
|
||||||
// Split the input string into individual words
|
// Split the input string into individual words
|
||||||
const words = inputString.split(' ');
|
const words = inputString.split(' ');
|
||||||
|
|
||||||
// Define a helper function to check if a number is close to an integer
|
// 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
|
// Function to convert a float to a fraction
|
||||||
const floatToFraction = (number) => {
|
const floatToFraction = (/** @type {number} */ number) => {
|
||||||
let bestNumerator = 0;
|
let bestNumerator = 0;
|
||||||
let bestDenominator = 1;
|
let bestDenominator = 1;
|
||||||
let minDifference = Math.abs(number);
|
let minDifference = Math.abs(number);
|
||||||
@@ -298,11 +307,11 @@ function convertFloatsToFractions(inputString) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Iterate through the words and convert floats to fractions
|
// 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")
|
// Check if the word contains a range (e.g., "300-400")
|
||||||
if (word.includes('-')) {
|
if (word.includes('-')) {
|
||||||
const rangeNumbers = word.split('-');
|
const rangeNumbers = word.split('-');
|
||||||
const rangeFractions = rangeNumbers.map((num) => {
|
const rangeFractions = rangeNumbers.map((/** @type {string} */ num) => {
|
||||||
const number = parseFloat(num);
|
const number = parseFloat(num);
|
||||||
return !isNaN(number) ? floatToFraction(number) : num;
|
return !isNaN(number) ? floatToFraction(number) : num;
|
||||||
});
|
});
|
||||||
@@ -317,8 +326,9 @@ function convertFloatsToFractions(inputString) {
|
|||||||
return result.join(' ');
|
return result.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} inputString @param {number} constant */
|
||||||
function multiplyNumbersInString(inputString, 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 number = match.includes(',') ? match.replace(/\./g, '').replace(',', '.') : match;
|
||||||
const multiplied = (parseFloat(number) * constant).toString();
|
const multiplied = (parseFloat(number) * constant).toString();
|
||||||
const rounded = parseFloat(multiplied).toFixed(3);
|
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)"
|
// "1-2 Kuchen (Durchmesser: 26cm", constant=2 -> "2-4 Kuchen (Durchmesser: 26cm)"
|
||||||
|
/** @param {string} inputString @param {number} constant */
|
||||||
function multiplyFirstAndSecondNumbers(inputString, constant) {
|
function multiplyFirstAndSecondNumbers(inputString, constant) {
|
||||||
const regex = /(\d+(?:[\.,]\d+)?)(\s*-\s*\d+(?:[\.,]\d+)?)?/;
|
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];
|
const numbersToMultiply = [firstNumber];
|
||||||
if (secondNumber) {
|
if (secondNumber) {
|
||||||
numbersToMultiply.push(secondNumber.replace(/-\s*/, ''));
|
numbersToMultiply.push(secondNumber.replace(/-\s*/, ''));
|
||||||
@@ -346,6 +357,7 @@ function multiplyFirstAndSecondNumbers(inputString, constant) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @param {string} string @param {number} multiplier */
|
||||||
function adjust_amount(string, multiplier){
|
function adjust_amount(string, multiplier){
|
||||||
let temp = multiplyNumbersInString(string, multiplier)
|
let temp = multiplyNumbersInString(string, multiplier)
|
||||||
temp = convertFloatsToFractions(temp)
|
temp = convertFloatsToFractions(temp)
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ let { data } = $props();
|
|||||||
let multiplier = $state(data.multiplier || 1);
|
let multiplier = $state(data.multiplier || 1);
|
||||||
|
|
||||||
// Recursively flatten nested instruction references
|
// Recursively flatten nested instruction references
|
||||||
|
/**
|
||||||
|
* @param {any[]} items
|
||||||
|
* @param {string} lang
|
||||||
|
* @param {Set<string>} visited
|
||||||
|
*/
|
||||||
function flattenInstructionReferences(items, lang, visited = new Set()) {
|
function flattenInstructionReferences(items, lang, visited = new Set()) {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,13 @@
|
|||||||
instructions?: any[]
|
instructions?: any[]
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let short_name = $state();
|
let short_name = $state('');
|
||||||
let password = $state();
|
let password = $state('');
|
||||||
let datecreated = $state(new Date());
|
let datecreated = $state(new Date());
|
||||||
let datemodified = $state(datecreated);
|
let datemodified = $state(datecreated);
|
||||||
|
let result = $state('');
|
||||||
|
let image_preview_url = $state('');
|
||||||
|
let selected_image_file = $state<File | null>(null);
|
||||||
|
|
||||||
async function doPost () {
|
async function doPost () {
|
||||||
const res = await fetch('/api/add', {
|
const res = await fetch('/api/add', {
|
||||||
@@ -64,15 +67,15 @@ input.temp{
|
|||||||
}
|
}
|
||||||
</style>
|
</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"/>
|
<input class=temp bind:value={short_name} placeholder="Kurzname"/>
|
||||||
|
|
||||||
<SeasonSelect bind:season={season}></SeasonSelect>
|
<SeasonSelect></SeasonSelect>
|
||||||
<button onclick={() => console.log(season)}>PRINTOUT season</button>
|
<button onclick={() => console.log(season)}>PRINTOUT season</button>
|
||||||
|
|
||||||
<h2>Zutaten</h2>
|
<h2>Zutaten</h2>
|
||||||
<CreateIngredientList bind:ingredients={ingredients}></CreateIngredientList>
|
<CreateIngredientList bind:ingredients={ingredients}></CreateIngredientList>
|
||||||
<h2>Zubereitung</h2>
|
<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}>
|
<input class=temp type="password" placeholder=Passwort bind:value={password}>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
favoritesOnly = false,
|
favoritesOnly = false,
|
||||||
lang = 'de',
|
lang = 'de',
|
||||||
recipes = [],
|
recipes = [],
|
||||||
onSearchResults = (matchedIds, matchedCategories) => {},
|
onSearchResults = (/** @type {Set<any>} */ matchedIds, /** @type {Set<any>} */ matchedCategories) => {},
|
||||||
isLoggedIn = false
|
isLoggedIn = false
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -32,13 +32,19 @@
|
|||||||
let showFilters = $state(false);
|
let showFilters = $state(false);
|
||||||
|
|
||||||
// Filter data loaded from APIs
|
// Filter data loaded from APIs
|
||||||
|
/** @type {any[]} */
|
||||||
let availableTags = $state([]);
|
let availableTags = $state([]);
|
||||||
|
/** @type {any[]} */
|
||||||
let availableIcons = $state([]);
|
let availableIcons = $state([]);
|
||||||
|
|
||||||
// Selected filters (internal state)
|
// Selected filters (internal state)
|
||||||
|
/** @type {any} */
|
||||||
let selectedCategory = $state(null);
|
let selectedCategory = $state(null);
|
||||||
|
/** @type {any[]} */
|
||||||
let selectedTags = $state([]);
|
let selectedTags = $state([]);
|
||||||
|
/** @type {any} */
|
||||||
let selectedIcon = $state(null);
|
let selectedIcon = $state(null);
|
||||||
|
/** @type {number[]} */
|
||||||
let selectedSeasons = $state([]);
|
let selectedSeasons = $state([]);
|
||||||
let selectedFavoritesOnly = $state(false);
|
let selectedFavoritesOnly = $state(false);
|
||||||
let useAndLogic = $state(true);
|
let useAndLogic = $state(true);
|
||||||
@@ -53,8 +59,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Apply non-text filters (category, tags, icon, season)
|
// Apply non-text filters (category, tags, icon, season)
|
||||||
|
/** @param {any[]} recipeList */
|
||||||
function applyNonTextFilters(recipeList) {
|
function applyNonTextFilters(recipeList) {
|
||||||
return recipeList.filter(recipe => {
|
return recipeList.filter((/** @type {any} */ recipe) => {
|
||||||
if (useAndLogic) {
|
if (useAndLogic) {
|
||||||
// AND mode: recipe must satisfy ALL active filters
|
// AND mode: recipe must satisfy ALL active filters
|
||||||
// Category filter (single value in AND mode)
|
// Category filter (single value in AND mode)
|
||||||
@@ -91,7 +98,9 @@
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// OR mode: recipe must satisfy AT LEAST ONE active filter
|
// OR mode: recipe must satisfy AT LEAST ONE active filter
|
||||||
|
/** @type {any[]} */
|
||||||
const categoryArray = Array.isArray(selectedCategory) ? selectedCategory : (selectedCategory ? [selectedCategory] : []);
|
const categoryArray = Array.isArray(selectedCategory) ? selectedCategory : (selectedCategory ? [selectedCategory] : []);
|
||||||
|
/** @type {any[]} */
|
||||||
const iconArray = Array.isArray(selectedIcon) ? selectedIcon : (selectedIcon ? [selectedIcon] : []);
|
const iconArray = Array.isArray(selectedIcon) ? selectedIcon : (selectedIcon ? [selectedIcon] : []);
|
||||||
|
|
||||||
const hasActiveFilters = categoryArray.length > 0 || selectedTags.length > 0 || iconArray.length > 0 || selectedSeasons.length > 0 || selectedFavoritesOnly;
|
const hasActiveFilters = categoryArray.length > 0 || selectedTags.length > 0 || iconArray.length > 0 || selectedSeasons.length > 0 || selectedFavoritesOnly;
|
||||||
@@ -114,6 +123,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform search directly (no worker)
|
// Perform search directly (no worker)
|
||||||
|
/** @param {string} query */
|
||||||
function performSearch(query) {
|
function performSearch(query) {
|
||||||
// Apply non-text filters first
|
// Apply non-text filters first
|
||||||
const filteredByNonText = applyNonTextFilters(recipes);
|
const filteredByNonText = applyNonTextFilters(recipes);
|
||||||
@@ -121,8 +131,8 @@
|
|||||||
// Empty query = show all (filtered) recipes
|
// Empty query = show all (filtered) recipes
|
||||||
if (!query || query.trim().length === 0) {
|
if (!query || query.trim().length === 0) {
|
||||||
onSearchResults(
|
onSearchResults(
|
||||||
new Set(filteredByNonText.map(r => r._id)),
|
new Set(filteredByNonText.map((/** @type {any} */ r) => r._id)),
|
||||||
new Set(filteredByNonText.map(r => r.category))
|
new Set(filteredByNonText.map((/** @type {any} */ r) => r.category))
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -131,10 +141,10 @@
|
|||||||
const searchText = query.toLowerCase().trim()
|
const searchText = query.toLowerCase().trim()
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/\p{Diacritic}/gu, "");
|
.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
|
// Filter recipes by text
|
||||||
const matched = filteredByNonText.filter(recipe => {
|
const matched = filteredByNonText.filter((/** @type {any} */ recipe) => {
|
||||||
// Build searchable string from recipe data
|
// Build searchable string from recipe data
|
||||||
const searchString = [
|
const searchString = [
|
||||||
recipe.name || '',
|
recipe.name || '',
|
||||||
@@ -147,17 +157,18 @@
|
|||||||
.replace(/­|/g, ''); // Remove soft hyphens
|
.replace(/­|/g, ''); // Remove soft hyphens
|
||||||
|
|
||||||
// All search terms must match
|
// 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
|
// Return matched recipe IDs and categories
|
||||||
onSearchResults(
|
onSearchResults(
|
||||||
new Set(matched.map(r => r._id)),
|
new Set(matched.map((/** @type {any} */ r) => r._id)),
|
||||||
new Set(matched.map(r => r.category))
|
new Set(matched.map((/** @type {any} */ r) => r.category))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build search URL with current filters
|
// Build search URL with current filters
|
||||||
|
/** @param {string} query */
|
||||||
function buildSearchUrl(query) {
|
function buildSearchUrl(query) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const url = new URL(searchResultsUrl, window.location.origin);
|
const url = new URL(searchResultsUrl, window.location.origin);
|
||||||
@@ -185,10 +196,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter change handlers - the effect will automatically trigger search
|
// Filter change handlers - the effect will automatically trigger search
|
||||||
|
/** @param {any} newCategory */
|
||||||
function handleCategoryChange(newCategory) {
|
function handleCategoryChange(newCategory) {
|
||||||
selectedCategory = newCategory;
|
selectedCategory = newCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {any} tag */
|
||||||
function handleTagToggle(tag) {
|
function handleTagToggle(tag) {
|
||||||
if (selectedTags.includes(tag)) {
|
if (selectedTags.includes(tag)) {
|
||||||
selectedTags = selectedTags.filter(t => t !== tag);
|
selectedTags = selectedTags.filter(t => t !== tag);
|
||||||
@@ -197,18 +210,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {any} newIcon */
|
||||||
function handleIconChange(newIcon) {
|
function handleIconChange(newIcon) {
|
||||||
selectedIcon = newIcon;
|
selectedIcon = newIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number[]} newSeasons */
|
||||||
function handleSeasonChange(newSeasons) {
|
function handleSeasonChange(newSeasons) {
|
||||||
selectedSeasons = newSeasons;
|
selectedSeasons = newSeasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {boolean} enabled */
|
||||||
function handleFavoritesToggle(enabled) {
|
function handleFavoritesToggle(enabled) {
|
||||||
selectedFavoritesOnly = enabled;
|
selectedFavoritesOnly = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {boolean} useAnd */
|
||||||
function handleLogicModeToggle(useAnd) {
|
function handleLogicModeToggle(useAnd) {
|
||||||
useAndLogic = useAnd;
|
useAndLogic = useAnd;
|
||||||
|
|
||||||
@@ -223,6 +240,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Event} event */
|
||||||
function handleSubmit(event) {
|
function handleSubmit(event) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
// For JS-enabled browsers, prevent default and navigate programmatically
|
// For JS-enabled browsers, prevent default and navigate programmatically
|
||||||
|
|||||||
@@ -48,15 +48,18 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} monthNumber */
|
||||||
function handleMonthSelect(monthNumber) {
|
function handleMonthSelect(monthNumber) {
|
||||||
onChange([...selectedSeasons, monthNumber]);
|
onChange([...selectedSeasons, monthNumber]);
|
||||||
inputValue = '';
|
inputValue = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} monthNumber */
|
||||||
function handleMonthRemove(monthNumber) {
|
function handleMonthRemove(monthNumber) {
|
||||||
onChange(selectedSeasons.filter(m => m !== monthNumber));
|
onChange(selectedSeasons.filter(m => m !== monthNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {KeyboardEvent} event */
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
lang?: string,
|
lang?: string,
|
||||||
recipes?: any[],
|
recipes?: any[],
|
||||||
isLoggedIn?: boolean,
|
isLoggedIn?: boolean,
|
||||||
onSearchResults?: (ids: any[], categories: any[]) => void,
|
onSearchResults?: (ids: Set<string>, categories: Set<string>) => void,
|
||||||
recipesSlot?: Snippet
|
recipesSlot?: Snippet
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let month: number = $state();
|
let month: number = $state(0);
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
a.month{
|
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.subscribe((s: number[]) => {
|
||||||
season_local = s
|
season_local = s;
|
||||||
});
|
});
|
||||||
|
|
||||||
export function set_season(){
|
export function set_season(){
|
||||||
let temp = []
|
let temp: number[] = [];
|
||||||
const el = document.getElementById("labels");
|
const el = document.getElementById("labels");
|
||||||
|
if (!el) return;
|
||||||
for(var i = 0; i < el.children.length; i++){
|
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)
|
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");
|
const el = document.getElementById("labels");
|
||||||
|
if (!el) return;
|
||||||
for(var i = 0; i < season.length; i++){
|
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){
|
function toggle_checkbox_on_key(event: Event){
|
||||||
event.path[0].children[0].checked = !event.path[0].children[0].checked
|
const target = event.target as HTMLElement;
|
||||||
|
const checkbox = target.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||||
|
if (checkbox) checkbox.checked = !checkbox.checked;
|
||||||
}
|
}
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
write_season(season_local)
|
write_season(season_local)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
let inputValue = $state('');
|
let inputValue = $state('');
|
||||||
let dropdownOpen = $state(false);
|
let dropdownOpen = $state(false);
|
||||||
|
/** @type {HTMLDivElement | null} */
|
||||||
let dropdownElement = $state(null);
|
let dropdownElement = $state(null);
|
||||||
|
|
||||||
// Filter tags based on input
|
// Filter tags based on input
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
dropdownOpen = true;
|
dropdownOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {FocusEvent} event */
|
||||||
function handleInputBlur(event) {
|
function handleInputBlur(event) {
|
||||||
// Delay to allow click events on dropdown items
|
// Delay to allow click events on dropdown items
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -40,12 +42,14 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} tag */
|
||||||
function handleTagSelect(tag) {
|
function handleTagSelect(tag) {
|
||||||
onToggle(tag);
|
onToggle(tag);
|
||||||
inputValue = '';
|
inputValue = '';
|
||||||
dropdownOpen = false;
|
dropdownOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {KeyboardEvent} event */
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
if(isredirected){
|
if(isredirected){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
document.querySelector("#img_carousel").showModal();
|
/** @type {HTMLDialogElement} */(document.querySelector("#img_carousel")).showModal();
|
||||||
}
|
}
|
||||||
function close_dialog_img(){
|
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 Cross from "$lib/assets/icons/Cross.svelte";
|
||||||
import "$lib/css/action_button.css";
|
import "$lib/css/action_button.css";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
let { item, ondelete, onedit, isEnglish = false } = $props();
|
let { item, ondelete, onedit, isEnglish = false } = $props();
|
||||||
|
|
||||||
|
/** @param {string} url */
|
||||||
function getDomain(url) {
|
function getDomain(url) {
|
||||||
try {
|
try {
|
||||||
return new URL(url).hostname.replace(/^www\./, '');
|
return new URL(url).hostname.replace(/^www\./, '');
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ button:disabled {
|
|||||||
{#each untranslatedBaseRecipes as baseRecipe}
|
{#each untranslatedBaseRecipes as baseRecipe}
|
||||||
<li>
|
<li>
|
||||||
<strong>{baseRecipe.name}</strong>
|
<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 →
|
Open in new tab →
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { writable } from 'svelte/store';
|
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`);
|
dataUrls.push(`/rezepte/${recipe.short_name}/__data.json`);
|
||||||
|
|
||||||
// English recipe data (if translation exists)
|
// English recipe data (if translation exists)
|
||||||
if (recipe.translations?.en?.short_name) {
|
if ((recipe as any).translations?.en?.short_name) {
|
||||||
dataUrls.push(`/recipes/${recipe.translations.en.short_name}/__data.json`);
|
dataUrls.push(`/recipes/${(recipe as any).translations.en.short_name}/__data.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect metadata for subroute caching
|
// Collect metadata for subroute caching
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ export async function requireAuth(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
nickname: session.user.nickname,
|
nickname: session.user.nickname,
|
||||||
name: session.user.name,
|
name: session.user.name ?? undefined,
|
||||||
email: session.user.email,
|
email: session.user.email ?? undefined,
|
||||||
image: session.user.image
|
image: session.user.image ?? undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +74,8 @@ export async function optionalAuth(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
nickname: session.user.nickname,
|
nickname: session.user.nickname,
|
||||||
name: session.user.name,
|
name: session.user.name ?? undefined,
|
||||||
email: session.user.email,
|
email: session.user.email ?? undefined,
|
||||||
image: session.user.image
|
image: session.user.image ?? undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ export function toBrief(recipe: any, recipeLang: string): BriefRecipeType {
|
|||||||
icon: recipe.icon,
|
icon: recipe.icon,
|
||||||
description: recipe.translations.en.description,
|
description: recipe.translations.en.description,
|
||||||
season: recipe.season || [],
|
season: recipe.season || [],
|
||||||
|
dateCreated: recipe.dateCreated,
|
||||||
dateModified: recipe.dateModified,
|
dateModified: recipe.dateModified,
|
||||||
germanShortName: recipe.short_name,
|
germanShortName: recipe.short_name,
|
||||||
} as BriefRecipeType;
|
} as unknown as BriefRecipeType;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...recipe,
|
...recipe,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { calculateNextExecutionDate } from '$lib/utils/recurring';
|
|||||||
|
|
||||||
class RecurringPaymentScheduler {
|
class RecurringPaymentScheduler {
|
||||||
private isRunning = false;
|
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 the scheduler - runs every minute to check for due payments
|
||||||
start() {
|
start() {
|
||||||
@@ -27,9 +27,8 @@ class RecurringPaymentScheduler {
|
|||||||
|
|
||||||
await this.processRecurringPayments();
|
await this.processRecurringPayments();
|
||||||
}, {
|
}, {
|
||||||
scheduled: true,
|
|
||||||
timezone: 'Europe/Zurich' // Adjust timezone as needed
|
timezone: 'Europe/Zurich' // Adjust timezone as needed
|
||||||
});
|
} as any);
|
||||||
|
|
||||||
console.log('[Scheduler] Recurring payments scheduler started (runs every minute)');
|
console.log('[Scheduler] Recurring payments scheduler started (runs every minute)');
|
||||||
}
|
}
|
||||||
@@ -155,7 +154,7 @@ class RecurringPaymentScheduler {
|
|||||||
return {
|
return {
|
||||||
isRunning: this.isRunning,
|
isRunning: this.isRunning,
|
||||||
isScheduled: this.task !== null,
|
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)
|
// 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) {
|
if (receiver && receiver.username) {
|
||||||
return receiver.username;
|
return receiver.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: find the user who is not the payer
|
// 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) {
|
if (otherUser && otherUser.username) {
|
||||||
return otherUser.username;
|
return otherUser.username;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,4 +175,6 @@ const RecipeSchema = new mongoose.Schema(
|
|||||||
RecipeSchema.index({ "translations.en.short_name": 1 });
|
RecipeSchema.index({ "translations.en.short_name": 1 });
|
||||||
RecipeSchema.index({ "translations.en.translationStatus": 1 });
|
RecipeSchema.index({ "translations.en.translationStatus": 1 });
|
||||||
|
|
||||||
export const Recipe = mongoose.model("Recipe", RecipeSchema);
|
let _recipeModel: any;
|
||||||
|
try { _recipeModel = mongoose.model("Recipe"); } catch { _recipeModel = mongoose.model("Recipe", RecipeSchema); }
|
||||||
|
export const Recipe = _recipeModel as mongoose.Model<any>;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const RecurringPaymentSchema = new mongoose.Schema(
|
|||||||
cronExpression: {
|
cronExpression: {
|
||||||
type: String,
|
type: String,
|
||||||
validate: {
|
validate: {
|
||||||
validator: function(value: string) {
|
validator: function(this: any, value: string) {
|
||||||
// Only validate if frequency is custom
|
// Only validate if frequency is custom
|
||||||
if (this.frequency === 'custom') {
|
if (this.frequency === 'custom') {
|
||||||
return value != null && value.trim().length > 0;
|
return value != null && value.trim().length > 0;
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ const RosaryStreakSchema = new mongoose.Schema(
|
|||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RosaryStreak = mongoose.models.RosaryStreak || mongoose.model("RosaryStreak", RosaryStreakSchema);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let _model: any;
|
||||||
|
try { _model = mongoose.model("RosaryStreak"); } catch { _model = mongoose.model("RosaryStreak", RosaryStreakSchema); }
|
||||||
|
export const RosaryStreak = _model as mongoose.Model<any>;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ let { data, children } = $props();
|
|||||||
let user = $derived(data.session?.user);
|
let user = $derived(data.session?.user);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header fullSymbol=true>
|
<Header fullSymbol={true}>
|
||||||
{#snippet language_selector_mobile()}
|
{#snippet language_selector_mobile()}
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { redirect } from "@sveltejs/kit"
|
|||||||
import type { Actions, PageServerLoad } from "./$types"
|
import type { Actions, PageServerLoad } from "./$types"
|
||||||
import { error } from "@sveltejs/kit"
|
import { error } from "@sveltejs/kit"
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ cookies }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
const user = await authenticateUser(cookies)
|
const session = await locals.auth();
|
||||||
|
const user = session?.user ?? null;
|
||||||
return {user}
|
return {user}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import {enhance} from '$app/forms';
|
import {enhance} from '$app/forms';
|
||||||
export let data
|
export let data
|
||||||
|
/** @type {string} */
|
||||||
let password;
|
let password;
|
||||||
const admin = data.user?.access.includes('admin') ?? false
|
/** @type {any} */
|
||||||
|
const user = data.user;
|
||||||
|
const admin = user?.access?.includes('admin') ?? false
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@@ -68,7 +71,7 @@ input.hide{
|
|||||||
<section>
|
<section>
|
||||||
<form action="?/change_password" method=POST use:enhance>
|
<form action="?/change_password" method=POST use:enhance>
|
||||||
<h2>Passwort ändern</h2>
|
<h2>Passwort ändern</h2>
|
||||||
<input type="text" bind:value={data.user.username} class=hide name="username" required>
|
<input type="text" value={user?.username ?? ''} class=hide name="username" required>
|
||||||
<label>
|
<label>
|
||||||
Altes Passwort:
|
Altes Passwort:
|
||||||
<input type="password" name="old_password" required>
|
<input type="password" name="old_password" required>
|
||||||
|
|||||||
@@ -11,9 +11,10 @@
|
|||||||
let user = $derived(session?.user);
|
let user = $derived(session?.user);
|
||||||
|
|
||||||
// Get Bible quote and language from SSR via handleError hook
|
// Get Bible quote and language from SSR via handleError hook
|
||||||
let bibleQuote = $derived($page.error?.bibleQuote);
|
let bibleQuote = $derived(/** @type {any} */ ($page.error)?.bibleQuote);
|
||||||
let isEnglish = $derived($page.error?.lang === 'en');
|
let isEnglish = $derived(/** @type {any} */ ($page.error)?.lang === 'en');
|
||||||
|
|
||||||
|
/** @param {number} status */
|
||||||
function getErrorTitle(status) {
|
function getErrorTitle(status) {
|
||||||
if (isEnglish) {
|
if (isEnglish) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} status */
|
||||||
function getErrorDescription(status) {
|
function getErrorDescription(status) {
|
||||||
if (isEnglish) {
|
if (isEnglish) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -52,6 +54,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} status */
|
||||||
function getErrorIcon(status) {
|
function getErrorIcon(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
@@ -112,9 +115,9 @@
|
|||||||
{getErrorDescription(status)}
|
{getErrorDescription(status)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if error?.details}
|
{#if /** @type {any} */ (error)?.details}
|
||||||
<div class="error-details">
|
<div class="error-details">
|
||||||
{error.details}
|
{/** @type {any} */ (error).details}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
onNavigate((navigation) => {
|
onNavigate((navigation) => {
|
||||||
if (!document.startViewTransition) return;
|
if (!(/** @type {any} */ (document)).startViewTransition) return;
|
||||||
|
|
||||||
// Skip if staying within the same route group (recipe layout handles its own)
|
// Skip if staying within the same route group (recipe layout handles its own)
|
||||||
const fromGroup = navigation.from?.route.id?.split('/')[1] ?? '';
|
const fromGroup = navigation.from?.route.id?.split('/')[1] ?? '';
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
if (fromGroup === toGroup) return;
|
if (fromGroup === toGroup) return;
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
document.startViewTransition(async () => {
|
(/** @type {any} */ (document)).startViewTransition(async () => {
|
||||||
resolve();
|
resolve();
|
||||||
await navigation.complete;
|
await navigation.complete;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const labels = $derived({
|
|||||||
rosary: isEnglish ? 'Rosary' : 'Rosenkranz'
|
rosary: isEnglish ? 'Rosary' : 'Rosenkranz'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @type {'de' | 'en'} */
|
||||||
|
const typedLang = /** @type {'de' | 'en'} */ (data.lang);
|
||||||
|
|
||||||
|
/** @param {string} path */
|
||||||
function isActive(path) {
|
function isActive(path) {
|
||||||
const currentPath = $page.url.pathname;
|
const currentPath = $page.url.pathname;
|
||||||
// Check if current path starts with the link path
|
// Check if current path starts with the link path
|
||||||
@@ -22,7 +26,7 @@ function isActive(path) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="preload" href="/fonts/crosses.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/fonts/crosses.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<Header>
|
<Header>
|
||||||
{#snippet links()}
|
{#snippet links()}
|
||||||
@@ -33,11 +37,11 @@ function isActive(path) {
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet language_selector_mobile()}
|
{#snippet language_selector_mobile()}
|
||||||
<LanguageSelector lang={data.lang} />
|
<LanguageSelector lang={typedLang} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet language_selector_desktop()}
|
{#snippet language_selector_desktop()}
|
||||||
<LanguageSelector lang={data.lang} />
|
<LanguageSelector lang={typedLang} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet right_side()}
|
{#snippet right_side()}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
// Create language context for prayer components
|
// Create language context for prayer components
|
||||||
const langContext = createLanguageContext({ urlLang: data.lang, initialLatin: data.initialLatin });
|
const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.initialLatin });
|
||||||
|
|
||||||
// Update lang store when data.lang changes (e.g., after navigation)
|
// Update lang store when data.lang changes (e.g., after navigation)
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -165,6 +165,7 @@
|
|||||||
const baseUrl = $derived(isEnglish ? '/faith/prayers' : '/glaube/gebete');
|
const baseUrl = $derived(isEnglish ? '/faith/prayers' : '/glaube/gebete');
|
||||||
|
|
||||||
// Get prayer name by ID (reactive based on language)
|
// Get prayer name by ID (reactive based on language)
|
||||||
|
/** @param {string} id */
|
||||||
function getPrayerName(id) {
|
function getPrayerName(id) {
|
||||||
const nameMap = {
|
const nameMap = {
|
||||||
signOfCross: labels.signOfCross,
|
signOfCross: labels.signOfCross,
|
||||||
@@ -188,7 +189,7 @@
|
|||||||
prayerbeforeacrucifix: labels.prayerbeforeacrucifix,
|
prayerbeforeacrucifix: labels.prayerbeforeacrucifix,
|
||||||
postcommunio: labels.postcommunio
|
postcommunio: labels.postcommunio
|
||||||
};
|
};
|
||||||
return nameMap[id] || id;
|
return /** @type {Record<string, string>} */(nameMap)[id] || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,6 +254,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper to get match class for a prayer
|
// Helper to get match class for a prayer
|
||||||
|
/** @param {string} id */
|
||||||
function getMatchClass(id) {
|
function getMatchClass(id) {
|
||||||
if (!jsEnabled) return '';
|
if (!jsEnabled) return '';
|
||||||
const match = matchResults.get(id);
|
const match = matchResults.get(id);
|
||||||
@@ -266,7 +268,8 @@
|
|||||||
const filteredPrayers = $derived.by(() => {
|
const filteredPrayers = $derived.by(() => {
|
||||||
let result = prayers;
|
let result = prayers;
|
||||||
if (selectedCategory) {
|
if (selectedCategory) {
|
||||||
result = result.filter(p => prayerCategories[p.id]?.includes(selectedCategory));
|
const cat = selectedCategory;
|
||||||
|
result = result.filter(p => /** @type {Record<string, string[]>} */(prayerCategories)[p.id]?.includes(cat));
|
||||||
}
|
}
|
||||||
if (!searchQuery.trim()) return result;
|
if (!searchQuery.trim()) return result;
|
||||||
|
|
||||||
@@ -282,6 +285,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Prayer metadata (bilingue status)
|
// Prayer metadata (bilingue status)
|
||||||
|
/** @type {Record<string, {bilingue: boolean}>} */
|
||||||
const prayerMeta = {
|
const prayerMeta = {
|
||||||
signOfCross: { bilingue: true },
|
signOfCross: { bilingue: true },
|
||||||
gloriaPatri: { bilingue: true },
|
gloriaPatri: { bilingue: true },
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
|
/** @type {boolean} */
|
||||||
export let is_bilingue;
|
export let is_bilingue;
|
||||||
|
/** @type {string} */
|
||||||
export let name;
|
export let name;
|
||||||
export let id = '';
|
export let id = '';
|
||||||
export let href = '';
|
export let href = '';
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const langContext = createLanguageContext({ urlLang: data.lang, initialLatin: data.initialLatin });
|
const langContext = createLanguageContext({ urlLang: /** @type {'de' | 'en'} */(data.lang), initialLatin: data.initialLatin });
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
langContext.lang.set(data.lang);
|
langContext.lang.set(data.lang);
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
'regina-caeli': { id: 'reginaCaeli', name: 'Regína Cæli', bilingue: true }
|
'regina-caeli': { id: 'reginaCaeli', name: 'Regína Cæli', bilingue: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
const prayer = $derived(prayerDefs[data.prayer]);
|
const prayer = $derived(/** @type {Record<string, {id: string, name: string, bilingue: boolean}>} */(prayerDefs)[data.prayer]);
|
||||||
const prayerName = $derived(prayer?.name || data.prayer);
|
const prayerName = $derived(prayer?.name || data.prayer);
|
||||||
const isBilingue = $derived(prayer?.bilingue ?? true);
|
const isBilingue = $derived(prayer?.bilingue ?? true);
|
||||||
const prayerId = $derived(prayer?.id);
|
const prayerId = $derived(prayer?.id);
|
||||||
@@ -172,7 +172,7 @@ h1 {
|
|||||||
{#if prayerId === 'postcommunio'}
|
{#if prayerId === 'postcommunio'}
|
||||||
<Postcommunio onlyIntro={false} />
|
<Postcommunio onlyIntro={false} />
|
||||||
{:else}
|
{:else}
|
||||||
<PrayerBeforeACrucifix verbose={true} />
|
<PrayerBeforeACrucifix />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import StreakCounter from "$lib/components/faith/StreakCounter.svelte";
|
|||||||
import RosarySvg from "./RosarySvg.svelte";
|
import RosarySvg from "./RosarySvg.svelte";
|
||||||
import MysterySelector from "./MysterySelector.svelte";
|
import MysterySelector from "./MysterySelector.svelte";
|
||||||
import MysteryImageColumn from "./MysteryImageColumn.svelte";
|
import MysteryImageColumn from "./MysteryImageColumn.svelte";
|
||||||
|
/** @typedef {import('./rosaryData.js').MysteryType} MysteryType */
|
||||||
import { mysteries, mysteriesLatin, mysteriesEnglish, mysteryTitles, mysteryTitlesEnglish, allMysteryImages, getLabels, getMysteryForWeekday, BEAD_SPACING, DECADE_OFFSET, sectionPositions } from "./rosaryData.js";
|
import { mysteries, mysteriesLatin, mysteriesEnglish, mysteryTitles, mysteryTitlesEnglish, allMysteryImages, getLabels, getMysteryForWeekday, BEAD_SPACING, DECADE_OFFSET, sectionPositions } from "./rosaryData.js";
|
||||||
import { isEastertide, getLiturgicalSeason } from "$lib/js/easter.svelte";
|
import { isEastertide, getLiturgicalSeason } from "$lib/js/easter.svelte";
|
||||||
import { setupScrollSync } from "./rosaryScrollSync.js";
|
import { setupScrollSync } from "./rosaryScrollSync.js";
|
||||||
@@ -38,7 +39,7 @@ let showImages = $state(data.initialShowImages);
|
|||||||
let hasLoadedFromStorage = $state(false);
|
let hasLoadedFromStorage = $state(false);
|
||||||
|
|
||||||
// Create language context for prayer components (LanguageToggle will use this)
|
// Create language context for prayer components (LanguageToggle will use this)
|
||||||
const langContext = createLanguageContext({ urlLang: data.lang, initialLatin: data.initialLatin });
|
const langContext = createLanguageContext({ urlLang: /** @type {'en'|'de'} */ (data.lang), initialLatin: data.initialLatin });
|
||||||
|
|
||||||
// Update lang store when data.lang changes (e.g., after navigation)
|
// Update lang store when data.lang changes (e.g., after navigation)
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -47,6 +48,8 @@ $effect(() => {
|
|||||||
|
|
||||||
// UI labels based on URL language (reactive)
|
// UI labels based on URL language (reactive)
|
||||||
const isEnglish = $derived(data.lang === 'en');
|
const isEnglish = $derived(data.lang === 'en');
|
||||||
|
/** @type {'en'|'de'} */
|
||||||
|
const lang = $derived(isEnglish ? 'en' : 'de');
|
||||||
const labels = $derived(getLabels(isEnglish));
|
const labels = $derived(getLabels(isEnglish));
|
||||||
|
|
||||||
// Save toggle states to localStorage whenever they change (but only after initial load)
|
// Save toggle states to localStorage whenever they change (but only after initial load)
|
||||||
@@ -62,8 +65,8 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use server-computed initial values (supports no-JS via URL params)
|
// Use server-computed initial values (supports no-JS via URL params)
|
||||||
let selectedMystery = $state(data.initialMystery);
|
let selectedMystery = $state(/** @type {MysteryType} */ (data.initialMystery));
|
||||||
let todaysMystery = $state(data.todaysMystery);
|
let todaysMystery = $state(/** @type {MysteryType} */ (data.todaysMystery));
|
||||||
|
|
||||||
// Derive these values from selectedMystery so they update automatically
|
// Derive these values from selectedMystery so they update automatically
|
||||||
let currentMysteries = $derived(mysteries[selectedMystery]);
|
let currentMysteries = $derived(mysteries[selectedMystery]);
|
||||||
@@ -73,12 +76,14 @@ let currentMysteryTitles = $derived(isEnglish ? mysteryTitlesEnglish[selectedMys
|
|||||||
let currentMysteryDescriptions = $derived(data.mysteryDescriptions[selectedMystery] || []);
|
let currentMysteryDescriptions = $derived(data.mysteryDescriptions[selectedMystery] || []);
|
||||||
|
|
||||||
// Function to switch mysteries
|
// Function to switch mysteries
|
||||||
|
/** @param {MysteryType} mysteryType */
|
||||||
function selectMystery(mysteryType) {
|
function selectMystery(mysteryType) {
|
||||||
selectedMystery = mysteryType;
|
selectedMystery = mysteryType;
|
||||||
lastMysteryTarget = 'before';
|
lastMysteryTarget = 'before';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build URLs preserving full state (for no-JS fallback)
|
// Build URLs preserving full state (for no-JS fallback)
|
||||||
|
/** @param {{ mystery?: string, luminous?: boolean, latin?: boolean, images?: boolean }} [opts] */
|
||||||
function buildHref({ mystery = selectedMystery, luminous = includeLuminous, latin = data.initialLatin, images = showImages } = {}) {
|
function buildHref({ mystery = selectedMystery, luminous = includeLuminous, latin = data.initialLatin, images = showImages } = {}) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('mystery', mystery);
|
params.set('mystery', mystery);
|
||||||
@@ -88,6 +93,7 @@ function buildHref({ mystery = selectedMystery, luminous = includeLuminous, lati
|
|||||||
return `?${params.toString()}`;
|
return `?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} mystery */
|
||||||
function mysteryHref(mystery) {
|
function mysteryHref(mystery) {
|
||||||
return buildHref({ mystery });
|
return buildHref({ mystery });
|
||||||
}
|
}
|
||||||
@@ -102,6 +108,7 @@ $effect(() => {
|
|||||||
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
|
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
|
||||||
if (!includeLuminous && selectedMystery === 'lichtreichen') {
|
if (!includeLuminous && selectedMystery === 'lichtreichen') {
|
||||||
const season = getLiturgicalSeason();
|
const season = getLiturgicalSeason();
|
||||||
|
/** @type {Record<string, MysteryType>} */
|
||||||
const seasonalMap = { eastertide: 'glorreichen', lent: 'schmerzhaften' };
|
const seasonalMap = { eastertide: 'glorreichen', lent: 'schmerzhaften' };
|
||||||
selectedMystery = (season ? seasonalMap[season] : null) ?? todaysMystery;
|
selectedMystery = (season ? seasonalMap[season] : null) ?? todaysMystery;
|
||||||
}
|
}
|
||||||
@@ -109,7 +116,9 @@ $effect(() => {
|
|||||||
|
|
||||||
// Active section tracking
|
// Active section tracking
|
||||||
let activeSection = $state("cross");
|
let activeSection = $state("cross");
|
||||||
|
/** @type {Record<string, HTMLElement>} */
|
||||||
let sectionElements = {};
|
let sectionElements = {};
|
||||||
|
/** @type {HTMLElement | undefined} */
|
||||||
let svgContainer;
|
let svgContainer;
|
||||||
|
|
||||||
// Map pater sections to the large bead they share (so SVG highlights correctly)
|
// Map pater sections to the large bead they share (so SVG highlights correctly)
|
||||||
@@ -123,6 +132,10 @@ const svgActiveSection = $derived(
|
|||||||
const hasMysteryImages = $derived(showImages && (allMysteryImages[selectedMystery]?.size ?? 0) > 0);
|
const hasMysteryImages = $derived(showImages && (allMysteryImages[selectedMystery]?.size ?? 0) > 0);
|
||||||
|
|
||||||
// Mystery image scroll target based on active section (returns decade number 1-5, or 'before'/'after')
|
// Mystery image scroll target based on active section (returns decade number 1-5, or 'before'/'after')
|
||||||
|
/**
|
||||||
|
* @param {string} section
|
||||||
|
* @returns {number | 'before' | 'after'}
|
||||||
|
*/
|
||||||
function getMysteryScrollTarget(section) {
|
function getMysteryScrollTarget(section) {
|
||||||
if (section === 'lbead2') return 1;
|
if (section === 'lbead2') return 1;
|
||||||
const secretMatch = section.match(/^secret(\d)/);
|
const secretMatch = section.match(/^secret(\d)/);
|
||||||
@@ -138,6 +151,10 @@ function getMysteryScrollTarget(section) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mobile PiP: which image src to show (null = hide)
|
// Mobile PiP: which image src to show (null = hide)
|
||||||
|
/**
|
||||||
|
* @param {MysteryType} mystery
|
||||||
|
* @param {string} section
|
||||||
|
*/
|
||||||
function getMysteryImage(mystery, section) {
|
function getMysteryImage(mystery, section) {
|
||||||
const images = allMysteryImages[mystery];
|
const images = allMysteryImages[mystery];
|
||||||
if (!images || images.size === 0) return null;
|
if (!images || images.size === 0) return null;
|
||||||
@@ -149,7 +166,9 @@ const mysteryPipSrc = $derived(getMysteryImage(selectedMystery, activeSection));
|
|||||||
|
|
||||||
// Mobile PiP drag/enlarge
|
// Mobile PiP drag/enlarge
|
||||||
const pip = createPip({ fullscreenEnabled: true });
|
const pip = createPip({ fullscreenEnabled: true });
|
||||||
|
/** @type {HTMLElement | null} */
|
||||||
let rosaryPipEl = $state(null);
|
let rosaryPipEl = $state(null);
|
||||||
|
/** @type {string | null} */
|
||||||
let lastPipSrc = $state(null);
|
let lastPipSrc = $state(null);
|
||||||
|
|
||||||
function isMobilePip() {
|
function isMobilePip() {
|
||||||
@@ -172,21 +191,26 @@ $effect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @type {HTMLElement | undefined} */
|
||||||
let mysteryImageContainer;
|
let mysteryImageContainer;
|
||||||
|
/** @type {number | null} */
|
||||||
let mysteryScrollRaf = null;
|
let mysteryScrollRaf = null;
|
||||||
|
/** @type {number | string} */
|
||||||
let lastMysteryTarget = 'before';
|
let lastMysteryTarget = 'before';
|
||||||
|
|
||||||
|
/** @param {number} targetY @param {number} [duration] */
|
||||||
function scrollMysteryImage(targetY, duration = 1200) {
|
function scrollMysteryImage(targetY, duration = 1200) {
|
||||||
if (!mysteryImageContainer) return;
|
const container = mysteryImageContainer;
|
||||||
|
if (!container) return;
|
||||||
if (mysteryScrollRaf) cancelAnimationFrame(mysteryScrollRaf);
|
if (mysteryScrollRaf) cancelAnimationFrame(mysteryScrollRaf);
|
||||||
const startY = mysteryImageContainer.scrollTop;
|
const startY = container.scrollTop;
|
||||||
const distance = targetY - startY;
|
const distance = targetY - startY;
|
||||||
if (Math.abs(distance) < 1) return;
|
if (Math.abs(distance) < 1) return;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const ease = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
const ease = (/** @type {number} */ t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||||
const step = (now) => {
|
const step = (/** @type {number} */ now) => {
|
||||||
const progress = Math.min((now - startTime) / duration, 1);
|
const progress = Math.min((now - startTime) / duration, 1);
|
||||||
mysteryImageContainer.scrollTop = startY + distance * ease(progress);
|
container.scrollTop = startY + distance * ease(progress);
|
||||||
if (progress < 1) mysteryScrollRaf = requestAnimationFrame(step);
|
if (progress < 1) mysteryScrollRaf = requestAnimationFrame(step);
|
||||||
else mysteryScrollRaf = null;
|
else mysteryScrollRaf = null;
|
||||||
};
|
};
|
||||||
@@ -198,7 +222,7 @@ const IMAGE_COL_HEADER_OFFSET = 6; // rem — keep images below the sticky heade
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!mysteryImageContainer || !hasMysteryImages) return;
|
if (!mysteryImageContainer || !hasMysteryImages) return;
|
||||||
const targetName = getMysteryScrollTarget(activeSection);
|
const targetName = getMysteryScrollTarget(activeSection);
|
||||||
const targetEl = mysteryImageContainer.querySelector(`[data-target="${targetName}"]`);
|
const targetEl = /** @type {HTMLElement | null} */ (mysteryImageContainer.querySelector(`[data-target="${targetName}"]`));
|
||||||
if (targetEl) {
|
if (targetEl) {
|
||||||
const isEdge = targetName === 'before' || targetName === 'after';
|
const isEdge = targetName === 'before' || targetName === 'after';
|
||||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
@@ -232,11 +256,13 @@ let decadeCounters = $state({
|
|||||||
let showModal = $state(false);
|
let showModal = $state(false);
|
||||||
let selectedReference = $state('');
|
let selectedReference = $state('');
|
||||||
let selectedTitle = $state('');
|
let selectedTitle = $state('');
|
||||||
|
/** @type {any} */
|
||||||
let selectedVerseData = $state(null);
|
let selectedVerseData = $state(null);
|
||||||
|
|
||||||
// Function to advance the counter for a specific decade
|
// Function to advance the counter for a specific decade
|
||||||
|
/** @param {number} decadeNum */
|
||||||
function advanceDecade(decadeNum) {
|
function advanceDecade(decadeNum) {
|
||||||
const key = `secret${decadeNum}`;
|
const key = /** @type {'secret1'|'secret2'|'secret3'|'secret4'|'secret5'} */ (`secret${decadeNum}`);
|
||||||
if (decadeCounters[key] < 10) {
|
if (decadeCounters[key] < 10) {
|
||||||
decadeCounters[key] += 1;
|
decadeCounters[key] += 1;
|
||||||
|
|
||||||
@@ -268,6 +294,7 @@ function advanceDecade(decadeNum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to handle citation click
|
// Function to handle citation click
|
||||||
|
/** @param {string} reference @param {string} [title] @param {any} [verseData] */
|
||||||
function handleCitationClick(reference, title = '', verseData = null) {
|
function handleCitationClick(reference, title = '', verseData = null) {
|
||||||
selectedReference = reference;
|
selectedReference = reference;
|
||||||
selectedTitle = title;
|
selectedTitle = title;
|
||||||
@@ -299,6 +326,7 @@ onMount(() => {
|
|||||||
if (!data.hasUrlMystery) {
|
if (!data.hasUrlMystery) {
|
||||||
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
|
todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
|
||||||
const season = getLiturgicalSeason();
|
const season = getLiturgicalSeason();
|
||||||
|
/** @type {Record<string, MysteryType>} */
|
||||||
const seasonalMap = { eastertide: 'glorreichen', lent: 'schmerzhaften' };
|
const seasonalMap = { eastertide: 'glorreichen', lent: 'schmerzhaften' };
|
||||||
selectMystery(season ? seasonalMap[season] ?? todaysMystery : todaysMystery);
|
selectMystery(season ? seasonalMap[season] ?? todaysMystery : todaysMystery);
|
||||||
}
|
}
|
||||||
@@ -760,7 +788,7 @@ h1 {
|
|||||||
|
|
||||||
<!-- Toggle Controls & Streak Counter -->
|
<!-- Toggle Controls & Streak Counter -->
|
||||||
<div class="controls-row">
|
<div class="controls-row">
|
||||||
<StreakCounter streakData={data.streakData} lang={data.lang} isLoggedIn={data.isLoggedIn} />
|
<StreakCounter streakData={data.streakData} {lang} isLoggedIn={data.isLoggedIn} />
|
||||||
<div class="toggle-controls">
|
<div class="toggle-controls">
|
||||||
<!-- Luminous Mysteries Toggle (link for no-JS, enhanced with onclick for JS) -->
|
<!-- Luminous Mysteries Toggle (link for no-JS, enhanced with onclick for JS) -->
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -925,7 +953,7 @@ h1 {
|
|||||||
data-section={`secret${decadeNum}`}
|
data-section={`secret${decadeNum}`}
|
||||||
>
|
>
|
||||||
{#if showImages && allMysteryImages[selectedMystery]?.get(decadeNum)}
|
{#if showImages && allMysteryImages[selectedMystery]?.get(decadeNum)}
|
||||||
{@const img = allMysteryImages[selectedMystery].get(decadeNum)}
|
{@const img = /** @type {NonNullable<ReturnType<(typeof allMysteryImages)[MysteryType]['get']>>} */ (allMysteryImages[selectedMystery].get(decadeNum))}
|
||||||
<figure class="decade-inline-image">
|
<figure class="decade-inline-image">
|
||||||
<img src={img.src} alt={isEnglish ? img.title : img.titleDe} loading="lazy" />
|
<img src={img.src} alt={isEnglish ? img.title : img.titleDe} loading="lazy" />
|
||||||
<figcaption>{img.artist ? `${img.artist}, ` : ''}<em>{isEnglish ? img.title : img.titleDe}</em>{img.year ? `, ${img.year}` : ''}</figcaption>
|
<figcaption>{img.artist ? `${img.artist}, ` : ''}<em>{isEnglish ? img.title : img.titleDe}</em>{img.year ? `, ${img.year}` : ''}</figcaption>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
let { selectedMystery, todaysMystery, includeLuminous, labels, mysteryHref, selectMystery, season = null } = $props();
|
let { selectedMystery, todaysMystery, includeLuminous, labels, mysteryHref, selectMystery, season = null } = $props();
|
||||||
|
|
||||||
|
/** @type {string | null} */
|
||||||
const seasonalMystery = $derived(
|
const seasonalMystery = $derived(
|
||||||
season === 'eastertide' ? 'glorreichen'
|
season === 'eastertide' ? 'glorreichen'
|
||||||
: season === 'lent' ? 'schmerzhaften'
|
: season === 'lent' ? 'schmerzhaften'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/** @typedef {'freudenreich' | 'schmerzhaften' | 'glorreichen' | 'lichtreichen'} MysteryType */
|
||||||
|
|
||||||
// Mystery variations for each type of rosary
|
// Mystery variations for each type of rosary
|
||||||
export const mysteries = {
|
export const mysteries = {
|
||||||
freudenreich: [
|
freudenreich: [
|
||||||
@@ -158,6 +160,7 @@ export const mysteryTitlesEnglish = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// UI labels based on language
|
// UI labels based on language
|
||||||
|
/** @param {boolean} isEnglish */
|
||||||
export function getLabels(isEnglish) {
|
export function getLabels(isEnglish) {
|
||||||
return {
|
return {
|
||||||
pageTitle: isEnglish ? 'Interactive Rosary' : 'Interaktiver Rosenkranz',
|
pageTitle: isEnglish ? 'Interactive Rosary' : 'Interaktiver Rosenkranz',
|
||||||
@@ -198,10 +201,16 @@ export function getLabels(isEnglish) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the appropriate mystery for a given weekday
|
// Get the appropriate mystery for a given weekday
|
||||||
|
/**
|
||||||
|
* @param {Date} date
|
||||||
|
* @param {boolean} includeLuminous
|
||||||
|
* @returns {MysteryType}
|
||||||
|
*/
|
||||||
export function getMysteryForWeekday(date, includeLuminous) {
|
export function getMysteryForWeekday(date, includeLuminous) {
|
||||||
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
const dayOfWeek = /** @type {0|1|2|3|4|5|6} */ (date.getDay()); // 0 = Sunday, 1 = Monday, etc.
|
||||||
|
|
||||||
if (includeLuminous) {
|
if (includeLuminous) {
|
||||||
|
/** @type {Record<number, MysteryType>} */
|
||||||
const schedule = {
|
const schedule = {
|
||||||
0: 'glorreichen', // Sunday
|
0: 'glorreichen', // Sunday
|
||||||
1: 'freudenreich', // Monday
|
1: 'freudenreich', // Monday
|
||||||
@@ -213,6 +222,7 @@ export function getMysteryForWeekday(date, includeLuminous) {
|
|||||||
};
|
};
|
||||||
return schedule[dayOfWeek];
|
return schedule[dayOfWeek];
|
||||||
} else {
|
} else {
|
||||||
|
/** @type {Record<number, MysteryType>} */
|
||||||
const schedule = {
|
const schedule = {
|
||||||
0: 'glorreichen', // Sunday
|
0: 'glorreichen', // Sunday
|
||||||
1: 'freudenreich', // Monday
|
1: 'freudenreich', // Monday
|
||||||
@@ -231,6 +241,7 @@ export const BEAD_SPACING = 22;
|
|||||||
export const DECADE_OFFSET = 10;
|
export const DECADE_OFFSET = 10;
|
||||||
|
|
||||||
// Map sections to their vertical positions in the SVG
|
// Map sections to their vertical positions in the SVG
|
||||||
|
/** @type {Record<string, number>} */
|
||||||
export const sectionPositions = {
|
export const sectionPositions = {
|
||||||
cross: 35,
|
cross: 35,
|
||||||
lbead1: 75,
|
lbead1: 75,
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { sectionPositions } from './rosaryData.js';
|
|||||||
* the SVG rosary visualization, and the mystery image column.
|
* the SVG rosary visualization, and the mystery image column.
|
||||||
*
|
*
|
||||||
* @param {object} opts
|
* @param {object} opts
|
||||||
* @param {() => HTMLElement} opts.getSvgContainer - getter for the SVG scroll container
|
* @param {() => HTMLElement | undefined} opts.getSvgContainer - getter for the SVG scroll container
|
||||||
* @param {() => object} opts.getSectionElements - getter for the section-name → DOM element map
|
* @param {() => Record<string, HTMLElement>} opts.getSectionElements - getter for the section-name → DOM element map
|
||||||
* @param {() => HTMLElement} opts.getMysteryImageContainer - getter for the image column container
|
* @param {() => HTMLElement | undefined} opts.getMysteryImageContainer - getter for the image column container
|
||||||
* @param {() => string} opts.getActiveSection - getter for current active section
|
* @param {() => string} opts.getActiveSection - getter for current active section
|
||||||
* @param {(s: string) => void} opts.setActiveSection - setter for active section
|
* @param {(s: string) => void} opts.setActiveSection - setter for active section
|
||||||
* @returns {() => void} cleanup function
|
* @returns {() => void} cleanup function
|
||||||
@@ -19,9 +19,12 @@ export function setupScrollSync({
|
|||||||
getActiveSection,
|
getActiveSection,
|
||||||
setActiveSection,
|
setActiveSection,
|
||||||
}) {
|
}) {
|
||||||
|
/** @type {string | null} */
|
||||||
let scrollLock = null; // 'prayer', 'svg', or 'click'
|
let scrollLock = null; // 'prayer', 'svg', or 'click'
|
||||||
let scrollLockTimeout = null;
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
||||||
|
let scrollLockTimeout;
|
||||||
|
|
||||||
|
/** @param {string} source @param {number} [duration] */
|
||||||
const setScrollLock = (source, duration = 1000) => {
|
const setScrollLock = (source, duration = 1000) => {
|
||||||
scrollLock = source;
|
scrollLock = source;
|
||||||
clearTimeout(scrollLockTimeout);
|
clearTimeout(scrollLockTimeout);
|
||||||
@@ -31,6 +34,11 @@ export function setupScrollSync({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper: convert SVG section position to pixel scroll target
|
// Helper: convert SVG section position to pixel scroll target
|
||||||
|
/**
|
||||||
|
* @param {SVGSVGElement} svg
|
||||||
|
* @param {string} section
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
function svgSectionToPixel(svg, section) {
|
function svgSectionToPixel(svg, section) {
|
||||||
const svgYPosition = sectionPositions[section];
|
const svgYPosition = sectionPositions[section];
|
||||||
if (svgYPosition === undefined) return null;
|
if (svgYPosition === undefined) return null;
|
||||||
@@ -56,12 +64,13 @@ export function setupScrollSync({
|
|||||||
// Skip observer updates when at the top — handleWindowScroll handles this
|
// Skip observer updates when at the top — handleWindowScroll handles this
|
||||||
if (window.scrollY < 50) return;
|
if (window.scrollY < 50) return;
|
||||||
|
|
||||||
const section = entry.target.dataset.section;
|
const section = /** @type {HTMLElement} */ (entry.target).dataset.section;
|
||||||
|
if (!section) return;
|
||||||
setActiveSection(section);
|
setActiveSection(section);
|
||||||
|
|
||||||
// Scroll SVG to keep active section visible at top
|
// Scroll SVG to keep active section visible at top
|
||||||
if (svgContainer && sectionPositions[section] !== undefined) {
|
if (svgContainer && sectionPositions[section] !== undefined) {
|
||||||
const svg = svgContainer.querySelector('svg');
|
const svg = /** @type {SVGSVGElement | null} */ (svgContainer.querySelector('svg'));
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
|
|
||||||
const pixelPosition = svgSectionToPixel(svg, section);
|
const pixelPosition = svgSectionToPixel(svg, section);
|
||||||
@@ -141,7 +150,8 @@ export function setupScrollSync({
|
|||||||
window.addEventListener('scroll', handleWindowScroll, { passive: true });
|
window.addEventListener('scroll', handleWindowScroll, { passive: true });
|
||||||
|
|
||||||
// Debounce SVG scroll handler to avoid excessive updates
|
// Debounce SVG scroll handler to avoid excessive updates
|
||||||
let svgScrollTimeout = null;
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
||||||
|
let svgScrollTimeout;
|
||||||
const handleSvgScroll = () => {
|
const handleSvgScroll = () => {
|
||||||
const svgContainer = getSvgContainer();
|
const svgContainer = getSvgContainer();
|
||||||
const sectionElements = getSectionElements();
|
const sectionElements = getSectionElements();
|
||||||
@@ -149,7 +159,7 @@ export function setupScrollSync({
|
|||||||
|
|
||||||
clearTimeout(svgScrollTimeout);
|
clearTimeout(svgScrollTimeout);
|
||||||
svgScrollTimeout = setTimeout(() => {
|
svgScrollTimeout = setTimeout(() => {
|
||||||
const svg = svgContainer.querySelector('svg');
|
const svg = /** @type {SVGSVGElement | null} */ (svgContainer.querySelector('svg'));
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
|
|
||||||
const scrollTop = svgContainer.scrollTop;
|
const scrollTop = svgContainer.scrollTop;
|
||||||
@@ -195,10 +205,11 @@ export function setupScrollSync({
|
|||||||
|
|
||||||
// Handle clicks on SVG elements to jump to prayers
|
// Handle clicks on SVG elements to jump to prayers
|
||||||
// preventDefault() overrides the anchor-link fallback when JS is enabled
|
// preventDefault() overrides the anchor-link fallback when JS is enabled
|
||||||
|
/** @param {MouseEvent} e */
|
||||||
const handleSvgClick = (e) => {
|
const handleSvgClick = (e) => {
|
||||||
const svgContainer = getSvgContainer();
|
const svgContainer = getSvgContainer();
|
||||||
const sectionElements = getSectionElements();
|
const sectionElements = getSectionElements();
|
||||||
let target = e.target;
|
let target = /** @type {HTMLElement | null} */ (e.target);
|
||||||
while (target && target !== svgContainer) {
|
while (target && target !== svgContainer) {
|
||||||
const section = target.dataset.section;
|
const section = target.dataset.section;
|
||||||
if (section && sectionElements[section]) {
|
if (section && sectionElements[section]) {
|
||||||
@@ -207,8 +218,8 @@ export function setupScrollSync({
|
|||||||
setScrollLock('click', 1500);
|
setScrollLock('click', 1500);
|
||||||
|
|
||||||
// Scroll the SVG visualization to the clicked section
|
// Scroll the SVG visualization to the clicked section
|
||||||
if (sectionPositions[section] !== undefined) {
|
if (svgContainer && sectionPositions[section] !== undefined) {
|
||||||
const svg = svgContainer.querySelector('svg');
|
const svg = /** @type {SVGSVGElement | null} */ (svgContainer.querySelector('svg'));
|
||||||
if (svg) {
|
if (svg) {
|
||||||
const pixelPosition = svgSectionToPixel(svg, section);
|
const pixelPosition = svgSectionToPixel(svg, section);
|
||||||
if (pixelPosition !== null) {
|
if (pixelPosition !== null) {
|
||||||
@@ -231,7 +242,7 @@ export function setupScrollSync({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const svg = svgContainer?.querySelector('svg');
|
const svg = /** @type {SVGSVGElement | null} */ (svgContainer?.querySelector('svg') ?? null);
|
||||||
if (svg) {
|
if (svg) {
|
||||||
svg.addEventListener('click', handleSvgClick);
|
svg.addEventListener('click', handleSvgClick);
|
||||||
svg.style.cursor = 'pointer';
|
svg.style.cursor = 'pointer';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { onNavigate } from '$app/navigation';
|
|||||||
import Header from '$lib/components/Header.svelte'
|
import Header from '$lib/components/Header.svelte'
|
||||||
|
|
||||||
onNavigate((navigation) => {
|
onNavigate((navigation) => {
|
||||||
if (!document.startViewTransition) return;
|
if (!(/** @type {any} */ (document)).startViewTransition) return;
|
||||||
|
|
||||||
// Only use view transitions when navigating to/from a recipe detail page
|
// Only use view transitions when navigating to/from a recipe detail page
|
||||||
const toRecipe = navigation.to?.params?.name;
|
const toRecipe = navigation.to?.params?.name;
|
||||||
@@ -21,23 +21,23 @@ onNavigate((navigation) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const vt = document.startViewTransition(async () => {
|
const vt = (/** @type {any} */ (document)).startViewTransition(async () => {
|
||||||
resolve();
|
resolve();
|
||||||
await navigation.complete;
|
await navigation.complete;
|
||||||
|
|
||||||
// Hide .image-wrap background so the color box doesn't show behind the morphing image
|
// Hide .image-wrap background so the color box doesn't show behind the morphing image
|
||||||
const wrap = document.querySelector('.image-wrap');
|
const wrap = /** @type {HTMLElement | null} */ (document.querySelector('.image-wrap'));
|
||||||
if (wrap) wrap.style.backgroundColor = 'transparent';
|
if (wrap) wrap.style.backgroundColor = 'transparent';
|
||||||
|
|
||||||
// Set view-transition-name on the matching CompactCard/hero image for reverse morph
|
// Set view-transition-name on the matching CompactCard/hero image for reverse morph
|
||||||
if (fromRecipe) {
|
if (fromRecipe) {
|
||||||
const card = document.querySelector(`img[data-recipe="${fromRecipe}"]`);
|
const card = /** @type {HTMLElement | null} */ (document.querySelector(`img[data-recipe="${fromRecipe}"]`));
|
||||||
if (card) card.style.viewTransitionName = `recipe-${fromRecipe}-img`;
|
if (card) /** @type {any} */ (card.style).viewTransitionName = `recipe-${fromRecipe}-img`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Restore background color once transition finishes
|
// Restore background color once transition finishes
|
||||||
vt.finished.then(() => {
|
vt.finished.then(() => {
|
||||||
const wrap = document.querySelector('.image-wrap');
|
const wrap = /** @type {HTMLElement | null} */ (document.querySelector('.image-wrap'));
|
||||||
if (wrap) wrap.style.backgroundColor = '';
|
if (wrap) wrap.style.backgroundColor = '';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -60,6 +60,7 @@ const labels = $derived({
|
|||||||
keywords: 'Tags'
|
keywords: 'Tags'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @param {string} path */
|
||||||
function isActive(path) {
|
function isActive(path) {
|
||||||
const currentPath = $page.url.pathname;
|
const currentPath = $page.url.pathname;
|
||||||
// Exact match for recipe lang root
|
// Exact match for recipe lang root
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export async function load({ params, data }) {
|
|||||||
// Check if we're offline:
|
// Check if we're offline:
|
||||||
// 1. Browser reports offline (navigator.onLine === false)
|
// 1. Browser reports offline (navigator.onLine === false)
|
||||||
// 2. Service worker returned offline flag (data.isOffline === true)
|
// 2. Service worker returned offline flag (data.isOffline === true)
|
||||||
const isClientOffline = browser && (!navigator.onLine || data?.isOffline);
|
const isClientOffline = browser && (!navigator.onLine || (data as any)?.isOffline);
|
||||||
|
|
||||||
if (isClientOffline) {
|
if (isClientOffline) {
|
||||||
// Return minimal data for offline mode
|
// Return minimal data for offline mode
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
let matchedCategories = $state(new Set());
|
let matchedCategories = $state(new Set());
|
||||||
let hasActiveSearch = $state(false);
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
function handleSearchResults(ids, categories) {
|
function handleSearchResults(ids: Set<string>, categories: Set<string>) {
|
||||||
matchedRecipeIds = ids;
|
matchedRecipeIds = ids;
|
||||||
matchedCategories = categories || new Set();
|
matchedCategories = categories || new Set();
|
||||||
hasActiveSearch = ids.size < data.all_brief.length;
|
hasActiveSearch = ids.size < data.all_brief.length;
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
// Pick a seasonal hero recipe (changes daily) — only recipes with hashed images
|
// Pick a seasonal hero recipe (changes daily) — only recipes with hashed images
|
||||||
// Only recipes with hashed images (e.g. myrecipe.a1b2c3d4.webp)
|
// Only recipes with hashed images (e.g. myrecipe.a1b2c3d4.webp)
|
||||||
const hasHashedImage = (r) => r.images?.length > 0 && /\.\w+\.\w+$/.test(r.images[0].mediapath);
|
const hasHashedImage = (r: any) => r.images?.length > 0 && /\.\w+\.\w+$/.test(r.images[0].mediapath);
|
||||||
|
|
||||||
// Server-generated random index ensures SSR and client pick the same hero
|
// Server-generated random index ensures SSR and client pick the same hero
|
||||||
const heroIndex = data.heroIndex;
|
const heroIndex = data.heroIndex;
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasActiveSearch) {
|
if (hasActiveSearch) {
|
||||||
pool = pool.filter(r => matchedRecipeIds.has(r._id));
|
pool = pool.filter((r: any) => matchedRecipeIds.has(r._id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return pool;
|
return pool;
|
||||||
@@ -368,8 +368,8 @@
|
|||||||
<p class="subheading">{labels.subheading}</p>
|
<p class="subheading">{labels.subheading}</p>
|
||||||
<a href="/{data.recipeLang}/{heroRecipe.short_name}" class="hero-featured"
|
<a href="/{data.recipeLang}/{heroRecipe.short_name}" class="hero-featured"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
const img = document.querySelector('.hero-img');
|
const img = document.querySelector('.hero-img') as HTMLElement | null;
|
||||||
if (img) img.style.viewTransitionName = `recipe-${heroRecipe.short_name}-img`;
|
if (img) (img.style as any).viewTransitionName = `recipe-${heroRecipe.short_name}-img`;
|
||||||
}}>
|
}}>
|
||||||
<span class="recipe-name"><span class="recipe-icon">{heroRecipe.icon}</span> {@html heroRecipe.name}</span>
|
<span class="recipe-name"><span class="recipe-icon">{heroRecipe.icon}</span> {@html heroRecipe.name}</span>
|
||||||
<svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="-10 -197 535 410"><path d="M503 31c12-13 12-33 0-46L343-175c-13-12-33-12-46 0-12 13-12 33 0 46L403-24H32C14-24 0-10 0 8s14 32 32 32h371L297 145c-12 13-12 33 0 46 13 12 33 12 46 0L503 31z"/></svg>
|
<svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="-10 -197 535 410"><path d="M503 31c12-13 12-33 0-46L343-175c-13-12-33-12-46 0-12 13-12 33 0 46L403-24H32C14-24 0-10 0 8s14 32 32 32h371L297 145c-12 13-12 33 0 46 13 12 33 12 46 0L503 31z"/></svg>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function load({ data }) {
|
|||||||
// 1. We're offline (navigator.onLine is false)
|
// 1. We're offline (navigator.onLine is false)
|
||||||
// 2. Service worker returned offline flag
|
// 2. Service worker returned offline flag
|
||||||
// 3. Server data is missing (e.g., client-side navigation while offline)
|
// 3. Server data is missing (e.g., client-side navigation while offline)
|
||||||
const shouldUseOfflineData = (isOffline() || data?.isOffline || !data?.all_brief?.length) && canUseOfflineData();
|
const shouldUseOfflineData = (isOffline() || (data as any)?.isOffline || !data?.all_brief?.length) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOfflineData) {
|
if (shouldUseOfflineData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var start = data.season[start_i]
|
var start = data.season[start_i]
|
||||||
var end_i
|
var end_i: number = start_i
|
||||||
const len = data.season.length
|
const len = data.season.length
|
||||||
for(var i = 0; i < len -1; i++){
|
for(var i = 0; i < len -1; i++){
|
||||||
if(data.season.includes((start + i) %12 + 1)){
|
if(data.season.includes((start + i) %12 + 1)){
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
const season_iv = $derived(season_intervals());
|
const season_iv = $derived(season_intervals());
|
||||||
|
|
||||||
const display_date = $derived(data.updatedAt ? new Date(data.updatedAt) : new Date(data.dateCreated));
|
const display_date = $derived(data.updatedAt ? new Date(data.updatedAt) : new Date(data.dateCreated));
|
||||||
const options = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function load({ fetch, params, url, data }) {
|
|||||||
let isOfflineMode = false;
|
let isOfflineMode = false;
|
||||||
|
|
||||||
// On the client, check if we need to load from IndexedDB
|
// On the client, check if we need to load from IndexedDB
|
||||||
const shouldUseOfflineData = browser && (isOffline() || data?.isOffline || !data?.item) && canUseOfflineData();
|
const shouldUseOfflineData = browser && (isOffline() || (data as any)?.isOffline || !data?.item) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOfflineData) {
|
if (shouldUseOfflineData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const actions = {
|
|||||||
const recipeData = extractRecipeFromFormData(formData);
|
const recipeData = extractRecipeFromFormData(formData);
|
||||||
console.log('[RecipeAdd] Recipe data extracted:', {
|
console.log('[RecipeAdd] Recipe data extracted:', {
|
||||||
short_name: recipeData.short_name,
|
short_name: recipeData.short_name,
|
||||||
title: recipeData.title
|
name: recipeData.name
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
@@ -147,8 +147,7 @@ export const actions = {
|
|||||||
|
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
error: `Failed to process recipe: ${error.message || 'Unknown error'}`,
|
error: `Failed to process recipe: ${error.message || 'Unknown error'}`,
|
||||||
errors: [error.message],
|
errors: [error.message]
|
||||||
values: Object.fromEntries(formData)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
let short_name = $state("");
|
let short_name = $state("");
|
||||||
let isBaseRecipe = $state(false);
|
let isBaseRecipe = $state(false);
|
||||||
let defaultForm = $state(null);
|
let defaultForm = $state<{ shape: string; diameter?: number; width?: number; length?: number; innerDiameter?: number } | null>(null);
|
||||||
let ingredients = $state<any[]>([]);
|
let ingredients = $state<any[]>([]);
|
||||||
let instructions = $state<any[]>([]);
|
let instructions = $state<any[]>([]);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
let processing = $state(false);
|
let processing = $state(false);
|
||||||
let filter = $state('missing');
|
let filter = $state('missing');
|
||||||
let limit = $state(50);
|
let limit = $state(50);
|
||||||
|
/** @type {any[]} */
|
||||||
let results = $state([]);
|
let results = $state([]);
|
||||||
let errorMsg = $state('');
|
let errorMsg = $state('');
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
const stats = $derived.by(() => {
|
const stats = $derived.by(() => {
|
||||||
const noTranslation = data.untranslated.filter(r => !r.translationStatus).length;
|
const noTranslation = data.untranslated.filter((r: any) => !r.translationStatus).length;
|
||||||
const pending = data.untranslated.filter(r => r.translationStatus === 'pending').length;
|
const pending = data.untranslated.filter((r: any) => r.translationStatus === 'pending').length;
|
||||||
const needsUpdate = data.untranslated.filter(r => r.translationStatus === 'needs_update').length;
|
const needsUpdate = data.untranslated.filter((r: any) => r.translationStatus === 'needs_update').length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: data.untranslated.length,
|
total: data.untranslated.length,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function load({ data, params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On the client, check if we need to load from IndexedDB
|
// On the client, check if we need to load from IndexedDB
|
||||||
const shouldUseOfflineData = (isOffline() || data?.isOffline || !data?.recipes?.length) && canUseOfflineData();
|
const shouldUseOfflineData = (isOffline() || (data as any)?.isOffline || !data?.recipes?.length) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOfflineData) {
|
if (shouldUseOfflineData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -182,8 +182,8 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update image mediapath if it was using the old short_name
|
// Update image mediapath if it was using the old short_name
|
||||||
if (recipeData.images[0].mediapath === `${originalShortName}.webp`) {
|
if (recipeData.images?.[0]?.mediapath === `${originalShortName}.webp`) {
|
||||||
recipeData.images[0].mediapath = `${recipeData.short_name}.webp`;
|
recipeData.images![0].mediapath = `${recipeData.short_name}.webp`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,8 +246,7 @@ export const actions = {
|
|||||||
|
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
error: `Failed to process recipe update: ${error.message || 'Unknown error'}`,
|
error: `Failed to process recipe update: ${error.message || 'Unknown error'}`,
|
||||||
errors: [error.message],
|
errors: [error.message]
|
||||||
values: Object.fromEntries(formData)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,8 +159,8 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const field of fieldsToCheck) {
|
for (const field of fieldsToCheck) {
|
||||||
const oldValue = JSON.stringify(originalRecipe[field] || '');
|
const oldValue = JSON.stringify((originalRecipe as Record<string, any>)[field] || '');
|
||||||
const newValue = JSON.stringify(current[field] || '');
|
const newValue = JSON.stringify((current as Record<string, any>)[field] || '');
|
||||||
if (oldValue !== newValue) {
|
if (oldValue !== newValue) {
|
||||||
changed.push(field);
|
changed.push(field);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const load: PageServerLoad = async ({ fetch, locals, params }) => {
|
|||||||
const favorites = await res.json();
|
const favorites = await res.json();
|
||||||
|
|
||||||
// Mark all favorites with isFavorite flag for filter compatibility
|
// Mark all favorites with isFavorite flag for filter compatibility
|
||||||
const favoritesWithFlag = favorites.map(recipe => ({
|
const favoritesWithFlag = favorites.map((recipe: any) => ({
|
||||||
...recipe,
|
...recipe,
|
||||||
isFavorite: true
|
isFavorite: true
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const load: PageLoad = async ({ data, params }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should use offline data
|
// Check if we should use offline data
|
||||||
const shouldUseOffline = browser && (isOffline() || data?.isOffline) && canUseOfflineData();
|
const shouldUseOffline = browser && (isOffline() || (data as any)?.isOffline) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOffline) {
|
if (shouldUseOffline) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,17 +13,17 @@
|
|||||||
let hasActiveSearch = $state(false);
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
// Handle search results from Search component
|
// Handle search results from Search component
|
||||||
function handleSearchResults(ids, categories) {
|
function handleSearchResults(ids: Set<string>, categories: Set<string>) {
|
||||||
matchedRecipeIds = ids;
|
matchedRecipeIds = ids;
|
||||||
hasActiveSearch = ids.size < data.season.length;
|
hasActiveSearch = ids.size < data.season.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter recipes based on search
|
// Filter recipes based on search
|
||||||
const filteredRecipes = $derived.by(() => {
|
const filteredRecipes: any[] = $derived.by(() => {
|
||||||
if (!hasActiveSearch) {
|
if (!hasActiveSearch) {
|
||||||
return data.season;
|
return data.season;
|
||||||
}
|
}
|
||||||
return data.season.filter(r => matchedRecipeIds.has(r._id));
|
return data.season.filter((r: any) => matchedRecipeIds.has(r._id));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function load({ data, params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On the client, check if we need to load from IndexedDB
|
// On the client, check if we need to load from IndexedDB
|
||||||
const shouldUseOfflineData = (isOffline() || data?.isOffline || !data?.season?.length) && canUseOfflineData();
|
const shouldUseOfflineData = (isOffline() || (data as any)?.isOffline || !data?.season?.length) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOfflineData) {
|
if (shouldUseOfflineData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
let current_month = new Date().getMonth() + 1;
|
let current_month = new Date().getMonth() + 1;
|
||||||
|
|
||||||
const isEnglish = $derived(data.lang === 'en');
|
const isEnglish = $derived(data.lang === 'en');
|
||||||
const categories = $derived(isEnglish
|
|
||||||
? ["Main course", "Noodle", "Bread", "Dessert", "Soup", "Side dish", "Salad", "Cake", "Breakfast", "Sauce", "Ingredient", "Drink", "Spread", "Biscuits", "Snack"]
|
|
||||||
: ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"]);
|
|
||||||
const labels = $derived({
|
const labels = $derived({
|
||||||
title: isEnglish ? 'Search Results' : 'Suchergebnisse',
|
title: isEnglish ? 'Search Results' : 'Suchergebnisse',
|
||||||
pageTitle: isEnglish
|
pageTitle: isEnglish
|
||||||
@@ -34,7 +31,7 @@
|
|||||||
let hasActiveSearch = $state(false);
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
// Handle search results from Search component
|
// Handle search results from Search component
|
||||||
function handleSearchResults(ids, categories) {
|
function handleSearchResults(ids: Set<string>, categories: Set<string>) {
|
||||||
matchedRecipeIds = ids;
|
matchedRecipeIds = ids;
|
||||||
hasActiveSearch = ids.size < data.allRecipes.length;
|
hasActiveSearch = ids.size < data.allRecipes.length;
|
||||||
}
|
}
|
||||||
@@ -46,7 +43,7 @@
|
|||||||
return data.results;
|
return data.results;
|
||||||
}
|
}
|
||||||
// Active search - show client-side filtered results
|
// Active search - show client-side filtered results
|
||||||
return data.allRecipes.filter(r => matchedRecipeIds.has(r._id));
|
return data.allRecipes.filter((r: any) => matchedRecipeIds.has(r._id));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -94,7 +91,6 @@
|
|||||||
favoritesOnly={data.filters.favoritesOnly}
|
favoritesOnly={data.filters.favoritesOnly}
|
||||||
lang={data.lang}
|
lang={data.lang}
|
||||||
recipes={data.allRecipes}
|
recipes={data.allRecipes}
|
||||||
categories={categories}
|
|
||||||
isLoggedIn={!!data.session?.user}
|
isLoggedIn={!!data.session?.user}
|
||||||
onSearchResults={handleSearchResults}
|
onSearchResults={handleSearchResults}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,17 +20,17 @@
|
|||||||
let hasActiveSearch = $state(false);
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
// Handle search results from Search component
|
// Handle search results from Search component
|
||||||
function handleSearchResults(ids, categories) {
|
function handleSearchResults(ids: Set<string>, categories: Set<string>) {
|
||||||
matchedRecipeIds = ids;
|
matchedRecipeIds = ids;
|
||||||
hasActiveSearch = ids.size < data.season.length;
|
hasActiveSearch = ids.size < data.season.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter recipes based on search
|
// Filter recipes based on search
|
||||||
const filteredRecipes = $derived.by(() => {
|
const filteredRecipes: any[] = $derived.by(() => {
|
||||||
if (!hasActiveSearch) {
|
if (!hasActiveSearch) {
|
||||||
return data.season;
|
return data.season;
|
||||||
}
|
}
|
||||||
return data.season.filter(r => matchedRecipeIds.has(r._id));
|
return data.season.filter((r: any) => matchedRecipeIds.has(r._id));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function load({ data }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On the client, check if we need to load from IndexedDB
|
// On the client, check if we need to load from IndexedDB
|
||||||
const shouldUseOfflineData = (isOffline() || data?.isOffline || !data?.season?.length) && canUseOfflineData();
|
const shouldUseOfflineData = (isOffline() || (data as any)?.isOffline || !data?.season?.length) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOfflineData) {
|
if (shouldUseOfflineData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,17 +18,17 @@
|
|||||||
let hasActiveSearch = $state(false);
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
// Handle search results from Search component
|
// Handle search results from Search component
|
||||||
function handleSearchResults(ids, categories) {
|
function handleSearchResults(ids: Set<string>, categories: Set<string>) {
|
||||||
matchedRecipeIds = ids;
|
matchedRecipeIds = ids;
|
||||||
hasActiveSearch = ids.size < data.season.length;
|
hasActiveSearch = ids.size < data.season.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter recipes based on search
|
// Filter recipes based on search
|
||||||
const filteredRecipes = $derived.by(() => {
|
const filteredRecipes: any[] = $derived.by(() => {
|
||||||
if (!hasActiveSearch) {
|
if (!hasActiveSearch) {
|
||||||
return data.season;
|
return data.season;
|
||||||
}
|
}
|
||||||
return data.season.filter(r => matchedRecipeIds.has(r._id));
|
return data.season.filter((r: any) => matchedRecipeIds.has(r._id));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function load({ data, params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On the client, check if we need to load from IndexedDB
|
// On the client, check if we need to load from IndexedDB
|
||||||
const shouldUseOfflineData = (isOffline() || data?.isOffline || !data?.season?.length) && canUseOfflineData();
|
const shouldUseOfflineData = (isOffline() || (data as any)?.isOffline || !data?.season?.length) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOfflineData) {
|
if (shouldUseOfflineData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
let query = $state('');
|
let query = $state('');
|
||||||
const filteredTags = $derived(
|
const filteredTags = $derived(
|
||||||
query
|
query
|
||||||
? data.tags.filter(t => t.toLowerCase().includes(query.toLowerCase()))
|
? data.tags.filter((t: string) => t.toLowerCase().includes(query.toLowerCase()))
|
||||||
: data.tags
|
: data.tags
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function load({ data, params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On the client, check if we need to load from IndexedDB
|
// On the client, check if we need to load from IndexedDB
|
||||||
const shouldUseOfflineData = (isOffline() || data?.isOffline || !data?.recipes?.length) && canUseOfflineData();
|
const shouldUseOfflineData = (isOffline() || (data as any)?.isOffline || !data?.recipes?.length) && canUseOfflineData();
|
||||||
|
|
||||||
if (shouldUseOfflineData) {
|
if (shouldUseOfflineData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
set trockenhefe(value) {
|
set trockenhefe(value) {
|
||||||
this._trockenhefe = value.replace(/\D/g, '');
|
this._trockenhefe = /** @type {any} */ (value).replace(/\D/g, '');
|
||||||
this._frischhefe = this._trockenhefe * 3;
|
this._frischhefe = this._trockenhefe * 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
set frischhefe(value) {
|
set frischhefe(value) {
|
||||||
this._frischhefe = value.replace(/\D/g, '');
|
this._frischhefe = /** @type {any} */ (value).replace(/\D/g, '');
|
||||||
this._trockenhefe = this._frischhefe / 3;
|
this._trockenhefe = this._frischhefe / 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
links.push({ url: '', label: '' });
|
links.push({ url: '', label: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {number} index */
|
||||||
function removeLinkRow(index) {
|
function removeLinkRow(index) {
|
||||||
links.splice(index, 1);
|
links.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -54,9 +55,10 @@
|
|||||||
showForm = false;
|
showForm = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {any} item */
|
||||||
function handleEdit(item) {
|
function handleEdit(item) {
|
||||||
name = item.name;
|
name = item.name;
|
||||||
links = item.links.map(l => ({ url: l.url, label: l.label || '' }));
|
links = item.links.map((/** @type {any} */ l) => ({ url: l.url, label: l.label || '' }));
|
||||||
notes = item.notes || '';
|
notes = item.notes || '';
|
||||||
editingId = item._id;
|
editingId = item._id;
|
||||||
showForm = true;
|
showForm = true;
|
||||||
@@ -76,7 +78,7 @@
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const updated = await res.json();
|
const updated = await res.json();
|
||||||
items = items.map(i => i._id === editingId ? updated : i);
|
items = items.map((/** @type {any} */ i) => i._id === editingId ? updated : i);
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {any} id */
|
||||||
async function handleDelete(id) {
|
async function handleDelete(id) {
|
||||||
const msg = isEnglish ? 'Delete this recipe?' : 'Dieses Rezept löschen?';
|
const msg = isEnglish ? 'Delete this recipe?' : 'Dieses Rezept löschen?';
|
||||||
if (!confirm(msg)) return;
|
if (!confirm(msg)) return;
|
||||||
@@ -106,7 +109,7 @@
|
|||||||
body: JSON.stringify({ id })
|
body: JSON.stringify({ id })
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
items = items.filter(i => i._id !== id);
|
items = items.filter((/** @type {any} */ i) => i._id !== id);
|
||||||
if (editingId === id) resetForm();
|
if (editingId === id) resetForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const POST: RequestHandler = async ({request, cookies, locals}) => {
|
|||||||
// Invalidate recipe caches after successful creation
|
// Invalidate recipe caches after successful creation
|
||||||
await invalidateRecipeCaches();
|
await invalidateRecipeCaches();
|
||||||
} catch(e){
|
} catch(e){
|
||||||
throw error(400, e)
|
throw error(400, e instanceof Error ? e.message : String(e))
|
||||||
}
|
}
|
||||||
return new Response(JSON.stringify({msg: "Added recipe successfully"}),{
|
return new Response(JSON.stringify({msg: "Added recipe successfully"}),{
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const POST: RequestHandler = async ({request, locals}) => {
|
|||||||
for (const depRecipe of referencingRecipes) {
|
for (const depRecipe of referencingRecipes) {
|
||||||
// Expand ingredient references
|
// Expand ingredient references
|
||||||
if (depRecipe.ingredients) {
|
if (depRecipe.ingredients) {
|
||||||
depRecipe.ingredients = depRecipe.ingredients.flatMap((item: any) => {
|
(depRecipe as any).ingredients = depRecipe.ingredients.flatMap((item: any) => {
|
||||||
if (item.type === 'reference' && item.baseRecipeRef && item.baseRecipeRef.equals(recipe._id)) {
|
if (item.type === 'reference' && item.baseRecipeRef && item.baseRecipeRef.equals(recipe._id)) {
|
||||||
if (item.includeIngredients && recipe.ingredients) {
|
if (item.includeIngredients && recipe.ingredients) {
|
||||||
return recipe.ingredients.filter((i: any) => i.type === 'section' || !i.type);
|
return recipe.ingredients.filter((i: any) => i.type === 'section' || !i.type);
|
||||||
@@ -47,7 +47,7 @@ export const POST: RequestHandler = async ({request, locals}) => {
|
|||||||
|
|
||||||
// Expand instruction references
|
// Expand instruction references
|
||||||
if (depRecipe.instructions) {
|
if (depRecipe.instructions) {
|
||||||
depRecipe.instructions = depRecipe.instructions.flatMap((item: any) => {
|
(depRecipe as any).instructions = depRecipe.instructions.flatMap((item: any) => {
|
||||||
if (item.type === 'reference' && item.baseRecipeRef && item.baseRecipeRef.equals(recipe._id)) {
|
if (item.type === 'reference' && item.baseRecipeRef && item.baseRecipeRef.equals(recipe._id)) {
|
||||||
if (item.includeInstructions && recipe.instructions) {
|
if (item.includeInstructions && recipe.instructions) {
|
||||||
return recipe.instructions.filter((i: any) => i.type === 'section' || !i.type);
|
return recipe.instructions.filter((i: any) => i.type === 'section' || !i.type);
|
||||||
|
|||||||
@@ -24,31 +24,33 @@ export const GET: RequestHandler = async ({ params, locals }) => {
|
|||||||
return json([]);
|
return json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { approvalFilter } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter } = briefQueryConfig(params.recipeLang!);
|
||||||
const en = isEnglish(params.recipeLang);
|
const en = isEnglish(params.recipeLang!);
|
||||||
|
|
||||||
let recipes = await Recipe.find({
|
let recipes = await Recipe.find({
|
||||||
_id: { $in: userFavorites.favorites },
|
_id: { $in: userFavorites.favorites },
|
||||||
...approvalFilter
|
...approvalFilter
|
||||||
}).lean() as RecipeModelType[];
|
}).lean() as unknown as RecipeModelType[];
|
||||||
|
|
||||||
if (en) {
|
if (en) {
|
||||||
const englishRecipes = recipes.map(recipe => ({
|
const englishRecipes = recipes.map(recipe => {
|
||||||
|
const t = recipe.translations?.en;
|
||||||
|
return {
|
||||||
_id: recipe._id,
|
_id: recipe._id,
|
||||||
short_name: recipe.translations.en.short_name,
|
short_name: t?.short_name,
|
||||||
name: recipe.translations.en.name,
|
name: t?.name,
|
||||||
category: recipe.translations.en.category,
|
category: t?.category,
|
||||||
icon: recipe.icon,
|
icon: recipe.icon,
|
||||||
dateCreated: recipe.dateCreated,
|
dateCreated: recipe.dateCreated,
|
||||||
dateModified: recipe.dateModified,
|
dateModified: recipe.dateModified,
|
||||||
images: recipe.images?.map((img, idx) => ({
|
images: recipe.images?.map((img, idx) => ({
|
||||||
mediapath: img.mediapath,
|
mediapath: img.mediapath,
|
||||||
alt: recipe.translations.en.images?.[idx]?.alt || img.alt,
|
alt: t?.images?.[idx]?.alt || img.alt,
|
||||||
caption: recipe.translations.en.images?.[idx]?.caption || img.caption,
|
caption: t?.images?.[idx]?.caption || img.caption,
|
||||||
})),
|
})),
|
||||||
description: recipe.translations.en.description,
|
description: t?.description,
|
||||||
note: recipe.translations.en.note,
|
note: t?.note,
|
||||||
tags: recipe.translations.en.tags || [],
|
tags: t?.tags || [],
|
||||||
season: recipe.season,
|
season: recipe.season,
|
||||||
baking: recipe.baking,
|
baking: recipe.baking,
|
||||||
preparation: recipe.preparation,
|
preparation: recipe.preparation,
|
||||||
@@ -56,13 +58,13 @@ export const GET: RequestHandler = async ({ params, locals }) => {
|
|||||||
portions: recipe.portions,
|
portions: recipe.portions,
|
||||||
cooking: recipe.cooking,
|
cooking: recipe.cooking,
|
||||||
total_time: recipe.total_time,
|
total_time: recipe.total_time,
|
||||||
ingredients: recipe.translations.en.ingredients || [],
|
ingredients: t?.ingredients || [],
|
||||||
instructions: recipe.translations.en.instructions || [],
|
instructions: t?.instructions || [],
|
||||||
preamble: recipe.translations.en.preamble,
|
preamble: t?.preamble,
|
||||||
addendum: recipe.translations.en.addendum,
|
addendum: t?.addendum,
|
||||||
germanShortName: recipe.short_name,
|
germanShortName: recipe.short_name,
|
||||||
translationStatus: recipe.translations.en.translationStatus
|
translationStatus: t?.translationStatus
|
||||||
}));
|
}});
|
||||||
return json(JSON.parse(JSON.stringify(englishRecipes)));
|
return json(JSON.parse(JSON.stringify(englishRecipes)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function mapBaseRecipeRefs(items: any[]): any[] {
|
|||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
const en = isEnglish(params.recipeLang);
|
const en = isEnglish(params.recipeLang!);
|
||||||
|
|
||||||
const query = en
|
const query = en
|
||||||
? { 'translations.en.short_name': params.name }
|
? { 'translations.en.short_name': params.name }
|
||||||
@@ -84,7 +84,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
let dbQuery = Recipe.findOne(query);
|
let dbQuery: any = Recipe.findOne(query);
|
||||||
for (const p of populatePaths) {
|
for (const p of populatePaths) {
|
||||||
dbQuery = dbQuery.populate(p);
|
dbQuery = dbQuery.populate(p);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import cache from '$lib/server/cache';
|
|||||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
|
||||||
const cacheKey = `recipes:${params.recipeLang}:all_brief`;
|
const cacheKey = `recipes:${params.recipeLang}:all_brief`;
|
||||||
|
|
||||||
let recipes: BriefRecipeType[] | null = null;
|
let recipes: BriefRecipeType[] | null = null;
|
||||||
@@ -17,10 +17,10 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
recipes = JSON.parse(cached);
|
recipes = JSON.parse(cached);
|
||||||
} else {
|
} else {
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
const dbRecipes = await Recipe.find(approvalFilter, projection).lean();
|
const dbRecipes: any[] = await Recipe.find(approvalFilter, projection).lean();
|
||||||
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang));
|
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||||
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json(JSON.parse(JSON.stringify(rand_array(recipes))));
|
return json(JSON.parse(JSON.stringify(rand_array(recipes!))));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
|||||||
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
|
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const { approvalFilter, prefix } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter, prefix } = briefQueryConfig(params.recipeLang!);
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
const field = `${prefix}category`;
|
const field = `${prefix}category`;
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { rand_array } from '$lib/js/randomize';
|
|||||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
const dbRecipes = await Recipe.find(
|
const dbRecipes: any[] = await Recipe.find(
|
||||||
{ [`${prefix}category`]: params.category, ...approvalFilter },
|
{ [`${prefix}category`]: params.category, ...approvalFilter },
|
||||||
projection
|
projection
|
||||||
).lean();
|
).lean();
|
||||||
|
|
||||||
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang)));
|
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang!)));
|
||||||
return json(JSON.parse(JSON.stringify(recipes)));
|
return json(JSON.parse(JSON.stringify(recipes)));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { rand_array } from '$lib/js/randomize';
|
|||||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
const dbRecipes = await Recipe.find(
|
const dbRecipes = await Recipe.find(
|
||||||
@@ -13,6 +13,6 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
projection
|
projection
|
||||||
).lean();
|
).lean();
|
||||||
|
|
||||||
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang)));
|
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang!)));
|
||||||
return json(JSON.parse(JSON.stringify(recipes)));
|
return json(JSON.parse(JSON.stringify(recipes)));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import cache from '$lib/server/cache';
|
|||||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
|
||||||
const cacheKey = `recipes:${params.recipeLang}:in_season:${params.month}`;
|
const cacheKey = `recipes:${params.recipeLang}:in_season:${params.month}`;
|
||||||
|
|
||||||
let recipes = null;
|
let recipes = null;
|
||||||
@@ -20,7 +20,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
{ season: params.month, icon: { $ne: "🍽️" }, ...approvalFilter },
|
{ season: params.month, icon: { $ne: "🍽️" }, ...approvalFilter },
|
||||||
projection
|
projection
|
||||||
).lean();
|
).lean();
|
||||||
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang));
|
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||||
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { dbConnect } from '$utils/db';
|
|||||||
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
|
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const { approvalFilter } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter } = briefQueryConfig(params.recipeLang!);
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
if (isEnglish(params.recipeLang)) {
|
if (isEnglish(params.recipeLang!)) {
|
||||||
const recipes = await Recipe.find(approvalFilter, 'translations.en.tags').lean();
|
const recipes = await Recipe.find(approvalFilter, 'translations.en.tags').lean();
|
||||||
const tagsSet = new Set<string>();
|
const tagsSet = new Set<string>();
|
||||||
recipes.forEach(recipe => {
|
recipes.forEach(recipe => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import cache from '$lib/server/cache';
|
|||||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
|
||||||
const cacheKey = `recipes:${params.recipeLang}:tag:${params.tag}`;
|
const cacheKey = `recipes:${params.recipeLang}:tag:${params.tag}`;
|
||||||
|
|
||||||
let recipes = null;
|
let recipes = null;
|
||||||
@@ -20,7 +20,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
{ [`${prefix}tags`]: params.tag, ...approvalFilter },
|
{ [`${prefix}tags`]: params.tag, ...approvalFilter },
|
||||||
projection
|
projection
|
||||||
).lean();
|
).lean();
|
||||||
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang));
|
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||||
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { error } from '@sveltejs/kit';
|
|||||||
|
|
||||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
let recipe = (await Recipe.findOne({ short_name: params.name }).lean()) as RecipeModelType;
|
let recipe = (await Recipe.findOne({ short_name: params.name }).lean()) as unknown as RecipeModelType;
|
||||||
|
|
||||||
recipe = JSON.parse(JSON.stringify(recipe));
|
recipe = JSON.parse(JSON.stringify(recipe));
|
||||||
if (recipe == null) {
|
if (recipe == null) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const GET: RequestHandler = async () => {
|
|||||||
const briefRecipes = await Recipe.find(
|
const briefRecipes = await Recipe.find(
|
||||||
{},
|
{},
|
||||||
'name short_name tags category icon description season dateModified images translations'
|
'name short_name tags category icon description season dateModified images translations'
|
||||||
).lean() as BriefRecipeType[];
|
).lean() as unknown as BriefRecipeType[];
|
||||||
|
|
||||||
// Fetch full recipes with populated base recipe references
|
// Fetch full recipes with populated base recipe references
|
||||||
const fullRecipes = await Recipe.find({})
|
const fullRecipes = await Recipe.find({})
|
||||||
@@ -39,7 +39,7 @@ export const GET: RequestHandler = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.lean() as RecipeModelType[];
|
.lean() as unknown as RecipeModelType[];
|
||||||
|
|
||||||
// Map populated refs to resolvedRecipe field (same as individual item endpoint)
|
// Map populated refs to resolvedRecipe field (same as individual item endpoint)
|
||||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { isEnglish, briefQueryConfig, toBrief } from '$lib/server/recipeHelpers'
|
|||||||
export const GET: RequestHandler = async ({ url, params, locals }) => {
|
export const GET: RequestHandler = async ({ url, params, locals }) => {
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang);
|
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
|
||||||
const en = isEnglish(params.recipeLang);
|
const en = isEnglish(params.recipeLang!);
|
||||||
|
|
||||||
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
|
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
|
||||||
const category = url.searchParams.get('category');
|
const category = url.searchParams.get('category');
|
||||||
@@ -46,12 +46,12 @@ export const GET: RequestHandler = async ({ url, params, locals }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dbRecipes = await Recipe.find(dbQuery, projection).lean();
|
const dbRecipes = await Recipe.find(dbQuery, projection).lean();
|
||||||
let recipes: BriefRecipeType[] = dbRecipes.map(r => toBrief(r, params.recipeLang));
|
let recipes: BriefRecipeType[] = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||||
|
|
||||||
// Handle favorites filter
|
// Handle favorites filter
|
||||||
if (favoritesOnly && locals.session?.user) {
|
if (favoritesOnly && (locals as any).session?.user) {
|
||||||
const { UserFavorites } = await import('$models/UserFavorites');
|
const { UserFavorites } = await import('$models/UserFavorites');
|
||||||
const userFavorites = await UserFavorites.findOne({ username: locals.session.user.username });
|
const userFavorites = await UserFavorites.findOne({ username: (locals as any).session.user.username });
|
||||||
if (userFavorites?.favorites) {
|
if (userFavorites?.favorites) {
|
||||||
const favoriteIds = userFavorites.favorites;
|
const favoriteIds = userFavorites.favorites;
|
||||||
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
|
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
.lean();
|
.lean();
|
||||||
|
|
||||||
// Get all other users who have splits with payments involving the current user
|
// Get all other users who have splits with payments involving the current user
|
||||||
const paymentIds = userSplits.map(split => split.paymentId._id);
|
const paymentIds = userSplits.map(split => (split.paymentId as any)._id);
|
||||||
const allRelatedSplits = await PaymentSplit.find({
|
const allRelatedSplits = await PaymentSplit.find({
|
||||||
paymentId: { $in: paymentIds },
|
paymentId: { $in: paymentIds },
|
||||||
username: { $ne: currentUser }
|
username: { $ne: currentUser }
|
||||||
@@ -60,7 +60,7 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
|
|
||||||
// Find other participants in this payment
|
// Find other participants in this payment
|
||||||
const otherSplits = allRelatedSplits.filter(s =>
|
const otherSplits = allRelatedSplits.filter(s =>
|
||||||
s.paymentId._id.toString() === split.paymentId._id.toString()
|
(s.paymentId as any)._id.toString() === (split.paymentId as any)._id.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const otherSplit of otherSplits) {
|
for (const otherSplit of otherSplits) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const results = await Payment.aggregate(pipeline);
|
const results = await Payment.aggregate(pipeline as any);
|
||||||
|
|
||||||
// Transform data into chart-friendly format
|
// Transform data into chart-friendly format
|
||||||
const monthsMap = new Map();
|
const monthsMap = new Map();
|
||||||
@@ -106,13 +106,13 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
|||||||
|
|
||||||
// Convert to arrays for Chart.js
|
// Convert to arrays for Chart.js
|
||||||
const allMonths = Array.from(monthsMap.keys()).sort();
|
const allMonths = Array.from(monthsMap.keys()).sort();
|
||||||
const categoryList = Array.from(categories).sort();
|
const categoryList = Array.from(categories).sort() as string[];
|
||||||
|
|
||||||
// Find the first month with any data and trim empty months from the start
|
// Find the first month with any data and trim empty months from the start
|
||||||
let firstMonthWithData = 0;
|
let firstMonthWithData = 0;
|
||||||
for (let i = 0; i < allMonths.length; i++) {
|
for (let i = 0; i < allMonths.length; i++) {
|
||||||
const monthData = monthsMap.get(allMonths[i]);
|
const monthData = monthsMap.get(allMonths[i]);
|
||||||
const hasData = Object.values(monthData).some(value => value > 0);
|
const hasData = Object.values(monthData).some((value: any) => value > 0);
|
||||||
if (hasData) {
|
if (hasData) {
|
||||||
firstMonthWithData = i;
|
firstMonthWithData = i;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
exchangeRate = conversion.exchangeRate;
|
exchangeRate = conversion.exchangeRate;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Currency conversion error:', e);
|
console.error('Currency conversion error:', e);
|
||||||
throw error(400, `Failed to convert ${inputCurrency} to CHF: ${e.message}`);
|
throw error(400, `Failed to convert ${inputCurrency} to CHF: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const splitPromises = convertedSplits.map((split) => {
|
const splitPromises = convertedSplits.map((split: any) => {
|
||||||
return PaymentSplit.create(split);
|
return PaymentSplit.create(split);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user