From 3a9eec3f222f8025be7c25f6b800dd5fc8b6d479 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Feb 2026 11:35:53 +0100 Subject: [PATCH] added public profiles --- backend/api/views.py | 17 ++++ frontend/src/App.tsx | 2 + frontend/src/pages/Dashboard.tsx | 27 +++++- frontend/src/pages/PublicProfile.tsx | 127 +++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/PublicProfile.tsx diff --git a/backend/api/views.py b/backend/api/views.py index a2bb444..0c14a87 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -127,6 +127,23 @@ class UserViewSet(viewsets.ModelViewSet): else: return Response({'error': 'Invalid verification code'}, status=status.HTTP_400_BAD_REQUEST) + @action(detail=False, methods=['get'], permission_classes=[permissions.AllowAny], url_path='profile/(?P[^/.]+)') + def public_profile(self, request, username=None): + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) + + ratings = user.ratings.select_related('flavor', 'flavor__category').all() + # Sort by score descending to highlight favorites + ratings = sorted(ratings, key=lambda x: x.score, reverse=True) + + return Response({ + 'username': user.username, + 'theme': user.theme, + 'ratings': RatingSerializer(ratings, many=True).data + }) + @action(detail=False, methods=['get']) def me(self, request): serializer = self.get_serializer(request.user) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d5c7b19..a828726 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,6 +27,7 @@ import CategoryList from './pages/CategoryList'; import CategoryFlavors from './pages/categories/CategoryFlavors'; import FlavorDetail from './pages/flavors/FlavorDetail'; import Dashboard from './pages/Dashboard'; +import PublicProfile from './pages/PublicProfile'; import Login from './pages/Login'; import Settings from './pages/Settings'; import api from './api'; @@ -185,6 +186,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 71f1f5b..11986e9 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,9 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { Typography, Card, CardContent, Box, Rating as MuiRating, Button } from '@mui/material'; +import { Typography, Card, CardContent, Box, Rating as MuiRating, Button, TextField } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import api from '../api'; import { useTitle } from '../hooks/useTitle'; interface DashboardData { + user: { username: string }; rated_count: number; missing_count: number; missing_flavors: any[]; @@ -15,6 +17,13 @@ const Dashboard: React.FC = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); + const shareUrl = data ? `${window.location.origin}/profile/${data.user.username}` : ''; + + const copyToClipboard = () => { + navigator.clipboard.writeText(shareUrl); + alert('Link copied to clipboard!'); + }; + useEffect(() => { const fetchData = async () => { try { @@ -34,7 +43,21 @@ const Dashboard: React.FC = () => { return ( - Personal Dashboard + + Personal Dashboard + + + + + + My Ratings ({data.rated_count}) diff --git a/frontend/src/pages/PublicProfile.tsx b/frontend/src/pages/PublicProfile.tsx new file mode 100644 index 0000000..1169530 --- /dev/null +++ b/frontend/src/pages/PublicProfile.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { + Typography, + Box, + Card, + CardContent, + Rating as MuiRating, + Avatar, + Chip +} from '@mui/material'; +import api from '../api'; +import { useTitle } from '../hooks/useTitle'; + +interface Rating { + id: number; + flavor_name: string; + flavor_image: string | null; + score: number; + comment: string; + created_at: string; +} + +interface ProfileData { + username: string; + theme: string; + ratings: Rating[]; +} + +const PublicProfile: React.FC = () => { + const { username } = useParams<{ username: string }>(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchProfile = async () => { + try { + const res = await api.get(`users/profile/${username}/`); + setData(res.data); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + fetchProfile(); + }, [username]); + + useTitle(data ? `${data.username}'s Ratings` : 'Profile'); + + if (loading) return Loading Profile...; + if (!data) return Profile not found.; + + const favorites = data.ratings.filter(r => r.score >= 8); + const others = data.ratings.filter(r => r.score < 8); + + return ( + + + + {data.username.charAt(0).toUpperCase()} + + + {data.username} + + Shared {data.ratings.length} flavor ratings + + + + + {favorites.length > 0 && ( + + + Top Favorites + + + {favorites.map(rating => ( + + + {rating.flavor_image && ( + + )} + + {rating.flavor_name} + + + {rating.score}/10 + + {rating.comment && ( + "{rating.comment}" + )} + + + + ))} + + + )} + + {others.length > 0 && ( + + Other Ratings + + {others.map(rating => ( + + + {rating.flavor_name} + + + {rating.score}/10 + + {rating.comment && ( + + "{rating.comment}" + + )} + + + ))} + + + )} + + ); +}; + +export default PublicProfile;