Story 4.1.4: Homepage Event Content Sections (Speakers, Sessions, Venue, Social)

Last updated: November 23, 2025 Reading time: 13 minutes File size: 21.89 KB

Story 4.1.4: Homepage Event Content Sections (Speakers, Sessions, Venue, Social)

Status

Done

Story

As a public website visitor, I want to see detailed event content including speakers, sessions, venue map, and social sharing on the homepage, so that I can learn about the program and decide which sessions to attend.

ARCHITECTURAL NOTE: All event content sections are integrated into the homepage for a comprehensive single-page event experience. There is no separate /current-event page.

Domain Context

Primary Domain

Attendee Experience Domain (Public event content discovery)

Involved Services

  • web-frontend (React 19 + TypeScript)
  • Event Management Service (via API Gateway)
  • API Gateway: Event data already includes speakers, sessions from Story 4.1.3

Cross-Domain Dependencies

  • Story 1.15a.1b (Session-User Many-to-Many Relationship) - βœ… COMPLETE
    • Backend implementation complete: session_users junction table, SessionUser entity, speaker enrichment
    • Sessions API now returns speakers array with enriched User data (name, company, photo, role)
    • 5 new speaker management endpoints available
    • OpenAPI spec updated with SessionSpeaker schema
  • Event Management Service (session and speaker data via unified API)

Requirements Context

  • Epic 4, Story 4.1
  • AC 5-9: Speaker Grid, Session Cards, Venue Map, Social Sharing

Acceptance Criteria

  1. API Integration: βœ… BACKEND READY - Sessions API now returns speakers array (Story 1.15a.1b)
    • GET /api/v1/events/{eventCode}/sessions/{sessionSlug} returns speakers: SessionSpeaker[]
    • SessionSpeaker includes: username, firstName, lastName, company, profilePictureUrl, speakerRole, isConfirmed
    • No need for separate speaker API calls - data embedded in session responses
  2. SpeakerGrid displays speaker photos, names, companies, session titles
  3. Speaker cards show session abstract on hover
  4. SessionCards component with grid/list toggle
  5. Session filtering by time/topic
  6. Venue map embedded (Google Maps) with event.venue.coordinates
  7. Social sharing buttons (LinkedIn, Twitter, Email) with Open Graph metadata
  8. EventProgram vertical timeline showing session schedule
  9. All data from real API endpoints
  10. Responsive design for all components

Tasks / Subtasks

  • Task 0: Backend Implementation βœ… COMPLETE

    • Files: See Story 1.15a.1b for complete implementation details
  • Task 1: Update TypeScript types from OpenAPI spec

    • Regenerate API types:
    • Verify Session type now includes
    • Verify SessionSpeaker
      typescript
      interface SessionSpeaker {
        username: string;
        firstName: string;
        lastName: string;
        company: string;
        profilePictureUrl: string;
        speakerRole: 'PRIMARY_SPEAKER' | 'CO_SPEAKER' | 'MODERATOR' | 'PANELIST';
        presentationTitle?: string;
        isConfirmed: boolean;
      }
      
    • Update event.types.ts
  • Task 2: Create SpeakerGrid component

    • Create
  • Task 3: Create SessionCards component

    • Create
  • Task 4: Create EventProgram timeline

    • Create
  • Task 5: Create VenueMap component

    • Create
    • Use @googlemaps/react-wrapper
  • Task 6: Create SocialSharing component

    • Create
    • LinkedIn:
    • Twitter:
    • Email:
  • Task 7: Add Open Graph meta tags

    • Install react-helmet-async:
    • Create
  • Task 8: Update HomePage with new sections

    • NOTE
  • Task 9: Implement and test

  • Task 10: Add tests

Dev Notes - Implementation Guide

Type Reference

Use existing types from event.types.ts:

Story 4.1.3 already fetches event with speakers and sessions. The EventDetail type from @/types/event.types.ts already includes:

typescript
// These types already exist in event.types.ts
export interface EventDetail {
  id: string;
  eventCode: string;
  title: string;
  description: string;
  date: string;
  // ... other fields
  speakers?: Speaker[];
  sessions?: Session[];
  topics?: Topic[];
  venue?: Venue;
}

export interface Speaker {
  id: string;
  firstName: string;
  lastName: string;
  company: string;
  bio: string;
  photoUrl?: string;
  // ... other fields
}

export interface Session {
  id: string;
  title: string;
  abstract: string;
  startTime: string;
  endTime: string;
  room: string;
  capacity: number;
  registrationCount: number;
  speakerId: string;
  topics?: string[];
}

No new API calls needed - all data already fetched in Story 4.1.3 with:

typescript
eventApiClient.getCurrentEvent({
  expand: ['topics', 'venue', 'speakers', 'sessions']
})

