diff --git a/sigap-website/app/(pages)/(admin)/_components/profile-form.tsx b/sigap-website/app/(pages)/(admin)/_components/profile-form.tsx index d54fe02..63a1cd2 100644 --- a/sigap-website/app/(pages)/(admin)/_components/profile-form.tsx +++ b/sigap-website/app/(pages)/(admin)/_components/profile-form.tsx @@ -30,7 +30,7 @@ import { ImageIcon, Loader2 } from "lucide-react"; import { createClient } from "@/app/_utils/supabase/client"; import { getFullName, getInitials } from "@/app/_utils/common"; import { useProfileFormHandlers } from "../dashboard/user-management/_handlers/use-profile-form"; -import { CTexts } from "@/app/_lib/const/string"; +import { CTexts } from "@/app/_lib/const/texts"; // Profile update form schema const profileFormSchema = z.object({ diff --git a/sigap-website/app/(pages)/(admin)/_components/settings/profile-settings.tsx b/sigap-website/app/(pages)/(admin)/_components/settings/profile-settings.tsx index 5deab88..9ecda4a 100644 --- a/sigap-website/app/(pages)/(admin)/_components/settings/profile-settings.tsx +++ b/sigap-website/app/(pages)/(admin)/_components/settings/profile-settings.tsx @@ -32,7 +32,7 @@ import { updateUser, } from "@/app/(pages)/(admin)/dashboard/user-management/action"; import { useProfileFormHandlers } from "../../dashboard/user-management/_handlers/use-profile-form"; -import { CTexts } from "@/app/_lib/const/string"; +import { CTexts } from "@/app/_lib/const/texts"; const profileFormSchema = z.object({ username: z.string().nullable().optional(), diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-profile-form.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-profile-form.ts index 07633c7..0fefa1b 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-profile-form.ts +++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-profile-form.ts @@ -11,8 +11,8 @@ import { z } from "zod" import { useUnbanUserMutation, useUpdateUserMutation, useUploadAvatarMutation } from "../_queries/mutations" import { useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" -import { CNumbers } from "@/app/_lib/const/number" -import { CTexts } from "@/app/_lib/const/string" +import { CNumbers } from "@/app/_lib/const/numbers" +import { CTexts } from "@/app/_lib/const/texts" import { useUserActionsHandler } from "./actions/use-user-actions" // Profile update form schema diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-send-password-recovery.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-send-password-recovery.ts index dc23a8a..37d67dd 100644 --- a/sigap-website/app/(pages)/(auth)/_handlers/use-send-password-recovery.ts +++ b/sigap-website/app/(pages)/(auth)/_handlers/use-send-password-recovery.ts @@ -3,29 +3,59 @@ import type { IUserSchema } from "@/src/entities/models/users/users.model" import { useSendMagicLinkMutation, useSendPasswordRecoveryMutation } from "@/app/(pages)/(auth)/_queries/mutations" import { toast } from "sonner" +import { useForm } from "react-hook-form" +import { ISendPasswordRecoverySchema, SendPasswordRecoverySchema } from "@/src/entities/models/auth/send-password-recovery.model" +import { zodResolver } from "@hookform/resolvers/zod" -export const useSendPasswordRecoveryHandler = (user: IUserSchema, onOpenChange: (open: boolean) => void) => { - const { mutateAsync: sendPasswordRecovery, isPending } = +export const useSendPasswordRecoveryHandler = () => { + const { mutateAsync: sendPasswordRecovery, isPending, error } = useSendPasswordRecoveryMutation() - const handleSendPasswordRecovery = async () => { - if (user.email) { - await sendPasswordRecovery(user.email, { - onSuccess: () => { - toast.success(`Password recovery email sent to ${user.email}`) - onOpenChange(false) + const { + register, + handleSubmit: handleFormSubmit, + reset, + formState: { errors: formErrors }, + setError: setFormError, + } = useForm({ + defaultValues: { + email: "", + }, + resolver: zodResolver(SendPasswordRecoverySchema), + }) + + const onSubmit = handleFormSubmit(async (data) => { + if (isPending) return + + const email = data.email; + + try { + toast.promise(sendPasswordRecovery(email), { + loading: "Sending password recovery...", + success: () => { + reset() + return "An email has been sent to you. Please check your inbox." }, - onError: (error) => { - toast.error(error.message) - onOpenChange(false) + error: (err) => { + const errorMessage = err?.message || "Failed to send email." + setFormError("email", { message: errorMessage }) + return errorMessage }, }) + } catch (err: any) { + setFormError("email", { message: err?.message || "An error occurred while sending the email." }) } - } + }) return { - handleSendPasswordRecovery, + register, + handleSubmit: onSubmit, + reset, + formErrors, isPending, + error: !!error, + errors: !!error || formErrors.email, + sendPasswordRecovery, } } diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in-passwordless.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in-passwordless.ts new file mode 100644 index 0000000..758f531 --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in-passwordless.ts @@ -0,0 +1,84 @@ +import { useNavigations } from "@/app/_hooks/use-navigations"; +import { useSignInPasswordlessMutation } from "../_queries/mutations"; +import { toast } from "sonner"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { ISignInPasswordlessSchema, SignInPasswordlessSchema } from "@/src/entities/models/auth/sign-in.model"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { createRoute } from "@/app/_utils/common"; +import { ROUTES } from "@/app/_lib/const/routes"; + +export function useSignInPasswordlessHandler() { + const { mutateAsync: signIn, isPending, error: queryError } = useSignInPasswordlessMutation(); + const { router } = useNavigations(); + + // For server/API errors + const [error, setError] = useState(); + + const { + register, + handleSubmit: handleFormSubmit, + reset, + formState: { errors: formErrors }, + setError: setFormError, + } = useForm({ + defaultValues: { + email: "", + }, + resolver: zodResolver(SignInPasswordlessSchema), + mode: "onSubmit" // Validate on form submission + }); + + const onSubmit = handleFormSubmit(async (data) => { + if (isPending) return; + + setError(undefined); + + const formData = new FormData(); + formData.append('email', data.email); + + try { + await toast.promise( + signIn(formData), + { + loading: 'Sending magic link...', + success: () => { + // If we reach here, the operation was successful + router.push(createRoute(ROUTES.AUTH.VERIFY_OTP, { email: data.email })); + return 'An email has been sent to you. Please check your inbox.'; + }, + error: (err) => { + const errorMessage = err?.message || 'Failed to send email.'; + setError(errorMessage); + return errorMessage; + } + } + ); + } catch (err: any) { + // Error is already handled in the toast.promise error callback + setError(err?.message || 'An error occurred while sending the email.'); + } + }); + + // Extract the validation error message for the email field + const getFieldErrorMessage = (fieldName: keyof ISignInPasswordlessSchema) => { + return formErrors[fieldName]?.message || ''; + }; + + // Wrapper to handle the form submission properly + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(e); + }; + + return { + reset, + register, + handleSignIn: handleSubmit, + error: getFieldErrorMessage('email') || error, // Prioritize form validation errors + isPending, + errors: !!error || !!queryError || Object.keys(formErrors).length > 0, + formErrors, + getFieldErrorMessage + }; +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in-with-password.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in-with-password.ts new file mode 100644 index 0000000..0c7345f --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in-with-password.ts @@ -0,0 +1,86 @@ +import { useNavigations } from "@/app/_hooks/use-navigations"; +import { useSignInWithPasswordMutation } from "../_queries/mutations"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { ISignInWithPasswordSchema, SignInWithPasswordSchema } from "@/src/entities/models/auth/sign-in.model"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "sonner"; +import { createRoute } from "@/app/_utils/common"; +import { ROUTES } from "@/app/_lib/const/routes"; + +export const useSignInWithPasswordHandler = () => { + const { mutateAsync: signInWithPassword, isPending, error: queryError } = useSignInWithPasswordMutation(); + + const { router } = useNavigations(); + + const [error, setError] = useState(); + + const { + register, + handleSubmit: handleFormSubmit, + reset, + formState: { errors: formErrors }, + setError: setFormError, + } = useForm({ + defaultValues: { + email: "", + password: "" + }, + resolver: zodResolver(SignInWithPasswordSchema), + mode: "onSubmit" // Validate on form submission + }); + + const onSubmit = handleFormSubmit(async (data) => { + if (isPending) return; + + setError(undefined); + + const formData = new FormData(); + formData.append('email', data.email); + + try { + toast.promise( + signInWithPassword(formData), + { + loading: 'Sending magic link...', + success: () => { + // If we reach here, the operation was successful + router.push(createRoute(ROUTES.APP.DASHBOARD)); + return 'An email has been sent to you. Please check your inbox.'; + }, + error: (err) => { + const errorMessage = err?.message || 'Failed to send email.'; + setError(errorMessage); + return errorMessage; + } + } + ); + } catch (err: any) { + // Error is already handled in the toast.promise error callback + setError(err?.message || 'An error occurred while sending the email.'); + } + }); + + // Extract the validation error message for the email field + const getFieldErrorMessage = (fieldName: keyof ISignInWithPasswordSchema) => { + return formErrors[fieldName]?.message || ''; + }; + + // Wrapper to handle the form submission properly + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(e); + }; + + return { + reset, + register, + handleSignIn: handleSubmit, + isPending, + error: getFieldErrorMessage('email') || error, // Prioritize form validation errors + queryError, + errors: !!error || !!queryError || Object.keys(formErrors).length > 0, + formErrors, + getFieldErrorMessage + }; +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in.ts deleted file mode 100644 index e55e021..0000000 --- a/sigap-website/app/(pages)/(auth)/_handlers/use-sign-in.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useNavigations } from "@/app/_hooks/use-navigations"; -import { useSignInPasswordlessMutation } from "../_queries/mutations"; -import { toast } from "sonner"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { ISignInPasswordlessSchema, SignInPasswordlessSchema } from "@/src/entities/models/auth/sign-in.model"; -import { zodResolver } from "@hookform/resolvers/zod"; - -export function useSignInHandler() { - const { mutateAsync: signIn, isPending, error: errors } = useSignInPasswordlessMutation(); - const { router } = useNavigations(); - - const [error, setError] = useState(); - - const { - register, - reset, - formState: { errors: formErrors }, - setError: setFormError, - } = useForm({ - defaultValues: { - email: "", - }, - resolver: zodResolver(SignInPasswordlessSchema), - }) - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - if (isPending) return; - - setError(undefined); - - const formData = new FormData(event.currentTarget); - const email = formData.get('email')?.toString(); - - const res = await signIn(formData); - - if (!res?.error) { - toast('An email has been sent to you. Please check your inbox.'); - if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`); - } else { - setError(res.error); - } - - - }; - - // const onSubmit = handleSubmit(async (data) => { - // if (isPending) return; - - // console.log(data); - - // setError(undefined); - - // const { email } = data; - - // const formData = new FormData(); - // formData.append('email', email); - - // const res = await signIn(formData); - - // if (!res?.error) { - // toast('An email has been sent to you. Please check your inbox.'); - // router.push(`/verify-otp?email=${encodeURIComponent(email)}`); - // } else { - // setError(res.error); - // } - // }) - - return { - // formData, - // handleChange, - reset, - register, - handleSignIn: handleSubmit, - error, - isPending, - errors: !!error || errors, - }; -} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/forgot-password/_components/forgot-password-form.tsx b/sigap-website/app/(pages)/(auth)/forgot-password/_components/forgot-password-form.tsx new file mode 100644 index 0000000..53f9670 --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/forgot-password/_components/forgot-password-form.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@/app/_components/ui/input-otp"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/app/_components/ui/card"; +import { cn } from "@/app/_lib/utils"; +import { Loader2 } from "lucide-react"; +import { Controller } from "react-hook-form"; +import { Button } from "@/app/_components/ui/button"; +import { useVerifyOtpHandler } from "../../_handlers/use-verify-otp"; +import { useSendPasswordRecoveryHandler } from "../../_handlers/use-send-password-recovery"; +import { FormField } from "@/app/_components/form-field"; +import { Input } from "@/app/_components/ui/input"; +import { Separator } from "@/app/_components/ui/separator"; + +interface ForgotPasswordFormProps extends React.HTMLAttributes { } + +export function ForgotPasswordForm({ className, ...props }: ForgotPasswordFormProps) { + + const { + register, + handleSubmit, + error, + errors, + formErrors, + isPending + } = useSendPasswordRecoveryHandler() + + return ( +
+ + + Reset Your Password + + Type in your email and we'll send you a link to reset your password + + + +
+ + } + error={formErrors.email?.message} + /> + +
+ +
+ +
+
+
+ By clicking continue, you agree to Sigap's{" "} + Terms of Service and Privacy Policy. +
+
+ ) +} + diff --git a/sigap-website/app/(pages)/(auth)/forgot-password/page.tsx b/sigap-website/app/(pages)/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..405b32d --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/forgot-password/page.tsx @@ -0,0 +1,25 @@ +import { VerifyOtpForm } from "@/app/(pages)/(auth)/verify-otp/_components/verify-otp-form"; +import { GalleryVerticalEnd } from "lucide-react"; +import { ForgotPasswordForm } from "./_components/forgot-password-form"; + +export default async function ForgotPasswordPage() { + return ( +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/sigap-website/app/(pages)/(auth)/handler.tsx b/sigap-website/app/(pages)/(auth)/handler.tsx index 7817f5a..73b2ac9 100644 --- a/sigap-website/app/(pages)/(auth)/handler.tsx +++ b/sigap-website/app/(pages)/(auth)/handler.tsx @@ -15,10 +15,10 @@ // * // * @returns {Object} Object berisi handler dan state untuk form sign in // * @example -// * const { handleSubmit, isPending, error } = useSignInHandler(); +// * const { handleSubmit, isPending, error } = useSignInPasswordlessHandler(); // *
...
// */ -// export function useSignInHandler() { +// export function useSignInPasswordlessHandler() { // const { signIn } = useAuthActions(); // const { router } = useNavigations(); diff --git a/sigap-website/app/(pages)/(auth)/sign-in/_components/sign-in-with-password-form.tsx b/sigap-website/app/(pages)/(auth)/sign-in/_components/sign-in-with-password-form.tsx new file mode 100644 index 0000000..53ec888 --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/sign-in/_components/sign-in-with-password-form.tsx @@ -0,0 +1,194 @@ +"use client"; + +import React, { useState } from "react"; +import { Loader2, Lock, Eye, EyeOff } from "lucide-react"; +import { Button } from "@/app/_components/ui/button"; +import { Input } from "@/app/_components/ui/input"; +import Link from "next/link"; +import { FormField } from "@/app/_components/form-field"; +import { useSignInPasswordlessHandler } from "../../_handlers/use-sign-in-passwordless"; +import { useSignInWithPasswordHandler } from "../../_handlers/use-sign-in-with-password"; +import { useNavigations } from "@/app/_hooks/use-navigations"; +import { createRoute } from "@/app/_utils/common"; +import { ROUTES } from "@/app/_lib/const/routes"; + +export function SignInWithPasswordForm({ + className, + ...props +}: React.ComponentPropsWithoutRef<"form">) { + + const { router } = useNavigations(); + // State for password visibility toggle + const [showPassword, setShowPassword] = useState(false); + const [isSignInWithPassword, setIsSignInWithPassword] = useState(false); + + // State to track if the password field has a value + const [hasPasswordValue, setHasPasswordValue] = useState(false); + + // Get handlers for both authentication methods + const passwordHandler = useSignInWithPasswordHandler(); + const passwordlessHandler = useSignInPasswordlessHandler(); + + // Choose the appropriate handler based on whether password is provided + const { + register, + isPending, + handleSignIn, + error, + errors, + formErrors, + getFieldErrorMessage + } = isSignInWithPassword ? passwordHandler : passwordlessHandler; + + // Handle password input changes to determine which auth method to use + const handlePasswordChange = (e: React.ChangeEvent) => { + setHasPasswordValue(!!e.target.value); + }; + + // Toggle password visibility + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + + // Toggle password form field + const togglePasswordField = () => { + setIsSignInWithPassword(!isSignInWithPassword); + }; + + const handleForgotPassword = () => { + router.push(createRoute(ROUTES.AUTH.FORGOT_PASSWORD)) + } + + return ( +
+
+
+
+

+ Welcome back +

+

+ {isSignInWithPassword + ? "Sign in with your password" + : "Sign in with a one-time password sent to your email"} +

+
+ +
+ + +
+
+ +
+
+ or +
+
+
+ +
+
+ + } + error={getFieldErrorMessage('email') || error} + /> +
+ + {isSignInWithPassword && ( + + + +
+ } + error={passwordHandler.getFieldErrorMessage('password') || error} + /> + )} + + + +
+ Don't have an account? + + Contact Us + +
+ +

