feat: reforming hero section
This commit is contained in:
@@ -32,7 +32,7 @@ export function Community(props: CommunitiesSection) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="komunitas"
|
id="community"
|
||||||
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"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 -z-10">
|
<div className="absolute inset-0 -z-10">
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { formatCMSImageDataToMedia } from '@/lib/format';
|
|
||||||
import { HeroSection } from '@/payload-types';
|
|
||||||
import { Button, CodeIcon, SparklesIcon, UsersIcon } from '@components/atoms';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { Fragment, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export function Hero(props: HeroSection) {
|
|
||||||
const {
|
|
||||||
badgeText,
|
|
||||||
buttons,
|
|
||||||
description,
|
|
||||||
highlight,
|
|
||||||
title,
|
|
||||||
stats,
|
|
||||||
heroImage,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [scrollY, setScrollY] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
setScrollY(window.scrollY);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="relative w-full py-20 md:py-32 lg:py-40 overflow-hidden">
|
|
||||||
<div className="absolute inset-0 -z-10 overflow-hidden">
|
|
||||||
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50" />
|
|
||||||
<div
|
|
||||||
className="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl"
|
|
||||||
style={{
|
|
||||||
transform: `translate(${scrollY * 0.1}px, ${scrollY * -0.05}px)`,
|
|
||||||
opacity: Math.max(0.2, 1 - scrollY * 0.001),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl"
|
|
||||||
style={{
|
|
||||||
transform: `translate(${scrollY * -0.1}px, ${scrollY * 0.05}px)`,
|
|
||||||
opacity: Math.max(0.2, 1 - scrollY * 0.001),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="container px-4 md:px-6 relative">
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
|
|
||||||
<motion.div
|
|
||||||
className="flex flex-col justify-center space-y-8"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit">
|
|
||||||
<SparklesIcon className="mr-1 h-3.5 w-3.5 text-primary" />
|
|
||||||
<span>{badgeText}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
|
|
||||||
{title} <br />
|
|
||||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
|
||||||
{highlight}
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-[600px] text-muted-foreground md:text-xl">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
className="w-full sm:w-auto bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
|
|
||||||
onClick={() => window.open(buttons.primaryUrl, '_blank')}
|
|
||||||
>
|
|
||||||
{buttons.primaryLabel}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
className="w-full sm:w-auto group relative overflow-hidden border-primary"
|
|
||||||
onClick={() => window.open(buttons.secondaryUrl, '_blank')}
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-primary/10 to-blue-400/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300" />
|
|
||||||
<span className="relative">{buttons.secondaryLabel}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center gap-6 sm:gap-8">
|
|
||||||
{stats?.map(({ value, label }, i) => (
|
|
||||||
<Fragment key={i}>
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">{label}</div>
|
|
||||||
</div>
|
|
||||||
{i < stats.length - 1 && (
|
|
||||||
<div className="hidden sm:block h-10 border-r border-border mx-4" />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="relative w-full lg:w-auto mx-auto lg:ml-auto"
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute -top-6 -left-6 w-12 h-12 rounded-lg border border-primary/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
|
|
||||||
<CodeIcon className="h-6 w-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div className="absolute -bottom-6 -right-6 w-12 h-12 rounded-lg border border-blue-400/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
|
|
||||||
<UsersIcon className="h-6 w-6 text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div className="relative z-10 rounded-2xl overflow-hidden border shadow-2xl">
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-tr from-primary/10 to-blue-400/10" />
|
|
||||||
{(() => {
|
|
||||||
const media = formatCMSImageDataToMedia(heroImage);
|
|
||||||
if (!media) return null;
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
src={String(media.url)}
|
|
||||||
alt={media.alt}
|
|
||||||
width={600}
|
|
||||||
height={500}
|
|
||||||
className="w-full h-auto object-cover"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@components/atoms';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { TbCalendarStar } from 'react-icons/tb';
|
||||||
|
|
||||||
|
export function ButtonExploreEvent() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full sm:w-auto group relative overflow-hidden border-primary flex items-center justify-center gap-2"
|
||||||
|
onClick={() => router.push('/events')}
|
||||||
|
>
|
||||||
|
<TbCalendarStar className="size-7" />
|
||||||
|
<span className="relative">Explore Event</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@components/atoms';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { TbLocationFilled } from 'react-icons/tb';
|
||||||
|
|
||||||
|
export function ButtonJoinCommunity() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
className="group relative w-full sm:w-auto bg-gradient-to-r from-primary to-blue-600 hover:from-primary/90 hover:to-blue-600/90 transition-all duration-300 font-bold text-white hover:text-white/90 shadow-lg"
|
||||||
|
onClick={() => router.push('#community')}
|
||||||
|
>
|
||||||
|
<TbLocationFilled className="size-6" />
|
||||||
|
Gabung Komunitas
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function HeroBadge() {
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit truncate">
|
||||||
|
<span>🇮🇩 Komunitas Programmer Indonesia</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
export function HeroImage() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="relative w-full lg:w-auto mx-auto lg:ml-auto max-w-[600px] mt-8 lg:mt-0"
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/logo.png"
|
||||||
|
alt=""
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
className="w-full h-auto object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export function HeroMainText() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70 leading-tight">
|
||||||
|
Programmer Handal <br />
|
||||||
|
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
|
||||||
|
Namun Enggan Ngoding
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-[600px] text-muted-foreground md:text-xl lg:text-lg">
|
||||||
|
Bergabunglah bersama 180.000+ programmer Indonesia dan berkembang
|
||||||
|
bersama komunitas ini.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function HeroWrapper({ children }: { children: ReactNode }) {
|
||||||
|
const [scrollY, setScrollY] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setScrollY(window.scrollY);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative w-full py-16 md:py-24 lg:py-32 xl:py-40 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 -z-10 overflow-hidden">
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50" />
|
||||||
|
<div
|
||||||
|
className="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl"
|
||||||
|
style={{
|
||||||
|
transform: `translate(${scrollY * 0.1}px, ${scrollY * -0.05}px)`,
|
||||||
|
opacity: Math.max(0.2, 1 - scrollY * 0.0005),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl"
|
||||||
|
style={{
|
||||||
|
transform: `translate(${scrollY * -0.1}px, ${scrollY * 0.05}px)`,
|
||||||
|
opacity: Math.max(0.2, 1 - scrollY * 0.0005),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { ButtonExploreEvent } from './button-explore-event';
|
||||||
|
import { ButtonJoinCommunity } from './button-join-community';
|
||||||
|
import { HeroBadge } from './hero-badge';
|
||||||
|
import { HeroImage } from './hero-image';
|
||||||
|
import { HeroMainText } from './hero-main-text';
|
||||||
|
import { HeroWrapper } from './hero-wrapper';
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<HeroWrapper>
|
||||||
|
<div className="container px-4 md:px-8 relative">
|
||||||
|
<div className="grid gap-8 lg:grid-cols-2 lg:gap-16 items-center">
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col justify-center space-y-4"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<HeroBadge />
|
||||||
|
<HeroMainText />
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||||
|
<ButtonJoinCommunity />
|
||||||
|
<ButtonExploreEvent />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<HeroImage />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HeroWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { getGlobalsCallToActionSection } from '@/services/get-globals-call-to-action-section';
|
import { getGlobalsCallToActionSection } from '@/services/get-globals-call-to-action-section';
|
||||||
import { getGlobalsCommunitiesSection } from '@/services/get-globals-communitues-section';
|
import { getGlobalsCommunitiesSection } from '@/services/get-globals-communitues-section';
|
||||||
import { getGlobalsHeroSection } from '@/services/get-globals-hero-section';
|
|
||||||
import { getGlobalsTestimonialsSection } from '@/services/get-globals-testimonials-section';
|
import { getGlobalsTestimonialsSection } from '@/services/get-globals-testimonials-section';
|
||||||
import { CallToAction } from './_components/call-to-action';
|
import { CallToAction } from './_components/call-to-action';
|
||||||
import { Community } from './_components/community';
|
import { Community } from './_components/community';
|
||||||
@@ -10,9 +9,8 @@ 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, communitiesData, testimonialsData, callToActionData] =
|
const [communitiesData, testimonialsData, callToActionData] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getGlobalsHeroSection(),
|
|
||||||
getGlobalsCommunitiesSection(),
|
getGlobalsCommunitiesSection(),
|
||||||
getGlobalsTestimonialsSection(),
|
getGlobalsTestimonialsSection(),
|
||||||
getGlobalsCallToActionSection(),
|
getGlobalsCallToActionSection(),
|
||||||
@@ -20,7 +18,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero {...heroData} />
|
<Hero />
|
||||||
<Community {...communitiesData} />
|
<Community {...communitiesData} />
|
||||||
<Testimonials {...testimonialsData} />
|
<Testimonials {...testimonialsData} />
|
||||||
<CallToAction {...callToActionData} />
|
<CallToAction {...callToActionData} />
|
||||||
|
|||||||
Reference in New Issue
Block a user