diff --git a/apps/landing/src/app/(frontend)/(auth)/signin/_actions/signin-action.ts b/apps/landing/src/app/(frontend)/(auth)/signin/_actions/signin-action.ts
index 0397189..0a42b5f 100644
--- a/apps/landing/src/app/(frontend)/(auth)/signin/_actions/signin-action.ts
+++ b/apps/landing/src/app/(frontend)/(auth)/signin/_actions/signin-action.ts
@@ -8,6 +8,8 @@ import {
} from '../_validation/signin-validation';
export async function SigninAction(request: SignInValidationType) {
+ console.log(request);
+
const validRequest = signInValidationSchema.parse(request);
const { data } = await fetchPostSignin(validRequest);
diff --git a/apps/landing/src/app/(frontend)/(auth)/signup/_actions/signup-action.ts b/apps/landing/src/app/(frontend)/(auth)/signup/_actions/signup-action.ts
new file mode 100644
index 0000000..a43aa4a
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/signup/_actions/signup-action.ts
@@ -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
+) {
+ const validRequest = signupValidationSchema.parse(request);
+
+ const data = await fetchPostSignin({ ...validRequest });
+
+ return data;
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/signup/_components/signup-form.tsx b/apps/landing/src/app/(frontend)/(auth)/signup/_components/signup-form.tsx
new file mode 100644
index 0000000..4323180
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/signup/_components/signup-form.tsx
@@ -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 | null>(null);
+
+ const { mutate, isPending, error } = useMutation({
+ mutationFn: async (data: z.infer) => {
+ const result = await SignupAction(data);
+
+ return {
+ ...result,
+ email: data.email,
+ };
+ },
+ onSuccess: ({ email }) => {
+ router.push(`/verification?ref=${email}`);
+ },
+ onError: () => {
+ secondForm.reset();
+ },
+ });
+
+ const firstForm = useForm>({
+ resolver: zodResolver(stepOneSignupValidationSchema),
+ defaultValues: {
+ email: '',
+ fullname: '',
+ password: '',
+ confirm_password: '',
+ },
+ });
+
+ const secondForm = useForm>({
+ resolver: zodResolver(stepTwoSignupValidationSchema),
+ defaultValues: {
+ phone_number: '',
+ referral_code: '',
+ referred_by: '',
+ student_type: '',
+ token: '',
+ },
+ });
+
+ const handleFirstSubmit = (
+ values: z.infer
+ ) => {
+ setStepOneData(values);
+ setStep(2);
+ };
+
+ const handleSecondSubmit = (
+ values: z.infer
+ ) => {
+ if (stepOneData) {
+ mutate({ ...stepOneData, ...values });
+ }
+ };
+
+ return (
+ <>
+ {step === 1 && (
+
+
+ )}
+
+ {step === 2 && (
+
+
+ )}
+ >
+ );
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/signup/_http/fetch-post-signup.ts b/apps/landing/src/app/(frontend)/(auth)/signup/_http/fetch-post-signup.ts
new file mode 100644
index 0000000..cb77c78
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/signup/_http/fetch-post-signup.ts
@@ -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;
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/signup/_validation/signup-validation.ts b/apps/landing/src/app/(frontend)/(auth)/signup/_validation/signup-validation.ts
new file mode 100644
index 0000000..55904f5
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/signup/_validation/signup-validation.ts
@@ -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
\ No newline at end of file
diff --git a/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx b/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx
index 3fe5b43..1143449 100644
--- a/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx
+++ b/apps/landing/src/app/(frontend)/(auth)/signup/page.tsx
@@ -1,4 +1,5 @@
import { LogoSimple } from '../../_components/logo';
+import { SignupForm } from './_components/signup-form';
export default function Page() {
return (
@@ -10,6 +11,8 @@ export default function Page() {
Buat akunmu sekarang
+
+
>
);
}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_actions/resend-otp-action.ts b/apps/landing/src/app/(frontend)/(auth)/verification/_actions/resend-otp-action.ts
new file mode 100644
index 0000000..0880def
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_actions/resend-otp-action.ts
@@ -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;
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_actions/verify-email-action.ts b/apps/landing/src/app/(frontend)/(auth)/verification/_actions/verify-email-action.ts
new file mode 100644
index 0000000..cc685c2
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_actions/verify-email-action.ts
@@ -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;
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_components/button-resend-otp.tsx b/apps/landing/src/app/(frontend)/(auth)/verification/_components/button-resend-otp.tsx
new file mode 100644
index 0000000..22c1152
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_components/button-resend-otp.tsx
@@ -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 (
+
+ );
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_components/verify-email-form.tsx b/apps/landing/src/app/(frontend)/(auth)/verification/_components/verify-email-form.tsx
new file mode 100644
index 0000000..cd4145e
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_components/verify-email-form.tsx
@@ -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 (
+
+
+
+
+ );
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-form-verify-email.ts b/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-form-verify-email.ts
new file mode 100644
index 0000000..54d9fb8
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-form-verify-email.ts
@@ -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({
+ resolver: zodResolver(verifyEmailSchema),
+ defaultValues: {
+ email: email ?? '',
+ otp: '',
+ },
+ });
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-post-resend-otp.ts b/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-post-resend-otp.ts
new file mode 100644
index 0000000..b2fa8ea
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-post-resend-otp.ts
@@ -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'),
+ });
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-post-verify-email.ts b/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-post-verify-email.ts
new file mode 100644
index 0000000..c47d42a
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_hooks/use-post-verify-email.ts
@@ -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
+) {
+ const router = useRouter();
+
+ return useMutation({
+ mutationFn: verifyEmailAction,
+ onSuccess: () => {
+ router.push('/feedback');
+ },
+ onError: () => {
+ form.resetField('otp');
+ },
+ });
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_http/fetch-post-verify-email.ts b/apps/landing/src/app/(frontend)/(auth)/verification/_http/fetch-post-verify-email.ts
new file mode 100644
index 0000000..d89ec7d
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_http/fetch-post-verify-email.ts
@@ -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;
+}
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/_validation/verify-email-validation.ts b/apps/landing/src/app/(frontend)/(auth)/verification/_validation/verify-email-validation.ts
new file mode 100644
index 0000000..ce5d5a8
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/_validation/verify-email-validation.ts
@@ -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;
diff --git a/apps/landing/src/app/(frontend)/(auth)/verification/page.tsx b/apps/landing/src/app/(frontend)/(auth)/verification/page.tsx
new file mode 100644
index 0000000..ef19ca6
--- /dev/null
+++ b/apps/landing/src/app/(frontend)/(auth)/verification/page.tsx
@@ -0,0 +1,18 @@
+import { LogoSimple } from '../../_components/logo';
+import { VerifyEmailForm } from './_components/verify-email-form';
+
+export default function Page() {
+ return (
+ <>
+
+
+
+
+ Verifikasi akun mu sekarang
+
+
+
+
+ >
+ );
+}
diff --git a/apps/landing/src/app/(frontend)/_components/header/desktop-navigation.tsx b/apps/landing/src/app/(frontend)/_components/header/desktop-navigation.tsx
index ed79e27..2a8a13c 100644
--- a/apps/landing/src/app/(frontend)/_components/header/desktop-navigation.tsx
+++ b/apps/landing/src/app/(frontend)/_components/header/desktop-navigation.tsx
@@ -3,8 +3,8 @@ import Link from 'next/link';
export function DesktopNavigation() {
return (