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:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
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 Layout from '../components/layout/Layout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { matchesAPI } from '../services/api';
|
import { matchesAPI } from '../services/api';
|
||||||
@@ -252,17 +252,21 @@ const MatchChatPage = () => {
|
|||||||
<div className="bg-primary-600 text-white p-4">
|
<div className="bg-primary-600 text-white p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<img
|
<Link to={`/${partner.username}`} className="flex-shrink-0">
|
||||||
src={partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${partner.username}`}
|
<img
|
||||||
alt={partner.username}
|
src={partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${partner.username}`}
|
||||||
className="w-12 h-12 rounded-full border-2 border-white"
|
alt={partner.username}
|
||||||
/>
|
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>
|
<div>
|
||||||
<h2 className="text-xl font-bold">
|
<Link to={`/${partner.username}`}>
|
||||||
{partner.firstName && partner.lastName
|
<h2 className="text-xl font-bold hover:text-primary-100 transition-colors">
|
||||||
? `${partner.firstName} ${partner.lastName}`
|
{partner.firstName && partner.lastName
|
||||||
: partner.username}
|
? `${partner.firstName} ${partner.lastName}`
|
||||||
</h2>
|
: partner.username}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
<p className="text-sm text-primary-100">@{partner.username}</p>
|
<p className="text-sm text-primary-100">@{partner.username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 Layout from '../components/layout/Layout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { matchesAPI } from '../services/api';
|
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 items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<img
|
<Link to={`/${match.partner.username}`} className="flex-shrink-0">
|
||||||
src={match.partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${match.partner.username}`}
|
<img
|
||||||
alt={match.partner.username}
|
src={match.partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${match.partner.username}`}
|
||||||
className="w-12 h-12 rounded-full"
|
alt={match.partner.username}
|
||||||
/>
|
className="w-12 h-12 rounded-full hover:ring-2 hover:ring-primary-500 transition-all"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900">
|
<Link to={`/${match.partner.username}`}>
|
||||||
{match.partner.firstName && match.partner.lastName
|
<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}
|
? `${match.partner.firstName} ${match.partner.lastName}`
|
||||||
</h3>
|
: match.partner.username}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
<p className="text-sm text-gray-600">@{match.partner.username}</p>
|
<p className="text-sm text-gray-600">@{match.partner.username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
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 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 PublicProfilePage = () => {
|
||||||
const { username } = useParams();
|
const { username } = useParams();
|
||||||
const [profile, setProfile] = useState(null);
|
const [profile, setProfile] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [ratings, setRatings] = useState(null);
|
||||||
|
const [ratingsLoading, setRatingsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
@@ -26,6 +28,22 @@ const PublicProfilePage = () => {
|
|||||||
fetchProfile();
|
fetchProfile();
|
||||||
}, [username]);
|
}, [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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
@@ -150,7 +168,7 @@ const PublicProfilePage = () => {
|
|||||||
|
|
||||||
{/* Social Media Links */}
|
{/* Social Media Links */}
|
||||||
{(profile.youtubeUrl || profile.instagramUrl || profile.facebookUrl || profile.tiktokUrl) && (
|
{(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>
|
<h2 className="text-xl font-semibold mb-4">Social Media</h2>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{profile.youtubeUrl && (
|
{profile.youtubeUrl && (
|
||||||
@@ -202,6 +220,118 @@ const PublicProfilePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user