feat(dashboard): improve RecordingSummaryCard styling and fix tab navigation
- Increase font size from xs to sm for better readability - Reduce avatar size from xs to 24px for better proportions - Add proper layout with heat names in separate line - Add truncate for long usernames to prevent overflow - Style status badges with colored backgrounds and icons (pending/accepted) - Fix EventChatPage to read and handle ?tab=records URL parameter - Map 'records' query param to 'recording' tab for proper navigation
This commit is contained in:
163
frontend/src/components/dashboard/RecordingSummaryCard.jsx
Normal file
163
frontend/src/components/dashboard/RecordingSummaryCard.jsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Video, Clock, CheckCircle, XCircle, ChevronRight } from 'lucide-react';
|
||||
import Avatar from '../common/Avatar';
|
||||
|
||||
/**
|
||||
* Recording summary card for dashboard - shows recording assignments for an event
|
||||
*/
|
||||
const RecordingSummaryCard = ({ event }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { toBeRecorded, toRecord } = event.recordingSuggestions || { toBeRecorded: [], toRecord: [] };
|
||||
|
||||
// Count by status
|
||||
const toBeRecordedPending = toBeRecorded.filter(s => s.status === 'pending').length;
|
||||
const toBeRecordedAccepted = toBeRecorded.filter(s => s.status === 'accepted').length;
|
||||
const toRecordPending = toRecord.filter(s => s.status === 'pending').length;
|
||||
const toRecordAccepted = toRecord.filter(s => s.status === 'accepted').length;
|
||||
|
||||
// If no recording suggestions, don't render
|
||||
if (toBeRecorded.length === 0 && toRecord.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Clock className="w-3 h-3 text-blue-600" />;
|
||||
case 'accepted':
|
||||
return <CheckCircle className="w-3 h-3 text-green-600" />;
|
||||
case 'rejected':
|
||||
return <XCircle className="w-3 h-3 text-red-600" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const formatHeat = (heat) => {
|
||||
return `${heat.division} ${heat.competitionType} H${heat.heatNumber}`;
|
||||
};
|
||||
|
||||
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 justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{event.name}</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{event.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Heats to be recorded */}
|
||||
{toBeRecorded.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-700 mb-2 flex items-center gap-1">
|
||||
<Video className="w-3 h-3" />
|
||||
Your heats ({toBeRecorded.length})
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{toBeRecorded.slice(0, 2).map((suggestion) => (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
className="flex items-start justify-between text-sm p-2.5 bg-gray-50 rounded"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 mb-1">{formatHeat(suggestion.heat)}</div>
|
||||
{suggestion.recorder ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Avatar user={suggestion.recorder} size={24} />
|
||||
<span className="text-gray-700 truncate">@{suggestion.recorder.username}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-amber-600">No recorder</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-2 flex-shrink-0">
|
||||
{getStatusIcon(suggestion.status)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{toBeRecorded.length > 2 && (
|
||||
<p className="text-xs text-gray-500 pl-2">
|
||||
+{toBeRecorded.length - 2} more
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2 text-xs">
|
||||
{toBeRecordedPending > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 rounded-md font-medium">
|
||||
<Clock className="w-3 h-3" />
|
||||
{toBeRecordedPending} pending
|
||||
</span>
|
||||
)}
|
||||
{toBeRecordedAccepted > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-md font-medium">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
{toBeRecordedAccepted} accepted
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recording others */}
|
||||
{toRecord.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-700 mb-2 flex items-center gap-1">
|
||||
<Video className="w-3 h-3" />
|
||||
You record ({toRecord.length})
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{toRecord.slice(0, 2).map((suggestion) => (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
className="flex items-start justify-between text-sm p-2.5 bg-blue-50 rounded"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 mb-1">{formatHeat(suggestion.heat)}</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Avatar user={suggestion.dancer} size={24} />
|
||||
<span className="text-gray-700 truncate">@{suggestion.dancer.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex-shrink-0">
|
||||
{getStatusIcon(suggestion.status)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{toRecord.length > 2 && (
|
||||
<p className="text-xs text-gray-500 pl-2">
|
||||
+{toRecord.length - 2} more
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2 text-xs">
|
||||
{toRecordPending > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 rounded-md font-medium">
|
||||
<Clock className="w-3 h-3" />
|
||||
{toRecordPending} pending
|
||||
</span>
|
||||
)}
|
||||
{toRecordAccepted > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-md font-medium">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
{toRecordAccepted} accepted
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => navigate(`/events/${event.slug}/chat?tab=records`)}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 mt-3 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors text-sm"
|
||||
>
|
||||
View Details
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingSummaryCard;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useParams, useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Send, UserPlus, Loader2, LogOut, AlertTriangle, QrCode, Edit2, Filter, X, MessageSquare, Users, Video } from 'lucide-react';
|
||||
@@ -20,6 +20,7 @@ const EventChatPage = () => {
|
||||
const { slug } = useParams();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [event, setEvent] = useState(null);
|
||||
const [isParticipant, setIsParticipant] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -56,6 +57,24 @@ const EventChatPage = () => {
|
||||
// Tab state: 'chat' | 'participants' | 'recording'
|
||||
const [activeTab, setActiveTab] = useState('chat');
|
||||
|
||||
// Read tab from URL query parameter
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (tabParam) {
|
||||
// Map 'records' to 'recording' for backwards compatibility
|
||||
const tabMapping = {
|
||||
'chat': 'chat',
|
||||
'participants': 'participants',
|
||||
'records': 'recording',
|
||||
'recording': 'recording'
|
||||
};
|
||||
const mappedTab = tabMapping[tabParam];
|
||||
if (mappedTab) {
|
||||
setActiveTab(mappedTab);
|
||||
}
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user