Compare commits

..

No commits in common. "ec24b39d1a233e5b5add322097999609dc80d8a5" and "3d043d9adecc18973bbebd70396e2edf6b3860f5" have entirely different histories.

9 changed files with 112 additions and 488 deletions

View File

@ -1,20 +0,0 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Recipe" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"instructions" TEXT,
"photoUrl" TEXT,
"time" TEXT NOT NULL DEFAULT 'Medium',
"station" TEXT NOT NULL DEFAULT 'Pans',
"hidden" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Recipe" ("createdAt", "description", "id", "instructions", "name", "photoUrl", "station", "time", "updatedAt") SELECT "createdAt", "description", "id", "instructions", "name", "photoUrl", "station", "time", "updatedAt" FROM "Recipe";
DROP TABLE "Recipe";
ALTER TABLE "new_Recipe" RENAME TO "Recipe";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -25,7 +25,6 @@ model Recipe {
photoUrl String?
time String @default("Medium") // Quick, Medium, Long
station String @default("Pans") // Garde Manger, Pans, Grill
hidden Boolean @default(false) // Hidden from non-chefs
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@ -1,200 +0,0 @@
/**
* Client-side image compression utility
* Compresses images to reduce file size while maintaining quality
*/
export interface CompressionOptions {
maxWidth?: number;
maxHeight?: number;
quality?: number;
maxSizeKB?: number;
format?: 'image/jpeg' | 'image/png' | 'image/webp';
}
export interface CompressionResult {
file: File;
originalSize: number;
compressedSize: number;
compressionRatio: number;
}
/**
* Compress an image file using HTML5 Canvas
*/
export async function compressImage(
file: File,
options: CompressionOptions = {}
): Promise<CompressionResult> {
const {
maxWidth = 1920,
maxHeight = 1080,
quality = 0.8,
maxSizeKB = 500, // Target 500KB to stay well under SvelteKit's 512KB limit
format = 'image/jpeg'
} = options;
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
try {
// Calculate new dimensions while maintaining aspect ratio
let { width, height } = img;
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width = Math.floor(width * ratio);
height = Math.floor(height * ratio);
}
// Set canvas dimensions
canvas.width = width;
canvas.height = height;
// Draw and compress the image
ctx?.drawImage(img, 0, 0, width, height);
// Try different quality levels and dimensions if file is still too large
const tryCompress = (currentQuality: number, currentWidth: number, currentHeight: number): void => {
// Update canvas dimensions
canvas.width = currentWidth;
canvas.height = currentHeight;
// Redraw with new dimensions
ctx?.drawImage(img, 0, 0, currentWidth, currentHeight);
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('Failed to compress image'));
return;
}
const sizeKB = blob.size / 1024;
console.log(`Compression attempt: ${currentWidth}x${currentHeight}, quality: ${currentQuality}, size: ${sizeKB.toFixed(1)}KB, target: ${maxSizeKB}KB`);
// If still too large, try more aggressive compression
if (sizeKB > maxSizeKB) {
// First try reducing quality more aggressively
if (currentQuality > 0.1) {
tryCompress(Math.max(0.1, currentQuality - 0.15), currentWidth, currentHeight);
return;
}
// Then try reducing dimensions
else if (currentWidth > 400 && currentHeight > 300) {
const newWidth = Math.max(400, Math.floor(currentWidth * 0.8));
const newHeight = Math.max(300, Math.floor(currentHeight * 0.8));
tryCompress(0.1, newWidth, newHeight);
return;
}
// If we've exhausted all compression options and still too large, reject
else {
console.error(`Failed to compress image below ${maxSizeKB}KB. Final size: ${sizeKB.toFixed(1)}KB`);
reject(new Error(`Unable to compress image below ${maxSizeKB}KB. Please try a different image.`));
return;
}
}
// Create new file with compressed data
const compressedFile = new File([blob], file.name, {
type: format,
lastModified: Date.now()
});
console.log(`Compression successful: ${sizeKB.toFixed(1)}KB (${((blob.size / file.size) * 100).toFixed(1)}% of original)`);
resolve({
file: compressedFile,
originalSize: file.size,
compressedSize: blob.size,
compressionRatio: blob.size / file.size
});
},
format,
currentQuality
);
};
tryCompress(quality, width, height);
} catch (error) {
reject(error);
}
};
img.onerror = () => {
reject(new Error('Failed to load image'));
};
// Load the image
img.src = URL.createObjectURL(file);
});
}
/**
* Format file size for display
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Check if a file is an image
*/
export function isImageFile(file: File): boolean {
return file.type.startsWith('image/');
}
/**
* Get optimal compression settings based on file size
*/
export function getCompressionSettings(file: File): CompressionOptions {
const sizeMB = file.size / (1024 * 1024);
let settings: CompressionOptions;
if (sizeMB > 5) {
// Very large files - moderate compression (food photos need detail)
settings = {
maxWidth: 1200,
maxHeight: 900,
quality: 0.7,
maxSizeKB: 400
};
} else if (sizeMB > 2) {
// Large files - light compression
settings = {
maxWidth: 1400,
maxHeight: 1050,
quality: 0.75,
maxSizeKB: 450
};
} else if (sizeMB > 1) {
// Medium files - minimal compression
settings = {
maxWidth: 1600,
maxHeight: 1200,
quality: 0.8,
maxSizeKB: 480
};
} else {
// Smaller files - very light compression
settings = {
maxWidth: 1800,
maxHeight: 1350,
quality: 0.85,
maxSizeKB: 500
};
}
console.log(`Compression settings for ${sizeMB.toFixed(1)}MB file:`, settings);
return settings;
}

