mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
added explore page on frontpage
This commit is contained in:
1 parent
8aaf4fc3d7
commit
3e6d08ee16
6 files changed
+174
-2
No files matched your search
@@ -7,6 +7,7 @@ export const queryKeys = {
|
||||
categories: () => ['categories'] as const,
|
||||
dashboard: () => ['dashboard'] as const,
|
||||
recommendations: () => ['recommendations'] as const,
|
||||
popularRecommendations: () => ['recommendations', 'popular'] as const,
|
||||
publicProfile: (username: string) => ['publicProfile', username] as const,
|
||||
publicProfileAll: () => ['publicProfile'] as const,
|
||||
flavors: (params: Record<string, unknown>) => ['flavors', params] as const,
|
||||
|
||||
@@ -18,3 +18,20 @@ export const useRecommendations = (enabled = true) =>
|
||||
enabled,
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
|
||||
/**
|
||||
* Public "popular picks" — the recommender's Bayesian-popularity ranking over the
|
||||
* whole catalog (backend `GET /flavors/popular/`), used for logged-out front-page
|
||||
* discovery. Same payload shape as {@link useRecommendations}; every row is
|
||||
* `reason: "popular"`. Popularity shifts slowly, so a long staleTime is fine.
|
||||
*/
|
||||
export const usePopularRecommendations = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: queryKeys.popularRecommendations(),
|
||||
queryFn: async () => {
|
||||
const res = await api.get<Recommendation[]>('flavors/popular/');
|
||||
return res.data;
|
||||
},
|
||||
enabled,
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
@@ -107,6 +107,10 @@ const resources = {
|
||||
exploreFlavors: 'Explore Flavors',
|
||||
hallOfFame: 'Hall of Fame',
|
||||
hallOfFameSubtitle: 'Highest-rated flavors in the archive',
|
||||
recForYou: 'Recommended for You',
|
||||
recForYouSubtitle: 'Picked by tasters who share your palate',
|
||||
popularPicks: 'Popular Picks',
|
||||
popularPicksSubtitle: 'Crowd favorites worth discovering',
|
||||
communityVoice: 'Community Voice',
|
||||
newArrivals: 'New Arrivals',
|
||||
newArrivalsSubtitle: 'The latest additions to the catalog',
|
||||
@@ -425,6 +429,10 @@ const resources = {
|
||||
exploreFlavors: 'Sorten entdecken',
|
||||
hallOfFame: 'Hall of Fame',
|
||||
hallOfFameSubtitle: 'Die bestbewerteten Sorten im Archiv',
|
||||
recForYou: 'Für dich empfohlen',
|
||||
recForYouSubtitle: 'Ausgewählt von Testern mit ähnlichem Geschmack',
|
||||
popularPicks: 'Beliebte Sorten',
|
||||
popularPicksSubtitle: 'Favoriten der Community zum Entdecken',
|
||||
communityVoice: 'Community-Stimmen',
|
||||
newArrivals: 'Neuheiten',
|
||||
newArrivalsSubtitle: 'Die neuesten Zugänge im Katalog',
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
FlavorCard,
|
||||
EmptyState,
|
||||
} from '../components/ui';
|
||||
import MainPageRecommendations from './MainPageRecommendations';
|
||||
|
||||
const TOP_LIMIT = 5;
|
||||
|
||||
@@ -459,6 +460,9 @@ const MainPage: React.FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Algorithmic discovery: personalized (members) or popular picks (visitors) */}
|
||||
<MainPageRecommendations isLoggedIn={isLoggedIn} />
|
||||
|
||||
{/* Newest Arrivals */}
|
||||
<Box>
|
||||
<SectionHeader title={t('home.newArrivals')} subtitle={t('home.newArrivalsSubtitle')} />
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Box, Skeleton } from '@mui/material';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRecommendations, usePopularRecommendations } from '../api/queries/useRecommendations';
|
||||
import RecommendationCard from './dashboard/RecommendationCard';
|
||||
import { SectionHeader } from '../components/ui';
|
||||
|
||||
export interface MainPageRecommendationsProps {
|
||||
/** Logged-in → personalized CF recs; logged-out → public popularity picks. */
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
const GRID_SX = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: {
|
||||
xs: 'repeat(2, minmax(0, 1fr))',
|
||||
sm: 'repeat(3, minmax(0, 1fr))',
|
||||
md: 'repeat(4, minmax(0, 1fr))',
|
||||
lg: 'repeat(6, minmax(0, 1fr))',
|
||||
},
|
||||
gap: 2,
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Algorithmic discovery row for the front page. Members get "Recommended for you"
|
||||
* (collaborative filtering); visitors get "Popular Picks" (Bayesian-popularity
|
||||
* ranking over the whole catalog). Both reuse {@link RecommendationCard}. The
|
||||
* section hides itself entirely when the engine returns nothing.
|
||||
*/
|
||||
const MainPageRecommendations: React.FC<MainPageRecommendationsProps> = ({ isLoggedIn }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const personalized = useRecommendations(isLoggedIn);
|
||||
const popular = usePopularRecommendations(!isLoggedIn);
|
||||
const { data, isLoading } = isLoggedIn ? personalized : popular;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box>
|
||||
<Skeleton variant="text" width={260} height={36} sx={{ mb: 2 }} />
|
||||
<Box sx={GRID_SX}>
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} variant="rounded" height={240} sx={{ borderRadius: 2 }} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SectionHeader
|
||||
title={
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}>
|
||||
<AutoAwesomeIcon color="primary" sx={{ fontSize: '1.75rem' }} />
|
||||
<span>{isLoggedIn ? t('home.recForYou') : t('home.popularPicks')}</span>
|
||||
</Box>
|
||||
}
|
||||
subtitle={isLoggedIn ? t('home.recForYouSubtitle') : t('home.popularPicksSubtitle')}
|
||||
/>
|
||||
<Box sx={GRID_SX}>
|
||||
{data.map((rec) => (
|
||||
<RecommendationCard key={rec.id} rec={rec} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageRecommendations;
|
||||
Reference in new issue
Block a user