annaville-sda-site/src/pages/EventDetail.jsx

270 lines
8.3 KiB
JavaScript

import React, { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { Helmet } from 'react-helmet-async'
import { getEvent } from '../utils/api'
import { googleCalendarUrl, downloadICS } from '../utils/calendar'
import { track, events as ga } from '../utils/analytics'
const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '')
function parseTime(timeStr) {
if (!timeStr) return '10:00'
const match = `${timeStr}`.match(/(\d+):(\d+)\s*(AM|PM)/i)
if (!match) return '10:00'
let hours = parseInt(match[1], 10)
const minutes = match[2]
const period = match[3].toUpperCase()
if (period === 'PM' && hours !== 12) hours += 12
if (period === 'AM' && hours === 12) hours = 0
return `${hours.toString().padStart(2, '0')}:${minutes}`
}
function resolveImageUrl(value, fallback) {
if (typeof value === 'string') {
const trimmed = value.trim()
if (!trimmed) return fallback
if (/^(?:https?:)?\/\//i.test(trimmed) || trimmed.startsWith('data:')) {
return trimmed
}
const path = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
if (apiBaseUrl) {
return `${apiBaseUrl}${path}`
}
return path
}
return fallback
}
function getFallbackImage(event) {
return ''
}
function formatDate(dateStr) {
const date = new Date(dateStr + 'T00:00:00')
if (Number.isNaN(date.getTime())) return dateStr
return date.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'America/Chicago',
})
}
function formatTime(timeStr) {
if (!timeStr) return 'TBA'
return timeStr
}
export default function EventDetail() {
const { slug } = useParams()
const [event, setEvent] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let ignore = false
async function load() {
setLoading(true)
setError(null)
try {
const data = await getEvent(slug)
if (!ignore) {
setEvent(data)
}
} catch (err) {
if (!ignore) {
setError(err)
setEvent(null)
}
} finally {
if (!ignore) {
setLoading(false)
}
}
}
load()
return () => {
ignore = true
}
}, [slug])
useEffect(() => {
if (event) {
track(ga.EVENT_DETAILS_VIEW, { slug: event.slug })
}
}, [event])
if (loading) {
return (
<section className="section">
<div className="container">
<p className="text-muted">Loading event...</p>
</div>
</section>
)
}
if (error) {
return (
<section className="section">
<div className="container">
<p className="text-red-600">Unable to load this event. Please try again later.</p>
</div>
</section>
)
}
if (!event) {
return (
<section className="section">
<div className="container">
<p>Event not found.</p>
</div>
</section>
)
}
const time24 = parseTime(event.time || '10:00 AM')
const start = new Date(`${event.date}T${time24}:00`)
const end = new Date(start.getTime() + 60 * 60 * 1000)
const fallbackImage = getFallbackImage(event)
const coverImage = resolveImageUrl(event.image, fallbackImage)
const displayDate = formatDate(event.date)
const displayTime = formatTime(event.time)
const eventUrl = typeof window !== 'undefined' ? window.location.href : ''
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Event',
name: event.title,
startDate: start.toISOString(),
endDate: end.toISOString(),
eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',
location: {
'@type': 'Place',
name: 'Annaville SDA Church',
address: '2710 Violet Rd, Corpus Christi, TX 78410',
},
description: event.description,
}
return (
<section className="section bg-sand/30">
<Helmet>
<title>{event.title} | Events</title>
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
</Helmet>
<div className="container space-y-8">
<Link to="/events" className="inline-flex items-center text-sm text-primary hover:text-primaryHover">
Back to all events
</Link>
<div className="grid lg:grid-cols-[2fr,1fr] gap-10 items-start">
<article className="bg-white rounded-2xl shadow-level1 overflow-hidden">
{coverImage && (
<div className="relative h-80 md:h-96 bg-sand">
<img
src={coverImage}
alt={`${event.title} hero image`}
className="absolute inset-0 h-full w-full object-cover"
loading="lazy"
/>
</div>
)}
<div className="p-10 space-y-6">
<header className="space-y-3">
<p className="text-xs uppercase tracking-[0.2em] text-muted">
{event.category || 'Featured Event'}
</p>
<h1 className="font-heading text-display30 text-ink">{event.title}</h1>
<p className="text-muted text-sm">
{displayDate} {displayTime} CT{event.location ? `${event.location}` : ''}
</p>
</header>
<div className="space-y-4 text-body leading-relaxed">
{`${event.description || ''}`.split(/\n{2,}/).map((paragraph, idx) => (
<p key={idx}>{paragraph}</p>
))}
</div>
<div className="flex flex-wrap gap-3 pt-4">
<a
className="btn"
target="_blank"
rel="noreferrer"
href={googleCalendarUrl({
title: event.title,
details: event.description,
location: event.location,
start,
end,
})}
>
Add to Google Calendar
</a>
<button
className="btn-outline"
onClick={() =>
downloadICS({
title: event.title,
details: event.description,
location: event.location,
start,
end,
filename: `${event.slug}.ics`,
})
}
>
Download iCal File
</button>
</div>
</div>
</article>
<aside className="bg-white rounded-2xl shadow-level1 p-8 space-y-6">
<div className="space-y-3">
<h2 className="font-heading text-h4 text-ink">Event Details</h2>
<dl className="space-y-4 text-sm">
<div>
<dt className="font-semibold text-ink">When</dt>
<dd className="text-muted">{displayDate} · {displayTime} CT</dd>
</div>
{event.location && (
<div>
<dt className="font-semibold text-ink">Where</dt>
<dd className="text-muted">{event.location}</dd>
</div>
)}
{event.category && (
<div>
<dt className="font-semibold text-ink">Category</dt>
<dd className="text-muted">{event.category}</dd>
</div>
)}
</dl>
</div>
<div className="space-y-3">
<h2 className="font-heading text-h4 text-ink">Share</h2>
<div className="flex flex-wrap gap-3 text-sm">
<a
className="btn-outline"
href={`mailto:?subject=${encodeURIComponent(event.title)}&body=${encodeURIComponent(`Join me at ${event.title} on ${displayDate}. More info: ${eventUrl}`)}`}
>
Email Invite
</a>
<button
className="btn-outline"
onClick={() => navigator?.clipboard?.writeText(eventUrl)}
>
Copy Link
</button>
</div>
</div>
</aside>
</div>
</div>
</section>
)
}