feat: add CommunitiesSection configuration and integrate with Community component

This commit is contained in:
ArraysID
2025-05-11 05:24:31 +07:00
parent a7f7521761
commit 009cabfc1c
8 changed files with 178 additions and 112 deletions
@@ -6,48 +6,26 @@ import {
InstagramIcon, InstagramIcon,
MessageCircleIcon, MessageCircleIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms'; } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { CommunitiesSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react'; import { useRef } from 'react';
export function Community() { export function Community(props: CommunitiesSection) {
const { items: communities, stats } = props;
const ref = useRef(null); const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 }); const isInView = useInView(ref, { once: true, amount: 0.2 });
const communities = [ const iconMap = {
{ FacebookIcon: (
icon: ( <FacebookIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
<FacebookIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" /> ),
), InstagramIcon: (
iconBg: 'bg-blue-100 dark:bg-blue-900', <InstagramIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
title: 'Facebook Group', ),
description: MessageCircleIcon: (
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.', <MessageCircleIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
buttonText: 'Gabung Sekarang', ),
buttonLink: '#', };
},
{
icon: (
<InstagramIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Instagram',
description:
'Ikuti kami di Instagram untuk tips programming, konten inspiratif, dan info event terbaru.',
buttonText: 'Follow Kami',
buttonLink: '#',
},
{
icon: (
<MessageCircleIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Discord Server',
description:
'Diskusikan langsung dengan sesama programmer dan dapatkan bantuan langsung dari para ahli.',
buttonText: 'Join Server',
buttonLink: '#',
},
];
const containerVariants = { const containerVariants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
@@ -73,105 +51,70 @@ export function Community() {
id="komunitas" id="komunitas"
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden" className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
> >
{/* Background Elements */}
<div className="absolute inset-0 -z-10"> <div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.1),transparent_70%)]" /> <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.1),transparent_70%)]" />
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(59,130,246,0.01)_1px,transparent_1px),linear-gradient(to_bottom,rgba(59,130,246,0.01)_1px,transparent_1px)] bg-[size:14px_14px]" /> <div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(59,130,246,0.01)_1px,transparent_1px),linear-gradient(to_bottom,rgba(59,130,246,0.01)_1px,transparent_1px)] bg-[size:14px_14px]" />
</div> </div>
<div className="container px-4 md:px-6" ref={ref}> <div className="container px-4 md:px-6" ref={ref}>
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
<motion.div
className="space-y-2"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
Komunitas{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Kami
</span>
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
Bergabunglah dengan ribuan programmer Indonesia yang saling
membantu dan berbagi pengalaman.
</p>
</motion.div>
</div>
<motion.div <motion.div
className="grid gap-8 md:grid-cols-3" initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl text-center mb-4">
Komunitas{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Kami
</span>
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground text-center md:text-lg">
Bergabunglah dengan ribuan programmer Indonesia yang saling membantu
dan berbagi pengalaman.
</p>
</motion.div>
<motion.div
className="grid gap-8 md:grid-cols-3 mt-12"
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
animate={isInView ? 'visible' : 'hidden'} animate={isInView ? 'visible' : 'hidden'}
> >
{communities.map((community, index) => ( {communities?.map((c, i) => (
<motion.div <motion.div
key={index} key={i}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-xl" className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-xl"
variants={itemVariants} variants={itemVariants}
> >
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="relative z-10"> <div className="relative z-10">
<div <div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full ${community.iconBg}`} {iconMap[c.iconName]}
>
{community.icon}
</div> </div>
<h3 className="mb-2 text-xl font-bold">{community.title}</h3> <h3 className="mb-2 text-xl font-bold">{c.title}</h3>
<p className="mb-6 text-muted-foreground"> <p className="mb-6 text-muted-foreground">{c.description}</p>
{community.description}
</p>
<Button <Button
variant="outline" variant="outline"
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300" className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300"
> >
{community.buttonText} {c.buttonText}
</Button> </Button>
</div> </div>
<div className="absolute -bottom-1 -right-1 w-20 h-20 bg-gradient-to-tl from-primary/20 to-transparent rounded-tl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> <div className="absolute -bottom-1 -right-1 w-20 h-20 bg-gradient-to-tl from-primary/20 to-transparent rounded-tl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</motion.div> </motion.div>
))} ))}
</motion.div> </motion.div>
{/* Community Stats */}
<motion.div <motion.div
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center" className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }} transition={{ duration: 0.5, delay: 0.3 }}
> >
<div className="space-y-2"> {stats?.map((s, i) => (
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400"> <div key={i} className="space-y-2">
10K+ <div className="text-4xl 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 className="text-sm text-muted-foreground">Member Aktif</div> ))}
</div>
<div className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
50+
</div>
<div className="text-sm text-muted-foreground">Event Bulanan</div>
</div>
<div className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
100+
</div>
<div className="text-sm text-muted-foreground">
Mentor Profesional
</div>
</div>
<div className="space-y-2">
<div className="text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
5K+
</div>
<div className="text-sm text-muted-foreground">
Diskusi Mingguan
</div>
</div>
</motion.div> </motion.div>
</div> </div>
</section> </section>
+4 -2
View File
@@ -1,3 +1,4 @@
import { getGlobalsCommunitiesSection } from '../../repositories/get-globals-communitues-section';
import { getGlobalsFeaturesSection } from '../../repositories/get-globals-features-section'; import { getGlobalsFeaturesSection } from '../../repositories/get-globals-features-section';
import { getGlobalsHeroSection } from '../../repositories/get-globals-hero-section'; import { getGlobalsHeroSection } from '../../repositories/get-globals-hero-section';
import { CallToAction } from './_components/call-to-action'; import { CallToAction } from './_components/call-to-action';
@@ -12,9 +13,10 @@ import { Testimonials } from './_components/testimonials';
export const revalidate = 10; // Seconds export const revalidate = 10; // Seconds
export default async function Page() { export default async function Page() {
const [heroData, featuresData] = await Promise.all([ const [heroData, featuresData, communitiesData] = await Promise.all([
getGlobalsHeroSection(), getGlobalsHeroSection(),
getGlobalsFeaturesSection(), getGlobalsFeaturesSection(),
getGlobalsCommunitiesSection(),
]); ]);
return ( return (
@@ -23,7 +25,7 @@ export default async function Page() {
<main className="flex-1"> <main className="flex-1">
<Hero {...heroData} /> <Hero {...heroData} />
<Features {...featuresData} /> <Features {...featuresData} />
<Community /> <Community {...communitiesData} />
<LearningResources /> <LearningResources />
<Testimonials /> <Testimonials />
<CallToAction /> <CallToAction />
@@ -0,0 +1,70 @@
import { GlobalConfig } from 'payload';
export const CommunitiesSection: GlobalConfig = {
slug: 'communities-section',
label: 'Communities Section',
fields: [
{
name: 'items',
type: 'array',
required: true,
defaultValue: [
{
iconName: 'FacebookIcon',
title: 'Facebook Group',
description:
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.',
buttonText: 'Gabung Sekarang',
buttonLink: 'https://facebook.com/groups/1032515944638255',
},
{
iconName: 'InstagramIcon',
title: 'Instagram',
description:
'Ikuti kami di Instagram untuk tips programming, konten inspiratif, dan info event terbaru.',
buttonText: 'Follow Kami',
buttonLink: 'https://www.instagram.com/imphnen.dev',
},
{
iconName: 'MessageCircleIcon',
title: 'Discord Server',
description:
'Diskusikan langsung dengan sesama programmer dan dapatkan bantuan langsung dari para ahli.',
buttonText: 'Join Server',
buttonLink: 'https://discord.com/invite/imphnen',
},
],
fields: [
{
name: 'iconName',
type: 'select',
options: [
{ label: 'Facebook', value: 'FacebookIcon' },
{ label: 'Instagram', value: 'InstagramIcon' },
{ label: 'Discord', value: 'MessageCircleIcon' },
],
required: true,
},
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'buttonText', type: 'text', required: true },
{ name: 'buttonLink', type: 'text', required: true },
],
},
{
name: 'stats',
type: 'array',
required: true,
defaultValue: [
{ value: '100K+', label: 'Member Aktif' },
{ value: '50+', label: 'Event Bulanan' },
{ value: '100+', label: 'Mentor Profesional' },
{ value: '5K+', label: 'Diskusi Mingguan' },
],
fields: [
{ name: 'value', type: 'text', required: true },
{ name: 'label', type: 'text', required: true },
],
},
],
};
@@ -3,9 +3,6 @@ import { GlobalConfig } from 'payload';
export const FeaturesSection: GlobalConfig = { export const FeaturesSection: GlobalConfig = {
slug: 'features-section', slug: 'features-section',
label: 'Features Section', label: 'Features Section',
access: {
read: (): boolean => true,
},
fields: [ fields: [
{ {
name: 'heading', name: 'heading',
@@ -3,10 +3,6 @@ import type { GlobalConfig } from 'payload';
export const HeroSection: GlobalConfig = { export const HeroSection: GlobalConfig = {
slug: 'hero-section', slug: 'hero-section',
label: 'Hero Section', label: 'Hero Section',
access: {
read: () => true,
update: () => true,
},
fields: [ fields: [
{ {
name: 'badgeText', name: 'badgeText',
+50
View File
@@ -87,10 +87,12 @@ export interface Config {
globals: { globals: {
'hero-section': HeroSection; 'hero-section': HeroSection;
'features-section': FeaturesSection; 'features-section': FeaturesSection;
'communities-section': CommunitiesSection;
}; };
globalsSelect: { globalsSelect: {
'hero-section': HeroSectionSelect<false> | HeroSectionSelect<true>; 'hero-section': HeroSectionSelect<false> | HeroSectionSelect<true>;
'features-section': FeaturesSectionSelect<false> | FeaturesSectionSelect<true>; 'features-section': FeaturesSectionSelect<false> | FeaturesSectionSelect<true>;
'communities-section': CommunitiesSectionSelect<false> | CommunitiesSectionSelect<true>;
}; };
locale: null; locale: null;
user: User & { user: User & {
@@ -322,6 +324,28 @@ export interface FeaturesSection {
updatedAt?: string | null; updatedAt?: string | null;
createdAt?: string | null; createdAt?: string | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "communities-section".
*/
export interface CommunitiesSection {
id: number;
items: {
iconName: 'FacebookIcon' | 'InstagramIcon' | 'MessageCircleIcon';
title: string;
description: string;
buttonText: string;
buttonLink: string;
id?: string | null;
}[];
stats: {
value: string;
label: string;
id?: string | null;
}[];
updatedAt?: string | null;
createdAt?: string | null;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hero-section_select". * via the `definition` "hero-section_select".
@@ -369,6 +393,32 @@ export interface FeaturesSectionSelect<T extends boolean = true> {
createdAt?: T; createdAt?: T;
globalType?: T; globalType?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "communities-section_select".
*/
export interface CommunitiesSectionSelect<T extends boolean = true> {
items?:
| T
| {
iconName?: T;
title?: T;
description?: T;
buttonText?: T;
buttonLink?: T;
id?: T;
};
stats?:
| T
| {
value?: T;
label?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth". * via the `definition` "auth".
+2 -1
View File
@@ -5,6 +5,7 @@ import { buildConfig } from 'payload';
import sharp from 'sharp'; import sharp from 'sharp';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { CommunitiesSection } from './collections/CommunitiesSection';
import { FeaturesSection } from './collections/FeaturesSection'; import { FeaturesSection } from './collections/FeaturesSection';
import { HeroSection } from './collections/HeroSection'; import { HeroSection } from './collections/HeroSection';
import { Media } from './collections/Media'; import { Media } from './collections/Media';
@@ -27,7 +28,7 @@ export default buildConfig({
}, },
}, },
collections: [Users, Media], collections: [Users, Media],
globals: [HeroSection, FeaturesSection], globals: [HeroSection, FeaturesSection, CommunitiesSection],
editor: lexicalEditor(), editor: lexicalEditor(),
secret: process.env.CMS_SECRET || '', secret: process.env.CMS_SECRET || '',
typescript: { typescript: {
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsCommunitiesSection() {
return await payload.findGlobal({
slug: 'communities-section',
});
}