diff --git a/apps/frontend/package.json b/apps/frontend/package.json index b89c51a..37d7123 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -10,11 +10,15 @@ }, "dependencies": { "@emotion/react": "^11.11.4", + "@hookform/resolvers": "^3.9.0", "@mantine/core": "^7.10.2", "@mantine/dates": "^7.10.2", "@mantine/form": "^7.10.2", "@mantine/hooks": "^7.10.2", "@mantine/notifications": "^7.10.2", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.45.0", "@tanstack/react-router": "^1.38.1", @@ -28,6 +32,7 @@ "mantine-form-zod-resolver": "^1.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", "react-icons": "^5.2.1", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", diff --git a/apps/frontend/src/routes/forgot-password/index.lazy.tsx b/apps/frontend/src/routes/forgot-password/index.lazy.tsx new file mode 100644 index 0000000..8f0a73e --- /dev/null +++ b/apps/frontend/src/routes/forgot-password/index.lazy.tsx @@ -0,0 +1,242 @@ +import { createLazyFileRoute } from "@tanstack/react-router"; +import { TbArrowNarrowRight } from "react-icons/tb"; +import { IoIosArrowUp } from "react-icons/io"; +import { HiOutlineGlobeAlt } from "react-icons/hi"; +import { useForm, Control, FieldError } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Button } from "@/shadcn/components/ui/button.tsx"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/shadcn/components/ui/form.tsx"; +import { Input } from "@/shadcn/components/ui/input.tsx"; +import client from "@/honoClient"; +import { useState } from "react"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "@/shadcn/components/ui/dropdown-menu"; + +// Define validation schema using zod +const formSchema = z.object({ + email: z.string().email(), +}); + +type FormSchema = z.infer; + +// Interface for props of CustomFormField +interface CustomFormFieldProps { + name: keyof FormSchema; + label: string; + control: Control; + type?: string; + placeholder?: string; + error?: FieldError; // Add error prop +} + +// Component for form fields with bold labels +const CustomFormField: React.FC = ({ + name, + label, + control, + type = "text", + placeholder, + error, +}) => ( + ( + + {label} + + + + + + )} + /> +); + +interface DropdownProps { + onSelect: (selectedOption: string) => void; + defaultOption?: string; + listOption?: string[]; +} + +const CustomDropdownMenu: React.FC = ({ + onSelect, + defaultOption = "", + listOption = [], +}) => { + const [selectedOption, setSelectedOption] = useState(defaultOption); + + const handleSelect = (option: string) => { + setSelectedOption(option); + onSelect(option); + }; + + return ( + + + + + + + {listOption.map((option, index) => ( + + {option} + + ))} + + + + ); +}; + +// Define a route for the registration form +export const Route = createLazyFileRoute("/forgot-password/")({ + component: () => ( +
+ +
+ ), +}); + +// Main components of the registration form +export function ForgotPasswordForm() { + // Set up form with react-hook-form and zod + const form = useForm({ + // Integrate schema with form + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + }, + }); + + // Function to handle form submission + const onSubmit = async (values: FormSchema) => { + try { + const response = await client["forgot-password"].$post({ + json: values, + }); + + if (response.ok) { + // Handle successful registration here + alert("Reset instructions sent successfully"); + const data = await response.json(); + return data; + } else { + throw response; + } + } catch (error) { + // Handle registration error here + console.error("Submission error:", error); + alert("Failed to send reset instructions"); + } + }; + + const handleSelect = (selectedOption: string) => { + console.log('Selected option:', selectedOption); + // Do something with selected option + }; + + return ( +
+
+
+
+
+
+
+
+
+
+
+ +
+ {/* Top */} +
Amati
+ + {/* Center */} +
+
+

+ Forgot Password +

+

+ No worries, we'll send you reset instructions +

