This commit is contained in:
2026-04-17 09:57:01 +02:00
parent dd1d78e6ad
commit c3c05e8af4
5 changed files with 176 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ import EditOrganizationPage from "./pages/Organization/EditOrganizationPage.jsx"
import CompanyPage from "./pages/Company/CompanyPage.jsx";
import EditCompanyPage from "./pages/Company/EditCompanyPage.jsx";
import CalenderPostPage from "./pages/Calender/CalenderPostPage.jsx";
import ProfilePage from "./pages/Auth/ProfilePage.jsx";
const theme = createTheme({
palette: {
@@ -76,6 +77,14 @@ export default function App() {
</PrivateRoute>
}
/>
<Route
path="profile"
element={
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER', 'SITE_OWNER']}>
<ProfilePage />
</PrivateRoute>
}
/>
<Route
path="calenderPosts"
element={

View File

@@ -1,7 +1,7 @@
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
import {
Box, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText,
AppBar, Toolbar, Typography, Divider, Avatar, Tooltip,
AppBar, Toolbar, Typography, Divider, Avatar, Tooltip, IconButton
} from '@mui/material';
import PeopleIcon from '@mui/icons-material/People';
import ArticleIcon from '@mui/icons-material/Article';
@@ -169,8 +169,10 @@ export default function Layout() {
<Typography variant="h6" color="text.primary" sx={{ flexGrow: 1 }}>
{getActiveLabel(location.pathname)}
</Typography>
<Tooltip title="Account">
<Avatar sx={{ width: 32, height: 32, bgcolor: 'primary.main', fontSize: 14 }}>A</Avatar>
<Tooltip title="Profil">
<IconButton onClick={() => navigate('/profile')} sx={{ p: 0 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: 'primary.main', fontSize: 14 }}>A</Avatar>
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>

View File

@@ -0,0 +1,156 @@
import { useState } from 'react';
import {
Box, Card, CardContent, TextField, Button, Typography,
Alert, CircularProgress, Divider,
} from '@mui/material';
import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import axiosInstance from '../../api/axiosInstance';
export default function ProfilePage() {
const [nickname, setNickname] = useState('');
const [nicknameLoading, setNicknameLoading] = useState(false);
const [nicknameError, setNicknameError] = useState('');
const [nicknameSuccess, setNicknameSuccess] = useState('');
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordLoading, setPasswordLoading] = useState(false);
const [passwordError, setPasswordError] = useState('');
const [passwordSuccess, setPasswordSuccess] = useState('');
const handleNicknameSave = async (e) => {
e.preventDefault();
setNicknameError('');
setNicknameSuccess('');
if (!nickname.trim()) {
setNicknameError('Bitte einen Nickname eingeben.');
return;
}
setNicknameLoading(true);
try {
await axiosInstance.patch('/users/nickname', { nickname });
setNicknameSuccess('Nickname erfolgreich gespeichert.');
} catch (err) {
setNicknameError(err.response?.data?.error ?? 'Fehler beim Speichern.');
} finally {
setNicknameLoading(false);
}
};
const handlePasswordSave = async (e) => {
e.preventDefault();
setPasswordError('');
setPasswordSuccess('');
if (!currentPassword || !newPassword || !confirmPassword) {
setPasswordError('Bitte alle Felder ausfüllen.');
return;
}
if (newPassword !== confirmPassword) {
setPasswordError('Neue Passwörter stimmen nicht überein.');
return;
}
if (newPassword.length < 8) {
setPasswordError('Neues Passwort muss mindestens 8 Zeichen haben.');
return;
}
setPasswordLoading(true);
try {
await axiosInstance.patch('/users/password', { currentPassword, newPassword });
setPasswordSuccess('Passwort erfolgreich geändert.');
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
} catch (err) {
setPasswordError(err.response?.data?.error ?? 'Fehler beim Ändern des Passworts.');
} finally {
setPasswordLoading(false);
}
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, maxWidth: 480 }}>
{/* ── Karte: Nickname ── */}
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<AccountCircleOutlinedIcon color="primary" />
<Typography variant="h6" fontWeight={600}>Nickname ändern</Typography>
</Box>
{nicknameError && <Alert severity="error" sx={{ mb: 2 }}>{nicknameError}</Alert>}
{nicknameSuccess && <Alert severity="success" sx={{ mb: 2 }}>{nicknameSuccess}</Alert>}
<Box component="form" onSubmit={handleNicknameSave} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Neuer Nickname"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
fullWidth
autoComplete="off"
/>
<Button
type="submit"
variant="contained"
fullWidth
disabled={nicknameLoading}
>
{nicknameLoading ? <CircularProgress size={22} color="inherit" /> : 'Speichern'}
</Button>
</Box>
</CardContent>
</Card>
{/* ── Karte: Passwort ── */}
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<LockOutlinedIcon color="primary" />
<Typography variant="h6" fontWeight={600}>Passwort ändern</Typography>
</Box>
{passwordError && <Alert severity="error" sx={{ mb: 2 }}>{passwordError}</Alert>}
{passwordSuccess && <Alert severity="success" sx={{ mb: 2 }}>{passwordSuccess}</Alert>}
<Box component="form" onSubmit={handlePasswordSave} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Aktuelles Passwort"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
fullWidth
/>
<Divider />
<TextField
label="Neues Passwort"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
fullWidth
helperText="Mindestens 8 Zeichen"
/>
<TextField
label="Neues Passwort bestätigen"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
fullWidth
/>
<Button
type="submit"
variant="contained"
fullWidth
disabled={passwordLoading}
>
{passwordLoading ? <CircularProgress size={22} color="inherit" /> : 'Passwort ändern'}
</Button>
</Box>
</CardContent>
</Card>
</Box>
);
}

View File

@@ -183,6 +183,9 @@ export default function EditCompanyPage() {
<TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth />
<TextField label="Email Adresse" name="email" value={form.email} onChange={handleChange} required fullWidth />
<TextField label="Adresse" name="address" value={form.address} onChange={handleChange} required fullWidth />
<Alert severity="info" sx={{ maxWidth: 560, mb: 3 }}>
Website muss mit "https://" anfangen
</Alert>
<TextField label="Webseite" name="website" value={form.website} onChange={handleChange} required fullWidth />
<TextField label="Kontaktperson" name="contactPerson" value={form.contactPerson} onChange={handleChange} required fullWidth />
<FormControlLabel

View File

@@ -185,6 +185,9 @@ export default function EditOrganizationPage() {
<TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth />
<TextField label="Email Adresse" name="email" value={form.email} onChange={handleChange} required fullWidth />
<TextField label="Adresse" name="address" value={form.address} onChange={handleChange} required fullWidth />
<Alert severity="info" sx={{ mb: 2 }}>
Website muss mit "https://" anfangen
</Alert>
<TextField label="Webseite" name="website" value={form.website} onChange={handleChange} required fullWidth />
<TextField label="Kontaktperson" name="contactPerson" value={form.contactPerson} onChange={handleChange} required fullWidth />
<FormControlLabel