mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
desktop redesign
This commit is contained in:
1 parent
12b973caac
commit
30a0ccda1b
6 files changed
+505
-28
No files matched your search
+7
-28
@@ -8,7 +8,6 @@ import {
|
||||
SwipeableDrawer,
|
||||
CircularProgress,
|
||||
Badge,
|
||||
alpha,
|
||||
useMediaQuery,
|
||||
useTheme as useMuiTheme,
|
||||
} from '@mui/material';
|
||||
@@ -22,8 +21,10 @@ import { GlobalSearch } from './app/GlobalSearch';
|
||||
import { useAuth } from './hooks/useAuth';
|
||||
import { useTheme } from './hooks/useTheme';
|
||||
import { useDrawerAnchor } from './hooks/useDrawerAnchor';
|
||||
import { NavSidebar, NAV_SIDEBAR_WIDTH } from './components/layout/NavSidebar';
|
||||
import { NavSidebar } from './components/layout/NavSidebar';
|
||||
import { BrandMark } from './components/layout/BrandMark';
|
||||
import { DesktopTopBar } from './components/layout/DesktopTopBar';
|
||||
import { DesktopCategoryBar } from './components/layout/DesktopCategoryBar';
|
||||
import { useCategories } from './api/queries/useCategories';
|
||||
import { NotificationMenu } from './components/layout/NotificationMenu';
|
||||
import { RequireSuperuser } from './components/auth/RequireSuperuser';
|
||||
@@ -144,6 +145,8 @@ const App: React.FC = () => {
|
||||
</GlassAppBar>
|
||||
)}
|
||||
|
||||
{!isMobile && <DesktopTopBar />}
|
||||
|
||||
{isMobile && (
|
||||
<SwipeableDrawer
|
||||
anchor={mobileAnchor}
|
||||
@@ -167,33 +170,9 @@ const App: React.FC = () => {
|
||||
</SwipeableDrawer>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, minHeight: 0 }}>
|
||||
{!isMobile && (
|
||||
<Box
|
||||
component="aside"
|
||||
sx={{
|
||||
width: NAV_SIDEBAR_WIDTH,
|
||||
flexShrink: 0,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
alignSelf: 'flex-start',
|
||||
height: '100vh',
|
||||
borderRight: '1px solid',
|
||||
borderColor: (theme) => 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,
|
||||
}}
|
||||
>
|
||||
<NavSidebar categories={categories} showSearch showNotifications />
|
||||
</Box>
|
||||
)}
|
||||
{!isMobile && <DesktopCategoryBar categories={categories} />}
|
||||
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, minHeight: 0 }}>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import EmojiEventsOutlinedIcon from '@mui/icons-material/EmojiEventsOutlined';
|
||||
import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined';
|
||||
import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GlassCard, SectionHeader } from '../ui';
|
||||
|
||||
export interface CategoryBrowseGridProps {
|
||||
categories: { name: string; slug: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop-only category browser for the home page. Mirrors the new top-bar
|
||||
* category emphasis with a tappable tile grid, so categories are reachable
|
||||
* from the content area, not just the nav.
|
||||
*/
|
||||
export const CategoryBrowseGrid = ({ categories }: CategoryBrowseGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (categories.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SectionHeader
|
||||
title={t('home.browseCategories', { defaultValue: 'Browse Categories' })}
|
||||
subtitle={t('home.browseCategoriesSubtitle', {
|
||||
defaultValue: 'Jump straight into a flavor line',
|
||||
})}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<GlassCard
|
||||
key={cat.slug}
|
||||
intensity="subtle"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
transition: 'transform 0.18s ease, box-shadow 0.18s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: (theme) => theme.tokens.elevation.md,
|
||||
},
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 4,
|
||||
background: (theme) => theme.tokens.accent.gradient,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component={Link}
|
||||
to={`/category/${cat.slug}`}
|
||||
sx={{
|
||||
p: 2.5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, pl: 1 }}>
|
||||
{t(`categories.${cat.slug}`, { defaultValue: cat.name })}
|
||||
</Typography>
|
||||
<ArrowForwardIcon sx={{ fontSize: '1.1rem', color: 'text.secondary' }} />
|
||||
</Box>
|
||||
</GlassCard>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface QuickStatsStripProps {
|
||||
topName?: string;
|
||||
topScore?: number;
|
||||
newestName?: string;
|
||||
categoryCount: number;
|
||||
}
|
||||
|
||||
/** Desktop-only featured strip — fills the wider canvas with at-a-glance picks. */
|
||||
export const QuickStatsStrip = ({
|
||||
topName,
|
||||
topScore,
|
||||
newestName,
|
||||
categoryCount,
|
||||
}: QuickStatsStripProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: <EmojiEventsOutlinedIcon />,
|
||||
label: t('home.statTopRated', { defaultValue: 'Top Rated' }),
|
||||
value: topName,
|
||||
sub: topScore != null ? `${topScore.toFixed(1)} / 10` : undefined,
|
||||
},
|
||||
{
|
||||
icon: <FiberNewOutlinedIcon />,
|
||||
label: t('home.statNewest', { defaultValue: 'Newest Arrival' }),
|
||||
value: newestName,
|
||||
},
|
||||
{
|
||||
icon: <CategoryOutlinedIcon />,
|
||||
label: t('home.statCategories', { defaultValue: 'Categories' }),
|
||||
value: String(categoryCount),
|
||||
},
|
||||
].filter((i) => i.value);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${items.length}, 1fr)`,
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<GlassCard
|
||||
key={item.label}
|
||||
intensity="subtle"
|
||||
sx={{ p: 2.5, display: 'flex', alignItems: 'center', gap: 2 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
flexShrink: 0,
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'primary.main',
|
||||
bgcolor: 'action.hover',
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2 }} noWrap>
|
||||
{item.value}
|
||||
</Typography>
|
||||
{item.sub && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.sub}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</GlassCard>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Box, Button, alpha } from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
|
||||
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
|
||||
export interface DesktopCategoryBarProps {
|
||||
categories: { name: string; slug: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop horizontal nav (md+), pinned just under the top bar. Categories are
|
||||
* the dominant browse axis here — laid out as a scrollable row of tabs — with
|
||||
* Home / Community as primary links on the left. Replaces the old left rail.
|
||||
*/
|
||||
export const DesktopCategoryBar = ({ categories }: DesktopCategoryBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const { user } = useAuth();
|
||||
|
||||
const isActive = (path: string) =>
|
||||
path === '/' ? location.pathname === '/' : location.pathname.startsWith(path);
|
||||
|
||||
const tabSx = (active: boolean) => ({
|
||||
flexShrink: 0,
|
||||
borderRadius: 2,
|
||||
px: 1.75,
|
||||
py: 0.75,
|
||||
fontWeight: active ? 700 : 500,
|
||||
color: active ? 'primary.main' : 'text.secondary',
|
||||
bgcolor: active ? (theme: Theme) => alpha(theme.palette.primary.main, 0.12) : 'transparent',
|
||||
'&:hover': {
|
||||
color: 'text.primary',
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="nav"
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 64,
|
||||
zIndex: (theme) => theme.zIndex.appBar - 1,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: (theme) => theme.tokens.glass.border,
|
||||
backgroundColor: (theme) =>
|
||||
alpha(theme.palette.background.default, theme.palette.mode === 'light' ? 0.8 : 0.6),
|
||||
backdropFilter: (theme) => theme.tokens.glass.blurStrong,
|
||||
WebkitBackdropFilter: (theme) => theme.tokens.glass.blurStrong,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 1400,
|
||||
mx: 'auto',
|
||||
px: { md: 6 },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
minHeight: 52,
|
||||
overflowX: 'auto',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
scrollbarWidth: 'none',
|
||||
}}
|
||||
>
|
||||
<Button component={Link} to="/" disableRipple sx={tabSx(isActive('/'))}>
|
||||
<HomeOutlinedIcon sx={{ fontSize: '1.1rem', mr: 0.75 }} />
|
||||
{t('nav.home')}
|
||||
</Button>
|
||||
|
||||
{user && (
|
||||
<Button component={Link} to="/community" disableRipple sx={tabSx(isActive('/community'))}>
|
||||
<GroupsOutlinedIcon sx={{ fontSize: '1.1rem', mr: 0.75 }} />
|
||||
{t('nav.community')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
alignSelf: 'stretch',
|
||||
my: 1.25,
|
||||
mx: 0.5,
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
/>
|
||||
|
||||
{categories.map((cat) => {
|
||||
const path = `/category/${cat.slug}`;
|
||||
return (
|
||||
<Button
|
||||
key={cat.slug}
|
||||
component={Link}
|
||||
to={path}
|
||||
disableRipple
|
||||
sx={tabSx(location.pathname === path)}
|
||||
>
|
||||
{t(`categories.${cat.slug}`, { defaultValue: cat.name })}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||
import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined';
|
||||
import CollectionsBookmarkOutlinedIcon from '@mui/icons-material/CollectionsBookmarkOutlined';
|
||||
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
|
||||
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined';
|
||||
import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
|
||||
import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GlassAppBar } from '../ui';
|
||||
import { GlobalSearch } from '../../app/GlobalSearch';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { BrandMark } from './BrandMark';
|
||||
import { NotificationMenu } from './NotificationMenu';
|
||||
|
||||
/**
|
||||
* Desktop top bar (md+). Promotes global search to the primary, centered slot;
|
||||
* notifications + an account menu sit on the right. Everything that used to be
|
||||
* a flat sidebar row (profile, dashboard, settings, support, admin, following,
|
||||
* logout) now lives behind the avatar menu so the chrome stays light.
|
||||
*/
|
||||
export const DesktopTopBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const { user, following, logout } = useAuth();
|
||||
|
||||
const [notifAnchorEl, setNotifAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [acctAnchorEl, setAcctAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const acctOpen = Boolean(acctAnchorEl);
|
||||
const closeAcct = () => setAcctAnchorEl(null);
|
||||
|
||||
const unreadCount = user?.unread_notifications_count || 0;
|
||||
|
||||
return (
|
||||
<GlassAppBar>
|
||||
<BrandMark />
|
||||
|
||||
<GlobalSearch />
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{user ? (
|
||||
<>
|
||||
<IconButton color="inherit" onClick={(e) => setNotifAnchorEl(e.currentTarget)}>
|
||||
<Badge badgeContent={unreadCount} color="error">
|
||||
<NotificationsIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<NotificationMenu
|
||||
anchorEl={notifAnchorEl}
|
||||
open={Boolean(notifAnchorEl)}
|
||||
onClose={() => setNotifAnchorEl(null)}
|
||||
menuSx={{ mt: 1 }}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onClick={(e) => setAcctAnchorEl(e.currentTarget)}
|
||||
aria-label={t('nav.profile')}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={acctOpen}
|
||||
sx={{ ml: 0.5 }}
|
||||
>
|
||||
<Avatar
|
||||
src={user.avatar || undefined}
|
||||
sx={{ width: 34, height: 34, border: '2px solid', borderColor: 'primary.main' }}
|
||||
>
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
anchorEl={acctAnchorEl}
|
||||
open={acctOpen}
|
||||
onClose={closeAcct}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
slotProps={{ paper: { sx: { mt: 1, minWidth: 240 } } }}
|
||||
>
|
||||
<Box sx={{ px: 2, py: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }} noWrap>
|
||||
{user.username}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
|
||||
<MenuItem component={Link} to={`/profile/${user.username}`} onClick={closeAcct}>
|
||||
<ListItemIcon>
|
||||
<PersonOutlineOutlinedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('nav.profile')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem component={Link} to="/dashboard" onClick={closeAcct}>
|
||||
<ListItemIcon>
|
||||
<CollectionsBookmarkOutlinedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('nav.dashboard')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem component={Link} to="/settings" onClick={closeAcct}>
|
||||
<ListItemIcon>
|
||||
<SettingsOutlinedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('nav.settings')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem component={Link} to="/support" onClick={closeAcct}>
|
||||
<ListItemIcon>
|
||||
<HelpOutlineOutlinedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('support.title')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{user.is_superuser && (
|
||||
<MenuItem component={Link} to="/admin-panel" onClick={closeAcct}>
|
||||
<ListItemIcon>
|
||||
<AdminPanelSettingsOutlinedIcon fontSize="small" color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ sx: { color: 'primary.main', fontWeight: 700 } }}
|
||||
>
|
||||
{t('admin.panelTitle', { defaultValue: 'Admin Panel' })}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{following.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{ px: 2, color: 'text.secondary', display: 'block' }}
|
||||
>
|
||||
{t('nav.following')} ({following.length})
|
||||
</Typography>
|
||||
<Box sx={{ maxHeight: 220, overflowY: 'auto' }}>
|
||||
{following.map((f) => (
|
||||
<MenuItem
|
||||
key={f.id}
|
||||
component={Link}
|
||||
to={`/profile/${f.username}`}
|
||||
onClick={closeAcct}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Avatar src={f.avatar || undefined} sx={{ width: 24, height: 24 }}>
|
||||
{!f.avatar && f.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</ListItemIcon>
|
||||
<ListItemText primaryTypographyProps={{ variant: 'body2' }}>
|
||||
{f.username}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
closeAcct();
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ color: 'error.main' }}>
|
||||
<LogoutOutlinedIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ sx: { color: 'error.main', fontWeight: 600 } }}
|
||||
>
|
||||
{t('nav.logout')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="contained" component={Link} to="/login" sx={{ borderRadius: 2 }}>
|
||||
{t('nav.login')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</GlassAppBar>
|
||||
);
|
||||
};
|
||||
@@ -114,6 +114,11 @@ const resources = {
|
||||
exploreFlavors: 'Explore Flavors',
|
||||
hallOfFame: 'Hall of Fame',
|
||||
hallOfFameSubtitle: 'Highest-rated flavors in the archive',
|
||||
browseCategories: 'Browse Categories',
|
||||
browseCategoriesSubtitle: 'Jump straight into a flavor line',
|
||||
statTopRated: 'Top Rated',
|
||||
statNewest: 'Newest Arrival',
|
||||
statCategories: 'Categories',
|
||||
recForYou: 'Recommended for You',
|
||||
recForYouSubtitle: 'Picked by tasters who share your palate',
|
||||
popularPicks: 'Popular Picks',
|
||||
@@ -645,6 +650,11 @@ const resources = {
|
||||
exploreFlavors: 'Sorten entdecken',
|
||||
hallOfFame: 'Hall of Fame',
|
||||
hallOfFameSubtitle: 'Die bestbewerteten Sorten im Archiv',
|
||||
browseCategories: 'Kategorien durchstöbern',
|
||||
browseCategoriesSubtitle: 'Direkt zu einer Sorten-Linie springen',
|
||||
statTopRated: 'Bestbewertet',
|
||||
statNewest: 'Neuheit',
|
||||
statCategories: 'Kategorien',
|
||||
recForYou: 'Für dich empfohlen',
|
||||
recForYouSubtitle: 'Ausgewählt von Testern mit ähnlichem Geschmack',
|
||||
popularPicks: 'Beliebte Sorten',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IconButton,
|
||||
Skeleton,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -34,6 +35,8 @@ import {
|
||||
EmptyState,
|
||||
} from '../components/ui';
|
||||
import MainPageRecommendations from './MainPageRecommendations';
|
||||
import { CategoryBrowseGrid, QuickStatsStrip } from '../components/home/DesktopHomeSections';
|
||||
import { useCategories } from '../api/queries/useCategories';
|
||||
|
||||
const TOP_LIMIT = 5;
|
||||
const AUTO_ADVANCE_MS = 6000;
|
||||
@@ -91,6 +94,8 @@ const MainPage: React.FC = () => {
|
||||
const [autoPlay, setAutoPlay] = useState(true);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
||||
const muiTheme = useTheme();
|
||||
const isDesktop = useMediaQuery(muiTheme.breakpoints.up('md'));
|
||||
const touchStartX = useRef<number | null>(null);
|
||||
|
||||
const query = new URLSearchParams(location.search).get('q') || '';
|
||||
@@ -100,6 +105,7 @@ const MainPage: React.FC = () => {
|
||||
const { data: searchResults = [], isLoading: searchLoading } = useFlavorSearch(query);
|
||||
const { data: topFlavorsAll = [], isLoading: topLoading } = useTopFlavors(null);
|
||||
const { data: newestFlavors = [], isLoading: newestLoading } = useNewestFlavors();
|
||||
const { data: categories = [] } = useCategories();
|
||||
|
||||
const topFlavors = topFlavorsAll
|
||||
.filter((f) => f.category_slug !== 'packs-and-other')
|
||||
@@ -268,6 +274,16 @@ const MainPage: React.FC = () => {
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
{/* Desktop-only featured strip — fills the wider canvas */}
|
||||
{isDesktop && (
|
||||
<QuickStatsStrip
|
||||
topName={currentTop?.name}
|
||||
topScore={currentTop?.average_rating ?? undefined}
|
||||
newestName={newestFlavors[0]?.name}
|
||||
categoryCount={categories.length}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Hall of Fame */}
|
||||
<Box>
|
||||
<SectionHeader
|
||||
@@ -527,6 +543,9 @@ const MainPage: React.FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Desktop-only category browser */}
|
||||
{isDesktop && <CategoryBrowseGrid categories={categories} />}
|
||||
|
||||
{/* Algorithmic discovery: personalized (members) or popular picks (visitors) */}
|
||||
<MainPageRecommendations isLoggedIn={isLoggedIn} />
|
||||
|
||||
|
||||
Reference in new issue
Block a user