Component Implementations

SpeakerGrid.tsx:

typescript
import { Card, CardContent, CardHeader } from '@/components/public/ui/card';
import type { Speaker } from '@/types/event.types';
import { Building2 } from 'lucide-react';

interface SpeakerGridProps {
  speakers: Speaker[];
}

export const SpeakerGrid = ({ speakers }: SpeakerGridProps) => {
  return (
    <div>
      <h2 className="text-3xl font-light mb-6">Speakers</h2>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {speakers.map((speaker) => (
          <Card key={speaker.id} className="group hover:border-blue-400 transition-colors">
            <CardHeader className="flex flex-row items-center gap-4">
              <div className="h-16 w-16 rounded-full bg-zinc-800 flex items-center justify-center overflow-hidden">
                {speaker.photoUrl ? (
                  <img src={speaker.photoUrl} alt={`${speaker.firstName} ${speaker.lastName}`} className="h-full w-full object-cover" />
                ) : (
                  <span className="text-2xl font-light">{speaker.firstName[0]}{speaker.lastName[0]}</span>
                )}
              </div>
              <div>
                <h3 className="font-light text-lg">{speaker.firstName} {speaker.lastName}</h3>
                <p className="text-sm text-zinc-400 flex items-center gap-1">
                  <Building2 className="h-3 w-3" />
                  {speaker.company}
                </p>
              </div>
            </CardHeader>
            <CardContent>
              {speaker.sessionTitle && (
                <div>
                  <p className="text-sm font-medium text-blue-400">{speaker.sessionTitle}</p>
                  <p className="mt-2 text-sm text-zinc-400 line-clamp-3 group-hover:line-clamp-none transition-all">
                    {speaker.sessionAbstract}
                  </p>
                </div>
              )}
            </CardContent>
          </Card>
        ))}
      </div>
    </div>
  );
};

SessionCards.tsx (simplified):

typescript
import { useState } from 'react';
import { Button } from '@/components/public/ui/button';
import { Card } from '@/components/public/ui/card';
import { Badge } from '@/components/public/ui/badge';
import { LayoutGrid, List, Clock, MapPin, Users } from 'lucide-react';
import type { Session, Topic } from '@/types/event.types';

interface SessionCardsProps {
  sessions: Session[];
  topics: Topic[];
}

export const SessionCards = ({ sessions, topics }: SessionCardsProps) => {
  const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
  const [selectedTopics, setSelectedTopics] = useState<string[]>([]);

  const filteredSessions = sessions.filter((session) =>
    selectedTopics.length === 0 ||
    session.topicIds.some((tid) => selectedTopics.includes(tid))
  );

  return (
    <div>
      <div className="flex items-center justify-between mb-6">
        <h2 className="text-3xl font-light">Sessions</h2>
        <div className="flex gap-2">
          <Button
            variant={viewMode === 'grid' ? 'default' : 'outline'}
            size="icon"
            onClick={() => setViewMode('grid')}
          >
            <LayoutGrid className="h-4 w-4" />
          </Button>
          <Button
            variant={viewMode === 'list' ? 'default' : 'outline'}
            size="icon"
            onClick={() => setViewMode('list')}
          >
            <List className="h-4 w-4" />
          </Button>
        </div>
      </div>

      {/* Topic filters */}
      <div className="flex flex-wrap gap-2 mb-6">
        {topics.map((topic) => (
          <Badge
            key={topic.id}
            className={`cursor-pointer ${selectedTopics.includes(topic.id) ? 'bg-blue-400 text-zinc-950' : 'bg-zinc-800'}`}
            onClick={() => setSelectedTopics((prev) =>
              prev.includes(topic.id)
                ? prev.filter((id) => id !== topic.id)
                : [...prev, topic.id]
            )}
          >
            {topic.name}
          </Badge>
        ))}
      </div>

      {/* Sessions */}
      <div className={viewMode === 'grid' ? 'grid grid-cols-1 md:grid-cols-2 gap-4' : 'space-y-4'}>
        {filteredSessions.map((session) => (
          <Card key={session.id} className="p-4">
            <h3 className="font-light text-lg">{session.title}</h3>
            <p className="text-sm text-zinc-400 mt-2">{session.abstract}</p>
            <div className="mt-4 flex flex-wrap gap-4 text-sm text-zinc-400">
              <span className="flex items-center gap-1">
                <Clock className="h-4 w-4" />
                {session.startTime} - {session.endTime}
              </span>
              <span className="flex items-center gap-1">
                <MapPin className="h-4 w-4" />
                {session.room}
              </span>
              <span className="flex items-center gap-1">
                <Users className="h-4 w-4" />
                {session.registrationCount}/{session.capacity}
              </span>
            </div>
            <p className="mt-2 text-sm text-zinc-300">Speaker: {session.speakerName}</p>
          </Card>
        ))}
      </div>
    </div>
  );
};

