-
+
+
{f.original_name}
))}
diff --git a/src/Pages/internify/mentor/CertificateForm.tsx b/src/Pages/internify/mentor/CertificateForm.tsx
index 7c9711e..7883e9e 100644
--- a/src/Pages/internify/mentor/CertificateForm.tsx
+++ b/src/Pages/internify/mentor/CertificateForm.tsx
@@ -5,7 +5,7 @@ import { useUploadCertificateTemplate } from "../../../hooks/useCertificates";
import { useProjectDetail } from "../../../hooks/useProjects";
import EligibleInternTable from "./components/EligibleInternTable";
import { useProjectInternProgress } from "../../../hooks/useInternProgress";
-import { resolveImageUrl } from "../../utils/SertificateGenerator";
+import { resolveFileUrl } from "../../utils/resolveFileFromUrl";
import { customToast } from "../../utils/showToast";
export default function CertificateDetail() {
@@ -140,7 +140,7 @@ export default function CertificateDetail() {
{previewUrl || project.certificate_template ? (
<>

diff --git a/src/Pages/internify/mentor/ProjectsInternTab.tsx b/src/Pages/internify/mentor/ProjectsInternTab.tsx
index 077a31f..0a489f8 100644
--- a/src/Pages/internify/mentor/ProjectsInternTab.tsx
+++ b/src/Pages/internify/mentor/ProjectsInternTab.tsx
@@ -4,7 +4,8 @@ import ManageInternsModal from './components/AddInternsDialog';
import type { ProjectDetail, ProjectMember } from '../../../types/project.types';
import { useRemoveMember } from '../../../hooks/useProjects';
import { customToast } from '../../utils/showToast';
-import { resolveFileUrl, getInitials } from '../../../hooks/useUser';
+import { getInitials } from '../../../hooks/useUser';
+import { resolveFileUrl } from '../../utils/resolveFileFromUrl';
export default function InternsTab({ project }: { project: ProjectDetail }) {
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -39,7 +40,7 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
- {adminHasPhoto ? (
+ {adminHasPhoto && !adminImageError ? (

+
+
+ {value}/{total}
+
+
+ );
+}
+
function StatusBadge({ status }: { status: boolean }) {
return (
@@ -131,13 +150,15 @@ export default function EligibleInternTable({
setGenerating(true);
setGenerateError(null);
- const ok = await customToast.promise(
+ const res = await customToast.promise(
(async () => {
const result = await SubmitCertificates(project.id, Array.from(selected));
if (!result.data) throw new Error("Tidak ada data sertifikat yang dikembalikan.");
const zip = new JSZip();
- const templateUrl = resolveImageUrl(project.certificate_template!);
+ const templateUrl = resolveFileUrl(project.certificate_template!);
+ if (!templateUrl) throw new Error("Template sertifikat tidak ditemukan.");
+
const duration = formatDateRange(project.start_date, project.end_date);
await Promise.all(
@@ -168,7 +189,7 @@ export default function EligibleInternTable({
).then(() => true).catch(() => false);
setGenerating(false);
- if (ok) navigate(`/mentor/certificates/${project.slug}/result`);
+ if (res) navigate(`/mentor/certificates/${project.slug}/result`);
};
if (loading) {
@@ -185,7 +206,7 @@ export default function EligibleInternTable({
return (
{/* Roster Card */}
-
+
Intern Roster
@@ -253,7 +274,7 @@ export default function EligibleInternTable({
{/* Name + role */}
{intern.avatar ? (
-

+

) : (
)}
diff --git a/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx b/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx
index 27cf5cf..59df0cf 100644
--- a/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx
+++ b/src/Pages/internify/mentor/components/StatusSubmissionTable.tsx
@@ -2,7 +2,7 @@
import { FileText, CheckCircle2, AlertCircle, MinusCircle } from "lucide-react";
import type { TaskSubmissionData, DisplayStatus } from "../../../../types/task.types";
-import { resolveImageUrl } from "../../../utils/SertificateGenerator";
+import { resolveFileUrl } from "../../../utils/resolveFileFromUrl";
// StatusBadge
@@ -66,7 +66,7 @@ export function StatusTable({ status, submission }: StatusTableProps) {
{submission.files.map((f) => (
=> {
const fileName = `Sertifikat - ${cert.user.full_name} - ${cert.certificate_no}`;
const blob = await generateCertificate(
- resolveImageUrl(templateUrl),
+ resolveFileUrl(templateUrl),
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
@@ -118,7 +119,7 @@ export const downloadCertificatePdf = async (
pdf.save(`${fileName}.pdf`);
};
-// ── 3. Download PNG atau JPG ──────────────────────────────────────────────────
+// Download PNG atau JPG
export const downloadCertificateImage = async (
cert: Certificate,
templateUrl: string,
@@ -126,7 +127,7 @@ export const downloadCertificateImage = async (
): Promise => {
const fileName = `Sertifikat - ${cert.user.full_name} - ${cert.certificate_no}`;
const blob = await generateCertificate(
- resolveImageUrl(templateUrl),
+ resolveFileUrl(templateUrl),
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
@@ -176,7 +177,7 @@ export const downloadCertificateImage = async (
URL.revokeObjectURL(url);
};
-// ── 4. Download All ZIP ───────────────────────────────────────────────────────
+// Download All ZIP
export const downloadAllCertificatesZip = async (
certificates: Certificate[],
templateUrl: string,
@@ -189,7 +190,7 @@ export const downloadAllCertificatesZip = async (
await Promise.all(
certificates.map(async (cert, i) => {
const blob = await generateCertificate(
- resolveImageUrl(templateUrl),
+ resolveFileUrl(templateUrl),
cert.user.full_name,
cert.project.project_name,
cert.certificate_no,
diff --git a/src/Pages/utils/SertificateGenerator.ts b/src/Pages/utils/SertificateGenerator.ts
index 6bf74da..f1469b9 100644
--- a/src/Pages/utils/SertificateGenerator.ts
+++ b/src/Pages/utils/SertificateGenerator.ts
@@ -1,24 +1,13 @@
-import api from "../../lib/api";
+import { resolveFileUrl } from "./resolveFileFromUrl";
import QRCode from "qrcode";
-export function resolveImageUrl(path: string): string {
- if (!path) return "";
- if (path.startsWith("data:")) return path;
- if (path.startsWith("http://") || path.startsWith("https://")) {
- return path;
- }
- const apiBaseURL = api.defaults.baseURL ?? "";
- const origin = new URL(apiBaseURL).origin;
- return `${origin}${path.startsWith("/") ? "" : "/"}${path}`;
-}
-
export function loadImage(src: string): Promise {
return new Promise((resolve, reject) => {
const img = new Image();
if (!src.startsWith("data:")) img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Gagal load gambar: ${src}`));
- img.src = resolveImageUrl(src);
+ img.src = resolveFileUrl(src);
});
}
@@ -30,14 +19,13 @@ export async function ensureFontsLoaded(): Promise {
await document.fonts.ready;
}
-
const LAYOUT = {
internName: { x: 0.57, y: 0.48, size: 140, font: "Great Vibes", color: "#800000" },
- internNameSmall: { x: 0.82, y: 0.889, size: 33, 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.692, y: 0.645, size: 48, font: "Grenze", color: "#090909" },
- certNo: { x: 0.6, y: 0.28, size: 50, font: "Grenze", color: "#090909" },
- certNoSmall: { x: 0.825, y: 0.91, size: 22, font: "Grenze", color: "#800000" },
+ duration: { x: 0.7, y: 0.645, size: 48, font: "Grenze", color: "#090909" },
+ certNo: { x: 0.597, 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 },
};
@@ -74,7 +62,7 @@ export async function generateCertificate(
text(LAYOUT.projectName, projectName);
text(LAYOUT.duration, ProjectDuration);
text(LAYOUT.certNo, certificateNo);
- text(LAYOUT.certNoSmall, certificateNo);
+ text(LAYOUT.certNoSmall, "No.2026/SPI3/" + certificateNo);
// QR code — nanti akan diganti ke alamat domain
const qrSize = Math.round(canvas.width * LAYOUT.qr.size);
diff --git a/src/Pages/utils/resolveFileFromUrl.ts b/src/Pages/utils/resolveFileFromUrl.ts
new file mode 100644
index 0000000..cb505b5
--- /dev/null
+++ b/src/Pages/utils/resolveFileFromUrl.ts
@@ -0,0 +1,8 @@
+import {API_BASE_URL} from "../../lib/api";
+
+
+export function resolveFileUrl(path?: string | null): string {
+ if (!path) return "";
+ if (/^(https?:|data:|blob:)/i.test(path)) return path;
+ return `${API_BASE_URL}${path.startsWith("/") ? path : `/${path}`}`;
+}
\ No newline at end of file
diff --git a/src/Pages/utils/resolveObjectError.ts b/src/Pages/utils/resolveObjectError.ts
deleted file mode 100644
index dcdb475..0000000
--- a/src/Pages/utils/resolveObjectError.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Ubah message backend (string ATAU object field-errors) jadi satu string aman
-export function resolveErrorMessage(message: unknown, fallback = "Silakan coba lagi."): string {
- if (typeof message === "string") return message;
- if (message && typeof message === "object") {
- // ambil semua value error, gabung: { end_date: "...", start_date: "..." } -> "... \n ..."
- return Object.values(message as Record)
- .flat()
- .filter((v): v is string => typeof v === "string")
- .join(" ") || fallback;
- }
- return fallback;
-}
\ No newline at end of file
diff --git a/src/Pages/utils/testlayout.html b/src/Pages/utils/testlayout.html
index 161a52e..d55f2bc 100644
--- a/src/Pages/utils/testlayout.html
+++ b/src/Pages/utils/testlayout.html
@@ -12,9 +12,7 @@