Files
oms-website-nextjs/app/(website)/_components/ClientLogosSection.tsx
2025-09-17 10:44:00 +02:00

174 lines
4.8 KiB
TypeScript

"use client";
import { useRef, useEffect, useState } from "react";
import Image from "next/image";
type Client = {
name: string;
logoUrl: string;
};
type ClientLogosSectionProps = {
title: string;
description?: string;
clients: Client[];
speed?: number; // pixels per frame
squareSize?: number;
};
const ClientLogosSection = ({
title,
description,
clients = [],
speed = 1,
squareSize = 120,
}: ClientLogosSectionProps) => {
const extendedClients = [...clients, ...clients];
const squareDim = `${squareSize}px`;
const padding = Math.round(squareSize / 6);
const paddingDim = `${padding}px`;
const scrollRef = useRef<HTMLDivElement>(null);
const [direction, setDirection] = useState<"left" | "right">("left");
const [paused, setPaused] = useState(false);
let resumeTimeout: NodeJS.Timeout;
const pauseAndScroll = (dir: "left" | "right") => {
if (!scrollRef.current) return;
setPaused(true);
scrollRef.current.scrollBy({
left: dir === "left" ? -200 : 200,
behavior: "smooth",
});
clearTimeout(resumeTimeout);
resumeTimeout = setTimeout(() => {
setPaused(false);
}, 3000);
setDirection(dir === "left" ? "right" : "left");
};
useEffect(() => {
const container = scrollRef.current;
if (!container) return;
let animationFrame: number;
const step = () => {
if (!paused) {
if (direction === "left") {
container.scrollLeft += speed;
if (container.scrollLeft >= container.scrollWidth / 2) {
container.scrollLeft = 0;
}
} else {
container.scrollLeft -= speed;
if (container.scrollLeft <= 0) {
container.scrollLeft = container.scrollWidth / 2;
}
}
}
animationFrame = requestAnimationFrame(step);
};
animationFrame = requestAnimationFrame(step);
return () => cancelAnimationFrame(animationFrame);
}, [direction, paused, speed]);
// ✅ Safe early return after hooks
if (clients.length === 0) return null;
return (
<section className="py-16 md:py-20 bg-background overflow-hidden">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-foreground">
{title}
</h2>
</div>
{/* Logos Container */}
<div className="relative w-full overflow-hidden">
<div
ref={scrollRef}
className="flex flex-nowrap overflow-x-hidden scrollbar-hide"
>
{extendedClients.map((client, index) => (
<div
key={`${client.name}-${index}`}
className="flex-shrink-0 mx-12 md:mx-16 py-4"
>
<div
title={client.name}
className="
relative flex items-center justify-center
bg-muted/30 dark:bg-muted/20
rounded-lg shadow-sm
overflow-hidden
grayscale hover:grayscale-0
opacity-70 hover:opacity-100
transition-all duration-300 ease-in-out
cursor-pointer
"
style={{
width: squareDim,
height: squareDim,
padding: paddingDim,
}}
>
<Image
src={client.logoUrl}
alt={`${client.name} Logo`}
fill
style={{ objectFit: "contain" }}
/>
</div>
</div>
))}
</div>
</div>
{/* Arrow Controls */}
<div className="flex justify-center mt-8 space-x-6">
<button
onClick={() => pauseAndScroll("right")}
className="px-4 py-2 rounded-full bg-muted hover:bg-muted/70 transition"
>
</button>
<button
onClick={() => pauseAndScroll("left")}
className="px-4 py-2 rounded-full bg-muted hover:bg-muted/70 transition"
>
</button>
</div>
{description && (
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p className="mt-10 text-muted-foreground italic text-sm">
{description}
</p>
</div>
)}
</section>
);
};
export default ClientLogosSection;
export const defaultClients: Client[] = [
{ name: "ABSA", logoUrl: "/images/absa.png" },
{ name: "SYBRIN", logoUrl: "/images/sybrin.svg" },
{ name: "SASOL", logoUrl: "/images/sasol.png" },
{ name: "JACARANDA", logoUrl: "/images/jacaranda.png" },
{ name: "SALESFORCE", logoUrl: "/images/salesforce.png" },
{ name: "BCX", logoUrl: "/images/bcx.png" },
{ name: "TOYOTA", logoUrl: "/images/toyota-logo.png" },
];