mirror of
https://github.com/modelec/modelec.club.git
synced 2026-01-18 16:37:30 +01:00
Fixed and refactored carousel
This commit is contained in:
107
src/components/carousel/carousel.css
Normal file
107
src/components/carousel/carousel.css
Normal file
@@ -0,0 +1,107 @@
|
||||
.carousel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carousel-wrapper {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carousel-slider {
|
||||
display: flex;
|
||||
gap: 25px;
|
||||
position: relative;
|
||||
padding-block: 1rem;
|
||||
transition: translate 0.3s;
|
||||
}
|
||||
|
||||
.slide {
|
||||
background: var(--white);
|
||||
border-radius: 15px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.slide-image {
|
||||
display: block;
|
||||
color: var(--darkGray);
|
||||
font-size: 0.8rem;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
height: 15rem;
|
||||
object-fit: contain;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.slide-content {
|
||||
padding: 1rem;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
.slide-title {
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.slide-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--darkGray);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.carousel-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--gray);
|
||||
border: none;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
font-size: 1em;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.carousel-image.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.carousel-image.fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.carousel-buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.carousel-buttons button {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.carousel-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.carousel-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--gray);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: width .3s, background .3s;
|
||||
&.dot_active { width: 24px; }
|
||||
&:hover { background: var(--darkGray); }
|
||||
}
|
||||
@@ -1,84 +1,92 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { NavArrowLeft, NavArrowRight } from "iconoir-react";
|
||||
|
||||
import "./carousel.css";
|
||||
|
||||
interface CarouselProps {
|
||||
images: string[];
|
||||
carousel: CarouselImageProps[];
|
||||
}
|
||||
|
||||
interface CarouselClickablesProps {
|
||||
images: string[];
|
||||
links: string[];
|
||||
interface CarouselImageProps {
|
||||
image: string;
|
||||
title: string;
|
||||
text: string;
|
||||
link?: string;
|
||||
alt?: string;
|
||||
}
|
||||
|
||||
export const Carousel: React.FC<CarouselProps> = ({ images }) => {
|
||||
const [currentImage, setCurrentImage] = React.useState(0);
|
||||
export const Carousel: React.FC<CarouselProps> = ({ carousel }) => {
|
||||
const [currentIndex, setCurrentImage] = React.useState(0);
|
||||
const [imagesSize, setImagesSize] = React.useState<number[]>([]);
|
||||
const carouselSlider = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const nextImage = () => {
|
||||
setCurrentImage((currentImage + 1) % images.length);
|
||||
setCurrentImage((currentIndex + 1) % carousel.length);
|
||||
}
|
||||
|
||||
const previousImage = () => {
|
||||
setCurrentImage((currentImage - 1 + images.length) % images.length);
|
||||
setCurrentImage((currentIndex - 1 + carousel.length) % carousel.length);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const interval = setTimeout(() => {
|
||||
nextImage();
|
||||
}, 3000);
|
||||
|
||||
if (carouselSlider.current) {
|
||||
const translate = imagesSize.reduce((acc, size, index) => index < currentIndex ? acc + size : acc, 0);
|
||||
const gap = 25; // TODO: Find a way to get this value (HARDCODED)
|
||||
carouselSlider.current.style.translate = `-${translate+gap*currentIndex}px`;
|
||||
}
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [currentIndex, imagesSize, carouselSlider]);
|
||||
|
||||
function handleImageLoad(i: number, event: React.SyntheticEvent<HTMLImageElement>) {
|
||||
setImagesSize((old) => {
|
||||
const newSizes = [...old];
|
||||
newSizes[i] = (event.target as HTMLImageElement).clientWidth;
|
||||
return newSizes;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"carousel"}>
|
||||
<div className={"carousel-image"}>
|
||||
<img src={images[currentImage]} alt={"carousel"}/>
|
||||
<div className={"carousel-wrapper"}>
|
||||
<div className={"carousel-slider"} ref={carouselSlider}>
|
||||
{
|
||||
carousel.map((image, index) => (
|
||||
<div key={index} className={'slide'}>
|
||||
<img
|
||||
key={index}
|
||||
className={`slide-image ${index === currentIndex ? "active" : ""}`}
|
||||
src={image.image}
|
||||
alt={image.alt ?? "Image Carousel"}
|
||||
onLoad={(e) => handleImageLoad(index, e)}
|
||||
/>
|
||||
<div className={'slide-content'}>
|
||||
<h3 className={'slide-title'}>{image.title}</h3>
|
||||
<p className={'slide-text'}>{image.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"carousel-buttons"}>
|
||||
<button onClick={previousImage}>Previous</button>
|
||||
<button onClick={nextImage}>Next</button>
|
||||
<div className="carousel-bar">
|
||||
<button className={"carousel-button button_left"} onClick={previousImage}><NavArrowLeft /></button>
|
||||
{
|
||||
carousel.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`carousel-dot ${index === currentIndex ? "dot_active" : ""}`}
|
||||
onClick={() => setCurrentImage(index)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<button className={"carousel-button button_right"} onClick={nextImage}><NavArrowRight /></button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CarouselClickables: React.FC<CarouselClickablesProps> = ({ images, links }) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [fade, setFade] = useState("fade-in");
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
handleNextImage();
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [images.length]);
|
||||
|
||||
const handleNextImage = () => {
|
||||
setFade("fade-out");
|
||||
setTimeout(() => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % images.length);
|
||||
setFade("fade-in");
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handlePreviousImage = () => {
|
||||
setFade("fade-out");
|
||||
setTimeout(() => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex - 1 + images.length) % images.length);
|
||||
setFade("fade-in");
|
||||
}, 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="carousel-container">
|
||||
<div className="carousel-slide" style={{ transform: `translateX(-${currentIndex * 100}%)` }}>
|
||||
{images.map((image, index) => (
|
||||
<a key={index} href={links[index]} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
src={image}
|
||||
alt={`Slide ${index}`}
|
||||
className={`carousel-image ${fade}`}
|
||||
style={{ display: index === currentIndex ? 'block' : 'none' }}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="carousel-buttons">
|
||||
<button onClick={handlePreviousImage}>Previous</button>
|
||||
<button onClick={handleNextImage}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user