diff --git a/src/app/globals.scss b/src/app/globals.scss index 66b5371..60e2a0c 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -21,7 +21,7 @@ --secondary-muted: 242 76% 90%; --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 225 100% 98%; + --muted: 207 28% 92%; --dimmed: 246 10% 37%; --muted-foreground: 215.4 16.3% 46.9%; diff --git a/src/app/services/_utils/marquee-wrapper.tsx b/src/app/services/_utils/marquee-wrapper.tsx new file mode 100644 index 0000000..ad0c90e --- /dev/null +++ b/src/app/services/_utils/marquee-wrapper.tsx @@ -0,0 +1,90 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Marquee from "@/components/ui/marquee"; + +interface MarqueeWrapperProps { + children: React.ReactNode; + duration?: string; + className?: string; + itemWidth?: number; + gapWidth?: number; +} + +const CARD_WIDTH = 300; // Default width of each card in pixels +const GAP_WIDTH = 16; // Default gap between cards in pixels +const BASE_SPEED = 80; // Much faster base speed +const SLOW_SPEED = 60; // Faster speed for more items +const MIN_DURATION = 15; // Shorter minimum duration +const MAX_DURATION = 30; // Shorter maximum duration + +const MarqueeWrapper = ({ + children, + duration: propDuration, + className = "", + itemWidth = CARD_WIDTH, + gapWidth = GAP_WIDTH +}: MarqueeWrapperProps) => { + // State to track if the marquee is hovered + const [isHovered, setIsHovered] = useState(false); + // State to check if the component is rendered on the client + const [isClient, setIsClient] = useState(false); + + // Effect to set isClient to true after the component mounts + useEffect(() => { + setIsClient(true); + }, []); + + // Convert children to an array for easier manipulation + const childrenArray = React.Children.toArray(children); + const itemCount = childrenArray.length; + + // Create duplicates for infinite scrolling + const infiniteChildren = [ + ...childrenArray.slice(0, 2), // Add first two items for seamless effect + ...childrenArray, + ...childrenArray.slice(0, 2) // Add first two items again at the end + ]; + + // Function to calculate the duration of the marquee animation + const calculateDuration = () => { + if (!isClient) return "0s"; // Return 0s if not on client + + // Calculate total width of all items + const totalWidth = infiniteChildren.length * (itemWidth + gapWidth); + // Determine speed factor based on item count + const speedFactor = itemCount > 5 ? SLOW_SPEED : BASE_SPEED; + // Calculate base duration based on total width and speed + const baseDuration = totalWidth / speedFactor; + // Ensure duration is within defined min and max limits + const duration = Math.min(MAX_DURATION, Math.max(MIN_DURATION, baseDuration)); + + return `${duration}s`; // Return calculated duration + }; + + // Use propDuration if provided, otherwise calculate it + const calculatedDuration = propDuration || calculateDuration(); + + // Style object for the marquee animation + const style = { + '--duration': calculatedDuration, + animationPlayState: isHovered ? 'paused' : 'running' // Pause on hover + } as React.CSSProperties; + + return ( +
+
setIsHovered(true)} // Set hover state on mouse enter + onMouseLeave={() => setIsHovered(false)} // Reset hover state on mouse leave + > +
+ {infiniteChildren} +
+
+
+ ); +}; + +export default MarqueeWrapper; diff --git a/src/app/services/_utils/testimonials.tsx b/src/app/services/_utils/testimonials.tsx index ce5bf47..60f250c 100644 --- a/src/app/services/_utils/testimonials.tsx +++ b/src/app/services/_utils/testimonials.tsx @@ -1,25 +1,14 @@ "use client"; -import { Swiper, SwiperSlide } from "swiper/react"; -import "swiper/css"; -import "swiper/css/grid"; -import "swiper/css/pagination"; - -import { - Autoplay, - Keyboard, - Mousewheel, - Navigation, - Pagination, -} from "swiper/modules"; import ServicesCTA from "@/components/molecule/services-cta"; import Tagline from "@/components/molecule/tagline"; import { Sparkle } from "lucide-react"; import ANIM__FadeInOutOnScroll from "@/components/anims/fadein.anim"; import Image from "next/image"; import { usePathname } from "next/navigation"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import clsx from "clsx"; +import MarqueeWrapper from "./marquee-wrapper"; const Testimonials = ({ data }: { data: any }) => { const pathname = usePathname(); @@ -35,6 +24,15 @@ const Testimonials = ({ data }: { data: any }) => { customwebdevelopment: "customwebdev", uiux: "uiux", }; + + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + const filteredData = data?.filter((item: any) => item.text?.length < 100) || []; + return (
@@ -48,89 +46,49 @@ const Testimonials = ({ data }: { data: any }) => {
-
-
-
- - - {data?.length - ? data?.filter((item: any) => item.text?.length < 100)?.map((item: any) => { - return ( - - - - ); - }) - : null} - - -
-
- Get Started Right Away, - link: `/joining?type=${path[materedPath]}`, - }, - secondary: { text: <>Get A Free Consultation, link: "/" }, - }} - /> -
+
+
+ {isClient && ( + + {filteredData?.slice(0, 10)?.map((item: any) => ( + + ))} + + )} +
+
+
+
+ Get Started Right Away, + link: `/joining?type=${path[materedPath]}`, + }, + secondary: { text: <>Get A Free Consultation, link: "/" }, + }} + />
); }; -export default Testimonials; - -export const TestimonialCard = ({ - details, -}: { - details: { - _id: number; - name: string; - rating?: number; - category: string; - text: string; - avatar: string; - company: string; - country: string; - image?: string; - date?: string; - }; +export const TestimonialCard = ({ + details, + className = "" +}: { + details: any; + className?: string; }) => { const { _id, @@ -148,58 +106,59 @@ export const TestimonialCard = ({ return Array.from({ length: n }, (_, i) => i + 1); }; const [moreText, setMoreText] = useState(false); - + const letterCount = 100; return ( -
+
{createArray(rating || 1).map((item: number) => { - return ; + return ( + + ); })}
-

- {`"${ - text?.length > letterCount - ? moreText - ? text - : text?.slice(0, letterCount) + "..." - : text - }"`} - {text?.length > letterCount ? ( +

+ {`"${text}"`} + {text?.length > letterCount && ( setMoreText(!moreText)} - className="text-secondary text-xs" + className="text-secondary text-xs ml-1" > {moreText ? "See less" : "See more"} - ) : null} + )}

-

{name}

-

{country}

+

{name}

+

{country}

- + {image && ( + + )}
); }; + +export default Testimonials; diff --git a/src/components/assets/brandlogo.tsx b/src/components/assets/brandlogo.tsx index 4dfbd6f..a29aae8 100644 --- a/src/components/assets/brandlogo.tsx +++ b/src/components/assets/brandlogo.tsx @@ -1,4 +1,4 @@ -const BrandLogo = () => { +const BrandLogo = ({dark = false}: {dark?: boolean}) => { return ( { > { return ( -
+
- +

Business Center 1, M Floor, Nad Al Sheba, Dubai, U.A.E @@ -54,7 +54,7 @@ const Footer = () => { key={item.id} className=" flex flex-col items-start flex-start small-gap md:flex-start" > -

+

{item.title}

{item.links.map((link) => { diff --git a/src/components/ui/marquee.tsx b/src/components/ui/marquee.tsx new file mode 100644 index 0000000..e626441 --- /dev/null +++ b/src/components/ui/marquee.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +interface MarqueeProps extends React.HTMLAttributes { + pauseOnHover?: boolean; +} + +const Marquee = React.forwardRef( + ({ children, className, ...props }, ref) => { + return ( +
+
+ {children} +
+
+ ); + } +); + +Marquee.displayName = "Marquee"; + +export default Marquee; diff --git a/tailwind.config.ts b/tailwind.config.ts index 8e465af..399297a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -12,115 +12,127 @@ module.exports = { "./src/**/*.{ts,tsx}", ], theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - secondarymuted: { - DEFAULT: "hsl(var(--secondary-muted))", - }, - dimmed: { - DEFAULT: "hsl(var(--dimmed))", - }, - decade: { - DEFAULT: "hsl(var(--decade))", - }, - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - "spin-around": { - "0%": { - transform: "translateZ(0) rotate(0)", - }, - "15%, 35%": { - transform: "translateZ(0) rotate(90deg)", - }, - "65%, 85%": { - transform: "translateZ(0) rotate(270deg)", - }, - "100%": { - transform: "translateZ(0) rotate(360deg)", - }, - }, - slide: { - to: { - transform: "translate(calc(100cqw - 100%), 0)", - }, - }, - aurora: { - from: { - backgroundPosition: "50% 50%, 50% 50%", - }, - to: { - backgroundPosition: "350% 50%, 350% 50%", - }, - }, - scroll: { - to: { - transform: "translate(calc(-50% - 0.5rem))", - }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - "spin-around": "spin-around calc(var(--speed) * 2) infinite linear", - slide: "slide var(--speed) ease-in-out infinite alternate", - aurora: "aurora 60s linear infinite", - scroll: - "scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite", - }, - }, + container: { + center: 'true', + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + secondarymuted: { + DEFAULT: 'hsl(var(--secondary-muted))' + }, + dimmed: { + DEFAULT: 'hsl(var(--dimmed))' + }, + decade: { + DEFAULT: 'hsl(var(--decade))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + }, + 'spin-around': { + '0%': { + transform: 'translateZ(0) rotate(0)' + }, + '15%, 35%': { + transform: 'translateZ(0) rotate(90deg)' + }, + '65%, 85%': { + transform: 'translateZ(0) rotate(270deg)' + }, + '100%': { + transform: 'translateZ(0) rotate(360deg)' + } + }, + slide: { + to: { + transform: 'translate(calc(100cqw - 100%), 0)' + } + }, + aurora: { + from: { + backgroundPosition: '50% 50%, 50% 50%' + }, + to: { + backgroundPosition: '350% 50%, 350% 50%' + } + }, + scroll: { + to: { + transform: 'translate(calc(-50% - 0.5rem))' + } + }, + marquee: { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(-50%)' } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear', + slide: 'slide var(--speed) ease-in-out infinite alternate', + aurora: 'aurora 60s linear infinite', + scroll: 'scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite', + marquee: 'marquee var(--duration, 20s) linear infinite' + } + } }, plugins: [require("tailwindcss-animate"), addVariablesForColors], };