+
+
+ +
+ +
+
+ + + Back to login + +
+
+ +
+ + {/* Bottom */} +
+ +
+
+
+ ); +} diff --git a/apps/frontend/src/routes/forgot-password/verify.lazy.tsx b/apps/frontend/src/routes/forgot-password/verify.lazy.tsx new file mode 100644 index 0000000..23d5b94 --- /dev/null +++ b/apps/frontend/src/routes/forgot-password/verify.lazy.tsx @@ -0,0 +1,283 @@ +import { createLazyFileRoute } from "@tanstack/react-router"; +import { TbArrowNarrowRight } from "react-icons/tb"; +import { useForm, Control, FieldError } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Button } from "@/shadcn/components/ui/button.tsx"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/shadcn/components/ui/form.tsx"; +import { Input } from "@/shadcn/components/ui/input.tsx"; +import { useEffect, useState } from "react"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "@/shadcn/components/ui/dropdown-menu"; +import { HiOutlineGlobeAlt } from "react-icons/hi"; +import { IoIosArrowUp } from "react-icons/io"; + +/// Define validation schema using zod +const formSchema = z + .object({ + password: z.string().min(1, "Password is required"), + confirm_password: z.string().min(1, "Password confirmation is required"), + }) + .refine((data) => data.password === data.confirm_password, { + message: "Passwords do not match", + path: ["confirm_password"], // Set error on confirm_password field + }); + +type FormSchema = z.infer; + +// Interface for props of CustomFormField +interface CustomFormFieldProps { + name: keyof FormSchema; + label: string; + control: Control; + type?: string; + placeholder?: string; + error?: FieldError; +} + +// Component for form fields with bold labels +const CustomFormField: React.FC = ({ + name, + label, + control, + type = "text", + placeholder, + error, +}) => ( + ( + + {label} + + + + {error &&

{error.message}

} +
+ )} + /> +); + +interface DropdownProps { + onSelect: (selectedOption: string) => void; + defaultOption?: string; + listOption?: string[]; +} + +const CustomDropdownMenu: React.FC = ({ + onSelect, + defaultOption = "", + listOption = [], +}) => { + const [selectedOption, setSelectedOption] = useState(defaultOption); + + const handleSelect = (option: string) => { + setSelectedOption(option); + onSelect(option); + }; + + return ( + + + + + + + {listOption.map((option, index) => ( + + {option} + + ))} + + + + ); +}; + +// Define a route for the registration form +export const Route = createLazyFileRoute("/forgot-password/verify")({ + component: () => ( +
+ +
+ ), +}); + +// Main component of the reset password form +export function ResetPasswordForm() { + const [token, setToken] = useState(null); + + // Set up form with react-hook-form and zod + const form = useForm({ + // Integrate schema with form + resolver: zodResolver(formSchema), + defaultValues: { + password: "", + confirm_password: "", + }, + }); + + // Use effect to get token from URL when component mounts + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const tokenFromURL = urlParams.get("token"); + setToken(tokenFromURL); + }, []); + + // Function to handle form submission + const onSubmit = async (values: FormSchema) => { + try { + if (!token) { + alert("Token not found in URL"); + return; + } + + // Create URL with token as query parameter + const urlWithToken = import.meta.env.VITE_BACKEND_BASE_URL + `/forgot-password/verify?token=${token}`; + + const response = await fetch(urlWithToken, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(values), + }); + + // Check status response + if (response.ok) { + // Periksa apakah respons memiliki konten + const responseText = await response.text(); + let data = {}; + try { + data = responseText ? JSON.parse(responseText) : {}; // Parsing respons hanya jika ada teks + } catch (jsonError) { + console.error("Error parsing JSON response:", jsonError); + alert("Failed to parse server response"); + } + alert("Password reset successfully"); + return data; + } else { + // Tangani kasus jika respons tidak OK + const errorText = await response.text(); + console.error("Server error:", errorText); + alert("Failed to reset password"); + } + } catch (error) { + console.error("Submission error:", error); + alert("Failed to reset password"); + } + }; + + const handleSelect = (selectedOption: string) => { + console.log('Selected option:', selectedOption); + // Do something with selected option + }; + + return ( +
+
+
+
+
+
+
+
+
+
+
+ +
+ {/* Top */} +
Amati
+ + {/* Center */} +
+
+

+ Change Password +

+

Enter your new password

+
+
+ +
+ + +
+
+ + + Back to login + +
+
+ +
+ +
+ +
+
+
+ ); +} diff --git a/apps/frontend/src/shadcn/components/ui/checkbox.tsx b/apps/frontend/src/shadcn/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/apps/frontend/src/shadcn/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/apps/frontend/src/shadcn/components/ui/dropdown-menu.tsx b/apps/frontend/src/shadcn/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/apps/frontend/src/shadcn/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/apps/frontend/src/shadcn/components/ui/form.tsx b/apps/frontend/src/shadcn/components/ui/form.tsx new file mode 100644 index 0000000..44dc758 --- /dev/null +++ b/apps/frontend/src/shadcn/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/shadcn/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +