Compare commits

4 Commits

Author SHA1 Message Date
ab2a6c9158 feat: add page titles to recipe and glaube routes
All checks were successful
CI / update (push) Successful in 1m20s
- Add titles to category, tag, icon, season routes
- Add bilingual support (German/English) for recipe route titles
- Use consistent "Bocken Recipes" / "Bocken Rezepte" branding
- Change English tagline from "Bocken's Recipes" to "Bocken Recipes"
- Add titles to /glaube and /glaube/gebete pages
- Make tips-and-tricks page language-aware
2026-01-20 19:54:33 +01:00
e366b44bba fix: include server load data in universal load for recipe page title
The +page.server.ts fetches recipe data and strips HTML tags server-side
to avoid bundling cheerio in the client. However, the universal load in
+page.ts wasn't including this data in its return value.

Fixed by:
1. Having +page.server.ts fetch the recipe directly (since it runs before
   +page.ts and can't access its data via parent())
2. Adding the `data` parameter to +page.ts and spreading it in the return

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 19:44:52 +01:00
1f5b342d8e cleanup 2026-01-20 19:37:49 +01:00
2b3aae8087 fix: append image to FormData before submission in use:enhance
The image upload broke because formData.append() was being called in the
async callback of use:enhance, which runs AFTER the form submission.
Moved the append call to the outer function which runs BEFORE submission.

Also cleaned up debug console.log statements from CardAdd.svelte.
2026-01-20 19:37:16 +01:00
22 changed files with 112 additions and 605 deletions

View File

@@ -1,300 +0,0 @@
# Formatter Replacement Summary
**Date:** 2025-11-18
**Status:** ✅ Complete
## Overview
Successfully replaced all inline formatting functions (65+ occurrences across 12 files) with shared formatter utilities from `$lib/utils/formatters.ts`.
---
## Files Modified
### Components (3 files)
1. **DebtBreakdown.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 4 calls to use `formatCurrency(amount, 'CHF', 'de-CH')`
2. **EnhancedBalance.svelte**
- ✅ Replaced inline `formatCurrency` with utility (kept wrapper for Math.abs)
- ✅ Added import: `import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters'`
- ✅ Wrapper function: `formatCurrency(amount) => formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH')`
3. **PaymentModal.svelte**
- ✅ Replaced inline `formatCurrency` with utility (kept wrapper for Math.abs)
- ✅ Added import: `import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters'`
- ✅ Wrapper function: `formatCurrency(amount) => formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH')`
### Cospend Pages (5 files)
4. **routes/cospend/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 5 calls to include CHF and de-CH parameters
5. **routes/cospend/payments/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 6 calls to include CHF and de-CH parameters
6. **routes/cospend/payments/view/[id]/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 7 calls to include CHF and de-CH parameters
7. **routes/cospend/recurring/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 5 calls to include CHF and de-CH parameters
8. **routes/cospend/settle/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 4 calls to include CHF and de-CH parameters
### Configuration (1 file)
9. **svelte.config.js**
- ✅ Added `$utils` alias for `src/utils` directory
- ✅ Enables clean imports: `import { formatCurrency } from '$lib/utils/formatters'`
---
## Changes Summary
### Before Refactoring
**Problem:** Duplicate `formatCurrency` functions in 8 files:
```typescript
// Repeated 8 times across codebase
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
}
// Usage
{formatCurrency(payment.amount)}
```
### After Refactoring
**Solution:** Single shared utility with consistent usage:
```typescript
// Once in $lib/utils/formatters.ts
export function formatCurrency(
amount: number,
currency: string = 'EUR',
locale: string = 'de-DE'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(amount);
}
// Usage in components/pages
import { formatCurrency } from '$lib/utils/formatters';
{formatCurrency(payment.amount, 'CHF', 'de-CH')}
```
---
## Impact
### Code Duplication Eliminated
- **Before:** 8 duplicate `formatCurrency` functions
- **After:** 1 shared utility function
- **Reduction:** ~88% less formatting code
### Function Calls Updated
- **Total calls updated:** 31 formatCurrency calls
- **Parameters added:** CHF and de-CH to all calls
- **Consistency:** 100% of currency formatting now uses shared utility
### Lines of Code Removed
Approximately **40-50 lines** of duplicate code removed across 8 files.
---
## Benefits
### 1. Maintainability ✅
- ✅ Single source of truth for currency formatting
- ✅ Future changes only need to update one file
- ✅ Consistent formatting across entire application
### 2. Consistency ✅
- ✅ All currency displayed with same format
- ✅ Locale-aware formatting (de-CH)
- ✅ Proper currency symbol placement
### 3. Testability ✅
- ✅ Formatting logic has comprehensive unit tests (29 tests)
- ✅ Easy to test edge cases centrally
- ✅ Regression testing in one location
### 4. Type Safety ✅
- ✅ TypeScript types for all formatter functions
- ✅ JSDoc comments with examples
- ✅ IDE auto-completion support
### 5. Extensibility ✅
- ✅ Easy to add new formatters (date, number, etc.)
- ✅ Support for multiple locales
- ✅ Support for multiple currencies
---
## Remaining Inline Formatting (Optional Future Work)
### Files Still Using Inline `.toFixed()`
These files use `.toFixed()` for specific formatting needs. Could be replaced with `formatNumber()` if desired:
1. **SplitMethodSelector.svelte**
- Uses `.toFixed(2)` for split calculations
- Could use: `formatNumber(amount, 2, 'de-CH')`
2. **BarChart.svelte**
- Uses `.toFixed(0)` and `.toFixed(2)` for chart labels
- Could use: `formatNumber(amount, decimals, 'de-CH')`
3. **payments/add/+page.svelte** & **payments/edit/[id]/+page.svelte**
- Uses `.toFixed(2)` and `.toFixed(4)` for currency conversions
- Could use: `formatNumber(amount, decimals, 'de-CH')`
4. **recurring/edit/[id]/+page.svelte**
- Uses `.toFixed(2)` and `.toFixed(4)` for exchange rates
- Could use: `formatNumber(rate, 4, 'de-CH')`
5. **IngredientsPage.svelte**
- Uses `.toFixed(3)` for recipe ingredient calculations
- This is domain-specific logic, probably best left as-is
### Files Using `.toLocaleString()`
These files use `.toLocaleString()` for date formatting:
1. **payments/add/+page.svelte**
- Uses `.toLocaleString('de-CH', options)` for next execution date
- Could use: `formatDateTime(date, 'de-CH', options)`
2. **recurring/edit/[id]/+page.svelte**
- Uses `.toLocaleString('de-CH', options)` for next execution date
- Could use: `formatDateTime(date, 'de-CH', options)`
**Recommendation:** These are lower priority since they're used less frequently and the pattern is consistent.
---
## Testing Results
### Unit Tests ✅
```bash
Test Files: 2 passed (2)
Tests: 38 passed, 1 skipped (39)
Duration: ~500ms
```
**Test Coverage:**
- ✅ formatCurrency function (5 tests)
- ✅ formatDate function (5 tests)
- ✅ formatDateTime function (2 tests)
- ✅ formatNumber function (4 tests)
- ✅ formatRelativeTime function (2 tests)
- ✅ formatFileSize function (6 tests)
- ✅ formatPercentage function (5 tests)
- ✅ Auth middleware (9 tests)
### Build Status ✅
```bash
149 modules transformed
✔ Build completed successfully
```
**No breaking changes:** All existing functionality preserved.
---
## Migration Notes
### For Future Developers
**When adding new currency displays:**
```typescript
// ✅ DO: Use shared formatter
import { formatCurrency } from '$lib/utils/formatters';
{formatCurrency(amount, 'CHF', 'de-CH')}
// ❌ DON'T: Create new inline formatters
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
}
```
**When adding new number/date formatting:**
```typescript
// Numbers
import { formatNumber } from '$lib/utils/formatters';
{formatNumber(value, 2, 'de-CH')} // 2 decimal places
// Dates
import { formatDate, formatDateTime } from '$lib/utils/formatters';
{formatDate(date, 'de-CH')}
{formatDateTime(date, 'de-CH', { dateStyle: 'long', timeStyle: 'short' })}
```
---
## Files Created/Modified
### Created
- `scripts/replace_formatters.py` - Automated replacement script
- `scripts/update_formatter_calls.py` - Update formatter call parameters
- `scripts/replace-formatters.md` - Progress tracking
- `FORMATTER_REPLACEMENT_SUMMARY.md` - This document
### Modified
- 8 Svelte components/pages (formatCurrency replaced)
- 1 configuration file (svelte.config.js - added alias)
### Scripts Used
- Python automation for consistent replacements
- Bash scripts for verification
- Manual cleanup for edge cases
---
## Conclusion
**Successfully eliminated all duplicate formatCurrency functions**
**31 function calls updated to use shared utility**
**All tests passing (38/38)**
**Build successful with no breaking changes**
**~40-50 lines of duplicate code removed**
**Single source of truth for currency formatting**
**Result:** Cleaner, more maintainable codebase with consistent formatting across the entire application. Future changes to currency formatting only require updating one file instead of 8.
**Next Steps (Optional):**
1. Replace remaining `.toFixed()` calls with `formatNumber()` (8 files)
2. Replace `.toLocaleString()` calls with `formatDateTime()` (2 files)
3. Add more formatter utilities as needed (file size, percentages, etc.)

View File

@@ -1,138 +0,0 @@
# TypeScript Error Cleanup TODO
Generated from `pnpm check` output. Total errors found: 1239
## Categories
### 1. Implicit 'any' Types (High Priority - Easy Fixes)
Files with missing type annotations for function parameters
### 2. Mongoose Type Issues
Property access and type compatibility issues with Mongoose models
### 3. Null/Undefined Safety
Properties that may be null or undefined
### 4. Type Mismatches
Arguments and assignments with incompatible types
### 5. Missing Imports/Definitions
Cannot find name/namespace errors
---
## Detailed Errors
### Category 1: Implicit 'any' Types
#### src/lib/components/do_on_key.js
- [x] Line 1:27 - Parameter 'event' implicitly has an 'any' type ✅
- [x] Line 1:34 - Parameter 'key' implicitly has an 'any' type ✅
- [x] Line 1:39 - Parameter 'needsctrl' implicitly has an 'any' type ✅
- [x] Line 1:50 - Parameter 'fn' implicitly has an 'any' type ✅
#### src/lib/js/randomize.js
- [x] Line 2:21 - Parameter 'a' implicitly has an 'any' type ✅
- [x] Line 11:28 - Parameter 'array' implicitly has an 'any' type ✅
#### src/utils/cookie.js
- [x] Line 2:35 - Parameter 'request' implicitly has an 'any' type ✅
- [x] Line 4:45 - Parameter 'cookie' implicitly has an 'any' type ✅
#### src/hooks.server.ts
- [ ] Line 26:32 - Binding element 'event' implicitly has an 'any' type
- [ ] Line 26:39 - Binding element 'resolve' implicitly has an 'any' type
#### src/lib/js/stripHtmlTags.ts
- [x] Line 4:31 - Parameter 'input' implicitly has an 'any' type ✅
#### src/lib/utils/settlements.ts
- [ ] Line 51:40 - Parameter 'split' implicitly has an 'any' type
- [ ] Line 57:41 - Parameter 'split' implicitly has an 'any' type
#### src/routes/[recipeLang=recipeLang]/favorites/+page.server.ts
- [ ] Line 25:49 - Parameter 'recipe' implicitly has an 'any' type
#### src/routes/api/cospend/payments/+server.ts
- [ ] Line 135:48 - Parameter 'split' implicitly has an 'any' type
### Category 2: Mongoose/Model Type Issues
#### src/models/RecurringPayment.ts
- [ ] Line 98:20 - Property 'frequency' does not exist on type
#### src/routes/api/cospend/debts/+server.ts
- [ ] Line 36:64 - Property '_id' does not exist on FlattenMaps type
- [ ] Line 54:21 - Property '_id' does not exist on FlattenMaps type
- [ ] Line 54:56 - Property '_id' does not exist on FlattenMaps type
#### src/routes/api/generate-alt-text/+server.ts
- [ ] Line 60:34 - Type 'never[]' is missing properties (DocumentArray)
- [ ] Lines 62-75 - Multiple 'possibly null/undefined' errors for recipe.translations
#### src/routes/api/generate-alt-text-bulk/+server.ts
- [ ] Lines 93-108 - Similar DocumentArray and null/undefined issues
### Category 3: Null/Undefined Safety
#### src/lib/server/middleware/auth.ts
- [ ] Lines 42-44 - Type 'string | null | undefined' not assignable to 'string | undefined' (3 instances)
- [ ] Lines 77-79 - Same issue (3 more instances)
### Category 4: Error Handling (unknown type)
#### src/routes/api/cospend/payments/+server.ts
- [ ] Line 91:70 - 'e' is of type 'unknown'
#### src/routes/api/cospend/payments/[id]/+server.ts
- [ ] Line 26:9 - 'e' is of type 'unknown'
- [ ] Line 88:9 - 'e' is of type 'unknown'
- [ ] Line 121:9 - 'e' is of type 'unknown'
#### src/routes/api/cospend/upload/+server.ts
- [ ] Line 59:9 - 'err' is of type 'unknown'
### Category 5: Missing Definitions
#### src/lib/server/scheduler.ts
- [ ] Line 10:17 - Cannot find namespace 'cron'
- [ ] Line 30:7 - Invalid argument for TaskOptions
#### src/routes/(main)/settings/+page.server.ts
- [ ] Line 6:22 - Cannot find name 'authenticateUser'
#### src/hooks.server.ts
- [ ] Lines 39, 51, 61 - error() function doesn't accept custom object format
### Category 6: Aggregate Pipeline Issues
#### src/routes/api/cospend/monthly-expenses/+server.ts
- [ ] Line 79:45 - No matching overload for aggregate pipeline
- [ ] Line 115:62 - 'value' is of type 'unknown'
- [ ] Line 125:39 - Type incompatibility in map function
### Category 7: Svelte Component Props
#### Various .svelte files
- [ ] Multiple implicit 'any' types in event handlers and derived state
- [ ] Missing prop type definitions
- [ ] Element binding type issues
---
## Quick Wins (Start Here)
1. **do_on_key.js** - Add type annotations (4 parameters)
2. **randomize.js** - Add type annotations (2 parameters)
3. **cookie.js** - Add type annotations (2 parameters)
4. **stripHtmlTags.ts** - Add input parameter type
5. **Error handling** - Type catch blocks properly (`catch (e: unknown)`)
## Progress Tracking
- Total Errors: 1239
- Fixed: 12
- Remaining: 1227
- Categories Completed: Quick Wins (Category 1 - Partial)
Last Updated: Mon Jan 5 11:48:00 PM CET 2026

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="-10 0 2058 2048"
id="svg1"
sodipodi:docname="dove.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.20494477"
inkscape:cx="875.84571"
inkscape:cy="3122.7925"
inkscape:window-width="1436"
inkscape:window-height="1749"
inkscape:window-x="1440"
inkscape:window-y="47"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill="currentColor"
d="M1935 90q0 32 -38 91q-21 29 -56 90q-20 55 -63 164q-35 86 -95 143q-22 -21 -43 -45q51 -49 85 -139q49 -130 61 -152q-126 48 -152 63q-76 46 -95 128q-27 -18 -58 -25q28 -104 97 -149q31 -20 138 -52q90 -28 137 -74l29 -39q22 -30 32 -30q21 0 21 26zM1714 653 q-90 30 -113 43q-65 36 -65 90q0 19 20 119q23 116 23 247q0 169 -103 299q-111 141 -275 141q-254 0 -283 87q-16 104 -31 207q-27 162 -76 162q-21 0 -41 -20q-16 -19 -32 -37q-10 3 -33 22q-18 15 -39 15q-28 0 -50 -44.5t-30 -44.5q-10 0 -35.5 11.5t-41.5 11.5 q-47 0 -58.5 -45.5t-21.5 -45.5t-29.5 2.5t-29.5 2.5q-46 0 -46 -30q0 -16 14 -44.5t14 -44.5q0 -8 -46.5 -25.5t-46.5 -48.5q0 -34 35.5 -52t99.5 -31q91 -19 103 -22q113 -32 171 -93q37 -39 105 -165q34 -64 43 -82q26 -53 31 -85q-129 -67 -224 -76q-33 0 -96 -11 q-36 -13 -36 -41q0 -7 2 -19.5t2 -19.5q0 -20 -67.5 -42t-67.5 -64q0 -11 8.5 -30t8.5 -30q0 -15 -79 -39t-79 -63q0 -16 9 -45t9 -45q0 -20 -29 -43q-23 -17 -46 -33q-49 -44 -49 -215q0 -8 1 -15q91 53 194 68l282 16q202 12 304 59q143 65 143 210q0 15 -2 44t-2 44 q0 122 78 122q73 0 108 -133q16 -70 32 -139q21 -81 57 -119q46 -51 130 -51q71 0 122 61q90 107 154 149zM1597 636q-25 -22 -77 -91q-30 -40 -75 -40q-91 0 -131 115q-30 106 -59 213q-44 115 -144 115q-146 0 -146 -180q0 -16 2.5 -46.5t2.5 -46.5q0 -62 -19 -87 q-70 -92 -303 -115q-173 -9 -347 -18q-55 -6 -116 -30v34q0 27 57.5 73.5t57.5 91.5q0 16 -10.5 45t-10.5 44q1 1 7 1q3 0 7 1q146 36 146 105q0 13 -8.5 32.5t-8.5 27.5h10q5 0 9 1q61 15 86 36q32 28 28 85q173 15 372 107q-7 77 -80 215q-67 128 -127 195 q-67 74 -169 104q-96 24 -193 47q-10 3 -29 13q86 18 86 70q0 19 -19 62q15 -5 33 -5q42 0 59 26q8 11 22 61l-1 3q10 0 34.5 -11.5t42.5 -11.5q55 0 88 84q38 -32 64 -32q37 0 66 41q25 -53 33 -151q10 -112 23 -154q43 -136 337 -136q116 0 215 -108q105 -114 105 -277 q0 -23 -12 -112l-28 -207q-4 -30 -4 -42q0 -97 124 -147zM1506 605q0 38 -38 38q-39 0 -39 -38t39 -38q38 0 38 38z"
id="path1" />
<path
d="m 1724.44,1054.6641 c -31.1769,-18 -37.7653,-42.5884 -19.7653,-73.76528 5.3333,-9.2376 12.354,-16.7312 21.0621,-22.4808 6.2201,-4.1068 44.7886,-7.2427 115.7055,-9.4077 70.9168,-2.1649 110.128,-1.0807 117.6336,3.2526 30.0222,17.3334 35.5333,42.45448 16.5333,75.36348 -7.3333,12.7017 -16.1754,20.6833 -26.5263,23.9448 -24.5645,1.2137 -56.7805,3.0135 -96.648,5.3994 -72.6282,5.7957 -115.2931,5.0269 -127.9949,-2.3065 z"
style="fill:currentColor"
id="path1-2"
sodipodi:nodetypes="ssssssccs" />
<path
d="m 386.57764,1262.0569 c 53.44793,-14.3214 85.17574,-2.8075 95.18337,34.5417 9.83517,36.7052 -12.29319,62.3047 -66.38503,76.7986 l -82.1037,21.9996 c -54.09184,14.4939 -86.05533,3.3882 -95.89047,-33.317 -10.00766,-37.3491 12.67841,-63.4432 68.05807,-78.2821 z"
style="fill:currentColor"
id="path1-7" />
<path
d="m 1115.7599,372.22724 c 14.3213,53.44793 2.8073,85.17581 -34.5418,95.18323 -36.705,9.83527 -62.3047,-12.29323 -76.7986,-66.38485 l -21.99962,-82.10394 c -14.4939,-54.09162 -3.3882,-86.05531 33.31712,-95.89019 37.349,-10.00765 63.4431,12.67818 78.2821,68.05802 z"
style="fill:currentColor"
id="path1-7-6" />
<path
d="m 1184.6228,1956.284 c -4.807,-8.0003 -6.8298,-42.7561 -6.0684,-104.2674 0.7614,-61.5113 2.7093,-100.0139 5.8437,-115.508 3.1343,-15.4941 11.8445,-27.5329 26.1306,-36.117 30.2866,-18.198 54.7006,-11.868 73.242,18.99 5.4937,9.1432 8.145,43.3269 7.9537,102.5512 -0.081,52.9359 -1.4296,89.5231 -4.0464,109.7617 -2.276,16.9226 -11.1284,30.0192 -26.5575,39.29 -33.1439,19.9148 -58.643,15.0146 -76.4977,-14.7005 z"
style="fill:currentColor"
id="path1-6" />
<path
d="m 1773.3127,1737.6952 c -9.0153,-2.4157 -34.6139,-26.0118 -76.7955,-70.7882 -42.1816,-44.7764 -67.5266,-73.826 -76.035,-87.1489 -8.5084,-13.3228 -10.6057,-28.0334 -6.2922,-44.1323 9.145,-34.1293 31.1041,-46.5353 65.8774,-37.2179 10.3033,2.7609 35.9565,25.5088 76.9595,68.2441 36.7142,38.1352 61.1596,65.3907 73.3362,81.7668 10.1182,13.7541 12.8479,29.3245 8.1892,46.7113 -10.0077,37.3492 -31.7542,51.5375 -65.2396,42.5651 z"
style="fill:currentColor"
id="path1-6-9" />
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""Extract crown emoji from Symbola font as SVG."""
import fontforge
import sys
# Path to Symbola font
font_path = "/usr/share/fonts/TTF/Symbola.ttf"
# Dove emoji Unicode codepoint
dove_codepoint = 0x1F54A # U+1F54A 🕊️
# Output SVG file
output_path = "dove.svg"
try:
# Open the font
font = fontforge.open(font_path)
# Select the dove glyph by Unicode codepoint
if dove_codepoint in font:
glyph = font[dove_codepoint]
# Export as SVG
glyph.export(output_path)
print(f"✓ Successfully extracted dove emoji to {output_path}")
print(f" Glyph name: {glyph.glyphname}")
print(f" Unicode: U+{dove_codepoint:04X}")
else:
print(f"✗ Dove emoji (U+{dove_codepoint:04X}) not found in font")
sys.exit(1)
font.close()
except Exception as e:
print(f"✗ Error: {e}")
sys.exit(1)

View File

@@ -27,33 +27,22 @@ function handleFileSelect(event: Event) {
const file = input.files?.[0]; const file = input.files?.[0];
if (!file) { if (!file) {
console.log('[CardAdd] No file selected');
return; return;
} }
console.log('[CardAdd] File selected:', {
name: file.name,
size: file.size,
type: file.type
});
// Validate MIME type // Validate MIME type
if (!ALLOWED_MIME_TYPES.includes(file.type)) { if (!ALLOWED_MIME_TYPES.includes(file.type)) {
console.error('[CardAdd] Invalid MIME type:', file.type);
alert('Invalid file type. Please upload a JPEG, PNG, or WebP image.'); alert('Invalid file type. Please upload a JPEG, PNG, or WebP image.');
input.value = ''; input.value = '';
return; return;
} }
console.log('[CardAdd] MIME type valid:', file.type);
// Validate file size // Validate file size
if (file.size > MAX_FILE_SIZE) { if (file.size > MAX_FILE_SIZE) {
console.error('[CardAdd] File too large:', file.size);
alert(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`); alert(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`);
input.value = ''; input.value = '';
return; return;
} }
console.log('[CardAdd] File size valid:', file.size, 'bytes');
// Clean up old preview URL if exists // Clean up old preview URL if exists
if (image_preview_url && image_preview_url.startsWith('blob:')) { if (image_preview_url && image_preview_url.startsWith('blob:')) {
@@ -63,10 +52,6 @@ function handleFileSelect(event: Event) {
// Create preview and store file // Create preview and store file
image_preview_url = URL.createObjectURL(file); image_preview_url = URL.createObjectURL(file);
selected_image_file = file; selected_image_file = file;
console.log('[CardAdd] Image preview created, file stored for upload:', {
previewUrl: image_preview_url,
fileName: file.name
});
} }
// Check if initial image_preview_url redirects to placeholder // Check if initial image_preview_url redirects to placeholder
@@ -77,19 +62,11 @@ onMount(() => {
img.onload = () => { img.onload = () => {
// Check if this is the placeholder image (150x150) // Check if this is the placeholder image (150x150)
if (img.naturalWidth === 150 && img.naturalHeight === 150) { if (img.naturalWidth === 150 && img.naturalHeight === 150) {
console.log('[CardAdd] Detected placeholder image (150x150), clearing preview');
image_preview_url = "" image_preview_url = ""
} else {
console.log('[CardAdd] Real image loaded:', {
url: image_preview_url,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight
});
} }
}; };
img.onerror = () => { img.onerror = () => {
console.log('[CardAdd] Image failed to load, clearing preview');
image_preview_url = "" image_preview_url = ""
}; };
@@ -109,7 +86,6 @@ let new_tag = $state("");
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
function remove_selected_images() { function remove_selected_images() {
console.log('[CardAdd] Removing selected image');
if (image_preview_url && image_preview_url.startsWith('blob:')) { if (image_preview_url && image_preview_url.startsWith('blob:')) {
URL.revokeObjectURL(image_preview_url); URL.revokeObjectURL(image_preview_url);
} }

View File

@@ -1,13 +1,24 @@
import { redirect, error } from '@sveltejs/kit'; import { redirect, error } from '@sveltejs/kit';
import { stripHtmlTags } from '$lib/js/stripHtmlTags'; import { stripHtmlTags } from '$lib/js/stripHtmlTags';
export async function load({ parent }) { export async function load({ params, fetch }) {
// Get data from universal load function // Fetch recipe data to strip HTML tags server-side
const data = await parent(); // This avoids bundling cheerio in the client bundle
const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
// Strip HTML tags server-side to avoid bundling cheerio in client const res = await fetch(`${apiBase}/items/${params.name}`);
const strippedName = stripHtmlTags(data.name); if (!res.ok) {
const strippedDescription = stripHtmlTags(data.description); // Let the universal load function handle the error
return {
strippedName: '',
strippedDescription: '',
};
}
const item = await res.json();
const strippedName = stripHtmlTags(item.name);
const strippedDescription = stripHtmlTags(item.description);
return { return {
strippedName, strippedName,

View File

@@ -98,7 +98,7 @@
season: isEnglish ? 'Season:' : 'Saison:', season: isEnglish ? 'Season:' : 'Saison:',
keywords: isEnglish ? 'Keywords:' : 'Stichwörter:', keywords: isEnglish ? 'Keywords:' : 'Stichwörter:',
lastModified: isEnglish ? 'Last modified:' : 'Letzte Änderung:', lastModified: isEnglish ? 'Last modified:' : 'Letzte Änderung:',
title: isEnglish ? "Bocken's Recipes" : "Bocken'sche Rezepte" title: isEnglish ? "Bocken Recipes" : "Bocken'sche Rezepte"
}); });
</script> </script>
<style> <style>

View File

@@ -1,7 +1,7 @@
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd'; import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd';
export async function load({ fetch, params, url}) { export async function load({ fetch, params, url, data }) {
const isEnglish = params.recipeLang === 'recipes'; const isEnglish = params.recipeLang === 'recipes';
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte'; const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
@@ -119,6 +119,7 @@ export async function load({ fetch, params, url}) {
const germanShortName = isEnglish ? (item.germanShortName || '') : item.short_name; const germanShortName = isEnglish ? (item.germanShortName || '') : item.short_name;
return { return {
...data, // Include server load data (strippedName, strippedDescription)
...item, ...item,
isFavorite, isFavorite,
multiplier, multiplier,

View File

@@ -292,26 +292,14 @@ button.action_button {
method="POST" method="POST"
bind:this={formElement} bind:this={formElement}
enctype="multipart/form-data" enctype="multipart/form-data"
use:enhance={() => { use:enhance={({ formData }) => {
submitting = true; submitting = true;
console.log('[RecipeAdd:Client] Form submission started'); // Append the image file BEFORE submission (in the outer function)
console.log('[RecipeAdd:Client] Selected image file:', { if (selected_image_file) {
hasFile: !!selected_image_file, formData.append('recipe_image', selected_image_file);
fileName: selected_image_file?.name, }
fileSize: selected_image_file?.size, return async ({ update }) => {
fileType: selected_image_file?.type
});
return async ({ update, formData }) => {
// Append the image file if one was selected
if (selected_image_file) {
console.log('[RecipeAdd:Client] Appending image to FormData');
formData.append('recipe_image', selected_image_file);
} else {
console.log('[RecipeAdd:Client] No image to append');
}
console.log('[RecipeAdd:Client] Sending form to server...');
await update(); await update();
console.log('[RecipeAdd:Client] Form submission complete');
submitting = false; submitting = false;
}; };
}} }}

View File

@@ -7,9 +7,14 @@
const isEnglish = $derived(data.lang === 'en'); const isEnglish = $derived(data.lang === 'en');
const labels = $derived({ const labels = $derived({
title: isEnglish ? 'Categories' : 'Kategorien' title: isEnglish ? 'Categories' : 'Kategorien',
siteTitle: isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte'
}); });
</script> </script>
<svelte:head>
<title>{labels.title} - {labels.siteTitle}</title>
</svelte:head>
<style> <style>
h1 { h1 {
text-align: center; text-align: center;

View File

@@ -9,6 +9,7 @@
const isEnglish = $derived(data.lang === 'en'); const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie'); const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
// Search state // Search state
let matchedRecipeIds = $state(new Set()); let matchedRecipeIds = $state(new Set());
@@ -34,6 +35,11 @@
font-size: 3em; font-size: 3em;
} }
</style> </style>
<svelte:head>
<title>{data.category} - {siteTitle}</title>
</svelte:head>
<h1>{label} <q>{data.category}</q>:</h1> <h1>{label} <q>{data.category}</q>:</h1>
<Search category={data.category} lang={data.lang} recipes={data.recipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search> <Search category={data.category} lang={data.lang} recipes={data.recipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
<section> <section>

View File

@@ -372,13 +372,13 @@
method="POST" method="POST"
bind:this={formElement} bind:this={formElement}
enctype="multipart/form-data" enctype="multipart/form-data"
use:enhance={() => { use:enhance={({ formData }) => {
submitting = true; submitting = true;
return async ({ update, formData }) => { // Append the image file BEFORE submission (in the outer function)
// Append the image file if one was selected if (selected_image_file) {
if (selected_image_file) { formData.append('recipe_image', selected_image_file);
formData.append('recipe_image', selected_image_file); }
} return async ({ update }) => {
await update(); await update();
submitting = false; submitting = false;
}; };

View File

@@ -7,7 +7,17 @@
import Card from '$lib/components/Card.svelte'; import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
let { data } = $props<{ data: PageData }>(); let { data } = $props<{ data: PageData }>();
const isEnglish = $derived(data.lang === 'en');
const labels = $derived({
title: isEnglish ? 'Icons' : 'Icons',
siteTitle: isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte'
});
</script> </script>
<svelte:head>
<title>{labels.title} - {labels.siteTitle}</title>
</svelte:head>
<style> <style>
a{ a{
font-family: "Noto Color Emoji", emoji, sans-serif; font-family: "Noto Color Emoji", emoji, sans-serif;

View File

@@ -8,6 +8,9 @@
let { data } = $props<{ data: PageData }>(); let { data } = $props<{ data: PageData }>();
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
const isEnglish = $derived(data.lang === 'en');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
// Search state // Search state
let matchedRecipeIds = $state(new Set()); let matchedRecipeIds = $state(new Set());
let hasActiveSearch = $state(false); let hasActiveSearch = $state(false);
@@ -26,6 +29,11 @@
return data.season.filter(r => matchedRecipeIds.has(r._id)); return data.season.filter(r => matchedRecipeIds.has(r._id));
}); });
</script> </script>
<svelte:head>
<title>{data.icon} - {siteTitle}</title>
</svelte:head>
<IconLayout icons={data.icons} active_icon={data.icon} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}> <IconLayout icons={data.icons} active_icon={data.icon} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}>
{#snippet recipesSlot()} {#snippet recipesSlot()}
<Recipes> <Recipes>

View File

@@ -14,6 +14,10 @@
const months = $derived(isEnglish const months = $derived(isEnglish
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] ? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]); : ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
const labels = $derived({
title: isEnglish ? 'In Season' : 'Saisonal',
siteTitle: isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte'
});
// Search state // Search state
let matchedRecipeIds = $state(new Set()); let matchedRecipeIds = $state(new Set());
@@ -34,6 +38,10 @@
}); });
</script> </script>
<svelte:head>
<title>{labels.title} - {labels.siteTitle}</title>
</svelte:head>
<SeasonLayout active_index={current_month-1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}> <SeasonLayout active_index={current_month-1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}>
{#snippet recipesSlot()} {#snippet recipesSlot()}
<Recipes> <Recipes>

View File

@@ -11,6 +11,8 @@
const months = $derived(isEnglish const months = $derived(isEnglish
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] ? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]); : ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
const currentMonth = $derived(months[data.month - 1]);
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
@@ -32,6 +34,11 @@
return data.season.filter(r => matchedRecipeIds.has(r._id)); return data.season.filter(r => matchedRecipeIds.has(r._id));
}); });
</script> </script>
<svelte:head>
<title>{currentMonth} - {siteTitle}</title>
</svelte:head>
<SeasonLayout active_index={data.month -1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} onSearchResults={handleSearchResults}> <SeasonLayout active_index={data.month -1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} onSearchResults={handleSearchResults}>
{#snippet recipesSlot()} {#snippet recipesSlot()}
<Recipes> <Recipes>

View File

@@ -7,9 +7,14 @@
const isEnglish = $derived(data.lang === 'en'); const isEnglish = $derived(data.lang === 'en');
const labels = $derived({ const labels = $derived({
title: isEnglish ? 'Keywords' : 'Stichwörter' title: isEnglish ? 'Keywords' : 'Stichwörter',
siteTitle: isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte'
}); });
</script> </script>
<svelte:head>
<title>{labels.title} - {labels.siteTitle}</title>
</svelte:head>
<style> <style>
h1 { h1 {
font-size: 3rem; font-size: 3rem;

View File

@@ -9,6 +9,7 @@
const isEnglish = $derived(data.lang === 'en'); const isEnglish = $derived(data.lang === 'en');
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort'); const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
const siteTitle = $derived(isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte');
// Search state // Search state
let matchedRecipeIds = $state(new Set()); let matchedRecipeIds = $state(new Set());
@@ -34,6 +35,11 @@
font-size: 2em; font-size: 2em;
} }
</style> </style>
<svelte:head>
<title>{data.tag} - {siteTitle}</title>
</svelte:head>
<h1>{label} <q>{data.tag}</q>:</h1> <h1>{label} <q>{data.tag}</q>:</h1>
<Search tag={data.tag} lang={data.lang} recipes={data.recipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search> <Search tag={data.tag} lang={data.lang} recipes={data.recipes} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
<section> <section>

View File

@@ -2,6 +2,16 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import AddButton from '$lib/components/AddButton.svelte'; import AddButton from '$lib/components/AddButton.svelte';
import Converter from './Converter.svelte'; import Converter from './Converter.svelte';
let { data } = $props<{ data: PageData }>();
const isEnglish = $derived(data.lang === 'en');
const labels = $derived({
title: isEnglish ? 'Tips & Tricks' : 'Tipps & Tricks',
siteTitle: isEnglish ? 'Bocken Recipes' : 'Bocken Rezepte',
description: isEnglish
? "A constantly growing collection of recipes from Bocken's kitchen."
: 'Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche.'
});
</script> </script>
<style> <style>
h1{ h1{
@@ -18,15 +28,15 @@ h1{
} }
</style> </style>
<svelte:head> <svelte:head>
<title>Bocken Rezepte</title> <title>{labels.title} - {labels.siteTitle}</title>
<meta name="description" content="Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche." /> <meta name="description" content="{labels.description}" />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" /> <meta property="og:image" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" /> <meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:type" content="image/webp" /> <meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="Pasta al Ragu mit Linguine" /> <meta property="og:image:alt" content="Pasta al Ragu mit Linguine" />
</svelte:head> </svelte:head>
<h1>Tipps & Tricks</h1> <h1>{labels.title}</h1>
<div class=content> <div class=content>
<h2>Trockenhefe vs. Frischhefe</h2> <h2>Trockenhefe vs. Frischhefe</h2>

View File

@@ -1,6 +1,11 @@
<script> <script>
import LinksGrid from '$lib/components/LinksGrid.svelte'; import LinksGrid from '$lib/components/LinksGrid.svelte';
</script> </script>
<svelte:head>
<title>Glaube - Bocken</title>
<meta name="description" content="Gebete und ein interaktiver Rosenkranz zum katholischen Glauben." />
</svelte:head>
<style> <style>
h1{ h1{
text-align: center; text-align: center;

View File

@@ -19,6 +19,11 @@
// Create language context for prayer components // Create language context for prayer components
createLanguageContext(); createLanguageContext();
</script> </script>
<svelte:head>
<title>Gebete - Bocken</title>
<meta name="description" content="Katholische Gebete auf Deutsch und Latein." />
</svelte:head>
<style> <style>
.ccontainer{ .ccontainer{
margin: auto; margin: auto;

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 0 1332 2048">
<path fill="currentColor"
d="M1240 488q0 -4 -9 47q-45 252 -164 367q-39 38 -192 103q-31 10 -74 38q-7 11 -9 68q-2 67 -7 202q-15 284 -15 291q0 30 9 45.5t40 28.5q28 6 84 19q209 48 209 159q0 95 -185 136q-116 26 -266 26q-149 0 -266 -26q-185 -41 -185 -136q0 -76 79 -116q41 -21 153 -46
q89 -20 100 -43q10 -20 10 -47q0 12 -12 -219t-12 -277q0 -6 1.5 -17t1.5 -17q0 -18 -12 -32t-122 -61t-142 -79q-131 -131 -162 -348q-11 -78 -11 -66q0 3 5 -40q24 -211 111 -341h926q41 46 83 185q11 37 27 156q6 45 6 40zM1153 456q0 -154 -63 -283h-858
q-63 132 -63 283q0 134 45 217q160 -21 231 -21h432q102 0 231 21q45 -77 45 -217zM1078 709q-28 -8 -85 -19q-64 -7 -107 -7h-450q-109 0 -192 26q36 8 110 20q109 13 153 13h308q93 0 263 -33zM943 889q-28 2 -82 10q-130 30 -203 30t-113 -7q-14 -2 -79 -20
q-49 -13 -81 -13h-7q54 21 107 41q63 28 91 66q12 17 15 68q8 144 16 349q7 179 7 197q0 38 -10 66q-2 6 -19 35.5t-17 31.5v41q33 16 103 16q47 0 83 -16v-42q0 3 -23 -46t-23 -86q0 -13 7 -197q4 -116 15 -349q3 -47 16 -68q22 -35 90 -65q53 -21 107 -42zM1034 1850
q0 -32 -102 -71q-91 -35 -133 -35q-10 0 -23 7q3 10 28 20.5t25 29.5q0 49 -168 49t-168 -49q0 -16 45 -37l8 -13q-13 -7 -23 -7q-42 0 -133 35q-102 39 -102 71q0 18 32 34q23 11 47 22l11 -11q-21 -11 -35 -19v-9l2 -2q54 22 139 30q72 6 143 13v14q-15 15 -45 15
q-53 0 -118 -16q-73 -18 -61 -18q-6 0 -6 7v4q105 35 264 35q116 0 192 -13q45 -8 103 -30q78 -30 78 -56zM639 1560q-12 -6 -12 -20l-10 -451q-1 -49 -9 -85h21q10 18 10 19v537zM826 1868q-43 22 -165 22q-121 0 -165 -22l7 -12q44 12 107 12h91q69 0 118 -12z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB