diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 03e827a..926486c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,7 +18,6 @@ import { import { GlassAppBar } from './components/ui'; import MenuIcon from '@mui/icons-material/Menu'; import NotificationsIcon from '@mui/icons-material/Notifications'; -import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined'; import { useTranslation } from 'react-i18next'; import api from './lib/api'; import Footer from './components/Footer'; @@ -62,16 +61,15 @@ const App: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const muiTheme = useMuiTheme(); - const { user, loadingUser, logout } = useAuth(); + const { user, loadingUser } = useAuth(); const { themeName, handleThemeChange } = useTheme(); const { notifications, markAllRead, markRead } = useNotifications(); const { anchor: mobileAnchor } = useDrawerAnchor(); - const [anchorEl, setAnchorEl] = useState(null); const [notifAnchorEl, setNotifAnchorEl] = useState(null); const [categories, setCategories] = useState<{ name: string; slug: string }[]>([]); const isMobile = useMediaQuery(muiTheme.breakpoints.down('md')); - const [drawerOpen, setDrawerOpen] = useState(!isMobile); + const [drawerOpen, setDrawerOpen] = useState(false); useEffect(() => { api @@ -90,7 +88,6 @@ const App: React.FC = () => { const onThemeChange = async (name: CatppuccinTheme) => { await handleThemeChange(name, !!user); - setAnchorEl(null); }; const handleNotificationClick = async (notif: Notification) => { @@ -216,85 +213,34 @@ const App: React.FC = () => { overflowX: 'hidden', }} > - - {mobileAnchor === 'left' && hamburger} + {isMobile && ( + + {mobileAnchor === 'left' && hamburger} - - - - Holy Flavors Archive - - - HFA - - - + - + + {!loadingUser && ( + <> + {user && notificationsMenu} - - {!loadingUser && ( - <> - {user && notificationsMenu} - - {user ? ( - - setAnchorEl(e.currentTarget)} - aria-label="account menu" + {!user && ( + - - )} - - )} + )} + + )} - {mobileAnchor === 'right' && hamburger} - - + {mobileAnchor === 'right' && hamburger} + + + )} {isMobile && ( { )} - {!isMobile && drawerOpen && ( + {!isMobile && ( theme.tokens.glass.border, + bgcolor: (theme) => + alpha(theme.palette.background.default, theme.palette.mode === 'light' ? 0.65 : 0.45), + backdropFilter: (theme) => theme.tokens.glass.blurStrong, + WebkitBackdropFilter: (theme) => theme.tokens.glass.blurStrong, zIndex: 1, }} > - + )} diff --git a/frontend/src/app/GlobalSearch.tsx b/frontend/src/app/GlobalSearch.tsx index b91201e..cb98780 100644 --- a/frontend/src/app/GlobalSearch.tsx +++ b/frontend/src/app/GlobalSearch.tsx @@ -14,7 +14,11 @@ import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useGlobalSearch } from '../hooks/useGlobalSearch'; -export const GlobalSearch = () => { +export interface GlobalSearchProps { + compact?: boolean; +} + +export const GlobalSearch = ({ compact = false }: GlobalSearchProps) => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); @@ -31,7 +35,7 @@ export const GlobalSearch = () => { }; return ( - + void; + showSearch?: boolean; + showNotifications?: boolean; } -export const NavSidebar = ({ categories, onNavigate }: NavSidebarProps) => { +export const NavSidebar = ({ + categories, + onNavigate, + showSearch = false, + showNotifications = false, +}: 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); const close = () => onNavigate?.(); 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 ( { - ) : ( + ) : null} + + {showSearch && ( - - - Holy Flavors Archive - - + )} @@ -139,6 +176,21 @@ export const NavSidebar = ({ categories, onNavigate }: NavSidebarProps) => { )} + {user && showNotifications && ( + + setNotifAnchorEl(e.currentTarget)}> + + + + + + + + + )} + setCatOpen(!catOpen)} selected={location.pathname.startsWith('/category/')} @@ -332,6 +384,88 @@ export const NavSidebar = ({ categories, onNavigate }: NavSidebarProps) => { )} + + {showNotifications && ( + setNotifAnchorEl(null)} + anchorOrigin={{ vertical: 'center', horizontal: 'right' }} + transformOrigin={{ vertical: 'center', horizontal: 'left' }} + elevation={3} + PaperProps={{ sx: { width: 320, maxHeight: 400, borderRadius: 1, ml: 1 } }} + > + + + {t('nav.notifications', { defaultValue: 'Notifications' })} + + {unreadCount > 0 && ( + + )} + + {notifications.length === 0 ? ( + + + {t('community.noNotifications', { defaultValue: 'No notifications yet' })} + + + ) : ( + notifications.map((n) => ( + 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', + }} + > + + {n.actor_username.charAt(0).toUpperCase()} + + + + {n.actor_username}{' '} + {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')} + + + {formatDate(n.created_at)} + + + + )) + )} + + )} ); };