+ By continuing, you agree to Sigap's{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + , and to receive periodic emails with updates. +

+
+
+ + ); +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx b/sigap-website/app/(pages)/(auth)/sign-in/_components/signin-form.tsx similarity index 73% rename from sigap-website/app/(pages)/(auth)/_components/signin-form.tsx rename to sigap-website/app/(pages)/(auth)/sign-in/_components/signin-form.tsx index 95b66d5..d41c381 100644 --- a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx +++ b/sigap-website/app/(pages)/(auth)/sign-in/_components/signin-form.tsx @@ -4,36 +4,24 @@ import type React from "react"; import { Loader2, Lock } from "lucide-react"; import { Button } from "@/app/_components/ui/button"; import { Input } from "@/app/_components/ui/input"; -import { SubmitButton } from "@/app/_components/submit-button"; import Link from "next/link"; import { FormField } from "@/app/_components/form-field"; -// import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in.controller"; -import { useState } from "react"; +import { useSignInPasswordlessHandler } from "../../_handlers/use-sign-in-passwordless"; -import { useSignInHandler } from "../_handlers/use-sign-in"; export function SignInForm({ className, ...props }: React.ComponentPropsWithoutRef<"form">) { - // const [error, setError] = useState(); - // const [loading, setLoading] = useState(false); - - // const onSubmit = async (event: React.FormEvent) => { - // event.preventDefault(); - // if (loading) return; - - // const formData = new FormData(event.currentTarget); - - // setLoading(true); - // const res = await signIn(formData); - // if (res && res.error) { - // setError(res.error); - // } - // setLoading(false); - // }; - - const { register, isPending, handleSignIn, error, errors } = useSignInHandler(); + const { + register, + isPending, + handleSignIn, + error, + errors, + formErrors, + getFieldErrorMessage + } = useSignInPasswordlessHandler(); return (
@@ -46,15 +34,6 @@ export function SignInForm({

Sign in to your account

- {/* {message && ( -
- {message} -
- )} */} -
); -} +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/sign-in/page.tsx b/sigap-website/app/(pages)/(auth)/sign-in/page.tsx index 90a04e6..9cfee88 100644 --- a/sigap-website/app/(pages)/(auth)/sign-in/page.tsx +++ b/sigap-website/app/(pages)/(auth)/sign-in/page.tsx @@ -1,10 +1,11 @@ -import { SignInForm } from "@/app/(pages)/(auth)/_components/signin-form"; +import { SignInForm } from "@/app/(pages)/(auth)/sign-in/_components/signin-form"; import { Message } from "@/app/_components/form-message"; import { Button } from "@/app/_components/ui/button"; import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/app/_components/ui/carousal"; import { IconQuoteFilled } from "@tabler/icons-react"; import { GalleryVerticalEnd, Globe, QuoteIcon } from "lucide-react"; import { CarousalQuotes } from "./_components/carousal-quote"; +import { SignInWithPasswordForm } from "./_components/sign-in-with-password-form"; export default async function Login(props: { searchParams: Promise }) { return ( @@ -20,7 +21,7 @@ export default async function Login(props: { searchParams: Promise }) {
- +
diff --git a/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx b/sigap-website/app/(pages)/(auth)/verify-otp/_components/verify-otp-form.tsx similarity index 98% rename from sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx rename to sigap-website/app/(pages)/(auth)/verify-otp/_components/verify-otp-form.tsx index a800adb..7655ea2 100644 --- a/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx +++ b/sigap-website/app/(pages)/(auth)/verify-otp/_components/verify-otp-form.tsx @@ -17,7 +17,7 @@ import { cn } from "@/app/_lib/utils"; import { Loader2 } from "lucide-react"; import { Controller } from "react-hook-form"; import { Button } from "@/app/_components/ui/button"; -import { useVerifyOtpHandler } from "../_handlers/use-verify-otp"; +import { useVerifyOtpHandler } from "../../_handlers/use-verify-otp"; interface VerifyOtpFormProps extends React.HTMLAttributes {} diff --git a/sigap-website/app/(pages)/(auth)/verify-otp/page.tsx b/sigap-website/app/(pages)/(auth)/verify-otp/page.tsx index 1d5679a..6c7fccc 100644 --- a/sigap-website/app/(pages)/(auth)/verify-otp/page.tsx +++ b/sigap-website/app/(pages)/(auth)/verify-otp/page.tsx @@ -1,4 +1,4 @@ -import { VerifyOtpForm } from "@/app/(pages)/(auth)/_components/verify-otp-form"; +import { VerifyOtpForm } from "@/app/(pages)/(auth)/verify-otp/_components/verify-otp-form"; import { GalleryVerticalEnd } from "lucide-react"; export default async function VerifyOtpPage() { diff --git a/sigap-website/app/_components/form-field.tsx b/sigap-website/app/_components/form-field.tsx index 904a56c..858da86 100644 --- a/sigap-website/app/_components/form-field.tsx +++ b/sigap-website/app/_components/form-field.tsx @@ -1,17 +1,107 @@ -import { Label } from "./ui/label"; +"use client" -interface FormFieldProps { - label: string; - input: React.ReactNode; - error?: string; +import React, { type ComponentPropsWithoutRef, ReactElement, type ReactNode, isValidElement } from "react" +import { Info } from "lucide-react" +import { Label } from "./ui/label" +import { Button } from "./ui/button" +import { cn } from "../_lib/utils" + +interface FormFieldProps extends ComponentPropsWithoutRef<"div"> { + label: string + link?: boolean + linkText?: string + onClick?: () => void + input: ReactElement + error?: string + description?: string + required?: boolean + size?: "sm" | "md" | "lg" + labelClassName?: string + inputWrapperClassName?: string + hideLabel?: boolean + id?: string } -export function FormField({ label, input, error }: FormFieldProps) { +export function FormField({ + label, + link, + linkText, + onClick, + input, + error, + description, + required = false, + size = "md", + labelClassName, + inputWrapperClassName, + hideLabel = false, + id, + className, + ...props +}: FormFieldProps) { + // Generate an ID for connecting label with input if not provided + const fieldId = id || label.toLowerCase().replace(/\s+/g, "-") + + // Size-based spacing + const sizeClasses = { + sm: "space-y-1", + md: "space-y-2", + lg: "space-y-3", + } + + // Safely clone the input element if it's a valid React element + const inputElement = isValidElement(input) + ? React.cloneElement(input as ReactElement, { + id: fieldId, + "aria-invalid": error ? "true" : "false", + "aria-describedby": error ? `${fieldId}-error` : description ? `${fieldId}-description` : undefined, + }) + : input + return ( -
- - {input} - {error &&

{error}

} +
+
+ {!hideLabel && ( + + )} + + {link && ( + + )} +
+ +
{inputElement}
+ + {description && !error && ( +

+ {description} +

+ )} + + {error && ( + + )}
- ); + ) } + diff --git a/sigap-website/app/_lib/const/number.ts b/sigap-website/app/_lib/const/numbers.ts similarity index 100% rename from sigap-website/app/_lib/const/number.ts rename to sigap-website/app/_lib/const/numbers.ts diff --git a/sigap-website/app/_lib/const/routes.ts b/sigap-website/app/_lib/const/routes.ts new file mode 100644 index 0000000..ebfa518 --- /dev/null +++ b/sigap-website/app/_lib/const/routes.ts @@ -0,0 +1,34 @@ +// src/constants/routes.ts + +/** + * Application route constants for better maintainability + * Use these constants instead of hard-coded strings throughout the app + */ +export const ROUTES = { + // Auth routes + AUTH: { + SIGN_IN_PASSWORDLESS: '/sign-in', + SIGN_IN_WITH_PASSWORD: '/sign-in-password', + SIGN_UP: '/sign-up', + VERIFY_OTP: '/verify-otp', + RESET_PASSWORD: '/reset-password', + FORGOT_PASSWORD: '/forgot-password', + }, + + // Main application routes + APP: { + DASHBOARD: '/dashboard', + PROFILE: '/profile', + SETTINGS: '/settings', + USER_MANAGEMENT: '/dashboard/user-management', + }, + + // Public routes + PUBLIC: { + HOME: '/', + CONTACT: '/contact-us', + ABOUT: '/about', + TERMS: '/terms', + PRIVACY: '/privacy', + } +}; \ No newline at end of file diff --git a/sigap-website/app/_lib/const/string.ts b/sigap-website/app/_lib/const/texts.ts similarity index 100% rename from sigap-website/app/_lib/const/string.ts rename to sigap-website/app/_lib/const/texts.ts diff --git a/sigap-website/app/_utils/common.ts b/sigap-website/app/_utils/common.ts index cd4ba9a..2c25233 100644 --- a/sigap-website/app/_utils/common.ts +++ b/sigap-website/app/_utils/common.ts @@ -330,3 +330,21 @@ export function calculateUserStats(users: IUserSchema[] | undefined) { totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : '0.0', }; } + +/** + * Generate route with query parameters + * @param baseRoute - The base route path + * @param params - Object containing query parameters + * @returns Formatted route with query parameters + */ +export const createRoute = (baseRoute: string, params?: Record): string => { + if (!params || Object.keys(params).length === 0) { + return baseRoute; + } + + const queryString = Object.entries(params) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join('&'); + + return `${baseRoute}?${queryString}`; +}; diff --git a/sigap-website/app/_utils/validation.ts b/sigap-website/app/_utils/validation.ts index 91d0697..f5a13d2 100644 --- a/sigap-website/app/_utils/validation.ts +++ b/sigap-website/app/_utils/validation.ts @@ -1,4 +1,4 @@ -import { CTexts } from "../_lib/const/string"; +import { CTexts } from "../_lib/const/texts"; import { CRegex } from "../_lib/const/regex"; /** diff --git a/sigap-website/src/entities/models/users/create-user.model.ts b/sigap-website/src/entities/models/users/create-user.model.ts index 8fec612..26a00af 100644 --- a/sigap-website/src/entities/models/users/create-user.model.ts +++ b/sigap-website/src/entities/models/users/create-user.model.ts @@ -1,5 +1,5 @@ -import { CNumbers } from "@/app/_lib/const/number"; -import { CTexts } from "@/app/_lib/const/string"; +import { CNumbers } from "@/app/_lib/const/numbers"; +import { CTexts } from "@/app/_lib/const/texts"; import { phonePrefixValidation, phoneRegexValidation } from "@/app/_utils/validation"; import { z } from "zod"; diff --git a/sigap-website/src/interface-adapters/controllers/users/upload-avatar.controller.ts b/sigap-website/src/interface-adapters/controllers/users/upload-avatar.controller.ts index 3f614d4..2cc75e5 100644 --- a/sigap-website/src/interface-adapters/controllers/users/upload-avatar.controller.ts +++ b/sigap-website/src/interface-adapters/controllers/users/upload-avatar.controller.ts @@ -1,5 +1,5 @@ -import { CNumbers } from "@/app/_lib/const/number"; -import { CTexts } from "@/app/_lib/const/string"; +import { CNumbers } from "@/app/_lib/const/numbers"; +import { CTexts } from "@/app/_lib/const/texts"; import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; import { IUploadAvatarUseCase } from "@/src/application/use-cases/users/upload-avatar.use-case"; import { z } from "zod";