"use client"; import { useState, useCallback, useEffect } from "react"; import { useRouter } from "@/components/navigation"; import { toast } from "sonner"; import { RegistrationFormData, JournalistRegistrationData, PersonnelRegistrationData, GeneralRegistrationData, InstituteData, UserCategory, PasswordValidation, TimerState, Association, OTPRequestResponse, OTPVerificationResponse, LocationResponse, InstituteResponse, RegistrationResponse, LocationData, } from "@/types/registration"; import { requestOTP, verifyRegistrationOTP, listProvince, listCity, listDistricts, listInstitusi, saveInstitutes, postRegistration, getDataJournalist, getDataPersonil, verifyOTP, } from "@/service/auth"; import { validatePassword, validateUsername, validateEmail, validatePhoneNumber, createTimer, formatTime, sanitizeRegistrationData, sanitizeInstituteData, isValidCategory, getCategoryRoleId, showRegistrationError, showRegistrationSuccess, showRegistrationInfo, transformRegistrationData, createInitialFormData, validateIdentityData, RegistrationRateLimiter, REGISTRATION_CONSTANTS, } from "@/lib/registration-utils"; // Global rate limiter instance const registrationRateLimiter = new RegistrationRateLimiter(); // Hook for OTP operations export const useOTP = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [timer, setTimer] = useState(createTimer()); const startTimer = useCallback(() => { setTimer(createTimer()); }, []); const stopTimer = useCallback(() => { setTimer(prev => ({ ...prev, isActive: false, isExpired: true, })); }, []); // Timer effect useEffect(() => { if (!timer.isActive || timer.countdown <= 0) { setTimer(prev => ({ ...prev, isActive: false, isExpired: true, })); return; } const interval = setInterval(() => { setTimer(prev => ({ ...prev, countdown: Math.max(0, prev.countdown - 1000), })); }, 1000); return () => clearInterval(interval); }, [timer.isActive, timer.countdown]); const requestOTPCode = useCallback(async ( email: string, category: UserCategory, memberIdentity?: string ): Promise => { try { setLoading(true); setError(null); // Check rate limiting const identifier = `${email}-${category}`; if (!registrationRateLimiter.canAttempt(identifier)) { const remainingAttempts = registrationRateLimiter.getRemainingAttempts(identifier); throw new Error(`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`); } const data = { memberIdentity: memberIdentity || null, email, category: getCategoryRoleId(category), }; // Debug logging console.log("OTP Request Data:", data); console.log("Category before conversion:", category); console.log("Category after conversion:", getCategoryRoleId(category)); const response = await requestOTP(data); if (response?.error) { registrationRateLimiter.recordAttempt(identifier); throw new Error(response.message || "Failed to send OTP"); } // Start timer on successful OTP request startTimer(); showRegistrationInfo("OTP sent successfully. Please check your email."); return true; } catch (error: any) { const errorMessage = error?.message || "Failed to send OTP"; setError(errorMessage); showRegistrationError(error, "Failed to send OTP"); return false; } finally { setLoading(false); } }, [startTimer]); const verifyOTPCode = useCallback(async ( email: string, otp: string, category: UserCategory, memberIdentity?: string ): Promise => { try { setLoading(true); setError(null); if (otp.length !== 6) { throw new Error("OTP must be exactly 6 digits"); } const data = { memberIdentity: memberIdentity || null, email, otp, category: getCategoryRoleId(category), }; const response = await verifyOTP(data.email, data.otp); if (response?.error) { throw new Error(response.message || "OTP verification failed"); } stopTimer(); showRegistrationSuccess("OTP verified successfully"); return response?.data?.userData; } catch (error: any) { const errorMessage = error?.message || "OTP verification failed"; setError(errorMessage); showRegistrationError(error, "OTP verification failed"); throw error; } finally { setLoading(false); } }, [stopTimer]); const resendOTP = useCallback(async ( email: string, category: UserCategory, memberIdentity?: string ): Promise => { return await requestOTPCode(email, category, memberIdentity); }, [requestOTPCode]); return { requestOTP: requestOTPCode, verifyOTP: verifyOTPCode, resendOTP, loading, error, timer, formattedTime: formatTime(timer.countdown), canResend: timer.isExpired, }; }; // Hook for location data export const useLocationData = () => { const [provinces, setProvinces] = useState([]); const [cities, setCities] = useState([]); const [districts, setDistricts] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchProvinces = useCallback(async () => { try { setLoading(true); setError(null); const response = await listProvince(); if (!response || response.error) { throw new Error(response?.message || "Failed to fetch provinces"); } setProvinces(response?.data?.data || []); } catch (error: any) { const errorMessage = error?.message || "Failed to fetch provinces"; setError(errorMessage); showRegistrationError(error, "Failed to fetch provinces"); } finally { setLoading(false); } }, []); const fetchCities = useCallback(async (provinceId: string) => { try { setLoading(true); setError(null); const response = await listCity(provinceId); if (response?.error) { throw new Error(response.message || "Failed to fetch cities"); } setCities(response?.data?.data || []); setDistricts([]); // Reset districts when province changes } catch (error: any) { const errorMessage = error?.message || "Failed to fetch cities"; setError(errorMessage); showRegistrationError(error, "Failed to fetch cities"); } finally { setLoading(false); } }, []); const fetchDistricts = useCallback(async (cityId: string) => { try { setLoading(true); setError(null); const response = await listDistricts(cityId); if (response?.error) { throw new Error(response.message || "Failed to fetch districts"); } setDistricts(response?.data?.data || []); } catch (error: any) { const errorMessage = error?.message || "Failed to fetch districts"; setError(errorMessage); showRegistrationError(error, "Failed to fetch districts"); } finally { setLoading(false); } }, []); // Load provinces on mount useEffect(() => { fetchProvinces(); }, [fetchProvinces]); return { provinces, cities, districts, loading, error, fetchProvinces, fetchCities, fetchDistricts, }; }; // Hook for institute data export const useInstituteData = (category?: number) => { const [institutes, setInstitutes] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchInstitutes = useCallback(async (categoryId?: number) => { try { setLoading(true); setError(null); const response = await listInstitusi(categoryId || category); if (response?.error) { throw new Error(response.message || "Failed to fetch institutes"); } setInstitutes(response?.data?.data || []); } catch (error: any) { const errorMessage = error?.message || "Failed to fetch institutes"; setError(errorMessage); showRegistrationError(error, "Failed to fetch institutes"); } finally { setLoading(false); } }, [category]); const saveInstitute = useCallback(async (instituteData: InstituteData): Promise => { try { setLoading(true); setError(null); const sanitizedData = sanitizeInstituteData(instituteData); const response = await saveInstitutes({ name: sanitizedData.name, address: sanitizedData.address, categoryRoleId: category || 6, // Use provided category or default to Journalist category }); if (response?.error) { throw new Error(response.message || "Failed to save institute"); } return response?.data?.data?.id || 1; } catch (error: any) { const errorMessage = error?.message || "Failed to save institute"; setError(errorMessage); showRegistrationError(error, "Failed to save institute"); throw error; } finally { setLoading(false); } }, [category]); // Load institutes on mount if category is provided useEffect(() => { if (category) { fetchInstitutes(); } }, [fetchInstitutes, category]); return { institutes, loading, error, fetchInstitutes, saveInstitute, }; }; // Hook for user data validation export const useUserDataValidation = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const validateJournalistData = useCallback(async (certificateNumber: string): Promise => { try { setLoading(true); setError(null); const response = await getDataJournalist(certificateNumber); if (response?.error) { throw new Error(response.message || "Invalid journalist certificate number"); } return response?.data?.data; } catch (error: any) { const errorMessage = error?.message || "Failed to validate journalist data"; setError(errorMessage); showRegistrationError(error, "Failed to validate journalist data"); throw error; } finally { setLoading(false); } }, []); const validatePersonnelData = useCallback(async (policeNumber: string): Promise => { try { setLoading(true); setError(null); const response = await getDataPersonil(policeNumber); if (response?.error) { throw new Error(response.message || "Invalid police number"); } return response?.data?.data; } catch (error: any) { const errorMessage = error?.message || "Failed to validate personnel data"; setError(errorMessage); showRegistrationError(error, "Failed to validate personnel data"); throw error; } finally { setLoading(false); } }, []); return { validateJournalistData, validatePersonnelData, loading, error, }; }; // Hook for registration submission export const useRegistration = () => { const router = useRouter(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const submitRegistration = useCallback(async ( data: RegistrationFormData, category: UserCategory, userData: any, instituteId?: number ): Promise => { try { setLoading(true); setError(null); // Sanitize and validate data const sanitizedData = sanitizeRegistrationData(data); // Validate password const passwordValidation = validatePassword(sanitizedData.password, sanitizedData.passwordConf); if (!passwordValidation.isValid) { throw new Error(passwordValidation.errors[0]); } // Validate username const usernameValidation = validateUsername(sanitizedData.username); if (!usernameValidation.isValid) { throw new Error(usernameValidation.error!); } // Transform data for API 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"); } showRegistrationSuccess("Registration successful! Please check your email for verification."); // Redirect to login page setTimeout(() => { router.push("/auth"); }, 2000); return true; } catch (error: any) { const errorMessage = error?.message || "Registration failed"; setError(errorMessage); showRegistrationError(error, "Registration failed"); return false; } finally { setLoading(false); } }, [router]); return { submitRegistration, loading, error, }; }; // Hook for form validation export const useFormValidation = () => { const validateIdentityForm = useCallback(( data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData, category: UserCategory ): { isValid: boolean; errors: string[] } => { return validateIdentityData(data, category); }, []); const validateProfileForm = useCallback((data: RegistrationFormData): { isValid: boolean; errors: string[] } => { const errors: string[] = []; // Validate required fields if (!data.firstName?.trim()) { errors.push("Full name is required"); } if (!data.username?.trim()) { errors.push("Username is required"); } else { const usernameValidation = validateUsername(data.username); if (!usernameValidation.isValid) { errors.push(usernameValidation.error!); } } if (!data.email?.trim()) { errors.push("Email is required"); } else { const emailValidation = validateEmail(data.email); if (!emailValidation.isValid) { errors.push(emailValidation.error!); } } if (!data.phoneNumber?.trim()) { errors.push("Phone number is required"); } else { const phoneValidation = validatePhoneNumber(data.phoneNumber); if (!phoneValidation.isValid) { errors.push(phoneValidation.error!); } } if (!data.address?.trim()) { errors.push("Address is required"); } if (!data.provinsi) { errors.push("Province is required"); } if (!data.kota) { errors.push("City is required"); } if (!data.kecamatan) { errors.push("Subdistrict is required"); } if (!data.password) { errors.push("Password is required"); } else { const passwordValidation = validatePassword(data.password, data.passwordConf); if (!passwordValidation.isValid) { errors.push(passwordValidation.errors[0]); } } return { isValid: errors.length === 0, errors, }; }, []); return { validateIdentityForm, validateProfileForm, }; };