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:
Rasyid
2025-05-30 11:57:17 +07:00
committed by GitHub
parent 9f25941a4a
commit 791f43986d
97 changed files with 5679 additions and 1144 deletions
@@ -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&apos;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>
);
}
@@ -0,0 +1,14 @@
import { CommunitySection } from './_components/community-section';
import { CTASection } from './_components/cta-section';
import { HeroSection } from './_components/hero-section';
import { TestimonialSection } from './_components/testimonial-section';
export default function Page() {
return (
<>
<HeroSection />
<CommunitySection />
<TestimonialSection />
<CTASection />
</>
);
}
@@ -0,0 +1,102 @@
import { LogoSimple } from '@/app/_components/logo';
import NAVIGATIONS from '@/data/navigations.json';
import SOCIALS from '@/data/socials.json';
import Link from 'next/link';
import {
FaDiscord,
FaFacebook,
FaInstagram,
FaLinkedinIn,
FaTiktok,
} from 'react-icons/fa';
export default function Footer() {
return (
<footer className="w-full border-t bg-background py-12 md:py-16">
<div className="container px-4 md:px-6">
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
<div className="space-y-4">
<div className="flex items-center gap-2">
<LogoSimple />
</div>
<p className="text-sm text-muted-foreground">
Ingin Menjadi Programmer Handal Namun Enggan Ngoding
</p>
<div className="flex space-x-4">
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<FaFacebook className="size-6" />
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<FaDiscord className="size-6" />
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<FaInstagram className="size-6" />
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<FaTiktok className="size-6" />
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground"
>
<FaLinkedinIn className="size-6" />
</Link>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-bold">Halaman</h3>
<ul className="space-y-2">
{NAVIGATIONS.map(({ link, title }) => (
<li key={link}>
<Link
href={link}
className="text-sm text-muted-foreground hover:text-foreground"
>
{title}
</Link>
</li>
))}
</ul>
</div>
<div className="space-y-4">
<h3 className="text-lg font-bold">Link</h3>
<ul className="space-y-2">
{SOCIALS.map(({ link, name }) => (
<li key={name}>
<Link
href={link}
className="text-sm text-muted-foreground hover:text-foreground"
>
{name}
</Link>
</li>
))}
</ul>
</div>
<div className="space-y-4">
<h3 className="text-lg font-bold">Patners</h3>
<ul className="space-y-2"></ul>
</div>
</div>
<div className="mt-8 border-t pt-8 text-center">
<p className="text-xs text-muted-foreground">
© {new Date().getFullYear()} IMPHNEN - Ingin Menjadi Programmer
Handal, Namun Enggan Ngoding. All rights reserved.
</p>
</div>
</div>
</footer>
);
}
@@ -0,0 +1,110 @@
'use client';
import { LogoSimple } from '@/app/_components/logo';
import NAVIGATIONS from '@/data/navigations.json';
import { Button } from '@components';
import { cn } from '@utils';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { LuMenu, LuX } from 'react-icons/lu';
export function Header() {
const router = useRouter();
const [isScrolled, setIsScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<header
className={cn(
'sticky top-0 z-50 w-full transition-all duration-300',
isScrolled
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
: 'bg-transparent'
)}
>
<div className="container flex h-16 items-center justify-between">
<div className="flex items-center gap-2">
<div className="relative overflow-hidden rounded">
<Link href="/">
<LogoSimple className="w-[80px]" />
</Link>
</div>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
{NAVIGATIONS.map(({ link, title }) => (
<Link
key={link}
href={link}
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
{title}
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary-500 transition-all duration-300 group-hover:w-full"></span>
</Link>
))}
</nav>
<div className="flex items-center gap-x-2">
<Button
onClick={() => router.push('/signin')}
className="hidden md:flex"
>
Masuk
</Button>
<Button
variant="secondary"
onClick={() => router.push('/signup')}
className="hidden md:flex"
>
Daftar
</Button>
{/* Mobile Menu Button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="flex md:hidden"
>
{mobileMenuOpen ? (
<LuX className="size-5" />
) : (
<LuMenu className="size-5" />
)}
</button>
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
<nav className="container flex flex-col py-4 text-center">
{NAVIGATIONS.map(({ link, title }) => (
<Link
key={link}
href={link}
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => router.push(link)}
>
{title}
</Link>
))}
<Button onClick={() => router.push('/signin')}>Login</Button>
</nav>
</div>
)}
</header>
);
}
@@ -0,0 +1,38 @@
'use client';
import { Button, MoonIcon, SunIcon } from '@components';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
return (
<Button
variant="bordered"
size="icon"
onClick={toggleTheme}
className="focus-visible:ring-0 cursor-pointer"
>
{theme === 'dark' ? (
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
) : (
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
);
}
@@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}
@@ -0,0 +1,192 @@
'use client';
import events from '@/data/events.json';
import { buttonVariants } from '@components';
import { cn } from '@utils';
import Image from 'next/image';
import { HiCalendar, HiClock, HiLocationMarker } from 'react-icons/hi';
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Asia/Jakarta',
});
};
const getEventStatus = (endDate: string) => {
const now = new Date();
const end = new Date(endDate);
return end > now ? 'upcoming' : 'past';
};
export default function EventsPage() {
const sortedEvents = [...events].sort(
(a, b) =>
new Date(b.start_date).getTime() - new Date(a.start_date).getTime()
);
return (
<section className="min-h-screen bg-background container py-10">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{/* Featured Card */}
<div className="col-span-full">
<div className="rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden bg-card">
<div className="grid md:grid-cols-2">
<div className="min-h-96 bg-muted relative">
<Image
src={sortedEvents[0].thumbnail}
alt={sortedEvents[0].name}
fill
className="object-cover object-top"
sizes="(max-width: 768px) 100vw, 50vw"
priority
unoptimized
/>
</div>
<div className="p-8 flex flex-col">
<div className="flex items-center gap-2 mb-4">
<span className="bg-primary/20 text-primary text-xs px-2.5 py-1 rounded-full">
Event Terbaru
</span>
<span
className={cn(
'px-2 py-1 rounded-full text-xs',
getEventStatus(sortedEvents[0].end_date) === 'upcoming'
? 'bg-primary/20 text-primary'
: 'bg-muted text-muted-foreground'
)}
>
{getEventStatus(sortedEvents[0].end_date) === 'upcoming'
? 'Upcoming'
: 'Selesai'}
</span>
</div>
<h2 className="text-2xl font-bold mb-4 text-foreground">
{sortedEvents[0].name}
</h2>
<div className="space-y-3 mb-6 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<HiCalendar className="w-4 h-4" />
<span>{formatDate(sortedEvents[0].start_date)}</span>
</div>
<div className="flex items-center gap-2">
<HiClock className="w-4 h-4" />
<span>
{formatTime(sortedEvents[0].start_date)} -{' '}
{formatTime(sortedEvents[0].end_date)} WIB
</span>
</div>
<div className="flex items-center gap-2">
<HiLocationMarker className="w-4 h-4" />
<span>
{sortedEvents[0].type === 'online'
? 'Online'
: sortedEvents[0].location}
</span>
</div>
{sortedEvents[0].price > 0 && (
<div className="mt-1 font-medium">
Rp {sortedEvents[0].price.toLocaleString('id-ID')}
</div>
)}
</div>
<p className="text-muted-foreground mb-6 line-clamp-4">
{sortedEvents[0].description}
</p>
<a
href={sortedEvents[0].detail_link}
target="_blank"
className={cn(
buttonVariants({ variant: 'bordered' }),
'mt-auto w-full md:w-fit'
)}
>
Lihat Detail
</a>
</div>
</div>
</div>
</div>
{/* Regular Cards */}
{sortedEvents.slice(1).map((event) => (
<div
key={event.name}
className="rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200 bg-card"
>
<div className="h-48 bg-muted relative">
<Image
src={event.thumbnail}
alt={event.name}
fill
className="object-cover object-top rounded-t-xl"
sizes="(max-width: 768px) 100vw, 33vw"
unoptimized
/>
</div>
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<span
className={cn(
'px-2 py-1 rounded-full text-xs',
getEventStatus(event.end_date) === 'upcoming'
? 'bg-primary/20 text-primary'
: 'bg-muted text-muted-foreground'
)}
>
{getEventStatus(event.end_date) === 'upcoming'
? 'Upcoming'
: 'Selesai'}
</span>
</div>
<h3 className="text-lg font-semibold mb-3 text-foreground">
{event.name}
</h3>
<div className="space-y-2 mb-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<HiCalendar className="w-4 h-4" />
<span>{formatDate(event.start_date)}</span>
</div>
<div className="flex items-center gap-2">
<HiLocationMarker className="w-4 h-4" />
<span>
{event.type === 'online' ? 'Online' : event.location}
</span>
</div>
{event.price > 0 && (
<div className="font-medium">
Rp {event.price.toLocaleString('id-ID')}
</div>
)}
</div>
<p className="text-muted-foreground text-sm mb-4 line-clamp-3">
{event.description}
</p>
<a
href={event.detail_link}
target="_blank"
className={cn(
buttonVariants({ variant: 'bordered' }),
'w-full text-sm'
)}
>
Lihat Detail
</a>
</div>
</div>
))}
</div>
</section>
);
}
+12
View File
@@ -0,0 +1,12 @@
import Footer from './_components/footer';
import { Header } from './_components/header';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
);
}
@@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}
@@ -0,0 +1,56 @@
import TESTIMONIALS from '@/data/testimonials.json';
import { Button } from '@components';
import Image from 'next/image';
import Link from 'next/link';
import { BsChatLeftQuote } from 'react-icons/bs';
import { FaQuoteLeft } from 'react-icons/fa';
export default function Page() {
return (
<div className="min-h-screen">
<div className="py-16 px-4 text-center border-b border-border">
<BsChatLeftQuote className="size-14 mx-auto my-4 text-foreground" />
<h1 className="text-4xl font-bold mb-4 text-foreground">Testimonial</h1>
<p className="max-w-2xl mx-auto text-lg mb-8 text-balance text-muted-foreground">
Menampilkan pengalaman dan cerita para member yang telah join ke dalam
komunitas kami
</p>
<Link href="/testimonials/submit">
<Button>Tulis Testimonialmu</Button>
</Link>
</div>
<div className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 my-16 container">
{TESTIMONIALS.map((testimonial) => (
<div
key={testimonial.id}
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>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}