feat: landing auth integration & landing ui improvements (#33)
* feat: setup openapi libs for landing * feat: seperate landing layouts * feat: movee landing providers * feat: add auth layout * feat: implement cookie management for access and refresh tokens * feat: replace Inter font with Bai Jamjuree in layout and add font utility * feat: add signin * feat: add Poppins font to the font utility * feat: expand theme colors in index.css for improved styling options * feat: update dependencies and enhance button and form components with new variants and context * feat: move old components to __old__ dir * feat: add react query support * feat: hide the footer * fix: header style * feat: replace Image component with LogoSimple in Header and update Logo component to accept props * fix: update label for tutorial stat in hero-stats.json * feat: implement EventsPage component with event listing and details * feat: add signup button and page * feat: add HERO_CONTENT data and integrate into Hero component * fix: update navigations import to uppercase and add missing Testimoni entry * feat: implement testimonials display and add submission page * fix: correct link path for testimonial submission * feat: add communites component and integrate social links from JSON data * fix: adjust grid gap in Communities component for better layout * fix: update font imports in layout components for consistency * fix: replace text logo with LogoSimple component in Footer and restore Footer in Layout * feat: add TestimonialSection component and update testimonials data * fix: update section ID from 'komunitas' to 'community' for consistency * feat: add CTASection component with interactive elements and background pattern * fix: update join URL in hero content for consistency * fix: update text in CTASection for improved emphasis and consistency * feat: add CommunitySection component and integrate it into the main page * feat: update Footer component to use dynamic navigation and social links * feat: implement HeroSection component with dynamic content and animations * refactor: replace hardcoded testimonials with dynamic data from JSON * feat: add signup page * feat: integrate Turnstile captcha verification in signup process * feat: implement verification page * feat: add signin link to the signup page * feat: add "Forgot Password?" link to the signin form * refactor: clean up unused ThemeProvider code and CSS variables * feat: add base styles for borders and background to improve UI consistency * feat: implement forgot password functionality with validation and UI components * fix: update password validation rules for clarity and consistency * feat: enable rich colors for Toaster component in layout * feat: implement reset password functionality with validation and UI components * feat: implement Turnstile captcha verification for forgot password and signup actions * refactor: remove console log and enhance error handling in signin and resend OTP actions * feat: add navigation entries for Roadmap and Artikel sections * feat: create initial Page component for articles section * feat: update footer component with additional social media links and icons * feat: remove all unused components in landing * chore: add @radix-ui/react-label deps * feat: wrap VerificationTabs in Suspense * feat: update .env.example to include TURNSTILE_SECRET_KEY and correct NEXT_PUBLIC_TURNSTILE_SITEKEY
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import SOCIALS from '@/data/socials.json';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import {
|
||||
FaArrowRight,
|
||||
FaDiscord,
|
||||
FaFacebook,
|
||||
FaInstagram,
|
||||
FaLinkedin,
|
||||
FaTiktok,
|
||||
} from 'react-icons/fa';
|
||||
|
||||
export function CommunitySection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getIconComponent = (iconName: string) => {
|
||||
switch (iconName) {
|
||||
case 'FaFacebook':
|
||||
return FaFacebook;
|
||||
case 'FaDiscord':
|
||||
return FaDiscord;
|
||||
case 'FaInstagram':
|
||||
return FaInstagram;
|
||||
case 'FaTiktok':
|
||||
return FaTiktok;
|
||||
case 'FaLinkedin':
|
||||
return FaLinkedin;
|
||||
default:
|
||||
return FaArrowRight;
|
||||
}
|
||||
};
|
||||
|
||||
const getPlatformColors = (iconName: string) => {
|
||||
switch (iconName) {
|
||||
case 'FaFacebook':
|
||||
return {
|
||||
iconColor: 'text-[#1877F2]',
|
||||
};
|
||||
case 'FaDiscord':
|
||||
return {
|
||||
iconColor: 'text-[#5865F2]',
|
||||
};
|
||||
case 'FaInstagram':
|
||||
return {
|
||||
iconColor: 'text-[#E4405F]',
|
||||
};
|
||||
case 'FaTiktok':
|
||||
return {
|
||||
iconColor: 'text-[#000000]',
|
||||
};
|
||||
case 'FaLinkedin':
|
||||
return {
|
||||
iconColor: 'text-[#0A66C2]',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
iconColor: 'text-primary-500',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="community" className="w-full py-20 md:py-28">
|
||||
<div className="container" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl">
|
||||
Bergabung dengan Komunitas Kami di
|
||||
<span className="block mt-2 text-primary-500">
|
||||
Berbagai Platform
|
||||
</span>
|
||||
</h2>
|
||||
<p className="max-w-[600px] mx-auto text-gray-600 md:text-lg/relaxed mt-4">
|
||||
Terhubung dengan sesama developer di komunitas kami
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{SOCIALS.map((community, index) => {
|
||||
const IconComponent = getIconComponent(community.icon);
|
||||
const colors = getPlatformColors(community.icon);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
className="group relative p-8 bg-white border border-gray-200 rounded-xl hover:border-gray-300 transition-all duration-300"
|
||||
>
|
||||
<div className="flex flex-col items-start gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<IconComponent
|
||||
className={`w-8 h-8 ${colors.iconColor} transition-colors`}
|
||||
/>
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{community.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm/relaxed">
|
||||
{community.description}
|
||||
</p>
|
||||
<a
|
||||
href={community.link}
|
||||
className={`inline-flex items-center gap-2 mt-2 text-sm font-medium transition-colors`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>Jelajahi Komunitas</span>
|
||||
<FaArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
'use client';
|
||||
|
||||
import { LogoSimple } from '@/app/_components/logo';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
|
||||
export function CTASection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 });
|
||||
|
||||
return (
|
||||
<section className="w-full py-20 md:py-32 bg-gradient-to-br from-primary-500 to-primary-600 relative overflow-hidden">
|
||||
{/* Background pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-10"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='52' height='26' viewBox='0 0 52 26' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.4'%3E%3Cpath d='M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="container relative" ref={ref}>
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-12">
|
||||
{/* Text Content */}
|
||||
<div className="flex-1 text-center lg:text-left space-y-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white leading-tight">
|
||||
<span className="block">LET'S GO</span>
|
||||
<span className="block text-5xl md:text-6xl mt-2">
|
||||
<span className="text-yellow-300">SAAT</span>
|
||||
<span className="text-white">NYA</span>
|
||||
</span>
|
||||
<span className="block mt-2">KAMU JOIN!</span>
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isInView ? { opacity: 1 } : {}}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="text-lg md:text-xl text-white/90 max-w-2xl mx-auto lg:mx-0"
|
||||
>
|
||||
Jadilah bagian dari komunitas developer terbesar di Indonesia.
|
||||
Tingkatkan skill, perluas jaringan, dan raih kesempatan karir
|
||||
bersama kami!
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={isInView ? { scale: 1, opacity: 1 } : {}}
|
||||
transition={{ type: 'spring', stiffness: 100 }}
|
||||
className="flex justify-center lg:justify-start"
|
||||
>
|
||||
<a
|
||||
href="#community"
|
||||
className="flex items-center gap-4 px-8 py-4 bg-yellow-300 hover:bg-yellow-400 text-gray-900 rounded-full text-lg font-semibold transition-all hover:gap-6 group"
|
||||
>
|
||||
<span>Join Sekarang</span>
|
||||
<FaArrowRight className="w-5 h-5 transition-all group-hover:rotate-45" />
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Illustration */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex-1 max-w-xl"
|
||||
>
|
||||
<div className="relative p-8">
|
||||
<div className="absolute inset-0 bg-white/10 rounded-3xl transform rotate-6"></div>
|
||||
<div className="relative bg-white/5 rounded-3xl p-8 backdrop-blur-lg border border-white/10">
|
||||
<div className="flex flex-col items-center gap-6 text-white">
|
||||
<LogoSimple className="w-[240px]" />
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-2xl font-bold">180.000+</h3>
|
||||
<p className="text-lg">Programmer Sudah Bergabung</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import HERO_CONTENT from '@/data/hero-content.json';
|
||||
import HERO_STATS from '@/data/hero-stats.json';
|
||||
import { Button } from '@components';
|
||||
import { motion } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
|
||||
export function HeroSection() {
|
||||
const router = useRouter();
|
||||
const { communityLabel, headingLine1, headingLine2, description, buttons } =
|
||||
HERO_CONTENT;
|
||||
|
||||
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">
|
||||
{/* Background gradients and motion */}
|
||||
<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 }}
|
||||
>
|
||||
<span className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit">
|
||||
{communityLabel}
|
||||
</span>
|
||||
|
||||
<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">
|
||||
{headingLine1}{' '}
|
||||
<span className="bg-clip-text bg-gradient-to-r text-primary-500">
|
||||
{headingLine2}
|
||||
</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" onClick={() => router.push(buttons.joinUrl)}>
|
||||
{buttons.join}
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="bordered"
|
||||
className="w-full sm:w-auto group relative overflow-hidden"
|
||||
onClick={() => router.push(buttons.exploreUrl)}
|
||||
>
|
||||
<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.explore}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-6 sm:gap-8">
|
||||
{HERO_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-primary-500">
|
||||
{value}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
</div>
|
||||
{i < HERO_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 }}
|
||||
>
|
||||
<Image
|
||||
src="/logo.webp"
|
||||
alt="logo"
|
||||
width={600}
|
||||
height={500}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import TESTIMONIALS from '@/data/testimonials.json';
|
||||
import { buttonVariants } from '@components';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRef } from 'react';
|
||||
import { FaQuoteLeft } from 'react-icons/fa';
|
||||
|
||||
export function TestimonialSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.1 });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="w-full py-20 md:py-28 bg-gray-50">
|
||||
<div className="container" ref={ref}>
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl">
|
||||
Apa Kata Mereka Tentang
|
||||
<span className="block mt-2 text-primary-500">
|
||||
Komunitas Kami?
|
||||
</span>
|
||||
</h2>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mb-16"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
{TESTIMONIALS.map((testimonial) => (
|
||||
<motion.div
|
||||
key={testimonial.id}
|
||||
variants={itemVariants}
|
||||
className="p-6 bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow duration-300"
|
||||
>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
src={testimonial.image}
|
||||
alt={testimonial.name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="w-12 h-12 rounded-full object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900">
|
||||
{testimonial.name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600">{testimonial.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-600 relative">
|
||||
<FaQuoteLeft className="text-primary-500/30 w-6 h-6 mb-2" />
|
||||
<p className="text-sm/relaxed">{testimonial.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Link href="/testimonials" className={buttonVariants({ size: 'lg' })}>
|
||||
Tulis Testimonimu
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user