Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 93 additions & 64 deletions src/components/testimonials/TestimonialCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { motion } from "framer-motion";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { useSafeColorMode } from "../../utils/useSafeColorMode";
import { ExternalLink, Quote } from "lucide-react";

interface TestimonialCardProps {
name: string;
Expand All @@ -22,92 +23,120 @@ const TestimonialCard: React.FC<TestimonialCardProps> = ({
}) => {
const { colorMode, isDark } = useSafeColorMode();

// Function to format the link display
const formatLinkDisplay = (url: string) => {
try {
const urlObj = new URL(url);
return urlObj.hostname + urlObj.pathname;
} catch {
// If URL parsing fails, return the original link
return url;
}
};

return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={`flex h-[250px] flex-col justify-between rounded-2xl p-6 shadow-lg transition-shadow duration-300 hover:shadow-xl ${
isDark ? "bg-[#1a1a1a] text-white" : "bg-white text-gray-900"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
whileHover={{ y: -5 }}
transition={{ duration: 0.3 }}
className={`group relative h-full overflow-hidden rounded-2xl border backdrop-blur-sm transition-all duration-300 hover:shadow-2xl ${
isDark
? "border-gray-700/50 bg-gray-900/80 shadow-xl"
: "border-gray-200/50 bg-white/90 shadow-lg"
}`}
>
{/* Header with Avatar and Name */}
<div className="flex items-center gap-4">
<Avatar className="h-24 w-24 rounded-full">
<AvatarImage src={avatar} className="object-contain" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div>
<h3
className={`text-lg font-semibold ${isDark ? "text-white" : "text-gray-900"}`}
>
{name}
</h3>
<p
className={`text-sm ${isDark ? "text-gray-400" : "text-gray-500"}`}
>
@{username}
{/* Gradient Background */}
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/5 via-blue-500/5 to-pink-500/5" />

{/* Quote Icon */}
<div className="absolute top-4 right-4 opacity-20">
<Quote size={32} className="text-purple-500" />
</div>

<div className="relative flex h-full flex-col p-6">
{/* Header */}
<div className="mb-6 flex items-center gap-4">
<div className="relative">
<Avatar className="h-16 w-16 border-2 border-gradient-to-r from-purple-500 to-pink-500">
<AvatarImage src={avatar} className="object-contain" />
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-pink-500 text-white font-semibold">
{name.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="absolute -bottom-1 -right-1 h-5 w-5 rounded-full bg-green-500 border-2 border-white" />
</div>
<div className="flex-1">
<h3 className={`text-lg font-bold ${
isDark ? "text-white" : "text-gray-900"
}`}>
{name}
</h3>
<p className={`text-sm ${
isDark ? "text-gray-400" : "text-gray-500"
}`}>
@{username}
</p>
</div>
</div>

{/* Content */}
<div className="flex-1">
<p className={`text-base leading-relaxed ${
isDark ? "text-gray-300" : "text-gray-700"
}`}>
{content.replace(/#\w+/g, '').trim()}
</p>
</div>
</div>

{/* Content */}
<p
className={`my-4 line-clamp-3 flex-grow ${isDark ? "text-gray-300" : "text-gray-700"}`}
>
{content}
</p>
{/* Footer */}
<div className={`mt-6 space-y-4 border-t pt-4 ${
isDark ? "border-gray-700/50" : "border-gray-200/50"
}`}>
{/* Hashtags */}
<div className="flex flex-wrap gap-2">
{content.match(/#\w+/g)?.map((hashtag, index) => (
<span
key={index}
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors hover:scale-105 ${
isDark
? "bg-blue-500/20 text-blue-400 hover:bg-blue-500/30"
: "bg-blue-100 text-blue-600 hover:bg-blue-200"
}`}
>
{hashtag}
</span>
))}
</div>

{/* Footer with Hashtags and Date */}
<div
className={`flex flex-col gap-2 border-t pt-2 text-sm ${
isDark ? "border-gray-700" : "border-gray-100"
}`}
>
{/* Hashtags */}
<div className="flex flex-wrap gap-2">
{content.match(/#\w+/g)?.map((hashtag, index) => (
<span
key={index}
className="cursor-pointer text-blue-500 hover:text-blue-600"
{/* Link and Date */}
<div className="flex items-center justify-between">
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className={`group/link flex items-center gap-2 text-sm font-medium transition-colors ${
isDark
? "text-purple-400 hover:text-purple-300"
: "text-purple-600 hover:text-purple-700"
}`}
>
{hashtag}
<span className="truncate">{formatLinkDisplay(link)}</span>
<ExternalLink size={14} className="transition-transform group-hover/link:translate-x-0.5 group-hover/link:-translate-y-0.5" />
</a>
<span className={`text-xs ${
isDark ? "text-gray-500" : "text-gray-400"
}`}>
{date}
</span>
))}
</div>

{/* Link and Date Row */}
<div className="flex items-center justify-between">
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className={`cursor-pointer hover:underline ${
isDark
? "text-blue-400 hover:text-blue-300"
: "text-blue-600 hover:text-blue-700"
}`}
>
{formatLinkDisplay(link)}
</a>
<span className={isDark ? "text-gray-500" : "text-gray-400"}>
{date}
</span>
</div>
</div>
</div>

