Compare commits

...
Author SHA1 Message Date
arraysid 5aa66540ba feat: add feature request navigation 2025-05-23 01:20:29 +07:00
arraysid 01da8339f4 feat: implement signup and verification basic functionality with forms and actions 2025-05-23 01:15:12 +07:00
arraysid e13e5b4de2 feat: implement events and testimonials base pages 2025-05-23 01:14:49 +07:00
arraysid 08b4c39c03 feat: update form label and message styles & add select component 2025-05-23 01:14:10 +07:00
arraysid 610a1b7556 feat: create fetchPostSignin function for handling signin API requests 2025-05-20 10:02:18 +07:00
arraysid f5f715175c feat: restructure signin functionality with new hooks and improved validation 2025-05-20 10:00:52 +07:00
arraysid 1ddb2f72c6 feat: refactor signin action and form to use validation schema 2025-05-20 04:10:29 +07:00
arraysid b04f687199 feat: add signin page with functionality 2025-05-19 19:01:55 +07:00
arraysid 50cc24e46f feat: implement cookie management for access and refresh tokens 2025-05-19 15:02:02 +07:00
arraysid 4c7bb18a4f feat: replace ThemeProvider with Providers component 2025-05-19 14:10:11 +07:00
arraysid 0bc7d084d5 feat: change all public page to public dir 2025-05-19 13:39:03 +07:00
arraysid 795bcb6ab1 feat: add open api client and types for API integration 2025-05-19 13:33:09 +07:00
arraysid 2de5fd56d8 feat: add signin and signup page 2025-05-19 13:32:40 +07:00
arraysid d078999eec feat: add basic events section 2025-05-19 12:45:52 +07:00
arraysid a8dd867e1a feat: update button text in testimonials component for clarity 2025-05-17 15:37:40 +07:00
arraysid ff65424ff9 feat: add CallToAction component to encourage community engagement 2025-05-17 15:31:00 +07:00
arraysid 2fc625e454 feat: add testimonials component to showcase user feedback 2025-05-17 15:18:48 +07:00
arraysid d1bc9e10d7 feat: enhance communities component with improved layout and updated content 2025-05-17 11:41:39 +07:00
arraysid 017b5309a5 feat: add communites basic component to enhance community engagement section 2025-05-17 11:14:51 +07:00
arraysid e5844327a5 feat: remove unused images 2025-05-17 10:41:22 +07:00
arraysid 7874800d5e feat: add herostat component 2025-05-17 10:35:40 +07:00
arraysid 32d482ba4c chore: move unused components to deprecated dir 2025-05-17 09:17:33 +07:00
arraysid 502e45b4ff feat: reforming hero section 2025-05-17 08:46:07 +07:00
arraysid 7411fd2a13 chore: update import and tsconfig 2025-05-17 05:42:47 +07:00
arraysid 9df5bf8bbf fix: refactor homepage into dedicated home directory 2025-05-17 05:19:18 +07:00
arraysid 9548272340 feat: implement poppins font 2025-05-17 05:12:27 +07:00
arraysid d1abc3c7d4 feat: make header components atomic & remove unused menu 2025-05-17 03:52:41 +07:00
RasyidandGitHub 5618a8e99b fix: hero section responsiveness (#28)
* fix: hero section responsiveness

* fix: adjust max-width for hero section on smaller screens

* fix: increase title font size in hero section
2025-05-17 00:58:23 +07:00
598bd22b5a feat: make content data dynamic from cms and fix button and badge ui (#27)
* feat: integrate payload cms to landing page

* feat: add payload configuration and update TypeScript target

* fix: update TypeScript paths for payload configuration

* refactor: update config import paths to use local payload.config

* fix: update quotation marks for testimonials to use HTML entities

* fix: include withPayload in Next.js plugins for proper configuration

* feat: implement HeroSection global configuration and integrate with Hero component

* feat: add FeaturesSection global configuration and integrate with Features component

* feat: add revalidate constant to control page revalidation interval

* refactor: optimize data fetching by using Promise.all

* fix: update revalidation interval to 10 seconds

* feat: add CommunitiesSection configuration and integrate with Community component

* refactor: move global section functions to services directory

* feat: add support for s3 bucket for media

* feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration

* feat: replace Link component with Button for improved button styling in Community component

* feat: update FeaturesSection to use Iconify icons and adjust icon properties

* feat: add heroImage field to HeroSection and implement image formatting utility

* feat: add Learning Resources section with dynamic data fetching and UI integration

* feat: enhance LearningResourcesSection with primary and secondary button links

* refactor: reorganize layout structure by moving Header and Footer components to RootLayout

* feat: implement Testimonials section with dynamic data fetching and UI integration

* feat: simplify joinTitle rendering in Testimonials section

* feat: implement CallToAction section with dynamic data fetching and UI integration

* feat: update CallToActionSection label and wrap Discord button in link

* feat: update button components to use onClick for external links and improve styling

* feat: wrap logo image in a link to navigate to the homepage

* fix: supress scss deps warning

* fix: add placeholder to .env.example

* chore: add environment placeholder variables for CMS configuration in test build workflow

* fix: remove unused environment variables from test build workflow

* fix: text color (#26)

* chore: update action

* feat: integrate payload cms to landing page

* feat: add payload configuration and update TypeScript target

* fix: update TypeScript paths for payload configuration

* refactor: update config import paths to use local payload.config

* fix: include withPayload in Next.js plugins for proper configuration

* feat: implement HeroSection global configuration and integrate with Hero component

* feat: add FeaturesSection global configuration and integrate with Features component

* feat: add revalidate constant to control page revalidation interval

* refactor: optimize data fetching by using Promise.all

* fix: update revalidation interval to 10 seconds

* feat: add CommunitiesSection configuration and integrate with Community component

* refactor: move global section functions to services directory

* feat: add support for s3 bucket for media

* feat: update Community component to use Iconify for icons and adjust CommunitiesSection configuration

* feat: replace Link component with Button for improved button styling in Community component

* feat: update FeaturesSection to use Iconify icons and adjust icon properties

* feat: add heroImage field to HeroSection and implement image formatting utility

* feat: add Learning Resources section with dynamic data fetching and UI integration

* feat: enhance LearningResourcesSection with primary and secondary button links

* refactor: reorganize layout structure by moving Header and Footer components to RootLayout

* feat: implement Testimonials section with dynamic data fetching and UI integration

* feat: simplify joinTitle rendering in Testimonials section

* feat: implement CallToAction section with dynamic data fetching and UI integration

* feat: update CallToActionSection label and wrap Discord button in link

* feat: update button components to use onClick for external links and improve styling

* feat: wrap logo image in a link to navigate to the homepage

* fix: supress scss deps warning

* fix: add placeholder to .env.example

* chore: add environment placeholder variables for CMS configuration in test build workflow

* fix: remove unused environment variables from test build workflow

* fix: remove mistakenly added env from test-build workflow

---------

Co-authored-by: Fahim Ardani <fahimardani25@gmail.com>
Co-authored-by: Maulana Sodiqin <53475078+maulanasdqn@users.noreply.github.com>
2025-05-11 21:55:20 +07:00
119 changed files with 9131 additions and 979 deletions
+12 -2
View File
@@ -1,4 +1,14 @@
# API Endpoint
VITE_API_URL=
CMS_SECRET=
CMS_POSTGRES_URL=
# Payload CMS Secret Key (minimum 32 characters)
CMS_SECRET=your-very-long-secret-key
# PostgreSQL Connection String
CMS_POSTGRES_URL=postgres://user:password@host:port/database
# S3 Storage Configuration
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
CMS_STORAGE_REGION=ap-southeast-1
+6
View File
@@ -62,3 +62,9 @@ storybook-static
# Next.js
.next
out
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
+8 -13
View File
@@ -1,20 +1,15 @@
//@ts-check
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { composePlugins, withNx } = require('@nx/next');
const { withPayload } = require('@payloadcms/next/withPayload');
/**
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
**/
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
const nextConfig = {
nx: {
// Set this to true if you would like to to use SVGR
// See: https://github.com/gregberge/svgr
svgr: false,
nx: { svgr: false },
sassOptions: {
quietDeps: true,
logger: {
warn: () => {},
},
},
};
const plugins = [withNx, withPayload];
module.exports = composePlugins(...plugins)(nextConfig);
module.exports = composePlugins(withNx, withPayload)(nextConfig);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,86 @@
'use client';
import { motion } from 'framer-motion';
import { useEffect, useMemo, useState } from 'react';
import {
SiCplusplus,
SiCss3,
SiGo,
SiHtml5,
SiJavascript,
SiPhp,
SiPython,
SiRuby,
SiRust,
SiSwift,
SiTypescript,
} from 'react-icons/si';
export function AnimatedBackground() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const iconColorMap = useMemo(
() => [
{ Icon: SiJavascript, color: '#F7DF1E' },
{ Icon: SiTypescript, color: '#3178C6' },
{ Icon: SiPython, color: '#3776AB' },
{ Icon: SiCplusplus, color: '#00599C' },
{ Icon: SiRuby, color: '#CC342D' },
{ Icon: SiSwift, color: '#F05138' },
{ Icon: SiRust, color: '#000000' },
{ Icon: SiGo, color: '#00ADD8' },
{ Icon: SiPhp, color: '#777BB4' },
{ Icon: SiHtml5, color: '#E34F26' },
{ Icon: SiCss3, color: '#1572B6' },
],
[]
);
const getRandom = (min: number, max: number) =>
Math.random() * (max - min) + min;
const floatingIcons = useMemo(() => {
if (!isClient) return [];
return Array.from({ length: 40 }).map((_, i) => {
const { Icon, color } = iconColorMap[i % iconColorMap.length];
return (
<motion.div
key={i}
className="pointer-events-none absolute"
style={{
top: `${getRandom(0, 100)}%`,
left: `${getRandom(0, 100)}%`,
fontSize: `${getRandom(16, 32)}px`,
color: color,
opacity: getRandom(0.1, 0.2),
rotate: getRandom(-180, 180),
}}
animate={{
y: [0, getRandom(-100, 100), 0],
x: [0, getRandom(-50, 50), 0],
rotate: getRandom(-180, 180),
}}
transition={{
duration: getRandom(15, 25),
repeat: Infinity,
repeatType: 'loop',
ease: 'easeInOut',
}}
>
<Icon className="h-full w-full" />
</motion.div>
);
});
}, [isClient, iconColorMap]);
if (!isClient) return null;
return (
<div className="fixed inset-0 z-0 overflow-hidden">{floatingIcons}</div>
);
}
@@ -0,0 +1,19 @@
import { Card } from '@components/atoms';
import { ReactNode } from 'react';
import { AnimatedBackground } from './_components/animated-background';
export default function Layout({ children }: { children: ReactNode }) {
return (
<div className="bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4">
<AnimatedBackground />
<div className="z-10 w-full">
<Card className="bg-background/90 mx-auto w-full max-w-[360px] p-6 shadow-xl backdrop-blur-lg sm:max-w-md sm:p-8">
<div className="flex flex-col items-center space-y-6">
<div className="w-full space-y-4">{children}</div>
</div>
</Card>
</div>
</div>
);
}
@@ -0,0 +1,24 @@
'use server';
import { setAccessToken, setRefreshToken } from '@/lib/cookies';
import { fetchPostSignin } from '../_http/fetch-post-signin';
import {
type SignInValidationType,
signInValidationSchema,
} from '../_validation/signin-validation';
export async function SigninAction(request: SignInValidationType) {
console.log(request);
const validRequest = signInValidationSchema.parse(request);
const { data } = await fetchPostSignin(validRequest);
const accessToken = data.token.access_token;
const refreshToken = data.token.refresh_token;
await setAccessToken(accessToken);
await setRefreshToken(refreshToken);
return data;
}
@@ -0,0 +1,73 @@
'use client';
import {
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from '@components/atoms';
import { LuLoader } from 'react-icons/lu';
import { useFormSignin } from '../_hooks/use-form-signin';
import { usePostSignin } from '../_hooks/use-post-signin';
import { type SignInValidationType } from '../_validation/signin-validation';
export function SigninForm() {
const form = useFormSignin();
const { mutate, isPending, error } = usePostSignin(form);
const onSubmit = (values: SignInValidationType) => {
mutate(values);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{error && (
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
{(error as Error).message}
</div>
)}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="emailmu@mail.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={isPending}
className="w-full hover:bg-[#5fbaef] bg-[#22a5f1] font-bold"
>
{isPending ? <LuLoader className="h-5 w-5 animate-spin" /> : 'Masuk'}
</Button>
</form>
</Form>
);
}
@@ -0,0 +1,16 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import {
signInValidationSchema,
type SignInValidationType,
} from '../_validation/signin-validation';
export function useFormSignin() {
return useForm<SignInValidationType>({
resolver: zodResolver(signInValidationSchema),
defaultValues: {
email: '',
password: '',
},
});
}
@@ -0,0 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { SigninAction } from '../_actions/signin-action';
import { useFormSignin } from './use-form-signin';
export function usePostSignin(form: ReturnType<typeof useFormSignin>) {
const router = useRouter();
return useMutation({
mutationFn: SigninAction,
onSuccess: () => {
router.push('/');
},
onError: () => {
form.resetField('password');
},
});
}
@@ -0,0 +1,24 @@
import { fetcher } from '@/lib/openapi';
import { SignInValidationType } from '../_validation/signin-validation';
export async function fetchPostSignin({
email,
password,
}: SignInValidationType) {
const { data, error, response } = await fetcher.POST('/v1/auth/login', {
body: {
email,
password,
},
});
if (error) {
throw new Error(error.message);
}
if (!response.ok) {
throw new Error('Someting went wrong, please try again later');
}
return data;
}
@@ -0,0 +1,18 @@
import { z } from 'zod';
export const signInValidationSchema = z.object({
email: z
.string({
required_error: 'Email tidak boleh kosong',
invalid_type_error: 'Email harus berupa string',
})
.min(1, 'Email tidak boleh kosong')
.email('Email harus valid'),
password: z
.string({
required_error: 'Password tidak boleh kosong',
invalid_type_error: 'Password harus berupa string',
})
.min(1, 'Password tidak boleh kosong'),
});
export type SignInValidationType = z.infer<typeof signInValidationSchema>;
@@ -0,0 +1,38 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { LogoSimple } from '../../_components/logo';
import { SigninForm } from './_components/signin-form';
export const metadata: Metadata = {
title: 'IMPHNEN - Signin',
description:
'Komunitas Ingin Menjadi Programmer Handal Namung Enggan Ngonding',
};
export default function Page() {
return (
<>
<div className="flex flex-col items-center gap-4">
<LogoSimple />
<p className="text-muted-foreground text-center text-sm sm:text-base">
Masuk untuk mengakses akunmu
</p>
</div>
<SigninForm />
<div className="w-full space-y-4">
<p className="text-muted-foreground px-4 text-center text-xs leading-5 text-balance sm:text-sm">
Belum punya akun?{' '}
<Link
href="/signup"
className="text-[#22a5f1] font-bold hover:underline hover:underline-offset-4 transition-colors"
>
Daftar
</Link>
</p>
</div>
</>
);
}
@@ -0,0 +1,15 @@
'use server';
import { z } from 'zod';
import { fetchPostSignin } from '../_http/fetch-post-signup';
import { signupValidationSchema } from '../_validation/signup-validation';
export async function SignupAction(
request: z.infer<typeof signupValidationSchema>
) {
const validRequest = signupValidationSchema.parse(request);
const data = await fetchPostSignin({ ...validRequest });
return data;
}
@@ -0,0 +1,267 @@
'use client';
import {
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from '@components/atoms';
import { zodResolver } from '@hookform/resolvers/zod';
import { Turnstile } from '@marsidev/react-turnstile';
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { LuLoader } from 'react-icons/lu';
import { z } from 'zod';
import { SignupAction } from '../_actions/signup-action';
import {
signupValidationSchema,
stepOneSignupValidationSchema,
stepTwoSignupValidationSchema,
} from '../_validation/signup-validation';
export function SignupForm() {
const router = useRouter();
const [step, setStep] = useState(1);
const [stepOneData, setStepOneData] = useState<z.infer<
typeof stepOneSignupValidationSchema
> | null>(null);
const { mutate, isPending, error } = useMutation({
mutationFn: async (data: z.infer<typeof signupValidationSchema>) => {
const result = await SignupAction(data);
return {
...result,
email: data.email,
};
},
onSuccess: ({ email }) => {
router.push(`/verification?ref=${email}`);
},
onError: () => {
secondForm.reset();
},
});
const firstForm = useForm<z.infer<typeof stepOneSignupValidationSchema>>({
resolver: zodResolver(stepOneSignupValidationSchema),
defaultValues: {
email: '',
fullname: '',
password: '',
confirm_password: '',
},
});
const secondForm = useForm<z.infer<typeof stepTwoSignupValidationSchema>>({
resolver: zodResolver(stepTwoSignupValidationSchema),
defaultValues: {
phone_number: '',
referral_code: '',
referred_by: '',
student_type: '',
token: '',
},
});
const handleFirstSubmit = (
values: z.infer<typeof stepOneSignupValidationSchema>
) => {
setStepOneData(values);
setStep(2);
};
const handleSecondSubmit = (
values: z.infer<typeof stepTwoSignupValidationSchema>
) => {
if (stepOneData) {
mutate({ ...stepOneData, ...values });
}
};
return (
<>
{step === 1 && (
<Form {...firstForm}>
<form
onSubmit={firstForm.handleSubmit(handleFirstSubmit)}
className="space-y-4"
>
<FormField
control={firstForm.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="Nama Lengkap" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={firstForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="emailmu@mail.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={firstForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={firstForm.control}
name="confirm_password"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full bg-[#5fbaef] hover:bg-[#22a5f1]"
>
Selanjutnya
</Button>
</form>
</Form>
)}
{step === 2 && (
<Form {...secondForm}>
<form
onSubmit={secondForm.handleSubmit(handleSecondSubmit)}
className="space-y-4"
>
{error && (
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
{(error as Error).message}
</div>
)}
<FormField
control={secondForm.control}
name="phone_number"
render={({ field }) => (
<FormItem>
<FormLabel>Nomor Telepon</FormLabel>
<FormControl>
<Input placeholder="081234567890" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-x-4">
<FormField
control={secondForm.control}
name="referral_code"
render={({ field }) => (
<FormItem>
<FormLabel>Kode Referral</FormLabel>
<FormControl>
<Input placeholder="ABCD" maxLength={4} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={secondForm.control}
name="referred_by"
render={({ field }) => (
<FormItem>
<FormLabel>Referrer</FormLabel>
<FormControl>
<Input placeholder="Referral ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={secondForm.control}
name="student_type"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<FormControl>
<Input placeholder="e.g., Undergraduate" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Turnstile
siteKey="1x00000000000000000000AA"
onSuccess={(token) => secondForm.setValue('token', token)}
options={{
theme: 'light',
size: 'flexible',
language: 'id',
}}
/>
<div className="flex justify-between">
<Button
type="button"
variant="secondary"
onClick={() => setStep(1)}
>
Kembali
</Button>
<Button
type="submit"
disabled={isPending || !secondForm.watch('token')}
className="flex items-center gap-2 hover:bg-[#5fbaef] bg-[#22a5f1]"
>
{isPending ? (
<LuLoader className="h-5 w-5 animate-spin" />
) : (
'Daftar'
)}
</Button>
</div>
</form>
</Form>
)}
</>
);
}
@@ -0,0 +1,34 @@
import { fetcher } from '@/lib/openapi';
import { SignupValidationSchema } from '../_validation/signup-validation';
export async function fetchPostSignin({
email,
password,
fullname,
phone_number,
student_type,
referral_code,
referred_by,
}: SignupValidationSchema) {
const { data, error, response } = await fetcher.POST('/v1/auth/register', {
body: {
email,
password,
fullname,
phone_number,
student_type,
referral_code,
referred_by,
},
});
if (error) {
throw new Error(error.message);
}
if (!response.ok) {
throw new Error('Someting went wrong, please try again later');
}
return data;
}
@@ -0,0 +1,76 @@
import { z } from 'zod';
export const stepOneSignupValidationSchema = z
.object({
email: z
.string({
required_error: 'Email tidak boleh kosong',
invalid_type_error: 'Email harus berupa string',
})
.min(1, 'Email tidak boleh kosong')
.email('Email harus valid'),
fullname: z
.string({
required_error: 'Nama tidak boleh kosong',
invalid_type_error: 'Nama harus berupa string',
})
.min(1, 'Nama tidak boleh kosong')
.max(50, 'Nama tidak boleh lebih dari 50 karakter'),
password: z
.string({
required_error: 'Password tidak boleh kosong',
invalid_type_error: 'Password harus berupa string',
})
.min(1, 'Password tidak boleh kosong')
.min(8, 'Password harus lebih dari 8 karakter')
.max(50, 'Password tidak boleh lebih dari 50 karakter')
.regex(
/(?=.*[A-Z])/,
'Password harus mengandung setidaknya satu huruf kapital'
)
.regex(
/(?=.*[a-z])/,
'Password harus mengandung setidaknya satu huruf kecil'
)
.regex(/(?=.*\d)/, 'Password harus mengandung setidaknya satu angka')
.regex(
/(?=.*[!@#$%^&*()_\-+={}[\]|\\:;"'<>,.?/~`])/,
'Password harus mengandung setidaknya satu karakter spesial'
),
confirm_password: z
.string({
required_error: 'Konfirmasi password tidak boleh kosong',
})
.min(1, 'Konfirmasi password tidak boleh kosong')
.max(50, 'Konfirmasi password tidak boleh lebih dari 50 karakter'),
})
.refine((data) => data.password === data.confirm_password, {
message: 'Password dan Konfirmasi Password harus sama',
path: ['confirm_password'],
});
export const stepTwoSignupValidationSchema = z.object({
phone_number: z
.string({
required_error: 'Nomor telepon tidak boleh kosong',
invalid_type_error: 'Nomor telepon harus berupa string',
})
.min(10, 'Nomor telepon tidak boleh kurang dari 10 karakter')
.max(15, 'Nomor telepon tidak boleh lebih dari 15 karakter')
.regex(/^\d+$/, 'Nomor telepon hanya boleh berisi angka'),
referral_code: z
.string()
.max(4, 'Kode referral tidak boleh lebih dari 4 karakter')
.optional(),
referred_by: z
.string()
.max(50, 'Kode referral tidak boleh lebih dari 50 karakter')
.optional(),
student_type: z.string(),
token: z.string(),
});
export const signupValidationSchema = stepOneSignupValidationSchema.and(
stepTwoSignupValidationSchema
);
export type SignupValidationSchema = z.infer<typeof signupValidationSchema>
@@ -0,0 +1,18 @@
import { LogoSimple } from '../../_components/logo';
import { SignupForm } from './_components/signup-form';
export default function Page() {
return (
<>
<div className="flex flex-col items-center gap-4">
<LogoSimple />
<p className="text-muted-foreground text-center text-sm sm:text-base">
Buat akunmu sekarang
</p>
</div>
<SignupForm />
</>
);
}
@@ -0,0 +1,15 @@
'use server';
import { fetcher } from '@/lib/openapi';
export async function resendOTPAction(email: string) {
const data = await fetcher.POST('/v1/auth/send-otp', {
body: {
email,
},
});
console.log(data);
return data;
}
@@ -0,0 +1,15 @@
'use server';
import { fetchPostVerifyEmail } from '../_http/fetch-post-verify-email';
import {
type VerifyEmailValidationType,
verifyEmailSchema,
} from '../_validation/verify-email-validation';
export async function verifyEmailAction(request: VerifyEmailValidationType) {
const validRequest = verifyEmailSchema.parse(request);
const data = await fetchPostVerifyEmail(validRequest);
return data;
}
@@ -0,0 +1,16 @@
'use client';
import { Button } from '@components/atoms';
import { usePostResendOTP } from '../_hooks/use-post-resend-otp';
export function ButtonResendOTP({ email }: { email: string }) {
const { mutate } = usePostResendOTP();
const handleResendOTP = () => mutate(email);
return (
<Button onClick={handleResendOTP} className="w-full -mt-2">
Kirim Ulang Kode OTP
</Button>
);
}
@@ -0,0 +1,80 @@
'use client';
import {
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from '@components/atoms';
import { LuLoader } from 'react-icons/lu';
import { useFormVerifyEmail } from '../_hooks/use-form-verify-email';
import { usePostVerifyEmail } from '../_hooks/use-post-verify-email';
import { type VerifyEmailValidationType } from '../_validation/verify-email-validation';
import { ButtonResendOTP } from './button-resend-otp';
export function VerifyEmailForm() {
const form = useFormVerifyEmail();
const { mutate, isPending, error } = usePostVerifyEmail(form);
const onSubmit = (values: VerifyEmailValidationType) => {
mutate(values);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{error && (
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
{(error as Error).message}
</div>
)}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="emailmu@mail.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="otp"
render={({ field }) => (
<FormItem>
<FormLabel>OTP</FormLabel>
<FormControl>
<Input placeholder="123456" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={isPending}
className="w-full hover:bg-[#5fbaef] bg-[#22a5f1] font-bold"
>
{isPending ? (
<LuLoader className="h-5 w-5 animate-spin" />
) : (
'Verifikasi'
)}
</Button>
</form>
<ButtonResendOTP email={form.watch('email')} />
</Form>
);
}
@@ -0,0 +1,20 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useSearchParams } from 'next/navigation';
import { useForm } from 'react-hook-form';
import {
verifyEmailSchema,
type VerifyEmailValidationType,
} from '../_validation/verify-email-validation';
export function useFormVerifyEmail() {
const searchParams = useSearchParams();
const email = searchParams.get('ref');
return useForm<VerifyEmailValidationType>({
resolver: zodResolver(verifyEmailSchema),
defaultValues: {
email: email ?? '',
otp: '',
},
});
}
@@ -0,0 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { resendOTPAction } from '../_actions/resend-otp-action';
export function usePostResendOTP() {
const router = useRouter();
return useMutation({
mutationFn: resendOTPAction,
onSuccess: () => router.replace('/verification?success=true'),
});
}
@@ -0,0 +1,20 @@
import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { verifyEmailAction } from '../_actions/verify-email-action';
import { useFormVerifyEmail } from './use-form-verify-email';
export function usePostVerifyEmail(
form: ReturnType<typeof useFormVerifyEmail>
) {
const router = useRouter();
return useMutation({
mutationFn: verifyEmailAction,
onSuccess: () => {
router.push('/feedback');
},
onError: () => {
form.resetField('otp');
},
});
}
@@ -0,0 +1,31 @@
import { fetcher } from '@/lib/openapi';
import { VerifyEmailValidationType } from '../_validation/verify-email-validation';
export async function fetchPostVerifyEmail({
email,
otp,
}: VerifyEmailValidationType) {
const formattedOTP = parseInt(otp);
const { data, error, response } = await fetcher.POST(
'/v1/auth/verify-email',
{
body: {
email,
otp: formattedOTP,
},
}
);
if (error) {
throw new Error(error.message);
}
if (!response.ok) {
throw new Error('Something went wrong, please try again later');
}
console.log(data);
return data;
}
@@ -0,0 +1,10 @@
import { z } from 'zod';
export const verifyEmailSchema = z.object({
email: z.string().email({ message: 'Format email tidak valid' }),
otp: z
.string()
.min(6, { message: 'OTP harus terdiri dari 6 digit' })
.max(6, { message: 'OTP harus terdiri dari 6 digit' }),
});
export type VerifyEmailValidationType = z.infer<typeof verifyEmailSchema>;
@@ -0,0 +1,18 @@
import { LogoSimple } from '../../_components/logo';
import { VerifyEmailForm } from './_components/verify-email-form';
export default function Page() {
return (
<>
<div className="flex flex-col items-center gap-4">
<LogoSimple />
<p className="text-muted-foreground text-center text-sm sm:text-base">
Verifikasi akun mu sekarang
</p>
</div>
<VerifyEmailForm />
</>
);
}
@@ -1,10 +1,21 @@
'use client';
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { CallToActionSection } from '@/payload-types';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function CallToAction() {
export function CallToAction(props: CallToActionSection) {
const {
title,
highlightedTitle,
subtitle,
primaryButtonLabel,
primaryButtonLink,
secondaryButtonLabel,
secondaryButtonLink,
} = props;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
@@ -28,30 +39,30 @@ export function CallToAction() {
<div className="relative z-10 text-center">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6">
Siap Menjadi{' '}
{title}{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Programmer Handal?
{highlightedTitle}
</span>
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai
perjalanan programming mu dengan cara yang menyenangkan!
{subtitle}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="lg"
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90
font-bold text-black hover:text-white cursor-pointer"
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer"
onClick={() => window.open(primaryButtonLink, '_blank')}
>
Gabung Discord
{primaryButtonLabel}
</Button>
<Button
size="lg"
variant="outline"
className="border-primary hover:bg-primary/10"
onClick={() => window.open(secondaryButtonLink, '_blank')}
>
Join Facebook Group
{secondaryButtonLabel}
</Button>
</div>
</div>
@@ -0,0 +1,110 @@
'use client';
import { CommunitiesSection } from '@/payload-types';
import { Button } from '@components/atoms';
import { Icon } from '@iconify/react';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Community(props: CommunitiesSection) {
const { items, stats } = props;
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,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5 },
},
};
return (
<section
id="community"
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 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>
<div className="container px-4 md:px-6" ref={ref}>
<motion.div
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}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{items?.map((c, i) => (
<motion.div
key={i}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-xl"
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="relative z-10">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<Icon
icon={c.iconName}
className="h-6 w-6 text-blue-600 dark:text-blue-400"
/>
</div>
<h3 className="mb-2 text-xl font-bold">{c.title}</h3>
<p className="mb-6 text-muted-foreground">{c.description}</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300"
onClick={() => window.open(c.buttonLink, '_blank')}
>
{c.buttonText}
</Button>
</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" />
</motion.div>
))}
</motion.div>
<motion.div
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }}
>
{stats?.map((s, i) => (
<div key={i} className="space-y-2">
<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>
))}
</motion.div>
</div>
</section>
);
}
@@ -1,45 +1,16 @@
'use client';
import {
BookOpenIcon,
CodeIcon,
LaptopIcon,
UsersIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { FeaturesSection } from '@/payload-types';
import { Icon } from '@iconify/react';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Features() {
export function Features(props: FeaturesSection) {
const { heading, subheading, features } = props;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const features = [
{
icon: <LaptopIcon className="h-6 w-6 text-primary" />,
title: 'Belajar Tanpa Koding',
description:
'Pelajari konsep programming dengan cara yang mudah dipahami tanpa harus menulis kode yang rumit.',
},
{
icon: <UsersIcon className="h-6 w-6 text-primary" />,
title: 'Komunitas Supportif',
description:
'Bergabunglah dengan komunitas programmer Indonesia yang siap membantu dan berbagi pengalaman.',
},
{
icon: <BookOpenIcon className="h-6 w-6 text-primary" />,
title: 'Tutorial Interaktif',
description:
'Akses tutorial interaktif yang membuat konsep programming lebih mudah untuk dipahami.',
},
{
icon: <CodeIcon className="h-6 w-6 text-primary" />,
title: 'Proyek Praktis',
description:
'Terapkan pengetahuan Anda dalam proyek nyata dengan panduan langkah demi langkah.',
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
@@ -64,7 +35,6 @@ export function Features() {
id="fitur"
className="w-full py-20 md:py-32 relative overflow-hidden"
>
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-muted/50" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full bg-gradient-to-tr from-primary/5 to-blue-400/5 blur-3xl" />
@@ -82,14 +52,10 @@ export function Features() {
Fitur Unggulan
</div>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
Belajar programming dengan{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
cara yang lebih baik
</span>
{heading}
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
IMPHNEN hadir dengan berbagai fitur untuk membantu kamu menjadi
programmer handal tanpa harus pusing dengan coding.
{subheading}
</p>
</motion.div>
</div>
@@ -100,7 +66,7 @@ export function Features() {
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{features.map((feature, index) => (
{features?.map((feature, index) => (
<motion.div
key={index}
className="group relative overflow-hidden rounded-xl border bg-background/50 backdrop-blur-sm p-6 transition-all hover:shadow-md hover:shadow-primary/5 hover:border-primary/50"
@@ -110,7 +76,10 @@ export function Features() {
<div className="relative z-10">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors">
{feature.icon}
<Icon
icon={feature.iconName}
className="h-6 w-6 text-primary"
/>
</div>
<h3 className="mb-2 text-xl font-bold">{feature.title}</h3>
<p className="text-muted-foreground">{feature.description}</p>
@@ -1,65 +1,17 @@
'use client';
import {
BookOpenIcon,
Button,
CodeIcon,
Share2Icon,
VideoIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { LearningResourcesSection } from '@/payload-types';
import { Button } from '@components/atoms';
import { Icon } from '@iconify/react';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function LearningResources() {
export function LearningResources(props: LearningResourcesSection) {
const { resources, featured, title, description } = props;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const resources = [
{
icon: <VideoIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Video Tutorial',
description:
'Belajar melalui tutorial video dari langkah awal hingga mahir.',
buttonText: 'Lihat Semua Video',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
{
icon: (
<BookOpenIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Artikel & Tutorial',
description:
'Pelajari konsep programming melalui artikel yang disusun secara terstruktur.',
buttonText: 'Baca Artikel',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
{
icon: <CodeIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Tantangan Koding',
description:
'Uji kemampuan koding kamu dengan tantangan yang menyenangkan dan menantang.',
buttonText: 'Mulai Tantangan',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
{
icon: <Share2Icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Sharing Session',
description:
'Ikuti sesi berbagi pengalaman dari programmer berpengalaman dan belajar dari pengalaman mereka.',
buttonText: 'Jadwal Session',
buttonLink: '#',
color: 'from-blue-500 to-blue-700',
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
@@ -84,97 +36,96 @@ export function LearningResources() {
id="sumber-belajar"
className="w-full py-20 md:py-32 relative overflow-hidden"
>
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-blue-400/5 to-transparent blur-3xl" />
</div>
<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 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight lg:text-5xl">
Sumber{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Belajar
</span>
{title}
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
Akses berbagai materi belajar yang akan membantu kamu menguasai
konsep programming dengan cara yang menyenangkan.
{description}
</p>
</motion.div>
</div>
<motion.div
className="grid gap-8 md:grid-cols-2 lg:grid-cols-4"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{resources.map((resource, index) => (
{resources.map((r, i) => (
<motion.div
key={index}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
key={i}
className="group relative overflow-hidden rounded-xl border bg-background p-6 hover:shadow-lg"
variants={itemVariants}
>
<div className="absolute top-0 left-0 h-1 w-full bg-gradient-to-r from-primary/50 to-blue-400/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div
className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full ${resource.iconBg}`}
>
{resource.icon}
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<Icon
icon={r.icon}
className="h-6 w-6 text-blue-600 dark:text-blue-400"
/>
</div>
<h3 className="mb-2 text-xl font-bold">{resource.title}</h3>
<p className="mb-6 text-muted-foreground">
{resource.description}
</p>
<h3 className="mb-2 text-xl font-bold">{r.title}</h3>
<p className="mb-6 text-muted-foreground">{r.description}</p>
<Button
variant="link"
className="p-0 h-auto font-medium text-primary hover:text-primary/80"
onClick={() => window.open(r.buttonLink, '_blank')}
>
{resource.buttonText}
{r.buttonText}
<Icon icon="tabler:arrow-right" className="h-6 w-6" />
</Button>
</div>
<div className="absolute -bottom-32 -right-32 w-64 h-64 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-10 group-hover:-translate-x-10" />
</motion.div>
))}
</motion.div>
{/* Featured Resource */}
<motion.div
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm"
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 }}
>
<div className="grid md:grid-cols-2 gap-0">
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary mb-4">
Rekomendasi Terbaik
<div className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary mb-4 w-fit">
{featured.label}
</div>
<h3 className="text-2xl md:text-3xl font-bold mb-4">
Kursus Lengkap Web Development
{featured.title}
</h3>
<p className="text-muted-foreground mb-6">
Pelajari HTML, CSS, JavaScript, React, dan Node.js dalam satu
kursus komprehensif yang dirancang untuk pemula hingga tingkat
menengah.
{featured.description}
</p>
<div className="flex flex-wrap gap-4">
<Button className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90
font-bold text-black hover:text-white cursor-pointer">
Mulai Kursus
<Button
className="bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer"
onClick={() =>
window.open(featured.primaryButtonLink, '_blank')
}
>
{featured.primaryButtonText}
</Button>
<Button
variant="outline"
onClick={() =>
window.open(featured.secondaryButtonLink, '_blank')
}
>
{featured.secondaryButtonText}
</Button>
<Button variant="outline">Lihat Silabus</Button>
</div>
</div>
<div className="relative h-64 md:h-auto">
@@ -0,0 +1,137 @@
'use client';
import { formatCMSImageDataToMedia } from '@/lib/format';
import { TestimonialsSection } from '@/payload-types';
import { QuoteIcon } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import Image from 'next/image';
import { useRef } from 'react';
export function Testimonials(props: TestimonialsSection) {
const { title, subtitle, items, stats, joinTitle, joinText } = props;
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const containerVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1, transition: { duration: 0.5 } },
};
return (
<section
id="testimoni"
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
ref={ref}
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(59,130,246,0.1),transparent_50%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(96,165,250,0.1),transparent_50%)]" />
</div>
<div className="container px-4 md:px-6">
<motion.div
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl lg:text-5xl">
{title.split(' ').map((word, i) =>
i === title.split(' ').length - 1 ? (
<span
key={i}
className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400"
>
{word}
</span>
) : (
<span key={i}>{word} </span>
)
)}
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
{subtitle}
</p>
</motion.div>
<motion.div
className="grid gap-8 md:grid-cols-3"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{items?.map((t, idx) => (
<motion.div
key={idx}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
variants={itemVariants}
>
<div className="absolute top-6 right-6 text-primary/20 group-hover:text-primary/40 transition-colors">
<QuoteIcon className="h-8 w-8" />
</div>
<div className="relative z-10">
<p className="mb-6 text-muted-foreground italic">
&ldquo;{t.quote}&rdquo;
</p>
<div className="flex items-center gap-4">
<div className="relative h-12 w-12 overflow-hidden rounded-full border-2 border-primary/20">
{(() => {
const media = formatCMSImageDataToMedia(t.avatar);
if (!media) return null;
return (
<Image
src={media.url!}
alt={media.alt}
width={600}
height={500}
className="object-cover"
/>
);
})()}
</div>
<div>
<h4 className="font-bold">{t.name}</h4>
<p className="text-sm text-muted-foreground">{t.role}</p>
</div>
</div>
</div>
<div className="absolute -bottom-1 -left-1 w-20 h-20 bg-gradient-to-tr from-primary/10 to-transparent rounded-tr-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</motion.div>
))}
</motion.div>
<motion.div
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm p-8 md:p-12"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 }}
>
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 className="text-2xl md:text-3xl font-bold mb-4">
{joinTitle}
</h3>
<p className="text-muted-foreground">{joinText}</p>
</div>
<div className="grid grid-cols-2 gap-4">
{stats?.map((s, idx) => (
<div key={idx} className="rounded-lg border p-4 text-center">
<div className="text-3xl 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>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,71 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useRef } from 'react';
export function CallToAction() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const router = useRouter();
return (
<section className="w-full py-20 md:py-32 relative overflow-hidden">
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background to-primary/20" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.2),transparent_70%)]" />
</div>
<div className="container px-4 md:px-6" ref={ref}>
<motion.div
className="max-w-4xl mx-auto rounded-2xl overflow-hidden border shadow-lg"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<div className="relative p-8 md:p-12 lg:p-16 bg-background">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5" />
<div className="relative z-10 text-center">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6">
Siap Menjadi{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Programmer Handal?
</span>
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai
perjalanan programming mu dengan cara yang menyenangkan!
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
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')}
>
Gabung Komunitas
</Button>
<Button
size="lg"
variant="outline"
className="border-primary hover:bg-primary/10"
onClick={() => router.push('/events')}
>
Explore Event
</Button>
</div>
</div>
{/* Decorative Elements */}
<div className="absolute -top-12 -left-12 w-24 h-24 rounded-full bg-primary/10 blur-2xl" />
<div className="absolute -bottom-12 -right-12 w-24 h-24 rounded-full bg-blue-400/10 blur-2xl" />
</div>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,247 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import {
FaDiscord,
FaFacebook,
FaInstagram,
FaLinkedin,
FaTiktok,
} from 'react-icons/fa';
export function Communities() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const communities = [
{
icon: FaFacebook,
title: 'Facebook Group',
description:
'Komunitas aktif dengan 180K+ anggota berdiskusi seputar programming dan sharing meme',
buttonText: 'Gabung Sekarang',
buttonLink: 'https://facebook.com/groups/programmerhandal',
},
{
icon: FaDiscord,
title: 'Discord Server',
description:
'Diskusi real-time dengan developer berbagai level, mulai dari pemula sampai expert!',
buttonText: 'Join Server',
buttonLink: 'https://discord.com/invite/imphnen',
},
{
icon: FaInstagram,
title: 'Instagram',
description:
'Temukan visual tutorial coding & tech trends terkini setiap harinya',
buttonText: 'Follow Kami',
buttonLink: 'https://www.instagram.com/imphnen.dev',
},
{
icon: FaTiktok,
title: 'TikTok',
description:
'Tips coding singkat & trik development praktis dalam 60 detik',
buttonText: 'Follow Sekarang',
buttonLink: 'https://www.tiktok.com/@imphnen',
},
{
icon: FaLinkedin,
title: 'LinkedIn',
description:
'Bangun jaringan profesional dengan perusahaan teknologi ternama & recruiter IT',
buttonText: 'Segera Hadir',
buttonLink: '#',
},
];
const stats = [
{ value: '180K+', label: 'Komunitas Aktif' },
{ value: '25K+', label: 'Postingan/Bulan' },
{ value: '95%', label: 'Respon Cepat' },
{ value: '10K+', label: 'Problem Solved' },
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
};
const cardVariants = {
hidden: { y: 40, opacity: 0, scale: 0.95 },
visible: {
y: 0,
opacity: 1,
scale: 1,
transition: {
type: 'spring',
stiffness: 100,
damping: 20,
},
},
};
const statVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.1 + 0.5,
duration: 0.6,
},
}),
};
return (
<section
id="community"
className="w-full py-20 md:py-32 relative overflow-hidden container"
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-primary/5 to-transparent blur-3xl" />
</div>
<div className="w-full px-4 md:px-6 lg:px-0" ref={ref}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
>
<h2 className="text-4xl font-bold tracking-tighter md:text-5xl/tight lg:text-6xl text-center mb-6">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/80">
Komunitas Programmer
</span>{' '}
<br className="hidden md:block" />
Terbesar di Indonesia
</h2>
<p className="max-w-4xl mx-auto text-muted-foreground text-center md:text-xl text-balance">
Bergabung dengan jaringan developer profesional untuk berkolaborasi,
belajar, dan berkembang bersama komunitas yang aktif dan suportif
</p>
</motion.div>
{/* First Row - Facebook & Discord */}
<motion.div
className="grid gap-4 grid-cols-1 md:grid-cols-2 w-full mt-16"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{communities.slice(0, 2).map((community, index) => (
<motion.div
key={index}
variants={cardVariants}
className="group relative overflow-hidden rounded-2xl border bg-background p-8 hover:shadow-xl transition-shadow h-full"
whileHover={{
y: -5,
transition: { duration: 0.2 },
}}
>
<div className="absolute top-0 left-0 h-1.5 w-full bg-gradient-to-r from-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div className="mb-6 inline-flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10">
<community.icon className="h-7 w-7 text-primary" />
</div>
<h3 className="mb-4 text-2xl font-bold">{community.title}</h3>
<p className="mb-8 text-muted-foreground text-lg">
{community.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary/10 group-hover:border-primary/20 group-hover:text-foreground transition-all duration-300 hover:scale-[1.02] border-primary/10 text-base h-12"
onClick={() => window.open(community.buttonLink, '_blank')}
>
{community.buttonText}
</Button>
</div>
<div className="absolute -bottom-40 -right-40 w-80 h-80 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-20 group-hover:-translate-x-20" />
</motion.div>
))}
</motion.div>
{/* Second Row - Instagram, TikTok, LinkedIn */}
<motion.div
className="grid gap-4 grid-cols-1 md:grid-cols-3 w-full mt-8"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{communities.slice(2).map((community, index) => (
<motion.div
key={index + 2}
variants={cardVariants}
className="group relative overflow-hidden rounded-2xl border bg-background p-6 hover:shadow-xl transition-shadow h-full"
whileHover={{
y: -5,
transition: { duration: 0.2 },
}}
>
<div className="absolute top-0 left-0 h-1.5 w-full bg-gradient-to-r from-primary/50 to-primary/30 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<community.icon className="h-6 w-6 text-primary" />
</div>
<h3 className="mb-3 text-xl font-bold">{community.title}</h3>
<p className="mb-6 text-muted-foreground">
{community.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary/10 group-hover:border-primary/20 group-hover:text-foreground transition-all duration-300 hover:scale-[1.02] border-primary/10 h-11"
onClick={() => window.open(community.buttonLink, '_blank')}
disabled={community.title === 'LinkedIn'}
>
{community.buttonText}
</Button>
</div>
<div className="absolute -bottom-32 -right-32 w-64 h-64 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-10 group-hover:-translate-x-10" />
</motion.div>
))}
</motion.div>
{/* Stats Section */}
<motion.div
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{stats.map((stat, index) => (
<motion.div
key={index}
variants={statVariants}
custom={index}
className="space-y-3 p-6 rounded-xl bg-background border"
>
<div className="text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/80">
{stat.value}
</div>
<div className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
{stat.label}
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,164 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Events() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const events = [
{
image: '/images/event-1.jpg',
title: 'Tech Summit 2024',
description:
'Konferensi teknologi terbesar tahun ini dengan pembicara expert dari perusahaan unicorn',
type: 'offline',
price: 'berbayar',
link: '#',
},
{
image: '/images/event-2.jpg',
title: 'Web Development Bootcamp',
description:
'Pelatihan intensif full-stack development selama 2 minggu secara online',
type: 'online',
price: 'gratis',
link: '#',
},
{
image: '/images/event-3.jpg',
title: 'UI/UX Workshop',
description:
'Workshop praktis membuat prototype aplikasi dengan Figma dan Framer',
type: 'hybrid',
price: 'berbayar',
link: '#',
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
};
const cardVariants = {
hidden: { y: 40, opacity: 0, scale: 0.95 },
visible: {
y: 0,
opacity: 1,
scale: 1,
transition: {
type: 'spring',
stiffness: 100,
damping: 20,
},
},
};
return (
<section
id="events"
className="w-full py-20 md:py-32 relative overflow-hidden container"
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-primary/5 to-transparent blur-3xl" />
</div>
<div className="w-full px-4 md:px-6 lg:px-0" ref={ref}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-4xl font-bold tracking-tighter md:text-5xl/tight lg:text-6xl mb-6">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/80">
Event Terbaru
</span>
</h2>
<p className="max-w-2xl mx-auto text-muted-foreground text-center md:text-xl text-balance">
Lihat dan ikuti event terbaru dari IMPHNEN
</p>
</motion.div>
<motion.div
className="grid gap-8 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{events.map((event, index) => (
<motion.div
key={index}
variants={cardVariants}
className="group relative overflow-hidden rounded-2xl border bg-background hover:shadow-xl transition-shadow h-full"
whileHover={{
y: -5,
transition: { duration: 0.2 },
}}
>
<div className="absolute top-0 left-0 h-1.5 w-full bg-gradient-to-r from-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div className="h-48 bg-muted/20 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent z-10" />
<div className="absolute top-4 right-4 flex gap-2">
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
event.type === 'online'
? 'bg-green-500/10 text-green-500'
: event.type === 'offline'
? 'bg-purple-500/10 text-purple-500'
: 'bg-yellow-500/10 text-yellow-500'
}`}
>
{event.type === 'hybrid'
? 'Hybrid'
: event.type.toUpperCase()}
</span>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
event.price === 'gratis'
? 'bg-blue-500/10 text-blue-500'
: 'bg-orange-500/10 text-orange-500'
}`}
>
{event.price.toUpperCase()}
</span>
</div>
</div>
<div className="p-6">
<h3 className="text-2xl font-bold mb-3">{event.title}</h3>
<p className="text-muted-foreground mb-6">
{event.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary/10 group-hover:border-primary/20 group-hover:text-foreground transition-all duration-300 hover:scale-[1.02] border-primary/10 text-base h-12"
onClick={() => window.open(event.link, '_blank')}
>
Lihat Detail
</Button>
</div>
</div>
<div className="absolute -bottom-40 -right-40 w-80 h-80 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-20 group-hover:-translate-x-20" />
</motion.div>
))}
</motion.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">
Tempat para programmer struggle bersama, berbagi meme, pengalaman,
tutorial, dan tempat yapping yang nyaman
</p>
</div>
);
}
@@ -0,0 +1,26 @@
import { Avatar, AvatarFallback, AvatarImage } from '@components/atoms';
export function HeroStat() {
return (
<div className="flex items-center rounded-full border border-border bg-background p-1 shadow shadow-black/5 md:w-fit">
<div className="flex -space-x-4">
<Avatar>
<AvatarImage src="/maulana.webp" />
<AvatarFallback />
</Avatar>
<Avatar>
<AvatarImage src="/rasyid.webp" />
<AvatarFallback />
</Avatar>
<Avatar>
<AvatarImage src="/ega.webp" />
<AvatarFallback />
</Avatar>
</div>
<p className="px-2 text-xs text-muted-foreground">
<strong className="font-medium text-foreground">180K+</strong>{' '}
Programmer Indonesia telah bergabung
</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,39 @@
'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 { HeroStat } from './hero-stat';
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>
<HeroStat />
</motion.div>
<HeroImage />
</div>
</div>
</HeroWrapper>
);
}
@@ -0,0 +1,143 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import Image from 'next/image';
import { useRef } from 'react';
import { ImPencil2 } from 'react-icons/im';
const testimonials = [
{
author: {
name: 'Ega',
role: 'Backend Engineer',
avatar: '/ega.webp',
},
quote:
'Tralalero Tralala, IMPHENOTRUNASA merupakan anomali yang sering datang ketika melihat orang ngoding',
},
{
author: {
name: 'Rasyid',
role: 'Fullsnack Engineer',
avatar: '/rasyid.webp',
},
quote:
'Dapet job dari meme yang di-share disini. Gak nyangka yapping random bisa jadi koneksi kerja akwokaowkoak',
},
{
author: {
name: 'Maulana Sodiqin',
role: 'Backend Engineer',
avatar: '/maulana.webp',
},
quote: 'Anjay aku bohong? Admin yang bohong',
},
];
export function Testimonials() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
// Double the testimonials for seamless loop
const duplicatedTestimonials = [...testimonials, ...testimonials];
return (
<section
id="testimoni"
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
ref={ref}
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(var(--primary)/0.1),transparent_50%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(var(--primary)/0.1),transparent_50%)]" />
</div>
<div className="container px-4 md:px-6">
<motion.div
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl lg:text-5xl">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Testimoni{' '}
</span>
Member
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?
</p>
</motion.div>
{/* Infinite Marquee Section */}
<div className="relative w-full overflow-hidden py-4 marquee-container">
<motion.div
className="flex"
animate={{
x: ['0%', '-100%'],
}}
transition={{
duration: 40,
ease: 'linear',
repeat: Infinity,
}}
>
{duplicatedTestimonials.map((testimonial, i) => (
<div
key={i}
className="w-[300px] md:w-[400px] shrink-0 mx-4 p-6 rounded-xl bg-background border border-border/20 hover:border-primary/30 transition-colors group flex flex-col"
style={{ minHeight: '200px' }} // Set a fixed minimum height
>
<div className="flex flex-col gap-4 flex-grow">
<p className="text-muted-foreground italic leading-relaxed line-clamp-4">
&ldquo;{testimonial.quote}&rdquo;
</p>
<div className="mt-auto">
{' '}
{/* This pushes the author info to the bottom */}
<div className="flex items-center gap-4">
<div className="relative h-12 w-12 rounded-full overflow-hidden border-2 border-primary/20 group-hover:border-primary/50 transition-colors">
<Image
src={testimonial.author.avatar}
alt={testimonial.author.name}
width={48}
height={48}
className="object-cover"
unoptimized
/>
</div>
<div>
<h4 className="font-bold">{testimonial.author.name}</h4>
<p className="text-sm text-muted-foreground">
{testimonial.author.role}
</p>
</div>
</div>
</div>
</div>
</div>
))}
</motion.div>
</div>
{/* Call to Action */}
<motion.div
className="mt-16 text-center"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.2 }}
>
<Button
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"
>
<ImPencil2 className="size-5" />
Tulis dan Tampilkan Disini
</Button>
</motion.div>
</div>
</section>
);
}
@@ -0,0 +1,17 @@
import { CallToAction } from './_components/call-to-action';
import { Communities } from './_components/communites';
import { Events } from './_components/events';
import { Hero } from './_components/hero';
import { Testimonials } from './_components/testimonials';
export default function Page() {
return (
<>
<Hero />
<Events />
<Communities />
<Testimonials />
<CallToAction />
</>
);
}
@@ -0,0 +1,194 @@
'use client';
import events from '@/data/events.json';
import { buttonVariants } from '@components/atoms';
import { cn } from '@utils/ui';
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 (
<div className="min-h-screen bg-background">
<div className="max-w-7xl mx-auto px-4 py-12">
<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: 'default' }),
'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: 'outline' }),
'w-full text-sm'
)}
>
Lihat Detail
</a>
</div>
</div>
))}
</div>
</div>
</div>
);
}
@@ -0,0 +1,13 @@
import { ReactNode } from 'react';
import Footer from '../_components/footer';
import { Header } from '../_components/header';
export default function Layout({ children }: { children: 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,114 @@
import {
Button,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@components/atoms';
import Link from 'next/link';
import { BsChatLeftQuote } from 'react-icons/bs';
const testimonials = [
{
id: 1,
name: 'User 1',
date: 'April 2025',
text: 'Bergabung dengan komunitas ini memberikan saya banyak inspirasi. Saya belajar banyak hal baru dan bertemu dengan orang-orang yang luar biasa.',
},
{
id: 2,
name: 'User 2',
date: 'Maret 2025',
text: 'Pengalaman yang sangat berharga. Saya mendapatkan banyak wawasan baru dan dapat mengembangkan keterampilan menulis saya dengan lebih baik.',
},
{
id: 3,
name: 'User 3',
date: 'Maret 2025',
text: 'Komunitas ini sangat mendukung dan memotivasi. Saya senang bisa menjadi bagian dari perjalanan ini dan berbagi pengalaman dengan sesama anggota.',
},
];
export default function TestimonialPage() {
return (
<div className="min-h-screen">
{/* Hero Section */}
<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="/testimonial/submit">
<Button>Tulis Testimonialmu</Button>
</Link>
</div>
{/* Search and Filter */}
<div className="max-w-7xl mx-auto px-4 py-6 border-b border-border">
<div className="flex flex-col md:flex-row gap-4 justify-between">
<div className="relative w-full md:w-96">
<Input
type="text"
placeholder="Cari berdasarkan topik..."
className="w-full bg-background"
/>
</div>
<div className="flex gap-4">
<Select defaultValue="all-time">
<SelectTrigger className="w-[180px] bg-background">
<SelectValue placeholder="Semua Periode" />
</SelectTrigger>
<SelectContent className="bg-popover text-popover-foreground">
<SelectItem value="all-time">Semua Periode</SelectItem>
<SelectItem value="this-month">Bulan Ini</SelectItem>
<SelectItem value="last-month">Bulan Lalu</SelectItem>
<SelectItem value="this-year">Tahun Ini</SelectItem>
</SelectContent>
</Select>
<Select defaultValue="all-topics">
<SelectTrigger className="w-[180px] bg-background">
<SelectValue placeholder="Semua Topik" />
</SelectTrigger>
<SelectContent className="bg-popover text-popover-foreground">
<SelectItem value="all-topics">Semua Topik</SelectItem>
<SelectItem value="writing">Menulis</SelectItem>
<SelectItem value="community">Komunitas</SelectItem>
<SelectItem value="learning">Pembelajaran</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Testimonial Cards */}
<div className="max-w-7xl mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{testimonials.map((testimonial) => (
<div
key={testimonial.id}
className="border rounded-lg p-6 shadow-sm bg-card border-border"
>
<div className="flex items-center mb-4">
<div>
<h3 className="font-semibold text-foreground">
{testimonial.name}
</h3>
<p className="text-sm text-muted-foreground">
Lulus: {testimonial.date}
</p>
</div>
</div>
<p className="text-foreground/80 italic">
&quot;{testimonial.text}&quot;
</p>
</div>
))}
</div>
</div>
</div>
);
}
@@ -1,179 +0,0 @@
'use client';
import {
Button,
FacebookIcon,
InstagramIcon,
MessageCircleIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Community() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const communities = [
{
icon: (
<FacebookIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
),
iconBg: 'bg-blue-100 dark:bg-blue-900',
title: 'Facebook Group',
description:
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.',
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 = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5 },
},
};
return (
<section
id="komunitas"
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 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>
<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
className="grid gap-8 md:grid-cols-3"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{communities.map((community, index) => (
<motion.div
key={index}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-xl"
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="relative z-10">
<div
className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full ${community.iconBg}`}
>
{community.icon}
</div>
<h3 className="mb-2 text-xl font-bold">{community.title}</h3>
<p className="mb-6 text-muted-foreground">
{community.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300"
>
{community.buttonText}
</Button>
</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" />
</motion.div>
))}
</motion.div>
{/* Community Stats */}
<motion.div
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<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">
10K+
</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>
</div>
</section>
);
}
@@ -1,145 +0,0 @@
'use client';
import {
Button,
MenuIcon,
XIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { cn } from '@imphnen-frontend-service/utils';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { SimpleThemeToggle } from './simple-theme-toggle';
export function Header() {
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">
<Image
src="/logo.png"
alt="IMPHNEN Logo"
width={64}
height={64}
className="object-cover cursor-pointer"
/>
</div>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
<Link href="#fitur" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">Fitur</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#komunitas"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Komunitas
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#sumber-belajar"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Sumber Belajar
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#testimoni"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Testimoni
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
</nav>
<div className="flex items-center gap-4">
<SimpleThemeToggle />
<Button className="hidden md:flex 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">
Gabung Discord
</Button>
{/* Mobile Menu Button */}
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? (
<XIcon className="h-6 w-6" />
) : (
<MenuIcon className="h-6 w-6" />
)}
</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">
<Link
href="#fitur"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Fitur
</Link>
<Link
href="#komunitas"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Komunitas
</Link>
<Link
href="#sumber-belajar"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Sumber Belajar
</Link>
<Link
href="#testimoni"
className="py-3 text-sm font-medium"
onClick={() => setMobileMenuOpen(false)}
>
Testimoni
</Link>
<Button className="mt-4 bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer">
Gabung Discord
</Button>
</nav>
</div>
)}
</header>
);
}
@@ -0,0 +1,11 @@
import { buttonVariants } from '@components/atoms';
import { cn } from '@utils/ui';
import Link from 'next/link';
export function ButtonSignin() {
return (
<Link href="/signin" className={cn(buttonVariants(), 'hidden lg:flex')}>
Masuk
</Link>
);
}
@@ -0,0 +1,14 @@
import { buttonVariants } from '@components/atoms';
import { cn } from '@utils/ui';
import Link from 'next/link';
export function ButtonSignup() {
return (
<Link
href="/signup"
className={cn(buttonVariants({ variant: 'outline' }), 'hidden lg:flex')}
>
Daftar
</Link>
);
}
@@ -0,0 +1,28 @@
import Link from 'next/link';
export function DesktopNavigation() {
return (
<nav className="hidden md:flex items-center gap-8">
<Link href="/" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">Home</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/events" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">Event</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/testimonials" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">
Testimonial
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/request" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">
Request Fitur
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
</nav>
);
}
@@ -0,0 +1,29 @@
'use client';
import { cn } from '@utils/ui';
import { ReactNode, useEffect, useState } from 'react';
export function HeaderWrapper({ children }: { children: ReactNode }) {
const [isScrolled, setIsScrolled] = 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'
)}
>
{children}
</header>
);
}
@@ -0,0 +1,32 @@
'use client';
import { useMobileMenuStore } from '@/stores/mobile-menu-store';
import { ButtonSignin } from './button-signin';
import { ButtonSignup } from './button-signup';
import { DesktopNavigation } from './desktop-navigation';
import { HeaderWrapper } from './header-wrapper';
import { Logo } from './logo';
import { MobileMenuHamburger } from './mobile-menu-hamburger';
import { MobileNavigation } from './mobile-navigation';
export function Header() {
const mobileMenuOpen = useMobileMenuStore((s) => s.mobileMenuOpen);
return (
<HeaderWrapper>
<div className="container flex h-16 items-center justify-between">
<Logo />
<DesktopNavigation />
<div className="flex items-center gap-x-2">
<ButtonSignin />
<ButtonSignup />
<MobileMenuHamburger />
</div>
</div>
{mobileMenuOpen && <MobileNavigation />}
</HeaderWrapper>
);
}
@@ -0,0 +1,16 @@
import Image from 'next/image';
import Link from 'next/link';
export function Logo() {
return (
<Link href="/">
<Image
src="/logo.png"
alt="IMPHNEN"
width={64}
height={64}
className="object-cover"
/>
</Link>
);
}
@@ -0,0 +1,25 @@
'use client';
import { useMobileMenuStore } from '@/stores/mobile-menu-store';
import { Button } from '@components/atoms';
import { LuMenu, LuX } from 'react-icons/lu';
export function MobileMenuHamburger() {
const mobileMenuOpen = useMobileMenuStore((s) => s.mobileMenuOpen);
const toggleMobileMenu = useMobileMenuStore((s) => s.toggleMobileMenu);
return (
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={toggleMobileMenu}
>
{mobileMenuOpen ? (
<LuX className="h-6 w-6" />
) : (
<LuMenu className="h-6 w-6" />
)}
</Button>
);
}
@@ -0,0 +1,34 @@
import Link from 'next/link';
export function MobileNavigation() {
return (
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
<nav className="container flex flex-col py-4 text-center">
<Link
href="/"
className="py-3 text-sm font-medium border-b border-border/50"
>
Home
</Link>
<Link
href="/events"
className="py-3 text-sm font-medium border-b border-border/50"
>
Event
</Link>
<Link
href="/testimonias"
className="py-3 text-sm font-medium border-b border-border/50"
>
Testimonial
</Link>
<Link href="/request" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">
Request Fitur
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
</nav>
</div>
);
}
@@ -1,12 +1,9 @@
'use client';
import {
Button,
MoonIcon,
SunIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { Button } from '@components/atoms';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
import { LuMoon, LuSun } from 'react-icons/lu';
export function SimpleThemeToggle() {
const { theme, setTheme } = useTheme();
@@ -32,9 +29,9 @@ export function SimpleThemeToggle() {
className="focus-visible:ring-0 cursor-pointer"
>
{theme === 'dark' ? (
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
<LuSun className="h-[1.2rem] w-[1.2rem]" />
) : (
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
<LuMoon className="h-[1.2rem] w-[1.2rem]" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
@@ -1,154 +0,0 @@
'use client';
import {
Button,
CodeIcon,
SparklesIcon,
UsersIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { useEffect, useState } from 'react';
export function Hero() {
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 Elements */}
<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" />
{/* Animated Gradient Orbs */}
<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),
}}
/>
{/* Grid Pattern */}
<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">
<SparklesIcon className="mr-1 h-3.5 w-3.5 text-primary" />
<span>Komunitas Programmer Indonesia</span>
</div>
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:text-7xl bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
Programmer Handal, <br />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Tanpa Ribet
</span>
</h1>
<p className="max-w-[600px] text-muted-foreground md:text-xl">
Temukan potensi programming Anda bersama komunitas yang
mendukung, tutorial interaktif, dan sumber daya berkualitas
tinggi.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<a href="https://facebook.com">
<Button
size="lg"
className="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"
>
Mulai Belajar
</Button>
</a>
<Button
size="lg"
variant="outline"
className="group relative overflow-hidden border-primary"
>
<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>
<span className="relative">Gabung Discord</span>
</Button>
</div>
<div className="flex items-center gap-8">
<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">
100K+
</div>
<div className="text-xs text-muted-foreground">Member</div>
</div>
<div className="h-10 border-r border-border"></div>
<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">
500+
</div>
<div className="text-xs text-muted-foreground">Tutorial</div>
</div>
<div className="h-10 border-r border-border"></div>
<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">
24/7
</div>
<div className="text-xs text-muted-foreground">Yapping</div>
</div>
</div>
</motion.div>
<motion.div
className="relative 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">
{/* Decorative Elements */}
<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>
{/* Main Image */}
<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" />
<Image
src="/hero.jpeg"
width={600}
height={500}
alt="IMPHNEN Programming Community"
className="w-full h-auto object-cover"
/>
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
'use client';
import { QueryProvider } from '@utils/ui';
import type { ThemeProviderProps } from 'next-themes';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
<QueryProvider>
<NextThemesProvider {...props}>{children}</NextThemesProvider>
</QueryProvider>
);
}
@@ -1,190 +0,0 @@
'use client';
import { QuoteIcon } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { motion, useInView } from 'framer-motion';
import Image from 'next/image';
import { useRef } from 'react';
export function Testimonials() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const testimonials = [
{
quote:
'Saya yang tadinya tidak mengerti apa-apa tentang programming, sekarang bisa membuat website sendiri dengan mudah. Terima kasih IMPHNEN!',
name: 'Budi Santoso',
role: 'Web Developer Pemula',
avatar: '/Budi Santoso.jpg',
},
{
quote:
'Komunitas yang sangat supportif! Setiap pertanyaan selalu dijawab dengan cepat dan jelas. Diskusi programnya mudah dipahami.',
name: 'Anita Ratna',
role: 'Mobile App Developer',
avatar: '/Anita Ratna.jpg',
},
{
quote:
'Server Discord IMPHNEN adalah tempat belajar terbaik untuk programmer pemula seperti saya. Materinya lengkap dan komunitasnya sangat membantu!',
name: 'Dedi Permana',
role: 'Data Scientist',
avatar: '/Dedi Permana.jpg',
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5 },
},
};
return (
<section
id="testimoni"
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 bg-[radial-gradient(ellipse_at_top,rgba(59,130,246,0.1),transparent_50%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(96,165,250,0.1),transparent_50%)]" />
</div>
<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">
Testimoni{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Member
</span>
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?
</p>
</motion.div>
</div>
<motion.div
className="grid gap-8 md:grid-cols-3"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{testimonials.map((testimonial, index) => (
<motion.div
key={index}
className="group relative overflow-hidden rounded-xl border bg-background p-6 transition-all hover:shadow-lg"
variants={itemVariants}
>
<div className="absolute top-6 right-6 text-primary/20 group-hover:text-primary/40 transition-colors">
<QuoteIcon className="h-8 w-8" />
</div>
<div className="relative z-10">
<p className="mb-6 text-muted-foreground italic">
&ldquo;{testimonial.quote}&rdquo;
</p>
<div className="flex items-center gap-4">
<div className="relative h-12 w-12 overflow-hidden rounded-full border-2 border-primary/20">
<Image
src={testimonial.avatar || '/placeholder.svg'}
alt={testimonial.name}
width={48}
height={48}
className="object-cover"
/>
</div>
<div>
<h4 className="font-bold">{testimonial.name}</h4>
<p className="text-sm text-muted-foreground">
{testimonial.role}
</p>
</div>
</div>
</div>
<div className="absolute -bottom-1 -left-1 w-20 h-20 bg-gradient-to-tr from-primary/10 to-transparent rounded-tr-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</motion.div>
))}
</motion.div>
{/* Testimonial Stats */}
<motion.div
className="mt-20 rounded-xl overflow-hidden border bg-background/50 backdrop-blur-sm p-8 md:p-12"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<div className="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 className="text-2xl md:text-3xl font-bold mb-4">
Bergabunglah dengan{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
10,000+
</span>{' '}
programmer Indonesia lainnya
</h3>
<p className="text-muted-foreground">
Komunitas kami terus berkembang dengan programmer dari berbagai
latar belakang dan tingkat keahlian. Bersama-sama, kita belajar,
berbagi, dan tumbuh sebagai profesional.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-lg border p-4 text-center">
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
98%
</div>
<div className="text-sm text-muted-foreground">
Tingkat Kemalasan
</div>
</div>
<div className="rounded-lg border p-4 text-center">
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
4.9/5
</div>
<div className="text-sm text-muted-foreground">
Rating Drama
</div>
</div>
<div className="rounded-lg border p-4 text-center">
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
85%
</div>
<div className="text-sm text-muted-foreground">
Mendapat Pekerjaan
</div>
</div>
<div className="rounded-lg border p-4 text-center">
<div className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
24/7
</div>
<div className="text-sm text-muted-foreground">Yapping</div>
</div>
</div>
</div>
</motion.div>
</div>
</section>
);
}
@@ -1,8 +0,0 @@
'use client';
import type { ThemeProviderProps } from 'next-themes';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
+10 -11
View File
@@ -1,13 +1,12 @@
import { poppins } from '@/lib/fonts';
import '@/styles/globals.css';
import { type Metadata } from 'next';
import { Inter } from 'next/font/google';
import '../../styles/globals.css';
import { ThemeProvider } from './_components/theme-provider';
const inter = Inter({ subsets: ['latin'] });
import { Providers } from './_components/providers';
export const metadata: Metadata = {
title: 'IMPHNEN - Ingin Menjadi Programmer Handal?',
description: 'Komunitas belajar programming untuk semua level',
title: 'IMPHNEN - Ingin Menjadi Programmer Handal Namun Enggan Ngonding',
description:
'Komunitas Ingin Menjadi Programmer Handal Namung Enggan Ngonding',
};
export default function RootLayout({
@@ -17,15 +16,15 @@ export default function RootLayout({
}) {
return (
<html lang="id" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider
<body className={poppins.className}>
<Providers
attribute="class"
defaultTheme="system"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</Providers>
</body>
</html>
);
-25
View File
@@ -1,25 +0,0 @@
import { CallToAction } from './_components/call-to-action';
import { Community } from './_components/community';
import { Features } from './_components/features';
import Footer from './_components/footer';
import { Header } from './_components/header';
import { Hero } from './_components/hero';
import { LearningResources } from './_components/learning-resources';
import { Testimonials } from './_components/testimonials';
export default function Page() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">
<Hero />
<Features />
<Community />
<LearningResources />
<Testimonials />
<CallToAction />
</main>
<Footer />
</div>
);
}
@@ -2,8 +2,8 @@
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next';
import config from '@/payload.config';
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views';
import config from '../../../../payload.config';
import { importMap } from '../importMap';
type Args = {
@@ -2,8 +2,8 @@
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next';
import config from '@/payload.config';
import { RootPage, generatePageMetadata } from '@payloadcms/next/views';
import config from '../../../../payload.config';
import { importMap } from '../importMap';
type Args = {
@@ -1 +1,5 @@
export const importMap = {};
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
export const importMap = {
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24
}
@@ -1,5 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import '@payloadcms/next/css';
import {
REST_DELETE,
@@ -9,7 +10,6 @@ import {
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const GET = REST_GET(config);
export const POST = REST_POST(config);
@@ -1,7 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import '@payloadcms/next/css';
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const GET = GRAPHQL_PLAYGROUND_GET(config);
@@ -1,7 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const POST = GRAPHQL_POST(config);
+1 -1
View File
@@ -1,10 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import '@payloadcms/next/css';
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts';
import type { ServerFunctionClient } from 'payload';
import React from 'react';
import config from '../../payload.config';
import { importMap } from './admin/importMap.js';
import './custom.scss';
@@ -0,0 +1,51 @@
import { GlobalConfig } from 'payload';
export const CallToActionSection: GlobalConfig = {
slug: 'call-to-action-section',
label: 'Call To Action Section',
fields: [
{
name: 'title',
type: 'text',
required: true,
defaultValue: 'Siap Menjadi',
},
{
name: 'highlightedTitle',
type: 'text',
required: true,
defaultValue: 'Programmer Handal?',
},
{
name: 'subtitle',
type: 'textarea',
required: true,
defaultValue:
'Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai perjalanan programming mu dengan cara yang menyenangkan!',
},
{
name: 'primaryButtonLabel',
type: 'text',
required: true,
defaultValue: 'Gabung Discord',
},
{
name: 'primaryButtonLink',
type: 'text',
required: true,
defaultValue: 'https://discord.gg/imphnen',
},
{
name: 'secondaryButtonLabel',
type: 'text',
required: true,
defaultValue: 'Join Facebook Group',
},
{
name: 'secondaryButtonLink',
type: 'text',
required: true,
defaultValue: 'https://facebook.com/groups/programmerhandal',
},
],
};
@@ -0,0 +1,65 @@
import { GlobalConfig } from 'payload';
export const CommunitiesSection: GlobalConfig = {
slug: 'communities-section',
label: 'Communities Section',
fields: [
{
name: 'items',
type: 'array',
required: true,
defaultValue: [
{
iconName: 'tabler:brand-facebook',
title: 'Facebook Group',
description:
'Bergabunglah dengan grup Facebook kami untuk diskusi santai dan berbagi artikel menarik.',
buttonText: 'Gabung Sekarang',
buttonLink: 'https://facebook.com/groups/programmerhandal',
},
{
iconName: 'tabler:brand-instagram',
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: 'tabler:brand-discord-filled',
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: 'text',
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 },
],
},
],
};
@@ -0,0 +1,65 @@
import { GlobalConfig } from 'payload';
export const FeaturesSection: GlobalConfig = {
slug: 'features-section',
label: 'Features Section',
fields: [
{
name: 'heading',
type: 'text',
defaultValue: 'Belajar programming dengan cara yang lebih baik',
},
{
name: 'subheading',
type: 'text',
defaultValue:
'IMPHNEN hadir dengan berbagai fitur untuk membantu kamu menjadi programmer handal tanpa harus pusing dengan coding.',
},
{
name: 'features',
type: 'array',
defaultValue: [
{
iconName: 'tabler:device-laptop',
title: 'Belajar Tanpa Koding',
description:
'Pelajari konsep programming dengan cara yang mudah dipahami tanpa harus menulis kode yang rumit.',
},
{
iconName: 'tabler:users',
title: 'Komunitas Supportif',
description:
'Bergabunglah dengan komunitas programmer Indonesia yang siap membantu dan berbagi pengalaman.',
},
{
iconName: 'tabler:book',
title: 'Tutorial Interaktif',
description:
'Akses tutorial interaktif yang membuat konsep programming lebih mudah untuk dipahami.',
},
{
iconName: 'tabler:code',
title: 'Proyek Praktis',
description:
'Terapkan pengetahuan Anda dalam proyek nyata dengan panduan langkah demi langkah.',
},
],
fields: [
{
name: 'iconName',
type: 'text',
required: true,
},
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'description',
type: 'textarea',
},
],
},
],
};
+104
View File
@@ -0,0 +1,104 @@
import type { GlobalConfig } from 'payload';
export const HeroSection: GlobalConfig = {
slug: 'hero-section',
label: 'Hero Section',
fields: [
{
name: 'heroImage',
label: 'Hero Image',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'badgeText',
label: 'Badge Text',
type: 'text',
required: true,
defaultValue: 'Komunitas Programmer Indonesia',
},
{
name: 'title',
label: 'Main Title',
type: 'text',
required: true,
defaultValue: 'Programmer Handal,',
},
{
name: 'highlight',
label: 'Highlight Text',
type: 'text',
required: true,
defaultValue: 'Tanpa Ribet',
},
{
name: 'description',
label: 'Description',
type: 'textarea',
required: true,
defaultValue:
'Temukan potensi programming Anda bersama komunitas yang mendukung, tutorial interaktif, dan sumber daya berkualitas tinggi.',
},
{
type: 'group',
name: 'buttons',
label: 'Buttons',
fields: [
{
name: 'primaryLabel',
label: 'Primary Button Label',
type: 'text',
required: true,
defaultValue: 'Mulai Belajar',
},
{
name: 'primaryUrl',
label: 'Primary Button URL',
type: 'text',
required: true,
defaultValue: 'https://facebook.com/groups/programmerhandal',
},
{
name: 'secondaryLabel',
label: 'Secondary Button Label',
type: 'text',
required: true,
defaultValue: 'Gabung Discord',
},
{
name: 'secondaryUrl',
label: 'Secondary Button URL',
type: 'text',
required: true,
defaultValue: 'https://discord.com/invite/imphnen',
},
],
},
{
name: 'stats',
label: 'Statistics',
type: 'array',
minRows: 1,
fields: [
{
name: 'value',
label: 'Value',
type: 'text',
required: true,
},
{
name: 'label',
label: 'Label',
type: 'text',
required: true,
},
],
defaultValue: [
{ value: '180K+', label: 'Member' },
{ value: '500+', label: 'Tutorial' },
{ value: '24/7', label: 'Yapping' },
],
},
],
};
@@ -0,0 +1,117 @@
// payload config
import { GlobalConfig } from 'payload';
export const LearningResourcesSection: GlobalConfig = {
slug: 'learning-resources-section',
label: 'Learning Resources Section',
fields: [
{
name: 'title',
type: 'text',
required: true,
defaultValue: 'Sumber Belajar',
},
{
name: 'description',
type: 'textarea',
required: true,
defaultValue:
'Akses berbagai materi belajar yang akan membantu kamu menguasai konsep programming dengan cara yang menyenangkan.',
},
{
name: 'resources',
type: 'array',
required: true,
fields: [
{ name: 'icon', type: 'text', 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 },
],
defaultValue: [
{
icon: 'tabler:video',
title: 'Video Tutorial',
description:
'Belajar melalui tutorial video dari langkah awal hingga mahir.',
buttonText: 'Lihat Semua Video',
buttonLink: '#',
},
{
icon: 'tabler:article',
title: 'Artikel & Tutorial',
description:
'Pelajari konsep programming melalui artikel yang disusun secara terstruktur.',
buttonText: 'Baca Artikel',
buttonLink: '#',
},
{
icon: 'tabler:brand-vscode',
title: 'Tantangan Koding',
description:
'Uji kemampuan koding kamu dengan tantangan yang menyenangkan dan menantang.',
buttonText: 'Mulai Tantangan',
buttonLink: '#',
},
{
icon: 'tabler:device-desktop-share',
title: 'Sharing Session',
description:
'Ikuti sesi berbagi pengalaman dari programmer berpengalaman dan belajar dari pengalaman mereka.',
buttonText: 'Jadwal Session',
buttonLink: '#',
},
],
},
{
name: 'featured',
type: 'group',
fields: [
{
name: 'label',
type: 'text',
required: true,
defaultValue: 'Rekomendasi Terbaik',
},
{
name: 'title',
type: 'text',
required: true,
defaultValue: 'Kursus Lengkap Web Development',
},
{
name: 'description',
type: 'textarea',
required: true,
defaultValue:
'Pelajari HTML, CSS, JavaScript, React, dan Node.js dalam satu kursus komprehensif yang dirancang untuk pemula hingga tingkat menengah.',
},
{
name: 'primaryButtonText',
type: 'text',
required: true,
defaultValue: 'Mulai Kursus',
},
{
name: 'primaryButtonLink',
type: 'text',
required: true,
defaultValue: '#',
},
{
name: 'secondaryButtonText',
type: 'text',
required: true,
defaultValue: 'Lihat Silabus',
},
{
name: 'secondaryButtonLink',
type: 'text',
required: true,
defaultValue: '#',
},
],
},
],
};
@@ -0,0 +1,93 @@
import { GlobalConfig } from 'payload';
export const TestimonialsSection: GlobalConfig = {
slug: 'testimonials-section',
label: 'Testimonials Section',
fields: [
{
name: 'title',
label: 'Section Title',
type: 'text',
defaultValue: 'Testimoni Member',
required: true,
},
{
name: 'subtitle',
label: 'Section Subtitle',
type: 'textarea',
defaultValue:
'Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?',
required: true,
},
{
name: 'items',
label: 'Testimonials Items',
type: 'array',
fields: [
{ name: 'quote', type: 'textarea', label: 'Quote', required: true },
{ name: 'name', type: 'text', label: 'Name', required: true },
{ name: 'role', type: 'text', label: 'Role', required: true },
{
name: 'avatar',
type: 'upload',
label: 'Avatar',
relationTo: 'media',
required: true,
},
],
defaultValue: [
{
quote:
'Saya yang tadinya tidak mengerti apa-apa tentang programming, sekarang bisa membuat website sendiri dengan mudah. Terima kasih IMPHNEN!',
name: 'Budi Santoso',
role: 'Web Developer Pemula',
avatar: { id: '', relationTo: 'media' },
},
{
quote:
'Komunitas yang sangat supportif! Setiap pertanyaan selalu dijawab dengan cepat dan jelas. Diskusi programnya mudah dipahami.',
name: 'Anita Ratna',
role: 'Mobile App Developer',
avatar: { id: '', relationTo: 'media' },
},
{
quote:
'Server Discord IMPHNEN adalah tempat belajar terbaik untuk programmer pemula seperti saya. Materinya lengkap dan komunitasnya sangat membantu!',
name: 'Dedi Permana',
role: 'Data Scientist',
avatar: { id: '', relationTo: 'media' },
},
],
},
{
name: 'stats',
label: 'Statistics',
type: 'array',
fields: [
{ name: 'value', type: 'text', label: 'Value', required: true },
{ name: 'label', type: 'text', label: 'Label', required: true },
],
defaultValue: [
{ value: '98%', label: 'Tingkat Kemalasan' },
{ value: '4.9/5', label: 'Rating Drama' },
{ value: '85%', label: 'Mendapat Pekerjaan' },
{ value: '24/7', label: 'Yapping' },
],
},
{
name: 'joinTitle',
label: 'Join Section Title',
type: 'text',
defaultValue: 'Bergabunglah dengan 10,000+ programmer Indonesia lainnya',
required: true,
},
{
name: 'joinText',
label: 'Join Section Text',
type: 'textarea',
defaultValue:
'Komunitas kami terus berkembang dengan programmer dari berbagai latar belakang dan tingkat keahlian. Bersama-sama, kita belajar, berbagi, dan tumbuh sebagai profesional.',
required: true,
},
],
};
+46
View File
@@ -0,0 +1,46 @@
[
{
"name": "IMPHNEN X GDG Medan",
"description": "Join us for the Google Cloud Roadshow: Build with AI in Medan! This is your chance to dive deep into the latest advancements in cloud technology and AI, guided by industry experts.",
"start_date": "2025-04-26T13:00:00+07:00",
"end_date": "2025-04-26T17:00:00+07:00",
"thumbnail": "https://scontent.fsoc1-2.fna.fbcdn.net/v/t39.30808-6/492363939_2132158393873766_2304913620264481070_n.jpg?_nc_cat=105&ccb=1-7&_nc_sid=127cfc&_nc_eui2=AeFe43oCdy2FOIVI6pr_grkO_q5XyVVxt5v-rlfJVXG3m9KgINIp9lo_vzXP4CtAAFJviDNJdDPaPyGnz7B8-gfE&_nc_ohc=bv3GExoL4pUQ7kNvwEpV7xQ&_nc_oc=Adk45dIpzgFxCHgtQKDTo99Sewj0ZMO4LX1cNEI2Dj7VeQDp8jd1RzYnG_UwsKkaTug&_nc_zt=23&_nc_ht=scontent.fsoc1-2.fna&_nc_gid=ZPKg-Y9FLRXcDiSak5WcLA&oh=00_AfL7BFg5g-q6MALMRFUlgaUWK8jHxQVTHdZ_1ezxBthXfw&oe=68354615",
"type": "onsite",
"price": 0,
"location": "Magnificient, Medan",
"detail_link": "https://n8n.gdgmedan.com/form/8466f60d-94b0-42a6-93aa-8738ae4ab5df?fbclid=IwY2xjawKcQoNleHRuA2FlbQIxMABicmlkETFnQ1RvSU5aWVhpWldaOXBjAR7LUWa_npCLb-QSfk1QFTl12tyw_eBhNO8oI8N2LvhwweeE5_41uQsARsEQGw_aem_1IP2vqrQcvjsvLCBy2C6Hg"
},
{
"name": "IMPHNEN X Connect Citcom",
"description": "Unleash the Future with AI: Decode the potential of artificial intelligence to revolutionize your strategies, unchain innovation, and gain a powerful competitive edge in today's fast-evolving business landscape. Harness the transformative capabilities of AI to outsmart competitors, optimize operations, and drive sustainable growth.",
"start_date": "2025-04-22T13:00:00+07:00",
"end_date": "2025-04-22T17:00:00+07:00",
"thumbnail": "https://scontent.fsoc1-2.fna.fbcdn.net/v/t39.30808-6/490432625_2122941998128739_5372979458124718061_n.jpg?stp=dst-jpg_p526x296_tt6&_nc_cat=105&ccb=1-7&_nc_sid=aa7b47&_nc_eui2=AeGc_W_0Gd3XY9X9ImKz1h1HpYKlinPYX-qlgqWKc9hf6k75HObQiJ8A8m6BZOIe2OrVQjjyXnHq08Rc4JuQWeCQ&_nc_ohc=uoSae0n37k0Q7kNvwHch8E5&_nc_oc=AdngSKtrCdZ4H3hlHh-9cttOnaKyrwiFEcm88kjp0gaflvQ65LUp4OT45-XVGDAoIo8&_nc_zt=23&_nc_ht=scontent.fsoc1-2.fna&_nc_gid=YGkoLGAxNuB0uF3C9hSYGQ&oh=00_AfLRkrzG7DRYa3HRuy9aXfUfG6zLkRBDj9LxqXKOfF9x7Q&oe=683546E0",
"type": "onsite",
"price": 0,
"location": "El Hotel, Bandung",
"detail_link": "/events/1"
},
{
"name": "JVM Meetup #64",
"description": "Join us for an insightful talk where well explore how AI is reshaping the payment industry. Whether youre into tech, AI, or just curious about the future of transactions, this is a must-attend event!",
"start_date": "2025-04-30T13:00:00+07:00",
"end_date": "2025-04-30T17:00:00+07:00",
"thumbnail": "https://scontent.fsoc1-1.fna.fbcdn.net/v/t39.30808-6/494158983_2136830150073257_5259003432367969457_n.jpg?_nc_cat=100&ccb=1-7&_nc_sid=833d8c&_nc_eui2=AeGcneO8ihnXyQ_7WV93z7Onz0UPVTr-bf_PRQ9VOv5t_wI-el8xouZ-Z4TKGMMlBPi21d9AA55SP54MHkzO4rx1&_nc_ohc=3Q9NKGIkpT4Q7kNvwHKzwXr&_nc_oc=AdkRJ--zwRGcfX_F2eIgx0qhK-N3OkhwqRwujFhgSRWy5mH_SdIyBmwW8PHHXp71ZUM&_nc_zt=23&_nc_ht=scontent.fsoc1-1.fna&_nc_gid=Xi30SjzSBAU1XN9rRxp0VA&oh=00_AfKvjd9LTMfOwfPLD43U8lKYEr4KABecj336J8hAY8ekMg&oe=68353105",
"type": "onsite",
"price": 0,
"location": "El Hotel, Bandung",
"detail_link": "https://lu.ma/422jkgz0?utm_campaign=jvmmeetup&utm_medium=social%2C%20event%2C%20meetup&utm_source=google%2C%20facebook%2C%20linkedin%2C%20instagram"
},
{
"name": "JVM Meetup #65",
"description": "Di era digital yang terus berkembang, Artificial Intelligence (AI) bukan lagi teknologi masa depantapi alat bantu masa kini yang siap mendongkrak efisiensi, kreativitas, dan produktivitas kita semua, terutama para penggiat IT dan pelaku usaha!",
"start_date": "2025-05-15T13:00:00+07:00",
"end_date": "2025-05-15T17:00:00+07:00",
"thumbnail": "https://scontent.fsoc1-1.fna.fbcdn.net/v/t39.30808-6/495382435_2144684952621110_8304851620615091109_n.jpg?_nc_cat=104&ccb=1-7&_nc_sid=833d8c&_nc_eui2=AeHwnU4KixfUsslo3M90ahT8GwMEI7zWSnQbAwQjvNZKdP3RHiUjdDZtuF1dIMvorL0nW0rDGOD6tPDi0i9ERK96&_nc_ohc=R1NyXnu-FKEQ7kNvwGbO-iN&_nc_oc=Adluf-uw1tmzkNME9vNhwkTv5-x_GHMfsLj1bzGyYds_ODq3UnWMq2DthyUisF9e3gU&_nc_zt=23&_nc_ht=scontent.fsoc1-1.fna&_nc_gid=kykMk9nPtcrtk-InL6tF6w&oh=00_AfLZSrEPpV83lVFRMz19n9RzfnsM4o7K7TijpdAjWYViiA&oe=6835231B",
"type": "onsite",
"price": 0,
"location": "Telkom Landmark Tower (Lt.31)",
"detail_link": "s.id/jvm65"
}
]
+24
View File
@@ -0,0 +1,24 @@
import { cookies } from 'next/headers';
const REFRESH_TOKEN_NAME = '__imphnen_refresh_token__';
const ACCESS_TOKEN_NAME = '__imphnen_access_token__';
export async function setRefreshToken(refreshToken: string) {
const cookieStore = await cookies();
cookieStore.set(REFRESH_TOKEN_NAME, refreshToken);
}
export async function setAccessToken(accessToken: string) {
const cookieStore = await cookies();
cookieStore.set(ACCESS_TOKEN_NAME, accessToken);
}
export async function getRefreshToken() {
const cookieStore = await cookies();
return cookieStore.get(REFRESH_TOKEN_NAME)?.value;
}
export async function getAccessToken() {
const cookieStore = await cookies();
return cookieStore.get(ACCESS_TOKEN_NAME)?.value;
}
+8
View File
@@ -0,0 +1,8 @@
// fonts.ts
import { Poppins } from 'next/font/google';
export const poppins = Poppins({
subsets: ['latin'],
weight: ['300', '400', '500', '600', '700'],
display: 'swap',
});
+10
View File
@@ -0,0 +1,10 @@
import { Media } from '../payload-types';
export function formatCMSImageDataToMedia(
image: number | Media | null | undefined
): Media | null {
if (image && typeof image === 'object' && 'url' in image) {
return image as Media;
}
return null;
}
+6
View File
@@ -0,0 +1,6 @@
import type { paths } from '@/openapi-types';
import createClient from 'openapi-fetch';
export const fetcher = createClient<paths>({
baseUrl: 'https://api.imphnen.dev',
});
File diff suppressed because it is too large Load Diff
+308 -2
View File
@@ -84,8 +84,22 @@ export interface Config {
db: {
defaultIDType: number;
};
globals: {};
globalsSelect: {};
globals: {
'hero-section': HeroSection;
'features-section': FeaturesSection;
'communities-section': CommunitiesSection;
'learning-resources-section': LearningResourcesSection;
'testimonials-section': TestimonialsSection;
'call-to-action-section': CallToActionSection;
};
globalsSelect: {
'hero-section': HeroSectionSelect<false> | HeroSectionSelect<true>;
'features-section': FeaturesSectionSelect<false> | FeaturesSectionSelect<true>;
'communities-section': CommunitiesSectionSelect<false> | CommunitiesSectionSelect<true>;
'learning-resources-section': LearningResourcesSectionSelect<false> | LearningResourcesSectionSelect<true>;
'testimonials-section': TestimonialsSectionSelect<false> | TestimonialsSectionSelect<true>;
'call-to-action-section': CallToActionSectionSelect<false> | CallToActionSectionSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
@@ -271,6 +285,298 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hero-section".
*/
export interface HeroSection {
id: number;
heroImage: number | Media;
badgeText: string;
title: string;
highlight: string;
description: string;
buttons: {
primaryLabel: string;
primaryUrl: string;
secondaryLabel: string;
secondaryUrl: string;
};
stats?:
| {
value: string;
label: string;
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "features-section".
*/
export interface FeaturesSection {
id: number;
heading?: string | null;
subheading?: string | null;
features?:
| {
iconName: string;
title: string;
description?: string | null;
id?: string | null;
}[]
| null;
updatedAt?: 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: string;
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
* via the `definition` "learning-resources-section".
*/
export interface LearningResourcesSection {
id: number;
title: string;
description: string;
resources: {
icon: string;
title: string;
description: string;
buttonText: string;
buttonLink: string;
id?: string | null;
}[];
featured: {
label: string;
title: string;
description: string;
primaryButtonText: string;
primaryButtonLink: string;
secondaryButtonText: string;
secondaryButtonLink: string;
};
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "testimonials-section".
*/
export interface TestimonialsSection {
id: number;
title: string;
subtitle: string;
items?:
| {
quote: string;
name: string;
role: string;
avatar: number | Media;
id?: string | null;
}[]
| null;
stats?:
| {
value: string;
label: string;
id?: string | null;
}[]
| null;
joinTitle: string;
joinText: string;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "call-to-action-section".
*/
export interface CallToActionSection {
id: number;
title: string;
highlightedTitle: string;
subtitle: string;
primaryButtonLabel: string;
primaryButtonLink: string;
secondaryButtonLabel: string;
secondaryButtonLink: string;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "hero-section_select".
*/
export interface HeroSectionSelect<T extends boolean = true> {
heroImage?: T;
badgeText?: T;
title?: T;
highlight?: T;
description?: T;
buttons?:
| T
| {
primaryLabel?: T;
primaryUrl?: T;
secondaryLabel?: T;
secondaryUrl?: T;
};
stats?:
| T
| {
value?: T;
label?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "features-section_select".
*/
export interface FeaturesSectionSelect<T extends boolean = true> {
heading?: T;
subheading?: T;
features?:
| T
| {
iconName?: T;
title?: T;
description?: T;
id?: T;
};
updatedAt?: T;
createdAt?: 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
* via the `definition` "learning-resources-section_select".
*/
export interface LearningResourcesSectionSelect<T extends boolean = true> {
title?: T;
description?: T;
resources?:
| T
| {
icon?: T;
title?: T;
description?: T;
buttonText?: T;
buttonLink?: T;
id?: T;
};
featured?:
| T
| {
label?: T;
title?: T;
description?: T;
primaryButtonText?: T;
primaryButtonLink?: T;
secondaryButtonText?: T;
secondaryButtonLink?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "testimonials-section_select".
*/
export interface TestimonialsSectionSelect<T extends boolean = true> {
title?: T;
subtitle?: T;
items?:
| T
| {
quote?: T;
name?: T;
role?: T;
avatar?: T;
id?: T;
};
stats?:
| T
| {
value?: T;
label?: T;
id?: T;
};
joinTitle?: T;
joinText?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "call-to-action-section_select".
*/
export interface CallToActionSectionSelect<T extends boolean = true> {
title?: T;
highlightedTitle?: T;
subtitle?: T;
primaryButtonLabel?: T;
primaryButtonLink?: T;
secondaryButtonLabel?: T;
secondaryButtonLink?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
+39 -1
View File
@@ -1,11 +1,20 @@
import { postgresAdapter } from '@payloadcms/db-postgres';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { s3Storage } from '@payloadcms/storage-s3';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import https from 'https';
import path from 'path';
import { buildConfig } from 'payload';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
import { CallToActionSection } from './collections/CallToActionSection';
import { CommunitiesSection } from './collections/CommunitiesSection';
import { FeaturesSection } from './collections/FeaturesSection';
import { HeroSection } from './collections/HeroSection';
import { LearningResourcesSection } from './collections/LearningResourcesSection';
import { Media } from './collections/Media';
import { TestimonialsSection } from './collections/TestimonialsSection';
import { Users } from './collections/Users';
const filename = fileURLToPath(import.meta.url);
@@ -25,6 +34,14 @@ export default buildConfig({
},
},
collections: [Users, Media],
globals: [
HeroSection,
FeaturesSection,
CommunitiesSection,
LearningResourcesSection,
TestimonialsSection,
CallToActionSection,
],
editor: lexicalEditor(),
secret: process.env.CMS_SECRET || '',
typescript: {
@@ -36,5 +53,26 @@ export default buildConfig({
},
}),
sharp,
plugins: [],
plugins: [
s3Storage({
collections: {
media: {
disableLocalStorage: true,
},
},
bucket: 'landing-cms',
config: {
endpoint: process.env.CMS_STORAGE_ENDPOINT,
credentials: {
accessKeyId: process.env.CMS_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: process.env.CMS_STORAGE_SECRET_ACCESS_KEY!,
},
region: process.env.CMS_STORAGE_REGION!,
forcePathStyle: true,
requestHandler: new NodeHttpHandler({
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
}),
},
}),
],
});
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsCallToActionSection() {
return await payload.findGlobal({
slug: 'call-to-action-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsCommunitiesSection() {
return await payload.findGlobal({
slug: 'communities-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsFeaturesSection() {
return await payload.findGlobal({
slug: 'features-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsHeroSection() {
return await payload.findGlobal({
slug: 'hero-section',
});
}
@@ -0,0 +1,7 @@
import { payload } from '../lib/payload';
export async function getGlobalsLearningResourcesSection() {
return await payload.findGlobal({
slug: 'learning-resources-section',
});
}

Some files were not shown because too many files have changed in this diff Show More