Add initial project setup with PocketBase integration, global state management, and UI components for gift tracking
This commit is contained in:
184
src/components/molecules/GiftModal/index.tsx
Normal file
184
src/components/molecules/GiftModal/index.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'use client';
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import { Modal } from '@/components/atoms/Modal';
|
||||
import { Button } from '@/components/atoms/Button';
|
||||
import { formatStatus } from '@/helpers/formatStatus';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
gift?: Gift;
|
||||
yearId: string;
|
||||
personId?: string;
|
||||
onSave: (data: {
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
cost: number;
|
||||
imageUrl: string;
|
||||
status: 'planned' | 'decided' | 'bought' | 'wrapped';
|
||||
year: string;
|
||||
person: string;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
|
||||
export const GiftModal: FC<Props> = ({ isOpen, onClose, gift, yearId, personId, onSave }) => {
|
||||
const [title, setTitle] = useState(gift?.title || '');
|
||||
const [description, setDescription] = useState(gift?.description || '');
|
||||
const [link, setLink] = useState(gift?.link || '');
|
||||
const [cost, setCost] = useState(gift?.cost || 0);
|
||||
const [imageUrl, setImageUrl] = useState(gift?.imageUrl || '');
|
||||
const [status, setStatus] = useState<Gift['status']>(gift?.status || 'planned');
|
||||
const [selectedPersonId, setSelectedPersonId] = useState(personId || '');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (gift) {
|
||||
setTitle(gift.title);
|
||||
setDescription(gift.description || '');
|
||||
setLink(gift.link || '');
|
||||
setCost(gift.cost);
|
||||
setImageUrl(gift.imageUrl || '');
|
||||
setStatus(gift.status);
|
||||
setSelectedPersonId(gift.person);
|
||||
} else {
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
setLink('');
|
||||
setCost(0);
|
||||
setImageUrl('');
|
||||
setStatus('planned');
|
||||
setSelectedPersonId(personId || '');
|
||||
}
|
||||
}, [gift, personId, isOpen]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onSave({
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
cost,
|
||||
imageUrl,
|
||||
status,
|
||||
year: yearId,
|
||||
person: selectedPersonId,
|
||||
});
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error saving gift:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={gift ? 'Edytuj prezent' : 'Dodaj nowy prezent'}
|
||||
footer={
|
||||
<div className='flex justify-end gap-3'>
|
||||
<Button variant='secondary' onClick={onClose} disabled={isLoading}>
|
||||
Anuluj
|
||||
</Button>
|
||||
<Button variant='primary' onClick={handleSubmit} disabled={isLoading || !title.trim() || !selectedPersonId}>
|
||||
{isLoading ? 'Zapisywanie...' : 'Zapisz'}
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||
<div>
|
||||
<label htmlFor='title' className='block text-sm font-medium text-gray-700 mb-2'>
|
||||
Tytuł *
|
||||
</label>
|
||||
<input
|
||||
id='title'
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className='w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all'
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor='description' className='block text-sm font-medium text-gray-700 mb-2'>
|
||||
Opis
|
||||
</label>
|
||||
<textarea
|
||||
id='description'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className='w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all resize-none'
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<label htmlFor='cost' className='block text-sm font-medium text-gray-700 mb-2'>
|
||||
Koszt (PLN) *
|
||||
</label>
|
||||
<input
|
||||
id='cost'
|
||||
type='number'
|
||||
step='0.01'
|
||||
min='0'
|
||||
value={cost}
|
||||
onChange={(e) => setCost(parseFloat(e.target.value) || 0)}
|
||||
className='w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all'
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor='status' className='block text-sm font-medium text-gray-700 mb-2'>
|
||||
Status *
|
||||
</label>
|
||||
<select
|
||||
id='status'
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value as typeof status)}
|
||||
className='w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all'
|
||||
required
|
||||
disabled={isLoading}>
|
||||
<option value='planned'>{formatStatus('planned')} </option>
|
||||
<option value='decided'>{formatStatus('decided')}</option>
|
||||
<option value='bought'>{formatStatus('bought')}</option>
|
||||
<option value='wrapped'>{formatStatus('wrapped')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor='link' className='block text-sm font-medium text-gray-700 mb-2'>
|
||||
Link
|
||||
</label>
|
||||
<input
|
||||
id='link'
|
||||
type='url'
|
||||
value={link}
|
||||
onChange={(e) => setLink(e.target.value)}
|
||||
className='w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all'
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor='imageUrl' className='block text-sm font-medium text-gray-700 mb-2'>
|
||||
URL obrazu
|
||||
</label>
|
||||
<input
|
||||
id='imageUrl'
|
||||
type='url'
|
||||
value={imageUrl}
|
||||
onChange={(e) => setImageUrl(e.target.value)}
|
||||
className='w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all'
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user