From d287403248cf97623f55395400eb47029e3ba93f Mon Sep 17 00:00:00 2001 From: Muhammad Usaid Rehman Date: Sun, 4 Aug 2024 20:32:15 +0500 Subject: [PATCH] userSettings + theme change working --- frontend/src/ThemeContext.js | 13 +- frontend/src/api/userService.js | 14 +- frontend/src/components/NavBar.js | 14 +- frontend/src/components/ProfileModal.js | 136 ++++++++++++++++++ frontend/src/components/SettingsModal.js | 30 +++- frontend/src/components/Sidebar.js | 64 ++++++--- .../src/features/expenses/expensesSlice.js | 3 + frontend/src/features/income/incomeSlice.js | 3 + frontend/src/features/user/userSlice.js | 56 +++++++- frontend/src/index.js | 9 +- frontend/src/pages/LoginPage.js | 13 +- frontend/src/pages/RegistrationPage.js | 2 +- 12 files changed, 313 insertions(+), 44 deletions(-) create mode 100644 frontend/src/components/ProfileModal.js diff --git a/frontend/src/ThemeContext.js b/frontend/src/ThemeContext.js index 9979caf..ba6bad5 100644 --- a/frontend/src/ThemeContext.js +++ b/frontend/src/ThemeContext.js @@ -1,22 +1,21 @@ -import React, { createContext, useMemo, useState, useContext } from 'react'; +// src/ThemeContext.js +import React, { createContext, useMemo, useState, useContext, useEffect } from 'react'; import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'; import { lightTheme, darkTheme } from './theme'; +import { useSelector } from 'react-redux'; const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { - const [mode, setMode] = useState('light'); + const mode = useSelector((state) => state.user.theme); const theme = useMemo(() => (mode === 'light' ? lightTheme : darkTheme), [mode]); - const toggleTheme = () => { - setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); - }; - return ( - + {children} ); }; export const useTheme = () => useContext(ThemeContext); + diff --git a/frontend/src/api/userService.js b/frontend/src/api/userService.js index 1a58b36..74aa020 100644 --- a/frontend/src/api/userService.js +++ b/frontend/src/api/userService.js @@ -27,7 +27,7 @@ export const requestPasswordReset = async (email) => { console.error('Error requesting password reset', error); throw error; } -} +}; export const resetPassword = async (token, password) => { try { @@ -37,4 +37,14 @@ export const resetPassword = async (token, password) => { console.error('Error resetting password:', error); throw error; } -} +}; + +export const updateUserProfile = async (profileData) => { + const response = await apiClient.put('/users/profile', profileData); + return response; +}; + +export const updateUserSettings = async (settings) => { + const response = await apiClient.put('/users/settings', settings); + return response.data; +}; diff --git a/frontend/src/components/NavBar.js b/frontend/src/components/NavBar.js index 675d0bf..c915941 100644 --- a/frontend/src/components/NavBar.js +++ b/frontend/src/components/NavBar.js @@ -13,22 +13,33 @@ import AccountCircle from "@mui/icons-material/AccountCircle"; import Logout from "@mui/icons-material/Logout"; import { Link } from "react-router-dom"; import SettingsModal from "./SettingsModal"; // Adjust the import path accordingly +import ProfileModal from "./ProfileModal"; import { useDispatch } from "react-redux"; import { logout } from '../features/user/userSlice'; const NavBar = () => { const [anchorEl, setAnchorEl] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); + const [profileOpen, setProfileOpen] = useState(false); const dispatch = useDispatch(); const handleMenuOpen = (event) => { setAnchorEl(event.currentTarget); }; + const handleProfileOpen = () => { + setProfileOpen(true); + handleMenuClose(); + }; + const handleMenuClose = () => { setAnchorEl(null); }; + const handleProfileClose = () => { + setProfileOpen(false); + }; + const handleSettingsOpen = () => { setSettingsOpen(true); handleMenuClose(); @@ -76,7 +87,7 @@ const NavBar = () => { }, }} > - + Profile @@ -95,6 +106,7 @@ const NavBar = () => { + ); }; diff --git a/frontend/src/components/ProfileModal.js b/frontend/src/components/ProfileModal.js new file mode 100644 index 0000000..d00aaa8 --- /dev/null +++ b/frontend/src/components/ProfileModal.js @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import { Modal, Box, Typography, TextField, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; +import { useDispatch, useSelector } from 'react-redux'; +import { updateUserProfileAsync } from '../features/user/userSlice'; + +const ProfileModal = ({ open, handleClose }) => { + const dispatch = useDispatch(); + const { userInfo } = useSelector((state) => state.user); + const [name, setName] = useState(userInfo.name); + const [email, setEmail] = useState(userInfo.email); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [confirmationOpen, setConfirmationOpen] = useState(false); + const [message, setMessage] = useState(''); + + const handleSave = () => { + if (password !== confirmPassword) { + setMessage('Passwords do not match!'); + return; + } + setMessage(''); + setConfirmationOpen(true); + }; + + const handleConfirmSave = () => { + const updatedProfile = { + name, + email, + password: password ? password : undefined, + }; + dispatch(updateUserProfileAsync(updatedProfile)); + setConfirmationOpen(false); + handleClose(); + }; + + const handleCancel = () => { + setConfirmationOpen(false); + }; + + return ( + <> + + + + Update Profile + + {message && ( + + {message} + + )} + setName(e.target.value)} + /> + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + setConfirmPassword(e.target.value)} + /> + + + + + + Confirm Profile Update + + + Are you sure you want to update your profile details? + + + + + + + + + ); +}; + +export default ProfileModal; + diff --git a/frontend/src/components/SettingsModal.js b/frontend/src/components/SettingsModal.js index 2e8c5d8..40eeb85 100644 --- a/frontend/src/components/SettingsModal.js +++ b/frontend/src/components/SettingsModal.js @@ -1,10 +1,21 @@ // src/components/SettingsModal.js import React from 'react'; -import { Modal, Box, Typography, Switch, FormControlLabel, TextField, MenuItem } from '@mui/material'; -import { useTheme } from '../ThemeContext'; +import { Modal, Box, Typography, Switch, FormControlLabel, TextField, MenuItem, Button } from '@mui/material'; +import { useDispatch, useSelector } from 'react-redux'; +import { toggleTheme, updateUserSettingsAsync } from '../features/user/userSlice'; const SettingsModal = ({ open, handleClose }) => { - const { mode, toggleTheme } = useTheme(); + const dispatch = useDispatch(); + const mode = useSelector((state) => state.user.theme); + + const handleToggleTheme = () => { + dispatch(toggleTheme()); + }; + + const handleSaveSettings = () => { + dispatch(updateUserSettingsAsync({ theme: mode })); + handleClose(); + }; return ( { control={ - } + />} label="Switch To Dark Mode" sx={{ marginTop: 1 }} /> @@ -64,6 +74,14 @@ const SettingsModal = ({ open, handleClose }) => { + ); diff --git a/frontend/src/components/Sidebar.js b/frontend/src/components/Sidebar.js index 8e549a4..a3875df 100644 --- a/frontend/src/components/Sidebar.js +++ b/frontend/src/components/Sidebar.js @@ -1,15 +1,12 @@ -// src/components/Sidebar.js import React from 'react'; -import { Drawer, List, ListItem, ListItemIcon, ListItemText, Toolbar } from '@mui/material'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ReceiptIcon from '@mui/icons-material/Receipt'; -import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'; -import PieChartIcon from '@mui/icons-material/PieChart'; -import BarChartIcon from '@mui/icons-material/BarChart'; +import { Drawer, List, ListItem, ListItemIcon, ListItemText, Toolbar, Typography } from '@mui/material'; +import { Dashboard as DashboardIcon, Receipt as ReceiptIcon, MonetizationOn as MonetizationOnIcon, PieChart as PieChartIcon, BarChart as BarChartIcon } from '@mui/icons-material'; import { Link } from 'react-router-dom'; +import { useTheme } from '@mui/material/styles'; const Sidebar = () => { const drawerWidth = 240; + const theme = useTheme(); return ( { sx={{ width: drawerWidth, flexShrink: 0, - [`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }, + [`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box', bgcolor: theme.palette.background.default }, }} > - + - + + Dashboard + + } + /> - + - + + Expenses + + } + /> - + - + + Income + + } + /> - + - + + Budget + + } + /> - + - + + Reports + + } + /> @@ -48,3 +75,4 @@ const Sidebar = () => { }; export default Sidebar; + diff --git a/frontend/src/features/expenses/expensesSlice.js b/frontend/src/features/expenses/expensesSlice.js index 0f28fdd..0fff343 100644 --- a/frontend/src/features/expenses/expensesSlice.js +++ b/frontend/src/features/expenses/expensesSlice.js @@ -10,6 +10,7 @@ export const fetchExpensesAsync = createAsyncThunk('expenses/fetchExpenses', asy export const addExpenseAsync = createAsyncThunk("expenses/addExpense", async (expenseData, thunkAPI) => { try { const newExpense = await addExpense(expenseData); + thunkAPI.dispatch(fetchExpensesAsync()); // Fetch updated data return newExpense; } catch (error) { return thunkAPI.rejectWithValue(error.response.data); @@ -19,6 +20,7 @@ export const addExpenseAsync = createAsyncThunk("expenses/addExpense", async (ex export const updateExpenseAsync = createAsyncThunk('expenses/updateExpense', async ({ id, expenseData }, thunkAPI) => { try { const updatedExpense = await updateExpense(id, expenseData); + thunkAPI.dispatch(fetchExpensesAsync()); // Fetch updated data return updatedExpense; } catch (error) { return thunkAPI.rejectWithValue(error.response.data); @@ -29,6 +31,7 @@ export const deleteExpenseAsync = createAsyncThunk("expenses/deleteExpense", async (id, thunkAPI) => { try { await deleteExpense(id); + thunkAPI.dispatch(fetchExpensesAsync()); // Fetch updated data return id; } catch (error) { return thunkAPI.rejectWithValue(error.response.data); diff --git a/frontend/src/features/income/incomeSlice.js b/frontend/src/features/income/incomeSlice.js index 55c2a63..f23bb11 100644 --- a/frontend/src/features/income/incomeSlice.js +++ b/frontend/src/features/income/incomeSlice.js @@ -11,6 +11,7 @@ export const fetchIncomesAsync = createAsyncThunk('expenses/fetchIncomes', async export const addIncomeAsync = createAsyncThunk('income/addIncome', async (incomeData, thunkAPI) => { try { const newIncome = await addIncome(incomeData); + thunkAPI.dispatch(fetchIncomesAsync()); // Fetch updated data return newIncome; } catch (error) { return thunkAPI.rejectWithValue(error.response.data); @@ -20,6 +21,7 @@ export const addIncomeAsync = createAsyncThunk('income/addIncome', async (income export const updateIncomeAsync = createAsyncThunk('income/updateIncome', async ({ id, incomeData }, thunkAPI) => { try { const updatedIncome = await updateIncome(id, incomeData); + thunkAPI.dispatch(fetchIncomesAsync()); // Fetch updated data return updatedIncome; } catch (error) { return thunkAPI.rejectWithValue(error.response.data); @@ -29,6 +31,7 @@ export const updateIncomeAsync = createAsyncThunk('income/updateIncome', async ( export const deleteIncomeAsync = createAsyncThunk('income/deleteIncome', async (id, thunkAPI) => { try { await deleteIncome(id); + thunkAPI.dispatch(fetchIncomesAsync()); // Fetch updated data return id; } catch (error) { return thunkAPI.rejectWithValue(error.response.data); diff --git a/frontend/src/features/user/userSlice.js b/frontend/src/features/user/userSlice.js index 3a9c71a..055540c 100644 --- a/frontend/src/features/user/userSlice.js +++ b/frontend/src/features/user/userSlice.js @@ -4,6 +4,8 @@ import { register as apiRegister, requestPasswordReset, resetPassword, + updateUserProfile, + updateUserSettings as apiUpdateUserSettings, } from '../../api/userService'; export const login = createAsyncThunk('user/login', async (credentials, thunkAPI) => { @@ -24,6 +26,16 @@ export const register = createAsyncThunk('user/register', async (userData, thunk } }); +export const updateUserSettingsAsync = createAsyncThunk('user/updateUserSettings', async (settings, thunkAPI) => { + try { + const response = await apiUpdateUserSettings(settings); + return response; + } catch (error) { + return thunkAPI.rejectWithValue(error.response.data); + } +}); + + export const requestPasswordResetAsync = createAsyncThunk('user/requestPasswordReset', async ({ email }, thunkAPI) => { try { const response = await requestPasswordReset(email); @@ -42,6 +54,18 @@ export const resetPasswordAsync = createAsyncThunk('user/resetPassword', async ( } }); +export const updateUserProfileAsync = createAsyncThunk( + 'user/updateUserProfile', + async (profileData, thunkAPI) => { + try { + const response = await updateUserProfile(profileData); + return response.data; + } catch (error) { + return thunkAPI.rejectWithValue(error.response.data); + } + } +); + const userInfoFromStorage = localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : null; @@ -52,12 +76,17 @@ const userSlice = createSlice({ userInfo: userInfoFromStorage, loading: false, error: null, + theme: localStorage.getItem('themeMode') || 'light', // Initialize theme from localStorage }, reducers: { logout: (state) => { state.userInfo = null; localStorage.removeItem('userInfo'); - } + }, + toggleTheme: (state) => { + state.theme = state.theme === 'light' ? 'dark' : 'light'; + localStorage.setItem('themeMode', state.theme); // Save theme preference to localStorage + }, }, extraReducers: (builder) => { builder @@ -107,11 +136,32 @@ const userSlice = createSlice({ .addCase(resetPasswordAsync.rejected, (state, action) => { state.loading = false; state.error = action.payload; + }) + .addCase(updateUserProfileAsync.fulfilled, (state, action) => { + state.userInfo = action.payload; + }) + .addCase(updateUserProfileAsync.rejected, (state, action) => { + state.error = action.payload; + }) + .addCase(updateUserSettingsAsync.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(updateUserSettingsAsync.fulfilled, (state, action) => { + state.loading = false; + state.userInfo = { + ...state.userInfo, + settings: action.payload.settings, + }; + localStorage.setItem('userInfo', JSON.stringify(state.userInfo)); // Save updated settings to local storage + }) + .addCase(updateUserSettingsAsync.rejected, (state, action) => { + state.loading = false; + state.error = action.payload; }); - }, }); -export const { logout } = userSlice.actions; +export const { logout, toggleTheme } = userSlice.actions; export default userSlice.reducer; diff --git a/frontend/src/index.js b/frontend/src/index.js index def3ed3..cf704b8 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; +import { CssBaseline, GlobalStyles } from '@mui/material'; import store from './store'; import App from './App'; import { Provider } from 'react-redux'; @@ -11,6 +11,13 @@ root.render( + + ({ + body: { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }, + })} /> diff --git a/frontend/src/pages/LoginPage.js b/frontend/src/pages/LoginPage.js index 982057d..8584b61 100644 --- a/frontend/src/pages/LoginPage.js +++ b/frontend/src/pages/LoginPage.js @@ -1,11 +1,12 @@ // src/components/LoginPage.js import React, { useState } from 'react'; -import { Container, Typography, TextField, Button, Box, Alert } from '@mui/material'; +import { Container, Typography, TextField, Button, Box, Alert, useTheme } from '@mui/material'; import { useNavigate, Link } from 'react-router-dom'; import { login } from "../features/user/userSlice"; import { useDispatch, useSelector } from 'react-redux'; const LoginPage = () => { + const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); const { loading, error, userInfo } = useSelector((state) => state.user); @@ -22,19 +23,20 @@ const LoginPage = () => { } return ( - + - + ExpenseMate - + Login {error && {error.message}} @@ -42,7 +44,7 @@ const LoginPage = () => { component="form" sx={{ mt: 1, - width: '100%', // Fix IE11 issue. + width: '100%', }} onSubmit={handleLogin} > @@ -57,6 +59,7 @@ const LoginPage = () => { autoFocus value={email} onChange={(e) => setEmail(e.target.value)} + sx={{ bgcolor: theme.palette.background.paper, borderRadius: 1 }} /> { Register - + {error && (