All files / src/components/public/Event SpeakerGrid.tsx

94.11% Statements 16/17
77.77% Branches 14/18
100% Functions 5/5
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114                                                          1x   1x 9x     9x 9x   9x 16x 16x 15x   15x 14x                                 9x     9x 1x     8x         14x                                                                                
/**
 * SpeakerGrid Component (Story 4.1.4)
 * Displays speakers in a responsive grid with session information
 * Hover to show speaker bio
 */
 
import { Card, CardContent, CardHeader } from '@/components/public/ui/card';
import type { Session } from '@/types/event.types';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SpeakerDisplay } from './SpeakerDisplay';
 
interface SpeakerWithSession {
  username: string;
  firstName: string;
  lastName: string;
  company?: string;
  companyDisplayName?: string;
  profilePictureUrl?: string;
  sessionTitle: string;
  sessionDescription?: string;
  bio?: string;
  speakerRole: string;
}
 
interface SpeakerGridProps {
  sessions: Session[];
}
 
const STRUCTURAL_TYPES = new Set(['moderation', 'break', 'lunch', 'aperitif']);
 
export const SpeakerGrid = ({ sessions }: SpeakerGridProps) => {
  const { t } = useTranslation('events');
 
  // Aggregate speakers from non-structural sessions only
  const speakersWithSessions = useMemo(() => {
    const speakerMap = new Map<string, SpeakerWithSession>();
 
    sessions.forEach((session) => {
      Iif (STRUCTURAL_TYPES.has(session.sessionType ?? '')) return;
      if (session.speakers && session.speakers.length > 0) {
        session.speakers.forEach((speaker) => {
          // Use primary speaker only (or first speaker if no primary)
          if (!speakerMap.has(speaker.username)) {
            speakerMap.set(speaker.username, {
              username: speaker.username,
              firstName: speaker.firstName,
              lastName: speaker.lastName,
              company: speaker.company,
              companyDisplayName: speaker.companyDisplayName,
              profilePictureUrl: speaker.profilePictureUrl,
              sessionTitle: speaker.presentationTitle || session.title,
              sessionDescription: session.description,
              bio: speaker.bio,
              speakerRole: speaker.speakerRole,
            });
          }
        });
      }
    });
 
    return Array.from(speakerMap.values());
  }, [sessions]);
 
  if (speakersWithSessions.length === 0) {
    return null;
  }
 
  return (
    <div className="py-12" data-testid="speaker-grid">
      <h2 className="text-3xl font-light mb-8 text-zinc-100">{t('common:navigation.speakers')}</h2>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {speakersWithSessions.map((speaker) => (
          <Card
            key={speaker.username}
            data-testid="speaker-card"
            className="group hover:border-blue-400 transition-colors bg-zinc-700 border-zinc-600"
          >
            <CardHeader className="pb-4">
              <SpeakerDisplay
                speaker={{
                  username: speaker.username,
                  firstName: speaker.firstName,
                  lastName: speaker.lastName,
                  company: speaker.company,
                  companyDisplayName: speaker.companyDisplayName,
                  profilePictureUrl: speaker.profilePictureUrl,
                  bio: speaker.bio,
                  speakerRole:
                    (speaker.speakerRole as
                      | 'PRIMARY_SPEAKER'
                      | 'CO_SPEAKER'
                      | 'MODERATOR'
                      | 'PANELIST') || 'PRIMARY_SPEAKER',
                  presentationTitle: undefined,
                  isConfirmed: true,
                }}
                size="medium"
                showProfilePicture={true}
              />
            </CardHeader>
            <CardContent className="pt-0">
              <div className="border-t border-zinc-800 pt-4">
                <p className="text-sm font-medium text-blue-400 mb-2">{speaker.sessionTitle}</p>
                {speaker.bio && <p className="text-sm text-zinc-400 mt-2">{speaker.bio}</p>}
              </div>
            </CardContent>
          </Card>
        ))}
      </div>
    </div>
  );
};