fertiges design

This commit is contained in:
2025-08-28 13:48:46 +02:00
parent c66a0e49f3
commit d31681cee1
50 changed files with 7516 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import React from 'react'
import { googleCalendarUrl, downloadICS } from '../utils/calendar'
import { track, events as ga } from '../utils/analytics'
export default function AddToCalendar() {
const handleAppleCalendar = () => {
const now = new Date()
const nextSat = new Date(now)
const day = now.getDay()
const diff = (6 - day + 7) % 7 || 7
nextSat.setDate(now.getDate() + diff)
const start = new Date(Date.UTC(nextSat.getFullYear(), nextSat.getMonth(), nextSat.getDate(), 14, 30)) // approx 9:30 CT
const end = new Date(Date.UTC(nextSat.getFullYear(), nextSat.getMonth(), nextSat.getDate(), 16, 0))
downloadICS({
title: 'Sabbath School',
details: 'Weekly Sabbath School at Annaville SDA Church.',
location: '2710 Violet Rd, Corpus Christi, TX 78410',
start,
end,
filename: 'sabbath-school.ics'
})
track(ga.CTA_CLICK, { label: 'add_to_apple_calendar' })
}
const handleGoogleCalendar = () => {
const nextSaturday = new Date()
const day = nextSaturday.getDay()
const diff = (6 - day + 7) % 7 || 7
nextSaturday.setDate(nextSaturday.getDate() + diff)
const end = new Date(nextSaturday.getTime() + 90 * 60 * 1000) // 90 minutes later
const url = googleCalendarUrl({
title: 'Sabbath School',
details: 'Weekly Sabbath School at Annaville SDA Church.',
location: '2710 Violet Rd, Corpus Christi, TX 78410',
start: nextSaturday,
end: end
})
window.open(url, '_blank', 'noopener,noreferrer')
track(ga.CTA_CLICK, { label: 'add_to_google_calendar' })
}
return (
<div className="flex flex-wrap gap-6">
<button
className="chip bg-primary text-white hover:bg-primaryHover focus-ring"
onClick={handleAppleCalendar}
aria-label="Add Sabbath School to Apple Calendar"
>
📅 Add to Apple Calendar
</button>
<button
className="chip bg-primaryDeep text-white hover:bg-primaryHover focus-ring"
onClick={handleGoogleCalendar}
aria-label="Add Sabbath School to Google Calendar"
>
📅 Add to Google Calendar
</button>
</div>
)
}

159
src/components/Cards.jsx Normal file
View File

