fixed translation

This commit is contained in:
nvrl committed 2026-06-15 19:58:58 +02:00
1 parent e90b3b073c
commit 6a8e7d53f9
9 files changed
+399 -260

No files matched your search

+5 -98
View File
@@ -1,13 +1,10 @@
import React, { Suspense, lazy, useEffect, useState } from 'react';
import { Routes, Route, Link, useNavigate, Navigate } from 'react-router-dom';
import { Routes, Route, Link, Navigate } from 'react-router-dom';
import {
Typography,
Button,
Box,
IconButton,
Avatar,
Menu,
MenuItem,
SwipeableDrawer,
CircularProgress,
Badge,
@@ -22,15 +19,13 @@ import { useTranslation } from 'react-i18next';
import api from './lib/api';
import Footer from './components/Footer';
import CookieBanner from './components/CookieBanner';
import { formatDate } from './utils/date';
import { GlobalSearch } from './app/GlobalSearch';
import { useAuth } from './hooks/useAuth';
import { useTheme } from './hooks/useTheme';
import { useNotifications } from './hooks/useNotifications';
import { useDrawerAnchor } from './hooks/useDrawerAnchor';
import { NavSidebar, NAV_SIDEBAR_WIDTH } from './components/layout/NavSidebar';
import { NotificationMenu } from './components/layout/NotificationMenu';
import { RequireSuperuser } from './components/auth/RequireSuperuser';
import type { Notification } from './contexts/NotificationContext';
import type { CatppuccinTheme } from './theme';
const MainPage = lazy(() => import('./pages/MainPage'));
@@ -60,11 +55,9 @@ const AdminReplyDetail = lazy(() => import('./pages/AdminReplyDetail'));
const App: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const muiTheme = useMuiTheme();
const { user, loadingUser } = useAuth();
const { themeName, handleThemeChange } = useTheme();
const { notifications, markAllRead, markRead } = useNotifications();
const { anchor: mobileAnchor } = useDrawerAnchor();
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
@@ -91,20 +84,6 @@ const App: React.FC = () => {
await handleThemeChange(name, !!user);
};
const handleNotificationClick = async (notif: Notification) => {
if (!notif.is_read) await markRead(notif.id);
setNotifAnchorEl(null);
if (notif.notification_type.startsWith('ticket')) {
navigate(user?.is_superuser ? '/admin-panel/tickets' : '/support');
} else if (notif.notification_type === 'profile_comment') {
navigate(`/profile/${user?.username}`);
} else if (notif.notification_type === 'follow') {
navigate(`/profile/${notif.actor_username}`);
} else if (notif.flavor_id) {
window.location.href = `/flavor/${notif.flavor_id}`;
}
};
const hamburger = (
<IconButton
color="inherit"
@@ -123,84 +102,12 @@ const App: React.FC = () => {
<NotificationsIcon />
</Badge>
</IconButton>
<Menu
<NotificationMenu
anchorEl={notifAnchorEl}
open={Boolean(notifAnchorEl)}
onClose={() => setNotifAnchorEl(null)}
elevation={3}
sx={{ mt: 1 }}
PaperProps={{ sx: { width: 320, maxHeight: 400, borderRadius: 1 } }}
>
<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={markAllRead}>
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(muiTheme.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 on ${n.flavor_name}`
: n.notification_type === 'mention'
? `mentioned you on ${n.flavor_name}`
: n.notification_type === 'follow'
? t('community.notifFollow')
: n.notification_type === 'profile_comment'
? `left a message on your guestbook`
: n.notification_type === 'ticket_new'
? t('community.notifTicketNew')
: user?.is_superuser
? t('community.notifTicketReplyAdmin')
: t('community.notifTicketReply')}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(n.created_at)}
</Typography>
</Box>
</MenuItem>
))
)}
</Menu>
menuSx={{ mt: 1 }}
/>
</>
);
+6 -101
View File
@@ -1,10 +1,9 @@
import { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import {
Avatar,
Badge,
Box,
Button,
Collapse,
Divider,
List,
@@ -13,11 +12,7 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Typography,
alpha,
useTheme,
} from '@mui/material';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
@@ -35,10 +30,8 @@ import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined
import PeopleOutlineOutlinedIcon from '@mui/icons-material/PeopleOutlineOutlined';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../hooks/useAuth';
import { useNotifications } from '../../hooks/useNotifications';
import { GlobalSearch } from '../../app/GlobalSearch';
import { formatDate } from '../../utils/date';
import type { Notification } from '../../contexts/NotificationContext';
import { NotificationMenu } from './NotificationMenu';
export const NAV_SIDEBAR_WIDTH = 280;
@@ -57,10 +50,7 @@ export const NavSidebar = ({
}: NavSidebarProps) => {
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const theme = useTheme();
const { user, following, logout } = useAuth();
const { notifications, markAllRead, markRead } = useNotifications();
const [catOpen, setCatOpen] = useState(false);
const [followingOpen, setFollowingOpen] = useState(false);
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
@@ -69,21 +59,6 @@ export const NavSidebar = ({
const isActive = (path: string) =>
path === '/' ? location.pathname === '/' : location.pathname.startsWith(path);
const handleNotificationClick = async (notif: Notification) => {
if (!notif.is_read) await markRead(notif.id);
setNotifAnchorEl(null);
close();
if (notif.notification_type.startsWith('ticket')) {
navigate(user?.is_superuser ? '/admin-panel/tickets' : '/support');
} else if (notif.notification_type === 'profile_comment') {
navigate(`/profile/${user?.username}`);
} else if (notif.notification_type === 'follow') {
navigate(`/profile/${notif.actor_username}`);
} else if (notif.flavor_id) {
window.location.href = `/flavor/${notif.flavor_id}`;
}
};
const unreadCount = user?.unread_notifications_count || 0;
return (
@@ -384,85 +359,15 @@ export const NavSidebar = ({
)}
{showNotifications && (
<Menu
<NotificationMenu
anchorEl={notifAnchorEl}
open={Boolean(notifAnchorEl)}
onClose={() => setNotifAnchorEl(null)}
onItemNavigate={close}
anchorOrigin={{ vertical: 'center', horizontal: 'right' }}
transformOrigin={{ vertical: 'center', horizontal: 'left' }}
elevation={3}
PaperProps={{ sx: { width: 320, maxHeight: 400, borderRadius: 1, ml: 1 } }}
>
<Box
sx={{
p: 1.5,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>
{t('nav.notifications', { defaultValue: 'Notifications' })}
</Typography>
{unreadCount > 0 && (
<Button size="small" onClick={markAllRead}>
{t('community.markAllRead', { defaultValue: 'Mark all read' })}
</Button>
)}
</Box>
{notifications.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t('community.noNotifications', { defaultValue: '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 on ${n.flavor_name}`
: n.notification_type === 'mention'
? `mentioned you on ${n.flavor_name}`
: n.notification_type === 'follow'
? t('community.notifFollow')
: n.notification_type === 'profile_comment'
? `left a message on your guestbook`
: n.notification_type === 'ticket_new'
? t('community.notifTicketNew')
: user?.is_superuser
? t('community.notifTicketReplyAdmin')
: t('community.notifTicketReply')}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(n.created_at)}
</Typography>
</Box>
</MenuItem>
))
)}
</Menu>
paperSx={{ ml: 1 }}
/>
)}
</Box>
);
@@ -0,0 +1,151 @@
import {
Avatar,
Box,
Button,
Menu,
MenuItem,
Typography,
alpha,
useTheme,
} from '@mui/material';
import type { MenuProps, SxProps, Theme } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../hooks/useAuth';
import { useNotifications } from '../../hooks/useNotifications';
import { useNotificationClick } from '../../hooks/useNotificationClick';
import { formatDate } from '../../utils/date';
import type { Notification } from '../../contexts/NotificationContext';
interface NotificationMenuProps {
anchorEl: HTMLElement | null;
open: boolean;
onClose: () => void;
/** Extra cleanup after a notification is clicked (e.g. close a drawer). */
onItemNavigate?: () => void;
anchorOrigin?: MenuProps['anchorOrigin'];
transformOrigin?: MenuProps['transformOrigin'];
menuSx?: SxProps<Theme>;
paperSx?: SxProps<Theme>;
}
/** Human-readable line for a notification, fully i18n'd. */
const NotifText = ({ n }: { n: Notification }) => {
const { t } = useTranslation();
const { user } = useAuth();
switch (n.notification_type) {
case 'reply':
return <>{t('community.notifReplyOn', { flavor: n.flavor_name })}</>;
case 'mention':
return <>{t('community.notifMentionOn', { flavor: n.flavor_name })}</>;
case 'follow':
return <>{t('community.notifFollow')}</>;
case 'profile_comment':
return <>{t('community.notifProfileComment')}</>;
case 'ticket_new':
return <>{t('community.notifTicketNew')}</>;
default:
return (
<>
{user?.is_superuser
? t('community.notifTicketReplyAdmin')
: t('community.notifTicketReply')}
</>
);
}
};
/**
* The notification dropdown shared by the mobile AppBar and the desktop
* sidebar. Placement differs per consumer, so anchor/transform origins and
* paper styling are props.
*/
export const NotificationMenu = ({
anchorEl,
open,
onClose,
onItemNavigate,
anchorOrigin,
transformOrigin,
menuSx,
paperSx,
}: NotificationMenuProps) => {
const { t } = useTranslation();
const theme = useTheme();
const { user } = useAuth();
const { notifications, markAllRead } = useNotifications();
const handleClick = useNotificationClick();
const unreadCount = user?.unread_notifications_count || 0;
return (
<Menu
anchorEl={anchorEl}
open={open}
onClose={onClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
elevation={3}
sx={menuSx}
slotProps={{ paper: { sx: { width: 320, maxHeight: 400, borderRadius: 1, ...paperSx } } }}
>
<Box
sx={{
p: 1.5,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>
{t('community.notifications')}
</Typography>
{unreadCount > 0 && (
<Button size="small" onClick={markAllRead}>
{t('community.markAllRead')}
</Button>
)}
</Box>
{notifications.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t('community.noNotifications')}
</Typography>
</Box>
) : (
notifications.map((n) => (
<MenuItem
key={n.id}
onClick={() => {
onClose();
handleClick(n, onItemNavigate);
}}
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> <NotifText n={n} />
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(n.created_at)}
</Typography>
</Box>
</MenuItem>
))
)}
</Menu>
);
};
@@ -0,0 +1,33 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from './useAuth';
import { useNotifications } from './useNotifications';
import type { Notification } from '../contexts/NotificationContext';
/**
* Shared notification-click behaviour for the AppBar and sidebar menus:
* marks the notification read, runs an optional cleanup (close menu/drawer),
* then routes via the SPA router (no full page reload).
*/
export const useNotificationClick = () => {
const navigate = useNavigate();
const { user } = useAuth();
const { markRead } = useNotifications();
return useCallback(
async (notif: Notification, onDone?: () => void) => {
if (!notif.is_read) await markRead(notif.id);
onDone?.();
if (notif.notification_type.startsWith('ticket')) {
navigate(user?.is_superuser ? '/admin-panel/tickets' : '/support');
} else if (notif.notification_type === 'profile_comment') {
navigate(`/profile/${user?.username}`);
} else if (notif.notification_type === 'follow') {
navigate(`/profile/${notif.actor_username}`);
} else if (notif.flavor_id) {
navigate(`/flavor/${notif.flavor_id}`);
}
},
[navigate, user, markRead],
);
};
+8
View File
@@ -182,12 +182,16 @@ const resources = {
topRated: "Circle's Top Rated",
notifications: 'Recent Notifications',
notifReply: 'replied',
notifReplyOn: 'replied to your review on {{flavor}}',
notifMention: 'mentioned you',
notifMentionOn: 'mentioned you on {{flavor}}',
notifFollow: 'started following you',
notifProfileComment: 'left a message on your guestbook',
notifTicketNew: 'opened a new support ticket',
notifTicketReply: 'replied to your support ticket',
notifTicketReplyAdmin: 'replied to a support ticket',
noNotifications: 'No notifications.',
markAllRead: 'Mark all read',
searchFriends: 'Search friends...',
writeReply: 'Add a reply...',
rated: 'rated',
@@ -488,12 +492,16 @@ const resources = {
topRated: 'Top-Bewertungen deines Kreises',
notifications: 'Neueste Benachrichtigungen',
notifReply: 'hat geantwortet',
notifReplyOn: 'hat auf deine Bewertung zu {{flavor}} geantwortet',
notifMention: 'hat dich erwähnt',
notifMentionOn: 'hat dich bei {{flavor}} erwähnt',
notifFollow: 'folgt dir jetzt',
notifProfileComment: 'hat dir einen Eintrag ins Gästebuch hinterlassen',
notifTicketNew: 'hat ein neues Support-Ticket eröffnet',
notifTicketReply: 'hat auf dein Support-Ticket geantwortet',
notifTicketReplyAdmin: 'hat auf ein Support-Ticket geantwortet',
noNotifications: 'Keine Benachrichtigungen.',
markAllRead: 'Alle als gelesen markieren',
searchFriends: 'Freunde suchen...',
writeReply: 'Eine Antwort hinzufügen...',
rated: 'bewertete',