Enhance image compression logic for recipe uploads by allowing dynamic adjustment of quality and dimensions. Update UI to reflect changes in photo handling, including improved feedback on compression status and file sizes. Adjust form elements for better responsiveness and usability.

This commit is contained in:
taogaetz 2025-09-11 13:19:12 -04:00
parent f46064452a
commit ec24b39d1a
3 changed files with 169 additions and 110 deletions

View File

@ -56,8 +56,15 @@ export async function compressImage(
// Draw and compress the image // Draw and compress the image
ctx?.drawImage(img, 0, 0, width, height); ctx?.drawImage(img, 0, 0, width, height);
// Try different quality levels if file is still too large // Try different quality levels and dimensions if file is still too large
const tryCompress = (currentQuality: number): void => { 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( canvas.toBlob(
(blob) => { (blob) => {
if (!blob) { if (!blob) {
@ -67,10 +74,28 @@ export async function compressImage(
const sizeKB = blob.size / 1024; const sizeKB = blob.size / 1024;
// If still too large and we can reduce quality further, try again console.log(`Compression attempt: ${currentWidth}x${currentHeight}, quality: ${currentQuality}, size: ${sizeKB.toFixed(1)}KB, target: ${maxSizeKB}KB`);
if (sizeKB > maxSizeKB && currentQuality > 0.3) {
tryCompress(currentQuality - 0.1); // If still too large, try more aggressive compression
return; 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 // Create new file with compressed data
@ -79,6 +104,8 @@ export async function compressImage(
lastModified: Date.now() lastModified: Date.now()
}); });
console.log(`Compression successful: ${sizeKB.toFixed(1)}KB (${((blob.size / file.size) * 100).toFixed(1)}% of original)`);
resolve({ resolve({
file: compressedFile, file: compressedFile,
originalSize: file.size, originalSize: file.size,
@ -91,7 +118,7 @@ export async function compressImage(
); );
}; };
tryCompress(quality); tryCompress(quality, width, height);
} catch (error) { } catch (error) {
reject(error); reject(error);
} }
@ -132,29 +159,42 @@ export function isImageFile(file: File): boolean {
export function getCompressionSettings(file: File): CompressionOptions { export function getCompressionSettings(file: File): CompressionOptions {
const sizeMB = file.size / (1024 * 1024); const sizeMB = file.size / (1024 * 1024);
let settings: CompressionOptions;
if (sizeMB > 5) { if (sizeMB > 5) {
// Very large files - aggressive compression // Very large files - moderate compression (food photos need detail)
return { settings = {
maxWidth: 1200, maxWidth: 1200,
maxHeight: 800, maxHeight: 900,
quality: 0.6, quality: 0.7,
maxSizeKB: 400 maxSizeKB: 400
}; };
} else if (sizeMB > 2) { } else if (sizeMB > 2) {
// Large files - moderate compression // Large files - light compression
return { settings = {
maxWidth: 1600, maxWidth: 1400,
maxHeight: 1000, maxHeight: 1050,
quality: 0.7, quality: 0.75,
maxSizeKB: 450 maxSizeKB: 450
}; };
} else { } else if (sizeMB > 1) {
// Smaller files - light compression // Medium files - minimal compression
return { settings = {
maxWidth: 1920, maxWidth: 1600,
maxHeight: 1080, maxHeight: 1200,
quality: 0.8, quality: 0.8,
maxSizeKB: 480
};
} else {
// Smaller files - very light compression
settings = {
maxWidth: 1800,
maxHeight: 1350,
quality: 0.85,
maxSizeKB: 500 maxSizeKB: 500
}; };
} }
console.log(`Compression settings for ${sizeMB.toFixed(1)}MB file:`, settings);
return settings;
} }

View File

@ -383,11 +383,21 @@
formData.set('description', descriptionText); formData.set('description', descriptionText);
formData.set('instructions', instructionsText); formData.set('instructions', instructionsText);
// Remove the original photo from FormData (if it exists)
formData.delete('photo');
// Add parsed ingredients as JSON // Add parsed ingredients as JSON
formData.append('parsedIngredients', JSON.stringify(parsedIngredients)); formData.append('parsedIngredients', JSON.stringify(parsedIngredients));
// Add photo if selected // Add compressed photo if selected
if (selectedPhoto) { 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); formData.append('photo', selectedPhoto.file);
} }
@ -459,7 +469,7 @@
method="POST" method="POST"
action="?/update" action="?/update"
{onsubmit} {onsubmit}
class="card relative grid gap-4 border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6" class="card relative grid gap-4 overflow-hidden border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6"
> >
{#if pending} {#if pending}
<div <div
@ -485,18 +495,18 @@
/> />
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"><span class="label-text font-bold">Time</span></span> <span class="label"><span class="label-text font-bold">Time</span></span>
<select name="time" class="select-bordered select w-full"> <select name="time" class="select-bordered select w-full min-w-0">
<option value="Quick" selected={data.recipe?.time === 'Quick'}>Quick</option> <option value="Quick" selected={data.recipe?.time === 'Quick'}>Quick</option>
<option value="Medium" selected={data.recipe?.time === 'Medium'}>Medium</option> <option value="Medium" selected={data.recipe?.time === 'Medium'}>Medium</option>
<option value="Long" selected={data.recipe?.time === 'Long'}>Long</option> <option value="Long" selected={data.recipe?.time === 'Long'}>Long</option>
</select> </select>
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"><span class="label-text font-bold">Station</span></span> <span class="label"><span class="label-text font-bold">Station</span></span>
<select name="station" class="select-bordered select w-full"> <select name="station" class="select-bordered select w-full min-w-0">
<option value="Garde Manger" selected={data.recipe?.station === 'Garde Manger'} <option value="Garde Manger" selected={data.recipe?.station === 'Garde Manger'}
>Garde Manger</option >Garde Manger</option
> >
@ -506,7 +516,7 @@
</label> </label>
</div> </div>
<label class="form-control"> <label class="form-control min-w-0">
<div class="label"> <div class="label">
<span class="label-text font-bold">Visibility</span> <span class="label-text font-bold">Visibility</span>
</div> </div>
@ -519,69 +529,58 @@
/> />
<span class="label-text">Hide from non-chefs (chef-only recipe)</span> <span class="label-text">Hide from non-chefs (chef-only recipe)</span>
</div> </div>
<div class="label">
<span class="label-text-alt text-sm text-base-content/60">
Hidden recipes will only be visible to authenticated chefs
</span>
</div>
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"><span class="label-text font-bold">Photo (optional)</span></span> <span class="label"><span class="label-text font-bold">Photo (optional)</span></span>
<input <input
type="file" type="file"
name="photo" name="photo"
accept="image/*" accept="image/*"
class="file-input-bordered file-input w-full file-input-primary" class="file-input-bordered file-input w-full min-w-0 file-input-primary"
onchange={handlePhotoChange} onchange={handlePhotoChange}
/> />
<div class="label">
<span class="label-text-alt text-sm text-base-content/60">
Supports: JPG, PNG, GIF, WebP (max 50MB, auto-compressed for upload)
</span>
</div>
</label> </label>
{#if selectedPhoto} {#if selectedPhoto}
<div class="flex items-center gap-4 rounded-lg bg-base-200 p-4"> <div class="flex items-center gap-3 rounded-lg bg-base-200 p-3">
<div class="avatar"> <div class="avatar">
<div class="w-24 rounded-lg"> <div class="w-20 rounded-lg">
<img src={selectedPhoto.preview} alt="Recipe preview" class="object-cover" /> <img src={selectedPhoto.preview} alt="Recipe preview" class="object-cover" />
</div> </div>
</div> </div>
<div class="flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm font-medium">{selectedPhoto.name}</p> <p class="truncate text-sm font-medium">{selectedPhoto.name}</p>
<div class="space-y-1 text-xs text-base-content/60"> <div class="text-xs text-base-content/60">
<p>Upload size: {formatFileSize(selectedPhoto.compressedSize)}</p> <p>Size: {formatFileSize(selectedPhoto.compressedSize)}</p>
{#if selectedPhoto.isCompressed} {#if selectedPhoto.isCompressed}
<p class="text-success"> <p class="text-success">
Compressed from {formatFileSize(selectedPhoto.originalSize)} {((1 - selectedPhoto.compressionRatio) * 100).toFixed(0)}% smaller
({((1 - selectedPhoto.compressionRatio) * 100).toFixed(1)}% reduction)
</p> </p>
{/if} {/if}
</div> </div>
<button type="button" class="btn mt-2 btn-sm btn-error" onclick={removePhoto}> <button type="button" class="btn mt-1 btn-xs btn-error" onclick={removePhoto}>
Remove Photo Remove
</button> </button>
</div> </div>
</div> </div>
{/if} {/if}
{#if data.recipe?.photoUrl && !selectedPhoto} {#if data.recipe?.photoUrl && !selectedPhoto}
<div class="flex items-center gap-4 rounded-lg bg-base-200 p-4"> <div class="flex items-center gap-3 rounded-lg bg-base-200 p-3">
<div class="avatar"> <div class="avatar">
<div class="w-24 rounded-lg"> <div class="w-20 rounded-lg">
<img src={data.recipe.photoUrl} alt="Current recipe photo" class="object-cover" /> <img src={data.recipe.photoUrl} alt="Current recipe photo" class="object-cover" />
</div> </div>
</div> </div>
<div class="flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm font-medium">Current Photo</p> <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> <p class="text-xs text-base-content/60">Upload a new photo to replace this one</p>
</div> </div>
</div> </div>
{/if} {/if}
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"><span class="label-text font-bold">Ingredients (one per line)</span></span <span class="label"><span class="label-text font-bold">Ingredients (one per line)</span></span
> >
@ -591,7 +590,7 @@
placeholder="2 cups flour, sifted placeholder="2 cups flour, sifted
3g salt 3g salt
500ml water" 500ml water"
class="textarea-bordered textarea w-full" class="textarea-bordered textarea w-full min-w-0"
bind:value={ingredientsText} bind:value={ingredientsText}
></textarea> ></textarea>
@ -611,16 +610,24 @@
{#if parsed} {#if parsed}
<div class="mb-2 rounded border-l-4 border-primary bg-base-100 p-2"> <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="mb-1 text-xs text-base-content/50">Line {i + 1}:</div>
<div class="flex flex-wrap gap-2 text-sm"> <div class="flex flex-wrap gap-1 overflow-hidden text-sm">
{#if parsed.quantity} {#if parsed.quantity}
<span class="badge badge-sm badge-primary">Qty: {parsed.quantity}</span> <span class="badge badge-sm whitespace-nowrap badge-primary"
>Qty: {parsed.quantity}</span
>
{/if} {/if}
{#if parsed.unit} {#if parsed.unit}
<span class="badge badge-sm badge-secondary">Unit: {parsed.unit}</span> <span class="badge badge-sm whitespace-nowrap badge-secondary"
>Unit: {parsed.unit}</span
>
{/if} {/if}
<span class="badge badge-sm badge-accent">Name: {parsed.name}</span> <span class="badge max-w-32 truncate badge-sm whitespace-nowrap badge-accent"
>Name: {parsed.name}</span
>
{#if parsed.prep} {#if parsed.prep}
<span class="badge badge-outline badge-sm">Prep: {parsed.prep}</span> <span class="badge max-w-32 truncate badge-outline badge-sm whitespace-nowrap"
>Prep: {parsed.prep}</span
>
{/if} {/if}
</div> </div>
</div> </div>
@ -631,7 +638,7 @@
{/if} {/if}
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"> <span class="label">
<span class="label-text font-bold">Description</span> <span class="label-text font-bold">Description</span>
<button <button
@ -645,7 +652,7 @@
<textarea <textarea
name="description" name="description"
rows="3" rows="3"
class="textarea-bordered textarea w-full" class="textarea-bordered textarea w-full min-w-0"
bind:value={descriptionText} bind:value={descriptionText}
placeholder="Enter a description for your recipe" placeholder="Enter a description for your recipe"
></textarea> ></textarea>
@ -660,7 +667,7 @@
{/if} {/if}
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"> <span class="label">
<span class="label-text font-bold">Instructions</span> <span class="label-text font-bold">Instructions</span>
<button <button
@ -674,7 +681,7 @@
<textarea <textarea
name="instructions" name="instructions"
rows="6" rows="6"
class="textarea-bordered textarea w-full" class="textarea-bordered textarea w-full min-w-0"
bind:value={instructionsText} bind:value={instructionsText}
placeholder="Enter step-by-step instructions" placeholder="Enter step-by-step instructions"
></textarea> ></textarea>

View File

@ -1,7 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { renderMarkdownToHTML } from '$lib/utils/markdown'; import { renderMarkdownToHTML } from '$lib/utils/markdown';
import { compressImage, formatFileSize, isImageFile, getCompressionSettings } from '$lib/utils/imageCompression'; import {
compressImage,
formatFileSize,
isImageFile,
getCompressionSettings
} from '$lib/utils/imageCompression';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@ -12,10 +17,10 @@
let descriptionText = $state(''); let descriptionText = $state('');
let instructionsText = $state(''); let instructionsText = $state('');
let formSubmitted = $state(false); let formSubmitted = $state(false);
let selectedPhoto = $state<{ let selectedPhoto = $state<{
file: File; file: File;
preview: string; preview: string;
name: string; name: string;
originalSize: number; originalSize: number;
compressedSize: number; compressedSize: number;
compressionRatio: number; compressionRatio: number;
@ -291,11 +296,21 @@
formData.set('description', descriptionText); formData.set('description', descriptionText);
formData.set('instructions', instructionsText); formData.set('instructions', instructionsText);
// Remove the original photo from FormData (if it exists)
formData.delete('photo');
// Add parsed ingredients as JSON // Add parsed ingredients as JSON
formData.append('parsedIngredients', JSON.stringify(parsedIngredients)); formData.append('parsedIngredients', JSON.stringify(parsedIngredients));
// Add photo if selected // Add compressed photo if selected
if (selectedPhoto) { 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); formData.append('photo', selectedPhoto.file);
} }
@ -352,7 +367,7 @@
} }
lastError = null; lastError = null;
try { try {
// Show loading state // Show loading state
const preview = URL.createObjectURL(file); const preview = URL.createObjectURL(file);
@ -383,7 +398,6 @@
// Clean up the original preview URL // Clean up the original preview URL
URL.revokeObjectURL(preview); URL.revokeObjectURL(preview);
} catch (error) { } catch (error) {
console.error('Error compressing image:', error); console.error('Error compressing image:', error);
lastError = 'Failed to process image. Please try a different file.'; lastError = 'Failed to process image. Please try a different file.';
@ -414,7 +428,7 @@
method="POST" method="POST"
action="?/create" action="?/create"
{onsubmit} {onsubmit}
class="card relative grid gap-4 border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6" class="card relative grid gap-4 overflow-hidden border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6"
> >
{#if pending} {#if pending}
<div <div
@ -434,13 +448,13 @@
required required
autocomplete="off" autocomplete="off"
inputmode="text" inputmode="text"
class="input-bordered input w-full" class="input-bordered input w-full min-w-0"
/> />
</label> </label>
<label class="form-control"> <label class="form-control">
<span class="label"><span class="label-text font-bold">Time</span></span> <span class="label"><span class="label-text font-bold">Time</span></span>
<select name="time" class="select-bordered select w-full"> <select name="time" class="select-bordered select w-full min-w-0">
<option>Quick</option> <option>Quick</option>
<option selected>Medium</option> <option selected>Medium</option>
<option>Long</option> <option>Long</option>
@ -449,7 +463,7 @@
<label class="form-control"> <label class="form-control">
<span class="label"><span class="label-text font-bold">Station</span></span> <span class="label"><span class="label-text font-bold">Station</span></span>
<select name="station" class="select-bordered select w-full"> <select name="station" class="select-bordered select w-full min-w-0">
<option>Garde Manger</option> <option>Garde Manger</option>
<option selected>Pans</option> <option selected>Pans</option>
<option>Grill</option> <option>Grill</option>
@ -457,7 +471,7 @@
</label> </label>
</div> </div>
<label class="form-control"> <label class="form-control min-w-0">
<div class="label"> <div class="label">
<span class="label-text font-bold">Visibility</span> <span class="label-text font-bold">Visibility</span>
</div> </div>
@ -465,55 +479,44 @@
<input type="checkbox" name="hidden" class="checkbox checkbox-primary" /> <input type="checkbox" name="hidden" class="checkbox checkbox-primary" />
<span class="label-text">Hide from non-chefs (chef-only recipe)</span> <span class="label-text">Hide from non-chefs (chef-only recipe)</span>
</div> </div>
<div class="label">
<span class="label-text-alt text-sm text-base-content/60">
Hidden recipes will only be visible to authenticated chefs
</span>
</div>
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"><span class="label-text font-bold">Photo (optional)</span></span> <span class="label"><span class="label-text font-bold">Photo (optional)</span></span>
<input <input
type="file" type="file"
name="photo" name="photo"
accept="image/*" accept="image/*"
class="file-input-bordered file-input w-full file-input-primary" class="file-input-bordered file-input w-full min-w-0 file-input-primary"
onchange={handlePhotoChange} onchange={handlePhotoChange}
/> />
<div class="label">
<span class="label-text-alt text-sm text-base-content/60">
Supports: JPG, PNG, GIF, WebP (max 50MB, auto-compressed for upload)
</span>
</div>
</label> </label>
{#if selectedPhoto} {#if selectedPhoto}
<div class="flex items-center gap-4 rounded-lg bg-base-200 p-4"> <div class="flex items-center gap-3 rounded-lg bg-base-200 p-3">
<div class="avatar"> <div class="avatar">
<div class="w-24 rounded-lg"> <div class="w-20 rounded-lg">
<img src={selectedPhoto.preview} alt="Recipe preview" class="object-cover" /> <img src={selectedPhoto.preview} alt="Recipe preview" class="object-cover" />
</div> </div>
</div> </div>
<div class="flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm font-medium">{selectedPhoto.name}</p> <p class="truncate text-sm font-medium">{selectedPhoto.name}</p>
<div class="text-xs text-base-content/60 space-y-1"> <div class="text-xs text-base-content/60">
<p>Upload size: {formatFileSize(selectedPhoto.compressedSize)}</p> <p>Size: {formatFileSize(selectedPhoto.compressedSize)}</p>
{#if selectedPhoto.isCompressed} {#if selectedPhoto.isCompressed}
<p class="text-success"> <p class="text-success">
Compressed from {formatFileSize(selectedPhoto.originalSize)} {((1 - selectedPhoto.compressionRatio) * 100).toFixed(0)}% smaller
({((1 - selectedPhoto.compressionRatio) * 100).toFixed(1)}% reduction)
</p> </p>
{/if} {/if}
</div> </div>
<button type="button" class="btn mt-2 btn-sm btn-error" onclick={removePhoto}> <button type="button" class="btn mt-1 btn-xs btn-error" onclick={removePhoto}>
Remove Photo Remove
</button> </button>
</div> </div>
</div> </div>
{/if} {/if}
<label class="form-control"> <label class="form-control min-w-0">
<span class="label" <span class="label"
><span class="label-text font-bold">Ingredients (one per line)</span></span ><span class="label-text font-bold">Ingredients (one per line)</span></span
> >
@ -524,7 +527,7 @@
placeholder="2 cups flour, sifted placeholder="2 cups flour, sifted
3g salt 3g salt
500ml water" 500ml water"
class="textarea-bordered textarea w-full" class="textarea-bordered textarea w-full min-w-0"
bind:value={ingredientsText} bind:value={ingredientsText}
></textarea> ></textarea>
@ -545,16 +548,25 @@
{#if parsed} {#if parsed}
<div class="mb-2 rounded border-l-4 border-primary bg-base-100 p-2"> <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="mb-1 text-xs text-base-content/50">Line {i + 1}:</div>
<div class="flex flex-wrap gap-2 text-sm"> <div class="flex flex-wrap gap-1 overflow-hidden text-sm">
{#if parsed.quantity} {#if parsed.quantity}
<span class="badge badge-sm badge-primary">Qty: {parsed.quantity}</span> <span class="badge badge-sm whitespace-nowrap badge-primary"
>Qty: {parsed.quantity}</span
>
{/if} {/if}
{#if parsed.unit} {#if parsed.unit}
<span class="badge badge-sm badge-secondary">Unit: {parsed.unit}</span> <span class="badge badge-sm whitespace-nowrap badge-secondary"
>Unit: {parsed.unit}</span
>
{/if} {/if}
<span class="badge badge-sm badge-accent">Name: {parsed.name}</span> <span class="badge max-w-32 truncate badge-sm whitespace-nowrap badge-accent"
>Name: {parsed.name}</span
>
{#if parsed.prep} {#if parsed.prep}
<span class="badge badge-outline badge-sm">Prep: {parsed.prep}</span> <span
class="badge max-w-32 truncate badge-outline badge-sm whitespace-nowrap"
>Prep: {parsed.prep}</span
>
{/if} {/if}
</div> </div>
</div> </div>
@ -564,7 +576,7 @@
</div> </div>
{/if} {/if}
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"> <span class="label">
<span class="label-text font-bold">Description</span> <span class="label-text font-bold">Description</span>
<button <button
@ -578,7 +590,7 @@
<textarea <textarea
name="description" name="description"
rows="3" rows="3"
class="textarea-bordered textarea w-full" class="textarea-bordered textarea w-full min-w-0"
bind:value={descriptionText} bind:value={descriptionText}
placeholder="Enter a description for your recipe" placeholder="Enter a description for your recipe"
></textarea> ></textarea>
@ -593,7 +605,7 @@
{/if} {/if}
</label> </label>
<label class="form-control"> <label class="form-control min-w-0">
<span class="label"> <span class="label">
<span class="label-text font-bold">Instructions</span> <span class="label-text font-bold">Instructions</span>
<button <button
@ -607,7 +619,7 @@
<textarea <textarea
name="instructions" name="instructions"
rows="6" rows="6"
class="textarea-bordered textarea w-full" class="textarea-bordered textarea w-full min-w-0"
bind:value={instructionsText} bind:value={instructionsText}
placeholder="Enter step-by-step instructions" placeholder="Enter step-by-step instructions"
></textarea> ></textarea>