@@ -0,0 +1,159 @@
import React from 'react'
import { Link } from 'react-router-dom'
export function EventCard({ e }){
const dt = new Date(e.date)
const mon = dt.toLocaleString('en', { month:'short' })
const day = dt.getDate()
// Map specific event names to images
const getEventImage = (title) => {
const lowerTitle = title.toLowerCase()
if (lowerTitle.includes('community sabbath lunch')) {
return '/assets/potluck.png'
}
if (lowerTitle.includes('youth vespers')) {
return '/assets/youth_vespers.png'
}
if (lowerTitle.includes('neighborhood food drive')) {
return '/assets/family_entry.png'
}
if (lowerTitle.includes('potluck') || lowerTitle.includes('lunch') || lowerTitle.includes('dinner')) {
return '/assets/potluck.png'
}
if (lowerTitle.includes('family') || lowerTitle.includes('community')) {
return '/assets/family_entry.png'
}
if (lowerTitle.includes('welcome') || lowerTitle.includes('committee')) {
return '/assets/welcome_commite.png'
}
// Default event image
return '/assets/potluck.png'
}
return (
<article className="card p-10 flex flex-col" style={{aspectRatio:'4/3'}}>
<div className="flex items-center gap-6 mb-8">
<div className="w-20 h-20 rounded-full overflow-hidden bg-primary text-white flex items-center justify-center">
<img
src={getEventImage(e.title)}
alt={`${e.title} event`}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
<div>
<h3 className="font-heading text-h3">{e.title}</h3>
<div className="text-muted text-small mt-2">{e.time} {e.location}</div>
</div>
</div>
<p className="mt-6 text-body mb-8">{e.description}</p>
<div className="mt-auto pt-8">
<Link
to={`/events/${e.slug}`}
className="btn-outline"
aria-label={`Event details: ${e.title} on ${mon} ${day}`}
>
Details {e.title}
</Link>
</div>
</article>
)
}
export function MinistryCard({ m }){
// Map specific ministry names to images
const getMinistryImage = (name) => {
const lowerName = name.toLowerCase()
if (lowerName === 'children\'s ministry') {
return '/assets/children_ministry_craft.png'
}
if (lowerName === 'youth ministry') {
return '/assets/youth_vespers.png'
}
if (lowerName === 'adult sabbath school') {
return '/assets/speeking.png'
}
if (lowerName === 'women\'s ministry') {
return '/assets/pray_heart.png'
}
if (lowerName === 'men\'s ministry') {
return '/assets/family_entry.png'
}
if (lowerName === 'community outreach') {
return '/assets/welcome_commite.png'
}
// Fallback for other ministries
if (lowerName.includes('children') || lowerName.includes('kids')) {
return '/assets/children_ministry_craft.png'
}
if (lowerName.includes('youth') || lowerName.includes('vespers')) {
return '/assets/youth_vespers.png'
}
if (lowerName.includes('welcome') || lowerName.includes('committee')) {
return '/assets/welcome_commite.png'
}
if (lowerName.includes('prayer') || lowerName.includes('pray')) {
return '/assets/pray_heart.png'
}
// Default ministry image
return '/assets/welcome_commite.png'
}
return (
<article className="card p-10 flex flex-col">
<div className="mb-6">
<img
src={getMinistryImage(m.name)}
alt={`${m.name} ministry`}
className="w-full h-80 object-cover rounded-lg mb-4"
loading="lazy"
/>
<h3 className="font-heading text-h3">{m.name}</h3>
</div>
<div className="text-muted text-small mb-3">{m.meeting}</div>
<div className="text-small mb-8">Leader: {m.leader}</div>
<div className="mt-auto pt-8">
<Link
to={`/ministries/${m.slug}`}
className="btn-outline"
aria-label={`Explore ${m.name} ministry`}
>
Explore {m.name}
</Link>
</div>
</article>
)
}
export function SermonCard({ s }){
return (
<article className="card p-10 flex flex-col" style={{aspectRatio:'4/3'}}>
<div className="flex items-center gap-6 mb-8">
<div className="w-24 h-24 bg-sand rounded-lg overflow-hidden flex items-center justify-center">
<img
src="/assets/speeking.png"
alt={`Sermon by ${s.speaker}`}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
<div>
<h3 className="font-heading text-h3">{s.title}</h3>
<div className="text-muted text-small mt-2">{s.speaker} {new Date(s.date).toLocaleDateString()}</div>
</div>
</div>
<p className="mt-6 text-body mb-8">{s.summary}</p>
<div className="mt-auto pt-8">
<Link
to={`/sermons/${s.slug}`}
className="btn-outline"
aria-label={`Watch or listen to ${s.title} by ${s.speaker}`}
>
Watch/Listen {s.title}
</Link>
</div>
</article>
)
}

95
src/components/Footer.jsx Normal file
View File

