feat: enhance GiftCard and add GiftModal for editing gift details

This commit is contained in:
Norbert Maciaszek
2025-11-17 22:19:46 +01:00
parent b13c69b46e
commit 2f53620de7
2 changed files with 189 additions and 2 deletions

View File

@@ -1,10 +1,18 @@
<script lang="ts">
import { refreshAll } from '$app/navigation';
import { formatCurrency } from '$lib/helpers/formatCurrency';
import { DB } from '$lib/integrations/db';
import Badge from '../atoms/Badge.svelte';
import Heading from '../atoms/Heading.svelte';
import { fly } from 'svelte/transition';
import GiftModal from './GiftModal.svelte';
let gift: Gift = $props();
type Props = {
editable?: boolean;
};
let gift: Gift & Props = $props();
let isEditModal = $state(false);
const bgByStatus = {
planned: 'bg-gray-600',
@@ -17,8 +25,10 @@
<div
class={`my-6 rounded-2xl p-4 shadow-sm transition-all duration-200 ease-in-out ${bgByStatus[gift.status]}`}
class:cursor-pointer={!!gift.editable}
in:fly={{ x: 100 }}
out:fly={{ x: -100 }}
onclick={() => (isEditModal = true)}
>
<div class="flex flex-col gap-2">
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -26,6 +36,19 @@
<Heading size="small" spacing="none">
{gift.title}
</Heading>
{#if gift.editable}
<p
class="cursor-pointer text-xs text-red-400"
onclick={(e) => {
e.stopPropagation();
DB.deleteGift(gift.id).finally(() => {
refreshAll();
});
}}
>
Usuń
</p>
{/if}
</div>
{#if gift.link}
@@ -38,7 +61,7 @@
</div>
{/if}
</div>
<div class="flex flex-wrap items-center gap-2">
<div class="flex flex-wrap items-center gap-2 text-white">
<span>Koszt: {formatCurrency(gift.cost)}</span>
</div>
{#if gift.description}
@@ -46,3 +69,18 @@
{/if}
</div>
</div>
{#if isEditModal}
<GiftModal
title="Edytuj prezent"
personId={gift.person}
editGift={gift}
isOpen={isEditModal}
onClose={() => (isEditModal = false)}
onSave={async (data) => {
await DB.updateGift(gift.id, data);
refreshAll();
isEditModal = false;
}}
/>
{/if}

View File

@@ -0,0 +1,149 @@
<script lang="ts">
import type { ComponentProps } from 'svelte';
import type { DB } from '$lib/integrations/db';
import Modal from '../atoms/Modal.svelte';
import Button from '../atoms/Button.svelte';
import { page } from '$app/state';
import { formatStatus } from '$lib/helpers/formatStatus';
type CreateGift = Parameters<typeof DB.createGift>[0];
type Props = {
onSave: (data: CreateGift) => Promise<void>;
editGift?: Gift;
personId: string;
} & Omit<ComponentProps<typeof Modal>, 'children'>;
let { onSave, isOpen, onClose, editGift, personId }: Props = $props();
let gift = $state<Gift>(editGift ? { ...editGift } : ({} as Gift));
let isLoading = $state(false);
if (!gift?.title) {
gift.title = '';
gift.description = '';
gift.link = '';
gift.cost = 0;
gift.imageUrl = '';
gift.status = 'planned';
gift.year = page.data.year.id;
gift.person = personId;
}
const handleSubmit = async (e: Event) => {
e.preventDefault();
isLoading = true;
try {
await onSave(gift);
} catch (error) {
console.error(error);
} finally {
isLoading = false;
}
};
</script>
<Modal {isOpen} {onClose} title={gift ? 'Edytuj prezent' : 'Dodaj nowy prezent'}>
<form onsubmit={handleSubmit} class="space-y-4">
<div>
<label for="title" class="mb-2 block text-sm font-medium text-gray-700"> Tytuł * </label>
<input
id="title"
type="text"
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
required
disabled={isLoading}
bind:value={gift.title}
/>
</div>
<div>
<label for="description" class="mb-2 block text-sm font-medium text-gray-700"> Opis </label>
<textarea
id="description"
rows={3}
class="w-full resize-none rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
bind:value={gift.description}
></textarea>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label for="cost" class="mb-2 block text-sm font-medium text-gray-700">
Koszt (PLN) *
</label>
<input
id="cost"
type="number"
step="0.01"
min="0"
bind:value={gift.cost}
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
required
disabled={isLoading}
/>
</div>
<div>
<label for="status" class="mb-2 block text-sm font-medium text-gray-700"> Status * </label>
<select
id="status"
bind:value={gift.status}
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
required
disabled={isLoading}
>
<option value="planned">{formatStatus('planned')} </option>
<option value="decided">{formatStatus('decided')}</option>
<option value="bought">{formatStatus('bought')}</option>
<option value="ready">{formatStatus('ready')}</option>
<option value="wrapped">{formatStatus('wrapped')}</option>
</select>
</div>
</div>
<div>
<label for="link" class="mb-2 block text-sm font-medium text-gray-700"> Link </label>
<textarea
id="linki"
bind:value={gift.link}
rows={3}
class="w-full resize-none rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
></textarea>
</div>
<div>
<label for="ceneoId" class="mb-2 block text-sm font-medium text-gray-700"> ID Ceneo </label>
<input
id="ceneoId"
type="text"
bind:value={gift.ceneo_id}
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
<p class="mt-2 text-xs text-gray-500">
Jeśli produkt nie został jeszcze kupiony i zostanie podany jego ID na ceneo, bot będzie
obserwował ceny i w razie zmian będzie informował o tym na discord.
</p>
</div>
<div>
<label for="imageUrl" class="mb-2 block text-sm font-medium text-gray-700">
URL obrazu
</label>
<input
id="imageUrl"
type="url"
bind:value={gift.imageUrl}
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
</div>
</form>
{#snippet footer()}
<div class="flex justify-end gap-3">
<Button variant="secondary" onClick={onClose} disabled={isLoading}>Anuluj</Button>
<Button variant="primary" onClick={handleSubmit} disabled={isLoading || !gift?.title?.trim()}>
{isLoading ? 'Zapisywanie...' : 'Zapisz'}
</Button>
</div>
{/snippet}
</Modal>