View File

@ -3,10 +3,8 @@ import type { LayoutServerLoad } from './$types';
import prisma from '$lib/server/prisma';
export const load: LayoutServerLoad = async ({ locals }) => {
// Get recipes for search functionality
// If not authenticated, filter out hidden recipes
// Get all recipes for search functionality
const recipes = await prisma.recipe.findMany({
where: locals.authenticated ? {} : { hidden: false },
include: {
ingredients: {
include: {

View File

@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import prisma from '$lib/server/prisma';
export const load: PageServerLoad = async ({ params, locals }) => {
export const load: PageServerLoad = async ({ params }) => {
const recipe = await prisma.recipe.findUnique({
where: { id: params.id },
include: {
@ -18,10 +18,5 @@ export const load: PageServerLoad = async ({ params, locals }) => {
throw error(404, 'Recipe not found');
}
// Check if recipe is hidden and user is not authenticated
if (recipe.hidden && !locals.authenticated) {
throw error(404, 'Recipe not found');
}
return { recipe };
};

View File

@ -2,12 +2,6 @@
import { page } from '$app/stores';
import type { PageData } from './$types';
import { renderMarkdownToHTML } from '$lib/utils/markdown';
import {
compressImage,
formatFileSize,
isImageFile,
getCompressionSettings
} from '$lib/utils/imageCompression';
let { data }: { data: PageData } = $props();
@ -19,15 +13,9 @@
let instructionsText = $state('');
let formSubmitted = $state(false);
let showDeleteConfirm = $state(false);
let selectedPhoto = $state<{
file: File;
preview: string;
name: string;
originalSize: number;
compressedSize: number;
compressionRatio: number;
isCompressed: boolean;
} | null>(null);
let selectedPhoto = $state<{ file: File; preview: string; name: string; size: number } | null>(
null
);
let showMarkdownHelp = $state(false);
let showIngredientHelp = $state(false);
@ -52,63 +40,34 @@
});
// Photo handling functions
async function handlePhotoChange(event: Event) {
function handlePhotoChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
// Validate file size (10MB limit)
if (file.size > 10 * 1024 * 1024) {
lastError = 'Photo must be less than 10MB';
input.value = '';
return;
}
// Validate file type
if (!isImageFile(file)) {
if (!file.type.startsWith('image/')) {
lastError = 'Please select a valid image file';
input.value = '';
return;
}
// Validate file size (50MB limit for original files)
if (file.size > 50 * 1024 * 1024) {
lastError = 'Photo must be less than 50MB';
input.value = '';
return;
}
lastError = null;
try {
// Show loading state
// Create preview URL
const preview = URL.createObjectURL(file);
selectedPhoto = {
file,
preview,
name: file.name,
originalSize: file.size,
compressedSize: file.size,
compressionRatio: 1,
isCompressed: false
size: file.size
};
// Compress the image
const compressionSettings = getCompressionSettings(file);
const result = await compressImage(file, compressionSettings);
// Update with compressed file
selectedPhoto = {
file: result.file,
preview: URL.createObjectURL(result.file),
name: file.name,
originalSize: result.originalSize,
compressedSize: result.compressedSize,
compressionRatio: result.compressionRatio,
isCompressed: result.compressionRatio < 0.95
};
// Clean up the original preview URL
URL.revokeObjectURL(preview);
} catch (error) {
console.error('Error compressing image:', error);
lastError = 'Failed to process image. Please try a different file.';
input.value = '';
selectedPhoto = null;
}
lastError = null;
}
}
@ -383,21 +342,11 @@
formData.set('description', descriptionText);
formData.set('instructions', instructionsText);
// Remove the original photo from FormData (if it exists)
formData.delete('photo');
// Add parsed ingredients as JSON
formData.append('parsedIngredients', JSON.stringify(parsedIngredients));
// Add compressed photo if selected
// Add photo if selected
if (selectedPhoto) {
console.log('Uploading compressed photo:', {
name: selectedPhoto.file.name,
size: selectedPhoto.file.size,
type: selectedPhoto.file.type,
originalSize: selectedPhoto.originalSize,
compressedSize: selectedPhoto.compressedSize
});
formData.append('photo', selectedPhoto.file);
}
@ -469,7 +418,7 @@
method="POST"
action="?/update"
{onsubmit}
class="card relative grid gap-4 overflow-hidden border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6"
class="card relative grid gap-4 border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6"
>
{#if pending}
<div
@ -495,18 +444,18 @@
/>
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label"><span class="label-text font-bold">Time</span></span>
<select name="time" class="select-bordered select w-full min-w-0">
<select name="time" class="select-bordered select w-full">
<option value="Quick" selected={data.recipe?.time === 'Quick'}>Quick</option>
<option value="Medium" selected={data.recipe?.time === 'Medium'}>Medium</option>
<option value="Long" selected={data.recipe?.time === 'Long'}>Long</option>
</select>
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label"><span class="label-text font-bold">Station</span></span>
<select name="station" class="select-bordered select w-full min-w-0">
<select name="station" class="select-bordered select w-full">
<option value="Garde Manger" selected={data.recipe?.station === 'Garde Manger'}
>Garde Manger</option
>
@ -516,71 +465,56 @@
</label>
</div>
<label class="form-control min-w-0">
<div class="label">
<span class="label-text font-bold">Visibility</span>
</div>
<div class="flex items-center gap-3">
<input
type="checkbox"
name="hidden"
class="checkbox checkbox-primary"
checked={data.recipe?.hidden || false}
/>
<span class="label-text">Hide from non-chefs (chef-only recipe)</span>
</div>
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label"><span class="label-text font-bold">Photo (optional)</span></span>
<input
type="file"
name="photo"
accept="image/*"
class="file-input-bordered file-input w-full min-w-0 file-input-primary"
class="file-input-bordered file-input w-full file-input-primary"
onchange={handlePhotoChange}
/>
<div class="label">
<span class="label-text-alt text-sm text-base-content/60">
Supports: JPG, PNG, GIF, WebP (max 10MB)
</span>
</div>
</label>
{#if selectedPhoto}
<div class="flex items-center gap-3 rounded-lg bg-base-200 p-3">
<div class="flex items-center gap-4 rounded-lg bg-base-200 p-4">
<div class="avatar">
<div class="w-20 rounded-lg">
<div class="w-24 rounded-lg">
<img src={selectedPhoto.preview} alt="Recipe preview" class="object-cover" />
</div>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{selectedPhoto.name}</p>
<div class="text-xs text-base-content/60">
<p>Size: {formatFileSize(selectedPhoto.compressedSize)}</p>
{#if selectedPhoto.isCompressed}
<p class="text-success">
{((1 - selectedPhoto.compressionRatio) * 100).toFixed(0)}% smaller
<div class="flex-1">
<p class="text-sm font-medium">{selectedPhoto.name}</p>
<p class="text-xs text-base-content/60">
{(selectedPhoto.size / 1024 / 1024).toFixed(2)} MB
</p>
{/if}
</div>
<button type="button" class="btn mt-1 btn-xs btn-error" onclick={removePhoto}>
Remove
<button type="button" class="btn mt-2 btn-sm btn-error" onclick={removePhoto}>
Remove Photo
</button>
</div>
</div>
{/if}
{#if data.recipe?.photoUrl && !selectedPhoto}
<div class="flex items-center gap-3 rounded-lg bg-base-200 p-3">
<div class="flex items-center gap-4 rounded-lg bg-base-200 p-4">
<div class="avatar">
<div class="w-20 rounded-lg">
<div class="w-24 rounded-lg">
<img src={data.recipe.photoUrl} alt="Current recipe photo" class="object-cover" />
</div>
</div>
<div class="min-w-0 flex-1">
<div class="flex-1">
<p class="text-sm font-medium">Current Photo</p>
<p class="text-xs text-base-content/60">Upload a new photo to replace this one</p>
</div>
</div>
{/if}
<label class="form-control min-w-0">
<label class="form-control">
<span class="label"><span class="label-text font-bold">Ingredients (one per line)</span></span
>
@ -590,7 +524,7 @@
placeholder="2 cups flour, sifted
3g salt
500ml water"
class="textarea-bordered textarea w-full min-w-0"
class="textarea-bordered textarea w-full"
bind:value={ingredientsText}
></textarea>
@ -610,24 +544,16 @@
{#if parsed}
<div class="mb-2 rounded border-l-4 border-primary bg-base-100 p-2">
<div class="mb-1 text-xs text-base-content/50">Line {i + 1}:</div>
<div class="flex flex-wrap gap-1 overflow-hidden text-sm">
<div class="flex flex-wrap gap-2 text-sm">
{#if parsed.quantity}
<span class="badge badge-sm whitespace-nowrap badge-primary"
>Qty: {parsed.quantity}</span
>
<span class="badge badge-sm badge-primary">Qty: {parsed.quantity}</span>
{/if}
{#if parsed.unit}
<span class="badge badge-sm whitespace-nowrap badge-secondary"
>Unit: {parsed.unit}</span
>
<span class="badge badge-sm badge-secondary">Unit: {parsed.unit}</span>
{/if}
<span class="badge max-w-32 truncate badge-sm whitespace-nowrap badge-accent"
>Name: {parsed.name}</span
>
<span class="badge badge-sm badge-accent">Name: {parsed.name}</span>
{#if parsed.prep}
<span class="badge max-w-32 truncate badge-outline badge-sm whitespace-nowrap"
>Prep: {parsed.prep}</span
>
<span class="badge badge-outline badge-sm">Prep: {parsed.prep}</span>
{/if}
</div>
</div>
@ -638,7 +564,7 @@
{/if}
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label">
<span class="label-text font-bold">Description</span>
<button
@ -652,7 +578,7 @@
<textarea
name="description"
rows="3"
class="textarea-bordered textarea w-full min-w-0"
class="textarea-bordered textarea w-full"
bind:value={descriptionText}
placeholder="Enter a description for your recipe"
></textarea>
@ -667,7 +593,7 @@
{/if}
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label">
<span class="label-text font-bold">Instructions</span>
<button
@ -681,7 +607,7 @@
<textarea
name="instructions"
rows="6"
class="textarea-bordered textarea w-full min-w-0"
class="textarea-bordered textarea w-full"
bind:value={instructionsText}
placeholder="Enter step-by-step instructions"
></textarea>

View File

@ -24,7 +24,6 @@ export const POST: RequestHandler = async ({ request, params }) => {
const instructions = (formData.get('instructions') as string | null)?.trim() || null;
const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
const hidden = formData.get('hidden') === 'on'; // Checkbox returns 'on' when checked
const photo = formData.get('photo') as File | null;
const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null;
let parsedIngredients: Array<{ name: string; quantity: number | null; unit: string | null; prep: string | null }> = [];
@ -100,8 +99,7 @@ export const POST: RequestHandler = async ({ request, params }) => {
instructions,
photoUrl: photoUrl || existingRecipe.photoUrl, // Keep existing photo if no new one uploaded
time,
station,
hidden
station
}
});

View File

@ -1,12 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
import { renderMarkdownToHTML } from '$lib/utils/markdown';
import {
compressImage,
formatFileSize,
isImageFile,
getCompressionSettings
} from '$lib/utils/imageCompression';
let { data }: { data: PageData } = $props();
@ -17,15 +11,9 @@
let descriptionText = $state('');
let instructionsText = $state('');
let formSubmitted = $state(false);
let selectedPhoto = $state<{
file: File;
preview: string;
name: string;
originalSize: number;
compressedSize: number;
compressionRatio: number;
isCompressed: boolean;
} | null>(null);
let selectedPhoto = $state<{ file: File; preview: string; name: string; size: number } | null>(
null
);
let showIngredientHelp = $state(false);
let showMarkdownHelp = $state(false);
@ -296,21 +284,11 @@
formData.set('description', descriptionText);
formData.set('instructions', instructionsText);
// Remove the original photo from FormData (if it exists)
formData.delete('photo');
// Add parsed ingredients as JSON
formData.append('parsedIngredients', JSON.stringify(parsedIngredients));
// Add compressed photo if selected
// Add photo if selected
if (selectedPhoto) {
console.log('Uploading compressed photo:', {
name: selectedPhoto.file.name,
size: selectedPhoto.file.size,
type: selectedPhoto.file.type,
originalSize: selectedPhoto.originalSize,
compressedSize: selectedPhoto.compressedSize
});
formData.append('photo', selectedPhoto.file);
}
@ -347,63 +325,34 @@
}
// Photo handling functions
async function handlePhotoChange(event: Event) {
function handlePhotoChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
// Validate file size (10MB limit)
if (file.size > 10 * 1024 * 1024) {
lastError = 'Photo must be less than 10MB';
input.value = '';
return;
}
// Validate file type
if (!isImageFile(file)) {
if (!file.type.startsWith('image/')) {
lastError = 'Please select a valid image file';
input.value = '';
return;
}
// Validate file size (50MB limit for original files)
if (file.size > 50 * 1024 * 1024) {
lastError = 'Photo must be less than 50MB';
input.value = '';
return;
}
lastError = null;
try {
// Show loading state
// Create preview URL
const preview = URL.createObjectURL(file);
selectedPhoto = {
file,
preview,
name: file.name,
originalSize: file.size,
compressedSize: file.size,
compressionRatio: 1,
isCompressed: false
size: file.size
};
// Compress the image
const compressionSettings = getCompressionSettings(file);
const result = await compressImage(file, compressionSettings);
// Update with compressed file
selectedPhoto = {
file: result.file,
preview: URL.createObjectURL(result.file),
name: file.name,
originalSize: result.originalSize,
compressedSize: result.compressedSize,
compressionRatio: result.compressionRatio,
isCompressed: result.compressionRatio < 0.95
};
// Clean up the original preview URL
URL.revokeObjectURL(preview);
} catch (error) {
console.error('Error compressing image:', error);
lastError = 'Failed to process image. Please try a different file.';
input.value = '';
selectedPhoto = null;
}
lastError = null;
}
}
@ -428,7 +377,7 @@
method="POST"
action="?/create"
{onsubmit}
class="card relative grid gap-4 overflow-hidden border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6"
class="card relative grid gap-4 border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6"
>
{#if pending}
<div
@ -448,13 +397,13 @@
required
autocomplete="off"
inputmode="text"
class="input-bordered input w-full min-w-0"
class="input-bordered input w-full"
/>
</label>
<label class="form-control">
<span class="label"><span class="label-text font-bold">Time</span></span>
<select name="time" class="select-bordered select w-full min-w-0">
<select name="time" class="select-bordered select w-full">
<option>Quick</option>
<option selected>Medium</option>
<option>Long</option>
@ -463,7 +412,7 @@
<label class="form-control">
<span class="label"><span class="label-text font-bold">Station</span></span>
<select name="station" class="select-bordered select w-full min-w-0">
<select name="station" class="select-bordered select w-full">
<option>Garde Manger</option>
<option selected>Pans</option>
<option>Grill</option>
@ -471,52 +420,42 @@
</label>
</div>
<label class="form-control min-w-0">
<div class="label">
<span class="label-text font-bold">Visibility</span>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" name="hidden" class="checkbox checkbox-primary" />
<span class="label-text">Hide from non-chefs (chef-only recipe)</span>
</div>
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label"><span class="label-text font-bold">Photo (optional)</span></span>
<input
type="file"
name="photo"
accept="image/*"
class="file-input-bordered file-input w-full min-w-0 file-input-primary"
class="file-input-bordered file-input w-full file-input-primary"
onchange={handlePhotoChange}
/>
<div class="label">
<span class="label-text-alt text-sm text-base-content/60">
Supports: JPG, PNG, GIF, WebP (max 10MB)
</span>
</div>
</label>
{#if selectedPhoto}
<div class="flex items-center gap-3 rounded-lg bg-base-200 p-3">
<div class="flex items-center gap-4 rounded-lg bg-base-200 p-4">
<div class="avatar">
<div class="w-20 rounded-lg">
<div class="w-24 rounded-lg">
<img src={selectedPhoto.preview} alt="Recipe preview" class="object-cover" />
</div>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{selectedPhoto.name}</p>
<div class="text-xs text-base-content/60">
<p>Size: {formatFileSize(selectedPhoto.compressedSize)}</p>
{#if selectedPhoto.isCompressed}
<p class="text-success">
{((1 - selectedPhoto.compressionRatio) * 100).toFixed(0)}% smaller
<div class="flex-1">
<p class="text-sm font-medium">{selectedPhoto.name}</p>
<p class="text-xs text-base-content/60">
{(selectedPhoto.size / 1024 / 1024).toFixed(2)} MB
</p>
{/if}
</div>
<button type="button" class="btn mt-1 btn-xs btn-error" onclick={removePhoto}>
Remove
<button type="button" class="btn mt-2 btn-sm btn-error" onclick={removePhoto}>
Remove Photo
</button>
</div>
</div>
{/if}
<label class="form-control min-w-0">
<label class="form-control">
<span class="label"
><span class="label-text font-bold">Ingredients (one per line)</span></span
>
@ -527,7 +466,7 @@
placeholder="2 cups flour, sifted
3g salt
500ml water"
class="textarea-bordered textarea w-full min-w-0"
class="textarea-bordered textarea w-full"
bind:value={ingredientsText}
></textarea>
@ -548,25 +487,16 @@
{#if parsed}
<div class="mb-2 rounded border-l-4 border-primary bg-base-100 p-2">
<div class="mb-1 text-xs text-base-content/50">Line {i + 1}:</div>
<div class="flex flex-wrap gap-1 overflow-hidden text-sm">
<div class="flex flex-wrap gap-2 text-sm">
{#if parsed.quantity}
<span class="badge badge-sm whitespace-nowrap badge-primary"
>Qty: {parsed.quantity}</span
>
<span class="badge badge-sm badge-primary">Qty: {parsed.quantity}</span>
{/if}
{#if parsed.unit}
<span class="badge badge-sm whitespace-nowrap badge-secondary"
>Unit: {parsed.unit}</span
>
<span class="badge badge-sm badge-secondary">Unit: {parsed.unit}</span>
{/if}
<span class="badge max-w-32 truncate badge-sm whitespace-nowrap badge-accent"
>Name: {parsed.name}</span
>
<span class="badge badge-sm badge-accent">Name: {parsed.name}</span>
{#if parsed.prep}
<span
class="badge max-w-32 truncate badge-outline badge-sm whitespace-nowrap"
>Prep: {parsed.prep}</span
>
<span class="badge badge-outline badge-sm">Prep: {parsed.prep}</span>
{/if}
</div>
</div>
@ -576,7 +506,7 @@
</div>
{/if}
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label">
<span class="label-text font-bold">Description</span>
<button
@ -590,7 +520,7 @@
<textarea
name="description"
rows="3"
class="textarea-bordered textarea w-full min-w-0"
class="textarea-bordered textarea w-full"
bind:value={descriptionText}
placeholder="Enter a description for your recipe"
></textarea>
@ -605,7 +535,7 @@
{/if}
</label>
<label class="form-control min-w-0">
<label class="form-control">
<span class="label">
<span class="label-text font-bold">Instructions</span>
<button
@ -619,7 +549,7 @@
<textarea
name="instructions"
rows="6"
class="textarea-bordered textarea w-full min-w-0"
class="textarea-bordered textarea w-full"
bind:value={instructionsText}
placeholder="Enter step-by-step instructions"
></textarea>

View File

@ -24,7 +24,6 @@ export const POST: RequestHandler = async ({ request }) => {
const instructions = (formData.get('instructions') as string | null)?.trim() || null;
const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
const hidden = formData.get('hidden') === 'on'; // Checkbox returns 'on' when checked
const photo = formData.get('photo') as File | null;
const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null;
let parsedIngredients: Array<{ name: string; quantity: number | null; unit: string | null; prep: string | null }> = [];
@ -90,8 +89,7 @@ export const POST: RequestHandler = async ({ request }) => {
instructions,
photoUrl,
time,
station,
hidden
station
}
});