@@ -0,0 +1,95 @@
import React from 'react'
import { Link } from 'react-router-dom'
export default function Footer(){
const year = new Date().getFullYear()
return (
<footer id="footer" role="contentinfo" className="bg-sand border-t border-subtle mt-12">
<div className="container grid md:grid-cols-3 gap-8 py-16">
<div>
<h3 className="font-heading text-h3 mb-4">Annaville Seventh-day Adventist Church</h3>
<p className="text-body mb-2">2710 Violet Rd<br/>Corpus Christi, TX 78410</p>
<p className="mb-2">
<a href="tel:+13612415501" className="text-primary underline hover:text-primaryHover">
Call (361) 241-5501
</a>
</p>
<p className="mb-2">
<a href="https://maps.google.com/?q=2710+Violet+Rd,+Corpus+Christi,+TX+78410"
className="text-primary underline hover:text-primaryHover">
Get Directions
</a>
</p>
<p className="text-muted text-small">Sabbath School 9:30 AM Divine Worship 11:00 AM</p>
{/* Leadership Information */}
<div className="mt-6 text-small text-muted">
<p><strong>Pastor:</strong> Matt McMearty</p>
<p><strong>Head Elder:</strong> Regena Simms</p>
</div>
</div>
<div>
<h3 className="font-heading text-h3 mb-4">Quick Links</h3>
<ul className="space-y-3">
<li><Link to="/about" className="text-body hover:text-primary transition-colors">About Us</Link></li>
<li><Link to="/services" className="text-body hover:text-primary transition-colors">Services</Link></li>
<li><Link to="/resources" className="text-body hover:text-primary transition-colors">Resources</Link></li>
<li><Link to="/prayer-requests" className="text-body hover:text-primary transition-colors">Prayer Requests</Link></li>
<li><Link to="/calendar" className="text-body hover:text-primary transition-colors">Calendar</Link></li>
<li><Link to="/beliefs" className="text-body hover:text-primary transition-colors">Our Beliefs</Link></li>
<li><Link to="/contact" className="text-body hover:text-primary transition-colors">Contact</Link></li>
</ul>
</div>
<div>
<h3 className="font-heading text-h3 mb-4">Newsletter</h3>
<p className="text-body mb-4">
If you would like to receive our newsletter please fill out the form below.
</p>
<form className="space-y-4">
<div>
<label htmlFor="footer-newsletter-name" className="block text-sm font-medium text-ink mb-2">
Name:
</label>
<input
id="footer-newsletter-name"
type="text"
className="w-full border border-subtle rounded-lg px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent transition-colors"
placeholder="Your name"
/>
</div>
<div>
<label htmlFor="footer-newsletter-email" className="block text-sm font-medium text-ink mb-2">
Email Address:
</label>
<input
id="footer-newsletter-email"
type="email"
className="w-full border border-subtle rounded-lg px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent transition-colors"
placeholder="your.email@example.com"
/>
</div>
<button type="submit" className="btn w-full">
Subscribe to Newsletter
</button>
</form>
</div>
</div>
<div className="border-t border-subtle py-6">
<div className="container flex flex-wrap items-center gap-4 justify-between">
<div className="text-small text-muted">
© {year} Annaville Seventh-day Adventist Church. All rights reserved.
</div>
<div className="text-small flex gap-6">
<Link className="text-muted hover:text-primary transition-colors" to="/privacy">Privacy Policy</Link>
<Link className="text-muted hover:text-primary transition-colors" to="/terms">Terms of Use</Link>
<Link className="text-muted hover:text-primary transition-colors" to="/accessibility">Accessibility</Link>
</div>
</div>
</div>
</footer>
)
}

93
src/components/Header.jsx Normal file
View File