{/* Hover Effect Border */}
<div className="absolute inset-0 rounded-2xl border-2 border-transparent bg-gradient-to-r from-purple-500/20 via-blue-500/20 to-pink-500/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
style={{ mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', maskComposite: 'xor' }} />
</motion.div>
);
};

export default TestimonialCard;
export default TestimonialCard;
146 changes: 100 additions & 46 deletions src/components/testimonials/TestimonialCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
import { Button } from "../ui/button";
import TestimonialCard from "./TestimonialCard";
import Autoplay from "embla-carousel-autoplay";
import { motion } from "framer-motion";
import { useSafeColorMode } from "../../utils/useSafeColorMode";

// Sample testimonial data
const testimonials = [
const baseTestimonials = [
{
name: "Rashi Chouhan",
username: "RashiChouhan",
Expand Down Expand Up @@ -42,10 +43,13 @@ const testimonials = [
},
];

const testimonials = [...baseTestimonials, ...baseTestimonials];

export function TestimonialCarousel() {
const [api, setApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0);
const [count, setCount] = useState(0);
const { colorMode } = useSafeColorMode();

useEffect(() => {
if (!api) {
Expand All @@ -61,52 +65,102 @@ export function TestimonialCarousel() {
}, [api]);

return (
<div className="w-full">
<div className="mb-10 text-center">
<h2 className="mb-2 text-3xl font-bold">Loved by Many Users</h2>
<div className="mx-auto h-1 w-32 rounded-full bg-blue-500"></div>
<div className="relative w-full py-20">
{/* Background Elements */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute top-20 right-10 h-64 w-64 rounded-full bg-gradient-to-br from-purple-400/10 to-pink-600/10 blur-3xl" />
<div className="absolute bottom-20 left-10 h-64 w-64 rounded-full bg-gradient-to-tr from-blue-400/10 to-cyan-600/10 blur-3xl" />
</div>

<Carousel
setApi={setApi}
className="w-full"
opts={{
align: "start",
loop: true,
}}
plugins={[
Autoplay({
delay: 4000,
}),
]}
>
<CarouselContent className="my-16 -ml-2 md:-ml-4">
{testimonials.map((testimonial, index) => (
<CarouselItem
key={index}
className="h-full pl-2 md:basis-1/2 md:pl-4"
>
<TestimonialCard {...testimonial} />
</CarouselItem>
))}
</CarouselContent>

<div className="mt-8 flex items-center justify-center gap-2">
<CarouselPrevious className="static translate-y-0" />
<div className="flex gap-2">
{Array.from({ length: count }).map((_, index) => (
<Button
key={index}
variant={current === index + 1 ? "default" : "outline"}
size="icon"
className="h-2 w-2 rounded-full p-0"
onClick={() => api?.scrollTo(index)}
/>
))}
<div className="relative mx-auto max-w-7xl px-4">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="mb-16 text-center"
>
<div className="mb-4">
<span className={`inline-block rounded-full px-4 py-2 text-sm font-medium ${
colorMode === "dark"
? "bg-purple-500/10 text-purple-400 border border-purple-500/20"
: "bg-purple-50 text-purple-600 border border-purple-200"
}`}>
⭐ Client Testimonials
</span>
</div>
<CarouselNext className="static translate-y-0" />
</div>
</Carousel>
<h2 className={`mb-6 text-5xl font-bold leading-tight ${
colorMode === "dark" ? "text-white" : "text-gray-900"
}`}>
Loved by <span className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">Many Users</span>
</h2>

</motion.div>

<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<Carousel
setApi={setApi}
className="w-full"
opts={{
align: "center",
loop: true,
}}
plugins={[
Autoplay({
delay: 2500,
stopOnInteraction: false,
stopOnMouseEnter: false,
}),
]}
>
<CarouselContent className="my-8 -ml-2 md:-ml-4 flex items-stretch">
{testimonials.map((testimonial, index) => (
<CarouselItem
key={index}
className="flex pl-2 md:basis-1/2 lg:basis-1/3 md:pl-4"
>
<TestimonialCard {...testimonial} />
</CarouselItem>
))}
</CarouselContent>

{/* Navigation */}
<div className="mt-12 flex items-center justify-center gap-6">
<CarouselPrevious className={`static translate-y-0 h-12 w-12 ${
colorMode === "dark"
? "border-gray-700 bg-gray-800 hover:bg-gray-700"
: "border-gray-200 bg-white hover:bg-gray-50"
}`} />

{/* Dots Indicator */}
<div className="flex gap-2">
{Array.from({ length: count }).map((_, index) => (
<button
key={index}
onClick={() => api?.scrollTo(index)}
className={`h-3 w-3 rounded-full transition-all duration-300 ${
current === index + 1
? "bg-gradient-to-r from-purple-600 to-pink-600 scale-125"
: colorMode === "dark"
? "bg-gray-600 hover:bg-gray-500"
: "bg-gray-300 hover:bg-gray-400"
}`}
/>
))}
</div>

<CarouselNext className={`static translate-y-0 h-12 w-12 ${
colorMode === "dark"
? "border-gray-700 bg-gray-800 hover:bg-gray-700"
: "border-gray-200 bg-white hover:bg-gray-50"
}`} />
</div>
</Carousel>
</motion.div>
</div>
</div>
);
}
}
Loading
Loading