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 && ( +
+ + ( + + Full Name + + + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + ( + + Password + + + + + + )} + /> + + ( + + Confirm Password + + + + + + )} + /> + + + + + )} + + {step === 2 && ( +
+ + {error && ( +
+ {(error as Error).message} +
+ )} + + ( + + Nomor Telepon + + + + + + )} + /> + +
+ ( + + Kode Referral + + + + + + )} + /> + + ( + + Referrer + + + + + + )} + /> +
+ + ( + + Status + + + + + + )} + /> + + secondForm.setValue('token', token)} + options={{ + theme: 'light', + size: 'flexible', + language: 'id', + }} + /> + +
+ + +
+ + + )} + + ); +} 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 ( +
+ + {error && ( +
+ {(error as Error).message} +
+ )} + + ( + + Email + + + + + + )} + /> + + ( + + OTP + + + + + + )} + /> + + + + + + + ); +} 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 (