refactor(frontend): extract DashboardPage into components
Split DashboardPage (578 lines) into focused components: - DashboardEventCard: event card with chat access - DashboardMatchCard: match card with status indicators - MatchRequestCards: incoming/outgoing request cards - EmptyState: reusable empty state component (in common/) DashboardPage now 295 lines (-49%)
This commit is contained in:
121
frontend/src/components/dashboard/DashboardMatchCard.jsx
Normal file
121
frontend/src/components/dashboard/DashboardMatchCard.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { MessageCircle, Video, Star, Check, Clock } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Video exchange status indicator
|
||||
*/
|
||||
const VideoStatus = ({ exchange }) => {
|
||||
const sent = exchange?.sentByMe;
|
||||
const received = exchange?.receivedFromPartner;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Video className="w-4 h-4 text-gray-400" />
|
||||
<span className={sent ? 'text-green-600' : 'text-gray-400'}>
|
||||
{sent ? <Check className="w-3 h-3 inline" /> : <Clock className="w-3 h-3 inline" />} Sent
|
||||
</span>
|
||||
<span className="text-gray-300">|</span>
|
||||
<span className={received ? 'text-green-600' : 'text-gray-400'}>
|
||||
{received ? <Check className="w-3 h-3 inline" /> : <Clock className="w-3 h-3 inline" />} Received
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rating status indicator
|
||||
*/
|
||||
const RatingStatus = ({ ratings }) => {
|
||||
const ratedByMe = ratings?.ratedByMe;
|
||||
const ratedByPartner = ratings?.ratedByPartner;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Star className="w-4 h-4 text-gray-400" />
|
||||
<span className={ratedByMe ? 'text-green-600' : 'text-gray-400'}>
|
||||
{ratedByMe ? <Check className="w-3 h-3 inline" /> : <Clock className="w-3 h-3 inline" />} You
|
||||
</span>
|
||||
<span className="text-gray-300">|</span>
|
||||
<span className={ratedByPartner ? 'text-green-600' : 'text-gray-400'}>
|
||||
{ratedByPartner ? <Check className="w-3 h-3 inline" /> : <Clock className="w-3 h-3 inline" />} Partner
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Match card for dashboard - shows active match with chat access
|
||||
*/
|
||||
const DashboardMatchCard = ({ match }) => {
|
||||
const navigate = useNavigate();
|
||||
const { partner, event, videoExchange, ratings, unreadCount } = match;
|
||||
|
||||
// Can rate when video exchange is complete and user hasn't rated yet
|
||||
const canRate = videoExchange?.sentByMe && videoExchange?.receivedFromPartner && !ratings?.ratedByMe;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Avatar */}
|
||||
<Link to={`/${partner.username}`} className="flex-shrink-0 relative">
|
||||
<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 hover:ring-2 hover:ring-primary-500 transition-all"
|
||||
/>
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-semibold">
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<Link to={`/${partner.username}`}>
|
||||
<h3 className="font-semibold text-gray-900 hover:text-primary-600 transition-colors">
|
||||
{partner.firstName && partner.lastName
|
||||
? `${partner.firstName} ${partner.lastName}`
|
||||
: partner.username}
|
||||
</h3>
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500">@{partner.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mt-1">{event.name}</p>
|
||||
|
||||
{/* Status Indicators */}
|
||||
<div className="flex flex-wrap gap-3 mt-3 text-sm">
|
||||
<VideoStatus exchange={videoExchange} />
|
||||
<RatingStatus ratings={ratings} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex-shrink-0 flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => navigate(`/matches/${match.slug}/chat`)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
Chat
|
||||
</button>
|
||||
{canRate && (
|
||||
<button
|
||||
onClick={() => navigate(`/matches/${match.slug}/rate`)}
|
||||
className="flex items-center gap-2 px-4 py-2 border border-amber-500 text-amber-600 rounded-md hover:bg-amber-50 transition-colors"
|
||||
>
|
||||
<Star className="w-4 h-4" />
|
||||
Rate
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardMatchCard;
|
||||
Reference in New Issue
Block a user