ExpenseTracker So Far
ExpenseTracker So Far
if (!description.trim()) {
window.alert("Description can't be empty");
return;
}
if (!amount.trim()) {
window.alert("Amount can't be empty");
return;
}
try {
await addMutation.mutateAsync(newExpense);
console.log("Expense added:", newExpense);
} catch (error) {
console.error("Error adding expense:", error);
}
setDescription("");
setAmount("");
}else {
console.error("Validation error: Name or amount is missing");
}
};
return(
<div className="add-expense-container">
<div className="add-expense-form-wrapper">
<h2>Add New Expense</h2>
<form onSubmit={handleSubmit}>
<input
className="add-expense-input"
type="text"
value={description}
onChange={(e)=>setDescription(e.target.value)}
placeholder='Description'
/>
<input
className="add-expense-input"
type="number"
value={amount}
onChange={(e)=>setAmount(e.target.value)}
placeholder='Amount'
/>
<button className="add-expense-button" type="submit"> Add
Expense</button>
</form>
</div>
</div>
);
};
return (
<button
className="logout-button"
onClick={handleLogout}
title='Logout'
>
<PowerOff size={24} />
</button>
);
};
if (!response.ok) {
// Handle the 404 error here
if (response.status === 404) {
throw new Error('Expense not found');
} else {
throw new Error('Failed to update expense');
}
}
return response.json();
},
onSuccess: (updatedExpense) => {
queryClient.invalidateQueries(['expenses']); // This refetches the
'expenses' query
dispatch(updateExpense(updatedExpense)); // Update Redux store if
needed
onCancel(); //closes the update option once user clicks update expense
button
},
onError: (error) => {
console.error('Update Expense Error:', error);
},
});
if (!description || !description.toString().trim()) {
window.alert("Description can't be empty");
return;
}
// first we convert the amount to string to use trim() since it doesnt work
on numbers
const amountStr = amount.toString();
if (!amountStr.trim()) {
window.alert("Amount can't be empty");
return;
}
const updatedExpense = {
...expense,
description,
amount: parseFloat(amount),
};
mutation.mutate(updatedExpense); //triggers the mutation(update action) to
send the updated expense to the backend.
};
useEffect(()=>{
setDescription(expense.description);
setAmount(expense.amount);
}, [expense]);
return(
<div className="update-expense-container">
<div className="update-expense-form-wrapper">
<h2>Update Expense</h2>
<form onSubmit={handleSubmit}>
<input
className="update-expense-input"
type="text"
value={description}
onChange={(e)=>setDescription(e.target.value)}
/>
<input
className="update-expense-input"
type="number"
value={amount}
onChange={(e)=>setAmount(e.target.value)}
/>
<div className="update-expense-button-wrapper">
<button
className="update-expense-button"
type="submit">
Update
</button>
<button
className="cancel-button"
type="button"
onClick={onCancel}>
Cancel
</button>
</div>
</form>
</div>
</div>
);
};
const CURRENCY_SYMBOLS = {
'USD': '$',
'INR': '₹',
'EUR': '€',
'GBP': '£',
'JPY': '¥'
};
// Cleanup
return () => {
window.removeEventListener('popstate', handlePopstate);
};
}, [navigate]);
//created the delete mutation here instead of a separate component like add and
update
const dispatch = useDispatch();
const queryClient = useQueryClient();
return (
<div className="dashboard-container">
<div className="dashboard-header">
<div className="total-expenses">
Total Expenses: {CURRENCY_SYMBOLS[selectedCurrency]}
{totalExpenses.toFixed(2)}
</div>
<div className="currency-selector">
<label htmlFor="currency-select">Currency: </label>
<select
id="currency-select"
value={selectedCurrency}
onChange={handleCurrencyChange}
>
{Object.keys(CURRENCY_SYMBOLS).map(currency => (
<option key={currency} value={currency}>
{currency} ({CURRENCY_SYMBOLS[currency]})
</option>
))}
</select>
</div>
<LogoutButton />
</div>
{editingExpense ? (
<div>
<UpdateExpenseForm expense={editingExpense}
onCancel={handleCancelEdit} />
</div>
) : (
expenses && expenses.length > 0 ? ( // check if expenses is defined
<table className="expenses-table">
<thead>
<tr>
<th>Description</th>
<th>Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{expenses.map((expense) => (
<tr key={expense._id}>
<td data-
label="Description">{expense.description}</td>
<td data-label="Amount">
{CURRENCY_SYMBOLS[selectedCurrency]}
{expense.amount}
</td>
<td data-label="Actions">
<div className="action-icons">
<Pencil
className="edit-icon"
size={20}
onClick={() => handleEdit(expense)}
title="Edit Expense"
/>
<Trash2
className="delete-icon"
size={20}
onClick={() =>
handleDelete(expense._id)}
title="Delete Expense"
/>
</div>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="no-expenses">
<p>No expenses added yet.</p>
</div>
)
)}
</div>
);
try {
if (token) {
localStorage.setItem("token", token);// Store the token in localStorage
queryClient.invalidateQueries("expenses");// Auto-refresh data after login
navigate("/dashboard");
} else {
setError("Login failed. Please try again.");
}
if (response.ok) {
localStorage.setItem("token", data.token); // Store the token in
localStorage
navigate("/dashboard");
} else {
setError(data.message || "Login failed. Please try again.");
}
} catch (err) {
setError("An error occurred. Please try again.");
}
};
return (
<div className="login-container">
<div className="login-form">
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<div>
<input
className="login-input"
type="text"
name="username"
placeholder="Username"
value={formData.username}
onChange={handleChange}
required
/>
</div>
<div>
<input
className="login-input"
type="password"
name="password"
placeholder="Password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<button className="login-button" type="submit">Login</button>
</form>
{error && <p style={{ color: "red" }}>{error}</p>}
<div className="register-link">
<Link to="/register">New user? Click here to register</Link>
</div>
</div>
</div>
);
};
if (username.length < 2) {
setError("Username cannot be less than 2 characters");
return;
}
if (password.length < 8) {
setError("Password cannot be less than 8 characters");
return;
}
try {
const response = await fetch("http://localhost:5000/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (response.ok) {
alert("Registration successful. Redirecting to login.");
navigate("/login"); // Redirect to login page (replace with your method)
} else {
if (data.message === "User already exists") {
setError("Username you entered is taken");
} else {
setError(data.message || "Error registering user");
}
}
} catch (error) {
console.error("Error:", error);
}
};
return (
<div className="register-container">
<div className="register-form">
<h2>Register</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<input
className="register-input"
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
className="register-input"
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button className="register-button"type="submit">Register</button>
</form>
<div className="login-link">
<Link to="/login" >
Already registered? Click here to login
</Link>
</div>
</div>
</div>
);
};
Services folder:
import axios from 'axios';
if (token) {
localStorage.setItem('authToken', token); // Save the token
} else {
console.error('Login failed: No token received.');
}
if (!response.ok) {
throw new Error('Failed to fetch expenses');
}
return response.json();
};
try {
const response = await axios.post(
'http://localhost:5000/api/expenses',
expense,
{
headers: {
Authorization: `Bearer ${token}`, // Attach token
},
}
);
return response.data;
} catch (error) {
console.error("Error in addNewExpense:", error.response?.data ||
error.message);
throw new Error("Failed to add expense");
}
};
});
if (!response.ok) {
throw new Error("Failed to delete expense");
}
return id; // returning id as resolved value after successful delete
};
});
if (!response.ok){
throw new Error("Failed to update expense");
}
return response.json(); //will return the updated expense from backend.
}
main.jsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import store from './redux/store';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</Provider>
);
App.jsx:
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Register from "./Pages/Register.jsx";
import Dashboard from "./Pages/Dashboard.jsx";
import Login from "./Pages/Login.jsx";
function App() {
return (
<Router>
<Routes>
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/" element={<Register />} /> {/* Default route */}
</Routes>
</Router>
);
}
Backend:
Controllers folder:
import Expense from "../models/Expense.js";
await expense.save();
res.status(201).json(expense);
} catch (error) {
console.error("Error adding expense:", error);
res.status(400).json({message: 'Error adding expense'})
}
};
//
if (amount !== undefined && (typeof amount !== "number" || amount <= 0)) {
return res.status(400).json({ message: "Invalid amount" });
}
try{
const expense = await Expense.findOneAndUpdate(
{_id: id, userId: req.user.id},
{ amount, description, date },
{ new: true}
);
if (!expense) {
return res.status(404).json({ message: 'Expense not found' });
}
res.status(200).json(expense);
} catch (error) {
res.status(400).json({ message: 'Error updating expense' });
}
};
Middleware folder:
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
dotenv.config();
if (!token) {
console.log("Token missing. Sending 401.");
return res.sendStatus(401); // Unauthorized
}
Models folder:
description:{
type: String,
required: true,
},
amount:{
type: Number,
required: true,
},
date:{
type: Date,
default: Date.now,
},
});
})
Routes folder:
try {
await user.save();
res.status(201).json({message: 'User Created'});
} catch (error) {
res.status(400).json({message: 'User already exists'})
}
});
//user login
router.post('/login', async(req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
const token = jwt.sign({ id: user._id }, secret, { expiresIn: '1h' }); // This
line uses the secret
res.json({ token });
});
// expenseRoutes.js
import express from 'express';
import { getAllExpenses, addExpense, deleteExpense, updateExpense } from
'../controllers/expenseController.js';
import { authenticateToken } from '../middleware/authMiddleware.js';
server.js:
import dotenv from 'dotenv';
dotenv.config();
import express from 'express';
import cors from 'cors';
import expenseRoutes from './routes/expenseRoutes.js';
import mongoose from 'mongoose';
import authRoutes from './routes/authRoutes.js';
//mongodb connection
const mongoURI = 'mongodb://localhost:27017/expense_tracker';
mongoose.connect(mongoURI)
.then(()=>console.log("MongoDB Connected"))
.catch(err=>console.error("MongoDB Connection Error", err));
//routes
app.use('/api/expenses', expenseRoutes);
app.use('/api/auth', authRoutes) //add authentication routes
.env file:
JWT_SECRET=4b156fef45091b0a2c86476a5af1f7c825de4bb2268ae6687ede2aca67dc36588ae3b982
b471539ef183787b05717359bb839eb2d3e4fcb92fefbc97724ca035