Toast Implementation; fix add interns dialogue: penghapusan UI delete pada dialog komponen; penambahan params pada generate sertificate; perubahan tipe state untuk submission (user bisa open file submission)

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-07 11:47:33 +07:00
parent 7a331b8e7d
commit 91d7bcf3f7
18 changed files with 501 additions and 290 deletions
+26 -19
View File
@@ -6,37 +6,44 @@ import type { Project } from '../../../types/project.types';
export default function ForumTab({ project }: { project: Project }) {
const { tasks, loading, error } = useProjectMyTasks(project.id);
return (
<div>
<HeroProject title={project.project_name} description={project.description} />
{/* Loading */}
{loading && (
{/* Loading skeleton */}
{/* {loading && (
<div className="flex flex-col gap-3 mt-5">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" />
))}
</div>
)}
)} */}
{/* Error */}
{error && <p className="text-red-500 text-sm mt-5">{error}</p>}
{/* Timeline + Task list */}
<div className="flex flex-col gap-7 md:flex-row md:items-start">
<div className="w-full md:w-1/5 bg-white rounded-xl p-5 border border-box-border shrink-0">
<ProjectTimeline tasks={tasks} />
</div>
<div className="w-full border-t border-box-border md:border-t-0 pt-4 md:pt-0 md:w-4/5">
{tasks.length === 0 ? (
<p className="text-sm text-gray-400">Belum ada task.</p>
) : (
tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))
)}
</div>
</div>
{/* Timeline + Task list — hanya tampil jika ada task */}
{/* {!loading && tasks.length === 0 ? ( */}
{tasks.length === 0 ? (
<p className="text-md text-font-shade mt-5">Belum ada tugas yang dapat dikerjakan.</p>
) : (
!loading && (
<div className="flex flex-col gap-7 md:flex-row md:items-start mt-5">
{/* Kolom kiri: Timeline */}
<div className="w-full md:w-1/5 bg-white rounded-xl p-5 border border-box-border shrink-0">
<ProjectTimeline tasks={tasks} />
</div>
{/* Kolom kanan: Task cards */}
<div className="w-full border-t border-box-border md:border-t-0 pt-4 md:pt-0 md:w-4/5">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
</div>
)
)}
</div>
);
}
}
@@ -58,10 +58,13 @@ export default function SertificateList() {
{/* Thumbnail */}
<div className="p-6 pb-0">
<div className="rounded-lg overflow-hidden">
<div className="w-full h-32 bg-gray-300 flex items-center justify-center">
<div
className="w-full h-32 flex items-center justify-center"
style={{ backgroundColor: item.background_color || '#403f3fff' }}
>
{(() => {
const Icon = getDynamicIcon(item.project_icon);
return <Icon className="w-10 h-10 text-primary" />;
return <Icon className="w-10 h-10 text-primary text-white" />;
})()}
</div>
</div>
@@ -69,7 +72,7 @@ export default function SertificateList() {
{/* Info */}
<div className="px-4 py-3">
<p className="text-xs text-gray-400 mb-0.5">
<p className="text-xs text-font mb-0.5">
{new Date(item.start_date).toLocaleDateString("id-ID", {
year: "numeric",
month: "long",
@@ -78,11 +81,11 @@ export default function SertificateList() {
month: "long",
})}
</p>
<p className="text-sm font-semibold text-gray-800 leading-snug">
<p className="text-sm font-semibold text-font-shade leading-snug">
{item.project_name}
</p>
<p className="text-xs text-gray-500 mt-1">
No: <span className="font-bold text-red-600">{item.status}</span>
<p className="text-xs text-font mt-1">
Status Projek: <span className="font-bold text-red-600">{item.status}</span>
</p>
</div>
</div>
@@ -5,6 +5,7 @@ import type { ProjectDetail } from "../../../../types/project.types";
import type { Certificate } from "../../../../types/certificate.types";
import { downloadCertificatePdf, downloadCertificateImage } from "../../../utils/Certificates";
import { generateCertificate, resolveImageUrl } from "../../../utils/SertificateGenerator";
import { customToast } from "../../../utils/showToast";
interface CertificateAvailableProps {
project: ProjectDetail | null;
@@ -19,29 +20,6 @@ export default function CertificateAvailable({ project, certificate, templateUrl
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [loadingPreview, setLoadingPreview] = useState(true);
useEffect(() => {
if (!templateUrl) return;
const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`;
generateCertificate(
resolveImageUrl(templateUrl),
certificate.user.full_name,
certificate.project.project_name,
certificate.certificate_no,
verifyUrl,
"20 Juni 2024 - 20 September 2024") //placeholder karena saya butuh data durasi certificate
.then((blob) => {
const url = URL.createObjectURL(blob);
setPreviewUrl(url);
})
.finally(() => setLoadingPreview(false));
// Cleanup supaya tidak memory leak
return () => {
if (previewUrl) URL.revokeObjectURL(previewUrl);
};
}, [templateUrl, certificate.user.full_name]);
const duration = project
? (() => {
const start = new Date(project.start_date);
@@ -56,10 +34,37 @@ export default function CertificateAvailable({ project, certificate, templateUrl
})()
: "-";
useEffect(() => {
if (!templateUrl) return;
const verifyUrl = `${window.location.origin}/verify-certificate/${certificate.uuid}`;
generateCertificate(
resolveImageUrl(templateUrl),
certificate.user.full_name,
certificate.project.project_name,
certificate.certificate_no,
verifyUrl,
duration)
.then((blob) => {
const url = URL.createObjectURL(blob);
setPreviewUrl(url);
})
.finally(() => setLoadingPreview(false));
return () => {
if (previewUrl) URL.revokeObjectURL(previewUrl);
};
}, [templateUrl, certificate.user.full_name]);
const handleDownloadImage = async () => {
setDownloadingImg(true);
try {
await downloadCertificateImage(certificate, templateUrl, "png");
customToast.success("Sertifikat berhasil diunduh!");
} catch {
customToast.error("Gagal mengunduh sertifikat!");
} finally {
setDownloadingImg(false);
}
@@ -69,6 +74,9 @@ export default function CertificateAvailable({ project, certificate, templateUrl
setDownloadingPdf(true);
try {
await downloadCertificatePdf(certificate, templateUrl);
customToast.success("Sertifikat berhasil diunduh!");
} catch {
customToast.error("Gagal mengunduh sertifikat!");
} finally {
setDownloadingPdf(false);
}
@@ -147,9 +155,13 @@ export default function CertificateAvailable({ project, certificate, templateUrl
<button
// onClick={handleCopyLink}
className="flex flex-col items-center justify-center gap-1.5 border border-card-outline hover:bg-gray-50 active:scale-95 transition-all text-red-700 font-medium py-3.5 rounded-xl text-sm"
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/verify-certificate/${certificate.uuid}`);
customToast.success("Link berhasil disalin!");
}}
>
<Link className="w-5 h-5" />
Salin Link
Salin Link
</button>
</div>
</div>
@@ -1,9 +1,9 @@
import type { InternTaskItem } from "../../../../types/project.types";
import type { ProjectTask } from "../../../../types/task.types";
const formatDate = (dateStr: string) =>
new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short' });
export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) {
export default function ProjectTimeline({ tasks }: { tasks: ProjectTask[] }) {
const today = new Date();
today.setHours(0, 0, 0, 0);
return (
@@ -98,10 +98,8 @@ export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
onClick={() => isLogout ? handleLogout() : handleNav(path)}
className={`flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-[13.5px] font-medium text-left transition-colors
${isActive
? "bg-red-50 text-red-600 font-semibold"
: danger
? "text-font hover:bg-base-foreground"
: "text-font hover:bg-red-foreground"
? "bg-red-foreground text-red font-semibold border-l-4 border-base"
: "text-font hover:bg-red-foreground"
}`}
>
<Icon className="w-[18px] h-[18px] flex-shrink-0" />