Merge branch 'main' into narl.io

This commit is contained in:
nvrl committed 2026-02-17 20:57:47 +01:00
commit 53687d3ca8
3 files changed
+142 -141

No files matched your search

+100 -101
View File
@@ -26,7 +26,8 @@ import {
ListItemAvatar,
ListSubheader,
Badge,
alpha
alpha,
useMediaQuery
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
@@ -82,6 +83,7 @@ const GlobalSearch = () => {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const isMobile = useMediaQuery('(max-width:900px)');
// Debounced search
useEffect(() => {
@@ -122,14 +124,14 @@ const GlobalSearch = () => {
};
return (
<Box sx={{ flexGrow: 1, mx: { xs: 1, sm: 2, md: 4 } }}>
<Box sx={{ flexGrow: 1, mx: isMobile ? 1 : 4 }}>
<Autocomplete
fullWidth
freeSolo
size="small"
loading={loading}
options={options}
filterOptions={(x) => x} // Disable local filtering as we do it on server
filterOptions={(x) => x}
getOptionLabel={(option) => {
if (typeof option === 'string') return option;
return option.name || '';
@@ -209,6 +211,7 @@ const App: React.FC = () => {
const [loadingUser, setLoadingUser] = useState(true);
const [drawerOpen, setDrawerOpen] = useState(false);
const [catOpen, setCatOpen] = useState(false);
const isMobile = useMediaQuery('(max-width:900px)');
const fetchNotifications = async () => {
try {
@@ -234,7 +237,6 @@ const App: React.FC = () => {
if (userRes.data.theme) setThemeName(userRes.data.theme);
if (userRes.data.language) i18n.changeLanguage(userRes.data.language);
// Also fetch following list for the mobile sidebar
try {
const followRes = await api.get('users/following_list/');
const followData = Array.isArray(followRes.data) ? followRes.data : (followRes.data.results || []);
@@ -257,11 +259,9 @@ const App: React.FC = () => {
};
fetchData();
// Poll for notifications every 60 seconds
const interval = setInterval(() => {
if (localStorage.getItem('token')) {
fetchNotifications();
// Also refresh user data for the count
api.get('users/me/').then(res => setUser(res.data)).catch(() => {});
}
}, 60000);
@@ -364,14 +364,6 @@ const App: React.FC = () => {
{user ? (
<>
<ListItem disablePadding>
<ListItemButton onClick={(e) => setNotifAnchorEl(e.currentTarget)}>
<ListItemText
primary="Notifications"
secondary={user.unread_notifications_count > 0 ? `${user.unread_notifications_count} unread` : null}
/>
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton component={Link} to={`/profile/${user.username}`} onClick={() => setDrawerOpen(false)}>
<ListItemText
@@ -436,6 +428,66 @@ const App: React.FC = () => {
</Box>
);
const notificationsMenu = (
<>
<IconButton color="inherit" onClick={(e) => setNotifAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
<Badge badgeContent={user?.unread_notifications_count || 0} color="error">
<NotificationsIcon />
</Badge>
</IconButton>
<Menu
anchorEl={notifAnchorEl}
open={Boolean(notifAnchorEl)}
onClose={() => setNotifAnchorEl(null)}
elevation={3}
sx={{ mt: 1 }}
PaperProps={{ sx: { width: 320, maxHeight: 400 } }}
>
<Box sx={{ p: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid', borderColor: 'divider' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>Notifications</Typography>
{(user?.unread_notifications_count || 0) > 0 && (
<Button size="small" onClick={handleMarkAllRead}>Mark all read</Button>
)}
</Box>
{notifications.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">No notifications yet</Typography>
</Box>
) : (
notifications.map(n => (
<MenuItem
key={n.id}
onClick={() => handleNotificationClick(n)}
sx={{
py: 1.5,
px: 2,
borderBottom: '1px solid',
borderColor: 'divider',
bgcolor: n.is_read ? 'transparent' : alpha(theme.palette.primary.main, 0.05),
whiteSpace: 'normal',
display: 'flex',
gap: 2,
alignItems: 'flex-start'
}}
>
<Avatar src={n.actor_avatar || undefined} sx={{ width: 32, height: 32 }}>
{n.actor_username.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" sx={{ lineHeight: 1.4 }}>
<strong>{n.actor_username}</strong> {n.notification_type === 'reply' ? 'replied' : 'mentioned you'} on <strong>{n.flavor_name}</strong>
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(n.created_at)}
</Typography>
</Box>
</MenuItem>
))
)}
</Menu>
</>
);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
@@ -453,14 +505,14 @@ const App: React.FC = () => {
}}
>
<Toolbar sx={{ minHeight: { xs: 56, sm: 64 }, px: { xs: 1, sm: 4, md: 6 } }}>
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
<Link to="/" style={{ color: 'inherit', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '8px' }}>
<Box component="img" src="/favicon.svg" sx={{ width: { xs: 32, sm: 24 }, height: { xs: 32, sm: 24 } }} />
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold', mr: 2 }}>
<Link to="/" style={{ color: 'inherit', textDecoration: 'none' }}>
<Box component="span" sx={{ display: { xs: 'none', sm: 'inline' } }}>Holy Flavors Archive</Box>
<Box component="span" sx={{ display: { xs: 'inline', sm: 'none' } }}>HFA</Box>
</Link>
</Typography>
<Box sx={{ display: { xs: 'none', md: 'flex' }, ml: 4 }}>
<Box sx={{ display: isMobile ? 'none' : 'flex', ml: 2 }}>
{user && (
<Button
color="inherit"
@@ -503,100 +555,47 @@ const App: React.FC = () => {
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{!loadingUser && (
user ? (
<Box sx={{ display: { xs: 'none', md: 'flex' }, alignItems: 'center' }}>
<IconButton color="inherit" onClick={(e) => setNotifAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
<Badge badgeContent={user.unread_notifications_count} color="error">
<NotificationsIcon />
</Badge>
</IconButton>
<Menu
anchorEl={notifAnchorEl}
open={Boolean(notifAnchorEl)}
onClose={() => setNotifAnchorEl(null)}
<>
{user && notificationsMenu}
{user ? (
<Box sx={{ display: isMobile ? 'none' : 'flex', ml: 1 }}>
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.currentTarget)}>
<Avatar src={user.avatar || undefined} sx={{ width: 36, height: 36, border: '2px solid', borderColor: 'primary.main' }}>
{user.username.charAt(0).toUpperCase()}
</Avatar>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
elevation={3}
sx={{ mt: 1 }}
PaperProps={{ sx: { width: 320, maxHeight: 400 } }}
>
<Box sx={{ p: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid', borderColor: 'divider' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>Notifications</Typography>
{user.unread_notifications_count > 0 && (
<Button size="small" onClick={handleMarkAllRead}>Mark all read</Button>
)}
</Box>
{notifications.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">No notifications yet</Typography>
</Box>
) : (
notifications.map(n => (
<MenuItem
key={n.id}
onClick={() => handleNotificationClick(n)}
sx={{
py: 1.5,
px: 2,
borderBottom: '1px solid',
borderColor: 'divider',
bgcolor: n.is_read ? 'transparent' : alpha(theme.palette.primary.main, 0.05),
whiteSpace: 'normal',
display: 'flex',
gap: 2,
alignItems: 'flex-start'
}}
>
<Avatar src={n.actor_avatar || undefined} sx={{ width: 32, height: 32 }}>
{n.actor_username.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" sx={{ lineHeight: 1.4 }}>
<strong>{n.actor_username}</strong> {n.notification_type === 'reply' ? 'replied to your review' : 'mentioned you'} on <strong>{n.flavor_name}</strong>
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(n.created_at)}
</Typography>
</Box>
</MenuItem>
))
)}
</Menu>
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
<Avatar src={user.avatar || undefined} sx={{ width: 36, height: 36, border: '2px solid', borderColor: 'primary.main' }}>
{user.username ? user.username.charAt(0).toUpperCase() : '?'}
</Avatar>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
elevation={3}
sx={{ mt: 1 }}
>
<MenuItem component={Link} to={`/profile/${user.username}`} onClick={() => setAnchorEl(null)}>{t('nav.profile')}</MenuItem>
<MenuItem component={Link} to="/dashboard" onClick={() => setAnchorEl(null)}>{t('nav.dashboard')}</MenuItem>
<MenuItem component={Link} to="/settings" onClick={() => setAnchorEl(null)}>{t('nav.settings')}</MenuItem>
<Divider />
<MenuItem onClick={handleLogout} sx={{ color: 'error.main' }}>{t('nav.logout')}</MenuItem>
</Menu>
</Box>
) : (
<Box sx={{ display: { xs: 'none', md: 'block' } }}>
<Button variant="contained" component={Link} to="/login" sx={{ borderRadius: 2 }}>Login</Button>
</Box>
)
>
<MenuItem component={Link} to={`/profile/${user.username}`} onClick={() => setAnchorEl(null)}>{t('nav.profile')}</MenuItem>
<MenuItem component={Link} to="/dashboard" onClick={() => setAnchorEl(null)}>{t('nav.dashboard')}</MenuItem>
<MenuItem component={Link} to="/settings" onClick={() => setAnchorEl(null)}>{t('nav.settings')}</MenuItem>
<Divider />
<MenuItem onClick={handleLogout} sx={{ color: 'error.main' }}>{t('nav.logout')}</MenuItem>
</Menu>
</Box>
) : (
<Box sx={{ display: isMobile ? 'none' : 'block' }}>
<Button variant="contained" component={Link} to="/login" sx={{ borderRadius: 2 }}>Login</Button>
</Box>
)}
</>
)}
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
{isMobile && (
<IconButton
color="inherit"
aria-label="open drawer"
onClick={() => setDrawerOpen(true)}
sx={{ ml: 1 }}
>
<MenuIcon />
</IconButton>
</Box>
)}
</Box>
</Toolbar>
</AppBar>
+12 -11
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Typography, Box, Container, Paper, Divider, Button } from '@mui/material';
import { Typography, Box, Container, Paper, Divider, Button, useMediaQuery } from '@mui/material';
import { Link } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTranslation } from 'react-i18next';
@@ -8,9 +8,10 @@ import { useTitle } from '../hooks/useTitle';
const About: React.FC = () => {
const { t } = useTranslation();
useTitle(t('nav.about'));
const isMobile = useMediaQuery('(max-width:600px)');
return (
<Container maxWidth="md" sx={{ py: 8 }}>
<Container maxWidth="md" sx={{ py: { xs: 4, md: 8 }, px: { xs: 2, sm: 3 } }}>
<Button
variant="outlined"
component={Link}
@@ -21,15 +22,15 @@ const About: React.FC = () => {
{t('common.backToArchive')}
</Button>
<Paper sx={{ p: { xs: 3, md: 6 }, borderRadius: 4 }}>
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold' }}>{t('about.title')}</Typography>
<Typography variant="body1" paragraph sx={{ fontSize: '1.1rem', lineHeight: 1.8 }}>
<Paper sx={{ p: { xs: 2.5, sm: 4, md: 6 }, borderRadius: 4, overflow: 'hidden' }}>
<Typography variant={isMobile ? "h4" : "h3"} gutterBottom sx={{ fontWeight: 'bold' }}>{t('about.title')}</Typography>
<Typography variant="body1" paragraph sx={{ fontSize: { xs: '1rem', md: '1.1rem' }, lineHeight: 1.8 }}>
{t('about.mission')}
</Typography>
<Box sx={{ my: 4 }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold' }}>{t('about.canDoTitle')}</Typography>
<Typography component="ul" sx={{ lineHeight: 2 }}>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ fontWeight: 'bold' }}>{t('about.canDoTitle')}</Typography>
<Typography component="ul" sx={{ lineHeight: 2, pl: 2 }}>
<li>{t('about.featureExplore')}</li>
<li>{t('about.featureRate')}</li>
<li>{t('about.featureCommunity')}</li>
@@ -41,9 +42,9 @@ const About: React.FC = () => {
{t('about.disclaimer')}
</Typography>
<Divider sx={{ my: 6 }} />
<Divider sx={{ my: { xs: 4, md: 6 } }} />
<Typography variant="h4" gutterBottom sx={{ fontWeight: 'bold' }}>{t('about.impressumTitle')}</Typography>
<Typography variant={isMobile ? "h5" : "h4"} gutterBottom sx={{ fontWeight: 'bold' }}>{t('about.impressumTitle')}</Typography>
<Typography variant="body2" sx={{ lineHeight: 2 }}>
<strong>{t('about.legalInfo')}</strong><br />
{import.meta.env.VITE_IMPRESSUM_NAME || '[Your Name or Project Name]'}<br />
@@ -74,12 +75,12 @@ const About: React.FC = () => {
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>{t('about.disclaimerTitle')}</Typography>
<Typography variant="caption" color="text.secondary" component="p" sx={{ mb: 2 }}>
<Typography variant="caption" color="text.secondary" component="p" sx={{ mb: 2, lineHeight: 1.6 }}>
<strong>Haftung für Inhalte:</strong> Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte
auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter
jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen.
</Typography>
<Typography variant="caption" color="text.secondary" component="p">
<Typography variant="caption" color="text.secondary" component="p" sx={{ lineHeight: 1.6 }}>
<strong>Urheberrecht:</strong> Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der
Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors.
+30 -29
View File
@@ -1,15 +1,16 @@
import React from 'react';
import { Typography, Box, Container, Paper, Button, ToggleButton, ToggleButtonGroup } from '@mui/material';
import React, { useState } from 'react';
import { Typography, Box, Container, Paper, Button, ToggleButton, ToggleButtonGroup, useMediaQuery } from '@mui/material';
import { Link } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTitle } from '../hooks/useTitle';
const Privacy: React.FC = () => {
useTitle('Privacy Policy / Datenschutzerklärung');
const [lang, setLang] = React.useState<'de' | 'en'>('de');
const [lang, setLang] = useState<'de' | 'en'>('de');
const isMobile = useMediaQuery('(max-width:600px)');
return (
<Container maxWidth="md" sx={{ py: 8 }}>
<Container maxWidth="md" sx={{ py: { xs: 4, md: 8 }, px: { xs: 2, sm: 3 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4, flexWrap: 'wrap', gap: 2 }}>
<Button
variant="outlined"
@@ -32,46 +33,46 @@ const Privacy: React.FC = () => {
</ToggleButtonGroup>
</Box>
<Paper sx={{ p: { xs: 3, md: 6 }, borderRadius: 4 }}>
<Paper sx={{ p: { xs: 2.5, sm: 4, md: 6 }, borderRadius: 4, overflow: 'hidden' }}>
{lang === 'de' ? (
<Box>
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold' }}>Datenschutzerklärung</Typography>
<Typography variant="body1" paragraph>
<Typography variant={isMobile ? "h4" : "h3"} gutterBottom sx={{ fontWeight: 'bold' }}>Datenschutzerklärung</Typography>
<Typography variant="body1" paragraph sx={{ lineHeight: 1.7 }}>
Der Schutz Ihrer persönlichen Daten ist uns ein wichtiges Anliegen. Nachfolgend informieren wir Sie
über die Verarbeitung Ihrer Daten auf unserer Webseite gemäß den Anforderungen der DSGVO.
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>1. Verantwortlicher</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>1. Verantwortlicher</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
Verantwortlich für die Datenverarbeitung auf dieser Webseite ist:<br />
{import.meta.env.VITE_IMPRESSUM_NAME || '[Ihr Name]'}<br />
{import.meta.env.VITE_IMPRESSUM_STREET || '[Ihre Straße]'}<br />
{import.meta.env.VITE_IMPRESSUM_CITY || '[Ihre Stadt]'}
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>2. Erfassung und Speicherung personenbezogener Daten</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>2. Erfassung und Speicherung personenbezogener Daten</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
<strong>Registrierung:</strong> Wenn Sie ein Konto erstellen, speichern wir Ihren Benutzernamen,
Ihre E-Mail-Adresse und Ihr Passwort (verschlüsselt). Diese Daten werden zur Bereitstellung der
Community-Funktionen (Bewertungen, Folgen von Nutzern) verwendet. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.
</Typography>
<Typography variant="body2" paragraph>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
<strong>Öffentliches Profil:</strong> Ihr Benutzername, Ihr Avatar und Ihre Bewertungen sind für
andere Nutzer öffentlich sichtbar. Sie können diese Daten jederzeit in den Einstellungen ändern oder löschen.
</Typography>
<Typography variant="body2" paragraph>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
<strong>Kontolöschung:</strong> Sie können Ihr Konto und alle damit verbundenen Daten jederzeit
in den Kontoeinstellungen dauerhaft löschen. Zur Sicherheit ist hierfür eine Verifizierung per E-Mail-Code erforderlich.
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>3. Datenübermittlung an Dritte</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>3. Datenübermittlung an Dritte</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
Wir geben Ihre Daten nicht ohne Ihre ausdrückliche Einwilligung an Dritte weiter, es sei denn,
dies ist zur Vertragserfüllung oder zur Erfüllung gesetzlicher Verpflichtungen erforderlich.
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>4. Ihre Rechte</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>4. Ihre Rechte</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Löschung (Art. 17 DSGVO)
und Einschränkung der Verarbeitung Ihrer Daten. Bitte kontaktieren Sie uns hierzu unter:
{import.meta.env.VITE_IMPRESSUM_EMAIL}
@@ -79,43 +80,43 @@ const Privacy: React.FC = () => {
</Box>
) : (
<Box>
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold' }}>Privacy Policy</Typography>
<Typography variant="body1" paragraph>
<Typography variant={isMobile ? "h4" : "h3"} gutterBottom sx={{ fontWeight: 'bold' }}>Privacy Policy</Typography>
<Typography variant="body1" paragraph sx={{ lineHeight: 1.7 }}>
Protecting your personal data is a top priority for us. Below we inform you about the processing
of your data on our website in accordance with the requirements of the GDPR.
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>1. Controller</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>1. Controller</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
The party responsible for data processing on this website is:<br />
{import.meta.env.VITE_IMPRESSUM_NAME || '[Your Name]'}<br />
{import.meta.env.VITE_IMPRESSUM_STREET || '[Your Street]'}<br />
{import.meta.env.VITE_IMPRESSUM_CITY || '[Your City]'}
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>2. Collection and Storage of Personal Data</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>2. Collection and Storage of Personal Data</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
<strong>Registration:</strong> When you create an account, we store your username, email address,
and password (encrypted). This data is used to provide community features (ratings, following users).
The legal basis is Art. 6 Para. 1 lit. b GDPR.
</Typography>
<Typography variant="body2" paragraph>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
<strong>Public Profile:</strong> Your username, avatar, and ratings are publicly visible to other users.
You can change or delete this data at any time in your settings.
</Typography>
<Typography variant="body2" paragraph>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
<strong>Account Deletion:</strong> You can permanently delete your account and all associated data
at any time in your account settings. For security reasons, this requires verification via an email code.
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>3. Data Transfer to Third Parties</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>3. Data Transfer to Third Parties</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
We do not pass your data on to third parties without your express consent, unless this is necessary
to fulfill a contract or a legal obligation.
</Typography>
<Typography variant="h5" gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>4. Your Rights</Typography>
<Typography variant="body2" paragraph>
<Typography variant={isMobile ? "h6" : "h5"} gutterBottom sx={{ mt: 4, fontWeight: 'bold' }}>4. Your Rights</Typography>
<Typography variant="body2" paragraph sx={{ lineHeight: 1.7 }}>
You have the right to information (Art. 15 GDPR), correction (Art. 16 GDPR), deletion (Art. 17 GDPR),
and restriction of the processing of your data. Please contact us at:
{import.meta.env.VITE_IMPRESSUM_EMAIL}