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
+210 -276

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...',
+182 -276
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,99 +27,100 @@ 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 })
}
/> />
{searchLoading ? (
<Box <Box
sx={{ sx={{
display: 'grid', display: 'grid',
@@ -136,107 +132,68 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
gap: 3, gap: 3,
}} }}
> >
{filteredFlavors.map((flavor) => ( {Array.from({ length: 8 }).map((_, i) => (
<FlavorCardSkeleton key={i} />
))}
</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} /> <FlavorCard key={flavor.id} flavor={flavor} />
))} ))}
</Box> </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={
<Button
component={Link}
to="/community"
variant="outlined"
sx={{ borderRadius: 2, textTransform: 'none' }}
>
{t('home.viewFullFeed')}
</Button>
}
/>
{feedRatings.length === 0 ? (
<GlassCard
intensity="subtle"
sx={{ sx={{
p: 4, fontWeight: 800,
textAlign: 'center', letterSpacing: '-0.02em',
border: '2px dashed', fontSize: { xs: '1.75rem', sm: '2.25rem', md: '2.75rem' },
borderColor: 'divider', background: (theme) => theme.tokens.accent.gradient,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
mb: 1,
}} }}
> >
<Typography variant="h6">{t('home.quietFeed')}</Typography> {t('home.archiveTitle')}
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('home.followMore')}
</Typography> </Typography>
<Button <Typography
variant="contained" variant="body1"
component={Link} color="text.secondary"
to="/" sx={{ maxWidth: 640, lineHeight: 1.6 }}
size="small"
sx={{ borderRadius: 2 }}
> >
{t('home.exploreFlavors')} {t('home.archiveSubtitle')}
</Button> </Typography>
</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,75 +245,48 @@ 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 && (
<Box sx={{ flex: { xs: '1 1 100%', xl: '1 1 65%' }, minWidth: 0 }}>
<SectionHeader <SectionHeader
title={ title={
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}>
<EmojiEventsIcon color="primary" sx={{ fontSize: '2rem' }} /> <EmojiEventsIcon color="primary" sx={{ fontSize: '1.75rem' }} />
<span>{t('home.hallOfFame')}</span> <span>{t('home.hallOfFame')}</span>
</Box> </Box>
} }
subtitle={t('home.hallOfFameSubtitle')}
action={ action={
topFlavors.length > 1 ? (
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<IconButton <IconButton
onClick={handlePrev} onClick={handlePrev}
aria-label={t('home.previousFlavor')}
sx={{ border: '1px solid', borderColor: 'divider' }} sx={{ border: '1px solid', borderColor: 'divider' }}
> >
<ChevronLeftIcon /> <ChevronLeftIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={handleNext} onClick={handleNext}
aria-label={t('home.nextFlavor')}
sx={{ border: '1px solid', borderColor: 'divider' }} sx={{ border: '1px solid', borderColor: 'divider' }}
> >
<ChevronRightIcon /> <ChevronRightIcon />
</IconButton> </IconButton>
</Box> </Box>
) : undefined
} }
/> />
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 4 }}> {topLoading ? (
<Tabs <HeroCarouselSkeleton />
value={selectedCategorySlug} ) : currentTop ? (
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 sx={{ position: 'relative', minHeight: { xs: 400, md: 500 } }}>
{categoryLoading && (
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
bgcolor: (theme) => theme.tokens.glass.tint,
backdropFilter: (theme) => theme.tokens.glass.blur,
zIndex: 2,
borderRadius: 4,
}}
>
<CircularProgress />
</Box>
)}
<GlassCard <GlassCard
intensity="strong" intensity="strong"
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', md: 'row' }, flexDirection: { xs: 'column', md: 'row' },
height: { xs: 'auto', md: 500 }, height: { xs: 'auto', md: 480 },
overflow: 'hidden', overflow: 'hidden',
position: 'relative', position: 'relative',
}} }}
@@ -374,6 +304,7 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
bgcolor: 'action.hover', bgcolor: 'action.hover',
overflow: 'hidden', overflow: 'hidden',
textDecoration: 'none', textDecoration: 'none',
position: 'relative',
}} }}
> >
<Box sx={{ position: 'absolute', top: 16, right: 16, zIndex: 1 }}> <Box sx={{ position: 'absolute', top: 16, right: 16, zIndex: 1 }}>
@@ -382,12 +313,15 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
isAvailable={currentTop.is_available} isAvailable={currentTop.is_available}
/> />
</Box> </Box>
{currentTop.image_url && (
<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> </Box>
<CardContent <CardContent
sx={{ sx={{
@@ -399,7 +333,7 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
}} }}
> >
<Chip <Chip
label={`${t('common.rank')} #${activeIndex + 1}`} label={`${t('common.rank')} #${safeIndex + 1}`}
color="primary" color="primary"
size="small" size="small"
sx={{ width: 'fit-content', mb: 1, fontWeight: 'bold' }} sx={{ width: 'fit-content', mb: 1, fontWeight: 'bold' }}
@@ -408,8 +342,9 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
variant="h3" variant="h3"
gutterBottom gutterBottom
sx={{ sx={{
fontWeight: 'bold', fontWeight: 800,
fontSize: { xs: '1.75rem', sm: '2.5rem', md: '3rem' }, fontSize: { xs: '1.75rem', sm: '2.25rem', md: '2.75rem' },
lineHeight: 1.1,
}} }}
> >
{currentTop.name} {currentTop.name}
@@ -432,25 +367,27 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
{featuredReview && ( {featuredReview && (
<GlassCard <GlassCard
intensity="subtle" intensity="subtle"
sx={{ mt: 1, p: { xs: 2, md: 3 }, boxShadow: 'none' }} sx={{ mt: 1, p: { xs: 2, md: 2.5 }, boxShadow: 'none' }}
> >
<Typography <Typography
variant="body2" variant="body2"
sx={{ sx={{
fontStyle: 'italic', fontStyle: 'italic',
mb: 1, mb: 1,
fontSize: { xs: '0.9rem', md: '1.1rem' }, fontSize: { xs: '0.9rem', md: '1rem' },
lineHeight: 1.5,
display: '-webkit-box', display: '-webkit-box',
overflow: 'hidden', overflow: 'hidden',
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
WebkitLineClamp: 3, WebkitLineClamp: 3,
}} }}
> >
"{featuredReview.comment}" &quot;{featuredReview.comment}&quot;
</Typography> </Typography>
<Link <Box
component={Link}
to={`/profile/${featuredReview.user}`} to={`/profile/${featuredReview.user}`}
style={{ sx={{
textDecoration: 'none', textDecoration: 'none',
color: 'inherit', color: 'inherit',
display: 'flex', display: 'flex',
@@ -460,14 +397,14 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
> >
<Avatar <Avatar
src={featuredReview.user_avatar || undefined} src={featuredReview.user_avatar || undefined}
sx={{ width: 24, height: 24, fontSize: '0.6rem' }} sx={{ width: 22, height: 22, fontSize: '0.6rem' }}
> >
{!featuredReview.user_avatar && featuredReview.user.charAt(0)} {!featuredReview.user_avatar && featuredReview.user.charAt(0)}
</Avatar> </Avatar>
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
— {featuredReview.user} — {featuredReview.user}
</Typography> </Typography>
</Link> </Box>
</GlassCard> </GlassCard>
)} )}
@@ -479,7 +416,7 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
mt: { xs: 3, md: 4 }, mt: { xs: 3, md: 4 },
width: { xs: '100%', sm: 'fit-content' }, width: { xs: '100%', sm: 'fit-content' },
px: 4, px: 4,
py: 1.5, py: 1.25,
borderRadius: 2, borderRadius: 2,
}} }}
> >
@@ -487,97 +424,66 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
</Button> </Button>
</CardContent> </CardContent>
</GlassCard> </GlassCard>
</Box>
{topFlavors.length > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, gap: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, gap: 1 }}>
{topFlavors.map((_, i) => ( {topFlavors.map((_, i) => (
<Box <Box
key={i} key={i}
component="button"
onClick={() => setActiveIndex(i)} onClick={() => setActiveIndex(i)}
aria-label={t('home.goToFlavor', { n: i + 1 })}
aria-current={safeIndex === i ? 'true' : undefined}
sx={{ sx={{
width: activeIndex === i ? 24 : 8, width: safeIndex === i ? 24 : 8,
height: 8, height: 8,
p: 0,
border: 'none',
borderRadius: 4, borderRadius: 4,
bgcolor: activeIndex === i ? 'primary.main' : 'divider', bgcolor: safeIndex === i ? 'primary.main' : 'divider',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
'&:focus-visible': {
outline: '2px solid',
outlineColor: 'primary.main',
outlineOffset: 2,
},
}} }}
/> />
))} ))}
</Box> </Box>
</Box>
)} )}
</>
{/* Recent Reviews */} ) : (
{recentReviews.length > 0 && ( <EmptyState title={t('home.noTopFlavors')} />
<Box
sx={{
flex: { xs: '1 1 100%', xl: '1 1 35%' },
minWidth: 0,
display: 'flex',
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={{
position: 'absolute',
top: 4,
right: 4,
fontSize: '0.65rem',
py: 0,
textTransform: 'none',
fontWeight: 'bold',
zIndex: 2,
}}
>
{t('admin.manageRating')}
</Button>
)}
</Box>
))}
<Button
component={Link}
to="/community"
variant="text"
size="small"
sx={{ alignSelf: 'center', mt: 1 }}
>
{t('home.viewMoreReviews')}
</Button>
</Box>
</Box>
)} )}
</Box> </Box>
{/* Newest Arrivals */} {/* Newest Arrivals */}
{newestFlavors.length > 0 && (
<Box> <Box>
<SectionHeader title={t('home.newArrivals')} /> <SectionHeader title={t('home.newArrivals')} subtitle={t('home.newArrivalsSubtitle')} />
{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>
); );
}; };