feat: enhance user profile update functionality and error handling

- Added detailed error messages for various HTTP status codes in useUpdateProfile hook.
- Refactored updateProfile service to handle full_name as separate first and last name fields.
- Updated UpdateProfilePayload type to allow profile_picture to be a string or File.
- Introduced resolveFileUrl utility function for consistent file URL resolution.
- Improved loading and error state management in useUpdateProfile hook.
- Added DEFAULT_AVATAR constant for default user avatar.
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-13 12:15:15 +07:00
parent 5516d35546
commit a124a2b96e
39 changed files with 729 additions and 392 deletions
+63 -9
View File
@@ -1,9 +1,10 @@
// hooks/useUser.ts
import { useState, useEffect, useCallback } from "react";
import { loginUser, getProfile, updateProfile } from "../services/UserService";
import type { Mahasiswa } from '../types/project.types';
import { getMahasiswa } from '../services/MahasiswaService';
import type { Mahasiswa } from "../types/project.types";
import { getMahasiswa } from "../services/MahasiswaService";
import type { CurrentUser, UpdateProfilePayload } from "../types/user.types";
import { useRef } from "react";
// GET /auth-api/me — ambil data user yang sedang login
export const useCurrentUser = () => {
@@ -54,29 +55,80 @@ export const useLogin = () => {
return { login, loading, error, setError };
};
export const DEFAULT_AVATAR = "https://placehold.co/150";
export const resolveFileUrl = (path?: string | null): string | null => {
if (!path) return null;
if (/^(https?:|data:|blob:)/i.test(path)) return path;
const base = import.meta.env.VITE_API_BASE_URL as string;
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
};
// PATCH /auth-api/update-profile
export const useUpdateProfile = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMsg, setSuccessMsg] = useState<string | null>(null);
const msgRef = useRef<{ success: string | null; error: string | null }>({
success: null,
error: null,
});
const save = async (payload: UpdateProfilePayload) => {
setLoading(true);
setError(null);
setSuccessMsg(null);
msgRef.current = { success: null, error: null };
try {
const updated = await updateProfile(payload);
setSuccessMsg("Profile updated successfully!");
const msg = "Profile updated successfully!";
msgRef.current.success = msg;
setSuccessMsg(msg);
return updated;
} catch (err: any) {
setError(err?.response?.data?.message || "Failed to save changes.");
return null;
const status = err?.response?.status;
const apiMessage = err?.response?.data?.message;
let message: string;
switch (status) {
case 400:
message =
apiMessage ||
"Data yang dikirim tidak valid. Periksa kembali isian Anda.";
break;
case 401:
message = "Sesi Anda telah berakhir. Silakan login kembali.";
break;
case 403:
message = "Anda tidak memiliki izin untuk mengubah profil ini.";
break;
case 413:
message = "Ukuran file terlalu besar. Maksimal 2MB.";
break;
case 417:
message = apiMessage || "Validasi gagal. Periksa kembali isian Anda.";
break;
case 500:
message = "Terjadi kesalahan di server. Coba beberapa saat lagi.";
break;
default:
message = apiMessage || "Failed to save changes.";
}
msgRef.current.error = message;
setError(message);
// WAJIB throw — kalau `return null`, toast selalu masuk cabang success.
throw new Error(message);
} finally {
setLoading(false);
}
};
return { save, loading, error, successMsg, setSuccessMsg, setError };
return { save, loading, error, successMsg, setSuccessMsg, setError, msgRef };
};
// Ambil Seluruh Mahasiswa
@@ -89,11 +141,13 @@ export const useMahasiswa = () => {
setLoading(true);
getMahasiswa()
.then(setMahasiswa)
.catch(() => setError('Gagal memuat data mahasiswa.'))
.catch(() => setError("Gagal memuat data mahasiswa."))
.finally(() => setLoading(false));
}, []);
useEffect(() => { refetch(); }, [refetch]);
useEffect(() => {
refetch();
}, [refetch]);
return { mahasiswa, loading, error, refetch };
};
};