feat: display user ratings on public profiles and add profile links

- Add comprehensive ratings section to PublicProfilePage showing average rating, individual reviews with comments, and collaboration preferences
- Make partner avatars and names clickable in MatchesPage and MatchChatPage to navigate to their public profiles
- Add hover effects on profile links for better UX
- Fetch and display ratings using ratingsAPI endpoint
This commit is contained in:
Radosław Gierwiało
2025-11-14 22:48:30 +01:00
parent 49e492a8f8
commit c2f4eddb55
3 changed files with 163 additions and 25 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate, Link } from 'react-router-dom';
import Layout from '../components/layout/Layout';
import { useAuth } from '../contexts/AuthContext';
import { matchesAPI } from '../services/api';
@@ -252,17 +252,21 @@ const MatchChatPage = () => {
<div className="bg-primary-600 text-white p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Link to={`/${partner.username}`} className="flex-shrink-0">
<img
src={partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${partner.username}`}
alt={partner.username}
className="w-12 h-12 rounded-full border-2 border-white"
className="w-12 h-12 rounded-full border-2 border-white hover:ring-2 hover:ring-white hover:ring-offset-2 hover:ring-offset-primary-600 transition-all"
/>
</Link>
<div>
<h2 className="text-xl font-bold">
<Link to={`/${partner.username}`}>
<h2 className="text-xl font-bold hover:text-primary-100 transition-colors">
{partner.firstName && partner.lastName
? `${partner.firstName} ${partner.lastName}`
: partner.username}
</h2>
</Link>
<p className="text-sm text-primary-100">@{partner.username}</p>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, Link } from 'react-router-dom';
import Layout from '../components/layout/Layout';
import { useAuth } from '../contexts/AuthContext';
import { matchesAPI } from '../services/api';
@@ -246,17 +246,21 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Link to={`/${match.partner.username}`} className="flex-shrink-0">
<img
src={match.partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${match.partner.username}`}
alt={match.partner.username}
className="w-12 h-12 rounded-full"
className="w-12 h-12 rounded-full hover:ring-2 hover:ring-primary-500 transition-all"
/>
</Link>
<div>
<h3 className="font-semibold text-gray-900">
<Link to={`/${match.partner.username}`}>
<h3 className="font-semibold text-gray-900 hover:text-primary-600 transition-colors">
{match.partner.firstName && match.partner.lastName
? `${match.partner.firstName} ${match.partner.lastName}`
: match.partner.username}
</h3>
</Link>
<p className="text-sm text-gray-600">@{match.partner.username}</p>
</div>
</div>

View File

@@ -1,14 +1,16 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { authAPI } from '../services/api';
import { authAPI, ratingsAPI } from '../services/api';
import Layout from '../components/layout/Layout';
import { User, MapPin, Globe, Hash, Youtube, Instagram, Facebook, Award, Users, Star, Calendar, Loader2, AlertCircle } from 'lucide-react';
import { User, MapPin, Globe, Hash, Youtube, Instagram, Facebook, Award, Users, Star, Calendar, Loader2, AlertCircle, ThumbsUp } from 'lucide-react';
const PublicProfilePage = () => {
const { username } = useParams();
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [ratings, setRatings] = useState(null);
const [ratingsLoading, setRatingsLoading] = useState(true);
useEffect(() => {
const fetchProfile = async () => {
@@ -26,6 +28,22 @@ const PublicProfilePage = () => {
fetchProfile();
}, [username]);
useEffect(() => {
const fetchRatings = async () => {
try {
setRatingsLoading(true);
const data = await ratingsAPI.getUserRatings(username);
setRatings(data.data);
} catch (err) {
console.error('Failed to load ratings:', err);
} finally {
setRatingsLoading(false);
}
};
fetchRatings();
}, [username]);
if (loading) {
return (
<Layout>
@@ -150,7 +168,7 @@ const PublicProfilePage = () => {
{/* Social Media Links */}
{(profile.youtubeUrl || profile.instagramUrl || profile.facebookUrl || profile.tiktokUrl) && (
<div className="bg-white rounded-lg shadow-sm p-6">
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Social Media</h2>
<div className="flex flex-wrap gap-3">
{profile.youtubeUrl && (
@@ -202,6 +220,118 @@ const PublicProfilePage = () => {
</div>
</div>
)}
{/* Ratings Section */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<Star className="w-5 h-5 text-yellow-500" />
Reviews
</h2>
{ratingsLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="w-8 h-8 animate-spin text-primary-600" />
</div>
) : ratings && ratings.ratings.length > 0 ? (
<div className="space-y-4">
{/* Summary */}
<div className="flex items-center gap-4 pb-4 border-b">
<div className="flex items-center gap-2">
<span className="text-4xl font-bold text-gray-900">{ratings.averageRating}</span>
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`w-6 h-6 ${
star <= Math.round(parseFloat(ratings.averageRating))
? 'fill-yellow-400 text-yellow-400'
: 'text-gray-300'
}`}
/>
))}
</div>
</div>
<span className="text-gray-600">
Based on {ratings.ratingsCount} {ratings.ratingsCount === 1 ? 'review' : 'reviews'}
</span>
</div>
{/* Individual Ratings */}
<div className="space-y-4">
{ratings.ratings.map((rating) => (
<div key={rating.id} className="border-b pb-4 last:border-b-0">
<div className="flex items-start gap-4">
{/* Rater Info */}
<Link to={`/${rating.rater.username}`} className="flex-shrink-0">
<img
src={rating.rater.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${rating.rater.username}`}
alt={rating.rater.username}
className="w-12 h-12 rounded-full hover:ring-2 hover:ring-primary-500 transition-all"
/>
</Link>
<div className="flex-1">
{/* Rater Name and Stars */}
<div className="flex items-start justify-between mb-2">
<div>
<Link
to={`/${rating.rater.username}`}
className="font-semibold text-gray-900 hover:text-primary-600"
>
{rating.rater.firstName && rating.rater.lastName
? `${rating.rater.firstName} ${rating.rater.lastName}`
: rating.rater.username}
</Link>
<p className="text-sm text-gray-600">@{rating.rater.username}</p>
</div>
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`w-5 h-5 ${
star <= rating.score
? 'fill-yellow-400 text-yellow-400'
: 'text-gray-300'
}`}
/>
))}
</div>
</div>
{/* Comment */}
{rating.comment && (
<p className="text-gray-700 mb-2">{rating.comment}</p>
)}
{/* Would Collaborate Again */}
{rating.wouldCollaborateAgain && (
<div className="flex items-center gap-1 text-sm text-green-600 mb-2">
<ThumbsUp className="w-4 h-4" />
<span>Would collaborate again</span>
</div>
)}
{/* Event and Date */}
<div className="flex items-center gap-4 text-sm text-gray-500">
<Link
to={`/events/${rating.event.slug}`}
className="hover:text-primary-600"
>
{rating.event.name}
</Link>
<span></span>
<span>{new Date(rating.createdAt).toLocaleDateString()}</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
) : (
<p className="text-gray-600 text-center py-8">No reviews yet</p>
)}
</div>
</div>
</div>
</Layout>