mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
desktop side/top bar redesign + mobile top bar redesign
This commit is contained in:
1 parent
683354ec93
commit
fdb067cf1e
3 files changed
+181
-94
No files matched your search
+33
-84
@@ -18,7 +18,6 @@ import {
|
|||||||
import { GlassAppBar } from './components/ui';
|
import { GlassAppBar } from './components/ui';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import api from './lib/api';
|
import api from './lib/api';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
@@ -62,16 +61,15 @@ const App: React.FC = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const muiTheme = useMuiTheme();
|
const muiTheme = useMuiTheme();
|
||||||
const { user, loadingUser, logout } = useAuth();
|
const { user, loadingUser } = useAuth();
|
||||||
const { themeName, handleThemeChange } = useTheme();
|
const { themeName, handleThemeChange } = useTheme();
|
||||||
const { notifications, markAllRead, markRead } = useNotifications();
|
const { notifications, markAllRead, markRead } = useNotifications();
|
||||||
const { anchor: mobileAnchor } = useDrawerAnchor();
|
const { anchor: mobileAnchor } = useDrawerAnchor();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
||||||
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
|
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const [categories, setCategories] = useState<{ name: string; slug: string }[]>([]);
|
const [categories, setCategories] = useState<{ name: string; slug: string }[]>([]);
|
||||||
const isMobile = useMediaQuery(muiTheme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(muiTheme.breakpoints.down('md'));
|
||||||
const [drawerOpen, setDrawerOpen] = useState(!isMobile);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api
|
api
|
||||||
@@ -90,7 +88,6 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
const onThemeChange = async (name: CatppuccinTheme) => {
|
const onThemeChange = async (name: CatppuccinTheme) => {
|
||||||
await handleThemeChange(name, !!user);
|
await handleThemeChange(name, !!user);
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNotificationClick = async (notif: Notification) => {
|
const handleNotificationClick = async (notif: Notification) => {
|
||||||
@@ -216,85 +213,34 @@ const App: React.FC = () => {
|
|||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GlassAppBar>
|
{isMobile && (
|
||||||
{mobileAnchor === 'left' && hamburger}
|
<GlassAppBar>
|
||||||
|
{mobileAnchor === 'left' && hamburger}
|
||||||
|
|
||||||
<Typography
|
<GlobalSearch />
|
||||||
variant="h6"
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 'bold',
|
|
||||||
mr: { xs: 0, sm: 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>
|
|
||||||
|
|
||||||
<GlobalSearch />
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
{!loadingUser && (
|
||||||
|
<>
|
||||||
|
{user && notificationsMenu}
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
{!user && (
|
||||||
{!loadingUser && (
|
<Button
|
||||||
<>
|
variant="contained"
|
||||||
{user && notificationsMenu}
|
component={Link}
|
||||||
|
to="/login"
|
||||||
{user ? (
|
sx={{ borderRadius: 2, ml: 1 }}
|
||||||
<Box sx={{ display: isMobile ? 'none' : 'flex', ml: 1 }}>
|
|
||||||
<IconButton
|
|
||||||
color="inherit"
|
|
||||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
|
||||||
aria-label="account menu"
|
|
||||||
>
|
>
|
||||||
<Avatar
|
{t('nav.login')}
|
||||||
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 }}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
logout();
|
|
||||||
}}
|
|
||||||
sx={{ color: 'error.main', gap: 1.5 }}
|
|
||||||
>
|
|
||||||
<LogoutOutlinedIcon fontSize="small" />
|
|
||||||
{t('nav.logout')}
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box sx={{ display: isMobile ? 'none' : 'block' }}>
|
|
||||||
<Button variant="contained" component={Link} to="/login" sx={{ borderRadius: 2 }}>
|
|
||||||
Login
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{mobileAnchor === 'right' && hamburger}
|
{mobileAnchor === 'right' && hamburger}
|
||||||
</Box>
|
</Box>
|
||||||
</GlassAppBar>
|
</GlassAppBar>
|
||||||
|
)}
|
||||||
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<SwipeableDrawer
|
<SwipeableDrawer
|
||||||
@@ -310,23 +256,26 @@ const App: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexGrow: 1, minHeight: 0 }}>
|
<Box sx={{ display: 'flex', flexGrow: 1, minHeight: 0 }}>
|
||||||
{!isMobile && drawerOpen && (
|
{!isMobile && (
|
||||||
<Box
|
<Box
|
||||||
component="aside"
|
component="aside"
|
||||||
sx={{
|
sx={{
|
||||||
width: NAV_SIDEBAR_WIDTH,
|
width: NAV_SIDEBAR_WIDTH,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: { xs: 56, sm: 64 },
|
top: 0,
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
height: { xs: 'calc(100vh - 56px)', sm: 'calc(100vh - 64px)' },
|
height: '100vh',
|
||||||
borderRight: '1px solid',
|
borderRight: '1px solid',
|
||||||
borderColor: 'divider',
|
borderColor: (theme) => theme.tokens.glass.border,
|
||||||
bgcolor: 'background.paper',
|
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,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NavSidebar categories={categories} />
|
<NavSidebar categories={categories} showSearch showNotifications />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useGlobalSearch } from '../hooks/useGlobalSearch';
|
import { useGlobalSearch } from '../hooks/useGlobalSearch';
|
||||||
|
|
||||||
export const GlobalSearch = () => {
|
export interface GlobalSearchProps {
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalSearch = ({ compact = false }: GlobalSearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -31,7 +35,7 @@ export const GlobalSearch = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1, mx: isMobile ? 1 : 4 }}>
|
<Box sx={{ flexGrow: 1, mx: compact ? 0 : isMobile ? 1 : 4 }}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
fullWidth
|
fullWidth
|
||||||
freeSolo
|
freeSolo
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Collapse,
|
Collapse,
|
||||||
Divider,
|
Divider,
|
||||||
List,
|
List,
|
||||||
@@ -11,7 +13,11 @@ import {
|
|||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
Typography,
|
Typography,
|
||||||
|
alpha,
|
||||||
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||||
@@ -25,28 +31,61 @@ import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined';
|
|||||||
import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
|
import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
|
||||||
import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined';
|
import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined';
|
||||||
import LoginOutlinedIcon from '@mui/icons-material/LoginOutlined';
|
import LoginOutlinedIcon from '@mui/icons-material/LoginOutlined';
|
||||||
|
import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined';
|
||||||
import PeopleOutlineOutlinedIcon from '@mui/icons-material/PeopleOutlineOutlined';
|
import PeopleOutlineOutlinedIcon from '@mui/icons-material/PeopleOutlineOutlined';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
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';
|
||||||
|
|
||||||
export const NAV_SIDEBAR_WIDTH = 280;
|
export const NAV_SIDEBAR_WIDTH = 280;
|
||||||
|
|
||||||
export interface NavSidebarProps {
|
export interface NavSidebarProps {
|
||||||
categories: { name: string; slug: string }[];
|
categories: { name: string; slug: string }[];
|
||||||
onNavigate?: () => void;
|
onNavigate?: () => 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 { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const theme = useTheme();
|
||||||
const { user, following, logout } = useAuth();
|
const { user, following, logout } = useAuth();
|
||||||
|
const { notifications, markAllRead, markRead } = useNotifications();
|
||||||
const [catOpen, setCatOpen] = useState(false);
|
const [catOpen, setCatOpen] = useState(false);
|
||||||
const [followingOpen, setFollowingOpen] = useState(false);
|
const [followingOpen, setFollowingOpen] = useState(false);
|
||||||
|
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
const close = () => onNavigate?.();
|
const close = () => onNavigate?.();
|
||||||
const isActive = (path: string) =>
|
const isActive = (path: string) =>
|
||||||
path === '/' ? location.pathname === '/' : location.pathname.startsWith(path);
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -103,13 +142,11 @@ export const NavSidebar = ({ categories, onNavigate }: NavSidebarProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : null}
|
||||||
|
|
||||||
|
{showSearch && (
|
||||||
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
<GlobalSearch compact />
|
||||||
<Link to="/" onClick={close} style={{ color: 'inherit', textDecoration: 'none' }}>
|
|
||||||
Holy Flavors Archive
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -139,6 +176,21 @@ export const NavSidebar = ({ categories, onNavigate }: NavSidebarProps) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{user && showNotifications && (
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemButton onClick={(e) => setNotifAnchorEl(e.currentTarget)}>
|
||||||
|
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||||
|
<Badge badgeContent={unreadCount} color="error">
|
||||||
|
<NotificationsOutlinedIcon />
|
||||||
|
</Badge>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={t('nav.notifications', { defaultValue: 'Notifications' })}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
onClick={() => setCatOpen(!catOpen)}
|
onClick={() => setCatOpen(!catOpen)}
|
||||||
selected={location.pathname.startsWith('/category/')}
|
selected={location.pathname.startsWith('/category/')}
|
||||||
@@ -332,6 +384,88 @@ export const NavSidebar = ({ categories, onNavigate }: NavSidebarProps) => {
|
|||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showNotifications && (
|
||||||
|
<Menu
|
||||||
|
anchorEl={notifAnchorEl}
|
||||||
|
open={Boolean(notifAnchorEl)}
|
||||||
|
onClose={() => setNotifAnchorEl(null)}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user