feat: implement authentication system, role-based routing, and profile settings for interns and mentors

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-06-28 10:13:50 +07:00
parent ad26a53f21
commit 37e21a1f39
17 changed files with 369 additions and 226 deletions
+26 -18
View File
@@ -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 (
<div>
{/* Header */}
@@ -31,44 +31,52 @@ export default function SertificateList() {
)}
{/* Empty */}
{!loading && !error && certificates.length === 0 && (
{!loading && !error && projects.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-gray-400">
<p className="text-sm">No certificates yet.</p>
<p className="text-sm">No ongoing projects yet.</p>
</div>
)}
{/* Grid */}
{!loading && !error && certificates.length > 0 && (
{!loading && !error && projects.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{certificates.map((item: Certificate) => (
{projects.map((item: Project) => (
<div
key={item.id}
className="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow cursor-pointer group"
onClick={() => navigate(`/intern/certificates/${item.slug}`)}
>
{/* Thumbnail */}
<div className="p-6 pb-0">
<div className="rounded-lg overflow-hidden">
<img
src={certificateImg}
alt={item.project.project_name}
className="w-full h-[160px] object-cover"
/>
<div className="w-full h-32 bg-gradient-to-br from-red-400 to-red-600 flex items-center justify-center">
<img
src={item.project_icon}
alt={item.project_name}
className="w-16 h-16 object-contain"
/>
</div>
</div>
</div>
{/* Info */}
<div className="px-4 py-3">
<p className="text-xs text-gray-400 mb-0.5">
{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",
})}
</p>
<p className="text-sm font-semibold text-gray-800 leading-snug">
{item.project.project_name}
{item.end_date}
</p>
<p className="text-xs text-gray-500 mt-1">
No: <span className="font-bold text-red-600">{item.certificate_no}</span>
No: <span className="font-bold text-red-600">{item.status}</span>
</p>
</div>
</div>
+19 -41
View File
@@ -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 = () => {
</div>
);
}
console.log(finished_task)
return (
<div>
<div className="mb-4 flex flex-col gap-1">
@@ -65,14 +45,12 @@ const SertificatePage = () => {
<div className="flex flex-col lg:flex-row gap-6 items-stretch">
<div className="w-full lg:w-5/8 flex flex-col">
{showCertificate ? (
<CertificateAvailable project={finished_task} />
<CertificateAvailable project={project} />
) : (
<CertificateNotAvailable
progress={progress}
remainingTasks={remainingTasks}
allTasksDone={allTasksDone}
onClaim={handleClaim}
claiming={claiming}
/>
)}
</div>
@@ -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 (
<div className="bg-box-primary w-full h-full rounded-2xl border border-box-border shadow-sm px-24 py-12 flex flex-col items-center justify-center">
@@ -27,9 +23,9 @@ export default function CertificateNotAvailable({
<Lock className="w-12 h-12 text-red-700" />
)}
</div>
{/* <div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
<div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
<Clock className="w-4 h-4 text-yellow-800" />
</div> */}
</div>
</div>
</div>
@@ -50,27 +46,8 @@ export default function CertificateNotAvailable({
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
<div className="h-full bg-green-500 rounded-full transition-all duration-700 w-full" />
</div>
<p className="text-xs text-gray-400 mt-2 text-left">Semua tugas telah diselesaikan!</p>
<p className="text-xs text-gray-400 mt-2 text-left">Semua tugas telah diselesaikan! Silahkan tunggu untuk mendapatkan sertifikat</p>
</div>
{/* Tombol Claim */}
<button
onClick={onClaim}
disabled={claiming}
className="flex items-center justify-center gap-2 px-8 py-3 bg-red-700 hover:bg-red-800 disabled:opacity-60 active:scale-95 transition-all text-white font-semibold rounded-xl text-sm"
>
{claiming ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Mengklaim...
</>
) : (
<>
<Award className="w-4 h-4" />
Claim Certificate
</>
)}
</button>
</>
) : (
<>
@@ -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 */}
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden flex items-center justify-center">
<div style={{ backgroundColor: project.background_color ?? '#dc2626' }} className="h-[150px] overflow-hidden flex items-center justify-center">
<Icon className="w-12 h-12 text-white opacity-80" />
</div>