Files
fund/components/featured-projects.tsx
zeekayandHanzo Dev 812b8cb778 State a person can perceive, and motion they can stop
Which DAO tab was open was signalled by a colour and an underline and
nothing else. A screen reader read five identical buttons and no current
one. The tabs carry aria-selected now, and role ties each to the panel it
opens.

The homepage carousel advanced every five seconds forever and paused only
while a mouse rested on it — no help to anyone using a keyboard, a screen
reader or a phone. Content that moves on its own for longer than five
seconds needs a stop anyone can reach, so there is a Pause control, and a
system set to prefer reduced motion now stops it without being asked.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 07:50:20 -07:00

184 lines
6.4 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { ProjectCard, ProjectCardProps } from './project-card'
export interface FeaturedProjectsProps {
projects: ProjectCardProps[]
autoRotate?: boolean
rotateInterval?: number
showDots?: boolean
}
export function FeaturedProjects({
projects,
autoRotate = true,
rotateInterval = 5000,
showDots = true
}: FeaturedProjectsProps) {
const [currentIndex, setCurrentIndex] = useState(0)
const [isHovering, setIsHovering] = useState(false)
// Rotation stopped only while a mouse sat on it, which is no help to anyone
// navigating by keyboard or touch — and content that moves on its own for
// longer than five seconds needs a way to stop it that everyone can reach.
const [paused, setPaused] = useState(false)
// Someone who has asked their system for less motion has asked for this too.
useEffect(() => {
const quiet = window.matchMedia?.('(prefers-reduced-motion: reduce)')
if (!quiet) return
const apply = () => setPaused(quiet.matches)
apply()
quiet.addEventListener?.('change', apply)
return () => quiet.removeEventListener?.('change', apply)
}, [])
// Auto-rotate carousel
useEffect(() => {
if (!autoRotate || isHovering || paused || projects.length <= 1) return
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % projects.length)
}, rotateInterval)
return () => clearInterval(interval)
}, [autoRotate, isHovering, paused, projects.length, rotateInterval])
const goToSlide = (index: number) => {
setCurrentIndex(index)
}
const goToPrevious = () => {
setCurrentIndex((prev) => (prev - 1 + projects.length) % projects.length)
}
const goToNext = () => {
setCurrentIndex((prev) => (prev + 1) % projects.length)
}
if (projects.length === 0) return null
// Get visible projects (current + next 2 on desktop)
const visibleProjects = [
projects[currentIndex],
projects[(currentIndex + 1) % projects.length],
projects[(currentIndex + 2) % projects.length]
]
return (
<div
className="relative"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{/* Main Carousel */}
<div className="relative overflow-hidden rounded-2xl">
{/* Desktop: 3 cards side-by-side */}
<div className="hidden lg:grid lg:grid-cols-3 gap-6">
{visibleProjects.map((project, idx) => (
<div
key={`${project.id}-${idx}`}
className="transition-opacity duration-500"
style={{ opacity: idx === 0 ? 1 : 0.6 }}
>
<ProjectCard {...project} featured={idx === 0} />
</div>
))}
</div>
{/* Tablet: 2 cards */}
<div className="hidden md:grid md:grid-cols-2 lg:hidden gap-6">
{visibleProjects.slice(0, 2).map((project, idx) => (
<div
key={`${project.id}-tablet-${idx}`}
className="transition-opacity duration-500"
style={{ opacity: idx === 0 ? 1 : 0.6 }}
>
<ProjectCard {...project} featured={idx === 0} />
</div>
))}
</div>
{/* Mobile: 1 card */}
<div className="md:hidden">
<ProjectCard {...projects[currentIndex]} featured />
</div>
{/* Navigation Arrows */}
{projects.length > 1 && (
<>
<button
onClick={() => setPaused((p) => !p)}
aria-pressed={paused}
className="p-2 rounded-full bg-white/5 border border-white/10 hover:bg-white/10 transition-colors text-white/70"
>
{paused ? 'Play' : 'Pause'}
</button>
<button
onClick={goToPrevious}
className="absolute left-2 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/60 backdrop-blur-sm border border-white/20 rounded-full flex items-center justify-center text-white hover:bg-black/80 hover:scale-110 transition-all z-10"
aria-label="Previous project"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
onClick={goToNext}
className="absolute right-2 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/60 backdrop-blur-sm border border-white/20 rounded-full flex items-center justify-center text-white hover:bg-black/80 hover:scale-110 transition-all z-10"
aria-label="Next project"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</>
)}
</div>
{/* Dots Indicator */}
{showDots && projects.length > 1 && (
<div className="flex items-center justify-center gap-2 mt-6">
{projects.map((_, index) => (
<button
key={index}
onClick={() => goToSlide(index)}
className={`h-2 rounded-full transition-all ${
index === currentIndex
? 'w-8 bg-[#667eea]'
: 'w-2 bg-white/20 hover:bg-white/40'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
)}
{/* Auto-rotate indicator */}
{autoRotate && !isHovering && projects.length > 1 && (
<div className="text-center mt-2">
<span className="text-xs text-white/40">Auto-rotating every {rotateInterval / 1000}s</span>
</div>
)}
</div>
)
}
// Simple featured section with title
export function FeaturedProjectsSection({ projects }: { projects: ProjectCardProps[] }) {
return (
<section className="py-8 sm:py-12 lg:py-15 border-b border-white/10">
<div className="container">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-3xl sm:text-4xl font-bold mb-2">Featured Projects</h2>
<p className="text-white/60">Highlighted conservation initiatives making real impact</p>
</div>
</div>
<FeaturedProjects projects={projects} />
</div>
</section>
)
}