feat: update navigation paths to use '/internship'; enhance certificate generation and user interface elements
This commit is contained in:
@@ -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/",
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
})(),
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user