feat: update project task card to use new task type and improve navigation
fix: update project card to reflect actual tasks done from project data feat: enhance task form components to accept project ID for submissions refactor: clean up certificate detail page by removing unused elements chore: streamline certificate generation page by removing unnecessary navigation refactor: update certificate list page to use dynamic project data feat: improve interns management page with API integration and better state handling feat: add modal for assigning interns to projects with project selection feat: create a new component for displaying project cards in certificate list fix: correct import paths in create project dialog component refactor: enhance project hooks to include task filtering and refetch capabilities fix: update mock certificate data to reflect correct status feat: extend certificate types to include new fields for better tracking feat: add task_done field to project types for improved task management
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ProjectTimeline from './components/ProjectTimeline';
|
||||
import TaskCard from './components/ProjectTaskCard';
|
||||
import HeroProject from './components/Hero-project';
|
||||
import { useProjectTasks } from '../../../hooks/useTasks';
|
||||
import { useProjectMyTasks } from '../../../hooks/useProjects';
|
||||
import type { Project } from '../../../types/project.types';
|
||||
|
||||
export default function ForumTab({ project }: { project: Project }) {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { tasks, loading, error } = useProjectTasks(slug ?? "");
|
||||
const { tasks, loading, error } = useProjectMyTasks(project.id);
|
||||
return (
|
||||
<div>
|
||||
<HeroProject title={project.project_name} description={project.description} />
|
||||
|
||||
@@ -8,8 +8,10 @@ export default function ProjectDetailPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { project, loading, error } = useProjectDetail(slug ?? "");
|
||||
const [activeTab, setActiveTab] = useState<'forum' | 'participants'>('forum');
|
||||
|
||||
if (loading) return <p className="p-10 text-gray-400">Memuat project...</p>;
|
||||
if (error) return <p className="p-10 text-red-500">{error}</p>;
|
||||
|
||||
if (!project) return <p className="p-10 text-gray-400">Project tidak ditemukan.</p>;
|
||||
|
||||
const tabClass = (tab: string) =>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Award } from "lucide-react";
|
||||
import { history, type CertificateHistory } from "../../../lib/mockSertificates";
|
||||
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";
|
||||
|
||||
export default function SertificateList() {
|
||||
const { certificates, loading, error } = useMyCertificates();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
@@ -11,15 +14,33 @@ export default function SertificateList() {
|
||||
<p className="page-title-desc">All your earned internship certificates</p>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{history.length === 0 ? (
|
||||
{/* Loading */}
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-20 text-gray-400 gap-2">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
<span className="text-sm">Loading certificates...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && !loading && (
|
||||
<div className="flex items-center justify-center py-20 text-red-500 gap-2">
|
||||
<AlertCircle className="w-6 h-6" />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty */}
|
||||
{!loading && !error && certificates.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-gray-400">
|
||||
<Award className="w-12 h-12 mb-3 opacity-40" />
|
||||
<p className="text-sm">No certificates yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{/* Grid */}
|
||||
{!loading && !error && certificates.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{history.map((item: CertificateHistory) => (
|
||||
{certificates.map((item: Certificate) => (
|
||||
<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"
|
||||
@@ -29,7 +50,7 @@ export default function SertificateList() {
|
||||
<div className="rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt={item.title}
|
||||
alt={item.project.project_name}
|
||||
className="w-full h-[160px] object-cover"
|
||||
/>
|
||||
</div>
|
||||
@@ -37,9 +58,18 @@ export default function SertificateList() {
|
||||
|
||||
{/* Info */}
|
||||
<div className="px-4 py-3">
|
||||
<p className="text-xs text-gray-400 mb-0.5">{item.month}</p>
|
||||
<p className="text-sm font-semibold text-gray-800 leading-snug">{item.title}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Grade: <span className="font-bold text-red-600">{item.grade}</span></p>
|
||||
<p className="text-xs text-gray-400 mb-0.5">
|
||||
{new Date(item.issued_at).toLocaleDateString("id-ID", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-gray-800 leading-snug">
|
||||
{item.project.project_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
No: <span className="font-bold text-red-600">{item.certificate_no}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,31 +1,81 @@
|
||||
import { certificateData } from "../../../lib/mockSertificates";
|
||||
import { useState, useMemo } from "react";
|
||||
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";
|
||||
|
||||
const SertificatePage = () => {
|
||||
const data = certificateData;
|
||||
const { projects, loading: projectsLoading } = useMyProjects();
|
||||
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 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 progress =
|
||||
current_task && current_task.total_tasks > 0
|
||||
? Math.round((current_task.task_done / current_task.total_tasks) * 100)
|
||||
: 100;
|
||||
|
||||
const remainingTasks = current_task
|
||||
? current_task.total_tasks - current_task.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;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20 text-gray-400 text-sm">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
console.log(finished_task)
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex flex-col gap-1">
|
||||
<h1 className="page-title">Certificates</h1>
|
||||
<p className="page-title-desc">View and download internship certificates</p>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row gap-6 items-stretch">
|
||||
{/* Kiri: Content */}
|
||||
<div className="w-full lg:w-5/8 flex flex-col">
|
||||
{data.isReceived ? (
|
||||
<CertificateAvailable data={data} />
|
||||
{showCertificate ? (
|
||||
<CertificateAvailable project={finished_task} />
|
||||
) : (
|
||||
<CertificateNotAvailable
|
||||
progress={data.projectProgress}
|
||||
remainingTasks={data.remainingTasks}
|
||||
progress={progress}
|
||||
remainingTasks={remainingTasks}
|
||||
allTasksDone={allTasksDone}
|
||||
onClaim={handleClaim}
|
||||
claiming={claiming}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Kanan: History */}
|
||||
<div className="w-full lg:w-3/8 flex-shrink-0">
|
||||
<SertificateHistory />
|
||||
</div>
|
||||
@@ -34,4 +84,4 @@ const SertificatePage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SertificatePage;
|
||||
export default SertificatePage;
|
||||
@@ -49,13 +49,15 @@ export default function TaskSubmission() {
|
||||
|
||||
{submissionType === "file_upload" ? (
|
||||
<TaskFormFile
|
||||
taskId={taskSlug!}
|
||||
taskId={String(task.id)}
|
||||
projectId={String(task.project.id)}
|
||||
deadline={new Date(task.deadline_at)}
|
||||
initialSubmission={task.my_submission}
|
||||
/>
|
||||
) : (
|
||||
<TaskFormLink
|
||||
taskId={taskSlug!}
|
||||
taskId={String(task.id)}
|
||||
projectId={String(task.project.id)}
|
||||
deadline={new Date(task.deadline_at)}
|
||||
initialSubmission={task.my_submission}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Download, Link, Printer } from "lucide-react";
|
||||
import { type CertificateData } from "../../../../lib/mockSertificates";
|
||||
import type { ProjectDetail } from "../../../../types/project.types";
|
||||
import certificateImg from "../../../../assets/certificate.png";
|
||||
|
||||
export default function CertificateAvailable({ data }: { data: CertificateData }) {
|
||||
const { certificate } = data;
|
||||
interface CertificateAvailableProps {
|
||||
project: ProjectDetail | null;
|
||||
}
|
||||
|
||||
export default function CertificateAvailable({ project }: CertificateAvailableProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
@@ -13,10 +16,25 @@ export default function CertificateAvailable({ data }: { data: CertificateData }
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
// Hitung durasi dari start_date - end_date
|
||||
const duration = project
|
||||
? (() => {
|
||||
const start = new Date(project.start_date);
|
||||
const end = new Date(project.end_date);
|
||||
const diffMs = end.getTime() - start.getTime();
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
const months = Math.floor(diffDays / 30);
|
||||
const days = diffDays % 30;
|
||||
if (months > 0 && days > 0) return `${months} Bulan ${days} Hari`;
|
||||
if (months > 0) return `${months} Bulan`;
|
||||
return `${diffDays} Hari`;
|
||||
})()
|
||||
: "-";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Certificate Card */}
|
||||
<div className="flex rounded-xl p-5 mb-1 border border-gray-300 shadow-md items-center justify-center">
|
||||
<div className="flex rounded-xl p-5 mb-1 border bg-box-primary border-box-border shadow-md items-center justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt="Certificate"
|
||||
@@ -32,13 +50,14 @@ export default function CertificateAvailable({ data }: { data: CertificateData }
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: "Nama Program", value: certificate.programName },
|
||||
{ label: "Durasi", value: certificate.duration },
|
||||
{ label: "Nama Program", value: project?.project_name ?? "-" },
|
||||
{ label: "Durasi", value: duration },
|
||||
{ label: "Total Tugas", value: project ? `${project.total_tasks} Tugas` : "-" },
|
||||
{
|
||||
label: "Status",
|
||||
value: (
|
||||
<span className="bg-green-100 text-green-700 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
VERIFIED
|
||||
UNVERIFIED
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,38 +1,69 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { History } from "lucide-react";
|
||||
import { type CertificateHistory, history as certificateHistory } from "../../../../lib/mockSertificates";
|
||||
import { History, Loader2, AlertCircle } from "lucide-react";
|
||||
import type { Certificate } from "../../../../types/certificate.types";
|
||||
import certificateImg from "../../../../assets/certificate.png";
|
||||
import { useMyCertificates } from "../../../../hooks/useSertificates";
|
||||
|
||||
export default function SertificateHistory() {
|
||||
const navigate = useNavigate();
|
||||
const { certificates, loading, error } = useMyCertificates();
|
||||
const displayed = certificates.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
|
||||
<div className="bg-box-primary rounded-2xl border border-box-border shadow-sm p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<History className="w-5 h-5 text-red-700" />
|
||||
<h3 className="font-semibold text-gray-800 text-lg">Certificate History</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{certificateHistory.slice(0, 2).map((item: CertificateHistory) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 p-3 bg-white border border-gray-200 shadow-sm rounded-xl hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="w-full flex justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt="Certificate"
|
||||
className="rounded-xl h-[150px] w-[150px] w-full object-cover"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-8 text-gray-400 gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span className="text-sm">Loading certificates...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div className="flex items-center gap-2 py-6 text-red-500">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && displayed.length === 0 && (
|
||||
<p className="text-sm text-gray-400 py-6 text-center">No certificates yet.</p>
|
||||
)}
|
||||
|
||||
{!loading && !error && displayed.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{displayed.map((item: Certificate) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 p-3 bg-white border border-gray-200 shadow-sm rounded-xl hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="w-full flex justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt="Certificate"
|
||||
className="rounded-xl h-[150px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<p className="text-xs text-gray-400">
|
||||
{new Date(item.issued_at).toLocaleDateString("id-ID", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-gray-800 leading-tight">
|
||||
{item.project.project_name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">No: {item.certificate_no}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<p className="text-xs text-gray-400">{item.month}</p>
|
||||
<p className="text-sm font-semibold text-gray-800 leading-tight">{item.title}</p>
|
||||
<p className="text-sm text-gray-500">Grade: {item.grade}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full my-3 flex justify-end">
|
||||
<button
|
||||
|
||||
@@ -1,43 +1,104 @@
|
||||
import { Lock, Clock } from "lucide-react";
|
||||
import { Lock, Award, Loader2 } from "lucide-react";
|
||||
|
||||
export default function CertificateNotAvailable({ progress, remainingTasks }: { progress: number; remainingTasks: number }) {
|
||||
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-white w-full h-full rounded-2xl border border-gray-100 shadow-sm px-24 flex flex-col items-center justify-center">
|
||||
<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">
|
||||
{/* Icon */}
|
||||
<div className="relative mb-6">
|
||||
<div className=" w-45 h-45 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
||||
|
||||
<div className="w-30 h-30 flex items-center rounded-lg justify-center bg-gray-300">
|
||||
<Lock className="w-16 h-16 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">
|
||||
<Clock className="w-5 h-5 text-yellow-800" />
|
||||
<div className="w-34 h-34 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
||||
<div className="w-26 h-26 flex items-center rounded-lg justify-center bg-gray-300">
|
||||
{allTasksDone ? (
|
||||
<Award className="w-12 h-12 text-green-600" />
|
||||
) : (
|
||||
<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">
|
||||
<Clock className="w-4 h-4 text-yellow-800" />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-[37px] font-bold text-gray-900 mb-3">Sertifikat Belum Tersedia</h2>
|
||||
<p className="text-gray-500 text-[17px] mb-6">
|
||||
Anda harus menyelesaikan <span className="font-bold text-red-600">semua tugas proyek</span> sebelum dapat mengunduh sertifikat kelulusan.
|
||||
Silakan periksa kembali progres tugas Anda di menu Proyek.
|
||||
</p>
|
||||
{allTasksDone ? (
|
||||
<>
|
||||
<h2 className="text-[37px] font-bold text-gray-900 mb-3">Selamat!</h2>
|
||||
<p className="text-gray-500 text-[17px] mb-6 text-center">
|
||||
Semua tugas proyek telah selesai. Klik tombol di bawah untuk{" "}
|
||||
<span className="font-bold text-green-600">mengklaim sertifikat</span> kelulusan Anda.
|
||||
</p>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-1/3">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
|
||||
<span className="text-sm font-bold text-red-700">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-red-700 rounded-full transition-all duration-700"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2 text-left">
|
||||
Selesaikan {remainingTasks} tugas lagi untuk membuka akses.
|
||||
</p>
|
||||
</div>
|
||||
{/* Progress Bar — 100% */}
|
||||
<div className="w-1/3 mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
|
||||
<span className="text-sm font-bold text-green-600">100%</span>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-[28px] font-bold text-gray-900 mb-3">Sertifikat Belum Tersedia</h2>
|
||||
<p className="text-gray-500 text-[15px] mb-6 text-center">
|
||||
Anda harus menyelesaikan <span className="font-bold text-red-600">semua tugas proyek</span> sebelum dapat mengunduh sertifikat kelulusan.
|
||||
Silakan periksa kembali progres tugas Anda di menu Proyek.
|
||||
</p>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-1/3">
|
||||
<p className="text-xs text-font mt-2">
|
||||
Selesaikan {remainingTasks} tugas lagi untuk membuka akses.
|
||||
</p>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
|
||||
<span className="text-sm font-bold text-red-700">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-red-700 rounded-full transition-all duration-700"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { ProjectTask } from "../../../../types/task.types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { InternTaskItem } from "../../../../types/project.types";
|
||||
|
||||
const formatDate = (dateStr: string) =>
|
||||
new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
@@ -7,37 +7,36 @@ const formatDate = (dateStr: string) =>
|
||||
const formatTime = (dateStr: string) =>
|
||||
new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
export default function TaskCard({ task }: { task: ProjectTask }) {
|
||||
export default function TaskCard({ task }: { task: InternTaskItem }) {
|
||||
const navigate = useNavigate();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
return (
|
||||
<div
|
||||
className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3"
|
||||
onClick={() => navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`)}
|
||||
className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
onClick={() => navigate(`/intern/projects/${task.project_slug}/${task.slug}?type=${task.submission_type}`)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className=" text-[11px] text-font mb-1">
|
||||
<div className="text-[11px] text-font mb-1">
|
||||
Deadline : {formatDate(task.deadline_at)} at {formatTime(task.deadline_at)}
|
||||
</div>
|
||||
<div className=" font-bold text-[15px] text-[#1a1a1a] mb-1">
|
||||
<div className="font-bold text-[15px] text-[#1a1a1a] mb-1">
|
||||
{task.title}
|
||||
</div>
|
||||
<div className=" text-[13px] text-font">
|
||||
<div className="text-[13px] text-font">
|
||||
{task.description}
|
||||
</div>
|
||||
<div className=" text-[13px] text-font">
|
||||
<div className="text-[13px] text-font mt-0.5">
|
||||
Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="block md:hidden h-[10px] bg-gray-600"/>
|
||||
|
||||
<div className="shrink-0 hidden md:block">
|
||||
<button
|
||||
className="bg-white text-font-shade border-[1.5px] border-card-outline font-semibold text-xs px-3.5 py-[5px] rounded-md cursor-pointer whitespace-nowrap transition-colors duration-150"
|
||||
<div className="shrink-0 hidden md:flex items-center">
|
||||
<span
|
||||
className={`font-semibold text-xs px-3.5 py-[5px] rounded-md whitespace-nowrap bg-gray-50`}
|
||||
>
|
||||
Add Submission
|
||||
</button>
|
||||
{task.submission_status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ const ProjectCard = (project: Project) => {
|
||||
const Icon = getDynamicIcon(project.project_icon);
|
||||
|
||||
|
||||
const tasksDone = 1;
|
||||
const tasksDone = project.task_done;
|
||||
const tasksTotal = project.total_tasks;
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types";
|
||||
|
||||
interface TaskFormFileProps {
|
||||
taskId: string;
|
||||
projectId?: string;
|
||||
deadline?: Date;
|
||||
initialSubmission?: TaskSubmissionData | null;
|
||||
}
|
||||
|
||||
export default function TaskFormFile({ taskId, deadline, initialSubmission }: TaskFormFileProps) {
|
||||
const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission);
|
||||
export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission }: TaskFormFileProps) {
|
||||
const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission, projectId);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isDragActive, setIsDragActive] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
@@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types";
|
||||
|
||||
interface TaskFormLinkProps {
|
||||
taskId: string;
|
||||
projectId?: string;
|
||||
deadline?: Date;
|
||||
initialSubmission?: TaskSubmissionData | null;
|
||||
}
|
||||
|
||||
export default function TaskFormLink({ taskId, deadline, initialSubmission }: TaskFormLinkProps) {
|
||||
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission);
|
||||
export default function TaskFormLink({ taskId, projectId, deadline, initialSubmission }: TaskFormLinkProps) {
|
||||
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission, projectId);
|
||||
const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? "");
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user