From 8a9d110f683ee94b39acc99d2a00e176eb184881 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Feb 2026 14:21:37 +0100 Subject: [PATCH] updated frontpage --- backend/api/views.py | 12 ++ frontend/src/pages/CategoryList.tsx | 198 +++++++++++++++++++++++----- 2 files changed, 179 insertions(+), 31 deletions(-) diff --git a/backend/api/views.py b/backend/api/views.py index 0c14a87..0056372 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -17,6 +17,12 @@ class FlavorViewSet(viewsets.ModelViewSet): def get_queryset(self): return Flavor.objects.select_related('category').annotate(average_rating=Avg('ratings__score')).order_by('-average_rating') + @action(detail=False, methods=['get'], permission_classes=[permissions.AllowAny]) + def top(self, request): + top_flavors = self.get_queryset().filter(ratings__isnull=False).distinct()[:10] + serializer = self.get_serializer(top_flavors, many=True) + return Response(serializer.data) + class CategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer @@ -47,6 +53,12 @@ class RatingViewSet(viewsets.ModelViewSet): if request.method in ['PUT', 'PATCH', 'DELETE'] and obj.user != request.user: self.permission_denied(request, message='You cannot edit/delete this rating.') + @action(detail=False, methods=['get'], permission_classes=[permissions.AllowAny]) + def recent(self, request): + recent_ratings = Rating.objects.filter(comment__isnull=False).exclude(comment='').select_related('user', 'flavor').order_by('-created_at')[:10] + serializer = self.get_serializer(recent_ratings, many=True) + return Response(serializer.data) + @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def reply(self, request, pk=None): rating = self.get_object() diff --git a/frontend/src/pages/CategoryList.tsx b/frontend/src/pages/CategoryList.tsx index 65d13a4..5e8255a 100644 --- a/frontend/src/pages/CategoryList.tsx +++ b/frontend/src/pages/CategoryList.tsx @@ -4,10 +4,15 @@ import { Box, Card, CardContent, + CircularProgress, + Button, + Avatar, Rating as MuiRating, - CircularProgress + Paper, + Chip } from '@mui/material'; import { Link, useLocation } from 'react-router-dom'; +import StarIcon from '@mui/icons-material/Star'; import api from '../api'; import { useTitle } from '../hooks/useTitle'; @@ -28,9 +33,22 @@ interface Flavor { is_legacy: boolean; } +interface Review { + id: number; + user: string; + flavor_name: string; + flavor_image: string | null; + score: number; + comment: string; + created_at: string; + flavor: number; +} + const CategoryList: React.FC = () => { const [categories, setCategories] = useState([]); - const [flavors, setFlavors] = useState([]); + const [topFlavors, setTopFlavors] = useState([]); + const [recentReviews, setRecentReviews] = useState([]); + const [allFlavors, setAllFlavors] = useState([]); const [loading, setLoading] = useState(true); const location = useLocation(); @@ -42,10 +60,16 @@ const CategoryList: React.FC = () => { try { if (query) { const res = await api.get(`flavors/`); - setFlavors(res.data); + setAllFlavors(res.data); } else { - const res = await api.get('categories/'); - setCategories(res.data); + const [catRes, topRes, recentRes] = await Promise.all([ + api.get('categories/'), + api.get('flavors/top/'), + api.get('ratings/recent/') + ]); + setCategories(catRes.data); + setTopFlavors(topRes.data); + setRecentReviews(recentRes.data); } } catch (err) { console.error(err); @@ -56,12 +80,13 @@ const CategoryList: React.FC = () => { fetchData(); }, [query]); - useTitle(query ? `Search: ${query}` : 'Catalog'); + useTitle(query ? `Search: ${query}` : 'Holy Flavors Archive'); if (loading) return ; + // --- SEARCH VIEW --- if (query) { - const filteredFlavors = flavors.filter(f => + const filteredFlavors = allFlavors.filter(f => f.name.toLowerCase().includes(query.toLowerCase()) || f.description.toLowerCase().includes(query.toLowerCase()) || f.category_name.toLowerCase().includes(query.toLowerCase()) @@ -108,34 +133,145 @@ const CategoryList: React.FC = () => { ); } + // --- HOME VIEW --- return ( - Catalog - - - {categories.map(category => ( - - - - - {category.name} - - - + {/* Hero Section */} + theme.palette.mode === 'dark' + ? 'linear-gradient(45deg, #1a1a2e 30%, #16213e 90%)' + : 'linear-gradient(45deg, #fdf6f7 30%, #f8e1e5 90%)', + textAlign: 'center', + border: '1px solid rgba(255,255,255,0.1)' + }} + > + + The Ultimate Holy Archive + + + Browse every flavor ever released, discover new favorites, and share your ratings with the community. + + + + + + + + {/* Top Rated Flavors */} + {topFlavors.length > 0 && ( + + + + Hall of Fame + + + + {topFlavors.map(flavor => ( + + + + {flavor.image_url && ( + + )} + + {flavor.name} + + + + {(flavor.average_rating || 0).toFixed(1)} + + + + + + + ))} + - ))} + )} + + {/* Categories Grid */} + + Browse by Category + + {categories.map(category => ( + + + + + {category.name} + + + + + ))} + + + {/* Recent Reviews */} + {recentReviews.length > 0 && ( + + Community Voice + + {recentReviews.map(review => ( + + + {review.user.charAt(0).toUpperCase()} + + {review.user} + + on {review.flavor_name} + + + + + + {review.score}/10 + + + "{review.comment}" + + + ))} + + + )} ); };