refactor: merge AddItemForm and EditItemForm into single ItemForm component

This commit is contained in:
Rasmus Q
2026-03-16 13:09:56 +00:00
parent 1089a6eb3a
commit 1808f6d0ac
3 changed files with 63 additions and 197 deletions

View File

@@ -1,154 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Textarea } from '$lib/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import ImageSelector from './ImageSelector.svelte';
import ColorPicker from '$lib/components/ui/ColorPicker.svelte';
import { enhance } from '$app/forms';
import { languageStore } from '$lib/stores/language.svelte';
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
import { getCardStyle } from '$lib/utils/colors';
interface Props {
onSuccess?: () => void;
wishlistColor?: string | null;
wishlistTheme?: string | null;
}
let { onSuccess, wishlistColor = null, wishlistTheme = null }: Props = $props();
const cardStyle = $derived(getCardStyle(wishlistColor, null));
const t = $derived(languageStore.t);
const currencies = ['DKK', 'EUR', 'USD', 'SEK', 'NOK', 'GBP'];
let linkUrl = $state('');
let imageUrl = $state('');
let color = $state<string | null>(null);
let scrapedImages = $state<string[]>([]);
let isLoadingImages = $state(false);
async function handleLinkChange(event: Event) {
const input = event.target as HTMLInputElement;
linkUrl = input.value;
if (linkUrl && linkUrl.startsWith('http')) {
isLoadingImages = true;
scrapedImages = [];
try {
const response = await fetch('/api/scrape-images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: linkUrl })
});
if (response.ok) {
const data = await response.json();
scrapedImages = data.images || [];
}
} catch (error) {
console.error('Failed to scrape images:', error);
} finally {
isLoadingImages = false;
}
}
}
</script>
<Card style={cardStyle} class="relative overflow-hidden">
<ThemeCard themeName={wishlistTheme} color={wishlistColor} showPattern={false} />
<CardHeader class="relative z-10">
<CardTitle>{t.form.addNewWish}</CardTitle>
</CardHeader>
<CardContent class="relative z-10">
<form
method="POST"
action="?/addItem"
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
onSuccess?.();
};
}}
class="space-y-4"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2 md:col-span-2">
<Label for="title">{t.form.wishName} ({t.form.required})</Label>
<Input id="title" name="title" required placeholder="e.g., Blue Headphones" />
</div>
<div class="space-y-2 md:col-span-2">
<Label for="description">{t.form.description}</Label>
<Textarea
id="description"
name="description"
placeholder="Add details about the item..."
rows={3}
/>
</div>
<div class="space-y-2 md:col-span-2">
<Label for="link">{t.form.link}</Label>
<Input
id="link"
name="link"
type="url"
placeholder="https://..."
bind:value={linkUrl}
oninput={handleLinkChange}
/>
</div>
<div class="space-y-2 md:col-span-2">
<Label for="imageUrl">{t.form.imageUrl}</Label>
<Input
id="imageUrl"
name="imageUrl"
type="url"
placeholder="https://..."
bind:value={imageUrl}
/>
<ImageSelector
images={scrapedImages}
bind:selectedImage={imageUrl}
isLoading={isLoadingImages}
/>
</div>
<div class="space-y-2">
<Label for="price">{t.form.price}</Label>
<Input id="price" name="price" type="number" step="0.01" placeholder="0.00" />
</div>
<div class="space-y-2 md:col-span-2">
<Label for="currency">{t.form.currency}</Label>
<select
id="currency"
name="currency"
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm"
>
{#each currencies as curr (curr)}
<option value={curr} selected={curr === 'DKK'}>{curr}</option>
{/each}
</select>
</div>
<div class="md:col-span-2">
<div class="flex items-center justify-between">
<Label for="color">{t.form.cardColor}</Label>
<ColorPicker bind:color />
</div>
<input type="hidden" name="color" value={color || ''} />
</div>
</div>
<Button type="submit" class="w-full md:w-auto">{t.wishlist.addWish}</Button>
</form>
</CardContent>
</Card>

View File

@@ -13,7 +13,8 @@
import { getCardStyle } from '$lib/utils/colors';
interface Props {
item: Item;
item?: Item | null;
mode: 'add' | 'edit';
onSuccess?: () => void;
onCancel?: () => void;
onColorChange?: (itemId: string, color: string) => void;
@@ -25,7 +26,8 @@
}
let {
item,
item = null,
mode,
onSuccess,
onCancel,
onColorChange,
@@ -36,18 +38,23 @@
wishlistTheme = null
}: Props = $props();
const isEdit = mode === 'edit' && item !== null;
const cardStyle = $derived(getCardStyle(wishlistColor, null));
const t = $derived(languageStore.t);
const currencies = ['DKK', 'EUR', 'USD', 'SEK', 'NOK', 'GBP'];
let linkUrl = $state(item.link || '');
let imageUrl = $state(item.imageUrl || '');
let color = $state<string | null>(item.color);
// Form state - initialized from item if editing
let linkUrl = $state(item?.link || '');
let imageUrl = $state(item?.imageUrl || '');
let color = $state<string | null>(item?.color || null);
let scrapedImages = $state<string[]>([]);
let isLoadingImages = $state(false);
// Form action based on mode
const formAction = isEdit ? '?/updateItem' : '?/addItem';
const submitLabel = isEdit ? t.form.saveChanges : t.wishlist.addWish;
const titleLabel = isEdit ? t.wishlist.editWish : t.form.addNewWish;
async function handleLinkChange(event: Event) {
const input = event.target as HTMLInputElement;
linkUrl = input.value;
@@ -74,17 +81,23 @@
}
}
}
function handleColorChange() {
if (isEdit && item) {
onColorChange?.(item.id, color || '');
}
}
</script>
<Card style={cardStyle} class="relative overflow-hidden">
<ThemeCard themeName={wishlistTheme} color={wishlistColor} showPattern={false} />
<CardHeader class="relative z-10">
<CardTitle>{t.wishlist.editWish}</CardTitle>
<CardTitle>{titleLabel}</CardTitle>
</CardHeader>
<CardContent class="relative z-10">
<form
method="POST"
action="?/updateItem"
action={formAction}
use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
@@ -93,7 +106,9 @@
}}
class="space-y-4"
>
<input type="hidden" name="itemId" value={item.id} />
{#if isEdit && item}
<input type="hidden" name="itemId" value={item.id} />
{/if}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2 md:col-span-2">
@@ -102,7 +117,7 @@
id="title"
name="title"
required
value={item.title}
value={item?.title || ''}
placeholder="e.g., Blue Headphones"
/>
</div>
@@ -112,7 +127,7 @@
<Textarea
id="description"
name="description"
value={item.description || ''}
value={item?.description || ''}
placeholder="Add details about the item..."
rows={3}
/>
@@ -154,7 +169,7 @@
name="price"
type="number"
step="0.01"
value={item.price || ''}
value={item?.price || ''}
placeholder="0.00"
/>
</div>
@@ -167,7 +182,9 @@
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm"
>
{#each currencies as curr (curr)}
<option value={curr} selected={item.currency === curr}>{curr}</option>
<option value={curr} selected={curr === (item?.currency || 'DKK')}>
{curr}
</option>
{/each}
</select>
</div>
@@ -175,39 +192,41 @@
<div class="md:col-span-2">
<div class="flex items-center justify-between">
<Label for="color">{t.form.cardColor}</Label>
<ColorPicker bind:color onchange={() => onColorChange?.(item.id, color || '')} />
<ColorPicker bind:color onchange={handleColorChange} />
</div>
<input type="hidden" name="color" value={color || ''} />
</div>
<div class="space-y-2 md:col-span-2">
<Label for="position">{t.form.position}</Label>
<Input
id="position"
type="number"
min="1"
max={totalItems}
value={currentPosition}
onchange={(e) => {
const newPos = parseInt((e.target as HTMLInputElement).value);
if (newPos >= 1 && newPos <= totalItems) {
onPositionChange?.(newPos);
}
}}
placeholder="1"
/>
<p class="text-sm text-muted-foreground">
Choose where this item appears in your wishlist (1 = top, {totalItems} = bottom)
</p>
</div>
{#if isEdit}
<div class="space-y-2 md:col-span-2">
<Label for="position">{t.form.position}</Label>
<Input
id="position"
type="number"
min="1"
max={totalItems}
value={currentPosition}
onchange={(e) => {
const newPos = parseInt((e.target as HTMLInputElement).value);
if (newPos >= 1 && newPos <= totalItems) {
onPositionChange?.(newPos);
}
}}
placeholder="1"
/>
<p class="text-sm text-muted-foreground">
Choose where this item appears in your wishlist (1 = top, {totalItems} = bottom)
</p>
</div>
{/if}
</div>
<div class="flex gap-2">
<Button type="submit" class="flex-1 md:flex-none">{t.form.saveChanges}</Button>
<Button type="submit" class="flex-1 md:flex-none">{submitLabel}</Button>
{#if onCancel}
<Button type="button" variant="outline" class="flex-1 md:flex-none" onclick={onCancel}
>{t.form.cancel}</Button
>
<Button type="button" variant="outline" class="flex-1 md:flex-none" onclick={onCancel}>
{t.form.cancel}
</Button>
{/if}
</div>
</form>

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
import AddItemForm from '$lib/components/wishlist/AddItemForm.svelte';
import EditItemForm from '$lib/components/wishlist/EditItemForm.svelte';
import ItemForm from '$lib/components/wishlist/ItemForm.svelte';
import ShareLinks from '$lib/components/wishlist/ShareLinks.svelte';
import PageContainer from '$lib/components/layout/PageContainer.svelte';
import Navigation from '$lib/components/layout/Navigation.svelte';
@@ -149,7 +148,8 @@
{#if showAddForm}
<div bind:this={addFormElement}>
<AddItemForm
<ItemForm
mode="add"
onSuccess={handleItemAdded}
wishlistColor={currentColor}
wishlistTheme={currentTheme}
@@ -159,7 +159,8 @@
{#if editingItem}
<div bind:this={editFormElement}>
<EditItemForm
<ItemForm
mode="edit"
item={editingItem}
onSuccess={handleItemUpdated}
onCancel={cancelEditing}