improved site header, added contact form, content card carousel, footer, and more photos
This commit is contained in:
126
components/content-card-carousel.tsx
Normal file
126
components/content-card-carousel.tsx
Normal 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"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={next}
|
||||
disabled={maxIndex === 0}
|
||||
className="carousel-control-btn"
|
||||
aria-label="Show next content cards"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user