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 { 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 Layout from '../components/layout/Layout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { Send, UserPlus, Loader2, LogOut, AlertTriangle, QrCode, Edit2, Filter, X, MessageSquare, Users, Video } from 'lucide-react';
|
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 { slug } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [event, setEvent] = useState(null);
|
const [event, setEvent] = useState(null);
|
||||||
const [isParticipant, setIsParticipant] = useState(false);
|
const [isParticipant, setIsParticipant] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -56,6 +57,24 @@ const EventChatPage = () => {
|
|||||||
// Tab state: 'chat' | 'participants' | 'recording'
|
// Tab state: 'chat' | 'participants' | 'recording'
|
||||||
const [activeTab, setActiveTab] = useState('chat');
|
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 = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user