VenueMap.tsx:

typescript
interface VenueMapProps {
  venue: {
    name: string;
    address: string;
    city: string;
    coordinates: { lat: number; lng: number };
  };
}

export const VenueMap = ({ venue }: VenueMapProps) => {
  const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY}&q=${venue.coordinates.lat},${venue.coordinates.lng}`;
  const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${venue.coordinates.lat},${venue.coordinates.lng}`;

  return (
    <div>
      <h2 className="text-3xl font-light mb-6">Venue</h2>
      <div className="rounded-lg overflow-hidden">
        <iframe
          src={mapUrl}
          width="100%"
          height="400"
          style={{ border: 0 }}
          allowFullScreen
          loading="lazy"
          referrerPolicy="no-referrer-when-downgrade"
        />
      </div>
      <div className="mt-4">
        <p className="text-lg font-light">{venue.name}</p>
        <p className="text-zinc-400">{venue.address}, {venue.city}</p>
        <Button asChild variant="outline" className="mt-4">
          <a href={directionsUrl} target="_blank" rel="noopener noreferrer">
            Get Directions
          </a>
        </Button>
      </div>
    </div>
  );
};

SocialSharing.tsx:

typescript
import { Button } from '@/components/public/ui/button';
import { Linkedin, Twitter, Mail } from 'lucide-react';

interface SocialSharingProps {
  eventTitle: string;
  eventUrl: string;
}

export const SocialSharing = ({ eventTitle, eventUrl }: SocialSharingProps) => {
  const shareText = `Check out ${eventTitle} at BATbern!`;

  const links = {
    linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(eventUrl)}`,
    twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(eventUrl)}`,
    email: `mailto:?subject=${encodeURIComponent(eventTitle)}&body=${encodeURIComponent(`${shareText} ${eventUrl}`)}`,
  };

  return (
    <div>
      <h2 className="text-3xl font-light mb-6">Share Event</h2>
      <div className="flex gap-4">
        <Button asChild variant="outline">
          <a href={links.linkedin} target="_blank" rel="noopener noreferrer">
            <Linkedin className="h-4 w-4 mr-2" />
            LinkedIn
          </a>
        </Button>
        <Button asChild variant="outline">
          <a href={links.twitter} target="_blank" rel="noopener noreferrer">
            <Twitter className="h-4 w-4 mr-2" />
            Twitter
          </a>
        </Button>
        <Button asChild variant="outline">
          <a href={links.email}>
            <Mail className="h-4 w-4 mr-2" />
            Email
          </a>
        </Button>
      </div>
    </div>
  );
};

Update HomePage

Add new event content sections in logical order after existing HeroSection:

typescript
// web-frontend/src/pages/public/HomePage.tsx
export const HomePage = () => {
  const { data: event, isLoading, error } = useCurrentEvent();

  // ... loading and error states

  return (
    <PublicLayout>
      <div className="container mx-auto px-4 py-12 space-y-16">
        {/* Existing from 4.1.3 */}
        <CountdownTimer eventDate={new Date(event.startDate)} />
        <HeroSection event={event} />
        <EventLogistics event={event} />
        <TopicBadges topics={event.topics} />

        {/* New sections from 4.1.4 */}
        <EventProgram sessions={event.sessions} />
        <SpeakerGrid speakers={event.speakers} />
        <SessionCards sessions={event.sessions} topics={event.topics} />
        <VenueMap venue={event.venue} />
        <SocialSharing eventTitle={event.title} eventUrl={window.location.href} />
      </div>
    </PublicLayout>
  );
};

Environment Variable

Add to .env:

VITE_GOOGLE_MAPS_API_KEY=your_google_maps_api_key

Change Log

Date Version Description Author
2025-11-02 1.0 Initial story creation Winston (Architect Agent)
2025-11-03 1.1 Updated to reflect HomePage architecture (not separate CurrentEventPage) James (Dev Agent)
2025-11-04 2.0 Frontend implementation complete - All 10 tasks finished, 32 tests passing James (Dev Agent - Sonnet 4.5)
2025-11-04 2.1 Fixed VenueMap to use address-based search (removed coordinates requirement) James (Dev Agent - Sonnet 4.5)
2025-11-04 2.2 Added i18n support (German/English) to all public event components James (Dev Agent - Sonnet 4.5)

Dev Agent Record

Debug Log References

2025-11-04 - VenueMap Fix: Fixed β€œMap not available” error

  • Issue: Venue schema doesn’t include coordinates field, VenueMap was requiring coordinates
  • Root Cause: VenueMap component was designed for coordinates but API only provides name/address
  • Solution: Updated VenueMap to use address-based Google Maps search instead of coordinates
  • Fallback: Uses basic Google Maps embed when VITE_GOOGLE_MAPS_API_KEY is not configured
  • Files Modified: VenueMap.tsx (removed coordinates requirement, uses venue name + address)
  • Result: Map now displays correctly using venue address from event data

