Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions src/libs/carousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
// src/libs/carousel.ts

export interface InfiniteCarouselOptions {
carouselSelector?: string;
trackSelector?: string;
moveIndicatorSelector?: string;
minDesktopWidth?: number;
speedPxPerSecond?: number;
}

export function initInfiniteCarousel(options: InfiniteCarouselOptions = {}): void {
if (typeof window === "undefined" || typeof document === "undefined") return;

const {
carouselSelector = "#carousel",
trackSelector = "#slide",
moveIndicatorSelector = "#move",
minDesktopWidth = 768,
speedPxPerSecond = 120,
} = options;

const setup = () => {
const carousel = document.querySelector<HTMLElement>(carouselSelector);
const track = document.querySelector<HTMLElement>(trackSelector);
// On récupère l'indicateur (la main)
const moveIndicator = document.querySelector<HTMLElement>(moveIndicatorSelector);

if (!carousel || !track) return;

// -----------------------------
// 1) DUPLICATION (SANS LA MAIN)
// -----------------------------
const originalItems = Array.from(track.children) as HTMLElement[];
if (originalItems.length === 0) return;

// ICI EST LA CORRECTION :
// On filtre pour ne cloner QUE les produits, pas l'indicateur (#move).
// Cela évite d'avoir 3 mains superposées.
const itemsToClone = originalItems.filter((item) => item !== moveIndicator);

const beforeFrag = document.createDocumentFragment();
const afterFrag = document.createDocumentFragment();

itemsToClone.forEach((item) => {
beforeFrag.appendChild(item.cloneNode(true));
afterFrag.appendChild(item.cloneNode(true));
});

track.insertBefore(beforeFrag, track.firstChild);
track.appendChild(afterFrag);

// -----------------------------
// 2) INITIALISATION POSITIONS
// -----------------------------
let segmentWidth = 0;
const computeSegmentWidth = () => {
// Le scrollWidth a changé après clonage, on recalcule
// Attention: itemsToClone ne contient pas tout le DOM actuel, donc on se base sur le track complet
// Mais pour segmentWidth, on veut la largeur d'un tiers (original)
// Approximation fiable : scrollWidth / 3
segmentWidth = track.scrollWidth / 3;
};

computeSegmentWidth();
carousel.scrollLeft = segmentWidth;

window.addEventListener("resize", () => {
const current = carousel.scrollLeft;
computeSegmentWidth();
if (segmentWidth > 0) {
const offset = current % segmentWidth;
carousel.scrollLeft = segmentWidth + offset;
}
});

// -----------------------------
// 3) GESTION DU HINT (Main)
// -----------------------------
let hintShown = false;

const hideHintOnce = () => {
if (hintShown || !moveIndicator) return;
hintShown = true;

// On attend 1 seconde
setTimeout(() => {
// 1. Transition CSS forcée via JS pour passer outre les classes
moveIndicator.style.transition = "opacity 0.5s ease";
moveIndicator.style.opacity = "0";

// 2. Suppression totale du flux
setTimeout(() => {
moveIndicator.style.display = "none";
}, 500);
}, 1000);
};

// -----------------------------
// 4) NAVIGATION & DRAG
// -----------------------------
let isDown = false;
let startX = 0;
let startScrollLeft = 0;
let isDragActive = false;
let longPressTimer: ReturnType<typeof setTimeout>;

const DRAG_THRESHOLD = 5;
const LONG_PRESS_DURATION = 200;

// Désactive le drag natif
carousel.querySelectorAll("img").forEach(img => img.draggable = false);
carousel.querySelectorAll("a").forEach(a => a.draggable = false);

const activateDragMode = (pointerId: number) => {
if (isDragActive) return;
isDragActive = true;
carousel.setPointerCapture(pointerId);
carousel.style.cursor = "grabbing";
};

// Capture du click pour empêcher l'ouverture du lien SI drag
carousel.addEventListener("click", (e) => {
if (isDragActive) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
}, { capture: true }
);

carousel.addEventListener("pointerdown", (e) => {
// Dès qu'on touche, on cache le hint (s'il est unique, ça marchera)
hideHintOnce();

isDown = true;
isDragActive = false;
startX = e.clientX;
startScrollLeft = carousel.scrollLeft;

longPressTimer = setTimeout(() => {
if (isDown) {
activateDragMode(e.pointerId);
stopAutoplay();
}
}, LONG_PRESS_DURATION);
});

carousel.addEventListener("pointermove", (e) => {
if (!isDown) return;

const x = e.clientX;
const walk = x - startX;

if (!isDragActive && Math.abs(walk) > DRAG_THRESHOLD) {
activateDragMode(e.pointerId);
stopAutoplay();
}

if (isDragActive) {
e.preventDefault();
carousel.scrollLeft = startScrollLeft - walk;
ensureInfiniteRange();
}
});

const endInteraction = (e: PointerEvent) => {
if (!isDown) return;

clearTimeout(longPressTimer);
isDown = false;

if (isDragActive) {
carousel.style.cursor = "grab";
try { carousel.releasePointerCapture(e.pointerId); } catch (err) {}

setTimeout(() => {
isDragActive = false;
updateAutoplayState();
}, 50);
} else {
updateAutoplayState();
}
};

carousel.addEventListener("pointerup", endInteraction);
carousel.addEventListener("pointercancel", endInteraction);

// -----------------------------
// 5) RECENTRAGE & AUTOPLAY
// -----------------------------
const ensureInfiniteRange = () => {
if (segmentWidth <= 0) return;
const current = carousel.scrollLeft;
const min = segmentWidth * 0.5;
const max = segmentWidth * 1.5;

if (current < min) {
carousel.scrollLeft = current + segmentWidth;
} else if (current > max) {
carousel.scrollLeft = current - segmentWidth;
}
};

let autoRunning = false;
let lastTimestamp: number | null = null;
let isHover = false;
const mediaQuery = window.matchMedia(`(min-width: ${minDesktopWidth}px)`);
let isDesktop = mediaQuery.matches;

mediaQuery.addEventListener("change", (e) => {
isDesktop = e.matches;
updateAutoplayState();
});

const autoStep = (timestamp: number) => {
if (!autoRunning) {
lastTimestamp = null;
return;
}
if (lastTimestamp === null) lastTimestamp = timestamp;
const deltaMs = timestamp - lastTimestamp;
lastTimestamp = timestamp;
const deltaPx = (speedPxPerSecond * deltaMs) / 1000;
carousel.scrollLeft += deltaPx;
ensureInfiniteRange();
requestAnimationFrame(autoStep);
};

const startAutoplay = () => {
if (!isDesktop || autoRunning) return;
autoRunning = true;
requestAnimationFrame(autoStep);
};

const stopAutoplay = () => {
autoRunning = false;
};

const updateAutoplayState = () => {
if (isDesktop && !isDown && !isHover) {
startAutoplay();
} else {
stopAutoplay();
}
};

carousel.addEventListener("mouseenter", () => {
isHover = true;
updateAutoplayState();
});
carousel.addEventListener("mouseleave", () => {
isHover = false;
updateAutoplayState();
});

updateAutoplayState();
};

if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", setup, { once: true });
} else {
setup();
}
}
80 changes: 26 additions & 54 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import GlobalSearch from 'components/global/GlobalSearch.astro';
<h1 class="font-semibold text-2xl w-2/3 lg:w-full lg:text-4xl lg:pr-8 lg:pb-6">
Bienvenue sur le site dédié aux passionnés de moto !
</h1>
<h2 class="lg:text-lg pl-2 w-full pr-4 lg:pr-0 text-xs gap-2 flex flex-col">
<h2 class="lg:text-lg w-full pr-4 lg:pr-0 text-xs gap-2 flex flex-col">
Découvrez une vaste collection de pièces, allant des classiques aux plus rares, soigneusement répertoriées pour répondre à tous vos besoins.
<span>Notre plateforme vous propose une gamme variée de tarifs, garantissant des options pour chaque budget.</span>
<span>En plus de vous offrir un accès facile à ces trésors mécaniques, je partage également mes conseils d'expert pour vous guider dans vos choix.</span>
Expand All @@ -31,41 +31,26 @@ import GlobalSearch from 'components/global/GlobalSearch.astro';
class="h-[37rem] flex items-center overflow-hidden lg:overflow-x-auto lg:overflow-y-hidden lg:select-none lg:cursor-grab lg:touch-pan-y"
>
<div id="slide" class="slide-track hover:[animation-play-state:paused] flex">
{generateProductData(config).map(product => (
<div class="mx-4">
<ProductCard
title={product.title}
logo={`/images/${product.logo}`}
alt={product.alt}
description={product.description}
typeMotor={product.typeMotor}
typeMotor1={product.typeMotor1}
price={product.price}
imageSrc={product.imageSrc}
lien={product.link}
/>
{generateProductData(config).map(product => (
<div class="mx-4">
<ProductCard
title={product.title}
logo={`/images/${product.logo}`}
alt={product.alt}
description={product.description}
typeMotor={product.typeMotor}
typeMotor1={product.typeMotor1}
price={product.price}
imageSrc={product.imageSrc}
lien={product.link}
/>
</div>
))}
<div id="move" class="flex justify-center opacity-75 items-end absolute ml-32 h-2/5 z-50 md:hidden">
<img src="/images/slide.png" class="w-10 h-10" alt="">
</div>
))}
{generateProductData(config).map(product => (
<div class="hidden lg:block mx-4">
<ProductCard
title={product.title}
logo={`/images/${product.logo}`}
alt={product.alt}
description={product.description}
typeMotor={product.typeMotor}
typeMotor1={product.typeMotor1}
price={product.price}
imageSrc={product.imageSrc}
lien={product.link}
/>
</div>
))}
<div id="move" class="flex justify-center opacity-75 items-end absolute ml-32 h-2/5 z-50 md:hidden">
<img src="/images/slide.png" class="w-10 h-10" alt="">
</div>
</div>
</div>
</div>
<div class="w-full flex justify-center mb-20 mt-16 lg:mt-0">
<div class="w-11/12 lg:w-4/5 rounded-2xl bg flex items-center pb-4 justify-center lg:py-0 lg:pr-20 flex-col lg:flex-row">
Expand All @@ -91,28 +76,15 @@ import GlobalSearch from 'components/global/GlobalSearch.astro';
</main>
</MainLayout>
<script>
import { initInfiniteCarousel } from "libs/carousel";

initInfiniteCarousel({
speedPxPerSecond: 130,
minDesktopWidth: 768,
});

document.querySelectorAll('img').forEach(img => {
img.loading = 'lazy';
img.decoding = 'async';
});
document.addEventListener('DOMContentLoaded', function() {
const slider: any = document.getElementById('slide');
const moveIndicator: any = document.getElementById('move');

let isSliding = false;

slider.addEventListener('touchstart', handleUserInteraction);
slider.addEventListener('mousedown', handleUserInteraction);
slider.addEventListener('scroll', handleUserInteraction);

function handleUserInteraction() {
if (!isSliding) {
isSliding = true;

setTimeout(function() {
moveIndicator.classList.add('hidden');
}, 1000);
}
}
});
</script>
</script>
Loading