mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 18:58:10 +00:00
Compare commits
4 Commits
33008c17f9
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 26e3d0975f | |||
| 4e6d0a5512 | |||
| f0c68f549d | |||
| 2fade93bf7 |
@ -1,42 +1,87 @@
|
|||||||
// 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; // 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);
|
||||||
|
|
||||||
|
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 (
|
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 +90,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 +116,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 && (
|
||||||
@ -103,6 +158,8 @@ const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default ClientLogosSection;
|
||||||
|
|
||||||
export const defaultClients: Client[] = [
|
export const defaultClients: Client[] = [
|
||||||
{ name: "ABSA", logoUrl: "/images/absa.png" },
|
{ name: "ABSA", logoUrl: "/images/absa.png" },
|
||||||
{ name: "SYBRIN", logoUrl: "/images/sybrin.svg" },
|
{ name: "SYBRIN", logoUrl: "/images/sybrin.svg" },
|
||||||
@ -113,4 +170,4 @@ export const defaultClients: Client[] = [
|
|||||||
{ name: "TOYOTA", logoUrl: "/images/toyota-logo.png" },
|
{ name: "TOYOTA", logoUrl: "/images/toyota-logo.png" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default ClientLogosSection;
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user