feat: reforming hero section
This commit is contained in:
@@ -32,7 +32,7 @@ export function Community(props: CommunitiesSection) {
|
||||
|
||||
return (
|
||||
<section
|
||||
id="komunitas"
|
||||
id="community"
|
||||
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
|
||||
>
|
||||
<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 { getGlobalsCommunitiesSection } from '@/services/get-globals-communitues-section';
|
||||
import { getGlobalsHeroSection } from '@/services/get-globals-hero-section';
|
||||
import { getGlobalsTestimonialsSection } from '@/services/get-globals-testimonials-section';
|
||||
import { CallToAction } from './_components/call-to-action';
|
||||
import { Community } from './_components/community';
|
||||
@@ -10,9 +9,8 @@ import { Testimonials } from './_components/testimonials';
|
||||
export const revalidate = 10; // Seconds
|
||||
|
||||
export default async function Page() {
|
||||
const [heroData, communitiesData, testimonialsData, callToActionData] =
|
||||
const [communitiesData, testimonialsData, callToActionData] =
|
||||
await Promise.all([
|
||||
getGlobalsHeroSection(),
|
||||
getGlobalsCommunitiesSection(),
|
||||
getGlobalsTestimonialsSection(),
|
||||
getGlobalsCallToActionSection(),
|
||||
@@ -20,7 +18,7 @@ export default async function Page() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero {...heroData} />
|
||||
<Hero />
|
||||
<Community {...communitiesData} />
|
||||
<Testimonials {...testimonialsData} />
|
||||
<CallToAction {...callToActionData} />
|
||||
|
||||
Reference in New Issue
Block a user