Skip to content

Commit

Permalink
Merge branch 'crescents-stack:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
musiur authored Dec 5, 2024
2 parents 42359c1 + 0731a08 commit 63ad56c
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 236 deletions.
2 changes: 1 addition & 1 deletion src/app/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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%;

Expand Down
90 changes: 90 additions & 0 deletions src/app/services/_utils/marquee-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative w-full overflow-hidden">
<div
className="animate-marquee inline-flex"
style={style}
onMouseEnter={() => setIsHovered(true)} // Set hover state on mouse enter
onMouseLeave={() => setIsHovered(false)} // Reset hover state on mouse leave
>
<div className="flex min-w-full shrink-0 gap-4 py-10">
{infiniteChildren}
</div>
</div>
</div>
);
};

export default MarqueeWrapper;
201 changes: 80 additions & 121 deletions src/app/services/_utils/testimonials.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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 (
<div className="py-16 bg-muted">
<ANIM__FadeInOutOnScroll>
Expand All @@ -48,89 +46,49 @@ const Testimonials = ({ data }: { data: any }) => {
</h2>
</div>
</ANIM__FadeInOutOnScroll>
<div className="py-[48px] container">
<div className="space-y-8">
<div className="relative">
<ANIM__FadeInOutOnScroll>
<Swiper
spaceBetween={0}
loop={true}
pagination={{
clickable: true,
}}
navigation={true}
mousewheel={true}
keyboard={true}
autoplay={{
delay: 2500,
disableOnInteraction: true,
}}
breakpoints={{
640: {
slidesPerView: 1,
},
768: {
slidesPerView: 2,
},
1024: {
slidesPerView: 3,
},
}}
modules={[
Pagination,
Navigation,
Mousewheel,
Keyboard,
Autoplay,
]}
>
{data?.length
? data?.filter((item: any) => item.text?.length < 100)?.map((item: any) => {
return (
<SwiperSlide key={item._id} className="pt-4 pb-16 px-4">
<TestimonialCard details={item} />
</SwiperSlide>
);
})
: null}
</Swiper>
</ANIM__FadeInOutOnScroll>
</div>
<div className="flex justify-center">
<ServicesCTA
position="center"
cta={{
primary: {
text: <>Get Started Right Away</>,
link: `/joining?type=${path[materedPath]}`,
},
secondary: { text: <>Get A Free Consultation</>, link: "/" },
}}
/>
</div>
<div className="space-y-8">
<div className="relative container">
{isClient && (
<MarqueeWrapper
className="gap-4 w-full"
itemWidth={300}
gapWidth={16}
>
{filteredData?.slice(0, 10)?.map((item: any) => (
<TestimonialCard
key={item._id}
details={item}
className="flex-shrink-0"
/>
))}
</MarqueeWrapper>
)}
<div className="pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-muted dark:from-background"></div>
<div className="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-gradient-to-l from-muted dark:from-background"></div>
</div>
<div className="flex justify-center">
<ServicesCTA
position="center"
cta={{
primary: {
text: <>Get Started Right Away</>,
link: `/joining?type=${path[materedPath]}`,
},
secondary: { text: <>Get A Free Consultation</>, link: "/" },
}}
/>
</div>
</div>
</div>
);
};

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,
Expand All @@ -148,58 +106,59 @@ export const TestimonialCard = ({
return Array.from({ length: n }, (_, i) => i + 1);
};
const [moreText, setMoreText] = useState(false);

const letterCount = 100;
return (
<div className="inline-block min-w-[300px] shadow-lg p-4 rounded-2xl space-y-[16px] border-2 border-white hover:border-secondary hover:scale-105 bg-white transition ease-in-out duration-500 hover:shadow-2xl">
<div className={`w-[300px] shrink-0 mx-2 hover:shadow-lg p-4 rounded-2xl space-y-[16px] border-2 border-white hover:border-secondary hover:scale-105 bg-white transition ease-in-out duration-500 ${className}`}>
<div className="flex">
{createArray(rating || 1).map((item: number) => {
return <Sparkle key={item} className="rotate-45 text-secondary fill-secondary/40" />;
return (
<Sparkle
key={item}
className="rotate-45 text-secondary fill-secondary/40"
/>
);
})}
</div>
<p
className={clsx("space-y-2 space-x-2 min-h-[120px]", {
"max-h-[160px] overflow-hidden": !moreText,
"h-auto": moreText,
})}
>
<i>{`"${
text?.length > letterCount
? moreText
? text
: text?.slice(0, letterCount) + "..."
: text
}"`}</i>
{text?.length > letterCount ? (
<p className={clsx(
"text-sm leading-relaxed",
moreText ? "h-auto" : "line-clamp-4"
)}>
<i>{`"${text}"`}</i>
{text?.length > letterCount && (
<span
role="button"
onClick={() => setMoreText(!moreText)}
className="text-secondary text-xs"
className="text-secondary text-xs ml-1"
>
{moreText ? "See less" : "See more"}
</span>
) : null}
)}
</p>
<div className="flex items-center gap-4">
<Image
src={avatar || ""}
alt=""
width={300}
height={300}
className="h-10 w-10 rounded-full bg-gray-200"
width={40}
height={40}
className="h-10 w-10 rounded-full bg-gray-200 object-cover"
/>
<div>
<p className="font-semibold">{name}</p>
<p className="text-gray-400 text-sm">{country}</p>
<p className="font-semibold text-sm">{name}</p>
<p className="text-gray-400 text-xs">{country}</p>
</div>
</div>
<Image
src={image || ""}
alt=""
width={300}
height={300}
className="w-full h-auto rounded-md bg-gray-200"
/>
{image && (
<Image
src={image}
alt=""
width={300}
height={200}
className="w-full h-auto rounded-md bg-gray-200 object-cover"
/>
)}
</div>
);
};

export default Testimonials;
Loading

0 comments on commit 63ad56c

Please sign in to comment.