diff --git a/src/App.tsx b/src/App.tsx index 7e7bb7a..f07af2e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,11 +42,11 @@ import InternifyLogin from "./Pages/Internify/LoginInternify" import FAQPage from "./Pages/Internify/FAQpage"; import NotificationList from "./Pages/Internify/NotificationList"; import SettingsContent from "./Pages/Internify/SettingsPage"; +import MentorSettingsPage from "./Pages/Internify/mentor/MentorSettingsPage"; import TaskSubmission from "./Pages/Internify/intern/TaskSubmission"; import SertificateList from "./Pages/Internify/intern/SertificateList"; import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage"; import ViewInternSubmission from "./Pages/Internify/mentor/ViewInternSubmission"; -import SettingsPage from "./Pages/Internify/mentor/MentorSettingsPage"; import ProtectedRoutes from "./Pages/utils/ProtectedRoute"; import CertificateDetail from "./Pages/Internify/mentor/CertificateDetail"; import CertificateResult from "./Pages/Internify/mentor/CertificateGenerate"; @@ -96,8 +96,8 @@ function App() { }> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> @@ -106,8 +106,8 @@ function App() { - {/* Mentor only */} - }> + {/* Mentor/Admin only */} + }> }> } /> } /> @@ -120,7 +120,7 @@ function App() { } /> } /> } /> - } /> + } /> diff --git a/src/Layout/Header-internify.tsx b/src/Layout/Header-internify.tsx index a095fcf..d491a3f 100644 --- a/src/Layout/Header-internify.tsx +++ b/src/Layout/Header-internify.tsx @@ -1,14 +1,35 @@ import { Bell, Menu } from "lucide-react" import humiclogo from "../assets/logo.png"; import { useNavigate } from "react-router-dom"; +import { useCurrentUser } from "../hooks/useUser"; + +/** Singkatan nama: ambil huruf pertama setiap kata, maksimal 2 huruf. + * "Andi Mahesa" → "AM" + * "Andi Bayu Cendika" → "AC" (huruf pertama + huruf pertama kata terakhir) + */ +function getInitials(fullName: string): string { + const parts = fullName.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) return "U"; + if (parts.length === 1) return parts[0].charAt(0).toUpperCase(); + return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase(); +} export default function Header({ toggleSidebar }: { toggleSidebar: () => void }) { const nav = useNavigate(); + const { user } = useCurrentUser(); + + // Tentukan base path settings sesuai role + const settingsPath = + user?.role === "intern" ? "/intern/settings" : "/mentor/settings"; + + const initials = user ? getInitials(user.full_name) : "U"; + const hasPhoto = !!user?.profile_picture; + return (
-
- -
-
+ {/* Gambar Profil */} +
) diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index a4c0fb9..facd508 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import logo from "../../assets/logo.png"; import { useNavigate } from "react-router-dom"; import { Mail, Lock, Eye, EyeOff } from 'lucide-react'; -import api from "../../lib/api"; +import { useLogin } from "../../hooks/useUser"; const decodeJWT = (token: string) => { try { @@ -19,42 +19,32 @@ export default function InternifyLogin() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); + + const { login, loading, error, setError } = useLogin(); const handleSignIn = async (e: React.FormEvent) => { e.preventDefault(); - setError(""); + setError(null); if (!email || !password) { setError("Email dan password wajib diisi."); return; } - try { - setLoading(true); - const response = await api.post("/auth-api/login", { email, password }); - console.log(response.data.data) - const { token } = response.data.data; - document.cookie = `token=${token}; path=/; SameSite=Strict`; - - const decoded = decodeJWT(token); - const role = decoded?.role; - if (role === "intern") { - navigate("/intern"); - } else if (role === "mentor" || role === "admin") { - navigate("/mentor"); - } else { - setError("Role user tidak dikenali."); - } - } catch (err: any) { - if (err.response?.status === 401 || err.response?.status === 400) { - setError("Email atau password salah."); - } else { - setError("Terjadi kesalahan. Coba beberapa saat lagi."); - } - } finally { - setLoading(false); + const data = await login(email, password); + if (!data) return; + + const { token } = data; + document.cookie = `token=${token}; path=/; SameSite=Strict`; + + const decoded = decodeJWT(token); + const role = decoded?.role; + if (role === "intern") { + navigate("/intern"); + } else if (role === "mentor" || role === "admin") { + navigate("/mentor"); + } else { + setError("Role user tidak dikenali."); } }; @@ -130,7 +120,7 @@ export default function InternifyLogin() { disabled={loading} className="w-full bg-[#B30000] hover:bg-[#990000] text-white font-medium text-sm py-2.5 rounded-lg transition-colors shadow-sm mt-2" > - Sign In + {loading ? "Signing in..." : "Sign In"} diff --git a/src/Pages/internify/SettingsPage.tsx b/src/Pages/internify/SettingsPage.tsx index 01284d1..9ac53ed 100644 --- a/src/Pages/internify/SettingsPage.tsx +++ b/src/Pages/internify/SettingsPage.tsx @@ -1,36 +1,26 @@ import { useState, useEffect } from "react"; -import api from "../../lib/api"; +import { useCurrentUser, useUpdateProfile } from "../../hooks/useUser"; export default function SettingsContent() { + const { user, loading } = useCurrentUser(); + const { save, loading: saving, error: errorMsg, successMsg } = useUpdateProfile(); + const [formData, setFormData] = useState({ fullName: "", email: "", - bio: "" + bio: "", }); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [successMsg, setSuccessMsg] = useState(""); - const [errorMsg, setErrorMsg] = useState(""); - + // Sync form data when user is loaded useEffect(() => { - const fetchData = async () => { - try { - const response = await api.patch("/auth-api/update-profile"); - const data = response.data.data; - setFormData({ - fullName: data.full_name || "", - email: data.email || "", - bio: data.professional_bio || "", - }); - } catch (err) { - console.error("Failed to fetch profile:", err) - } finally { - setLoading(false); - } - }; - fetchData(); - }, []); + if (user) { + setFormData({ + fullName: user.full_name || "", + email: user.email || "", + bio: user.professional_bio || "", + }); + } + }, [user]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -39,25 +29,14 @@ export default function SettingsContent() { const handleSaveChanges = async (e: React.FormEvent) => { e.preventDefault(); - setSuccessMsg(""); - setErrorMsg(""); - setSaving(true); - - try { - await api.patch("/auth-api/update-profile", { - full_name: formData.fullName, - email: formData.email, - professional_bio: formData.bio, - }); - setSuccessMsg("Profile updated successfully!"); - } catch (err: any) { - setErrorMsg(err?.response?.data?.message || "Failed to save changes."); - } finally { - setSaving(false); - } + await save({ + full_name: formData.fullName, + email: formData.email, + professional_bio: formData.bio, + }); }; - const initialLetter = formData.fullName ? formData.fullName.charAt(1).toUpperCase() : "U"; + const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U"; if (loading) { return ( diff --git a/src/Pages/internify/intern/SertificateList.tsx b/src/Pages/internify/intern/SertificateList.tsx index 475d89f..a302062 100644 --- a/src/Pages/internify/intern/SertificateList.tsx +++ b/src/Pages/internify/intern/SertificateList.tsx @@ -1,11 +1,11 @@ -import { Award, Loader2, AlertCircle } from "lucide-react"; -import { useMyCertificates } from "../../../hooks/useSertificates"; -import type { Certificate } from "../../../types/certificate.types"; -import certificateImg from "../../../assets/certificate.png"; +import { Loader2, AlertCircle } from "lucide-react"; +import { useMyProjects } from "../../../hooks/useProjects"; +import type { Project } from "../../../types/project.types"; +import { useNavigate } from "react-router-dom"; export default function SertificateList() { - const { certificates, loading, error } = useMyCertificates(); - + const { projects, loading, error } = useMyProjects(); + const navigate = useNavigate(); return (
{/* Header */} @@ -31,44 +31,52 @@ export default function SertificateList() { )} {/* Empty */} - {!loading && !error && certificates.length === 0 && ( + {!loading && !error && projects.length === 0 && (
-

No certificates yet.

+

No ongoing projects yet.

)} {/* Grid */} - {!loading && !error && certificates.length > 0 && ( + {!loading && !error && projects.length > 0 && (
- {certificates.map((item: Certificate) => ( + {projects.map((item: Project) => (
navigate(`/intern/certificates/${item.slug}`)} > {/* Thumbnail */}
- {item.project.project_name} +
+ {item.project_name} +
{/* Info */}

- {new Date(item.issued_at).toLocaleDateString("id-ID", { + {new Date(item.start_date).toLocaleDateString("id-ID", { + year: "numeric", + month: "long", + })} + until + {new Date(item.start_date).toLocaleDateString("id-ID", { year: "numeric", month: "long", })}

- {item.project.project_name} + {item.end_date}

- No: {item.certificate_no} + No: {item.status}

diff --git a/src/Pages/internify/intern/SertificatePage.tsx b/src/Pages/internify/intern/SertificatePage.tsx index 4f6bc72..60f267d 100644 --- a/src/Pages/internify/intern/SertificatePage.tsx +++ b/src/Pages/internify/intern/SertificatePage.tsx @@ -1,52 +1,32 @@ -import { useState, useMemo } from "react"; +import { useMemo } from "react"; +import { useParams } from "react-router-dom"; import CertificateAvailable from "./components/CertificateAvailabe"; import CertificateNotAvailable from "./components/CertificateNotAvailable"; import SertificateHistory from "./components/CertificateHistory"; -import { useMyCertificates, useClaimCertificate } from "../../../hooks/useSertificates"; -import { useMyProjects } from "../../../hooks/useProjects"; -import type { ProjectDetail } from "../../../types/project.types"; +import { useMyCertificates } from "../../../hooks/useSertificates"; +import { useProjectDetail } from "../../../hooks/useProjects"; const SertificatePage = () => { - const { projects, loading: projectsLoading } = useMyProjects(); + const { slug } = useParams<{ slug: string }>(); + const { project, loading: projectLoading } = useProjectDetail(slug ?? ""); const { certificates, loading: certLoading } = useMyCertificates(); - const { claim, loading: claiming } = useClaimCertificate(); - const [justClaimed, setJustClaimed] = useState(false); - const current_task = useMemo(() => { - if (!projects || projects.length === 0) return null; - return projects.find((p) => p.task_done < p.total_tasks) ?? null; - }, [projects]); + const allTasksDone = !projectLoading && !!project && project.task_done >= project.total_tasks; - const finished_task = useMemo((): ProjectDetail | null => { - if (!projects || projects.length === 0) return null; - return projects.find((p) => p.task_done >= p.total_tasks) ?? null; - }, [projects]); - - // Cek apakah finished_task sudah punya certificate di backend - const alreadyClaimed = useMemo(() => { - if (!finished_task || !certificates.length) return false; - return certificates.some((cert) => cert.project.id === finished_task.id); - }, [certificates, finished_task]); - - const allTasksDone = !projectsLoading && current_task === null && projects.length > 0; + const alreadyDone = useMemo(() => { + if (!project || !certificates.length) return false; + return certificates.some((cert) => cert.project.id === project.id); + }, [certificates, project]); const progress = - current_task && current_task.total_tasks > 0 - ? Math.round((current_task.task_done / current_task.total_tasks) * 100) - : 100; + project && project.total_tasks > 0 + ? Math.round((project.task_done / project.total_tasks) * 100) + : 0; - const remainingTasks = current_task - ? current_task.total_tasks - current_task.task_done - : 0; + const remainingTasks = project ? project.total_tasks - project.task_done : 0; - const handleClaim = async () => { - if (!finished_task) return; - const result = await claim(finished_task.id); - if (result) setJustClaimed(true); - }; - - const isLoading = projectsLoading || certLoading; - const showCertificate = alreadyClaimed || justClaimed; + const isLoading = projectLoading || certLoading; + const showCertificate = alreadyDone; if (isLoading) { return ( @@ -55,7 +35,7 @@ const SertificatePage = () => {
); } - console.log(finished_task) + return (
@@ -65,14 +45,12 @@ const SertificatePage = () => {
{showCertificate ? ( - + ) : ( )}
diff --git a/src/Pages/internify/intern/components/CertificateNotAvailable.tsx b/src/Pages/internify/intern/components/CertificateNotAvailable.tsx index 5a9b26c..2704cb4 100644 --- a/src/Pages/internify/intern/components/CertificateNotAvailable.tsx +++ b/src/Pages/internify/intern/components/CertificateNotAvailable.tsx @@ -1,19 +1,15 @@ -import { Lock, Award, Loader2 } from "lucide-react"; +import { Clock, Lock, Award } from "lucide-react"; interface CertificateNotAvailableProps { progress: number; remainingTasks: number; allTasksDone?: boolean; - onClaim?: () => void; - claiming?: boolean; } export default function CertificateNotAvailable({ progress, remainingTasks, allTasksDone = false, - onClaim, - claiming = false, }: CertificateNotAvailableProps) { return (
@@ -27,9 +23,9 @@ export default function CertificateNotAvailable({ )}
- {/*
+
-
*/} +
@@ -50,27 +46,8 @@ export default function CertificateNotAvailable({
-

Semua tugas telah diselesaikan!

+

Semua tugas telah diselesaikan! Silahkan tunggu untuk mendapatkan sertifikat

- - {/* Tombol Claim */} - ) : ( <> diff --git a/src/Pages/internify/intern/components/ProjectsCard.tsx b/src/Pages/internify/intern/components/ProjectsCard.tsx index 0176689..2183882 100644 --- a/src/Pages/internify/intern/components/ProjectsCard.tsx +++ b/src/Pages/internify/intern/components/ProjectsCard.tsx @@ -23,7 +23,7 @@ const ProjectCard = (project: Project) => { className=" rounded-xl overflow-hidden shadow-sm border border-gray-100 w-full flex flex-col cursor-pointer hover:shadow-md transition-shadow duration-200" > {/* Thumbnail */} -
+
diff --git a/src/Pages/internify/mentor/CertificatePage.tsx b/src/Pages/internify/mentor/CertificatePage.tsx index 45c66d1..1423465 100644 --- a/src/Pages/internify/mentor/CertificatePage.tsx +++ b/src/Pages/internify/mentor/CertificatePage.tsx @@ -47,17 +47,13 @@ const MentorCertificatePage = () => { return (
- {/* Title */}

Certificates

Administrate, issue, and track professional certifications for all interns.

- - {/* Main Grid Content */}
- {/* Kiri: Form Issue New Certificate (4 Kolom) */}
@@ -65,7 +61,6 @@ const MentorCertificatePage = () => {
- {/* Select Intern */}
- {/* Select Project / Course */}
- - {/* Certificate Image Upload Area */}
diff --git a/src/Pages/internify/mentor/MentorSettingsPage.tsx b/src/Pages/internify/mentor/MentorSettingsPage.tsx index 4a8f4de..7fea5ce 100644 --- a/src/Pages/internify/mentor/MentorSettingsPage.tsx +++ b/src/Pages/internify/mentor/MentorSettingsPage.tsx @@ -1,30 +1,44 @@ -import { useState, useRef } from "react"; - -const mockProfileData = { - fullName: "JonathanKristina", - email: "JonathanKristina@gmail.com", - bio: "UIUX holic" -}; +import { useState, useRef, useEffect } from "react"; +import { useCurrentUser, useUpdateProfile } from "../../../hooks/useUser"; export default function MentorSettingsPage() { - const [formData, setFormData] = useState(mockProfileData); + const { user, loading: userLoading } = useCurrentUser(); + const { save, loading: saving, error: errorMsg, successMsg } = useUpdateProfile(); + + const [formData, setFormData] = useState({ + fullName: "", + email: "", + bio: "", + }); + + // File state untuk profile_picture + const [photoFile, setPhotoFile] = useState(null); const [photoPreview, setPhotoPreview] = useState(null); const fileInputRef = useRef(null); + // Sync form data ketika user sudah di-fetch + useEffect(() => { + if (user) { + setFormData({ + fullName: user.full_name || "", + email: user.email || "", + bio: user.professional_bio || "", + }); + if (user.profile_picture) { + setPhotoPreview(user.profile_picture); + } + } + }, [user]); + const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; - const handleSaveChanges = (e: React.FormEvent) => { - e.preventDefault(); - alert("Changes saved successfully!"); - console.log("Saved Data:", formData); - }; - const handlePhotoUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { + setPhotoFile(file); const reader = new FileReader(); reader.onloadend = () => setPhotoPreview(reader.result as string); reader.readAsDataURL(file); @@ -32,12 +46,35 @@ export default function MentorSettingsPage() { }; const handleRemovePhoto = () => { + setPhotoFile(null); setPhotoPreview(null); if (fileInputRef.current) fileInputRef.current.value = ""; }; + const handleSaveChanges = async (e: React.FormEvent) => { + e.preventDefault(); + await save({ + full_name: formData.fullName, + email: formData.email, + professional_bio: formData.bio, + ...(photoFile ? { profile_picture: photoFile } : {}), + }); + // Reset file state setelah berhasil save + if (!errorMsg) { + setPhotoFile(null); + } + }; + const initialLetter = formData.fullName ? formData.fullName.charAt(0).toUpperCase() : "U"; + if (userLoading) { + return ( +
+ Loading profile... +
+ ); + } + return (
{/* Header Title */} @@ -47,6 +84,7 @@ export default function MentorSettingsPage() { Manage and edit your profile and account information.

+ {/* White Card Container */}
@@ -91,6 +129,11 @@ export default function MentorSettingsPage() {

JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K.

+ {photoFile && ( +

+ 📎 {photoFile.name} +

+ )}
+ {/* Feedback Messages */} + {successMsg && ( +

+ {successMsg} +

+ )} + {errorMsg && ( +

+ {errorMsg} +

+ )} + {/* Footer Action */}
diff --git a/src/Pages/internify/mentor/components/MentorProjectCard.tsx b/src/Pages/internify/mentor/components/MentorProjectCard.tsx index b850c12..fa9f6e1 100644 --- a/src/Pages/internify/mentor/components/MentorProjectCard.tsx +++ b/src/Pages/internify/mentor/components/MentorProjectCard.tsx @@ -4,11 +4,7 @@ import * as LucideIcons from 'lucide-react'; import type { LucideProps } from 'lucide-react'; import type { Project } from '../../../../types/project.types'; -const getDynamicIcon = (iconName: string) => { - const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1); - const Icon = (LucideIcons as unknown as Record>)[formatted]; - return Icon ?? LucideIcons.FolderOpen; -}; + const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); diff --git a/src/Pages/utils/ProtectedRoute.tsx b/src/Pages/utils/ProtectedRoute.tsx index 59c0119..c29c3bb 100644 --- a/src/Pages/utils/ProtectedRoute.tsx +++ b/src/Pages/utils/ProtectedRoute.tsx @@ -1,7 +1,7 @@ import { Outlet, Navigate } from "react-router-dom"; interface PrivateRouteProps { - allowedRoles?: ("intern" | "admin")[]; + allowedRoles?: ("intern" | "admin" | "mentor")[]; } const decodeJWT = (token: string) => { @@ -32,7 +32,7 @@ const ProtectedRoutes = ({ allowedRoles }: PrivateRouteProps) => { } const decoded = decodeJWT(token); - const userRole = decoded?.role as"intern" | "admin" | undefined; + const userRole = decoded?.role as "intern" | "admin" | "mentor" | undefined; if (!userRole) { return ; @@ -40,7 +40,7 @@ const ProtectedRoutes = ({ allowedRoles }: PrivateRouteProps) => { if (allowedRoles && !allowedRoles.includes(userRole)) { // Jika role tidak diizinkan, kembalikan ke base dashboard masing-masing role - if (userRole === "admin") return ; + if (userRole === "admin" || userRole === "mentor") return ; if (userRole === "intern") return ; } diff --git a/src/hooks/useInterns.ts b/src/hooks/useInterns.ts index 06e3805..5ef2cbf 100644 --- a/src/hooks/useInterns.ts +++ b/src/hooks/useInterns.ts @@ -1,5 +1,5 @@ // hooks/useMahasiswa.ts -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import type { Mahasiswa } from '../types/project.types'; import { getMahasiswa } from '../services/MahasiswaService'; @@ -8,12 +8,15 @@ export const useMahasiswa = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { + setLoading(true); getMahasiswa() .then(setMahasiswa) .catch(() => setError('Gagal memuat data mahasiswa.')) .finally(() => setLoading(false)); }, []); - return { mahasiswa, loading, error }; + useEffect(() => { refetch(); }, [refetch]); + + return { mahasiswa, loading, error, refetch }; }; \ No newline at end of file diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index aa8d4b0..f313574 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -67,7 +67,7 @@ export const useProjectDetail = (id: string) => { //Khusus Ambil Intern dari Project/{id} export const useProjectInterns = (projectId: string) => { - const [interns, setInterns] = useState([] ); + const [interns, setInterns] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -89,30 +89,35 @@ export const useMyProjects = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { + setLoading(true); getMyProjects() .then(setProjects) .catch(() => setError("Gagal memuat my projects.")) .finally(() => setLoading(false)); }, []); - return { projects, loading, error }; -}; + useEffect(() => { refetch(); }, [refetch]); + return { projects, loading, error, refetch }; +}; // GET /project-api/my-tasks export const useMyTasks = () => { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { + setLoading(true); getMyTasks() .then(setTasks) .catch(() => setError("Gagal memuat tasks.")) .finally(() => setLoading(false)); }, []); - return { tasks, loading, error }; + useEffect(() => { refetch(); }, [refetch]); + + return { tasks, loading, error, refetch }; }; // GET /project-api/mentor-projects @@ -121,14 +126,17 @@ export const useMentorProjects = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { + setLoading(true); getMentorProjects() .then(setProjects) .catch(() => setError("Gagal memuat mentor projects.")) .finally(() => setLoading(false)); }, []); - return { projects, loading, error }; + useEffect(() => { refetch(); }, [refetch]); + + return { projects, loading, error, refetch }; }; // GET /project-api/interns diff --git a/src/hooks/useSertificates.ts b/src/hooks/useSertificates.ts index 5451c70..25134ab 100644 --- a/src/hooks/useSertificates.ts +++ b/src/hooks/useSertificates.ts @@ -18,30 +18,35 @@ export const useMyCertificates = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { + setLoading(true); getMyCertificates() .then(setCertificates) .catch(() => setError("Gagal memuat sertifikat.")) .finally(() => setLoading(false)); }, []); - return { certificates, loading, error }; -}; + useEffect(() => { refetch(); }, [refetch]); + return { certificates, loading, error, refetch }; +}; // GET /certificate-api/all export const useAllCertificates = () => { const [certificates, setCertificates] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { + setLoading(true); getAllCertificates() .then(setCertificates) .catch(() => setError("Gagal memuat semua sertifikat.")) .finally(() => setLoading(false)); }, []); - return { certificates, loading, error }; + useEffect(() => { refetch(); }, [refetch]); + + return { certificates, loading, error, refetch }; }; // GET /certificate-api/detail/{id} @@ -67,15 +72,18 @@ export const useProjectCertificates = (id_project: number) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const refetch = useCallback(() => { if (!id_project) return; + setLoading(true); getCertificatesByProject(id_project) .then(setCertificates) .catch(() => setError("Gagal memuat sertifikat project.")) .finally(() => setLoading(false)); }, [id_project]); - return { certificates, loading, error }; + useEffect(() => { refetch(); }, [refetch]); + + return { certificates, loading, error, refetch }; }; // GET /certificate-api/verify & verify-uuid (public, on-demand) diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index ae76b2b..906d84f 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -1,24 +1,98 @@ -import { useState, useEffect } from "react"; -import api from "../lib/api"; +// hooks/useUser.ts +import { useState, useEffect, useCallback } from "react"; +import { loginUser, getProfile, updateProfile } from "../services/UserService"; + +// ─── Types ──────────────────────────────────────────────────────────────────── export interface CurrentUser { id: number; full_name: string; email: string; role: "mentor" | "intern" | "admin"; + professional_bio?: string; + profile_picture?: string | null; } +export interface UpdateProfilePayload { + full_name?: string; + email?: string; + professional_bio?: string; + profile_picture?: File | null; +} + +// ─── useCurrentUser ─────────────────────────────────────────────────────────── +// GET /auth-api/me — ambil data user yang sedang login export const useCurrentUser = () => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - api.get("/auth-api/me") - .then((res) => setUser(res.data.data)) - .catch(() => setError("Gagal memuat user.")) + const refetch = useCallback(() => { + setLoading(true); + setError(null); + getProfile() + .then(setUser) + .catch(() => setError("Gagal memuat data user.")) .finally(() => setLoading(false)); }, []); - return { user, loading, error }; + useEffect(() => { + refetch(); + }, [refetch]); + + return { user, loading, error, refetch }; +}; + +// ─── useLogin ──────────────────────────────────────────────────────────────── +// POST /auth-api/login +export const useLogin = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const login = async (email: string, password: string) => { + setLoading(true); + setError(null); + try { + const data = await loginUser({ email, password }); + return data; // { token } + } catch (err: any) { + const status = err?.response?.status; + if (status === 401 || status === 400) { + setError("Email atau password salah."); + } else { + setError("Terjadi kesalahan. Coba beberapa saat lagi."); + } + return null; + } finally { + setLoading(false); + } + }; + + return { login, loading, error, setError }; +}; + +// ─── useUpdateProfile ──────────────────────────────────────────────────────── +// PATCH /auth-api/update-profile +export const useUpdateProfile = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); + + const save = async (payload: UpdateProfilePayload) => { + setLoading(true); + setError(null); + setSuccessMsg(null); + try { + const updated = await updateProfile(payload); + setSuccessMsg("Profile updated successfully!"); + return updated; + } catch (err: any) { + setError(err?.response?.data?.message || "Failed to save changes."); + return null; + } finally { + setLoading(false); + } + }; + + return { save, loading, error, successMsg, setSuccessMsg, setError }; }; \ No newline at end of file diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 013b11b..3104b63 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -1,3 +1,38 @@ +// services/UserService.ts import api from "../lib/api"; +import type { CurrentUser, UpdateProfilePayload } from "../hooks/useUser"; -// POST auth-api/login +// POST /auth-api/login +export const loginUser = async (payload: { + email: string; + password: string; +}): Promise<{ token: string }> => { + const res = await api.post("/auth-api/login", payload); + return res.data.data; +}; + +// GET /auth-api/me +export const getProfile = async (): Promise => { + const res = await api.get("/auth-api/me"); + return res.data.data; +}; + +// PATCH /auth-api/update-profile +// Jika payload mengandung profile_picture (File), kirim sebagai multipart/form-data +export const updateProfile = async ( + payload: UpdateProfilePayload +): Promise => { + if (payload.profile_picture instanceof File) { + const formData = new FormData(); + if (payload.full_name !== undefined) formData.append("full_name", payload.full_name); + if (payload.email !== undefined) formData.append("email", payload.email); + if (payload.professional_bio !== undefined) formData.append("professional_bio", payload.professional_bio); + formData.append("profile_picture", payload.profile_picture); + const res = await api.patch("/auth-api/update-profile", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data.data; + } + const res = await api.patch("/auth-api/update-profile", payload); + return res.data.data; +};