Compare commits
2 Commits
0263a18c5f
...
a5f2a1d6de
| Author | SHA1 | Date | |
|---|---|---|---|
|
a5f2a1d6de
|
|||
|
3b805861cf
|
77
src/lib/components/Toast.svelte
Normal file
77
src/lib/components/Toast.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script>
|
||||
import { X } from 'lucide-svelte';
|
||||
import { getToasts } from '$lib/js/toast.svelte';
|
||||
|
||||
const toasts = getToasts();
|
||||
</script>
|
||||
|
||||
{#if toasts.items.length > 0}
|
||||
<div class="toast-container">
|
||||
{#each toasts.items as t (t.id)}
|
||||
<div class="toast toast-{t.type}" role="alert">
|
||||
<span class="toast-msg">{t.message}</span>
|
||||
<button class="toast-close" onclick={() => toasts.remove(t.id)} aria-label="Dismiss">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - 2rem);
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
pointer-events: auto;
|
||||
animation: slide-up 0.2s ease-out;
|
||||
}
|
||||
.toast-error {
|
||||
background: var(--nord11);
|
||||
color: var(--nord6, #eceff4);
|
||||
}
|
||||
.toast-success {
|
||||
background: var(--nord14);
|
||||
color: var(--nord0, #2e3440);
|
||||
}
|
||||
.toast-info {
|
||||
background: var(--nord10);
|
||||
color: var(--nord6, #eceff4);
|
||||
}
|
||||
.toast-msg {
|
||||
flex: 1;
|
||||
}
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
}
|
||||
.toast-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@keyframes slide-up {
|
||||
from { opacity: 0; transform: translateY(0.5rem); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
|
||||
import Cross from '$lib/assets/icons/Cross.svelte'
|
||||
import { toast } from '$lib/js/toast.svelte'
|
||||
import "$lib/css/shake.css"
|
||||
import "$lib/css/icon.css"
|
||||
import { onMount } from 'svelte'
|
||||
@@ -32,14 +33,14 @@ function handleFileSelect(event: Event) {
|
||||
|
||||
// Validate MIME type
|
||||
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||
alert('Invalid file type. Please upload a JPEG, PNG, or WebP image.');
|
||||
toast.error('Invalid file type. Please upload a JPEG, PNG, or WebP image.');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
alert(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`);
|
||||
toast.error(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`);
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function parseTimeToISO8601(timeString: string): string | undefined {
|
||||
function parseTimeToISO8601(timeString: string | undefined): string | undefined {
|
||||
if (!timeString) return undefined;
|
||||
|
||||
// Handle common German time formats
|
||||
@@ -66,7 +66,7 @@ export function generateRecipeJsonLd(data: RecipeModelType) {
|
||||
"name": "Alexander Bocken"
|
||||
},
|
||||
"datePublished": data.dateCreated ? new Date(data.dateCreated).toISOString() : undefined,
|
||||
"dateModified": data.dateModified || data.updatedAt ? new Date(data.dateModified || data.updatedAt).toISOString() : undefined,
|
||||
"dateModified": data.dateModified ? new Date(data.dateModified).toISOString() : undefined,
|
||||
"recipeCategory": data.category,
|
||||
"keywords": data.tags?.join(', '),
|
||||
"image": {
|
||||
@@ -98,7 +98,7 @@ export function generateRecipeJsonLd(data: RecipeModelType) {
|
||||
// Extract ingredients
|
||||
if (data.ingredients) {
|
||||
for (const ingredientGroup of data.ingredients) {
|
||||
if (ingredientGroup.list) {
|
||||
if ('list' in ingredientGroup && ingredientGroup.list) {
|
||||
for (const ingredient of ingredientGroup.list) {
|
||||
if (ingredient.name) {
|
||||
let ingredientText = ingredient.name;
|
||||
@@ -115,7 +115,7 @@ export function generateRecipeJsonLd(data: RecipeModelType) {
|
||||
// Extract instructions
|
||||
if (data.instructions) {
|
||||
for (const instructionGroup of data.instructions) {
|
||||
if (instructionGroup.steps) {
|
||||
if ('steps' in instructionGroup && instructionGroup.steps) {
|
||||
for (let i = 0; i < instructionGroup.steps.length; i++) {
|
||||
jsonLd.recipeInstructions.push({
|
||||
"@type": "HowToStep",
|
||||
|
||||
31
src/lib/js/toast.svelte.ts
Normal file
31
src/lib/js/toast.svelte.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
type: 'error' | 'success' | 'info';
|
||||
}
|
||||
|
||||
let nextId = 0;
|
||||
let toasts = $state<Toast[]>([]);
|
||||
|
||||
export function getToasts() {
|
||||
return {
|
||||
get items() { return toasts; },
|
||||
remove(id: number) {
|
||||
toasts = toasts.filter(t => t.id !== id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function add(message: string, type: Toast['type'], duration = 5000) {
|
||||
const id = nextId++;
|
||||
toasts = [...toasts, { id, message, type }];
|
||||
setTimeout(() => {
|
||||
toasts = toasts.filter(t => t.id !== id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
export const toast = {
|
||||
error: (msg: string) => add(msg, 'error', 6000),
|
||||
success: (msg: string) => add(msg, 'success', 3000),
|
||||
info: (msg: string) => add(msg, 'info', 4000),
|
||||
};
|
||||
@@ -312,7 +312,7 @@ export async function invalidateRecipeCaches(): Promise<void> {
|
||||
*/
|
||||
export async function invalidateCospendCaches(usernames: string[], paymentId?: string): Promise<void> {
|
||||
try {
|
||||
const invalidations: Promise<void>[] = [];
|
||||
const invalidations: Promise<unknown>[] = [];
|
||||
|
||||
// Invalidate balance and debts caches for all affected users
|
||||
for (const username of usernames) {
|
||||
|
||||
@@ -32,17 +32,18 @@ export function briefQueryConfig(recipeLang: string) {
|
||||
*/
|
||||
export function toBrief(recipe: RecipeModelType, recipeLang: string): BriefRecipeType {
|
||||
if (isEnglish(recipeLang)) {
|
||||
const en = recipe.translations?.en;
|
||||
return {
|
||||
_id: recipe._id,
|
||||
name: recipe.translations.en.name,
|
||||
short_name: recipe.translations.en.short_name,
|
||||
name: en?.name ?? '',
|
||||
short_name: en?.short_name ?? '',
|
||||
images: recipe.images?.[0]
|
||||
? [{ alt: recipe.images[0].alt, mediapath: recipe.images[0].mediapath, color: recipe.images[0].color }]
|
||||
: [],
|
||||
tags: recipe.translations.en.tags || [],
|
||||
category: recipe.translations.en.category,
|
||||
tags: en?.tags || [],
|
||||
category: en?.category ?? '',
|
||||
icon: recipe.icon,
|
||||
description: recipe.translations.en.description,
|
||||
description: en?.description,
|
||||
season: recipe.season || [],
|
||||
dateCreated: recipe.dateCreated,
|
||||
dateModified: recipe.dateModified,
|
||||
|
||||
@@ -213,4 +213,5 @@ const WorkoutSessionSchema = new mongoose.Schema(
|
||||
WorkoutSessionSchema.index({ createdBy: 1, startTime: -1 });
|
||||
WorkoutSessionSchema.index({ templateId: 1 });
|
||||
|
||||
export const WorkoutSession = mongoose.models.WorkoutSession as mongoose.Model<IWorkoutSession> ?? mongoose.model<IWorkoutSession>("WorkoutSession", WorkoutSessionSchema);
|
||||
// @ts-expect-error Mongoose model() produces a union type too complex for TS
|
||||
export const WorkoutSession: mongoose.Model<IWorkoutSession> = mongoose.models.WorkoutSession || mongoose.model("WorkoutSession", WorkoutSessionSchema);
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
let { children } = $props();
|
||||
|
||||
onNavigate((navigation) => {
|
||||
@@ -20,4 +21,5 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
{@render children()}
|
||||
<Toast />
|
||||
@@ -1,7 +1,8 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export async function load({ params, data }) {
|
||||
export const load: LayoutLoad = async ({ params, data }) => {
|
||||
// Validate recipeLang parameter
|
||||
if (params.recipeLang !== 'rezepte' && params.recipeLang !== 'recipes') {
|
||||
throw error(404, 'Not found');
|
||||
@@ -31,4 +32,4 @@ export async function load({ params, data }) {
|
||||
recipeLang: params.recipeLang,
|
||||
isOffline: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,9 @@ import { browser } from '$app/environment';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getAllBriefRecipes, getBriefRecipesBySeason, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export async function load({ data }) {
|
||||
export const load: PageLoad = async ({ data }) => {
|
||||
// On the server, just pass through the server data unchanged
|
||||
if (!browser) {
|
||||
return {
|
||||
@@ -48,4 +49,4 @@ export async function load({ data }) {
|
||||
...data,
|
||||
isOffline: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { stripHtmlTags } from '$lib/js/stripHtmlTags';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, params, locals }) => {
|
||||
@@ -31,7 +31,7 @@ export const load: PageServerLoad = async ({ fetch, params, locals }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
export const actions: Actions = {
|
||||
toggleFavorite: async ({ request, locals, url, fetch }) => {
|
||||
const session = await locals.auth();
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getFullRecipe, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { stripHtmlTags } from '$lib/js/stripHtmlTags';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export async function load({ fetch, params, url, data }) {
|
||||
export const load: PageLoad = async ({ fetch, params, url, data }) => {
|
||||
const isEnglish = params.recipeLang === 'recipes';
|
||||
|
||||
// Check if we need to load from IndexedDB (offline mode)
|
||||
@@ -200,4 +201,4 @@ export async function load({ fetch, params, url, data }) {
|
||||
strippedDescription,
|
||||
isOffline: isOfflineMode,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import CardAdd from '$lib/components/recipes/CardAdd.svelte';
|
||||
import CreateIngredientList from '$lib/components/recipes/CreateIngredientList.svelte';
|
||||
import CreateStepList from '$lib/components/recipes/CreateStepList.svelte';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import Toggle from '$lib/components/Toggle.svelte';
|
||||
import '$lib/css/action_button.css';
|
||||
|
||||
@@ -109,11 +110,11 @@
|
||||
function prepareSubmit() {
|
||||
// Client-side validation
|
||||
if (!short_name.trim()) {
|
||||
alert('Bitte geben Sie einen Kurznamen ein');
|
||||
toast.error('Bitte geben Sie einen Kurznamen ein');
|
||||
return;
|
||||
}
|
||||
if (!card_data.name) {
|
||||
alert('Bitte geben Sie einen Namen ein');
|
||||
toast.error('Bitte geben Sie einen Namen ein');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,7 +160,7 @@
|
||||
// Display form errors if any
|
||||
$effect(() => {
|
||||
if (form?.error) {
|
||||
alert(`Fehler: ${form.error}`);
|
||||
toast.error(form.error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,8 +2,9 @@ import { browser } from '$app/environment';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getBriefRecipesByCategory, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export async function load({ data, params }) {
|
||||
export const load: PageLoad = async ({ data, params }) => {
|
||||
// On the server, just pass through the server data unchanged
|
||||
if (!browser) {
|
||||
return {
|
||||
@@ -37,4 +38,4 @@ export async function load({ data, params }) {
|
||||
...data,
|
||||
isOffline: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { season } from '$lib/js/season_store';
|
||||
import { portions } from '$lib/js/portions_store';
|
||||
import '$lib/css/action_button.css';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
|
||||
@@ -173,11 +174,11 @@
|
||||
function prepareSubmit() {
|
||||
// Client-side validation
|
||||
if (!short_name.trim()) {
|
||||
alert('Bitte geben Sie einen Kurznamen ein');
|
||||
toast.error('Bitte geben Sie einen Kurznamen ein');
|
||||
return;
|
||||
}
|
||||
if (!card_data.name) {
|
||||
alert('Bitte geben Sie einen Namen ein');
|
||||
toast.error('Bitte geben Sie einen Namen ein');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,7 +240,7 @@
|
||||
// Display form errors if any
|
||||
$effect(() => {
|
||||
if (form?.error) {
|
||||
alert(`Fehler: ${form.error}`);
|
||||
toast.error(form.error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,8 +2,9 @@ import { browser } from '$app/environment';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getBriefRecipesByIcon, getAllBriefRecipes, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export async function load({ data, params }) {
|
||||
export const load: PageLoad = async ({ data, params }) => {
|
||||
// On the server, just pass through the server data unchanged
|
||||
if (!browser) {
|
||||
return {
|
||||
@@ -49,4 +50,4 @@ export async function load({ data, params }) {
|
||||
...data,
|
||||
isOffline: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,9 @@ import { browser } from '$app/environment';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getBriefRecipesBySeason, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export async function load({ data }) {
|
||||
export const load: PageLoad = async ({ data }) => {
|
||||
// On the server, just pass through the server data unchanged
|
||||
if (!browser) {
|
||||
return {
|
||||
@@ -38,4 +39,4 @@ export async function load({ data }) {
|
||||
...data,
|
||||
isOffline: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,9 @@ import { browser } from '$app/environment';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getBriefRecipesBySeason, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export async function load({ data, params }) {
|
||||
export const load: PageLoad = async ({ data, params }) => {
|
||||
// On the server, just pass through the server data unchanged
|
||||
if (!browser) {
|
||||
return {
|
||||
@@ -38,4 +39,4 @@ export async function load({ data, params }) {
|
||||
...data,
|
||||
isOffline: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,9 @@ import { browser } from '$app/environment';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getBriefRecipesByTag, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export async function load({ data, params }) {
|
||||
export const load: PageLoad = async ({ data, params }) => {
|
||||
// On the server, just pass through the server data unchanged
|
||||
if (!browser) {
|
||||
return {
|
||||
@@ -37,4 +38,4 @@ export async function load({ data, params }) {
|
||||
...data,
|
||||
isOffline: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,10 +5,8 @@ import { error } from '@sveltejs/kit';
|
||||
import type { RecipeModelType, IngredientItem, InstructionItem } from '$types/types';
|
||||
import { isEnglish } from '$lib/server/recipeHelpers';
|
||||
|
||||
type RecipeItem = (IngredientItem | InstructionItem) & { baseRecipeRef?: Record<string, unknown>; resolvedRecipe?: Record<string, unknown> };
|
||||
|
||||
/** Recursively map populated baseRecipeRef to resolvedRecipe field */
|
||||
function mapBaseRecipeRefs(items: RecipeItem[]): RecipeItem[] {
|
||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||
return items.map((item) => {
|
||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||
const resolvedRecipe = { ...item.baseRecipeRef };
|
||||
@@ -131,10 +129,10 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
};
|
||||
|
||||
if (recipe.ingredients) {
|
||||
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients as RecipeItem[]);
|
||||
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients as any[]);
|
||||
}
|
||||
if (recipe.instructions) {
|
||||
recipe.instructions = mapBaseRecipeRefs(recipe.instructions as RecipeItem[]);
|
||||
recipe.instructions = mapBaseRecipeRefs(recipe.instructions as any[]);
|
||||
}
|
||||
|
||||
// Merge English alt/caption with original image paths
|
||||
|
||||
@@ -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
|
||||
const monthsMap = new Map();
|
||||
|
||||
@@ -87,7 +87,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
// Validate personal + equal split method
|
||||
if (splitMethod === 'personal_equal' && splits) {
|
||||
const totalPersonal = splits.reduce((sum: number, split: SplitInput) => {
|
||||
return sum + (parseFloat(split.personalAmount) || 0);
|
||||
return sum + (split.personalAmount ?? 0);
|
||||
}, 0);
|
||||
|
||||
if (totalPersonal > amount) {
|
||||
|
||||
@@ -87,7 +87,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
// Validate personal + equal split method
|
||||
if (splitMethod === 'personal_equal' && splits) {
|
||||
const totalPersonal = splits.reduce((sum: number, split: { personalAmount?: number }) => {
|
||||
return sum + (parseFloat(split.personalAmount) || 0);
|
||||
return sum + (split.personalAmount ?? 0);
|
||||
}, 0);
|
||||
|
||||
if (totalPersonal > amount) {
|
||||
|
||||
@@ -114,7 +114,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
// Validate personal + equal split method
|
||||
if (splitMethod === 'personal_equal' && splits && amount) {
|
||||
const totalPersonal = splits.reduce((sum: number, split: { personalAmount?: number }) => {
|
||||
return sum + (parseFloat(split.personalAmount) || 0);
|
||||
return sum + (split.personalAmount ?? 0);
|
||||
}, 0);
|
||||
|
||||
if (totalPersonal > amount) {
|
||||
@@ -127,7 +127,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
const updatedPayment = { ...existingPayment.toObject(), ...updateData };
|
||||
updateData.nextExecutionDate = calculateNextExecutionDate(
|
||||
updatedPayment,
|
||||
updateData.startDate || existingPayment.startDate
|
||||
(updateData.startDate || existingPayment.startDate) as Date
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
for (const recipe of recipes) {
|
||||
let processed = 0;
|
||||
let failed = 0;
|
||||
if (!recipe.images) continue;
|
||||
|
||||
for (let i = 0; i < recipe.images.length; i++) {
|
||||
const image = recipe.images[i];
|
||||
|
||||
@@ -48,6 +48,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
}> = [];
|
||||
|
||||
for (const recipe of recipes) {
|
||||
if (!recipe.images?.length) continue;
|
||||
const image = recipe.images[0];
|
||||
if (!image?.mediapath) continue;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
|
||||
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import { isSettlementPayment, getSettlementIcon, getSettlementReceiver } from '$lib/utils/settlements';
|
||||
import AddButton from '$lib/components/AddButton.svelte';
|
||||
|
||||
@@ -88,7 +89,7 @@
|
||||
|
||||
payments = payments.filter((/** @type {any} */ p) => p._id !== paymentId);
|
||||
} catch (err) {
|
||||
alert('Error: ' + (err instanceof Error ? err.message : String(err)));
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
|
||||
import { getFrequencyDescription, formatNextExecution } from '$lib/utils/recurring';
|
||||
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import AddButton from '$lib/components/AddButton.svelte';
|
||||
import { formatCurrency } from '$lib/utils/formatters';
|
||||
|
||||
@@ -53,7 +54,7 @@
|
||||
// Refresh the list
|
||||
await fetchRecurringPayments();
|
||||
} catch (err) {
|
||||
alert(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@
|
||||
// Refresh the list
|
||||
await fetchRecurringPayments();
|
||||
} catch (err) {
|
||||
alert(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { page as appPage } from '$app/stores';
|
||||
import SessionCard from '$lib/components/fitness/SessionCard.svelte';
|
||||
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($appPage.url.pathname));
|
||||
|
||||
@@ -31,10 +32,11 @@
|
||||
page++;
|
||||
try {
|
||||
const res = await fetch(`/api/fitness/sessions?limit=50&skip=${sessions.length}`);
|
||||
if (!res.ok) { toast.error('Failed to load sessions'); loading = false; return; }
|
||||
const data = await res.json();
|
||||
sessions = [...sessions, ...(data.sessions ?? [])];
|
||||
total = data.total ?? total;
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to load sessions'); }
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Clock, Weight, Trophy, Trash2, Pencil, Plus, Upload, Route, X, RefreshCw, Gauge, Flame, Info } from 'lucide-svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
const sl = $derived(fitnessSlugs(lang));
|
||||
@@ -166,8 +167,11 @@
|
||||
editing = false;
|
||||
editData = null;
|
||||
await invalidateAll();
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to save session');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to save session'); }
|
||||
saving = false;
|
||||
}
|
||||
|
||||
@@ -262,8 +266,11 @@
|
||||
const res = await fetch(`/api/fitness/sessions/${session._id}/recalculate`, { method: 'POST' });
|
||||
if (res.ok) {
|
||||
await invalidateAll();
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to recalculate');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to recalculate'); }
|
||||
recalculating = false;
|
||||
}
|
||||
|
||||
@@ -274,8 +281,11 @@
|
||||
const res = await fetch(`/api/fitness/sessions/${session._id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
await goto(`/fitness/${sl.history}`);
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to delete session');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to delete session'); }
|
||||
deleting = false;
|
||||
}
|
||||
|
||||
@@ -483,8 +493,11 @@
|
||||
});
|
||||
if (res.ok) {
|
||||
await invalidateAll();
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to upload GPX');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to upload GPX'); }
|
||||
uploading = -1;
|
||||
};
|
||||
input.click();
|
||||
@@ -501,8 +514,11 @@
|
||||
});
|
||||
if (res.ok) {
|
||||
await invalidateAll();
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to remove GPX');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to remove GPX'); }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Pencil, Trash2 } from 'lucide-svelte';
|
||||
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
import { getWorkout } from '$lib/js/workout.svelte';
|
||||
@@ -186,7 +187,7 @@
|
||||
async function refreshLatest() {
|
||||
try {
|
||||
const latestRes = await fetch('/api/fitness/measurements/latest');
|
||||
latest = await latestRes.json();
|
||||
if (latestRes.ok) latest = await latestRes.json();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -207,6 +208,9 @@
|
||||
await refreshLatest();
|
||||
showForm = false;
|
||||
resetForm();
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to save measurement');
|
||||
}
|
||||
} else {
|
||||
const res = await fetch('/api/fitness/measurements', {
|
||||
@@ -220,9 +224,12 @@
|
||||
await refreshLatest();
|
||||
showForm = false;
|
||||
resetForm();
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to save measurement');
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to save measurement'); }
|
||||
saving = false;
|
||||
}
|
||||
|
||||
@@ -238,8 +245,11 @@
|
||||
showForm = false;
|
||||
resetForm();
|
||||
}
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to delete measurement');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to delete measurement'); }
|
||||
}
|
||||
|
||||
/** @param {string} dateStr */
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import FitnessStreakAura from '$lib/components/fitness/FitnessStreakAura.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
|
||||
@@ -59,8 +60,11 @@
|
||||
goalWeekly = d.weeklyWorkouts;
|
||||
goalStreak = d.streak;
|
||||
goalEditing = false;
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to save goal');
|
||||
}
|
||||
} finally {
|
||||
} catch { toast.error('Failed to save goal'); } finally {
|
||||
goalSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { getWorkout } from '$lib/js/workout.svelte';
|
||||
import { getWorkoutSync } from '$lib/js/workoutSync.svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
const sl = $derived(fitnessSlugs(lang));
|
||||
@@ -198,6 +199,9 @@
|
||||
if (res.ok) {
|
||||
const { template } = await res.json();
|
||||
templates = templates.map((t) => t._id === template._id ? { ...template, lastUsed: t.lastUsed } : t);
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to save template');
|
||||
}
|
||||
} else {
|
||||
const res = await fetch('/api/fitness/templates', {
|
||||
@@ -208,10 +212,13 @@
|
||||
if (res.ok) {
|
||||
const { template } = await res.json();
|
||||
templates = [...templates, template];
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to save template');
|
||||
}
|
||||
}
|
||||
closeEditor();
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to save template'); }
|
||||
editorSaving = false;
|
||||
}
|
||||
|
||||
@@ -227,8 +234,11 @@
|
||||
scheduleOrder = scheduleOrder.filter((id) => id !== template._id);
|
||||
saveSchedule(scheduleOrder);
|
||||
}
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to delete template');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to delete template'); }
|
||||
}
|
||||
|
||||
// Schedule editor functions
|
||||
@@ -273,8 +283,11 @@
|
||||
const schedRes = await fetch('/api/fitness/schedule');
|
||||
const schedData = await schedRes.json();
|
||||
nextTemplateId = schedData.nextTemplateId ?? null;
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
toast.error(err?.error ?? 'Failed to save schedule');
|
||||
}
|
||||
} catch {}
|
||||
} catch { toast.error('Failed to save schedule'); }
|
||||
}
|
||||
|
||||
async function saveAndCloseSchedule() {
|
||||
|
||||
Reference in New Issue
Block a user