From 3aff88e72732b1c1367b4db8ae346204caf5b875 Mon Sep 17 00:00:00 2001 From: Mahesh S Date: Wed, 23 Jul 2025 08:40:54 +0530 Subject: [PATCH 1/3] [ENHANCEMENT] Improved Transaction Form with Custom Calendar, Validation, and Dark Mode Support #50 --- src/components/AddTransactionModal.jsx | 454 +++++++++++++++++++------ 1 file changed, 346 insertions(+), 108 deletions(-) diff --git a/src/components/AddTransactionModal.jsx b/src/components/AddTransactionModal.jsx index b9efb28..588dfd4 100644 --- a/src/components/AddTransactionModal.jsx +++ b/src/components/AddTransactionModal.jsx @@ -1,3 +1,20 @@ +import { useState, useEffect, useRef } from 'react'; +import { useTransactions } from './TransactionContext'; +import { Calendar, ChevronDown } from 'lucide-react'; + +const defaultCategories = [ + "Food", + "Entertainment", + "Utilities", + "Transport", + "Shopping", + "Health", + "Education", + "Salary", + "Gift", + "Investment" +]; + const formatDate = (date) => { const d = new Date(date); const day = String(d.getDate()).padStart(2, '0'); @@ -6,11 +23,8 @@ const formatDate = (date) => { return `${day}/${month}/${year}`; }; -import { useState, useEffect } from 'react'; -import { useTransactions } from './TransactionContext'; - export default function AddTransactionModal({ showModal = true, setShowModal = () => {}, darkMode = false }) { - const { addTransaction } = useTransactions(); + const { addTransaction, transactions } = useTransactions(); const [form, setForm] = useState({ amount: '', category: '', @@ -21,6 +35,17 @@ export default function AddTransactionModal({ showModal = true, setShowModal = ( const [isVisible, setIsVisible] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [errors, setErrors] = useState({}); + const [showCategoryDropdown, setShowCategoryDropdown] = useState(false); + const [showCustomCategoryInput, setShowCustomCategoryInput] = useState(false); + const [customCategory, setCustomCategory] = useState(''); + const [showDatePicker, setShowDatePicker] = useState(false); + const categoryRef = useRef(null); + const dateRef = useRef(null); + + // Get unique categories from existing transactions + const existingCategories = [...new Set(transactions.map(t => t.category))]; + const allCategories = [...defaultCategories, ...existingCategories.filter(c => !defaultCategories.includes(c))]; + const categories = [...new Set(allCategories)].sort(); useEffect(() => { if (showModal) { @@ -28,14 +53,25 @@ export default function AddTransactionModal({ showModal = true, setShowModal = ( } }, [showModal]); + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if (categoryRef.current && !categoryRef.current.contains(event.target)) { + setShowCategoryDropdown(false); + setShowCustomCategoryInput(false); + } + if (dateRef.current && !dateRef.current.contains(event.target)) { + setShowDatePicker(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + const validateForm = () => { const newErrors = {}; - if (!form.amount || parseFloat(form.amount) <= 0) { - newErrors.amount = 'Please enter a valid amount'; - } - if (!form.category.trim()) { - newErrors.category = 'Category is required'; - } + if (!form.amount || parseFloat(form.amount) <= 0) newErrors.amount = 'Please enter a valid amount'; + if (!form.category.trim()) newErrors.category = 'Category is required'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -45,7 +81,6 @@ export default function AddTransactionModal({ showModal = true, setShowModal = ( if (!validateForm()) return; setIsSubmitting(true); - await new Promise(resolve => setTimeout(resolve, 800)); addTransaction({ @@ -71,15 +106,133 @@ export default function AddTransactionModal({ showModal = true, setShowModal = ( setTimeout(() => setShowModal(false), 300); }; - const handleInputChange = (field, value) => { - let newValue = value; - if (field === 'date') { - newValue = formatDate(value); + const handleAmountChange = (value) => { + const sanitizedValue = value + .replace(/[^0-9.]/g, '') + .replace(/^\./, '') + .replace(/(\..*)\./g, '$1'); + + setForm({ ...form, amount: sanitizedValue }); + if (errors.amount) setErrors({ ...errors, amount: '' }); + }; + + const handleCategorySelect = (category) => { + if (category === 'Other') { + setShowCustomCategoryInput(true); + setForm({ ...form, category: '' }); + setCustomCategory(''); + } else { + setForm({ ...form, category }); + setShowCategoryDropdown(false); + } + if (errors.category) setErrors({ ...errors, category: '' }); + }; + + const handleCustomCategorySave = () => { + if (customCategory.trim()) { + setForm({ ...form, category: customCategory }); + setShowCustomCategoryInput(false); + setShowCategoryDropdown(false); } - setForm({ ...form, [field]: newValue }); - if (errors[field]) { - setErrors({ ...errors, [field]: '' }); + }; + + const handleDateChange = (dateString) => { + setForm({ ...form, date: formatDate(dateString) }); + setShowDatePicker(false); + }; + + const renderDatePicker = () => { + const [dd, mm, yyyy] = form.date.split('/'); + const currentDate = new Date(`${yyyy}-${mm}-${dd}`); + const monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"]; + + const daysInMonth = new Date(yyyy, mm, 0).getDate(); + const firstDayOfMonth = new Date(yyyy, mm - 1, 1).getDay(); + + const days = []; + for (let i = 0; i < firstDayOfMonth; i++) { + days.push(
); + } + + for (let i = 1; i <= daysInMonth; i++) { + const isSelected = i === parseInt(dd); + days.push( + + ); } + + return ( +
+
+ +
+ {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()} +
+ +
+
+ {['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => ( +
{day}
+ ))} +
+
+ {days} +
+
+ +
+
+ ); }; if (!showModal) return null; @@ -88,24 +241,22 @@ export default function AddTransactionModal({ showModal = true, setShowModal = (
- {/* Blurry overlay that shows the dashboard content */}
e.stopPropagation()} > - {/* Modal header */} -

Add Transaction

@@ -120,132 +271,219 @@ export default function AddTransactionModal({ showModal = true, setShowModal = (
- {/* Modal body */} -
- {/* Amount field */} +
+ {/* Amount Field */}
- +
- + handleInputChange('amount', e.target.value)} - className={`w-full pl-8 pr-4 py-3 border-2 rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ + onChange={(e) => handleAmountChange(e.target.value)} + className={`w-full pl-8 pr-4 py-2.5 border rounded-lg focus:ring-2 focus:outline-none ${ errors.amount - ? 'border-red-300 bg-red-50 dark:bg-red-900/20 dark:border-red-700' - : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 dark:bg-gray-700 dark:text-white' + ? 'border-red-500 bg-red-50 dark:bg-red-900/10 dark:border-red-700' + : 'border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white' }`} placeholder="0.00" />
- {errors.amount &&

{errors.amount}

} + {errors.amount &&

{errors.amount}

}
- {/* Category field */} + {/* Category Field */}
- - handleInputChange('category', e.target.value)} - className={`w-full px-4 py-3 border-2 rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ - errors.category - ? 'border-red-300 bg-red-50 dark:bg-red-900/20 dark:border-red-700' - : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 dark:bg-gray-700 dark:text-white' - }`} - placeholder="e.g., Food, Transport, Entertainment" - /> - {errors.category &&

{errors.category}

} + +
+ + + {showCategoryDropdown && !showCustomCategoryInput && ( +
+ {categories.map((category) => ( + + ))} + +
+ )} + + {showCustomCategoryInput && ( +
+ setCustomCategory(e.target.value)} + className={`w-full px-3 py-2 border-b ${ + darkMode ? 'border-blue-400 bg-transparent text-white' : 'border-blue-500 bg-transparent' + } focus:outline-none`} + placeholder="Enter custom category" + autoFocus + onKeyDown={(e) => e.key === 'Enter' && handleCustomCategorySave()} + /> +
+ + +
+
+ )} +
+ {errors.category &&

{errors.category}

}
- {/* Type selector */} + {/* Type Selector */}
- -
- {['Income', 'Expense'].map((type) => ( - - ))} + +
+
+ +
+ {['Income', 'Expense'].map((type) => ( + + ))} +
- {/* Date field */} + {/* Date Field */}
- - { - const [dd, mm, yyyy] = form.date.split('/'); - return `${yyyy}-${mm}-${dd}`; - })() - } - onChange={(e) => handleInputChange('date', e.target.value)} - className={`w-full px-4 py-3 border-2 rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ - errors.date - ? 'border-red-300 bg-red-50 dark:bg-red-900/20 dark:border-red-700' - : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 dark:bg-gray-700 dark:text-white' - }`} - /> - {errors.date &&

{errors.date}

} + +
+ + {showDatePicker && renderDatePicker()} +
- {/* Note field */} + {/* Note Field */}
- +