| import { useState, useEffect } from 'react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { | |
| Coffee, ShoppingCart, Zap, Home as HomeIcon, | |
| Car, Utensils, Briefcase, CreditCard, Calendar, MessageSquare | |
| } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { cn } from '@/lib/utils'; | |
| import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; | |
| import { Calendar as CalendarComponent } from '@/components/ui/calendar'; | |
| import { toast } from 'sonner'; | |
| import { v4 as uuidv4 } from 'uuid'; | |
| import database from '@/services/database'; | |
| import { Textarea } from '@/components/ui/textarea'; | |
| const CATEGORIES = [ | |
| { id: 'food', name: 'Food', icon: Utensils, color: 'bg-amber-100 text-amber-600' }, | |
| { id: 'coffee', name: 'Coffee', icon: Coffee, color: 'bg-brown-100 text-brown-600' }, | |
| { id: 'shopping', name: 'Shopping', icon: ShoppingCart, color: 'bg-indigo-100 text-indigo-600' }, | |
| { id: 'utilities', name: 'Utilities', icon: Zap, color: 'bg-yellow-100 text-yellow-600' }, | |
| { id: 'housing', name: 'Housing', icon: HomeIcon, color: 'bg-blue-100 text-blue-600' }, | |
| { id: 'transport', name: 'Transport', icon: Car, color: 'bg-green-100 text-green-600' }, | |
| { id: 'salary', name: 'Salary', icon: Briefcase, color: 'bg-emerald-100 text-emerald-600' }, | |
| { id: 'other', name: 'Other', icon: CreditCard, color: 'bg-gray-100 text-gray-600' }, | |
| ]; | |
| const TransactionForm = () => { | |
| const navigate = useNavigate(); | |
| const [type, setType] = useState<'expense' | 'income'>('expense'); | |
| const [title, setTitle] = useState(''); | |
| const [amount, setAmount] = useState(''); | |
| const [category, setCategory] = useState(''); | |
| const [date, setDate] = useState<Date>(new Date()); | |
| const [note, setNote] = useState(''); | |
| const [message, setMessage] = useState(''); | |
| const [isDbInitialized, setIsDbInitialized] = useState(false); | |
| useEffect(() => { | |
| const initDb = async () => { | |
| try { | |
| await database.initialize(); | |
| setIsDbInitialized(true); | |
| } catch (error) { | |
| console.error('Failed to initialize database:', error); | |
| toast.error('Failed to initialize database'); | |
| } | |
| }; | |
| initDb(); | |
| }, []); | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!title || !amount || !category) { | |
| toast.error('Please fill in all required fields'); | |
| return; | |
| } | |
| if (!isDbInitialized) { | |
| toast.error('Database not initialized yet, please try again'); | |
| return; | |
| } | |
| try { | |
| const formattedDate = date.toISOString().split('T')[0]; | |
| const id = uuidv4(); | |
| database.exec( | |
| 'INSERT INTO transactions (id, title, amount, type, category, date, note, message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', | |
| [id, title, parseFloat(amount), type, category, formattedDate, note || null, message || null] | |
| ); | |
| toast.success('Transaction saved successfully'); | |
| navigate('/transactions'); | |
| } catch (error) { | |
| console.error('Error saving transaction:', error); | |
| toast.error('Failed to save transaction'); | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit} className="space-y-6 p-4 animate-fade-in"> | |
| <div className="flex rounded-lg border border-border overflow-hidden p-1 w-full"> | |
| <button | |
| type="button" | |
| onClick={() => setType('expense')} | |
| className={cn( | |
| "flex-1 py-2.5 text-center text-sm font-medium rounded-md transition-all", | |
| type === 'expense' | |
| ? "bg-primary text-white" | |
| : "bg-transparent text-muted-foreground hover:bg-secondary" | |
| )} | |
| > | |
| Expense | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setType('income')} | |
| className={cn( | |
| "flex-1 py-2.5 text-center text-sm font-medium rounded-md transition-all", | |
| type === 'income' | |
| ? "bg-primary text-white" | |
| : "bg-transparent text-muted-foreground hover:bg-secondary" | |
| )} | |
| > | |
| Income | |
| </button> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium">Title</label> | |
| <input | |
| type="text" | |
| value={title} | |
| onChange={(e) => setTitle(e.target.value)} | |
| placeholder="What was this for?" | |
| className="w-full p-3 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium">Amount</label> | |
| <div className="relative"> | |
| <span className="absolute left-3 top-3 text-muted-foreground">$</span> | |
| <input | |
| type="number" | |
| value={amount} | |
| onChange={(e) => setAmount(e.target.value)} | |
| placeholder="0.00" | |
| className="w-full p-3 pl-8 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card" | |
| /> | |
| </div> | |
| </div> | |
| <div className="space-y-3"> | |
| <label className="text-sm font-medium">Category</label> | |
| <div className="grid grid-cols-4 gap-2"> | |
| {CATEGORIES.map((cat) => ( | |
| <button | |
| key={cat.id} | |
| type="button" | |
| onClick={() => setCategory(cat.id)} | |
| className={cn( | |
| "flex flex-col items-center justify-center p-3 rounded-lg transition-all", | |
| category === cat.id | |
| ? "border-2 border-primary scale-in" | |
| : "border border-border hover:border-primary/50", | |
| )} | |
| > | |
| <div className={cn( | |
| "w-10 h-10 rounded-full flex items-center justify-center mb-1", | |
| cat.color | |
| )}> | |
| <cat.icon size={18} /> | |
| </div> | |
| <span className="text-xs font-medium">{cat.name}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium">Date</label> | |
| <Popover> | |
| <PopoverTrigger asChild> | |
| <button | |
| type="button" | |
| className="w-full flex items-center justify-between p-3 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card" | |
| > | |
| <span> | |
| {date ? date.toLocaleDateString('en-US', { | |
| month: 'short', | |
| day: 'numeric', | |
| year: 'numeric' | |
| }) : 'Select date'} | |
| </span> | |
| <Calendar size={18} className="text-muted-foreground" /> | |
| </button> | |
| </PopoverTrigger> | |
| <PopoverContent className="w-auto p-0" align="center"> | |
| <CalendarComponent | |
| mode="single" | |
| selected={date} | |
| onSelect={(newDate) => newDate && setDate(newDate)} | |
| initialFocus | |
| /> | |
| </PopoverContent> | |
| </Popover> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium">Note (Optional)</label> | |
| <textarea | |
| value={note} | |
| onChange={(e) => setNote(e.target.value)} | |
| placeholder="Add a note..." | |
| rows={3} | |
| className="w-full p-3 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card resize-none" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2"> | |
| <MessageSquare size={16} className="text-primary" /> | |
| <label className="text-sm font-medium">Message (Optional)</label> | |
| </div> | |
| <Textarea | |
| value={message} | |
| onChange={(e) => setMessage(e.target.value)} | |
| placeholder="Add a message about this transaction..." | |
| className="min-h-[100px] border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card" | |
| /> | |
| <p className="text-xs text-muted-foreground"> | |
| Add any additional information or messages related to this transaction. | |
| </p> | |
| </div> | |
| <Button | |
| type="submit" | |
| className="w-full py-6 text-base shadow-[0_2px_10px_rgba(0,122,255,0.3)]" | |
| > | |
| Save Transaction | |
| </Button> | |
| </form> | |
| ); | |
| }; | |
| export default TransactionForm; | |