497 lines
19 KiB
TypeScript
497 lines
19 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
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 { Textarea } from "@/components/ui/textarea";
|
|
import { Icon } from "@/components/ui/icon";
|
|
import { useTranslations } from "next-intl";
|
|
import { FormField } from "@/components/auth/form-field";
|
|
import {
|
|
ProfileFormProps,
|
|
RegistrationFormData,
|
|
InstituteData,
|
|
UserCategory,
|
|
registrationSchema
|
|
} from "@/types/registration";
|
|
import { useLocationData, useInstituteData, useRegistration } from "@/hooks/use-registration";
|
|
import { validatePassword } from "@/lib/registration-utils";
|
|
import dynamic from "next/dynamic";
|
|
|
|
const PasswordChecklist = dynamic(() => import("react-password-checklist"), {
|
|
ssr: false,
|
|
});
|
|
|
|
export const ProfileForm: React.FC<ProfileFormProps> = ({
|
|
userData,
|
|
category,
|
|
onSuccess,
|
|
onError,
|
|
className,
|
|
}) => {
|
|
const t = useTranslations("LandingPage");
|
|
const { submitRegistration, loading: submitLoading } = useRegistration();
|
|
const { provinces, cities, districts, fetchCities, fetchDistricts, loading: locationLoading } = useLocationData();
|
|
const { institutes, saveInstitute, loading: instituteLoading } = useInstituteData(Number(category));
|
|
|
|
const [selectedProvince, setSelectedProvince] = useState("");
|
|
const [selectedCity, setSelectedCity] = useState("");
|
|
const [selectedDistrict, setSelectedDistrict] = useState("");
|
|
const [selectedInstitute, setSelectedInstitute] = useState("");
|
|
const [isCustomInstitute, setIsCustomInstitute] = useState(false);
|
|
const [customInstituteName, setCustomInstituteName] = useState("");
|
|
const [instituteAddress, setInstituteAddress] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [passwordConf, setPasswordConf] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showPasswordConf, setShowPasswordConf] = useState(false);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors, isSubmitting },
|
|
setValue,
|
|
watch,
|
|
} = useForm<RegistrationFormData>({
|
|
resolver: zodResolver(registrationSchema),
|
|
mode: "onChange",
|
|
});
|
|
|
|
const watchedPassword = watch("password");
|
|
|
|
const handleProvinceChange = (provinceId: string) => {
|
|
setSelectedProvince(provinceId);
|
|
setSelectedCity("");
|
|
setSelectedDistrict("");
|
|
setValue("kota", "");
|
|
setValue("kecamatan", "");
|
|
if (provinceId) {
|
|
fetchCities(provinceId);
|
|
}
|
|
};
|
|
|
|
const handleCityChange = (cityId: string) => {
|
|
setSelectedCity(cityId);
|
|
setSelectedDistrict("");
|
|
setValue("kecamatan", "");
|
|
if (cityId) {
|
|
fetchDistricts(cityId);
|
|
}
|
|
};
|
|
|
|
const handleDistrictChange = (districtId: string) => {
|
|
setSelectedDistrict(districtId);
|
|
setValue("kecamatan", districtId);
|
|
};
|
|
|
|
const handleInstituteChange = (instituteId: string) => {
|
|
setSelectedInstitute(instituteId);
|
|
setIsCustomInstitute(instituteId === "0");
|
|
};
|
|
|
|
const handlePasswordChange = (value: string) => {
|
|
setPassword(value);
|
|
setValue("password", value);
|
|
};
|
|
|
|
const handlePasswordConfChange = (value: string) => {
|
|
setPasswordConf(value);
|
|
setValue("passwordConf", value);
|
|
};
|
|
|
|
const togglePasswordVisibility = () => {
|
|
setShowPassword(!showPassword);
|
|
};
|
|
|
|
const togglePasswordConfVisibility = () => {
|
|
setShowPasswordConf(!showPasswordConf);
|
|
};
|
|
|
|
const onSubmit = async (data: RegistrationFormData) => {
|
|
try {
|
|
let instituteId = 1;
|
|
|
|
// Handle custom institute for journalists
|
|
if (category === "6" && isCustomInstitute) {
|
|
if (!customInstituteName.trim() || !instituteAddress.trim()) {
|
|
onError?.("Please fill in all institute details");
|
|
return;
|
|
}
|
|
|
|
const instituteData: InstituteData = {
|
|
name: customInstituteName,
|
|
address: instituteAddress,
|
|
};
|
|
|
|
instituteId = await saveInstitute(instituteData);
|
|
} else if (category === "6" && selectedInstitute) {
|
|
instituteId = Number(selectedInstitute);
|
|
}
|
|
|
|
const success = await submitRegistration(data, category, userData, instituteId);
|
|
|
|
if (success) {
|
|
onSuccess?.(data);
|
|
}
|
|
} catch (error: any) {
|
|
onError?.(error.message || "Registration failed");
|
|
}
|
|
};
|
|
|
|
const renderInstituteFields = () => {
|
|
if (category !== "6") return null;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3 px-0 lg:px-[34px] mb-4">
|
|
<div className="flex flex-col mb-2">
|
|
<Label htmlFor="institute" className="mb-2">
|
|
{t("institutions", { defaultValue: "Institution" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<select
|
|
className="mb-3 p-2 border text-sm text-slate-400 rounded-md border-slate-300 bg-white cursor-pointer"
|
|
id="institute"
|
|
onChange={(e) => handleInstituteChange(e.target.value)}
|
|
value={selectedInstitute}
|
|
>
|
|
<option value="" disabled>
|
|
{t("selectInst", { defaultValue: "Select Institution" })}
|
|
</option>
|
|
{institutes?.map((institute) => (
|
|
<option key={institute.id} value={institute.id}>
|
|
{institute.name}
|
|
</option>
|
|
))}
|
|
<option value="0">
|
|
{t("otherInst", { defaultValue: "Other Institution" })}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
{isCustomInstitute && (
|
|
<>
|
|
<div>
|
|
<Label htmlFor="customInstituteName" className="mb-2">
|
|
{t("instName", { defaultValue: "Institution Name" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
className="mb-3"
|
|
autoComplete="off"
|
|
placeholder="Enter your institution name"
|
|
type="text"
|
|
value={customInstituteName}
|
|
onChange={(e) => setCustomInstituteName(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="instituteAddress" className="mb-2">
|
|
{t("instAddress", { defaultValue: "Institution Address" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Textarea
|
|
className="mb-3"
|
|
placeholder={t("addressInst", { defaultValue: "Enter institution address" })}
|
|
rows={3}
|
|
value={instituteAddress}
|
|
onChange={(e) => setInstituteAddress(e.target.value)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={className}>
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
<div className="flex flex-col px-8 lg:px-12">
|
|
{/* Identity Information (Read-only) */}
|
|
{(category === "6" || category === "7") && (
|
|
<div className="px-0 lg:px-[34px] mb-4">
|
|
<Label className="mb-2">
|
|
{category === "6" ? t("journalistNumber", { defaultValue: "Journalist Number" }) : "NRP"}
|
|
<span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
type="text"
|
|
autoComplete="off"
|
|
className="mb-3"
|
|
placeholder={t("inputNumberIdentity", { defaultValue: "Enter identity number" })}
|
|
value={userData?.journalistCertificate || userData?.policeNumber || ""}
|
|
disabled
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Personal Information */}
|
|
<div className="mb-4 px-0 lg:px-[34px]">
|
|
<Label className="mb-2">
|
|
{t("fullName", { defaultValue: "Full Name" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
type="text"
|
|
autoComplete="off"
|
|
className={errors.firstName ? "border-red-500" : ""}
|
|
{...register("firstName")}
|
|
placeholder={t("enterFullName", { defaultValue: "Enter your full name" })}
|
|
/>
|
|
{errors.firstName && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.firstName.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mb-4 px-0 lg:px-[34px]">
|
|
<Label className="mb-2">
|
|
Username <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
type="text"
|
|
autoComplete="off"
|
|
className={errors.username ? "border-red-500" : ""}
|
|
{...register("username")}
|
|
placeholder={t("enterUsername", { defaultValue: "Enter username" })}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/[^\w.-]/g, "").toLowerCase();
|
|
setValue("username", value);
|
|
}}
|
|
/>
|
|
{errors.username && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.username.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mb-4 px-0 lg:px-[34px]">
|
|
<Label className="mb-2">
|
|
Email <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
type="email"
|
|
autoComplete="off"
|
|
className={errors.email ? "border-red-500" : ""}
|
|
{...register("email")}
|
|
placeholder="Enter your email"
|
|
value={userData?.email || ""}
|
|
disabled
|
|
/>
|
|
{errors.email && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.email.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col px-0 lg:px-[34px] mb-4">
|
|
<Label className="mb-2">
|
|
{t("number", { defaultValue: "Phone Number" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
type="tel"
|
|
autoComplete="off"
|
|
className={errors.phoneNumber ? "border-red-500" : ""}
|
|
{...register("phoneNumber")}
|
|
placeholder={t("enterNumber", { defaultValue: "Enter phone number" })}
|
|
/>
|
|
{errors.phoneNumber && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.phoneNumber.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mb-4 px-0 lg:px-[34px]">
|
|
<Label htmlFor="address" className="mb-2">
|
|
{t("address", { defaultValue: "Address" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Textarea
|
|
className={errors.address ? "border-red-500" : ""}
|
|
{...register("address")}
|
|
placeholder={t("insertAddress", { defaultValue: "Enter your address" })}
|
|
rows={3}
|
|
/>
|
|
{errors.address && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.address.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Institute Fields (Journalist only) */}
|
|
{renderInstituteFields()}
|
|
|
|
{/* Location Fields */}
|
|
<div className="flex flex-col px-0 lg:px-[34px] mb-4">
|
|
<Label htmlFor="provinsi" className="mb-2">
|
|
{t("province", { defaultValue: "Province" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<select
|
|
className={`mb-3 p-2 border rounded-md text-sm text-slate-400 border-slate-300 bg-white cursor-pointer ${
|
|
errors.provinsi ? "border-red-500" : ""
|
|
}`}
|
|
{...register("provinsi")}
|
|
id="provinsi"
|
|
onChange={(e) => handleProvinceChange(e.target.value)}
|
|
value={selectedProvince}
|
|
>
|
|
<option value="" disabled>
|
|
{t("selectProv", { defaultValue: "Select Province" })}
|
|
</option>
|
|
{provinces.map((province) => (
|
|
<option key={province.id} value={province.id}>
|
|
{province.provName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{errors.provinsi && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.provinsi.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col px-0 lg:px-[34px] mb-4">
|
|
<Label htmlFor="kota" className="mb-2">
|
|
{t("city", { defaultValue: "City" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<select
|
|
className={`mb-3 p-2 border text-sm text-slate-400 rounded-md border-slate-300 bg-white cursor-pointer ${
|
|
errors.kota ? "border-red-500" : ""
|
|
}`}
|
|
{...register("kota")}
|
|
id="kota"
|
|
onChange={(e) => handleCityChange(e.target.value)}
|
|
value={selectedCity}
|
|
disabled={!selectedProvince}
|
|
>
|
|
<option value="" disabled>
|
|
{t("selectCity", { defaultValue: "Select City" })}
|
|
</option>
|
|
{cities.map((city) => (
|
|
<option key={city.id} value={city.id}>
|
|
{city.cityName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{errors.kota && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.kota.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col px-0 lg:px-[34px] mb-4">
|
|
<Label htmlFor="kecamatan" className="mb-2">
|
|
{t("subdistrict", { defaultValue: "Subdistrict" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<select
|
|
className={`p-2 border text-sm text-slate-400 rounded-md border-slate-300 bg-white cursor-pointer ${
|
|
errors.kecamatan ? "border-red-500" : ""
|
|
}`}
|
|
{...register("kecamatan")}
|
|
id="kecamatan"
|
|
onChange={(e) => handleDistrictChange(e.target.value)}
|
|
value={selectedDistrict}
|
|
disabled={!selectedCity}
|
|
>
|
|
<option value="" disabled>
|
|
{t("selectSub", { defaultValue: "Select Subdistrict" })}
|
|
</option>
|
|
{districts.map((district) => (
|
|
<option key={district.id} value={district.id}>
|
|
{district.disName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{errors.kecamatan && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.kecamatan.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Password Fields */}
|
|
<div className="mt-3.5 space-y-2 px-0 lg:px-[34px] mb-4">
|
|
<Label htmlFor="password" className="mb-2 font-medium text-default-600">
|
|
{t("password", { defaultValue: "Password" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<div className="relative">
|
|
<Input
|
|
size="lg"
|
|
type={showPassword ? "text" : "password"}
|
|
autoComplete="off"
|
|
className={errors.password ? "border-red-500" : ""}
|
|
{...register("password")}
|
|
placeholder={t("inputPass", { defaultValue: "Enter password" })}
|
|
onChange={(e) => handlePasswordChange(e.target.value)}
|
|
/>
|
|
<div className="absolute top-1/2 -translate-y-1/2 ltr:right-4 rtl:left-4 cursor-pointer" onClick={togglePasswordVisibility}>
|
|
{showPassword ? (
|
|
<Icon icon="heroicons:eye-slash" className="w-5 h-5 text-default-400" />
|
|
) : (
|
|
<Icon icon="heroicons:eye" className="w-5 h-5 text-default-400" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
{errors.password && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.password.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-3.5 space-y-2 px-0 lg:px-[34px] mb-4">
|
|
<Label htmlFor="passwordConf" className="mb-2 font-medium text-default-600">
|
|
{t("confirmPass", { defaultValue: "Confirm Password" })} <span className="text-red-500">*</span>
|
|
</Label>
|
|
<div className="relative">
|
|
<Input
|
|
size="lg"
|
|
type={showPasswordConf ? "text" : "password"}
|
|
autoComplete="off"
|
|
className={errors.passwordConf ? "border-red-500" : ""}
|
|
{...register("passwordConf")}
|
|
placeholder={t("samePass", { defaultValue: "Confirm your password" })}
|
|
onChange={(e) => handlePasswordConfChange(e.target.value)}
|
|
/>
|
|
<div className="absolute top-1/2 -translate-y-1/2 ltr:right-4 rtl:left-4 cursor-pointer" onClick={togglePasswordConfVisibility}>
|
|
{showPasswordConf ? (
|
|
<Icon icon="heroicons:eye-slash" className="w-5 h-5 text-default-400" />
|
|
) : (
|
|
<Icon icon="heroicons:eye" className="w-5 h-5 text-default-400" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
{errors.passwordConf && (
|
|
<div className="text-red-500 text-sm mt-1">{errors.passwordConf.message}</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Password Checklist */}
|
|
<div className="form-group px-0 lg:px-[34px] mb-4">
|
|
<PasswordChecklist
|
|
rules={["minLength", "specialChar", "number", "capital", "match"]}
|
|
minLength={8}
|
|
value={password}
|
|
valueAgain={passwordConf}
|
|
onChange={(isValid: boolean) => {
|
|
// Password validation is handled by the form schema
|
|
}}
|
|
messages={{
|
|
minLength: t("passCharacter", { defaultValue: "Password must be at least 8 characters" }),
|
|
specialChar: t("passSpecial", { defaultValue: "Password must contain at least one special character" }),
|
|
number: t("passNumber", { defaultValue: "Password must contain at least one number" }),
|
|
capital: t("passCapital", { defaultValue: "Password must contain at least one uppercase letter" }),
|
|
match: t("passSame", { defaultValue: "Passwords must match" }),
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<div className="flex justify-center items-center mt-2 mb-4 px-[34px]">
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting || submitLoading || locationLoading || instituteLoading}
|
|
className="border w-[550px] text-center bg-red-700 text-white hover:bg-white hover:text-red-700"
|
|
>
|
|
{isSubmitting || submitLoading || locationLoading || instituteLoading ? (
|
|
<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("register", { defaultValue: "Register" })
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|