feat: update certificate handling and project management features

- Refactored CertificateHistory component to display dynamic certificate images.
- Modified CertificateNotAvailable component text for clarity on certificate publication.
- Enhanced CertificateForm to include file preview and clear functionality for uploaded images.
- Removed obsolete CertificatePage component to streamline mentor interface.
- Updated CertificateResult to handle project details and loading states more effectively.
- Improved HomeMentorPage structure by adjusting imports for better organization.
- Enhanced EligibleInternTable with navigation to results and improved error handling for certificate generation.
- Updated HomeMentorProject styles for consistency with design system.
- Added utility function to resolve image URLs for certificates.
- Introduced new hooks for generating certificates and managing notifications.
- Expanded project service functions to include project completion and assignable interns retrieval.
- Updated types for certificates and projects to reflect new data structures.
- Added user types for better type safety in user management.
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-02 10:27:28 +07:00
parent 7fffc08912
commit 5dec77862c
23 changed files with 326 additions and 350 deletions
@@ -1,6 +1,6 @@
import HomeHeader from "./components/HomeHeader";
import HomeProjectList from "./components/HomeProjectList";
import HomeCalendar from "./components/HomeCalendar";
import HomeCalendar from "../HomeCalendar";
import TaskTimeline from "./components/TaskTimeline";
export default function HomeInternPage() {
@@ -4,6 +4,7 @@ import { Download, Link, Printer } from "lucide-react";
import type { ProjectDetail } from "../../../../types/project.types";
import type { Certificate } from "../../../../types/certificate.types";
import { downloadCertificateAsImage, downloadCertificateAsPdf } from "../../../utils/DownloadImagetoDevice";
import { resolveImageUrl } from "../../../utils/SertificateGenerator";
interface CertificateAvailableProps {
project: ProjectDetail | null;
@@ -62,7 +63,7 @@ export default function CertificateAvailable({ project, certificate }: Certifica
{/* Certificate Card */}
<div className="flex rounded-xl p-5 mb-1 border bg-box-primary border-box-border shadow-md items-center justify-center">
<img
src={certificate.image_path}
src={resolveImageUrl(certificate.image_path)}
alt="Certificate"
className="rounded-xl w-[550px] h-[350px] object-contain"
/>
@@ -83,7 +84,7 @@ export default function CertificateAvailable({ project, certificate }: Certifica
label: "Status",
value: (
<span className="bg-green-100 text-green-700 text-xs font-bold px-2 py-0.5 rounded-full">
{certificate.certificate_status.toUpperCase()}
{certificate.certificate_status}
</span>
),
},
@@ -1,7 +1,6 @@
import { useNavigate } from "react-router-dom";
import { History, Loader2, AlertCircle } from "lucide-react";
import type { Certificate } from "../../../../types/certificate.types";
import certificateImg from "../../../../assets/certificate.png"; //Placeholder
import { useMyCertificates } from "../../../../hooks/useCertificates";
export default function SertificateHistory() {
@@ -43,7 +42,7 @@ export default function SertificateHistory() {
>
<div className="w-full flex justify-center">
<img
src={certificateImg}
src={item.image_path}
alt="Certificate"
className="rounded-xl h-[150px] w-full object-cover"
/>
@@ -40,8 +40,8 @@ export default function CertificateNotAvailable({
<>
<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.
Semua tugas proyek telah selesai. Silahkan tunggu Mentor untuk{" "}
<span className="font-bold text-green-600">Mempublish Sertifikat</span> kelulusan Anda.
</p>
{/* Progress Bar — 100% */}
@@ -53,7 +53,6 @@ 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! Silahkan tunggu untuk mendapatkan sertifikat</p>
</div>
<button
onClick={onClaim}
@@ -1,201 +0,0 @@
import { useState, useEffect } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useMyTasks } from '../../../../hooks/useProjects';
import { useCurrentUser } from '../../../../hooks/useUser';
import { useAllMentorTasks } from '../../../../hooks/useTasks';
const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
]
function getDaysInMonth(year: number, month: number) {
return new Date(year, month + 1, 0).getDate()
}
function getFirstDayOfMonth(year: number, month: number) {
return new Date(year, month, 1).getDay()
}
export default function HomeCalendar() {
const [today, setToday] = useState(new Date());
const { user } = useCurrentUser();
const isIntern = user?.role === "intern";
const { tasks: internTasks } = useMyTasks();
const { tasks: mentorTasks } = useAllMentorTasks(!isIntern && user !== null);
const DEADLINES = isIntern
? internTasks.map(task => ({
date: new Date(task.deadline_at),
label: task.title,
}))
: mentorTasks.map(task => ({
date: new Date(task.deadline_at),
label: `[${task.project_name}] ${task.title}`,
}));
useEffect(() => {
const interval = setInterval(() => {
setToday(new Date())
}, 60 * 1000)
return () => clearInterval(interval)
}, [])
const [currentYear, setCurrentYear] = useState(today.getFullYear())
const [currentMonth, setCurrentMonth] = useState(today.getMonth())
const daysInMonth = getDaysInMonth(currentYear, currentMonth)
const firstDay = getFirstDayOfMonth(currentYear, currentMonth)
const prevMonth = () => {
if (currentMonth === 0) {
setCurrentMonth(11)
setCurrentYear(y => y - 1)
} else {
setCurrentMonth(m => m - 1)
}
}
const nextMonth = () => {
if (currentMonth === 11) {
setCurrentMonth(0)
setCurrentYear(y => y + 1)
} else {
setCurrentMonth(m => m + 1)
}
}
const isToday = (day: number) =>
day === today.getDate() &&
currentMonth === today.getMonth() &&
currentYear === today.getFullYear()
const hasDeadline = (day: number) =>
DEADLINES.some(d =>
d.date.getDate() === day &&
d.date.getMonth() === currentMonth &&
d.date.getFullYear() === currentYear
)
const upcomingDeadlines = DEADLINES.filter(d => {
return (
d.date.getMonth() === currentMonth &&
d.date.getFullYear() === currentYear
);
}).sort((a, b) => a.date.getTime() - b.date.getTime());
// Previous month trailing days
const prevMonthDays = getDaysInMonth(currentYear, currentMonth - 1 < 0 ? 11 : currentMonth - 1)
const trailingDays = Array.from({ length: firstDay }, (_, i) => prevMonthDays - firstDay + 1 + i)
// Current month days
const currentDays = Array.from({ length: daysInMonth }, (_, i) => i + 1)
// Next month leading days
const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7
const leadingDays = Array.from({ length: totalCells - firstDay - daysInMonth }, (_, i) => i + 1)
// Bold dates (weekends or special)
const isBold = (day: number) => {
const d = new Date(currentYear, currentMonth, day).getDay()
return d === 0 || d === 6 // Sun or Sat
}
return (
<div className=" px-13 py-5 border border-card-outline rounded-2xl">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-font">
{MONTH_NAMES[currentMonth]} {currentYear}
</h2>
<div className="flex items-center">
<button
onClick={prevMonth}
className="p-1 rounded hover:bg-gray-100 transition-colors"
aria-label="Previous month"
>
<ChevronLeft className="w-4 h-4 text-gray-500" />
</button>
<button
onClick={nextMonth}
className="p-1 rounded hover:bg-gray-100 transition-colors"
aria-label="Next month"
>
<ChevronRight className="w-4 h-4 text-gray-500" />
</button>
</div>
</div>
{/* Day labels */}
<div className="grid grid-cols-7 mb-1">
{DAY_LABELS.map(day => (
<div key={day} className="text-center text-xs text-gray-400 font-medium py-1">
{day}
</div>
))}
</div>
{/* Calendar grid */}
<div className="grid grid-cols-7">
{/* Trailing days from prev month */}
{trailingDays.map(day => (
<div key={`prev-${day}`} className="text-center py-2">
<span className="text-xs text-gray-300">{day}</span>
</div>
))}
{/* Current month days */}
{currentDays.map(day => (
<div key={day} className="flex justify-center items-center py-0.5">
{isToday(day) ? (
<span className="w-8 h-7 flex items-center justify-center border-b-2 border-red-800 text-xs font-semibold">
{day}
</span>
) : (
<span
className={`w-full h-7 flex items-center justify-center text-xs
${isBold(day) ? 'font-bold text-gray-800' : 'text-gray-700'}
${hasDeadline(day) ? 'bg-red-800 text-white' : ''}
transition-colors`}
>
{day}
</span>
)}
</div>
))}
{/* Leading days from next month */}
{leadingDays.map(day => (
<div key={`next-${day}`} className="text-center py-2">
<span className="text-xs text-gray-300">{day}</span>
</div>
))}
</div>
{/* Divider */}
<hr className="my-3 border-gray-400" />
{/* Upcoming Deadlines */}
<div>
<p className="text-[10px] font-semibold text-font uppercase tracking-widest mb-2">
Upcoming Deadlines
</p>
{upcomingDeadlines.length > 0 ? (
<ul className="flex flex-col gap-1">
{upcomingDeadlines.map((d, i) => (
<li key={i} className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-red-600 flex-shrink-0" />
<span className="text-xs text-font-shade">{d.label}</span>
</li>
))}
</ul>
) : (
<p className="text-[11px] text-font-shade">No upcoming deadlines this month.</p>
)}
</div>
</div>
)
}