@@ -0,0 +1,93 @@
import React, { useState } from 'react'
import { Link, NavLink } from 'react-router-dom'
import { track, events } from '../utils/analytics'
const navItems = [
{ to:'/about', label:'ABOUT US' },
{ to:'/services', label:'SERVICES' },
{ to:'/resources', label:'RESOURCES' },
{ to:'/prayer-requests', label:'PRAYER REQUESTS' },
{ to:'/calendar', label:'CALENDAR' },
{ to:'/beliefs', label:'OUR BELIEFS' },
]
export default function Header(){
const [open, setOpen] = useState(false)
return (
<header role="banner" className="z-50 bg-white/90 backdrop-blur border-b border-subtle">
<nav id="navigation" aria-label="Main navigation" className="container flex items-center justify-between h-20">
<div className="flex items-center gap-4">
<Link to="/" className="flex items-center gap-4 font-heading text-lg font-semibold tracking-tight text-ink leading-tight">
<img
src="/assets/favicon.ico.gif"
alt="Annaville SDA Church Logo"
className="w-20 h-20 rounded-lg"
/>
<span>
Annaville Seventh-day<br />Adventist Church
</span>
</Link>
</div>
<div className="hidden md:flex items-center gap-6">
{navItems.map(item => (
<NavLink
key={item.to}
to={item.to}
className={({isActive})=>`text-sm font-medium transition-colors ${isActive?'text-primary font-semibold':'text-ink hover:text-primary'}`}
>
{item.label}
</NavLink>
))}
<Link
to="/contact"
className="btn text-sm px-4 py-2"
onClick={()=>track(events.CTA_CLICK,{label:'contact'})}
>
Contact Us
</Link>
</div>
<button
aria-label="Open menu"
className="md:hidden btn-ghost"
onClick={()=>setOpen(true)}
>
<span aria-hidden className="text-3xl"></span>
</button>
</nav>
{open && (
<div role="dialog" aria-modal="true" className="md:hidden fixed inset-0 bg-black/30" onClick={()=>setOpen(false)}>
<div className="absolute top-0 right-0 w-[80%] h-full bg-white shadow-level1 p-8" onClick={e=>e.stopPropagation()}>
<div className="flex justify-between items-center mb-12">
<span className="font-heading text-h2">Menu</span>
<button className="btn-ghost" onClick={()=>setOpen(false)} aria-label="Close menu"></button>
</div>
<ul className="space-y-6">
{navItems.map(item => (
<li key={item.to}>
<NavLink
to={item.to}
onClick={()=>setOpen(false)}
className={({isActive})=>`block py-5 text-body ${isActive?'text-primary font-semibold':'text-ink hover:text-primary'}`}
>
{item.label}
</NavLink>
</li>
))}
<li>
<Link
to="/contact"
className="btn w-full"
onClick={()=>{setOpen(false); track(events.CTA_CLICK,{label:'contact'})}}
>
Contact Us
</Link>
</li>
</ul>
</div>
</div>
)}
</header>
)
}

95
src/components/Hero.jsx Normal file
View File

