From a3c4e1bbc811b08c7a5e041be43b265a4b7ac3bb Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Feb 2026 21:16:21 +0100 Subject: [PATCH] added follow list --- backend/api/views.py | 15 +- frontend/src/pages/CommunityFeed.tsx | 286 +++++++++++++++++---------- 2 files changed, 199 insertions(+), 102 deletions(-) diff --git a/backend/api/views.py b/backend/api/views.py index 54a0eb9..ebce8eb 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -6,9 +6,13 @@ from django.db.models import Avg from django_filters.rest_framework import DjangoFilterBackend from django_ratelimit.decorators import ratelimit from django.utils.decorators import method_decorator +from rest_framework.pagination import PageNumberPagination from .models import User, Flavor, Category, Rating, Reply from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, RatingSerializer, ReplySerializer +class FeedPagination(PageNumberPagination): + page_size = 10 + class FlavorViewSet(viewsets.ModelViewSet): queryset = Flavor.objects.annotate(average_rating=Avg('ratings__score')).order_by('-average_rating') serializer_class = FlavorSerializer @@ -77,10 +81,11 @@ class RatingViewSet(viewsets.ModelViewSet): followed_users = request.user.following.all() feed_ratings = Rating.objects.filter(user__in=followed_users).select_related('user', 'flavor', 'flavor__category').prefetch_related('replies', 'replies__user').order_by('-created_at') - page = self.paginate_queryset(feed_ratings) + paginator = FeedPagination() + page = paginator.paginate_queryset(feed_ratings, request) if page is not None: serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) + return paginator.get_paginated_response(serializer.data) serializer = self.get_serializer(feed_ratings, many=True) return Response(serializer.data) @@ -116,6 +121,12 @@ class UserViewSet(viewsets.ModelViewSet): return [permissions.AllowAny()] return super().get_permissions() + @action(detail=False, methods=['get'], permission_classes=[permissions.IsAuthenticated]) + def following_list(self, request): + following = request.user.following.all() + serializer = self.get_serializer(following, many=True) + return Response(serializer.data) + @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) @method_decorator(ratelimit(key='ip', rate='5/h', method='POST', block=True)) def signup(self, request): diff --git a/frontend/src/pages/CommunityFeed.tsx b/frontend/src/pages/CommunityFeed.tsx index 559c654..0d663d2 100644 --- a/frontend/src/pages/CommunityFeed.tsx +++ b/frontend/src/pages/CommunityFeed.tsx @@ -9,7 +9,13 @@ import { CircularProgress, Divider, Rating as MuiRating, - Button + Button, + Grid, + List, + ListItem, + ListItemAvatar, + ListItemText, + ListItemButton } from '@mui/material'; import StarIcon from '@mui/icons-material/Star'; import { Link } from 'react-router-dom'; @@ -31,126 +37,206 @@ interface FeedRating { created_at: string; } +interface FollowedUser { + id: number; + username: string; + avatar: string | null; +} + const CommunityFeed: React.FC = () => { useTitle('Community Feed'); const [ratings, setRatings] = useState([]); + const [following, setFollowing] = useState([]); const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [nextPage, setNextPage] = useState(null); + + const fetchInitialData = async () => { + setLoading(true); + try { + const [feedRes, followingRes] = await Promise.all([ + api.get('ratings/feed/'), + api.get('users/following_list/') + ]); + + const feedData = feedRes.data.results || feedRes.data; + setRatings(Array.isArray(feedData) ? feedData : []); + setNextPage(feedRes.data.next || null); + setFollowing(followingRes.data); + } catch (err) { + console.error('Failed to fetch initial feed data', err); + } finally { + setLoading(false); + } + }; + + const handleLoadMore = async () => { + if (!nextPage || loadingMore) return; + setLoadingMore(true); + try { + // nextPage is a full URL from DRF, we can use axios directly or parse it + const res = await api.get(nextPage); + const newData = res.data.results || res.data; + setRatings(prev => [...prev, ...(Array.isArray(newData) ? newData : [])]); + setNextPage(res.data.next || null); + } catch (err) { + console.error('Failed to load more feed items', err); + } finally { + setLoadingMore(false); + } + }; useEffect(() => { - const fetchFeed = async () => { - try { - const res = await api.get('ratings/feed/'); - // Handle pagination if results is present - const data = Array.isArray(res.data) ? res.data : (res.data.results || []); - setRatings(data); - } catch (err) { - console.error('Failed to fetch feed', err); - } finally { - setLoading(false); - } - }; - fetchFeed(); + fetchInitialData(); }, []); if (loading) return ; return ( - + Community Feed - {ratings.length === 0 ? ( - - Your feed is quiet... - - Follow other Holy Energy fans to see their latest flavor ratings and reviews here! - - - - ) : ( - - {ratings.map(rating => ( - - - - - - {!rating.user_avatar && rating.user.charAt(0).toUpperCase()} - - - - {rating.user} - - - {new Date(rating.created_at).toLocaleDateString()} - - - + + {/* Main Feed */} + + {ratings.length === 0 ? ( + + Your feed is quiet... + + Follow other Holy Energy fans to see their latest flavor ratings and reviews here! + + - - - - - - - - - - - - - {rating.flavor_name} - + ) : ( + + {ratings.map(rating => ( + + + + + + {!rating.user_avatar && rating.user.charAt(0).toUpperCase()} + + + + {rating.user} + + + {new Date(rating.created_at).toLocaleDateString()} + + - - {rating.category_name} - - - - {/* Compact rating for mobile, stars for desktop */} - - - {rating.score}/10 - - - - {rating.score}/10 - - {rating.comment && ( - - "{rating.comment}" - - )} + + + + + + + + + + + + {rating.flavor_name} + + + + {rating.category_name} + + + + {/* Compact rating for mobile, stars for desktop */} + + + {rating.score}/10 + + + + {rating.score}/10 + + + + {rating.comment && ( + + "{rating.comment}" + + )} + + + + + ))} + + {nextPage && ( + + + )} - - - ))} - - )} + )} + + + {/* Sidebar */} + + + + + Following ({following.length}) + + {following.length === 0 ? ( + + You aren't following anyone yet. + + ) : ( + + {following.map((user) => ( + + + + + + {!user.avatar && user.username.charAt(0).toUpperCase()} + + + + + + + + ))} + + )} + + + + ); }; -// Internal helper for empty state -const Paper = ({ children, sx }: any) => ( - - {children} - -); - export default CommunityFeed;