feat: update navigation paths to use '/internship'; enhance certificate generation and user interface elements

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-08-04 09:21:16 +07:00
parent 5dbb21bf5a
commit 753012732d
17 changed files with 78 additions and 46 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ const OurDeveloper = () => {
link: "https://www.linkedin.com/in/reihanramadhana/",
},
{
name: "Ayu",
name: "Nomi",
role: "Web Designer",
img: ayu,
link: "https://www.linkedin.com/in/anom-ayu-radhaswasti/",
+9 -7
View File
@@ -143,21 +143,23 @@ export default function HomeCalendar() {
))}
{currentDays.map(day => (
<div key={day} className="flex justify-center items-center py-0.5">
<div key={day} className="flex justify-center items-center py-1">
<div className="w-7 flex items-center justify-center">
{isToday(day) ? (
<span className="w-8 h-7 flex items-center justify-center border-b-2 border-red- text-xs font-semibold">
<span className="w-8 h-7 flex items-center justify-center border-b-2 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 text-white' : ''}
transition-colors`}
>
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 text-white rounded-full' : ''}
transition-colors`}
>
{day}
</span>
)}
</div>
</div>
))}
+7 -1
View File
@@ -52,7 +52,13 @@ export default function InternifyLogin() {
<div className="w-full flex flex-col items-center mb-8">
<div className="text-center mb-8">
<div className="flex items-center justify-center gap-2 font-bold text-2xl tracking-tight text-black">
<img src={logo} alt="Logo" />
<button
type="button"
onClick={() => navigate("/")}
className="flex items-center gap-2 hover:cursor-pointer w-[160px] md:w-full"
>
<img src={logo} alt="Logo" />
</button>
</div>
<p className="text-[10px] tracking-widest text-yellow-950 font-semibold mt-1 uppercase">
HUMIC LMS PORTAL
@@ -56,7 +56,7 @@ export default function SertificateList() {
<div
key={item.id}
className="bg-white border border-card-outline rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow cursor-pointer group"
onClick={() => navigate(`/intern/certificates/${item.slug}`, { state: { project: item } })}
onClick={() => navigate(`/internship/certificates/${item.slug}`, { state: { project: item } })}
>
{/* Thumbnail */}
<div className="p-6 pb-0">
+2 -2
View File
@@ -22,9 +22,9 @@ export default function ProjectsPage() {
if (projects.length === 0) {
return (
<div className="col-span-full flex flex-col items-center py-2 gap-3 mt-19">
<div className="flex w-full flex-col items-center justify-center py-2 gap-3 mt-19">
<img className="w-[440px]" src={TeamPana} alt="" />
<p className="text-[18px] text-font-shade">Kamu belum tergabung ke projek apapun</p>
<p className="text-[13px] text-font-shade">Kamu belum tergabung ke projek apapun</p>
</div>
);
}
@@ -41,7 +41,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl
generateCertificate(
resolveFileUrl(templateUrl),
certificate.user.full_name,
certificate.project.project_name,
certificate.intern_position,
certificate.certificate_no,
duration,
verifyUrl,
@@ -139,7 +139,7 @@ export default function CertificateAvailable({ project, certificate, templateUrl
className="flex justify-between items-center text-sm border-b border-card-outline pb-2 last:border-0"
>
<span className="text-font text-start text-xs md:text-sm">{row.label}</span>
<span className="text-font-shade font-bold text-start text-xs md:text-sm">{row.value}</span>
<span className="text-font-shade font-bold text-end text-xs md:text-sm">{row.value}</span>
</div>
))}
</div>
@@ -33,7 +33,7 @@ const HomeProjectList = () => {
{projects.map((project) => (
<div
key={project.id}
onClick={() => navigate(`/intern/projects/${toSlug(project.slug)}`)}
onClick={() => navigate(`/internship/projects/${toSlug(project.slug)}`)}
className="flex items-center gap-4 border border-card-outline rounded-lg px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors duration-150"
>
<div
@@ -31,7 +31,7 @@ export default function TaskCard({ task }: { task: InternTaskItem }) {
return (
<div
className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-box-primary 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}`)}
onClick={() => navigate(`/internship/projects/${task.project_slug}/${task.slug}?type=${task.submission_type}`)}
>
<div className="flex-1 min-w-0">
<div className="md:hidden mb-[8px]">
@@ -12,7 +12,7 @@ const ProjectCard = (project: Project) => {
return (
<div
onClick={() => navigate(`/intern/projects/${project.slug}`)}
onClick={() => navigate(`/internship/projects/${project.slug}`)}
className="w-[220px] min-h-[320px] flex flex-col bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 cursor-pointer hover:shadow-md transition-shadow duration-200"
>
{/* Thumbnail */}
@@ -15,13 +15,31 @@ interface SubmitStatusTableProps {
function getTimeEarly(submittedAt: Date, deadline: Date): string {
const diffMs = deadline.getTime() - submittedAt.getTime();
if (diffMs <= 0) return `Submitted after deadline (${Math.abs(Math.floor(diffMs / (1000 * 60 * 60)))} hours late)`;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffMs <= 0) {
const absHours = Math.abs(diffHours);
const days = Math.floor(absHours / 24);
const hours = absHours % 24;
const overdueParts = [] as string[];
if (days > 0) overdueParts.push(`${days} day${days > 1 ? "s" : ""}`);
if (hours > 0) overdueParts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
if (overdueParts.length === 0) {
return "Submitted after deadline (0 hours late)";
}
return `Submitted after deadline (${overdueParts.join(" ")} late)`;
}
const parts = [] as string[];
const days = Math.floor(diffHours / 24);
const hours = diffHours % 24;
const parts = [];
if (days > 0) parts.push(`${days} day${days > 1 ? "s" : ""}`);
if (hours > 0) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
return `Assignment was submitted ${parts.join(" ")} early`;
}
@@ -27,7 +27,14 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
selectedFiles.forEach((file) => {
const ext = file.name.split(".").pop()?.toLowerCase();
if (ext && allowedExtensions.includes(ext)) {
if (file.size <= 5 * 1024 * 1024) validFiles.push(file);
if (file.size <= 5 * 1024 * 1024) {
validFiles.push(file);
} else {
customToast.error(
"File terlalu besar!",
`File ${file.name} melebihi batas ukuran 5MB.`
);
}
}
});
if (validFiles.length > 0) setFiles((prev) => [...prev, ...validFiles]);
@@ -27,8 +27,8 @@ export default function MentorProjectsPage() {
if (loading) {
return (
<div className="flex flex-row gap-3 w-full">
{[...Array(4)].map((_) => (
<div className="flex flex-row bg-gray-300 rounded-xl overflow-hidden shadow-sm w-[180px] min-h-[270px] animate-pulse">
{[...Array(4)].map((_, index) => (
<div key={index} className="flex flex-row bg-gray-300 rounded-xl overflow-hidden shadow-sm w-[180px] min-h-[270px] animate-pulse">
</div>
))}
</div>
@@ -132,13 +132,6 @@ export default function CertResultTable({ certificates, templateUrl }: Certifica
: <Download className="w-4 h-4" />
}
</button>
{/*
<button
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
title="Share"
>
<Share2 className="w-4 h-4" />
</button> */}
</div>
</div>
))
@@ -118,12 +118,14 @@ export default function EligibleInternTable({
id: intern.id_user,
id_project: project.id,
id_user: intern.id_user,
certificate_no: "No.2025/SPI3/CERT/yyddmm/X",
certificate_no: "No.2025/SPI3/CERT/MMDDYY/XXX",
issued_at: new Date().toISOString().slice(0, 10),
uuid: "HANYA SEBATAS PREVIEW",
intern_position: "[Posisi Intern]",
user: {
id: intern.id_user,
full_name: intern.full_name,
intern_position: "[Posisi Intern]",
email: intern.email,
},
project: {
@@ -185,12 +187,12 @@ export default function EligibleInternTable({
const blob = await generateCertificate(
templateUrl,
cert.user.full_name,
cert.project.project_name,
cert.intern_position,
cert.certificate_no,
duration,
cert.uuid,
);
zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
zip.file(`${cert.certificate_no}_${cert.user.full_name}_${cert.project.project_name}.png`, blob);
})
);
})(),
+8 -8
View File
@@ -25,7 +25,7 @@ export const previewCertificate = async (
const blob = await generateCertificate(
resolveFileUrl(templateUrl),
cert.user.full_name,
cert.project.project_name,
cert.intern_position,
cert.certificate_no,
formatDateRange(cert.project.start_date, cert.project.end_date),
cert.uuid,
@@ -85,11 +85,11 @@ export const downloadCertificatePdf = async (
cert: Certificate,
templateUrl: string,
): Promise<void> => {
const fileName = `Sertifikat - ${cert.user.full_name} - ${cert.certificate_no}`;
const fileName = `${cert.certificate_no}_${cert.user.full_name}_${cert.project.project_name}`;
const blob = await generateCertificate(
resolveFileUrl(templateUrl),
cert.user.full_name,
cert.project.project_name,
cert.intern_position,
cert.certificate_no,
formatDateRange(cert.project.start_date, cert.project.end_date),
cert.uuid,
@@ -125,11 +125,11 @@ export const downloadCertificateImage = async (
templateUrl: string,
format: "png" | "jpg" = "png",
): Promise<void> => {
const fileName = `Sertifikat - ${cert.user.full_name} - ${cert.certificate_no}`;
const fileName = `${cert.certificate_no}_${cert.user.full_name}_${cert.project.project_name}`;
const blob = await generateCertificate(
resolveFileUrl(templateUrl),
cert.user.full_name,
cert.project.project_name,
cert.intern_position,
cert.certificate_no,
formatDateRange(cert.project.start_date, cert.project.end_date),
cert.uuid,
@@ -192,12 +192,12 @@ export const downloadAllCertificatesZip = async (
const blob = await generateCertificate(
resolveFileUrl(templateUrl),
cert.user.full_name,
cert.project.project_name,
cert.intern_position,
cert.certificate_no,
formatDateRange(cert.project.start_date, cert.project.end_date),
cert.uuid,
);
zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
zip.file(`${cert.certificate_no}_${projectName}_${cert.user.full_name}.png`, blob);
onProgress?.(i + 1, total);
}),
);
@@ -205,7 +205,7 @@ export const downloadAllCertificatesZip = async (
const zipBlob = await zip.generateAsync({ type: "blob" });
const a = document.createElement("a");
a.href = URL.createObjectURL(zipBlob);
a.download = `Sertifikat - ${projectName}.zip`;
a.download = `Sertifikat_${projectName}_${new Date().toISOString().slice(0, 10)}.zip`;
a.click();
URL.revokeObjectURL(a.href);
};
+6 -6
View File
@@ -23,13 +23,13 @@ async function ensureFontsLoaded(): Promise<void> {
// LAYOUT DEFAULT SERTIFIKAT YANG AKAN DIGENERATE
const LAYOUT = {
internName: { x: 0.54, y: 0.5, size: 130, font: "Great Vibes", color: "#800000" },
internName: { x: 0.54, y: 0.47, size: 130, font: "Great Vibes", color: "#800000" },
internNameSmall: { x: 0.82, y: 0.889, size: 47, font: "Great Vibes", color: "#090909" },
projectName: { x: 0.815, y: 0.598, size: 46, font: "Grenze", color: "#800000" },
duration: { x: 0.7, y: 0.645, size: 48, font: "Grenze", color: "#090909" },
userposition: { x: 0.855, y: 0.557, size: 46, font: "Grenze", color: "#800000" },
duration: { x: 0.7, y: 0.615, size: 48, font: "Grenze", color: "#090909" },
certNo: { x: 0.56, y: 0.305, size: 50, font: "Grenze", color: "#090909" },
certNoSmall: { x: 0.827, y: 0.92, size: 32, font: "Grenze", color: "#090909"},
qr: { x: 0.77, y: 0.715, size: 0.103 },
qr: { x: 0.772, y: 0.715, size: 0.103 },
};
// HELPER: kurangi ukuran font 25% jika nama melebihi 22 karakter (termasuk spasi)
@@ -48,7 +48,7 @@ function fitNameText(name: string, baseSize: number): number {
export async function generateCertificate(
templateUrl: string,
internName: string,
projectName: string,
userposition: string,
certificateNo: string,
ProjectDuration: string,
verifyUrl: string,
@@ -76,7 +76,7 @@ export async function generateCertificate(
text(LAYOUT.internName, internName);
text(LAYOUT.internNameSmall, internName);
text(LAYOUT.projectName, projectName);
text(LAYOUT.userposition, userposition);
text(LAYOUT.duration, ProjectDuration);
text(LAYOUT.certNo, certificateNo);
text(LAYOUT.certNoSmall, certificateNo);
+4
View File
@@ -2,6 +2,7 @@ export interface CertificateUser {
id: number;
full_name: string;
email: string;
intern_position: string;
}
export interface CertificateProject {
@@ -19,6 +20,7 @@ export interface Certificate {
id_user: number;
certificate_no: string;
issued_at: string;
intern_position: string;
uuid: string;
user: CertificateUser;
project: CertificateProject;
@@ -28,6 +30,7 @@ export interface Certificate {
export interface CertificateVerification {
uuid: string;
intern_name: string;
intern_position: string;
project_name: string;
created_at: string;
}
@@ -49,5 +52,6 @@ export interface CertificateVerifyResult {
uuid: string;
intern_name: string;
project: projectbysertif;
intern_position: string;
created_at: string;
}