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() {
|
export default function EnHome() {
|
||||||
return (
|
return (
|
||||||
<section className="hero" aria-labelledby="home-title">
|
<section className="hero" aria-labelledby="home-title">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
<ImageCarousel />
|
||||||
<h1 id="home-title" className="title">Portuguese Powerlifting Association</h1>
|
<h1 id="home-title" className="title">Portuguese Powerlifting Association</h1>
|
||||||
<p className="subtitle">Accessible. Transparent. For all athletes.</p>
|
<p className="subtitle">Accessible. Transparent. For all athletes.</p>
|
||||||
<div className="mt-4 flex gap-2">
|
<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;
|
margin-top: 0.5rem;
|
||||||
color: var(--color-muted);
|
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 {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
import { ImageCarousel } from "@/components/layout/ImageCarousel";
|
||||||
|
|
||||||
export default function PtHome() {
|
export default function PtHome() {
|
||||||
return (
|
return (
|
||||||
<section className="hero" aria-labelledby="home-title">
|
<section className="hero" aria-labelledby="home-title">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
<ImageCarousel />
|
||||||
<h1 id="home-title" className="title">Associação Portuguesa de Powerlifting</h1>
|
<h1 id="home-title" className="title">Associação Portuguesa de Powerlifting</h1>
|
||||||
<p className="subtitle">Acessível. Transparente. Para todos os atletas.</p>
|
<p className="subtitle">Acessível. Transparente. Para todos os atletas.</p>
|
||||||
<div className="mt-4 flex gap-2">
|
<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