feat: enhance GiftCard and add GiftModal for editing gift details
This commit is contained in:
@@ -1,10 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { refreshAll } from '$app/navigation';
|
||||||
import { formatCurrency } from '$lib/helpers/formatCurrency';
|
import { formatCurrency } from '$lib/helpers/formatCurrency';
|
||||||
|
import { DB } from '$lib/integrations/db';
|
||||||
import Badge from '../atoms/Badge.svelte';
|
import Badge from '../atoms/Badge.svelte';
|
||||||
import Heading from '../atoms/Heading.svelte';
|
import Heading from '../atoms/Heading.svelte';
|
||||||
import { fly } from 'svelte/transition';
|
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 = {
|
const bgByStatus = {
|
||||||
planned: 'bg-gray-600',
|
planned: 'bg-gray-600',
|
||||||
@@ -17,8 +25,10 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class={`my-6 rounded-2xl p-4 shadow-sm transition-all duration-200 ease-in-out ${bgByStatus[gift.status]}`}
|
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 }}
|
in:fly={{ x: 100 }}
|
||||||
out:fly={{ x: -100 }}
|
out:fly={{ x: -100 }}
|
||||||
|
onclick={() => (isEditModal = true)}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
@@ -26,6 +36,19 @@
|
|||||||
<Heading size="small" spacing="none">
|
<Heading size="small" spacing="none">
|
||||||
{gift.title}
|
{gift.title}
|
||||||
</Heading>
|
</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>
|
</div>
|
||||||
|
|
||||||
{#if gift.link}
|
{#if gift.link}
|
||||||
@@ -38,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
<span>Koszt: {formatCurrency(gift.cost)}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if gift.description}
|
{#if gift.description}
|
||||||
@@ -46,3 +69,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</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