Completion Notes

Status: βœ… COMPLETE - Frontend Implementation Finished

Implementation Summary:

  • βœ… All 10 tasks completed successfully
  • βœ… 32 tests written and passing (9 + 12 + 11)
  • βœ… All components integrated into HomePage
  • βœ… TypeScript types regenerated from OpenAPI spec
  • βœ… SEO meta tags added with react-helmet-async
  • βœ… Responsive design for all components
  • βœ… i18n support added for German/English (5 components internationalized)
  • βœ… Story ready for QA testing

Backend Implementation Complete βœ…:

  • Story 1.15a.1b implemented session-user many-to-many relationship
  • Sessions API now returns speakers: SessionSpeaker[] array with enriched User data
  • 13 backend files created: migration, entities, repositories, services, DTOs, controllers
  • OpenAPI spec updated with SessionSpeaker schema and 5 new endpoints
  • 36 test cases written (unit + integration tests with real PostgreSQL)
  • Direct User repository access eliminates cross-service dependencies

API Endpoints Available:

  • GET /api/v1/events/{eventCode}/sessions/{sessionSlug} - Returns session with speakers array
  • GET /api/v1/events/{eventCode}/sessions - Lists all sessions with speakers
  • GET /api/v1/events/{eventCode}/sessions/{sessionSlug}/speakers - List session speakers
  • Additional speaker management endpoints for ORGANIZER role

SessionSpeaker Data Structure:

typescript
{
  username: string;          // e.g., "john.doe"
  firstName: string;         // e.g., "John"
  lastName: string;          // e.g., "Doe"
  company: string;           // e.g., "GoogleZH"
  profilePictureUrl: string; // CDN URL
  speakerRole: 'PRIMARY_SPEAKER' | 'CO_SPEAKER' | 'MODERATOR' | 'PANELIST';
  presentationTitle?: string; // Optional speaker-specific title
  isConfirmed: boolean;
}

Key Architectural Update: All event content sections (Speakers, Sessions, Venue, Social Sharing, Event Program) will be added to the HomePage component, not a separate CurrentEventPage. This provides a comprehensive single-page event experience.

File List

Files Created (11 files):

Components:

  • web-frontend/src/components/public/Event/SpeakerGrid.tsx - Speaker grid with hover states (126 lines)
  • web-frontend/src/components/public/Event/SessionCards.tsx - Session cards with grid/list toggle and filtering (185 lines)
  • web-frontend/src/components/public/Event/EventProgram.tsx - Vertical timeline schedule (136 lines)
  • web-frontend/src/components/public/Event/VenueMap.tsx - Google Maps embed with directions (106 lines)
  • web-frontend/src/components/public/Event/SocialSharing.tsx - Social sharing buttons (68 lines)
  • web-frontend/src/components/SEO/OpenGraphTags.tsx - Open Graph meta tags (49 lines)

Tests (32 tests total):

  • web-frontend/src/components/public/Event/SpeakerGrid.test.tsx - 9 tests, all passing
  • web-frontend/src/components/public/Event/SessionCards.test.tsx - 12 tests, all passing
  • web-frontend/src/components/public/Event/EventProgram.test.tsx - 11 tests, all passing

Generated Types:

  • web-frontend/src/types/generated/events-api.types.ts - Regenerated with SessionSpeaker and SessionResponse types

Dependencies:

  • web-frontend/package.json - Added react-helmet-async

Files Modified (5 files):

  • web-frontend/src/pages/public/HomePage.tsx - Added all new event content sections with SEO tags
  • web-frontend/src/App.tsx - Added HelmetProvider wrapper
  • web-frontend/src/types/event.types.ts - Added SessionSpeaker type export
  • docs/api/events-api.openapi.yml - Added SessionResponse schema
  • docs/stories/4.1.4.event-content-sections.md - Updated all tasks and status

Dependencies

  • Story 1.15a.1b (Session-User Many-to-Many) - βœ… COMPLETE
    • Backend implementation: session_users table, SessionUser entity, speaker enrichment
    • API returns speakers array in session responses
    • See Story 1.15a.1b for complete technical details
  • Story 4.1.3 must be complete (useCurrentEvent hook, HeroSection, EventLogistics, CountdownTimer, TopicBadges)
  • Event data from API must include speakers, sessions, venue

Deployment Notes

  • Add VITE_GOOGLE_MAPS_API_KEY environment variable for venue map
  • Test all sections render correctly on homepage
  • Verify responsive design for all new components