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:
Radosław Gierwiało
2025-11-30 15:13:50 +01:00
parent 2e49fa5c62
commit 6ce3111cdd
2 changed files with 183 additions and 1 deletions

View 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;

View File

@@ -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' });
};