363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { useTranslations } from "next-intl";
|
|
import { FormField } from "@/components/auth/form-field";
|
|
import {
|
|
IdentityFormProps,
|
|
JournalistRegistrationData,
|
|
PersonnelRegistrationData,
|
|
GeneralRegistrationData,
|
|
UserCategory,
|
|
journalistRegistrationSchema,
|
|
personnelRegistrationSchema,
|
|
generalRegistrationSchema,
|
|
} from "@/types/registration";
|
|
import { useUserDataValidation } from "@/hooks/use-registration";
|
|
import { ASSOCIATIONS } from "@/lib/registration-utils";
|
|
import React, { useState, useRef } from "react";
|
|
|
|
export const IdentityForm: React.FC<IdentityFormProps> = ({
|
|
category,
|
|
onSuccess,
|
|
onError,
|
|
className,
|
|
}) => {
|
|
const t = useTranslations("LandingPage");
|
|
const {
|
|
validateJournalistData,
|
|
validatePersonnelData,
|
|
loading: validationLoading,
|
|
} = useUserDataValidation();
|
|
|
|
const [memberIdentity, setMemberIdentity] = useState("");
|
|
const [memberIdentityError, setMemberIdentityError] = useState("");
|
|
|
|
const [personelName, setPersonelName] = useState<string>("");
|
|
const [loadingPersonel, setLoadingPersonel] = useState(false);
|
|
|
|
const fetchPersonelByNRP = async (nrp: string) => {
|
|
setLoadingPersonel(true);
|
|
try {
|
|
const res = await fetch(
|
|
`https://mediahub.polri.go.id/api/v2/public/users/search-personil?nrp=${nrp}&timemilis=${Date.now()}`
|
|
);
|
|
|
|
if (!res.ok) {
|
|
throw new Error("Personel not found");
|
|
}
|
|
|
|
const json = await res.json();
|
|
|
|
// asumsi responseData ada di json.data
|
|
const name = json?.data?.nama;
|
|
|
|
if (!name) {
|
|
throw new Error("Nama personel tidak ditemukan");
|
|
}
|
|
|
|
setPersonelName(name);
|
|
setValue("policeNumber" as keyof PersonnelRegistrationData, nrp);
|
|
} catch (err: any) {
|
|
setPersonelName("");
|
|
setMemberIdentityError(err.message || "NRP tidak valid");
|
|
} finally {
|
|
setLoadingPersonel(false);
|
|
}
|
|
};
|
|
|
|
// Determine which schema to use based on category
|
|
const getSchema = () => {
|
|
switch (category) {
|
|
case "6":
|
|
return journalistRegistrationSchema;
|
|
case "7":
|
|
return personnelRegistrationSchema;
|
|
default:
|
|
return generalRegistrationSchema;
|
|
}
|
|
};
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors, isSubmitting },
|
|
setValue,
|
|
watch,
|
|
} = useForm<
|
|
| JournalistRegistrationData
|
|
| PersonnelRegistrationData
|
|
| GeneralRegistrationData
|
|
>({
|
|
resolver: zodResolver(getSchema()),
|
|
mode: "onChange",
|
|
});
|
|
|
|
const watchedEmail = watch("email");
|
|
|
|
// const handleMemberIdentityChange = async (value: string) => {
|
|
// setMemberIdentity(value);
|
|
// setMemberIdentityError("");
|
|
|
|
// if (!value.trim()) {
|
|
// return;
|
|
// }
|
|
|
|
// try {
|
|
// if (category === "6") {
|
|
// await validateJournalistData(value);
|
|
// setValue(
|
|
// "journalistCertificate" as keyof JournalistRegistrationData,
|
|
// value
|
|
// );
|
|
// } else if (category === "7") {
|
|
// await validatePersonnelData(value);
|
|
// setValue("policeNumber" as keyof PersonnelRegistrationData, value);
|
|
// }
|
|
// } catch (error: any) {
|
|
// setMemberIdentityError(error.message || "Invalid identity number");
|
|
// }
|
|
// };
|
|
|
|
const handleMemberIdentityChange = (value: string) => {
|
|
setMemberIdentity(value);
|
|
setMemberIdentityError("");
|
|
|
|
// reset nama personel jika input kosong
|
|
if (!value.trim()) {
|
|
setPersonelName("");
|
|
return;
|
|
}
|
|
|
|
// hanya debounce untuk PERSONNEL (category 7)
|
|
if (category !== "7") return;
|
|
|
|
// clear timer sebelumnya
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
|
|
// set timer baru (3 detik)
|
|
debounceTimerRef.current = setTimeout(async () => {
|
|
try {
|
|
await fetchPersonelByNRP(value);
|
|
} catch (error: any) {
|
|
setMemberIdentityError(error.message || "NRP tidak valid");
|
|
}
|
|
}, 2000);
|
|
};
|
|
|
|
const onSubmit = async (
|
|
data:
|
|
| JournalistRegistrationData
|
|
| PersonnelRegistrationData
|
|
| GeneralRegistrationData
|
|
) => {
|
|
try {
|
|
if (category === "7") {
|
|
if (!memberIdentity.trim()) {
|
|
setMemberIdentityError("NRP wajib diisi");
|
|
return;
|
|
}
|
|
|
|
if (!personelName) {
|
|
setMemberIdentityError("Data personel belum valid");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const payload = {
|
|
...data,
|
|
nrp: memberIdentity,
|
|
personelName,
|
|
};
|
|
|
|
// simpan sementara untuk step OTP & next page
|
|
sessionStorage.setItem("registration_identity", JSON.stringify(payload));
|
|
|
|
onSuccess?.(payload);
|
|
} catch (error: any) {
|
|
onError?.(error.message || "Form submission failed");
|
|
}
|
|
};
|
|
|
|
// const onSubmit = async (
|
|
// data:
|
|
// | JournalistRegistrationData
|
|
// | PersonnelRegistrationData
|
|
// | GeneralRegistrationData
|
|
// ) => {
|
|
// try {
|
|
// // Additional validation for member identity
|
|
// if ((category === "6" || category === "7") && !memberIdentity.trim()) {
|
|
// setMemberIdentityError("Identity number is required");
|
|
// return;
|
|
// }
|
|
|
|
// if (memberIdentityError) {
|
|
// onError?.(memberIdentityError);
|
|
// return;
|
|
// }
|
|
|
|
// onSuccess?.(data);
|
|
// } catch (error: any) {
|
|
// onError?.(error.message || "Form submission failed");
|
|
// }
|
|
// };
|
|
|
|
const renderJournalistFields = () => (
|
|
<>
|
|
<div className="flex flex-col px-0 lg:px-8 mb-4">
|
|
<Label htmlFor="association" className="mb-2">
|
|
{t("member", { defaultValue: "Member" })}{" "}
|
|
<span className="text-red-500">*</span>
|
|
</Label>
|
|
<select
|
|
className={`py-2 px-3 rounded-md border text-sm border-slate-300 bg-white dark:bg-slate-600`}
|
|
{...register("association" as keyof JournalistRegistrationData)}
|
|
id="association"
|
|
>
|
|
<option value="" disabled>
|
|
{t("association", { defaultValue: "Select Association" })}
|
|
</option>
|
|
{ASSOCIATIONS.map((association) => (
|
|
<option key={association.id} value={association.value}>
|
|
{association.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{/* {errors.association && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.association.message}</div>
|
|
)} */}
|
|
</div>
|
|
|
|
<div className="px-0 lg:px-[34px] mb-4">
|
|
<Label htmlFor="journalistCertificate">
|
|
{t("journalistNumber", {
|
|
defaultValue: "Journalist Certificate Number",
|
|
})}{" "}
|
|
<span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
className={`mt-2 ${memberIdentityError ? "border-red-500" : ""}`}
|
|
autoComplete="off"
|
|
placeholder={t("inputJournalist", {
|
|
defaultValue: "Enter journalist certificate number",
|
|
})}
|
|
type="text"
|
|
value={memberIdentity}
|
|
onChange={(e) => handleMemberIdentityChange(e.target.value)}
|
|
/>
|
|
{memberIdentityError && (
|
|
<p className="text-red-500 text-sm mt-1">{memberIdentityError}</p>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const renderPersonnelFields = () => (
|
|
<div className="px-0 lg:px-[34px] mb-4">
|
|
<Label htmlFor="policeNumber">
|
|
<b>{t("policeNumber", { defaultValue: "Police Number" })}</b>{" "}
|
|
<span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
className={`mt-2 ${memberIdentityError ? "border-red-500" : ""}`}
|
|
autoComplete="off"
|
|
placeholder="Enter your Police Registration Number"
|
|
type="text"
|
|
value={memberIdentity}
|
|
onChange={(e) => handleMemberIdentityChange(e.target.value)}
|
|
/>
|
|
{memberIdentityError && (
|
|
<p className="text-red-500 text-sm mt-1">{memberIdentityError}</p>
|
|
)}
|
|
|
|
{personelName && (
|
|
<div className="mt-3">
|
|
<Label>Nama Personel</Label>
|
|
<Input
|
|
value={personelName}
|
|
disabled
|
|
className="mt-2 bg-slate-100 dark:bg-slate-700"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className={className}>
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
<div className="flex flex-col w-full px-8 lg:px-12 gap-4">
|
|
{/* Category-specific fields */}
|
|
{category === "6" && renderJournalistFields()}
|
|
{category === "7" && renderPersonnelFields()}
|
|
|
|
{/* Email field - common for all categories */}
|
|
<div className="flex flex-col w-full px-0 lg:px-8 gap-2">
|
|
<Label htmlFor="email">
|
|
<b>Email</b> <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
className={`w-full ${errors.email ? "border-red-500" : ""}`}
|
|
autoComplete="off"
|
|
placeholder={t("inputEmail", {
|
|
defaultValue: "Enter your email",
|
|
})}
|
|
type="email"
|
|
{...register("email")}
|
|
/>
|
|
{errors.email && (
|
|
<p className="text-sm text-red-500 mt-1">
|
|
{errors.email.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Terms and conditions */}
|
|
<div className="text-center mb-2 px-[34px]">
|
|
<p className="text-sm lg:text-base">
|
|
{t("byRegis", {
|
|
defaultValue: "By registering, you agree to our",
|
|
})}{" "}
|
|
<br />{" "}
|
|
<a href="/privacy" target="_blank" className="text-red-500">
|
|
<b>{t("terms", { defaultValue: "Terms of Service" })}</b>
|
|
</a>{" "}
|
|
{t("and", { defaultValue: "and" })}{" "}
|
|
<a href="/privacy" target="_blank" className="text-red-500">
|
|
<b>{t("privacy", { defaultValue: "Privacy Policy" })}</b>
|
|
</a>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Submit button */}
|
|
<div className="mb-5 mt-7 px-[34px] w-full text-center flex justify-center">
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting || validationLoading}
|
|
className="border cursor-pointer border-red-500 px-4 py-3 rounded-lg text-white bg-[#dc3545] w-[550px] hover:bg-red-600 transition-colors"
|
|
>
|
|
{isSubmitting || validationLoading ? (
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
Processing...
|
|
</div>
|
|
) : (
|
|
`${t("send", { defaultValue: "Send" })} OTP`
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|