mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed translation
This commit is contained in:
1 parent
e90b3b073c
commit
6a8e7d53f9
9 files changed
+399
-260
No files matched your search
+5
-98
@@ -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 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user