improved site header, added contact form, content card carousel, footer, and more photos

This commit is contained in:
2026-05-28 21:20:47 +01:00
parent 6a579699e7
commit 74a908f07f
32 changed files with 1394 additions and 386 deletions

View File

@@ -0,0 +1,126 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import ContentCard from './ui/content-cards'
import './content-card-carousel.css'
interface ContentCardItem {
src?: string
button_link?: string
heading?: string
button_text?: string
card_text?: string
}
interface ContentCardCarouselProps {
cards: ContentCardItem[]
}
function getVisibleCardCount() {
if (typeof window === 'undefined') return 1
if (window.matchMedia('(min-width: 1024px)').matches) return 3
if (window.matchMedia('(min-width: 640px)').matches) return 2
return 1
}
export default function ContentCardCarousel({ cards }: ContentCardCarouselProps) {
const [visibleCards, setVisibleCards] = useState(1)
const [currentIndex, setCurrentIndex] = useState(0)
const syncVisibleCards = useCallback(() => {
setVisibleCards(getVisibleCardCount())
}, [])
useEffect(() => {
syncVisibleCards()
const mediaQueries = [
window.matchMedia('(min-width: 640px)'),
window.matchMedia('(min-width: 1024px)'),
]
const onBreakpointChange = () => syncVisibleCards()
mediaQueries.forEach((mq) => mq.addEventListener('change', onBreakpointChange))
window.addEventListener('resize', onBreakpointChange)
return () => {
mediaQueries.forEach((mq) => mq.removeEventListener('change', onBreakpointChange))
window.removeEventListener('resize', onBreakpointChange)
}
}, [syncVisibleCards])
const maxIndex = useMemo(() => Math.max(cards.length - visibleCards, 0), [cards.length, visibleCards])
useEffect(() => {
setCurrentIndex((previous) => Math.min(previous, maxIndex))
}, [maxIndex])
// % of track width — matches flex slide sizes (100% / 50% / 33.333% per breakpoint)
const trackOffsetPercent = cards.length > 0 ? (currentIndex * 100) / cards.length : 0
const next = () => {
if (maxIndex === 0) return
setCurrentIndex((previous) => (previous >= maxIndex ? 0 : previous + 1))
}
const previous = () => {
if (maxIndex === 0) return
setCurrentIndex((index) => (index <= 0 ? maxIndex : index - 1))
}
return (
<div className="w-full max-w-full pb-30">
<h1 className="mb-4 flex justify-center text-5xl font-bold">Our Case Studies</h1>
<div className="carousel-viewport relative overflow-hidden">
<div
className="carousel-track flex items-stretch transition-transform duration-500 ease-in-out"
style={{ transform: `translate3d(-${trackOffsetPercent}%, 0, 0)` }}
>
{cards.map((card, index) => {
const isEdgeCard =
visibleCards > 1 && (index === currentIndex || index === currentIndex + visibleCards - 1)
return (
<div
key={`${card.heading ?? 'card'}-${index}`}
className={`carousel-slide flex shrink-0 justify-center px-2 py-3 transition-opacity duration-500 ${
isEdgeCard ? 'opacity-75' : 'opacity-100'
}`}
>
<div className="flex h-full w-full max-w-sm">
<ContentCard {...card} />
</div>
</div>
)
})}
</div>
<div className="pointer-events-none absolute inset-y-0 left-0 w-12 bg-gradient-to-r from-white to-transparent dark:from-black" />
<div className="pointer-events-none absolute inset-y-0 right-0 w-12 bg-gradient-to-l from-white to-transparent dark:from-black" />
</div>
<div className="carousel-controls relative z-20 mt-6 flex justify-center gap-4">
<button
type="button"
onClick={previous}
disabled={maxIndex === 0}
className="carousel-control-btn"
aria-label="Show previous content cards"
>
&#8592;
</button>
<button
type="button"
onClick={next}
disabled={maxIndex === 0}
className="carousel-control-btn"
aria-label="Show next content cards"
>
&#8594;
</button>
</div>
</div>
)
}