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

799 lines
26 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 {
ProfileFormProps,
RegistrationFormData,
InstituteData,
registrationSchema,
} from "@/types/registration";
import {
useLocationData,
useInstituteData,
useRegistration,
} from "@/hooks/use-registration";
import dynamic from "next/dynamic";
const PasswordChecklist = dynamic(() => import("react-password-checklist"), {
ssr: false,
});
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
const MySwal = withReactContent(Swal);
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 [usernameTouched, setUsernameTouched] = useState(false);
const [otpIdentity, setOtpIdentity] = useState<{
email: string;
nrp?: string;
personelName?: string;
}>({
email: "",
});
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setValue,
watch,
} = useForm<RegistrationFormData>({
resolver: zodResolver(registrationSchema),
mode: "onChange",
defaultValues: { email: userData },
});
React.useEffect(() => {
const stored = sessionStorage.getItem("registration_identity");
if (stored) {
const parsed = JSON.parse(stored);
setOtpIdentity({
email: parsed.email,
nrp: parsed.nrp,
personelName: parsed.personelName,
});
// default nama dari API
if (parsed.personelName) {
setValue("firstName", parsed.personelName);
const autoUsername = parsed.personelName
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
setValue("username", autoUsername);
}
setValue("email", parsed.email);
}
}, [setValue]);
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 = {
// id: "0",
// 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 onSubmit = async (data: RegistrationFormData) => {
// 🔹 TAMPILKAN LOADING
MySwal.fire({
title: "Processing",
text: "Pleasewait",
allowOutsideClick: false,
allowEscapeKey: false,
didOpen: () => {
Swal.showLoading();
},
});
try {
let instituteId = 1; // default (non-journalist / personel)
// khusus journalist
if (category === "6") {
if (isCustomInstitute) {
if (!customInstituteName.trim() || !instituteAddress.trim()) {
Swal.close();
onError?.("Please fill in all institute details");
return;
}
const instituteData: InstituteData = {
id: "0",
name: customInstituteName,
address: instituteAddress,
};
instituteId = await saveInstitute(instituteData);
} else if (selectedInstitute) {
instituteId = Number(selectedInstitute);
}
}
const payload = {
...data,
email: otpIdentity.email,
nrp: otpIdentity.nrp,
personelName: otpIdentity.personelName,
};
const success = await submitRegistration(
payload,
category,
otpIdentity.email,
instituteId
);
// 🔹 TUTUP LOADING
Swal.close();
if (success) {
sessionStorage.removeItem("registration_identity");
await Swal.fire({
icon: "success",
title:"Success",
text: "Register Success !!",
confirmButtonColor: "#dc3545",
});
onSuccess?.(payload);
}
} catch (error: any) {
// 🔹 TUTUP LOADING
Swal.close();
Swal.fire({
icon: "error",
title: "Failed",
text: error?.message || "Registration failed",
confirmButtonColor: "#dc3545",
});
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
/> */}
<Input
type="text"
autoComplete="off"
className="mb-3"
value={category === "7" ? otpIdentity.nrp || "" : ""}
disabled
/>
</div>
)}
{/* Personal Information */}
{category === "7" && (
<div className="px-0 lg:px-[34px] mb-4">
<Label className="mb-2">
Nama Personel <span className="text-red-500">*</span>
</Label>
<Input
type="text"
autoComplete="off"
className={errors.firstName ? "border-red-500" : ""}
{...register("firstName")}
onChange={(e) => {
const name = e.target.value;
// set nama
setValue("firstName", name);
// auto-generate username
const username = name
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
setValue("username", username);
}}
/>
{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">
{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);
// }}
onChange={(e) => {
const value = e.target.value
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
setValue("username", value);
}}
/> */}
<Input
{...register("username")}
onChange={(e) => {
setUsernameTouched(true);
const value = e.target.value
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
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 || ""}
disabled
/> */}
<Input {...register("email")} value={otpIdentity.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>
);
};