This commit is contained in:
Sabda Yagra 2025-07-18 21:59:02 +07:00
parent 2e75d679b3
commit 00ddca22b9
6 changed files with 210 additions and 85 deletions

View File

@ -9,16 +9,17 @@ 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 {
import {
ProfileFormProps,
RegistrationFormData,
InstituteData,
UserCategory,
registrationSchema
registrationSchema,
} from "@/types/registration";
import { useLocationData, useInstituteData, useRegistration } from "@/hooks/use-registration";
import { validatePassword } from "@/lib/registration-utils";
import {
useLocationData,
useInstituteData,
useRegistration,
} from "@/hooks/use-registration";
import dynamic from "next/dynamic";
const PasswordChecklist = dynamic(() => import("react-password-checklist"), {
@ -34,9 +35,20 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
}) => {
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 {
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("");
@ -58,6 +70,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
} = useForm<RegistrationFormData>({
resolver: zodResolver(registrationSchema),
mode: "onChange",
defaultValues: { email: userData, },
});
const watchedPassword = watch("password");
@ -132,8 +145,13 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
instituteId = Number(selectedInstitute);
}
const success = await submitRegistration(data, category, userData, instituteId);
const success = await submitRegistration(
data,
category,
userData,
instituteId
);
if (success) {
onSuccess?.(data);
}
@ -149,7 +167,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
<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>
{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"
@ -175,7 +194,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
<>
<div>
<Label htmlFor="customInstituteName" className="mb-2">
{t("instName", { defaultValue: "Institution Name" })} <span className="text-red-500">*</span>
{t("instName", { defaultValue: "Institution Name" })}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
className="mb-3"
@ -188,11 +208,14 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
</div>
<div>
<Label htmlFor="instituteAddress" className="mb-2">
{t("instAddress", { defaultValue: "Institution Address" })} <span className="text-red-500">*</span>
{t("instAddress", { defaultValue: "Institution Address" })}{" "}
<span className="text-red-500">*</span>
</Label>
<Textarea
className="mb-3"
placeholder={t("addressInst", { defaultValue: "Enter institution address" })}
placeholder={t("addressInst", {
defaultValue: "Enter institution address",
})}
rows={3}
value={instituteAddress}
onChange={(e) => setInstituteAddress(e.target.value)}
@ -212,15 +235,23 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
{(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"}
{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 || ""}
placeholder={t("inputNumberIdentity", {
defaultValue: "Enter identity number",
})}
value={
userData?.journalistCertificate ||
userData?.policeNumber ||
""
}
disabled
/>
</div>
@ -229,17 +260,22 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
{/* 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>
{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" })}
placeholder={t("enterFullName", {
defaultValue: "Enter your full name",
})}
/>
{errors.firstName && (
<div className="text-red-500 text-sm mt-1">{errors.firstName.message}</div>
<div className="text-red-500 text-sm mt-1">
{errors.firstName.message}
</div>
)}
</div>
@ -252,14 +288,20 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
autoComplete="off"
className={errors.username ? "border-red-500" : ""}
{...register("username")}
placeholder={t("enterUsername", { defaultValue: "Enter username" })}
placeholder={t("enterUsername", {
defaultValue: "Enter username",
})}
onChange={(e) => {
const value = e.target.value.replace(/[^\w.-]/g, "").toLowerCase();
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 className="text-red-500 text-sm mt-1">
{errors.username.message}
</div>
)}
</div>
@ -273,42 +315,54 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
className={errors.email ? "border-red-500" : ""}
{...register("email")}
placeholder="Enter your email"
value={userData?.email || ""}
value={userData || ""}
disabled
/>
{errors.email && (
<div className="text-red-500 text-sm mt-1">{errors.email.message}</div>
<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>
{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" })}
placeholder={t("enterNumber", {
defaultValue: "Enter phone number",
})}
/>
{errors.phoneNumber && (
<div className="text-red-500 text-sm mt-1">{errors.phoneNumber.message}</div>
<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>
{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" })}
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 className="text-red-500 text-sm mt-1">
{errors.address.message}
</div>
)}
</div>
@ -318,7 +372,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
{/* 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>
{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 ${
@ -339,13 +394,16 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
))}
</select>
{errors.provinsi && (
<div className="text-red-500 text-sm mt-1">{errors.provinsi.message}</div>
<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>
{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 ${
@ -367,13 +425,16 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
))}
</select>
{errors.kota && (
<div className="text-red-500 text-sm mt-1">{errors.kota.message}</div>
<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>
{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 ${
@ -395,14 +456,20 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
))}
</select>
{errors.kecamatan && (
<div className="text-red-500 text-sm mt-1">{errors.kecamatan.message}</div>
<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
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
@ -414,22 +481,37 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
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}>
<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-slash"
className="w-5 h-5 text-default-400"
/>
) : (
<Icon icon="heroicons:eye" 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 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
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
@ -438,19 +520,32 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
autoComplete="off"
className={errors.passwordConf ? "border-red-500" : ""}
{...register("passwordConf")}
placeholder={t("samePass", { defaultValue: "Confirm your password" })}
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}>
<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-slash"
className="w-5 h-5 text-default-400"
/>
) : (
<Icon icon="heroicons:eye" 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 className="text-red-500 text-sm mt-1">
{errors.passwordConf.message}
</div>
)}
</div>
@ -465,10 +560,20 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
// 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" }),
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" }),
}}
/>
@ -478,10 +583,18 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
<div className="flex justify-center items-center mt-2 mb-4 px-[34px]">
<Button
type="submit"
disabled={isSubmitting || submitLoading || locationLoading || instituteLoading}
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 ? (
{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...
@ -495,4 +608,4 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
</form>
</div>
);
};
};

View File

@ -22,7 +22,8 @@ export const RegistrationOTPForm: React.FC<RegistrationOTPFormProps> = ({
className,
}) => {
const t = useTranslations("LandingPage");
const { verifyOTP, resendOTP, loading, error, formattedTime, canResend } = useOTP();
const { verifyOTP, resendOTP, loading, error, formattedTime, canResend } =
useOTP();
const [otpValue, setOtpValue] = useState("");
const handleOTPChange = (value: string) => {
@ -36,8 +37,14 @@ export const RegistrationOTPForm: React.FC<RegistrationOTPFormProps> = ({
}
try {
const userData = await verifyOTP(email, otpValue, category, memberIdentity);
onSuccess?.(userData);
const userData = await verifyOTP(
email,
otpValue,
category,
memberIdentity
);
if (userData?.error) return false;
onSuccess(email);
} catch (error: any) {
onError?.(error.message || "OTP verification failed");
}
@ -95,39 +102,39 @@ export const RegistrationOTPForm: React.FC<RegistrationOTPFormProps> = ({
className="gap-2"
>
<InputOTPGroup>
<InputOTPSlot
index={0}
<InputOTPSlot
index={0}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
<InputOTPSlot
index={1}
<InputOTPSlot
index={1}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot
index={2}
<InputOTPSlot
index={2}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
<InputOTPSlot
index={3}
<InputOTPSlot
index={3}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot
index={4}
<InputOTPSlot
index={4}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
<InputOTPSlot
index={5}
<InputOTPSlot
index={5}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
@ -151,10 +158,11 @@ export const RegistrationOTPForm: React.FC<RegistrationOTPFormProps> = ({
disabled={!canResend || loading}
className="bg-slate-300 dark:bg-black text-center rounded-lg mr-1 w-[200px] py-2 text-base"
>
{canResend
{canResend
? t("resend", { defaultValue: "Resend OTP" })
: `${t("resending", { defaultValue: "Resending" })} (${formattedTime})`
}
: `${t("resending", {
defaultValue: "Resending",
})} (${formattedTime})`}
</Button>
<Button
type="button"
@ -176,18 +184,21 @@ export const RegistrationOTPForm: React.FC<RegistrationOTPFormProps> = ({
{/* Help Text */}
<div className="text-center px-8">
<p className="text-sm text-gray-600">
{t("otpHelp", { defaultValue: "Didn't receive the code? Check your spam folder or" })}{" "}
{t("otpHelp", {
defaultValue:
"Didn't receive the code? Check your spam folder or",
})}{" "}
<button
type="button"
onClick={handleResendOTP}
disabled={!canResend || loading}
className="text-blue-600 hover:text-blue-800 underline disabled:opacity-50 disabled:cursor-not-allowed"
>
{t("resendOTP", { defaultValue: "resend OTP" })}
{t("resend", { defaultValue: "resend OTP" })}
</button>
</p>
</div>
</div>
</div>
);
};
};

View File

@ -458,7 +458,7 @@ export const useRegistration = () => {
const transformedData = transformRegistrationData(sanitizedData, category, userData, instituteId);
const response = await postRegistration(transformedData);
console.log("PPPP", transformedData)
if (response?.error) {
throw new Error(response.message || "Registration failed");
}

View File

@ -566,6 +566,8 @@
"polda": "Official coverage sourced from Polri activities at Polda",
"satker": "Official coverage sourced from Polri activities at Satker",
"resending": "Resending",
"resend": "Resend",
"otpHelp": "Didn't receive the code? Check your spam folder or",
"regionNews": "Region Police News",
"divisionNews": "Division Police News",
"areaCoverage": "Area Coverage & Divison",

View File

@ -353,6 +353,8 @@
"versionHistory": "VERSION HISTORY"
},
"LandingPage": {
"resend": "Kirim Ulang",
"otpHelp": "Tidak menerima kode? Periksa folder spam Anda atau",
"content": "Konten",
"new": "Terbaru",
"schedule": "Jadwal",

View File

@ -117,10 +117,7 @@ export async function saveInstitutes(data: any) {
export async function postRegistration(data: any) {
const url = "public/users/save";
const headers = {
"content-type": "application/json",
};
return httpPost(url, headers, data);
return httpPost(url, data);
}
export async function requestOTP(data: any) {
@ -176,4 +173,4 @@ export async function getDataJournalist(cert: any) {
export async function getDataPersonil(nrp: any) {
const url = `public/users/search-personil?nrp=${nrp}`;
return httpGetInterceptor(url);
}
}