* feat: integrate payload cms to landing page * feat: add payload configuration and update TypeScript target * fix: update TypeScript paths for payload configuration * refactor: update config import paths to use local payload.config * fix: update quotation marks for testimonials to use HTML entities * fix: include withPayload in Next.js plugins for proper configuration * feat: implement HeroSection global configuration and integrate with Hero component * feat: add FeaturesSection global configuration and integrate with Features component * feat: add revalidate constant to control page revalidation interval * refactor: optimize data fetching by using Promise.all * fix: update revalidation interval to 10 seconds * feat: add CommunitiesSection configuration and integrate with Community component * refactor: move global section functions to services directory * feat: add support for s3 bucket for media * feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration * feat: replace Link component with Button for improved button styling in Community component * feat: update FeaturesSection to use Iconify icons and adjust icon properties * feat: add heroImage field to HeroSection and implement image formatting utility * feat: add Learning Resources section with dynamic data fetching and UI integration * feat: enhance LearningResourcesSection with primary and secondary button links * refactor: reorganize layout structure by moving Header and Footer components to RootLayout * feat: implement Testimonials section with dynamic data fetching and UI integration * feat: simplify joinTitle rendering in Testimonials section * feat: implement CallToAction section with dynamic data fetching and UI integration * feat: update CallToActionSection label and wrap Discord button in link * feat: update button components to use onClick for external links and improve styling * feat: wrap logo image in a link to navigate to the homepage * fix: supress scss deps warning * fix: add placeholder to .env.example * chore: add environment placeholder variables for CMS configuration in test build workflow * fix: remove unused environment variables from test build workflow * fix: text color (#26) * chore: update action * feat: integrate payload cms to landing page * feat: add payload configuration and update TypeScript target * fix: update TypeScript paths for payload configuration * refactor: update config import paths to use local payload.config * fix: include withPayload in Next.js plugins for proper configuration * feat: implement HeroSection global configuration and integrate with Hero component * feat: add FeaturesSection global configuration and integrate with Features component * feat: add revalidate constant to control page revalidation interval * refactor: optimize data fetching by using Promise.all * fix: update revalidation interval to 10 seconds * feat: add CommunitiesSection configuration and integrate with Community component * refactor: move global section functions to services directory * feat: add support for s3 bucket for media * feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration * feat: replace Link component with Button for improved button styling in Community component * feat: update FeaturesSection to use Iconify icons and adjust icon properties * feat: add heroImage field to HeroSection and implement image formatting utility * feat: add Learning Resources section with dynamic data fetching and UI integration * feat: enhance LearningResourcesSection with primary and secondary button links * refactor: reorganize layout structure by moving Header and Footer components to RootLayout * feat: implement Testimonials section with dynamic data fetching and UI integration * feat: simplify joinTitle rendering in Testimonials section * feat: implement CallToAction section with dynamic data fetching and UI integration * feat: update CallToActionSection label and wrap Discord button in link * feat: update button components to use onClick for external links and improve styling * feat: wrap logo image in a link to navigate to the homepage * fix: supress scss deps warning * fix: add placeholder to .env.example * chore: add environment placeholder variables for CMS configuration in test build workflow * fix: remove unused environment variables from test build workflow * fix: remove mistakenly added env from test-build workflow --------- Co-authored-by: Fahim Ardani <fahimardani25@gmail.com> Co-authored-by: Maulana Sodiqin <53475078+maulanasdqn@users.noreply.github.com>
138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import { QuoteIcon } from '@imphnen-frontend-service/shadcn-ui/atoms';
|
|
import { formatCMSImageDataToMedia } from 'apps/landing/src/lib/format';
|
|
import { TestimonialsSection } from 'apps/landing/src/payload-types';
|
|
import { motion, useInView } from 'framer-motion';
|
|
import Image from 'next/image';
|
|
import { useRef } from 'react';
|
|
|
|
export function Testimonials(props: TestimonialsSection) {
|
|
const { title, subtitle, items, stats, joinTitle, joinText } = props;
|
|
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
|
|
|
const containerVariants = {
|
|
hidden: { opacity: 0 },
|
|
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
|
|
};
|
|
|
|
const itemVariants = {
|
|
hidden: { y: 20, opacity: 0 },
|
|
visible: { y: 0, opacity: 1, transition: { duration: 0.5 } },
|
|
};
|
|
|
|
return (
|
|
<section
|
|
id="testimoni"
|
|
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
|
|
ref={ref}
|
|
>
|
|
<div className="absolute inset-0 -z-10">
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(59,130,246,0.1),transparent_50%)]" />
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(96,165,250,0.1),transparent_50%)]" />
|
|
</div>
|
|
|
|
<div className="container px-4 md:px-6">
|
|
<motion.div
|
|
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl lg:text-5xl">
|
|
{title.split(' ').map((word, i) =>
|
|
i === title.split(' ').length - 1 ? (
|
|
<span
|
|
key={i}
|
|
className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400"
|
|
>
|
|
{word}
|
|
</span>
|
|
) : (
|
|
<span key={i}>{word} </span>
|
|
)
|
|
)}
|
|
</h2>
|
|
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
|
|
{subtitle}
|
|
</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="grid gap-8 md:grid-cols-3"
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate={isInView ? 'visible' : 'hidden'}
|
|
>
|
|
{items?.map((t, idx) => (
|
|
<motion.div
|
|
key={idx}
|
|
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
|
|
variants={itemVariants}
|
|
>
|
|
<div className="absolute top-6 right-6 text-primary/20 group-hover:text-primary/40 transition-colors">
|
|
<QuoteIcon className="h-8 w-8" />
|
|
</div>
|
|
<div className="relative z-10">
|
|
<p className="mb-6 text-muted-foreground italic">
|
|
“{t.quote}”
|
|
</p>
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative h-12 w-12 overflow-hidden rounded-full border-2 border-primary/20">
|
|
{(() => {
|
|
const media = formatCMSImageDataToMedia(t.avatar);
|
|
if (!media) return null;
|
|
return (
|
|
<Image
|
|
src={media.url!}
|
|
alt={media.alt}
|
|
width={600}
|
|
height={500}
|
|
className="object-cover"
|
|
/>
|
|
);
|
|
})()}
|
|
</div>
|
|
<div>
|
|
<h4 className="font-bold">{t.name}</h4>
|
|
<p className="text-sm text-muted-foreground">{t.role}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="absolute -bottom-1 -left-1 w-20 h-20 bg-gradient-to-tr from-primary/10 to-transparent rounded-tr-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm p-8 md:p-12"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
|
transition={{ duration: 0.5, delay: 0.3 }}
|
|
>
|
|
<div className="grid md:grid-cols-2 gap-8 items-center">
|
|
<div>
|
|
<h3 className="text-2xl md:text-3xl font-bold mb-4">
|
|
{joinTitle}
|
|
</h3>
|
|
<p className="text-muted-foreground">{joinText}</p>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{stats?.map((s, idx) => (
|
|
<div key={idx} className="rounded-lg border p-4 text-center">
|
|
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
|
{s.value}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">{s.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|