feat: add homepage image carousel with controls
This commit is contained in:
parent
ac022933c8
commit
c4541f461a
4 changed files with 136 additions and 0 deletions
|
|
@ -1,7 +1,10 @@
|
|||
import { ImageCarousel } from "@/components/layout/ImageCarousel";
|
||||
|
||||
export default function EnHome() {
|
||||
return (
|
||||
<section className="hero" aria-labelledby="home-title">
|
||||
<div className="container">
|
||||
<ImageCarousel />
|
||||
<h1 id="home-title" className="title">Portuguese Powerlifting Association</h1>
|
||||
<p className="subtitle">Accessible. Transparent. For all athletes.</p>
|
||||
<div className="mt-4 flex gap-2">
|
||||
|
|
|
|||
|
|
@ -152,6 +152,72 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); }
|
|||
margin-top: 0.5rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
/* Carousel */
|
||||
.carousel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.carousel-viewport {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
.carousel-slide {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
.carousel-slide.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.carousel-controls {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 100%);
|
||||
}
|
||||
.carousel-btn {
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.carousel-btn:hover,
|
||||
.carousel-btn:focus-visible {
|
||||
background: #fff;
|
||||
border-color: var(--color-green);
|
||||
}
|
||||
.carousel-dots {
|
||||
display: inline-flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.carousel-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #fff;
|
||||
background: rgba(255,255,255,0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.carousel-dot.active {
|
||||
background: #fff;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { ImageCarousel } from "@/components/layout/ImageCarousel";
|
||||
|
||||
export default function PtHome() {
|
||||
return (
|
||||
<section className="hero" aria-labelledby="home-title">
|
||||
<div className="container">
|
||||
<ImageCarousel />
|
||||
<h1 id="home-title" className="title">Associação Portuguesa de Powerlifting</h1>
|
||||
<p className="subtitle">Acessível. Transparente. Para todos os atletas.</p>
|
||||
<div className="mt-4 flex gap-2">
|
||||
|
|
|
|||
64
src/components/layout/ImageCarousel.tsx
Normal file
64
src/components/layout/ImageCarousel.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"use client";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const slides = [
|
||||
{ src: "/content/images/1.png", alt: "Powerlifting highlight 1" },
|
||||
{ src: "/content/images/2.png", alt: "Powerlifting highlight 2" },
|
||||
{ src: "/content/images/3.png", alt: "Powerlifting highlight 3" },
|
||||
];
|
||||
|
||||
export function ImageCarousel() {
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setIndex((prev) => (prev + 1) % slides.length);
|
||||
}, 5000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const goTo = (i: number) => setIndex(i % slides.length);
|
||||
const prev = () => setIndex((prev) => (prev - 1 + slides.length) % slides.length);
|
||||
const next = () => setIndex((prev) => (prev + 1) % slides.length);
|
||||
|
||||
return (
|
||||
<section aria-label="Destaques" className="carousel" role="region">
|
||||
<div className="carousel-viewport" aria-live="polite">
|
||||
{slides.map((slide, i) => (
|
||||
<div
|
||||
key={slide.src}
|
||||
className={`carousel-slide ${i === index ? "active" : ""}`}
|
||||
aria-hidden={i !== index ? "true" : "false"}
|
||||
>
|
||||
<Image
|
||||
src={slide.src}
|
||||
alt={slide.alt}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 1200px"
|
||||
priority={i === 0}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="carousel-controls">
|
||||
<button type="button" onClick={prev} aria-label="Imagem anterior" className="carousel-btn">‹</button>
|
||||
<div className="carousel-dots" role="tablist" aria-label="Selecionar imagem do carrossel">
|
||||
{slides.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={i === index ? "true" : "false"}
|
||||
aria-label={`Ir para imagem ${i + 1}`}
|
||||
className={`carousel-dot ${i === index ? "active" : ""}`}
|
||||
onClick={() => goTo(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" onClick={next} aria-label="Próxima imagem" className="carousel-btn">›</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue