mainpage redesign

This commit is contained in:
nvrl committed 2026-04-26 13:35:32 +02:00
1 parent ecedabe8b0
commit 6f5f3eaf37
2 files changed
+371 -437

No files matched your search

+28
View File
@@ -98,11 +98,25 @@ const resources = {
followMore: 'Follow more users to see their activity here!', followMore: 'Follow more users to see their activity here!',
exploreFlavors: 'Explore Flavors', exploreFlavors: 'Explore Flavors',
hallOfFame: 'Hall of Fame', hallOfFame: 'Hall of Fame',
hallOfFameSubtitle: 'Highest-rated flavors in the archive',
communityVoice: 'Community Voice', communityVoice: 'Community Voice',
newArrivals: 'New Arrivals', newArrivals: 'New Arrivals',
newArrivalsSubtitle: 'The latest additions to the catalog',
viewAllReviews: 'View All Reviews', viewAllReviews: 'View All Reviews',
viewMoreReviews: 'View more reviews', viewMoreReviews: 'View more reviews',
rated: 'rated', rated: 'rated',
archiveTitle: 'The Holy Archive',
archiveSubtitle:
'Top-rated flavors and the latest arrivals. Use the search bar to find anything.',
searchResultsTitle: 'Search results for "{{query}}"',
searchResultsCount_one: 'Found {{count}} item',
searchResultsCount_other: 'Found {{count}} items',
noSearchResults: 'No flavors match "{{query}}".',
noTopFlavors: 'No top-rated flavors yet.',
noNewest: 'No new arrivals yet.',
previousFlavor: 'Previous flavor',
nextFlavor: 'Next flavor',
goToFlavor: 'Go to flavor {{n}}',
}, },
common: { common: {
search: 'Search...', search: 'Search...',
@@ -382,11 +396,25 @@ const resources = {
followMore: 'Folge mehr Benutzern, um deren Aktivitäten hier zu sehen!', followMore: 'Folge mehr Benutzern, um deren Aktivitäten hier zu sehen!',
exploreFlavors: 'Sorten entdecken', exploreFlavors: 'Sorten entdecken',
hallOfFame: 'Hall of Fame', hallOfFame: 'Hall of Fame',
hallOfFameSubtitle: 'Die bestbewerteten Sorten im Archiv',
communityVoice: 'Community-Stimmen', communityVoice: 'Community-Stimmen',
newArrivals: 'Neuheiten', newArrivals: 'Neuheiten',
newArrivalsSubtitle: 'Die neuesten Zugänge im Katalog',
viewAllReviews: 'Alle Bewertungen ansehen', viewAllReviews: 'Alle Bewertungen ansehen',
viewMoreReviews: 'Mehr Bewertungen', viewMoreReviews: 'Mehr Bewertungen',
rated: 'bewertete', rated: 'bewertete',
archiveTitle: 'Das Holy-Archiv',
archiveSubtitle:
'Top-bewertete Sorten und die neuesten Zugänge. Nutze die Suche, um alles zu finden.',
searchResultsTitle: 'Suchergebnisse für „{{query}}"',
searchResultsCount_one: '{{count}} Treffer',
searchResultsCount_other: '{{count}} Treffer',
noSearchResults: 'Keine Sorten passen zu „{{query}}".',
noTopFlavors: 'Noch keine bestbewerteten Sorten.',
noNewest: 'Noch keine Neuheiten.',
previousFlavor: 'Vorherige Sorte',
nextFlavor: 'Nächste Sorte',
goToFlavor: 'Zu Sorte {{n}}',
}, },
common: { common: {
search: 'Suchen...', search: 'Suchen...',
+343 -437
View File
@@ -1,27 +1,22 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useState } from 'react';
import { import {
Typography, Typography,
Box, Box,
CardContent, CardContent,
CircularProgress,
Button, Button,
Avatar, Avatar,
Chip, Chip,
IconButton, IconButton,
Tabs, Skeleton,
Tab,
} from '@mui/material'; } from '@mui/material';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import { useCategories } from '../api/queries/useFlavorsList';
import { import {
useFlavorSearch, useFlavorSearch,
useFollowingFeedTop,
useNewestFlavors, useNewestFlavors,
useRecentReviews,
useTopFlavors, useTopFlavors,
} from '../api/queries/useMainPageQueries'; } from '../api/queries/useMainPageQueries';
import { useTitle } from '../hooks/useTitle'; import { useTitle } from '../hooks/useTitle';
@@ -32,211 +27,173 @@ import {
PageShell, PageShell,
SectionHeader, SectionHeader,
HeroBackdrop, HeroBackdrop,
ReviewRow,
FlavorCard, FlavorCard,
EmptyState,
} from '../components/ui'; } from '../components/ui';
interface MainPageProps { interface MainPageProps {
adminMode?: boolean; adminMode?: boolean;
} }
const MainPage: React.FC<MainPageProps> = ({ adminMode }) => { const TOP_LIMIT = 5;
const FlavorCardSkeleton: React.FC<{ compact?: boolean }> = ({ compact }) => (
<Box sx={{ height: '100%' }}>
<Skeleton
variant="rectangular"
sx={{
width: '100%',
aspectRatio: '1 / 1',
borderRadius: 2,
}}
/>
<Skeleton variant="text" sx={{ mt: 1, fontSize: compact ? '0.95rem' : '1rem' }} />
<Skeleton variant="text" width="60%" sx={{ fontSize: '0.75rem' }} />
</Box>
);
const HeroCarouselSkeleton: React.FC = () => (
<GlassCard
intensity="strong"
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
height: { xs: 'auto', md: 480 },
overflow: 'hidden',
}}
>
<Skeleton
variant="rectangular"
sx={{
width: { xs: '100%', md: '45%' },
height: { xs: 240, md: '100%' },
}}
/>
<Box sx={{ flex: 1, p: { xs: 3, md: 6 } }}>
<Skeleton variant="text" width="40%" sx={{ fontSize: '0.85rem' }} />
<Skeleton variant="text" width="80%" sx={{ fontSize: '2.5rem', mb: 1 }} />
<Skeleton variant="text" width="50%" />
<Skeleton variant="text" width="100%" sx={{ mt: 3 }} />
<Skeleton variant="text" width="90%" />
<Skeleton variant="rectangular" width={140} height={42} sx={{ mt: 3, borderRadius: 1 }} />
</Box>
</GlassCard>
);
const MainPage: React.FC<MainPageProps> = (_props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [selectedCategorySlug, setSelectedCategorySlug] = useState<string>('');
const [activeIndex, setActiveIndex] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const location = useLocation(); const location = useLocation();
const [activeIndex, setActiveIndex] = useState(0);
const query = new URLSearchParams(location.search).get('q') || ''; const query = new URLSearchParams(location.search).get('q') || '';
const isLoggedIn = !!localStorage.getItem('access'); const isLoggedIn = !!localStorage.getItem('access');
const { data: searchResults = [], isLoading: searchLoading } = useFlavorSearch(query); const { data: searchResults = [], isLoading: searchLoading } = useFlavorSearch(query);
const { const { data: topFlavorsAll = [], isLoading: topLoading } = useTopFlavors(null);
data: topFlavors = [],
isLoading: topLoading,
isFetching: topFetching,
} = useTopFlavors(selectedCategorySlug || null);
const { data: newestFlavors = [], isLoading: newestLoading } = useNewestFlavors(); const { data: newestFlavors = [], isLoading: newestLoading } = useNewestFlavors();
const { data: recentReviews = [], isLoading: recentLoading } = useRecentReviews();
const { data: categories = [], isLoading: catLoading } = useCategories();
const { data: feedRatings = [] } = useFollowingFeedTop(isLoggedIn && !query);
const allFlavors = searchResults; const topFlavors = topFlavorsAll.slice(0, TOP_LIMIT);
const loading = query const safeIndex = topFlavors.length > 0 ? activeIndex % topFlavors.length : 0;
? searchLoading const currentTop = topFlavors[safeIndex];
: topLoading || newestLoading || recentLoading || catLoading; const featuredReview = currentTop?.ratings?.find((r) => r.comment) || null;
const categoryLoading = topFetching && !topLoading;
useEffect(() => {
setActiveIndex(0);
}, [selectedCategorySlug]);
const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => {
setSelectedCategorySlug(newValue);
};
const startTimer = useCallback(() => {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % topFlavors.length);
}, 6000);
}, [topFlavors.length]);
useEffect(() => {
if (topFlavors.length > 0 && !query) {
startTimer();
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}
}, [topFlavors, query, startTimer]);
useTitle(query ? `Search: ${query}` : 'Holy Flavors Archive'); useTitle(query ? `Search: ${query}` : 'Holy Flavors Archive');
if (loading)
return (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}>
<CircularProgress />
</Box>
);
const handlePrev = () => { const handlePrev = () => {
if (topFlavors.length === 0) return;
setActiveIndex((prev) => (prev - 1 + topFlavors.length) % topFlavors.length); setActiveIndex((prev) => (prev - 1 + topFlavors.length) % topFlavors.length);
startTimer();
}; };
const handleNext = () => { const handleNext = () => {
if (topFlavors.length === 0) return;
setActiveIndex((prev) => (prev + 1) % topFlavors.length); setActiveIndex((prev) => (prev + 1) % topFlavors.length);
startTimer();
}; };
// --- SEARCH VIEW --- // --- SEARCH VIEW ---
if (query) { if (query) {
const filteredFlavors = allFlavors.filter(
(f) =>
f.name.toLowerCase().includes(query.toLowerCase()) ||
f.description.toLowerCase().includes(query.toLowerCase()) ||
f.category_name.toLowerCase().includes(query.toLowerCase()),
);
return ( return (
<PageShell> <PageShell>
<SectionHeader <SectionHeader
title={`Search Results for "${query}"`} title={t('home.searchResultsTitle', { query })}
subtitle={`Found ${filteredFlavors.length} items`} subtitle={
searchLoading
? t('common.loading')
: t('home.searchResultsCount', { count: searchResults.length })
}
/> />
<Box {searchLoading ? (
sx={{ <Box
display: 'grid', sx={{
gridTemplateColumns: { display: 'grid',
xs: '1fr', gridTemplateColumns: {
sm: 'repeat(auto-fill, minmax(280px, 1fr))', xs: '1fr',
lg: 'repeat(auto-fill, minmax(240px, 1fr))', sm: 'repeat(auto-fill, minmax(280px, 1fr))',
}, lg: 'repeat(auto-fill, minmax(240px, 1fr))',
gap: 3, },
}} gap: 3,
> }}
{filteredFlavors.map((flavor) => ( >
<FlavorCard key={flavor.id} flavor={flavor} /> {Array.from({ length: 8 }).map((_, i) => (
))} <FlavorCardSkeleton key={i} />
</Box> ))}
</Box>
) : searchResults.length === 0 ? (
<EmptyState title={t('home.noSearchResults', { query })} />
) : (
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(auto-fill, minmax(280px, 1fr))',
lg: 'repeat(auto-fill, minmax(240px, 1fr))',
},
gap: 3,
}}
>
{searchResults.map((flavor) => (
<FlavorCard key={flavor.id} flavor={flavor} />
))}
</Box>
)}
</PageShell> </PageShell>
); );
} }
// --- HOME VIEW --- // --- HOME VIEW ---
const currentTop = topFlavors[activeIndex];
const featuredReview = currentTop?.ratings?.find((r) => r.comment) || null;
return ( return (
<PageShell hero={<HeroBackdrop variant={isLoggedIn ? 'minimal' : 'mesh'} />}> <PageShell hero={<HeroBackdrop variant={isLoggedIn ? 'minimal' : 'mesh'} />}>
{/* Hero / Activity Feed */} {/* Hero */}
{isLoggedIn ? ( {isLoggedIn ? (
<Box> <Box sx={{ mb: 1 }}>
<SectionHeader <Typography
title={t('home.activity')} variant="h3"
subtitle={t('home.activitySubtitle')} component="h1"
action={ sx={{
<Button fontWeight: 800,
component={Link} letterSpacing: '-0.02em',
to="/community" fontSize: { xs: '1.75rem', sm: '2.25rem', md: '2.75rem' },
variant="outlined" background: (theme) => theme.tokens.accent.gradient,
sx={{ borderRadius: 2, textTransform: 'none' }} WebkitBackgroundClip: 'text',
> WebkitTextFillColor: 'transparent',
{t('home.viewFullFeed')} backgroundClip: 'text',
</Button> mb: 1,
} }}
/> >
{t('home.archiveTitle')}
{feedRatings.length === 0 ? ( </Typography>
<GlassCard <Typography
intensity="subtle" variant="body1"
sx={{ color="text.secondary"
p: 4, sx={{ maxWidth: 640, lineHeight: 1.6 }}
textAlign: 'center', >
border: '2px dashed', {t('home.archiveSubtitle')}
borderColor: 'divider', </Typography>
}}
>
<Typography variant="h6">{t('home.quietFeed')}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('home.followMore')}
</Typography>
<Button
variant="contained"
component={Link}
to="/"
size="small"
sx={{ borderRadius: 2 }}
>
{t('home.exploreFlavors')}
</Button>
</GlassCard>
) : (
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
gap: 2,
}}
>
{feedRatings.map((rating) => (
<Box key={rating.id} sx={{ position: 'relative' }}>
<ReviewRow review={rating} compact />
{adminMode && (
<Button
size="small"
variant="outlined"
color="secondary"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(`/admin-panel/rating/${rating.id}`);
}}
sx={{
position: 'absolute',
top: 8,
right: 8,
borderRadius: 1,
fontSize: '0.6rem',
py: 0,
textTransform: 'none',
zIndex: 2,
}}
>
{t('admin.manageRating')}
</Button>
)}
</Box>
))}
</Box>
)}
</Box> </Box>
) : ( ) : (
<GlassCard <GlassCard
intensity="strong" intensity="strong"
sx={{ sx={{
p: { xs: 4, md: 10 }, p: { xs: 4, md: 8 },
textAlign: 'center', textAlign: 'center',
overflow: 'hidden', overflow: 'hidden',
position: 'relative', position: 'relative',
@@ -247,8 +204,8 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
component="h1" component="h1"
gutterBottom gutterBottom
sx={{ sx={{
fontWeight: 'bold', fontWeight: 800,
fontSize: { xs: '2.2rem', sm: '3rem', md: '4.5rem' }, fontSize: { xs: '2.2rem', sm: '3rem', md: '4rem' },
overflowWrap: 'break-word', overflowWrap: 'break-word',
background: (theme) => theme.tokens.accent.gradient, background: (theme) => theme.tokens.accent.gradient,
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
@@ -259,9 +216,9 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
{t('hero.title')} {t('hero.title')}
</Typography> </Typography>
<Typography <Typography
variant="h5" variant="h6"
color="text.secondary" color="text.secondary"
sx={{ mb: 4, maxWidth: 800, mx: 'auto', px: 2 }} sx={{ mb: 4, maxWidth: 720, mx: 'auto', px: 2, lineHeight: 1.5 }}
> >
{t('hero.subtitle')} {t('hero.subtitle')}
</Typography> </Typography>
@@ -288,296 +245,245 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
</GlassCard> </GlassCard>
)} )}
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', xl: 'row' }, gap: 6 }}> {/* Hall of Fame */}
{/* Hall of Fame Carousel */} <Box>
{topFlavors.length > 0 && currentTop && ( <SectionHeader
<Box sx={{ flex: { xs: '1 1 100%', xl: '1 1 65%' }, minWidth: 0 }}> title={
<SectionHeader <Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}>
title={ <EmojiEventsIcon color="primary" sx={{ fontSize: '1.75rem' }} />
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}> <span>{t('home.hallOfFame')}</span>
<EmojiEventsIcon color="primary" sx={{ fontSize: '2rem' }} />
<span>{t('home.hallOfFame')}</span>
</Box>
}
action={
<Box sx={{ display: 'flex', gap: 1 }}>
<IconButton
onClick={handlePrev}
sx={{ border: '1px solid', borderColor: 'divider' }}
>
<ChevronLeftIcon />
</IconButton>
<IconButton
onClick={handleNext}
sx={{ border: '1px solid', borderColor: 'divider' }}
>
<ChevronRightIcon />
</IconButton>
</Box>
}
/>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 4 }}>
<Tabs
value={selectedCategorySlug}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
aria-label="hall of fame categories"
sx={{ '& .MuiTab-root': { fontWeight: 'bold', minWidth: 100 } }}
>
<Tab label="All Flavors" value="" />
{categories.map((cat) => (
<Tab key={cat.id} label={cat.name} value={cat.slug} />
))}
</Tabs>
</Box> </Box>
}
<Box sx={{ position: 'relative', minHeight: { xs: 400, md: 500 } }}> subtitle={t('home.hallOfFameSubtitle')}
{categoryLoading && ( action={
<Box topFlavors.length > 1 ? (
sx={{ <Box sx={{ display: 'flex', gap: 1 }}>
position: 'absolute', <IconButton
inset: 0, onClick={handlePrev}
display: 'flex', aria-label={t('home.previousFlavor')}
justifyContent: 'center', sx={{ border: '1px solid', borderColor: 'divider' }}
alignItems: 'center',
bgcolor: (theme) => theme.tokens.glass.tint,
backdropFilter: (theme) => theme.tokens.glass.blur,
zIndex: 2,
borderRadius: 4,
}}
> >
<CircularProgress /> <ChevronLeftIcon />
</Box> </IconButton>
)} <IconButton
<GlassCard onClick={handleNext}
intensity="strong" aria-label={t('home.nextFlavor')}
sx={{ border: '1px solid', borderColor: 'divider' }}
>
<ChevronRightIcon />
</IconButton>
</Box>
) : undefined
}
/>
{topLoading ? (
<HeroCarouselSkeleton />
) : currentTop ? (
<>
<GlassCard
intensity="strong"
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
height: { xs: 'auto', md: 480 },
overflow: 'hidden',
position: 'relative',
}}
>
<Box
component={Link}
to={`/flavor/${currentTop.id}`}
sx={{ sx={{
width: { xs: '100%', md: '45%' },
height: { md: '100%' },
aspectRatio: { xs: '16/9', md: 'auto' },
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', md: 'row' }, alignItems: 'center',
height: { xs: 'auto', md: 500 }, justifyContent: 'center',
bgcolor: 'action.hover',
overflow: 'hidden', overflow: 'hidden',
textDecoration: 'none',
position: 'relative', position: 'relative',
}} }}
> >
<Box <Box sx={{ position: 'absolute', top: 16, right: 16, zIndex: 1 }}>
component={Link} <StatusBadge
to={`/flavor/${currentTop.id}`} isLegacy={currentTop.is_legacy}
sx={{ isAvailable={currentTop.is_available}
width: { xs: '100%', md: '45%' }, />
height: { md: '100%' }, </Box>
aspectRatio: { xs: '16/9', md: 'auto' }, {currentTop.image_url && (
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'action.hover',
overflow: 'hidden',
textDecoration: 'none',
}}
>
<Box sx={{ position: 'absolute', top: 16, right: 16, zIndex: 1 }}>
<StatusBadge
isLegacy={currentTop.is_legacy}
isAvailable={currentTop.is_available}
/>
</Box>
<Box <Box
component="img" component="img"
src={currentTop.image_url || ''} src={currentTop.image_url}
alt={currentTop.name}
sx={{ width: '100%', height: '100%', objectFit: 'cover' }} sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
loading="lazy" loading="lazy"
/> />
</Box> )}
<CardContent </Box>
<CardContent
sx={{
flex: 1,
p: { xs: 3, sm: 4, md: 6 },
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
>
<Chip
label={`${t('common.rank')} #${safeIndex + 1}`}
color="primary"
size="small"
sx={{ width: 'fit-content', mb: 1, fontWeight: 'bold' }}
/>
<Typography
variant="h3"
gutterBottom
sx={{ sx={{
flex: 1, fontWeight: 800,
p: { xs: 3, sm: 4, md: 6 }, fontSize: { xs: '1.75rem', sm: '2.25rem', md: '2.75rem' },
display: 'flex', lineHeight: 1.1,
flexDirection: 'column',
justifyContent: 'center',
}} }}
> >
<Chip {currentTop.name}
label={`${t('common.rank')} #${activeIndex + 1}`} </Typography>
color="primary"
size="small"
sx={{ width: 'fit-content', mb: 1, fontWeight: 'bold' }}
/>
<Typography
variant="h3"
gutterBottom
sx={{
fontWeight: 'bold',
fontSize: { xs: '1.75rem', sm: '2.5rem', md: '3rem' },
}}
>
{currentTop.name}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
mb: { xs: 2, md: 3 },
flexWrap: 'wrap',
gap: 2,
}}
>
<RatingBadge score={currentTop.average_rating || 0} size="large" />
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 'bold' }}>
{currentTop.ratings.length} {t('common.reviews')}
</Typography>
</Box>
{featuredReview && (
<GlassCard
intensity="subtle"
sx={{ mt: 1, p: { xs: 2, md: 3 }, boxShadow: 'none' }}
>
<Typography
variant="body2"
sx={{
fontStyle: 'italic',
mb: 1,
fontSize: { xs: '0.9rem', md: '1.1rem' },
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 3,
}}
>
"{featuredReview.comment}"
</Typography>
<Link
to={`/profile/${featuredReview.user}`}
style={{
textDecoration: 'none',
color: 'inherit',
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
<Avatar
src={featuredReview.user_avatar || undefined}
sx={{ width: 24, height: 24, fontSize: '0.6rem' }}
>
{!featuredReview.user_avatar && featuredReview.user.charAt(0)}
</Avatar>
<Typography variant="caption" color="text.secondary">
— {featuredReview.user}
</Typography>
</Link>
</GlassCard>
)}
<Button
variant="contained"
component={Link}
to={`/flavor/${currentTop.id}`}
sx={{
mt: { xs: 3, md: 4 },
width: { xs: '100%', sm: 'fit-content' },
px: 4,
py: 1.5,
borderRadius: 2,
}}
>
{t('home.viewAllReviews')}
</Button>
</CardContent>
</GlassCard>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, gap: 1 }}>
{topFlavors.map((_, i) => (
<Box <Box
key={i}
onClick={() => setActiveIndex(i)}
sx={{ sx={{
width: activeIndex === i ? 24 : 8, display: 'flex',
height: 8, alignItems: 'center',
borderRadius: 4, mb: { xs: 2, md: 3 },
bgcolor: activeIndex === i ? 'primary.main' : 'divider', flexWrap: 'wrap',
cursor: 'pointer', gap: 2,
transition: 'all 0.3s ease',
}} }}
/> >
))} <RatingBadge score={currentTop.average_rating || 0} size="large" />
</Box> <Typography variant="body2" color="text.secondary" sx={{ fontWeight: 'bold' }}>
</Box> {currentTop.ratings.length} {t('common.reviews')}
)} </Typography>
</Box>
{/* Recent Reviews */} {featuredReview && (
{recentReviews.length > 0 && ( <GlassCard
<Box intensity="subtle"
sx={{ sx={{ mt: 1, p: { xs: 2, md: 2.5 }, boxShadow: 'none' }}
flex: { xs: '1 1 100%', xl: '1 1 35%' }, >
minWidth: 0, <Typography
display: 'flex', variant="body2"
flexDirection: 'column',
}}
>
<SectionHeader title={t('home.communityVoice')} />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
height: { xl: 500 },
overflowY: { xl: 'auto' },
pr: { xl: 1 },
'&::-webkit-scrollbar': { width: '6px' },
'&::-webkit-scrollbar-thumb': { bgcolor: 'divider', borderRadius: '3px' },
}}
>
{recentReviews.map((review) => (
<Box key={review.id} sx={{ position: 'relative' }}>
<ReviewRow review={review} compact />
{adminMode && (
<Button
size="small"
variant="text"
color="secondary"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(`/admin-panel/rating/${review.id}`);
}}
sx={{ sx={{
position: 'absolute', fontStyle: 'italic',
top: 4, mb: 1,
right: 4, fontSize: { xs: '0.9rem', md: '1rem' },
fontSize: '0.65rem', lineHeight: 1.5,
py: 0, display: '-webkit-box',
textTransform: 'none', overflow: 'hidden',
fontWeight: 'bold', WebkitBoxOrient: 'vertical',
zIndex: 2, WebkitLineClamp: 3,
}} }}
> >
{t('admin.manageRating')} &quot;{featuredReview.comment}&quot;
</Button> </Typography>
)} <Box
</Box> component={Link}
))} to={`/profile/${featuredReview.user}`}
<Button sx={{
component={Link} textDecoration: 'none',
to="/community" color: 'inherit',
variant="text" display: 'flex',
size="small" alignItems: 'center',
sx={{ alignSelf: 'center', mt: 1 }} gap: 1,
> }}
{t('home.viewMoreReviews')} >
</Button> <Avatar
</Box> src={featuredReview.user_avatar || undefined}
</Box> sx={{ width: 22, height: 22, fontSize: '0.6rem' }}
>
{!featuredReview.user_avatar && featuredReview.user.charAt(0)}
</Avatar>
<Typography variant="caption" color="text.secondary">
— {featuredReview.user}
</Typography>
</Box>
</GlassCard>
)}
<Button
variant="contained"
component={Link}
to={`/flavor/${currentTop.id}`}
sx={{
mt: { xs: 3, md: 4 },
width: { xs: '100%', sm: 'fit-content' },
px: 4,
py: 1.25,
borderRadius: 2,
}}
>
{t('home.viewAllReviews')}
</Button>
</CardContent>
</GlassCard>
{topFlavors.length > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, gap: 1 }}>
{topFlavors.map((_, i) => (
<Box
key={i}
component="button"
onClick={() => setActiveIndex(i)}
aria-label={t('home.goToFlavor', { n: i + 1 })}
aria-current={safeIndex === i ? 'true' : undefined}
sx={{
width: safeIndex === i ? 24 : 8,
height: 8,
p: 0,
border: 'none',
borderRadius: 4,
bgcolor: safeIndex === i ? 'primary.main' : 'divider',
cursor: 'pointer',
transition: 'all 0.3s ease',
'&:focus-visible': {
outline: '2px solid',
outlineColor: 'primary.main',
outlineOffset: 2,
},
}}
/>
))}
</Box>
)}
</>
) : (
<EmptyState title={t('home.noTopFlavors')} />
)} )}
</Box> </Box>
{/* Newest Arrivals */} {/* Newest Arrivals */}
{newestFlavors.length > 0 && ( <Box>
<Box> <SectionHeader title={t('home.newArrivals')} subtitle={t('home.newArrivalsSubtitle')} />
<SectionHeader title={t('home.newArrivals')} /> {newestLoading ? (
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(2, minmax(0, 1fr))',
sm: 'repeat(3, minmax(0, 1fr))',
md: 'repeat(4, minmax(0, 1fr))',
lg: 'repeat(5, minmax(0, 1fr))',
xl: 'repeat(6, minmax(0, 1fr))',
},
gap: 2,
width: '100%',
}}
>
{Array.from({ length: 6 }).map((_, i) => (
<FlavorCardSkeleton key={i} compact />
))}
</Box>
) : newestFlavors.length === 0 ? (
<EmptyState title={t('home.noNewest')} />
) : (
<Box <Box
sx={{ sx={{
display: 'grid', display: 'grid',
@@ -597,8 +503,8 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
<FlavorCard key={flavor.id} flavor={flavor} compact /> <FlavorCard key={flavor.id} flavor={flavor} compact />
))} ))}
</Box> </Box>
</Box> )}
)} </Box>
</PageShell> </PageShell>
); );
}; };