@@ -0,0 +1,95 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { track, events } from '../utils/analytics'
import LazyImage from './LazyImage'
export function TextHero(){
const scrollToServiceTimes = () => {
const element = document.getElementById('service-times')
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
track(events.CTA_CLICK, { label: 'service_times' })
}
return (
<section className="relative bg-gradient-to-br from-sand to-surface">
<div className="container py-20">
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Content on the Left */}
<div className="order-1 lg:order-1">
<h1 className="font-heading text-h1 mb-8">Welcome to Annaville Seventh-day Adventist Church</h1>
<p className="text-ink text-body mb-10">The Annaville SDA Church offers worship services for members, non-members, or anyone interested in learning more about practical Christian living from the Word of God.</p>
<div className="mb-10 text-muted text-small">Sabbath School 9:30 AM Divine Worship 11:00 AM 2710 Violet Rd</div>
<div className="flex flex-wrap gap-6">
<Link
to="/visit"
className="btn"
onClick={() => track(events.CTA_CLICK, { label: 'plan_visit' })}
data-cta="plan_visit"
>
Plan Your Visit
</Link>
<button
className="btn-outline"
onClick={scrollToServiceTimes}
data-cta="service_times"
>
See Service Times
</button>
</div>
</div>
{/* Large Hero Image on the Right */}
<div className="order-2 lg:order-2">
<LazyImage
src="/assets/hero_golden_hour.png"
alt="Annaville SDA Church building during golden hour with people walking on the path"
className="w-full h-[400px] object-cover rounded-lg shadow-lg"
/>
</div>
</div>
</div>
</section>
)
}
// Safe photo hero variant with dim overlay; H1 remains text-first
export function PhotoHeroSafe(){
const scrollToServiceTimes = () => {
const element = document.getElementById('service-times')
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
track(events.CTA_CLICK, { label: 'service_times' })
}
return (
<section className="relative">
<LazyImage src="/assets/hero_golden_hour.png" alt="Annaville SDA Church building during golden hour" className="absolute right-0 top-8 w-1/4 h-full" />
<div className="absolute right-0 top-8 w-1/4 h-full bg-black/40" aria-hidden="true"></div>
<div className="relative container py-40 text-white">
<h1 className="font-heading text-h1 mb-8">Welcome to Annaville Seventh-day Adventist Church</h1>
<p className="text-white text-body mb-10">The Annaville SDA Church offers worship services for members, non-members, or anyone interested in learning more about practical Christian living from the Word of God.</p>
<div className="mb-10 text-white/90 text-small">Sabbath School 9:30 AM Divine Worship 11:00 AM 2710 Violet Rd</div>
<div className="flex flex-wrap gap-6">
<Link
to="/visit"
className="btn"
data-cta="plan_visit"
>
Plan Your Visit
</Link>
<button
className="btn-outline text-white border-white hover:bg-white/10"
onClick={scrollToServiceTimes}
data-cta="service_times"
>
See Service Times
</button>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,82 @@
import React, { useState, useEffect, useRef } from 'react'
export default function LazyImage({
src,
alt,
className = "",
placeholder = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300'%3E%3Crect width='400' height='300' fill='%23f3f4f6'/%3E%3C/svg%3E",
...props
}) {
const [isLoaded, setIsLoaded] = useState(false)
const [isInView, setIsInView] = useState(false)
const imgRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true)
observer.disconnect()
}
},
{
rootMargin: '50px 0px',
threshold: 0.01
}
)
if (imgRef.current) {
observer.observe(imgRef.current)
}
return () => observer.disconnect()
}, [])
const handleLoad = () => {
setIsLoaded(true)
}
const handleError = () => {
// Fallback to placeholder if image fails to load
setIsLoaded(true)
}
return (
<div
ref={imgRef}
className={`relative overflow-hidden ${className}`}
{...props}
>
{/* Placeholder */}
<img
src={placeholder}
alt=""
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${
isLoaded ? 'opacity-0' : 'opacity-100'
}`}
aria-hidden="true"
/>
{/* Actual image */}
{isInView && (
<img
src={src}
alt={alt}
className={`w-full h-full object-cover transition-opacity duration-300 ${
isLoaded ? 'opacity-100' : 'opacity-0'
}`}
loading="lazy"
onLoad={handleLoad}
onError={handleError}
/>
)}
{/* Loading indicator */}
{!isLoaded && isInView && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,43 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { track, events } from '../utils/analytics'
export default function MobileStickyBar(){
const scrollToVisitForm = () => {
const element = document.getElementById('visit-form')
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
track(events.CTA_CLICK, { label: 'plan_visit' })
}
return (
<nav className="fixed inset-x-0 bottom-0 z-40 flex justify-around border-t bg-surface p-2 md:hidden">
<a
href="tel:+13612415501"
className="flex flex-col items-center gap-1 text-sm focus-ring"
aria-label="Call the church"
onClick={() => track(events.CLICK_TO_CALL, { tel: '3612415501' })}
data-cta="click_to_call"
>
📞 Call
</a>
<a
href="https://maps.apple.com/?q=2710+Violet+Rd+Corpus+Christi"
className="flex flex-col items-center gap-1 text-sm focus-ring"
onClick={() => track(events.OPEN_DIRECTIONS, { provider: 'maps' })}
data-cta="open_directions"
>
🧭 Directions
</a>
<button
className="flex flex-col items-center gap-1 text-sm focus-ring"
onClick={scrollToVisitForm}
data-cta="plan_visit"
>
📝 Plan Visit
</button>
</nav>
)
}

103
src/components/SEOHead.jsx Normal file
View File

@@ -0,0 +1,103 @@
import React from 'react'
import { Helmet } from 'react-helmet-async'
export default function SEOHead({
title,
description,
keywords = '',
image = '/assets/og-image.jpg',
url = '',
type = 'website',
publishedAt = '',
modifiedAt = '',
author = 'Annaville Seventh-day Adventist Church'
}) {
const siteUrl = 'https://annavillesda.org' // Replace with your actual domain
const fullUrl = url ? `${siteUrl}${url}` : siteUrl
const fullImageUrl = image.startsWith('http') ? image : `${siteUrl}${image}`
return (
<Helmet>
{/* Basic Meta Tags */}
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords} />
<meta name="author" content={author} />
{/* Open Graph / Facebook */}
<meta property="og:type" content={type} />
<meta property="og:url" content={fullUrl} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullImageUrl} />
<meta property="og:site_name" content="Annaville Seventh-day Adventist Church" />
<meta property="og:locale" content="en_US" />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={fullUrl} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullImageUrl} />
{/* Additional Meta Tags */}
<meta name="robots" content="index, follow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#7E0F14" />
{/* Canonical URL */}
<link rel="canonical" href={fullUrl} />
{/* Favicon */}
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
{/* Manifest */}
<link rel="manifest" href="/site.webmanifest" />
{/* Article specific meta tags */}
{type === 'article' && (
<>
<meta property="article:published_time" content={publishedAt} />
<meta property="article:modified_time" content={modifiedAt} />
<meta property="article:author" content={author} />
</>
)}
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": type === 'article' ? 'Article' : 'WebPage',
"name": title,
"description": description,
"url": fullUrl,
"image": fullImageUrl,
"publisher": {
"@type": "Organization",
"name": "Annaville Seventh-day Adventist Church",
"url": siteUrl,
"logo": {
"@type": "ImageObject",
"url": `${siteUrl}/logo.png`
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": fullUrl
},
...(type === 'article' && {
"datePublished": publishedAt,
"dateModified": modifiedAt,
"author": {
"@type": "Person",
"name": author
}
})
})}
</script>
</Helmet>
)
}

View File

@@ -0,0 +1,43 @@
import React from 'react'
import { NavLink } from 'react-router-dom'
const navItems = [
{ to:'/about', label:'ABOUT US' },
{ to:'/services', label:'SERVICES' },
{ to:'/resources', label:'RESOURCES' },
{ to:'/prayer-requests', label:'PRAYER REQUESTS' },
{ to:'/calendar', label:'CALENDAR' },
{ to:'/beliefs', label:'OUR BELIEFS' },
]
export default function SidebarNav() {
return (
<div className="bg-yellow-100 p-4 min-h-screen w-64">
<nav className="space-y-2">
{navItems.map(item => (
<NavLink
key={item.to}
to={item.to}
className={({isActive}) => `
block w-full text-left px-4 py-3 text-sm font-bold uppercase
border border-gray-300 bg-gray-200 hover:bg-gray-300
${isActive ? 'bg-gray-400' : ''}
`}
>
{item.label}
</NavLink>
))}
</nav>
<div className="mt-6">
<button className="w-full px-4 py-2 text-sm font-medium text-red-600 bg-yellow-100 border border-red-600 rounded-full hover:bg-red-50">
Subscribe to our Newsletter &gt;&gt;
</button>
</div>
<div className="mt-4">
<a href="#" className="text-blue-600 underline text-sm">Message Center</a>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { track, events as ga } from '../utils/analytics'
export default function StaticMap() {
return (
<section aria-labelledby="mapTitle">
<h2 id="mapTitle" className="sr-only">Location Map</h2>
<div className="rounded-xl shadow-card bg-surface p-8 max-w-xl">
<iframe
title="Google Map"
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
className="w-full h-[320px] rounded-lg"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3474.1234567890123!2d-97.3964!3d27.8006!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x8668516b6c8c8c8c%3A0x8c8c8c8c8c8c8c8c!2s2710%20Violet%20Rd%2C%20Corpus%20Christi%2C%20TX%2078410!5e0!3m2!1sen!2sus!4v1234567890123"
/>
<div className="mt-8">
<a
className="btn-outline"
href="https://maps.google.com/?q=2710+Violet+Rd,+Corpus+Christi,+TX+78410"
target="_blank"
rel="noopener noreferrer"
onClick={() => track(ga.CTA_CLICK, { label: 'open_google_maps' })}
data-cta="open_google_maps"
>
Open in Google Maps
</a>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,240 @@
import React, { useState } from 'react'
import { track, events as ga } from '../utils/analytics'
import { sendVisitRequest } from '../utils/email'
export default function VisitForm() {
const [errors, setErrors] = useState({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitStatus, setSubmitStatus] = useState(null)
const showError = (field, show) => {
setErrors(prev => ({
...prev,
[field]: show
}))
}
const handleSubmit = async (e) => {
e.preventDefault()
setIsSubmitting(true)
setSubmitStatus(null)
const form = e.target
let isValid = true
// Validate name
if (!form.name.value.trim()) {
showError('name', true)
isValid = false
} else {
showError('name', false)
}
// Validate email
if (!form.email.validity.valid) {
showError('email', true)
isValid = false
} else {
showError('email', false)
}
// Validate message
if (!form.message.value.trim()) {
showError('message', true)
isValid = false
} else {
showError('message', false)
}
if (!isValid) {
setIsSubmitting(false)
return false
}
// Track form submission
track(ga.FORM_SUBMIT, { form_id: 'visit_home' })
// Prepare form data
const formData = {
name: form.name.value.trim(),
email: form.email.value.trim(),
date: form.date.value || null,
message: form.message.value.trim(),
consent: form.consent.checked
}
try {
// Send email
const result = await sendVisitRequest(formData)
if (result.success) {
setSubmitStatus('success')
form.reset()
setErrors({})
// Show success toast
const toast = document.createElement('div')
toast.setAttribute('role', 'status')
toast.className = 'fixed bottom-6 right-6 bg-green-600 text-white px-4 py-3 rounded-lg shadow z-50'
toast.textContent = 'Thanks! We\'ve received your request and will email you soon.'
document.body.appendChild(toast)
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast)
}
}, 5000)
} else {
setSubmitStatus('error')
throw new Error(result.message)
}
} catch (error) {
console.error('Form submission error:', error)
setSubmitStatus('error')
// Show error toast
const toast = document.createElement('div')
toast.setAttribute('role', 'status')
toast.className = 'fixed bottom-6 right-6 bg-red-600 text-white px-4 py-3 rounded-lg shadow z-50'
toast.textContent = 'Sorry, there was an error. Please try again or contact us directly.'
document.body.appendChild(toast)
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast)
}
}, 5000)
} finally {
setIsSubmitting(false)
}
return false
}
const handleFocus = () => {
// Track form start (only once)
if (!window.formStartTracked) {
track(ga.FORM_START, { form_id: 'visit_home' })
window.formStartTracked = true
}
}
return (
<form
id="visit-form"
noValidate
onSubmit={handleSubmit}
aria-describedby="visit-help"
onFocus={handleFocus}
className="space-y-8"
>
<div>
<label htmlFor="name" className="block text-sm font-medium text-ink mb-3">
Your Name *
</label>
<input
id="name"
name="name"
type="text"
required
aria-describedby="name-err"
className={`w-full border rounded-lg px-6 py-4 focus:ring-2 focus:ring-focus focus:border-transparent ${
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Your Name"
/>
{errors.name && (
<p id="name-err" className="text-red-600 text-sm mt-3">
Please enter your name.
</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-ink mb-3">
Your Email *
</label>
<input
id="email"
type="email"
name="email"
required
aria-describedby="email-err"
className={`w-full border rounded-lg px-6 py-4 focus:ring-2 focus:ring-focus focus:border-transparent ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Your Email"
/>
{errors.email && (
<p id="email-err" className="text-red-600 text-sm mt-3">
Please enter a valid email.
</p>
)}
</div>
<div>
<label htmlFor="date" className="block text-sm font-medium text-ink mb-3">
Visit Date (optional)
</label>
<input
id="date"
type="date"
name="date"
className="w-full border border-gray-300 rounded-lg px-6 py-4 focus:ring-2 focus:ring-focus focus:border-transparent"
/>
</div>
<div>
<label htmlFor="msg" className="block text-sm font-medium text-ink mb-3">
Your Message *
</label>
<textarea
id="msg"
name="message"
required
aria-describedby="msg-err"
rows={6}
className={`w-full border rounded-lg px-6 py-4 focus:ring-2 focus:ring-focus focus:border-transparent ${
errors.message ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Your Message"
/>
{errors.message && (
<p id="msg-err" className="text-red-600 text-sm mt-3">
Please add a short message.
</p>
)}
</div>
<div className="flex items-start gap-4">
<input
id="consent"
type="checkbox"
name="consent"
className="mt-1 rounded focus:ring-2 focus:ring-focus"
/>
<label htmlFor="consent" className="text-sm text-ink">
I consent to be contacted about my visit.
</label>
</div>
<button
className="btn w-full"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? 'Sending...' : 'Submit'}
</button>
{submitStatus === 'error' && (
<p className="text-red-600 text-sm">
There was an error sending your request. Please try again or contact us directly.
</p>
)}
<p id="visit-help" className="text-sm text-muted">
We'll email directions and parking info within 24 hours.
</p>
</form>
)
}