mediahub-fe/components/auth/profile-form.tsx

497 lines
19 KiB
TypeScript
Raw Normal View History

"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>
);
};