Merge pull request #33 from OwethuManagedServices/09-09-2025-/Please-add-a-scroller-onTrusted-By-Industry-Leaders

feat: enhance ClientLogosSection with scrolling functionality and con…
This commit is contained in:
Lebohang-OMSAfrica
2025-09-16 16:52:11 +02:00
committed by GitHub
2 changed files with 95 additions and 44 deletions

View File

@ -1,42 +1,85 @@
// components/ClientLogosSection.tsx "use client";
import React from "react";
import { useRef, useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
// Define structure for client data - focusing on logos now
type Client = { type Client = {
name: string; name: string;
logoUrl: string; // Expecting actual logo URLs now logoUrl: string;
}; };
type ClientLogosSectionProps = { type ClientLogosSectionProps = {
title: string; title: string;
description?: string; description?: string;
clients: Client[]; clients: Client[];
/** Control animation speed (lower number = faster). Default: 40s */ speed?: number; // pixels per frame
speed?: number;
/** Size of the square background container in pixels. Default: 120 */
squareSize?: number; squareSize?: number;
}; };
const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({ const ClientLogosSection = ({
title, title,
description, description,
clients = [], // Default to empty array clients = [],
speed = 40, //Default speed in seconds for one full cycle speed = 1,
squareSize = 120, // Default size for the square container (e.g., 120px) squareSize = 120,
}) => { }: ClientLogosSectionProps) => {
// Need at least one client to render the marquee if (clients.length === 0) return null;
if (clients.length === 0) {
return null; // Or render a placeholder message if preferred
}
// Duplicate the clients for a seamless loop effect
const extendedClients = [...clients, ...clients]; const extendedClients = [...clients, ...clients];
const squareDim = `${squareSize}px`;
const squareDim = `${squareSize}px`; // Convert size to string for inline style const padding = Math.round(squareSize / 6);
const padding = Math.round(squareSize / 6); // Calculate padding based on size (adjust divisor as needed)
const paddingDim = `${padding}px`; 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);
// Scroll manually
scrollRef.current.scrollBy({
left: dir === "left" ? -200 : 200,
behavior: "smooth",
});
// Clear previous timeout and resume after 3 seconds
clearTimeout(resumeTimeout);
resumeTimeout = setTimeout(() => {
setPaused(false);
}, 3000);
// Set the direction for automatic scroll after pause
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]);
return ( return (
<section className="py-16 md:py-20 bg-background overflow-hidden"> <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"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
@ -45,19 +88,17 @@ const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
</h2> </h2>
</div> </div>
{/* Marquee container - group allows pausing animation on hover */} {/* Logos Container */}
<div className="relative w-full overflow-hidden group"> <div className="relative w-full overflow-hidden">
{/* Inner container that will animate */}
<div <div
className="flex flex-nowrap animate-marquee-continuous" ref={scrollRef}
style={{ animationDuration: `${speed}s` }} className="flex flex-nowrap overflow-x-hidden scrollbar-hide"
> >
{extendedClients.map((client, index) => ( {extendedClients.map((client, index) => (
<div <div
key={`${client.name}-${index}`} key={`${client.name}-${index}`}
className="flex-shrink-0 mx-12 md:mx-16 py-4" className="flex-shrink-0 mx-12 md:mx-16 py-4"
> >
{/* Square Background Container */}
<div <div
title={client.name} title={client.name}
className=" className="
@ -73,23 +114,35 @@ const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
style={{ style={{
width: squareDim, width: squareDim,
height: squareDim, height: squareDim,
padding: paddingDim, // Add padding inside the square padding: paddingDim,
}} }}
> >
<Image <Image
src={client.logoUrl} src={client.logoUrl}
alt={`${client.name} Logo`} alt={`${client.name} Logo`}
layout="fill" // Let image fill the relative container fill
objectFit="contain" // Maintain aspect ratio within the container style={{ objectFit: "contain" }}
/> />
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div>
{/* Optional: Add fade effect at the edges */} {/* Arrow Controls */}
<div className="absolute inset-y-0 left-0 w-16 md:w-24 bg-gradient-to-r from-background to-transparent pointer-events-none z-10"></div> <div className="flex justify-center mt-8 space-x-6">
<div className="absolute inset-y-0 right-0 w-16 md:w-24 bg-gradient-to-l from-background to-transparent pointer-events-none z-10"></div> <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> </div>
{description && ( {description && (

View File

@ -208,18 +208,16 @@
} /* Added longer delay */ } /* Added longer delay */
} }
@keyframes fadeInUp {
from { @keyframes marquee {
opacity: 0; from { transform: translateX(0); }
transform: translateY(20px); to { transform: translateX(-50%); }
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.animate-fade-in-up { .animate-marquee-continuous {
animation: fadeInUp 0.8s ease-out forwards; animation: marquee linear infinite;
opacity: 0; /* Start hidden */ }
.paused {
animation-play-state: paused !important;
} }