#5CRSE Event Discovery & Booking Implementation Plan
#Overview
The event discovery and booking flow is a core part of the 5CRSE platform, allowing users to find events (both from Ticketmaster and custom/partner events), view details, and book luxury transportation. This document outlines the implementation plan for creating a seamless, visually appealing event discovery and booking experience.
#User Journey & Flow
flowchart TD
A[Homepage] -->|"Browse Events"| B[Event Discovery]
B -->|"Apply Filters"| B
B -->|"Select Event"| C[Event Details]
C -->|"Book Event"| D[Transportation Selection]
D -->|"Select Vehicle"| E[Booking Details]
E -->|"Add Guests"| F[Review & Checkout]
F -->|"Complete Payment"| G[Booking Confirmation]
subgraph "Event Discovery"
B1[Filter Panel] --> B2[Search]
B2 --> B3[Event Grid]
B3 --> B4[Pagination]
end
subgraph "Event Sources"
S1[Ticketmaster Events]
S2[Custom Partner Events]
S3[User-Created Events]
end
S1 --> B
S2 --> B
S3 --> B
subgraph "Booking Flow"
D1[Vehicle Options] --> E1[Guest Management]
E1 --> F1[Price Breakdown]
F1 --> F2[Payment Processing]
end
D --> D1
E --> E1
F --> F1
F1 --> F2
#Implementation Phases
gantt
title Event Discovery & Booking Implementation Plan
dateFormat YYYY-MM-DD
section Phase 1: Core Discovery
Event Grid UI :2025-04-01, 5d
Basic Filtering :2025-04-06, 4d
Ticketmaster Integration :2025-04-10, 7d
section Phase 2: Advanced Discovery
Advanced Filtering :2025-04-17, 4d
Search Functionality :2025-04-21, 3d
Event Detail Pages :2025-04-24, 5d
section Phase 3: Booking Flow
Transportation Selection :2025-04-29, 4d
Booking Details Form :2025-05-03, 3d
Guest Information :2025-05-06, 3d
section Phase 4: Checkout & Completion
Order Summary :2025-05-09, 3d
Payment Integration :2025-05-12, 5d
Confirmation Pages :2025-05-17, 3d
section Phase 5: Enhancements
Map View Integration :future, 0d
Personalized Recommendations :future, 0d
Enhanced UI Animations :future, 0d
#Detailed Component Implementations
#1. Event Discovery Grid
The event discovery grid is the main interface for users to browse and find events of interest.
User Interface
- Responsive grid of event cards
- Visually appealing cards with event images
- Clear event information display
- Hover effects and interactive elements
- Quick-access booking buttons
Component Structure
// src/components/EventDiscovery/EventGrid.tsx
import React from 'react';
import { EventCard } from './EventCard';
import { Pagination } from '../Pagination';
import { Spinner } from '../ui/Spinner';
interface EventGridProps {
events: any[];
loading: boolean;
totalEvents: number;
currentPage: number;
eventsPerPage: number;
onPageChange: (page: number) => void;
}
export const EventGrid: React.FC<EventGridProps> = ({
events,
loading,
totalEvents,
currentPage,
eventsPerPage,
onPageChange
}) => {
if (loading) {
return (
<div className="flex justify-center items-center py-20">
<Spinner size="lg" />
</div>
);
}
if (events.length === 0) {
return (
<div className="text-center py-16">
<h3 className="text-2xl font-bold text-white mb-2">No Events Found</h3>
<p className="text-gray-400">
Try adjusting your filters or search for different events.
</p>
</div>
);
}
return (
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{events.map((event) => (
<EventCard key={event.id} event={event} />
))}
</div>
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(totalEvents / eventsPerPage)}
onPageChange={onPageChange}
/>
</div>
);
};
#2. Event Card Component
Each event is displayed in a visually appealing card that showcases key information.
// src/components/EventDiscovery/EventCard.tsx
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { formatDateTime } from '@/utilities/formatDateTime';
import { CalendarIcon, MapPinIcon, TicketIcon } from '@heroicons/react/24/outline';
interface EventCardProps {
event: any;
}
export const EventCard: React.FC<EventCardProps> = ({ event }) => {
// Generate the event image URL or use a fallback
const imageUrl = event.images && event.images.length > 0
? event.images[0].image?.url || '/images/event-placeholder.jpg'
: '/images/event-placeholder.jpg';
// Determine if event is from Ticketmaster or custom
const isTicketmasterEvent = Boolean(event.ticketmaster_id);
const eventSource = isTicketmasterEvent ? 'Ticketmaster' : 'Partner Event';
return (
<div className="bg-gray-900 rounded-lg overflow-hidden group hover:ring-2 hover:ring-gold transition-all">
<div className="relative aspect-[16/9]">
<Image
src={imageUrl}
alt={event.event_name}
fill
className="object-cover transition-transform group-hover:scale-105"
/>
{/* Event source badge */}
<div className="absolute top-4 left-4 bg-black bg-opacity-70 rounded-full px-3 py-1 text-sm">
<span className={isTicketmasterEvent ? 'text-blue-400' : 'text-gold'}>
{eventSource}
</span>
</div>
{/* Price badge */}
{event.event_price && (
<div className="absolute top-4 right-4 bg-gold text-black font-bold rounded-full px-3 py-1 text-sm">
${typeof event.event_price === 'number' ? event.event_price.toFixed(0) : event.event_price}
</div>
)}
</div>
<div className="p-5">
<h3 className="text-xl font-bold text-white mb-2 line-clamp-2">
{event.event_name}
</h3>
<div className="space-y-2 mb-4">
{/* Date & Time */}
<div className="flex items-start">
<CalendarIcon className="w-5 h-5 text-gold mr-2 mt-0.5" />
<span className="text-gray-300">
{formatDateTime(event.event_date)}
</span>
</div>
{/* Location */}
<div className="flex items-start">
<MapPinIcon className="w-5 h-5 text-gold mr-2 mt-0.5" />
<span className="text-gray-300 line-clamp-1">
{event.location?.venue || 'Location TBD'}
</span>
</div>
{/* Category */}
{event.categories && event.categories.length > 0 && (
<div className="flex items-start">
<TicketIcon className="w-5 h-5 text-gold mr-2 mt-0.5" />
<div className="flex flex-wrap gap-1">
{event.categories.map((category: any) => (
<span
key={category.id}
className="text-xs bg-gray-800 text-gray-300 px-2 py-1 rounded"
>
{category.name}
</span>
))}
</div>
</div>
)}
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-800">
<Link
href={`/events/${event.id}`}
className="text-gold font-medium hover:underline"
>
View Details
</Link>
<Link
href={`/book/${event.id}`}
className="bg-gold text-black px-4 py-2 rounded font-medium hover:bg-yellow-500 transition-colors"
>
Book Now
</Link>
</div>
</div>
</div>
);
};
#3. Event Filtering System
A comprehensive filtering system that allows users to narrow down events based on various criteria.
// src/components/EventDiscovery/EventFilters.tsx
import React, { useState } from 'react';
import { DateRangePicker } from '@/components/DateRangePicker';
import { FilterChip } from '@/components/ui/FilterChip';
import { Slider } from '@/components/ui/Slider';
import { FilterIcon, XMarkIcon } from '@heroicons/react/24/outline';
interface EventFiltersProps {
filters: {
categories: string[];
dateRange: { start: Date | null; end: Date | null };
location: string;
distance: number;
priceRange: { min: number; max: number };
};
categories: { id: string; name: string }[];
maxPrice: number;
onChange: (filters: any) => void;
onReset: () => void;
}
export const EventFilters: React.FC<EventFiltersProps> = ({
filters,
categories,
maxPrice,
onChange,
onReset
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleCategoryToggle = (categoryId: string) => {
const newCategories = filters.categories.includes(categoryId)
? filters.categories.filter(id => id !== categoryId)
: [...filters.categories, categoryId];
onChange({ ...filters, categories: newCategories });
};
const handleDateRangeChange = (dateRange: { start: Date | null; end: Date | null }) => {
onChange({ ...filters, dateRange });
};
const handlePriceRangeChange = (priceRange: { min: number; max: number }) => {
onChange({ ...filters, priceRange });
};
const handleLocationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({ ...filters, location: e.target.value });
};
const handleDistanceChange = (distance: number) => {
onChange({ ...filters, distance });
};
return (
<div className="bg-gray-900 rounded-lg overflow-hidden">
{/* Mobile filter toggle */}
<div className="md:hidden p-4 border-b border-gray-800">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between w-full"
>
<span className="text-lg font-medium text-white">Filters</span>
<FilterIcon className="w-5 h-5 text-gold" />
</button>
</div>
{/* Filter content - responsive */}
<div className={`${isOpen ? 'block' : 'hidden'} md:block p-5 space-y-6`}>
{/* Header with reset */}
<div className="flex justify-between items-center">
<h3 className="text-xl font-bold text-white">Filter Events</h3>
<button
onClick={onReset}
className="text-sm text-gold hover:underline flex items-center"
>
<XMarkIcon className="w-4 h-4 mr-1" />
Reset Filters
</button>
</div>
{/* Event Categories */}
<div>
<h4 className="text-lg font-medium text-white mb-3">Event Type</h4>
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<FilterChip
key={category.id}
label={category.name}
selected={filters.categories.includes(category.id)}
onClick={() => handleCategoryToggle(category.id)}
/>
))}
</div>
</div>
{/* Date Range */}
<div>
<h4 className="text-lg font-medium text-white mb-3">Date Range</h4>
<DateRangePicker
startDate={filters.dateRange.start}
endDate={filters.dateRange.end}
onChange={handleDateRangeChange}
/>
</div>
{/* Location */}
<div>
<h4 className="text-lg font-medium text-white mb-3">Location</h4>
<input
type="text"
value={filters.location}
onChange={handleLocationChange}
placeholder="City or zip code"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-md text-white"
/>
<div className="mt-3">
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-gray-400">Distance</span>
<span className="text-sm text-gray-400">{filters.distance} miles</span>
</div>
<Slider
min={5}
max={100}
step={5}
value={filters.distance}
onChange={handleDistanceChange}
/>
</div>
</div>
{/* Price Range */}
<div>
<h4 className="text-lg font-medium text-white mb-3">Price Range</h4>
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-gray-400">${filters.priceRange.min}</span>
<span className="text-sm text-gray-400">${filters.priceRange.max === maxPrice ? filters.priceRange.max + '+' : filters.priceRange.max}</span>
</div>
<Slider
min={0}
max={maxPrice}
step={10}
range
value={[filters.priceRange.min, filters.priceRange.max]}
onChange={([min, max]) => handlePriceRangeChange({ min, max })}
/>
</div>
{/* Apply button - mobile only */}
<div className="md:hidden">
<button
onClick={() => setIsOpen(false)}
className="w-full py-3 bg-gold text-black rounded-md font-medium"
>
Apply Filters
</button>
</div>
</div>
</div>
);
};
#4. Event Details Page
This page displays comprehensive information about an event along with booking options.
// src/app/(frontend)/events/[id]/page.tsx
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { getEventById } from '@/lib/events';
import { EventGallery } from '@/components/EventDetails/EventGallery';
import { EventInfo } from '@/components/EventDetails/EventInfo';
import { EventLocation } from '@/components/EventDetails/EventLocation';
import { RelatedEvents } from '@/components/EventDetails/RelatedEvents';
import { TransportationOptions } from '@/components/EventDetails/TransportationOptions';
export default async function EventDetailsPage({ params }: { params: { id: string } }) {
const event = await getEventById(params.id);
if (!event) {
return (
<div className="container mx-auto px-4 py-20 text-center">
<h1 className="text-3xl font-bold text-white mb-4">Event Not Found</h1>
<p className="text-gray-400 mb-8">
The event you're looking for doesn't exist or has been removed.
</p>
<Link
href="/events"
className="inline-block px-6 py-3 bg-gold text-black rounded-md font-medium"
>
Browse Events
</Link>
</div>
);
}
return (
<div className="bg-black min-h-screen">
{/* Hero section with main image */}
<div className="relative h-[50vh] lg:h-[60vh]">
<Image
src={event.images?.[0]?.image?.url || '/images/event-placeholder.jpg'}
alt={event.event_name}
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-gradient-to-t from-black to-transparent" />
<div className="absolute bottom-0 left-0 w-full p-8">
<div className="container mx-auto">
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
{event.event_name}
</h1>
<div className="flex flex-wrap items-center gap-4 text-gray-300">
<span className="bg-gold text-black px-3 py-1 rounded-full text-sm font-medium">
${typeof event.event_price === 'number' ? event.event_price.toFixed(0) : event.event_price}
</span>
<span>{new Date(event.event_date).toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
})}</span>
<span>{event.location?.venue}</span>
</div>
</div>
</div>
</div>
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main content */}
<div className="lg:col-span-2 space-y-8">
{/* Event description */}
<div className="bg-gray-900 rounded-lg p-6">
<h2 className="text-2xl font-bold text-white mb-4">About This Event</h2>
<div className="prose prose-invert max-w-none">
{event.description}
</div>
</div>
{/* Event gallery */}
{event.images && event.images.length > 1 && (
<EventGallery images={event.images} />
)}
{/* Event information */}
<EventInfo event={event} />
{/* Event location */}
<EventLocation location={event.location} />
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Transportation booking */}
<div className="bg-gray-900 rounded-lg p-6 sticky top-24">
<h2 className="text-2xl font-bold text-white mb-4">Book Transportation</h2>
<p className="text-gray-300 mb-6">
Arrive in style with our premium transportation service. Select your preferred vehicle option below.
</p>
<TransportationOptions eventId={event.id} />
<div className="mt-6 text-sm text-gray-400">
<p>* All transportation includes professional chauffeur service</p>
<p>* Prices include 3-hour minimum booking</p>
</div>
</div>
</div>
</div>
{/* Related events */}
<div className="mt-16">
<h2 className="text-2xl font-bold text-white mb-6">Similar Events You Might Like</h2>
<RelatedEvents
currentEventId={event.id}
categories={event.categories?.map(cat => cat.id) || []}
/>
</div>
</div>
</div>
);
}
#5. Transportation Options Component
This component displays available vehicles for booking with an event.
// src/components/EventDetails/TransportationOptions.tsx
import React, { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { CheckIcon } from '@heroicons/react/24/outline';
interface TransportationOptionsProps {
eventId: string;
}
const vehicles = [
{
id: 'escalade',
name: 'Cadillac Escalade',
image: '/images/vehicles/escalade.jpg',
capacity: 7,
pricePerHour: 150,
features: [
'Premium leather interior',
'Professional chauffeur',
'Complimentary beverages',
'WiFi & device charging'
]
},
{
id: 'tesla',
name: 'Tesla Model X',
image: '/images/vehicles/tesla.jpg',
capacity: 5,
pricePerHour: 120,
features: [
'Eco-friendly electric vehicle',
'Professional chauffeur',
'Panoramic glass roof',
'WiFi & device charging'
]
}
];
export const TransportationOptions: React.FC<TransportationOptionsProps> = ({ eventId }) => {
const [selectedVehicle, setSelectedVehicle] = useState(vehicles[0].id);
const handleVehicleChange = (vehicleId: string) => {
setSelectedVehicle(vehicleId);
};
const selectedVehicleData = vehicles.find(v => v.id === selectedVehicle);
return (
<div className="space-y-6">
{/* Vehicle selection tabs */}
<div className="grid grid-cols-2 gap-2">
{vehicles.map((vehicle) => (
<button
key={vehicle.id}
className={`py-3 rounded-md font-medium transition-colors text-center ${
selectedVehicle === vehicle.id
? 'bg-gold text-black'
: 'bg-gray-800 text-white hover:bg-gray-700'
}`}
onClick={() => handleVehicleChange(vehicle.id)}
>
{vehicle.name}
</button>
))}
</div>
{/* Selected vehicle details */}
{selectedVehicleData && (
<div className="space-y-4">
<div className="relative aspect-[16/9] rounded-md overflow-hidden">
<Image
src={selectedVehicleData.image}
alt={selectedVehicleData.name}
fill
className="object-cover"
/>
</div>
<div className="flex justify-between items-center">
<div>
<h3 className="text-xl font-bold text-white">{selectedVehicleData.name}</h3>
<p className="text-gray-400">Up to {selectedVehicleData.capacity} passengers</p>
</div>
<div className="text-right">
<span className="text-xl text-gold font-bold">${selectedVehicleData.pricePerHour}</span>
<p className="text-gray-400">per hour</p>
</div>
</div>
<ul className="space-y-2">
{selectedVehicleData.features.map((feature, index) => (
<li key={index} className="flex items-center text-gray-300">
<CheckIcon className="w-5 h-5 text-gold mr-2" />
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={`/book/${eventId}?vehicle=${selectedVehicleData.id}`}
className="block w-full py-3 bg-gold text-black text-center rounded-md font-medium hover:bg-yellow-500 transition-colors"
>
Book This Vehicle
</Link>
</div>
)}
</div>
);
};
#6. Booking Form
This component allows users to enter booking details.
// src/app/(frontend)/book/[eventId]/BookingForm.tsx
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { DateTimePicker } from '@/components/DateTimePicker';
import { AddressInput } from '@/components/AddressInput';
import { PassengerCounter } from '@/components/PassengerCounter';
import { CheckIcon } from '@heroicons/react/24/outline';
interface BookingFormProps {
event: any;
vehicle: {
id: string;
name: string;
capacity: number;
pricePerHour: number;
};
}
export const BookingForm: React.FC<BookingFormProps> = ({ event, vehicle }) => {
const router = useRouter();
const [bookingData, setBookingData] = useState({
passengers: 1,
pickupLocation: '',
pickupTime: event.event_date ? new Date(new Date(event.event_date).getTime() - 2 * 60 * 60 * 1000) : new Date(),
dropoffLocation: event.location?.venue ? `${event.location.venue}, ${event.location.address}` : '',
dropoffTime: event.event_date ? new Date(event.event_date) : new Date(),
specialRequests: '',
addons: {
ambassador: false,
refreshments: false,
redCarpet: false
}
});
const calculateDuration = () => {
if (!bookingData.pickupTime || !bookingData.dropoffTime) return 3; // Minimum 3 hours
const diffMs = bookingData.dropoffTime.getTime() - bookingData.pickupTime.getTime();
const diffHours = diffMs / (1000 * 60 * 60);
return Math.max(3, Math.ceil(diffHours)); // Minimum 3 hours, rounded up
};
const calculatePrice = () => {
const hours = calculateDuration();
let total = hours * vehicle.pricePerHour;
// Add-ons
if (bookingData.addons.ambassador) total += 150;
if (bookingData.addons.refreshments) total += 75;
if (bookingData.addons.redCarpet) total += 100;
return total;
};
const handleChange = (field, value) => {
setBookingData({
...bookingData,
[field]: value
});
};
const handleAddonChange = (addon) => {
setBookingData({
...bookingData,
addons: {
...bookingData.addons,
[addon]: !bookingData.addons[addon]
}
});
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validate form
if (!bookingData.pickupLocation || !bookingData.dropoffLocation) {
alert('Please provide both pickup and dropoff locations');
return;
}
// Prepare booking data
const bookingPayload = {
event: event.id,
vehicle: vehicle.id,
passengers: bookingData.passengers,
pickupLocation: bookingData.pickupLocation,
pickupTime: bookingData.pickupTime.toISOString(),
dropoffLocation: bookingData.dropoffLocation,
dropoffTime: bookingData.dropoffTime.toISOString(),
specialRequests: bookingData.specialRequests,
addons: bookingData.addons,
totalPrice: calculatePrice()
};
try {
// Create draft booking
const response = await fetch('/api/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(bookingPayload)
});
const data = await response.json();
if (data.success) {
// Redirect to checkout
router.push(`/checkout/${data.bookingId}`);
} else {
alert('Error creating booking: ' + data.message);
}
} catch (error) {
console.error('Error creating booking:', error);
alert('Failed to create booking. Please try again.');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-8">
<div className="bg-gray-900 rounded-lg p-6">
<h2 className="text-2xl font-bold text-white mb-4">Transportation Details</h2>
<div className="space-y-5">
{/* Passenger count */}
<div>
<label className="block text-lg font-medium text-white mb-2">
Number of Passengers
</label>
<PassengerCounter
value={bookingData.passengers}
max={vehicle.capacity}
onChange={(value) => handleChange('passengers', value)}
/>
<p className="mt-1 text-sm text-gray-400">
Maximum capacity: {vehicle.capacity} passengers
</p>
</div>
{/* Pickup details */}
<div>
<label className="block text-lg font-medium text-white mb-2">
Pickup Details
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<AddressInput
value={bookingData.pickupLocation}
onChange={(value) => handleChange('pickupLocation', value)}
placeholder="Enter pickup address"
className="w-full"
/>
<DateTimePicker
label="Pickup Time"
value={bookingData.pickupTime}
onChange={(value) => handleChange('pickupTime', value)}
className="w-full"
/>
</div>
</div>
{/* Dropoff details */}
<div>
<label className="block text-lg font-medium text-white mb-2">
Dropoff Details
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<AddressInput
value={bookingData.dropoffLocation}
onChange={(value) => handleChange('dropoffLocation', value)}
placeholder="Enter dropoff address"
className="w-full"
/>
<DateTimePicker
label="Dropoff Time"
value={bookingData.dropoffTime}
onChange={(value) => handleChange('dropoffTime', value)}
className="w-full"
/>
</div>
</div>
{/* Special requests */}
<div>
<label className="block text-lg font-medium text-white mb-2">
Special Requests
</label>
<textarea
value={bookingData.specialRequests}
onChange={(e) => handleChange('specialRequests', e.target.value)}
placeholder="Any special requests or instructions for your driver..."
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-md text-white h-32"
/>
</div>
</div>
</div>
{/* Premium add-ons */}
<div className="bg-gray-900 rounded-lg p-6">
<h2 className="text-2xl font-bold text-white mb-4">Premium Add-ons</h2>
<div className="space-y-4">
<div className="flex items-start">
<input
type="checkbox"
id="ambassador"
checked={bookingData.addons.ambassador}
onChange={() => handleAddonChange('ambassador')}
className="mt-1 mr-3"
/>
<div>
<label htmlFor="ambassador" className="text-lg font-medium text-white block">
5CRSE Ambassador ($150)
</label>
<p className="text-gray-400">
A personal host who will ensure your event experience is flawless from start to finish.
</p>
</div>
</div>
<div className="flex items-start">
<input
type="checkbox"
id="refreshments"
checked={bookingData.addons.refreshments}
onChange={() => handleAddonChange('refreshments')}
className="mt-1 mr-3"
/>
<div>
<label htmlFor="refreshments" className="text-lg font-medium text-white block">
Premium Refreshments ($75)
</label>
<p className="text-gray-400">
Complimentary champagne and gourmet snacks for your journey.
</p>
</div>
</div>
<div className="flex items-start">
<input
type="checkbox"
id="redCarpet"
checked={bookingData.addons.redCarpet}
onChange={() => handleAddonChange('redCarpet')}
className="mt-1 mr-3"
/>
<div>
<label htmlFor="redCarpet" className="text-lg font-medium text-white block">
Red Carpet Arrival ($100)
</label>
<p className="text-gray-400">
Make a grand entrance with our exclusive red carpet service.
</p>
</div>
</div>
</div>
</div>
{/* Price summary */}
<div className="bg-gray-900 rounded-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold text-white">Price Summary</h2>
<span className="text-2xl font-bold text-gold">${calculatePrice()}</span>
</div>
<div className="space-y-2 border-t border-gray-800 pt-4">
<div className="flex justify-between">
<span className="text-gray-300">{vehicle.name} ({calculateDuration()} hours)</span>
<span className="text-white">${vehicle.pricePerHour * calculateDuration()}</span>
</div>
{bookingData.addons.ambassador && (
<div className="flex justify-between">
<span className="text-gray-300">5CRSE Ambassador</span>
<span className="text-white">$150</span>
</div>
)}
{bookingData.addons.refreshments && (
<div className="flex justify-between">
<span className="text-gray-300">Premium Refreshments</span>
<span className="text-white">$75</span>
</div>
)}
{bookingData.addons.redCarpet && (
<div className="flex justify-between">
<span className="text-gray-300">Red Carpet Arrival</span>
<span className="text-white">$100</span>
</div>
)}
</div>
<div className="mt-6">
<p className="text-gray-400 text-sm mb-4">
* Transportation charges include a 3-hour minimum booking.
* Payment will be securely processed on the next page.
</p>
<button
type="submit"
className="w-full py-4 bg-gold text-black rounded-md font-medium text-lg hover:bg-yellow-500 transition-colors"
>
Proceed to Checkout
</button>
</div>
</div>
</form>
);
};
#7. Ticketmaster API Integration
This service handles fetching and normalizing data from the Ticketmaster API.
// src/services/TicketmasterService.ts
import axios from 'axios';
import { getPayloadClient } from '@/payload/payloadClient';
const API_KEY = process.env.TICKETMASTER_API_KEY;
const API_BASE_URL = 'https://app.ticketmaster.com/discovery/v2';
export class TicketmasterService {
/**
* Search for events from Ticketmaster API
*/
static async searchEvents(params = {}) {
try {
const response = await axios.get(`${API_BASE_URL}/events.json`, {
params: {
apikey: API_KEY,
...params
}
});
// Handle empty results
if (!response.data._embedded?.events) {
return {
events: [],
page: {
totalElements: 0,
totalPages: 0,
number: 0,
size: 0
}
};
}
// Extract events and pagination data
const events = response.data._embedded.events.map(this.normalizeEvent);
const page = response.data.page;
return {
events,
page
};
} catch (error) {
console.error('Error fetching events from Ticketmaster:', error);
throw error;
}
}
/**
* Get event details by Ticketmaster ID
*/
static async getEventById(id) {
try {
const response = await axios.get(`${API_BASE_URL}/events/${id}`, {
params: {
apikey: API_KEY
}
});
return this.normalizeEvent(response.data);
} catch (error) {
console.error(`Error fetching event details for ID ${id}:`, error);
throw error;
}
}
/**
* Sync an event from Ticketmaster to Payload CMS
*/
static async syncEvent(ticketmasterId) {
try {
// Get event from Ticketmaster
const ticketmasterEvent = await this.getEventById(ticketmasterId);
// Check if event already exists in Payload
const payload = await getPayloadClient();
const existingEvents = await payload.find({
collection: 'events',
where: {
ticketmaster_id: {
equals: ticketmasterId
}
}
});
// If event exists, update it
if (existingEvents.docs.length > 0) {
const updatedEvent = await payload.update({
collection: 'events',
id: existingEvents.docs[0].id,
data: this.mapToPayloadEvent(ticketmasterEvent)
});
return {
success: true,
message: 'Event updated successfully',
event: updatedEvent
};
}
// Otherwise, create a new event
const newEvent = await payload.create({
collection: 'events',
data: this.mapToPayloadEvent(ticketmasterEvent)
});
return {
success: true,
message: 'Event created successfully',
event: newEvent
};
} catch (error) {
console.error(`Error syncing event ${ticketmasterId}:`, error);
return {
success: false,
message: `Error syncing event: ${error.message}`,
error
};
}
}
/**
* Normalize a Ticketmaster event to a standard format
*/
static normalizeEvent(event) {
// Handle venue and location
const venue = event._embedded?.venues?.[0] || {};
const location = venue ? {
name: venue.name,
address: venue.address?.line1,
city: venue.city?.name,
state: venue.state?.stateCode,
country: venue.country?.countryCode,
postalCode: venue.postalCode,
location: venue.location ? {
latitude: parseFloat(venue.location.latitude),
longitude: parseFloat(venue.location.longitude)
} : null
} : null;
// Handle images
const images = event.images?.map(img => ({
url: img.url,
width: img.width,
height: img.height,
ratio: img.ratio
})) || [];
// Handle dates
let eventDate = null;
if (event.dates?.start) {
const { localDate, localTime } = event.dates.start;
if (localDate) {
eventDate = localTime
? new Date(`${localDate}T${localTime}`)
: new Date(`${localDate}T00:00:00`);
}
}
// Handle pricing
let priceRange = null;
if (event.priceRanges && event.priceRanges.length > 0) {
const range = event.priceRanges[0];
priceRange = {
min: range.min,
max: range.max,
currency: range.currency
};
}
// Handle classifications/categories
const categories = event.classifications?.map(classification => ({
segment: classification.segment?.name,
genre: classification.genre?.name,
subGenre: classification.subGenre?.name
})) || [];
return {
id: event.id,
name: event.name,
description: event.info || event.description || '',
ticketmasterUrl: event.url,
eventDate,
onsaleStartDate: event.sales?.public?.startDateTime
? new Date(event.sales.public.startDateTime)
: null,
status: event.dates?.status?.code,
images,
venue: location,
priceRange,
categories,
source: 'ticketmaster'
};
}
/**
* Map a normalized event to Payload CMS format
*/
static mapToPayloadEvent(event) {
return {
event_name: event.name,
event_date: event.eventDate,
event_price: event.priceRange?.min || 0,
description: event.description,
location: {
venue: event.venue?.name || '',
address: [
event.venue?.address,
event.venue?.city,
event.venue?.state
].filter(Boolean).join(', '),
city: event.venue?.city || '',
state: event.venue?.state || '',
zip: event.venue?.postalCode || '',
country: event.venue?.country || 'US',
coordinates: event.venue?.location ? [
event.venue.location.longitude,
event.venue.location.latitude
] : undefined
},
ticketmaster_id: event.id,
ticketmaster_url: event.ticketmasterUrl,
images: event.images.slice(0, 5).map(img => ({
image: {
url: img.url
}
})),
capacity: 0, // Not provided by Ticketmaster
status: 'published',
source: 'ticketmaster'
};
}
}
#8. Checkout & Payment Flow
The checkout component handles payment processing and booking confirmation.
// src/app/(frontend)/checkout/[bookingId]/page.tsx
import React from 'react';
import { redirect } from 'next/navigation';
import { getBookingById } from '@/lib/bookings';
import { getEventById } from '@/lib/events';
import { getVehicleById } from '@/lib/vehicles';
import { CheckoutSummary } from '@/components/Checkout/CheckoutSummary';
import { PaymentForm } from '@/components/Checkout/PaymentForm';
export default async function CheckoutPage({ params }: { params: { bookingId: string } }) {
// Get booking details
const booking = await getBookingById(params.bookingId);
if (!booking) {
redirect('/events');
}
// Get event and vehicle details
const event = booking.event ? await getEventById(booking.event.toString()) : null;
const vehicle = booking.vehicle ? await getVehicleById(booking.vehicle.toString()) : null;
if (!event || !vehicle) {
redirect('/events');
}
return (
<div className="bg-black min-h-screen py-12">
<div className="container mx-auto px-4">
<h1 className="text-4xl font-bold text-white mb-8">Complete Your Booking</h1>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Payment form */}
<div className="lg:col-span-2">
<div className="bg-gray-900 rounded-lg p-6">
<h2 className="text-2xl font-bold text-white mb-6">Payment Details</h2>
<PaymentForm booking={booking} />
</div>
</div>
{/* Order summary */}
<div className="lg:col-span-1">
<CheckoutSummary
booking={booking}
event={event}
vehicle={vehicle}
/>
</div>
</div>
</div>
</div>
);
}
#Data Flow Architecture
sequenceDiagram
participant User
participant Frontend
participant PayloadAPI
participant TicketmasterAPI
participant Payments
User->>Frontend: Visit Event Discovery
Frontend->>PayloadAPI: Request Events
Frontend->>TicketmasterAPI: Request External Events
TicketmasterAPI-->>Frontend: Return Events
PayloadAPI-->>Frontend: Return Events
Frontend->>Frontend: Merge & Normalize Data
Frontend-->>User: Display Events
User->>Frontend: Select Event
Frontend->>PayloadAPI: Get Event Details
PayloadAPI-->>Frontend: Return Event Data
Frontend-->>User: Display Event Details
User->>Frontend: Book Transportation
Frontend->>PayloadAPI: Create Booking (Draft)
PayloadAPI-->>Frontend: Return Booking ID
Frontend-->>User: Show Checkout
User->>Frontend: Enter Payment Details
Frontend->>Payments: Process Payment
Payments-->>Frontend: Payment Confirmation
Frontend->>PayloadAPI: Update Booking Status
PayloadAPI-->>Frontend: Return Updated Booking
Frontend-->>User: Show Confirmation
#Filter & Search Architecture
flowchart LR
Client[Client Browser] -->|Request Events| API[API Layer]
API -->|Find Events| QueryBuilder[Query Builder]
subgraph Query Builder
TM[Ticketmaster Query] -->|Combine Results| Normalizer[Data Normalizer]
Payload[Payload Query] -->|Combine Results| Normalizer
end
QueryBuilder -->|Execute Queries| DataSources
subgraph DataSources
Ticketmaster[Ticketmaster API]
PayloadCMS[Payload CMS]
end
Ticketmaster --> TM
PayloadCMS --> Payload
Normalizer -->|Return Normalized Results| API
API -->|Return Events| Client
subgraph Client Filters
Category[Event Category]
Date[Date Range]
Location[Geographic Location]
Price[Price Range]
end
Category --> Client
Date --> Client
Location --> Client
Price --> Client
#Implementation Priorities & Timeline
-
Phase 1: Core Discovery (3 weeks)
- Implement basic event grid
- Develop event card component
- Create initial filter UI
- Set up Ticketmaster API integration
-
Phase 2: Advanced Discovery (3 weeks)
- Enhance filtering with more options
- Add search functionality
- Create detailed event pages
- Implement event recommendations
-
Phase 3: Booking Flow (2 weeks)
- Develop transportation selection UI
- Create booking details form
- Implement guest management features
- Design booking summary component
-
Phase 4: Checkout & Completion (2 weeks)
- Build payment integration
- Create order summary component
- Implement booking confirmation emails
- Develop booking management interface
-
Future Enhancements
- Map view for geographic exploration
- Personalized event recommendations
- Enhanced animations and transitions
- Additional payment methods
#Questions to Consider
- How should we handle the merging and normalization of events from multiple sources (Ticketmaster + custom)?
- What are the most important filter criteria for our target user base?
- Should the transportation booking be fully integrated into the event booking process, or kept as separate steps?
- What payment processor integrations should we prioritize?
- How will bookings be managed after they're created (modifications, cancellations, etc.)?
#Next Steps
- Set up the Ticketmaster API integration
- Create the event card and grid components
- Implement the filtering system
- Develop the event detail page template
- Design the booking form and transportation selection components
