diff --git a/src/components/AddTransactionModal.jsx b/src/components/AddTransactionModal.jsx index 93b3da0..8a05661 100644 --- a/src/components/AddTransactionModal.jsx +++ b/src/components/AddTransactionModal.jsx @@ -1,3 +1,20 @@ +import { useState, useEffect, useRef } from 'react'; +import { useTransactions, useCurrency } from './TransactionContext'; +import { Calendar, ChevronDown } from 'lucide-react'; +import toast from 'react-hot-toast'; + +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,35 +23,43 @@ const formatDate = (date) => { return `${day}/${month}/${year}`; }; -import { useState, useEffect } from 'react'; -import { useCurrency } from "./CurrencyContext"; -import { useTransactions } from './TransactionContext'; -import toast from 'react-hot-toast'; - export default function AddTransactionModal({ showModal = true, setShowModal = () => {}, darkMode = false }) { - const { addTransaction } = useTransactions(); + const { addTransaction, transactions } = useTransactions(); + const { currency, locale } = useCurrency(); const [form, setForm] = useState({ - amount: "", - category: "", - type: "Expense", + amount: '', + category: '', + type: 'Expense', date: formatDate(new Date()), - note: " " + note: '' + }); const [isVisible, setIsVisible] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const { currency, locale, setCurrency, setLocale } = useCurrency(); const [errors, setErrors] = useState({}); - const suggestedCategories = ['Food', 'Transport', 'Groceries', 'Entertainment', 'Bills', 'Shopping', 'Rent', 'Utilities', 'Salary', 'Others']; - const [categorySuggestions, setCategorySuggestions] = useState([]); - const [showSuggestions, setShowSuggestions] = useState(false); +const [showCategoryDropdown, setShowCategoryDropdown] = useState(false); +const [showCustomCategoryInput, setShowCustomCategoryInput] = useState(false); +const [customCategory, setCustomCategory] = useState(''); +const [showDatePicker, setShowDatePicker] = useState(false); +const [categorySuggestions, setCategorySuggestions] = useState([]); +const [showSuggestions, setShowSuggestions] = useState(false); + +const categoryRef = useRef(null); +const dateRef = useRef(null); - const getCurrencySymbol = (currency, locale) => { +const suggestedCategories = ['Food', 'Transport', 'Groceries', 'Entertainment', 'Bills', 'Shopping', 'Rent', 'Utilities', 'Salary', 'Others']; + +const existingCategories = [...new Set(transactions.map(t => t.category))]; +const allCategories = [...defaultCategories, ...existingCategories.filter(c => !defaultCategories.includes(c))]; +const categories = [...new Set(allCategories)].sort(); + + const getCurrencySymbol = () => { return (0).toLocaleString(locale, { - style: "currency", + style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: 0, - }).replace(/\d/g, "").trim(); + }).replace(/\d/g, '').trim(); }; @@ -44,81 +69,225 @@ 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; }; const handleSubmit = async (e) => { - e.preventDefault(); - if (!validateForm()) return; + e.preventDefault(); + if (!validateForm()) return; + setIsSubmitting(true); + + await new Promise(resolve => setTimeout(resolve, 800)); + + try { + addTransaction({ + ...form, + id: Date.now(), + amount: parseFloat(form.amount) + }); + + toast.success('Transaction Added Successfully!'); + + setForm({ + amount: '', + category: '', + type: 'Expense', + date: formatDate(new Date()), + note: '' + }); + + setErrors({}); + handleClose(); + + } catch (error) { + console.error("Failed to add transaction:", error); + toast.error('Could not add transaction. Please try again.'); + } finally { + setIsSubmitting(false); + } +}; - setIsSubmitting(true); - await new Promise(resolve => setTimeout(resolve, 800)); - - try { - - addTransaction({ - ...form, - id: Date.now(), - amount: parseFloat(form.amount) - }); - - - toast.success('Transaction Added Successfully!'); - - - setForm({ - amount: '', - category: '', - type: 'Expense', - date: formatDate(new Date()), - note: '' - }); - handleClose(); - - } catch (error) { - - console.error("Failed to add transaction:", error); - toast.error('Could not add transaction. Please try again.'); - } finally { - - setIsSubmitting(false); - setErrors({}); - } }; const handleClose = () => { setIsVisible(false); 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'); - if (field === "category") { - const input = value.toLowerCase(); - const filtered = suggestedCategories.filter(cat => - cat.toLowerCase().includes(input) && input - ); - setCategorySuggestions(filtered); - setShowSuggestions(filtered.length > 0); - } + setForm({ ...form, amount: sanitizedValue }); + if (errors.amount) setErrors({ ...errors, amount: '' }); +}; + +const handleInputChange = (field, value) => { + let newValue = value; + + if (field === "date") { + newValue = formatDate(value); + } + + if (field === "category") { + const input = value.toLowerCase(); + const filtered = suggestedCategories.filter(cat => + cat.toLowerCase().includes(input) && input + ); + setCategorySuggestions(filtered); + setShowSuggestions(filtered.length > 0); + } + + setForm({ ...form, [field]: newValue }); + if (errors[field]) { + setErrors({ ...errors, [field]: '' }); + } +}; + +const handleCategorySelect = (category) => { + if (category === 'Other') { + setShowCustomCategoryInput(true); + setForm({ ...form, category: '' }); + setCustomCategory(''); + } else { + setForm({ ...form, category }); + setShowCategoryDropdown(false); + setShowSuggestions(false); + } + if (errors.category) setErrors({ ...errors, category: '' }); +}; - setForm({ ...form, [field]: newValue }); - if (errors[field]) { - setErrors({ ...errors, [field]: " " }); +const handleCustomCategorySave = () => { + if (customCategory.trim()) { + setForm({ ...form, category: customCategory }); + setShowCustomCategoryInput(false); + setShowCategoryDropdown(false); + } +}; + + 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; @@ -127,30 +296,32 @@ const handleSubmit = async (e) => {
- {/* Blurry overlay that shows the dashboard content */} +
e.stopPropagation()} > - - {/* Modal header */} -
}`}> -

Add Transaction

-
- -
- -
- -
- {getCurrencySymbol(currency,locale)} +
+ {/* Amount Field */} +
+ +
+ + {getCurrencySymbol(currency, locale)} + 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}

} -
- - -
- - handleInputChange("category", e.target.value)} - onFocus={() => setShowSuggestions(categorySuggestions.length > 0)} - onBlur={() => setTimeout(() => setShowSuggestions(false), 100)} - 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}

} - - {/* 💡 Suggestion Dropdown */} - {showSuggestions && ( -
    - {categorySuggestions.map((suggestion, index) => ( -
  • { - setForm(prev => ({ ...prev, category: suggestion })); - setShowSuggestions(false); - setCategorySuggestions([]); - }} - className="px-4 py-2 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-600 dark:text-white transition-colors" - > - {suggestion} -
  • - - ))} -
- )} + {errors.amount &&

{errors.amount}

}
+
+ +
+ - -
- -
- {['Income', 'Expense'].map((type) => ( - - ))} -
-
- - -
- - { - - 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}

} -
+ {showCategoryDropdown && !showCustomCategoryInput && ( +
+ {categories.map((category) => ( + + ))} + +
+ )} - -
- -