diff --git a/backend/api/tests.py b/backend/api/tests.py index 7ce503c..90358db 100644 --- a/backend/api/tests.py +++ b/backend/api/tests.py @@ -1,3 +1,45 @@ -from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from .models import Category, Flavor, User, Rating -# Create your tests here. +class FlavorTopTests(APITestCase): + def setUp(self): + self.category_energy = Category.objects.create(name='Energy', slug='energy') + self.category_iced_tea = Category.objects.create(name='Iced Tea', slug='iced-tea') + + self.user = User.objects.create_user(username='testuser', password='password') + + # Create flavors + self.flavor1 = Flavor.objects.create(name='Energy 1', category=self.category_energy) + self.flavor2 = Flavor.objects.create(name='Iced Tea 1', category=self.category_iced_tea) + + # Add ratings to make them appear in top (since we filter by ratings__isnull=False) + Rating.objects.create(user=self.user, flavor=self.flavor1, score=5) + Rating.objects.create(user=self.user, flavor=self.flavor2, score=4) + + def test_top_flavors_all(self): + url = reverse('flavor-top') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + def test_top_flavors_filtered_energy(self): + url = reverse('flavor-top') + response = self.client.get(url, {'category': 'energy'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'Energy 1') + + def test_top_flavors_filtered_iced_tea(self): + url = reverse('flavor-top') + response = self.client.get(url, {'category': 'iced-tea'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'Iced Tea 1') + + def test_top_flavors_filtered_non_existent(self): + url = reverse('flavor-top') + response = self.client.get(url, {'category': 'non-existent'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 0) diff --git a/backend/api/views.py b/backend/api/views.py index 24f1e11..ae4317f 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -165,7 +165,11 @@ class FlavorViewSet(viewsets.ModelViewSet): @action(detail=False, methods=['get'], permission_classes=[permissions.AllowAny]) def top(self, request): - top_flavors = self.get_queryset().filter(ratings__isnull=False).distinct()[:10] + queryset = self.get_queryset().filter(ratings__isnull=False) + category_slug = request.query_params.get('category') + if category_slug: + queryset = queryset.filter(category__slug=category_slug) + top_flavors = queryset.distinct()[:10] serializer = self.get_serializer(top_flavors, many=True) return Response(serializer.data) diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index d4417a6..7d4b333 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -11,7 +11,9 @@ import { Chip, Container, IconButton, - alpha + alpha, + Tabs, + Tab } from '@mui/material'; import { Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -25,6 +27,12 @@ import RatingBadge from '../components/RatingBadge'; import StatusBadge from '../components/StatusBadge'; import { useNavigate } from 'react-router-dom'; +interface Category { + id: number; + name: string; + slug: string; +} + interface Flavor { id: number; name: string; @@ -61,9 +69,13 @@ const MainPage: React.FC = ({ adminMode }) => { const [recentReviews, setRecentReviews] = useState([]); const [allFlavors, setAllFlavors] = useState([]); const [feedRatings, setFeedRatings] = useState([]); + const [categories, setCategories] = useState([]); + const [selectedCategorySlug, setSelectedCategorySlug] = useState(''); const [loading, setLoading] = useState(true); + const [categoryLoading, setCategoryLoading] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const timerRef = useRef | null>(null); + const isFirstRun = useRef(true); const location = useLocation(); const query = new URLSearchParams(location.search).get('q') || ''; @@ -81,7 +93,8 @@ const MainPage: React.FC = ({ adminMode }) => { const requests: any[] = [ api.get('flavors/top/'), api.get('ratings/recent/'), - api.get('flavors/newest/') + api.get('flavors/newest/'), + api.get('categories/') ]; if (isLoggedIn) { @@ -93,9 +106,10 @@ const MainPage: React.FC = ({ adminMode }) => { setTopFlavors(Array.isArray(results[0].data) ? results[0].data : (results[0].data.results || [])); setRecentReviews(Array.isArray(results[1].data) ? results[1].data : (results[1].data.results || [])); setNewestFlavors(Array.isArray(results[2].data) ? results[2].data : (results[2].data.results || [])); + setCategories(Array.isArray(results[3].data) ? results[3].data : (results[3].data.results || [])); - if (isLoggedIn && results[3]) { - const feedData = Array.isArray(results[3].data) ? results[3].data : (results[3].data.results || []); + if (isLoggedIn && results[4]) { + const feedData = Array.isArray(results[4].data) ? results[4].data : (results[4].data.results || []); setFeedRatings(feedData.slice(0, 6)); // Show top 6 on home } } @@ -108,6 +122,35 @@ const MainPage: React.FC = ({ adminMode }) => { fetchData(); }, [query, isLoggedIn]); + useEffect(() => { + if (query) return; // Don't refetch if we are in search mode + + const fetchTopFlavors = async () => { + setCategoryLoading(true); + try { + const url = selectedCategorySlug ? `flavors/top/?category=${selectedCategorySlug}` : 'flavors/top/'; + const res = await api.get(url); + setTopFlavors(Array.isArray(res.data) ? res.data : (res.data.results || [])); + setActiveIndex(0); // Reset carousel + } catch (err) { + console.error(err); + } finally { + setCategoryLoading(false); + } + }; + + if (isFirstRun.current) { + isFirstRun.current = false; + return; + } + + fetchTopFlavors(); + }, [selectedCategorySlug, query]); + + const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => { + setSelectedCategorySlug(newValue); + }; + const startTimer = () => { if (timerRef.current) clearInterval(timerRef.current); timerRef.current = setInterval(() => { @@ -379,7 +422,41 @@ const MainPage: React.FC = ({ adminMode }) => { - + + + {categories.map((cat) => ( + + ))} + + + + + {categoryLoading && ( + + + + )} + = ({ adminMode }) => { > {t('home.viewAllReviews')} - - - - {/* Progress Indicators */} + + + + + {/* Progress Indicators */} {topFlavors.map((_, i) => (