Story 4.1.4: Homepage Event Content Sections (Speakers, Sessions, Venue, Social)
Table of Contents
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
Related Functional Requirements
- Epic 4, Story 4.1
- AC 5-9: Speaker Grid, Session Cards, Venue Map, Social Sharing
Acceptance Criteria
- API Integration: β
BACKEND READY - Sessions API now returns speakers array (Story 1.15a.1b)
- GET
/api/v1/events/{eventCode}/sessions/{sessionSlug}returnsspeakers: SessionSpeaker[] - SessionSpeaker includes: username, firstName, lastName, company, profilePictureUrl, speakerRole, isConfirmed
- No need for separate speaker API calls - data embedded in session responses
- GET
- SpeakerGrid displays speaker photos, names, companies, session titles
- Speaker cards show session abstract on hover
- SessionCards component with grid/list toggle
- Session filtering by time/topic
- Venue map embedded (Google Maps) with event.venue.coordinates
- Social sharing buttons (LinkedIn, Twitter, Email) with Open Graph metadata
- EventProgram vertical timeline showing session schedule
- All data from real API endpoints
- 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
Sessiontype now includes - Verify
SessionSpeakertypescriptinterface 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
- Install
-
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:
// 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:
eventApiClient.getCurrentEvent({
expand: ['topics', 'venue', 'speakers', 'sessions']
})
Component Implementations
SpeakerGrid.tsx:
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):
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:
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:
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:
// 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_KEYis 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 arrayGET /api/v1/events/{eventCode}/sessions- Lists all sessions with speakersGET /api/v1/events/{eventCode}/sessions/{sessionSlug}/speakers- List session speakers- Additional speaker management endpoints for ORGANIZER role
SessionSpeaker Data Structure:
{
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 passingweb-frontend/src/components/public/Event/SessionCards.test.tsx- 12 tests, all passingweb-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 tagsweb-frontend/src/App.tsx- Added HelmetProvider wrapperweb-frontend/src/types/event.types.ts- Added SessionSpeaker type exportdocs/api/events-api.openapi.yml- Added SessionResponse schemadocs/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