feat: enhance GiftCard and add GiftModal for editing gift details
This commit is contained in:
@@ -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}
|
||||
|
||||
149
src/lib/components/molecules/GiftModal.svelte
Normal file
149
src/lib/components/molecules/GiftModal.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user