284 lines
9.1 KiB
TypeScript
284 lines
9.1 KiB
TypeScript
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<typeof formSchema>;
|
|
|
|
// Interface for props of CustomFormField
|
|
interface CustomFormFieldProps {
|
|
name: keyof FormSchema;
|
|
label: string;
|
|
control: Control<FormSchema>;
|
|
type?: string;
|
|
placeholder?: string;
|
|
error?: FieldError;
|
|
}
|
|
|
|
// Component for form fields with bold labels
|
|
const CustomFormField: React.FC<CustomFormFieldProps> = ({
|
|
name,
|
|
label,
|
|
control,
|
|
type = "text",
|
|
placeholder,
|
|
error,
|
|
}) => (
|
|
<FormField
|
|
control={control}
|
|
name={name}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="font-bold">{label}</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder={placeholder}
|
|
type={type}
|
|
{...field}
|
|
className={`border ${error ? "border-red-500" : "border-gray-300"}`}
|
|
/>
|
|
</FormControl>
|
|
{error && <p className="text-red-500">{error.message}</p>}
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
);
|
|
|
|
interface DropdownProps {
|
|
onSelect: (selectedOption: string) => void;
|
|
defaultOption?: string;
|
|
listOption?: string[];
|
|
}
|
|
|
|
const CustomDropdownMenu: React.FC<DropdownProps> = ({
|
|
onSelect,
|
|
defaultOption = "",
|
|
listOption = [],
|
|
}) => {
|
|
const [selectedOption, setSelectedOption] = useState(defaultOption);
|
|
|
|
const handleSelect = (option: string) => {
|
|
setSelectedOption(option);
|
|
onSelect(option);
|
|
};
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button className="bg-transparent text-gray-800 w-full justify-between text-xs hover:bg-blue-100">
|
|
<div className="flex items-center gap-1">
|
|
<HiOutlineGlobeAlt className="h-3 w-3" />
|
|
{selectedOption || "Select an option"}
|
|
</div>
|
|
<IoIosArrowUp className="h-3 w-3 ml-auto" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuRadioGroup
|
|
value={selectedOption}
|
|
onValueChange={handleSelect}
|
|
>
|
|
{listOption.map((option, index) => (
|
|
<DropdownMenuRadioItem key={index} value={option}>
|
|
{option}
|
|
</DropdownMenuRadioItem>
|
|
))}
|
|
</DropdownMenuRadioGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
};
|
|
|
|
// Define a route for the registration form
|
|
export const Route = createLazyFileRoute("/forgot-password/verify")({
|
|
component: () => (
|
|
<div>
|
|
<ResetPasswordForm />
|
|
</div>
|
|
),
|
|
});
|
|
|
|
// Main component of the reset password form
|
|
export function ResetPasswordForm() {
|
|
const [token, setToken] = useState<string | null>(null);
|
|
|
|
// Set up form with react-hook-form and zod
|
|
const form = useForm<FormSchema>({
|
|
// 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 (
|
|
<div className="flex flex-col lg:flex-row min-h-screen w-screen overflow-hidden">
|
|
<div
|
|
className="flex w-screen h-screen items-start lg:items-center justify-center lg:justify-end absolute -top-56 lg:top-0 lg:overflow-hidden"
|
|
>
|
|
<div className="flex absolute border border-slate-200 rounded-2xl w-[455px] h-[455px] lg:w-[650px] lg:h-[650px] items-center justify-center -rotate-[15deg]">
|
|
<div className="flex absolute border border-slate-300 rounded-2xl w-2/3 h-2/3 lg:w-4/5 lg:h-4/5 items-center justify-center">
|
|
<div className="flex absolute border border-slate-400 rounded-2xl w-3/5 h-3/5 lg:w-3/4 lg:h-3/4 items-center justify-center">
|
|
<div className="hidden lg:flex absolute border border-slate-500 rounded-2xl w-2/3 h-2/3">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col min-h-screen p-7 bg-transparent justify-between z-20">
|
|
{/* Top */}
|
|
<div className="flex items-center font-bold">Amati</div>
|
|
|
|
{/* Center */}
|
|
<div className="flex flex-col h-full w-full bg-transparent justify-center lg:px-28">
|
|
<div className="flex flex-col w-full gap-y-1 pb-12 justify-between lg:justify-end">
|
|
<h1
|
|
className="text-4xl font-bold"
|
|
style={{ color: "#000000" }}
|
|
>
|
|
Change Password
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">Enter your new password</p>
|
|
</div>
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="flex flex-col gap-12 lg:w-96"
|
|
>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<CustomFormField
|
|
control={form.control}
|
|
name="password"
|
|
label="Password"
|
|
type="password"
|
|
placeholder="password"
|
|
error={form.formState.errors.password}
|
|
/>
|
|
<CustomFormField
|
|
control={form.control}
|
|
name="confirm_password"
|
|
label="Password Confirmation"
|
|
type="password"
|
|
placeholder="password confirmation"
|
|
error={form.formState.errors.confirm_password}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col justify-between gap-9">
|
|
<Button
|
|
type="submit"
|
|
style={{
|
|
backgroundColor: "#2555FF",
|
|
color: "white",
|
|
width: "100%",
|
|
}}
|
|
className="flex items-center justify-between shadow-xl"
|
|
>
|
|
<span className="flex">Submit</span>
|
|
<TbArrowNarrowRight className="h-5 w-5" />
|
|
</Button>
|
|
<a
|
|
href="/login"
|
|
className="text-xs text-blue-500 hover:underline font-bold"
|
|
>
|
|
Back to login
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center lg:justify-start w-56 h-8 mx-auto lg:mx-0 bg-muted rounded-md">
|
|
<CustomDropdownMenu
|
|
onSelect={handleSelect}
|
|
defaultOption="English (United States)"
|
|
listOption={["English (United States)", "Indonesia"]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|