mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
filter flavor on hall of fame + setup categories for future new product lineups
This commit is contained in:
1 parent
f3ca748c55
commit
5e3474dd59
3 files changed
+135
-11
No files matched your search
+44
-2
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<MainPageProps> = ({ adminMode }) => {
|
||||
const [recentReviews, setRecentReviews] = useState<Review[]>([]);
|
||||
const [allFlavors, setAllFlavors] = useState<Flavor[]>([]);
|
||||
const [feedRatings, setFeedRatings] = useState<any[]>([]);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [selectedCategorySlug, setSelectedCategorySlug] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [categoryLoading, setCategoryLoading] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | 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<MainPageProps> = ({ 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<MainPageProps> = ({ 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<MainPageProps> = ({ 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<MainPageProps> = ({ adminMode }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Card sx={{
|
||||
<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 sx={{ position: 'relative', minHeight: { xs: 400, md: 500 } }}>
|
||||
{categoryLoading && (
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0, right: 0, bottom: 0,
|
||||
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
||||
bgcolor: 'rgba(255,255,255,0.5)',
|
||||
zIndex: 2,
|
||||
borderRadius: 4
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
<Card sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
height: { xs: 'auto', md: 500 },
|
||||
@@ -457,10 +534,11 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
|
||||
>
|
||||
{t('home.viewAllReviews')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Progress Indicators */}
|
||||
{/* Progress Indicators */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3, gap: 1 }}>
|
||||
{topFlavors.map((_, i) => (
|
||||
<Box
|
||||
|
||||
Reference in new issue
Block a user