mirror of
https://github.com/Alvin-Zilverstand/challenge-11.git
synced 2026-03-06 11:06:21 +01:00
Enhance App component by adding Register and BackToHome routes, and update CustomerList to CustomerManagement for improved customer management functionality. Refactor CustomerManagement to utilize a centralized axios instance for API calls and implement a dropdown for car model selection.
This commit is contained in:
@@ -3,13 +3,16 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-d
|
|||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
|
import Register from './components/Register';
|
||||||
import Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
import CustomerList from './components/CustomerList';
|
import CustomerList from './components/CustomerList';
|
||||||
import CustomerDetail from './components/CustomerDetail';
|
import CustomerDetail from './components/CustomerDetail';
|
||||||
import CarModifications from './components/CarModifications';
|
import CarModifications from './components/CarModifications';
|
||||||
import ContactHistory from './components/ContactHistory';
|
import ContactHistory from './components/ContactHistory';
|
||||||
|
import CustomerManagement from './components/CustomerManagement';
|
||||||
import PrivateRoute from './components/PrivateRoute';
|
import PrivateRoute from './components/PrivateRoute';
|
||||||
import UserManagement from './components/UserManagement';
|
import UserManagement from './components/UserManagement';
|
||||||
|
import BackToHome from './components/BackToHome';
|
||||||
|
|
||||||
// Create a theme that matches the "stoer en snel" (tough and fast) requirement
|
// Create a theme that matches the "stoer en snel" (tough and fast) requirement
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
@@ -55,6 +58,7 @@ function App() {
|
|||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/register" element={<Register />} />
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -67,7 +71,7 @@ function App() {
|
|||||||
path="/customers"
|
path="/customers"
|
||||||
element={
|
element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<CustomerList />
|
<CustomerManagement />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -105,6 +109,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
<BackToHome />
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
39
client/src/components/BackToHome.js
Normal file
39
client/src/components/BackToHome.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Button, Box } from '@mui/material';
|
||||||
|
import HomeIcon from '@mui/icons-material/Home';
|
||||||
|
|
||||||
|
const BackToHome = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 20,
|
||||||
|
right: 20,
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<HomeIcon />}
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
sx={{
|
||||||
|
borderRadius: '50px',
|
||||||
|
padding: '10px 20px',
|
||||||
|
boxShadow: 3,
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
transition: 'transform 0.2s ease-in-out',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BackToHome;
|
||||||
@@ -18,12 +18,57 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
IconButton,
|
IconButton,
|
||||||
Alert,
|
Alert,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const carModels = [
|
||||||
|
{ value: 'bmw_m3', label: 'BMW M3' },
|
||||||
|
{ value: 'bmw_m4', label: 'BMW M4' },
|
||||||
|
{ value: 'audi_rs3', label: 'Audi RS3' },
|
||||||
|
{ value: 'audi_rs4', label: 'Audi RS4' },
|
||||||
|
{ value: 'mercedes_c63', label: 'Mercedes-AMG C63' },
|
||||||
|
{ value: 'mercedes_e63', label: 'Mercedes-AMG E63' },
|
||||||
|
{ value: 'volkswagen_golf_r', label: 'Volkswagen Golf R' },
|
||||||
|
{ value: 'volkswagen_arteon_r', label: 'Volkswagen Arteon R' },
|
||||||
|
{ value: 'toyota_supra', label: 'Toyota Supra' },
|
||||||
|
{ value: 'nissan_gtr', label: 'Nissan GT-R' },
|
||||||
|
{ value: 'porsche_911', label: 'Porsche 911' },
|
||||||
|
{ value: 'porsche_cayman', label: 'Porsche Cayman' },
|
||||||
|
{ value: 'honda_civic_type_r', label: 'Honda Civic Type R' },
|
||||||
|
{ value: 'subaru_wrx_sti', label: 'Subaru WRX STI' },
|
||||||
|
{ value: 'mitsubishi_evo', label: 'Mitsubishi Lancer Evolution' },
|
||||||
|
{ value: 'other', label: 'Other' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create axios instance with default config
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:5000/api',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add request interceptor to add token to all requests
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const CustomerManagement = () => {
|
const CustomerManagement = () => {
|
||||||
const [customers, setCustomers] = useState([]);
|
const [customers, setCustomers] = useState([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -45,7 +90,7 @@ const CustomerManagement = () => {
|
|||||||
|
|
||||||
const fetchCustomers = async () => {
|
const fetchCustomers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('http://localhost:5000/api/customers');
|
const response = await api.get('/customers');
|
||||||
setCustomers(response.data);
|
setCustomers(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to fetch customers');
|
setError('Failed to fetch customers');
|
||||||
@@ -87,10 +132,10 @@ const CustomerManagement = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
if (selectedCustomer) {
|
if (selectedCustomer) {
|
||||||
await axios.put(`http://localhost:5000/api/customers/${selectedCustomer._id}`, formData);
|
await api.put(`/customers/${selectedCustomer._id}`, formData);
|
||||||
setSuccess('Customer updated successfully');
|
setSuccess('Customer updated successfully');
|
||||||
} else {
|
} else {
|
||||||
await axios.post('http://localhost:5000/api/customers', formData);
|
await api.post('/customers', formData);
|
||||||
setSuccess('Customer added successfully');
|
setSuccess('Customer added successfully');
|
||||||
}
|
}
|
||||||
fetchCustomers();
|
fetchCustomers();
|
||||||
@@ -103,7 +148,7 @@ const CustomerManagement = () => {
|
|||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
if (window.confirm('Are you sure you want to delete this customer?')) {
|
if (window.confirm('Are you sure you want to delete this customer?')) {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`http://localhost:5000/api/customers/${id}`);
|
await api.delete(`/customers/${id}`);
|
||||||
setSuccess('Customer deleted successfully');
|
setSuccess('Customer deleted successfully');
|
||||||
fetchCustomers();
|
fetchCustomers();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -218,15 +263,21 @@ const CustomerManagement = () => {
|
|||||||
multiline
|
multiline
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<FormControl fullWidth margin="normal" required>
|
||||||
fullWidth
|
<InputLabel>Car Model</InputLabel>
|
||||||
label="Car Model"
|
<Select
|
||||||
name="carModel"
|
name="carModel"
|
||||||
value={formData.carModel}
|
value={formData.carModel}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
margin="normal"
|
label="Car Model"
|
||||||
required
|
>
|
||||||
/>
|
{carModels.map((model) => (
|
||||||
|
<MenuItem key={model.value} value={model.value}>
|
||||||
|
{model.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Car Year"
|
label="Car Year"
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Box,
|
Box,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
|
|
||||||
import PeopleIcon from '@mui/icons-material/People';
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
import ChatIcon from '@mui/icons-material/Chat';
|
import BuildIcon from '@mui/icons-material/Build';
|
||||||
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -20,43 +20,38 @@ const Dashboard = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAdminStatus = async () => {
|
const checkAdminStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('http://localhost:5000/api/users/me', {
|
const response = await axios.get('http://localhost:5000/api/users/me');
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setIsAdmin(response.data.role === 'admin');
|
setIsAdmin(response.data.role === 'admin');
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('Error checking admin status:', error);
|
console.error('Error checking admin status:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkAdminStatus();
|
checkAdminStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
title: 'Customers',
|
title: 'Customers',
|
||||||
|
description: 'Manage customer information and car details',
|
||||||
icon: <PeopleIcon sx={{ fontSize: 40 }} />,
|
icon: <PeopleIcon sx={{ fontSize: 40 }} />,
|
||||||
description: 'View and manage customer information',
|
|
||||||
path: '/customers',
|
path: '/customers',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Car Modifications',
|
title: 'Car Modifications',
|
||||||
icon: <DirectionsCarIcon sx={{ fontSize: 40 }} />,
|
description: 'Browse available car modifications and upgrades',
|
||||||
description: 'Browse available car modifications',
|
icon: <BuildIcon sx={{ fontSize: 40 }} />,
|
||||||
path: '/modifications',
|
path: '/modifications',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Contact History',
|
title: 'Contact History',
|
||||||
icon: <ChatIcon sx={{ fontSize: 40 }} />,
|
description: 'View and manage customer interactions',
|
||||||
description: 'View customer interaction history',
|
icon: <HistoryIcon sx={{ fontSize: 40 }} />,
|
||||||
path: '/contacts',
|
path: '/contacts',
|
||||||
},
|
},
|
||||||
...(isAdmin ? [{
|
...(isAdmin ? [{
|
||||||
title: 'User Management',
|
title: 'User Management',
|
||||||
icon: <AdminPanelSettingsIcon sx={{ fontSize: 40 }} />,
|
|
||||||
description: 'Manage system users and permissions',
|
description: 'Manage system users and permissions',
|
||||||
|
icon: <AdminPanelSettingsIcon sx={{ fontSize: 40 }} />,
|
||||||
path: '/users',
|
path: '/users',
|
||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
@@ -68,7 +63,7 @@ const Dashboard = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<Grid item xs={12} md={4} key={item.title}>
|
<Grid item xs={12} sm={6} md={4} key={item.title}>
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
p: 3,
|
p: 3,
|
||||||
@@ -86,20 +81,20 @@ const Dashboard = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexGrow: 1,
|
alignItems: 'center',
|
||||||
|
mb: 2,
|
||||||
|
color: 'primary.main',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<Typography variant="h6" component="h2" sx={{ mt: 2, fontWeight: 'bold' }}>
|
</Box>
|
||||||
|
<Typography variant="h5" component="h2" gutterBottom>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, textAlign: 'center' }}>
|
<Typography variant="body1" color="text.secondary">
|
||||||
{item.description}
|
{item.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
|||||||
164
client/src/components/Register.js
Normal file
164
client/src/components/Register.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Alert,
|
||||||
|
} from '@mui/material';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const Register = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
});
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState('');
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setSuccess('');
|
||||||
|
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:5000/api/users/register', {
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
password: formData.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSuccess('Registration successful! Redirecting to login...');
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/login');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Registration failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component="main" maxWidth="xs">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 8,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
padding: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography component="h1" variant="h5" sx={{ mb: 3, fontWeight: 'bold' }}>
|
||||||
|
Register
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ width: '100%', mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<Alert severity="success" sx={{ width: '100%', mb: 2 }}>
|
||||||
|
{success}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="name"
|
||||||
|
label="Full Name"
|
||||||
|
name="name"
|
||||||
|
autoComplete="name"
|
||||||
|
autoFocus
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="email"
|
||||||
|
label="Email Address"
|
||||||
|
name="email"
|
||||||
|
autoComplete="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="confirmPassword"
|
||||||
|
label="Confirm Password"
|
||||||
|
type="password"
|
||||||
|
id="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="text"
|
||||||
|
onClick={() => navigate('/login')}
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
>
|
||||||
|
Already have an account? Login
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Register;
|
||||||
Reference in New Issue
Block a user