feat: implement authentication system, role-based routing, and profile settings for interns and mentors
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user