Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions frontend/src/pages/Budgets.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ const Budgets = () => {
const { currency } = useCurrency();
const [isBudgetModalOpen, setIsBudgetModalOpen] = useState(false);
const [editingBudget, setEditingBudget] = useState(null);
const [error, setError] = useState('');
const [budgetAlerts, setBudgetAlerts] = useState([]);

const fetchData = useCallback(async () => {
setLoading(true);
setError('');
try {
const [budgetsRes, categoriesRes, transactionsRes] = await Promise.all([
api.get('/budgets'),
Expand All @@ -27,6 +30,7 @@ const Budgets = () => {
setTransactions(transactionsRes.data.transactions || []);
} catch (error) {
console.error('Failed to fetch budgets or transactions', error);
setError('Failed to load budgets. Please check your connection.');
} finally {
setLoading(false);
}
Expand All @@ -36,6 +40,29 @@ const Budgets = () => {
fetchData();
}, [fetchData]);

// Budget alerts - auto calculates
useEffect(() => {
const alerts = budgets.map(budget => {
const spent = calculateSpent(budget);
const percent = (spent / budget.amount) * 100;
if (percent > 100) {
return {
message: `🚨 OVER BUDGET: ${budget.category} exceeded by ${(percent-100).toFixed(1)}%`,
type: 'error'
};
}
if (percent > 80) {
return {
message: `⚠️ WARNING: ${budget.category} at ${percent.toFixed(1)}% of budget`,
type: 'warning'
};
}
return null;
}).filter(alert => alert);

setBudgetAlerts(alerts);
}, [budgets, transactions]);

const handleOpenBudgetModal = (budget = null) => {
setEditingBudget(budget);
setIsBudgetModalOpen(true);
Expand All @@ -48,22 +75,26 @@ const Budgets = () => {

const handleFormSubmit = async (formData, id) => {
try {
setError('');
if (id) await api.put(`/budgets/${id}`, formData);
else await api.post('/budgets', formData);
fetchData();
handleCloseBudgetModal();
} catch (error) {
console.error('Failed to save budget', error);
setError('Failed to save budget. Please try again.');
}
};

const handleDeleteBudget = async (id) => {
if (window.confirm('Are you sure you want to delete this budget?')) {
try {
setError('');
await api.delete(`/budgets/${id}`);
fetchData();
} catch (error) {
console.error('Failed to delete budget', error);
setError('Failed to delete budget. Please try again.');
}
}
};
Expand Down Expand Up @@ -95,6 +126,34 @@ const Budgets = () => {
</div>
</div>

{/* Error Display */}
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center">
<svg className="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<p className="text-red-800">{error}</p>
</div>
</div>
)}

{/* Budget Alerts */}
{budgetAlerts.map((alert, index) => (
<div
key={index}
className={`mb-3 p-3 rounded-lg border ${
alert.type === 'error'
? 'bg-red-50 border-red-200 text-red-800'
: 'bg-yellow-50 border-yellow-200 text-yellow-800'
}`}
>
<div className="flex items-center">
<span className="font-semibold">{alert.message}</span>
</div>
</div>
))}

{loading ? (
<Spinner />
) : budgets.length > 0 ? (
Expand Down Expand Up @@ -129,9 +188,7 @@ const Budgets = () => {
{budgets.map((b) => {
const spent = calculateSpent(b);
const remaining = b.amount - spent;
const percent = Math.min((spent / b.amount) * 100, 100).toFixed(
1
);
const percent = Math.min((spent / b.amount) * 100, 100).toFixed(1);

return (
<tr key={b._id}>
Expand Down Expand Up @@ -177,6 +234,12 @@ const Budgets = () => {
<span className="text-xs text-gray-500">{percent}%</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleOpenBudgetModal(b)}
className="text-indigo-600 hover:text-indigo-900 mr-4"
>
Edit
</button>
<button
onClick={() => handleDeleteBudget(b._id)}
className="text-red-600 hover:text-red-900"
Expand Down Expand Up @@ -207,4 +270,4 @@ const Budgets = () => {
);
};

export default Budgets;
export default Budgets;
66 changes: 50 additions & 16 deletions frontend/src/pages/ContactUs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Link } from "react-router-dom";
import ThemeToggle from "../components/ThemeToggle";
import { useEffect } from "react";
import { toast } from 'react-toastify';

const ContactUs = () => {
const [formData, setFormData] = useState({
Expand All @@ -14,6 +15,7 @@ const ContactUs = () => {

const [isSubmitted, setIsSubmitted] = useState(false);
const [activeFaq, setActiveFaq] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const handleChange = (e) => {
const { name, value } = e.target;
Expand All @@ -23,20 +25,41 @@ const ContactUs = () => {
}));
};

const handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
console.log("Form submitted:", formData);
setIsSubmitted(true);
setTimeout(() => {
setIsSubmitted(false);
setFormData({
name: "",
email: "",
subject: "",
message: "",
inquiryType: "general",
setIsSubmitting(true);

try {
// Simulate API call - replace with actual endpoint
await new Promise(resolve => setTimeout(resolve, 1000));

// Log to console for now (replace with actual API call)
console.log("Form submitted:", formData);

toast.success('Message sent successfully! We will reply within 24 hours.', {
position: "top-right",
autoClose: 5000,
});
}, 5000);

setIsSubmitted(true);
setTimeout(() => {
setIsSubmitted(false);
setFormData({
name: "",
email: "",
subject: "",
message: "",
inquiryType: "general",
});
}, 5000);
} catch (error) {
toast.error('Failed to send message. Please try again.', {
position: "top-right",
autoClose: 5000,
});
} finally {
setIsSubmitting(false);
}
};

const toggleFaq = (index) => {
Expand Down Expand Up @@ -173,7 +196,7 @@ const faqs = [
{
question: "Will there be a mobile app version?",
answer:
"Were currently developing a mobile app for both iOS and Android so you can manage your finances on the go. Stay tuned for announcements on our official site!",
"We're currently developing a mobile app for both iOS and Android so you can manage your finances on the go. Stay tuned for announcements on our official site!",
},
];

Expand Down Expand Up @@ -504,9 +527,20 @@ const faqs = [
<div className="flex justify-end">
<button
type="submit"
className="inline-flex justify-center py-3 px-8 border border-transparent shadow-sm text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
disabled={isSubmitting}
className="inline-flex justify-center py-3 px-8 border border-transparent shadow-sm text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send Message
{isSubmitting ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</>
) : (
'Send Message'
)}
</button>
</div>
</form>
Expand Down Expand Up @@ -588,4 +622,4 @@ const faqs = [
);
};

export default ContactUs;
export default ContactUs;
40 changes: 29 additions & 11 deletions frontend/src/pages/ReceiptsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ const ReceiptsPage = () => {
return;
}

// Validate file type
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'application/pdf'];
if (!validTypes.includes(file.type)) {
setError("Please upload JPG, PNG, or PDF files only.");
return;
}

// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
setError("File size must be less than 5MB.");
return;
}

const formData = new FormData();
formData.append("receipt", file);

Expand All @@ -68,7 +81,13 @@ const ReceiptsPage = () => {
// Open the modal to allow user to edit the extracted data
setOpenEditReceiptResult(true);
} catch (err) {
setError("Upload failed. Please try again.");
if (err.response?.status === 413) {
setError("File too large. Please upload a smaller image.");
} else if (err.response?.status === 400) {
setError("Invalid file type. Please upload JPG, PNG, or PDF.");
} else {
setError("Upload failed. Please try again.");
}
console.error(err);
} finally {
setUploading(false);
Expand Down Expand Up @@ -158,7 +177,7 @@ const ReceiptsPage = () => {
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
<form onSubmit={handleSubmit}>
<label className="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Select a receipt file (JPG, PNG, PDF)
Select a receipt file (JPG, PNG, PDF - Max 5MB)
</label>
<input
type="file"
Expand Down Expand Up @@ -233,14 +252,13 @@ const ReceiptsPage = () => {
</button>
</div>

<img
src={`${import.meta.env.VITE_API_URL?.replace(
"/api",
""
)}${receiptResult.fileUrl}`}
alt="Uploaded Receipt"
className="mt-4 rounded-lg max-w-full h-auto"
/>
{receiptResult.fileUrl && (
<img
src={`/api${receiptResult.fileUrl}`}
alt="Uploaded Receipt"
className="mt-4 rounded-lg max-w-full h-auto max-h-64 object-contain"
/>
)}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400">
Expand Down Expand Up @@ -288,4 +306,4 @@ const ReceiptsPage = () => {
);
};

export default ReceiptsPage;
export default ReceiptsPage;