kontenhumas-fe/hooks/use-registration.ts

625 lines
16 KiB
TypeScript

"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<string | null>(null);
const [timer, setTimer] = useState<TimerState>(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<boolean> => {
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),
name: email.split("@")[0],
};
// 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<any> => {
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<boolean> => {
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<LocationData[]>([]);
const [cities, setCities] = useState<LocationData[]>([]);
const [districts, setDistricts] = useState<LocationData[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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<InstituteData[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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<number> => {
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<string | null>(null);
const validateJournalistData = useCallback(
async (certificateNumber: string): Promise<any> => {
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<any> => {
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<string | null>(null);
const submitRegistration = useCallback(
async (
data: RegistrationFormData,
category: UserCategory,
userData: any,
instituteId?: number
): Promise<boolean> => {
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,
};
};