diff --git a/backend/api/views.py b/backend/api/views.py index 6e05329..e99404f 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,5 +1,5 @@ from django.core.mail import send_mail -from rest_framework import viewsets, permissions, status +from rest_framework import viewsets, permissions, status, filters from rest_framework.response import Response from rest_framework.decorators import action from django.db.models import Avg @@ -12,13 +12,29 @@ from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, R class FlavorViewSet(viewsets.ModelViewSet): queryset = Flavor.objects.annotate(average_rating=Avg('ratings__score')).order_by('-average_rating') serializer_class = FlavorSerializer - filter_backends = [DjangoFilterBackend] + filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ['category', 'category__slug'] + search_fields = ['name', 'description'] permission_classes = [permissions.IsAuthenticatedOrReadOnly] 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 search(self, request): + # Unpaginated list of all flavors for the search autocomplete + flavors = Flavor.objects.select_related('category').order_by('name') + data = [ + { + 'id': f.id, + 'name': f.name, + 'image_url': request.build_absolute_uri(f.image.url) if f.image else f.image_url, + 'category_name': f.category.name + } + for f in flavors + ] + return Response(data) + @action(detail=False, methods=['get'], permission_classes=[permissions.AllowAny]) def top(self, request): top_flavors = self.get_queryset().filter(ratings__isnull=False).distinct()[:10] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9474911..6c14ba5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -57,14 +57,15 @@ const GlobalSearch = () => { useEffect(() => { const fetchFlavors = async () => { try { - const res = await api.get('flavors/'); - setOptions(Array.isArray(res.data) ? res.data : (res.data.results || [])); + // Fetch from unpaginated search endpoint for full catalog coverage + const res = await api.get('flavors/search/'); + setOptions(res.data); } catch (err) { console.error('Failed to fetch search options'); } }; fetchFlavors(); - }, []); + }, []); // Only fetch once to avoid excessive API calls useEffect(() => { const params = new URLSearchParams(location.search); diff --git a/frontend/src/pages/CategoryList.tsx b/frontend/src/pages/CategoryList.tsx index 6c890d6..1a76061 100644 --- a/frontend/src/pages/CategoryList.tsx +++ b/frontend/src/pages/CategoryList.tsx @@ -68,7 +68,8 @@ const CategoryList: React.FC = () => { setLoading(true); try { if (query) { - const res = await api.get(`flavors/`); + // Perform server-side search to get all matching flavors across pages + const res = await api.get(`flavors/?search=${encodeURIComponent(query)}`); setAllFlavors(Array.isArray(res.data) ? res.data : (res.data.results || [])); } else { const [catRes, topRes, recentRes] = await Promise.all([