From 5689898f334b44a7bd899bb9ab426d19b0081a81 Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:42:22 +0700 Subject: [PATCH] feat: enhance certificate management and project task features - Updated CertificateForm to improve loading state UI with skeletons. - Enhanced CertificateResult with better loading indicators and structure. - Refactored ProjectForumTab to include a skeleton loader and improved error handling. - Introduced ProjectTaskTab for managing project tasks with modals for creating and editing tasks. - Improved ProjectsPage with a skeleton loader during project data fetching. - Refined AddInternsToProjects, ArchiveProjectDialog, CompleteProjectDialog, CreateProjectDialog, CreateTaskDialog, and EditTaskDialog components to handle modal states more effectively. - Updated EligibleInternTable to enhance loading states and formatting of dates. - Modified StatusSubmissionTable to simplify status display. - Improved certificate generation utility functions for better date formatting and layout management. - Removed deprecated certificate assets and added a new template for certificates. - Updated types for certificates and projects to include start and end dates. --- src/Pages/internify/HomeCalendar.tsx | 21 ++-- src/Pages/internify/LoginInternify.tsx | 6 +- src/Pages/internify/intern/ForumTab.tsx | 63 +++++++----- src/Pages/internify/intern/HomeInternPage.tsx | 2 +- .../internify/intern/InternCertDetail.tsx | 2 +- src/Pages/internify/intern/InternCertList.tsx | 15 +-- .../internify/intern/ProjectDetailPage.tsx | 34 ++++++- src/Pages/internify/intern/TaskSubmission.tsx | 6 +- .../intern/components/CertificateHistory.tsx | 2 +- .../intern/components/ProjectTaskCard.tsx | 2 +- .../intern/components/ProjectTimeline.tsx | 4 +- .../components/SubmissionStatusTable.tsx | 1 - .../intern/components/TaskForm-link.tsx | 8 +- .../intern/components/TaskTimeline.tsx | 6 +- .../internify/mentor/CertificateForm.tsx | 30 ++++-- .../internify/mentor/CertificateResult.tsx | 15 ++- .../internify/mentor/ProjectForumTab.tsx | 66 +++++++----- .../{TaskTab.tsx => ProjectTaskTab.tsx} | 14 +-- src/Pages/internify/mentor/ProjectsPage.tsx | 69 +++++++++++-- .../components/AddInternsToProjects.tsx | 7 +- .../components/ArchiveProjectDialog.tsx | 1 - .../components/CertificateProjectCard.tsx | 4 +- .../components/CompleteProjectDialog.tsx | 5 +- .../mentor/components/CreateProjectDialog.tsx | 1 - .../mentor/components/CreateTaskDialog.tsx | 1 - .../mentor/components/EditSubmissionTask.tsx | 1 - .../mentor/components/EditTaskDialog.tsx | 19 +++- .../mentor/components/EligibleInternTable.tsx | 30 +++--- .../components/StatusSubmissionTable.tsx | 2 +- src/Pages/utils/Certificates.ts | 18 +++- src/Pages/utils/SertificateGenerator.ts | 73 ++++++-------- src/Pages/utils/testlayout.html | 95 +++++++++++------- src/assets/certificate.png | Bin 37274 -> 0 bytes src/assets/template.png | Bin 0 -> 228883 bytes src/types/certificate.types.ts | 3 + src/types/project.types.ts | 4 +- 36 files changed, 390 insertions(+), 240 deletions(-) rename src/Pages/internify/mentor/{TaskTab.tsx => ProjectTaskTab.tsx} (96%) delete mode 100644 src/assets/certificate.png create mode 100644 src/assets/template.png diff --git a/src/Pages/internify/HomeCalendar.tsx b/src/Pages/internify/HomeCalendar.tsx index 26330e5..2f81818 100644 --- a/src/Pages/internify/HomeCalendar.tsx +++ b/src/Pages/internify/HomeCalendar.tsx @@ -102,9 +102,9 @@ export default function HomeCalendar() { } return ( -
+
{/* Header */} -
+

{MONTH_NAMES[currentMonth]} {currentYear}

@@ -126,8 +126,7 @@ export default function HomeCalendar() {
- {/* Day labels */} -
+
{DAY_LABELS.map(day => (
{day} @@ -135,8 +134,7 @@ export default function HomeCalendar() { ))}
- {/* Calendar grid */} -
+
{/* Trailing days from prev month */} {trailingDays.map(day => (
@@ -144,18 +142,17 @@ export default function HomeCalendar() {
))} - {/* Current month days */} {currentDays.map(day => (
{isToday(day) ? ( - + {day} ) : ( {day} @@ -173,10 +170,10 @@ export default function HomeCalendar() {
{/* Divider */} -
+
{/* Upcoming Deadlines */} -
+

Upcoming Deadlines

@@ -184,7 +181,7 @@ export default function HomeCalendar() {
    {upcomingDeadlines.map((d, i) => (
  • - + {formatDate(d.date)} - {d.label}
  • ))} diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index 3426a3c..169b90c 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -57,9 +57,9 @@ export default function InternifyLogin() { }; return ( -
    -
    -
    +
    +
    +
    Logo
    diff --git a/src/Pages/internify/intern/ForumTab.tsx b/src/Pages/internify/intern/ForumTab.tsx index 869e9ff..203671e 100644 --- a/src/Pages/internify/intern/ForumTab.tsx +++ b/src/Pages/internify/intern/ForumTab.tsx @@ -8,38 +8,53 @@ import { FileText } from 'lucide-react'; export default function ForumTab({ project }: { project: Project }) { const { tasks, loading, error } = useProjectMyTasks(project.id); + const renderSkeleton = () => ( +
    +
    +
    + {[...Array(3)].map((_, i) => ( +
    +
    +
    + ))} +
    +
    +
    + {[...Array(3)].map((_, i) => ( +
    + ))} +
    +
    + ); + + const isEmpty = !loading && !error && tasks.length === 0; + return (
    - {loading && ( -
    - {[...Array(3)].map((_, i) => ( -
    - ))} -
    - )} - {error &&

    {error}

    } - - {tasks.length === 0 ? ( -
    - + {/* Urutan eksklusif: hanya satu state yang tampil dalam satu waktu */} + {loading ? ( + renderSkeleton() + ) : error ? ( +

    {error}

    + ) : isEmpty ? ( +
    +

    Belum ada tugas yang dapat dikerjakan.

    ) : ( - !loading && ( -
    -
    - -
    -
    - {tasks.map((task) => ( - - ))} -
    +
    +
    +
    - ) +
    + {tasks.map((task) => ( + + ))} +
    +
    )}
    ); -} +} \ No newline at end of file diff --git a/src/Pages/internify/intern/HomeInternPage.tsx b/src/Pages/internify/intern/HomeInternPage.tsx index b7c8365..2f2aba9 100644 --- a/src/Pages/internify/intern/HomeInternPage.tsx +++ b/src/Pages/internify/intern/HomeInternPage.tsx @@ -16,7 +16,7 @@ export default function HomeInternPage() {
    -
    +
    diff --git a/src/Pages/internify/intern/InternCertDetail.tsx b/src/Pages/internify/intern/InternCertDetail.tsx index 4a673b7..de7f6ad 100644 --- a/src/Pages/internify/intern/InternCertDetail.tsx +++ b/src/Pages/internify/intern/InternCertDetail.tsx @@ -15,7 +15,7 @@ export default function InternCertificateDetail() { const projectFromList = location.state?.project as Project | undefined; const { project: projectDetail, loading: projectLoading } = useProjectDetail(slug ?? ""); - const { certificates, loading: certLoading, refetch: refetchCertificates } = useMyCertificates(); + const { certificates, loading: certLoading } = useMyCertificates(); const myCertificateForProject = useMemo(() => { diff --git a/src/Pages/internify/intern/InternCertList.tsx b/src/Pages/internify/intern/InternCertList.tsx index aa75939..637ce10 100644 --- a/src/Pages/internify/intern/InternCertList.tsx +++ b/src/Pages/internify/intern/InternCertList.tsx @@ -2,13 +2,12 @@ import { AlertCircle } from "lucide-react"; import { useMyProjects } from "../../../hooks/useProjects"; import type { Project } from "../../../types/project.types"; import { useNavigate } from "react-router-dom"; -import * as LucideIcons from 'lucide-react'; -import type { LucideProps } from 'lucide-react'; +import { getProjectIcon } from "../../../lib/ProjectIcons"; import emptyimage from '../../../assets/Learning-amico.svg' function CertCardSkeleton() { return ( -
    +
    {/* Thumbnail */}
    @@ -25,12 +24,6 @@ function CertCardSkeleton() { export default function SertificateList() { const { projects, loading, error } = useMyProjects(); const navigate = useNavigate(); - - const getDynamicIcon = (iconName: string) => { - const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1); - const Icon = (LucideIcons as unknown as Record>)[formatted]; - return Icon ?? LucideIcons.FolderOpen; - }; return (
    @@ -73,8 +66,8 @@ export default function SertificateList() { style={{ backgroundColor: item.background_color || '#403f3fff' }} > {(() => { - const Icon = getDynamicIcon(item.project_icon); - return ; + const Icon = getProjectIcon(item.project_icon); + return ; })()}
    diff --git a/src/Pages/internify/intern/ProjectDetailPage.tsx b/src/Pages/internify/intern/ProjectDetailPage.tsx index fa8a315..8029cbc 100644 --- a/src/Pages/internify/intern/ProjectDetailPage.tsx +++ b/src/Pages/internify/intern/ProjectDetailPage.tsx @@ -3,15 +3,41 @@ import { useParams } from 'react-router-dom'; import { useProjectDetail } from '../../../hooks/useProjects'; import ForumTab from './ForumTab'; import ParticipantsTab from './ParticipantTab'; +import { Loader2 } from 'lucide-react'; + +function ProjectDetailSkeleton() { + return ( +
    + {/* Tab bar skeleton — tinggi & spacing disamakan dengan tombol tab asli */} +
    +
    +
    +
    +
    +
    +
    +
    + + {/* Konten tab skeleton (default tab = forum) */} +
    + +
    + + Memuat Konten.. +
    +
    +
    + ); +} export default function ProjectDetailPage() { - const { slug } = useParams<{ slug: string }>(); + const { slug } = useParams<{ slug: string }>(); const { project, loading, error } = useProjectDetail(slug ?? ""); const [activeTab, setActiveTab] = useState<'forum' | 'participants'>('forum'); - if (loading) return

    Memuat project...

    ; - if (error) return

    {error}

    ; - + if (loading) return ; + if (error) return

    {error}

    ; + if (!project) return

    Project tidak ditemukan.

    ; const tabClass = (tab: string) => diff --git a/src/Pages/internify/intern/TaskSubmission.tsx b/src/Pages/internify/intern/TaskSubmission.tsx index 9d5ad9c..479f6f9 100644 --- a/src/Pages/internify/intern/TaskSubmission.tsx +++ b/src/Pages/internify/intern/TaskSubmission.tsx @@ -41,7 +41,7 @@ export default function TaskSubmission() {
    -

    Task Description

    +

    Deskripsi

    {task.description}

    @@ -71,8 +71,8 @@ export default function TaskSubmission() {
    -

    - Once submitted, your work will be locked for mentor review. You can request a re-submission if needed. +

    + Setelah disubmit, submission anda dapat dilihat oleh mentor. Pastikan submission yang anda masukan benar

    diff --git a/src/Pages/internify/intern/components/CertificateHistory.tsx b/src/Pages/internify/intern/components/CertificateHistory.tsx index 7ee991b..26c3ea9 100644 --- a/src/Pages/internify/intern/components/CertificateHistory.tsx +++ b/src/Pages/internify/intern/components/CertificateHistory.tsx @@ -16,7 +16,7 @@ export default function SertificateHistory() { {loading && (
    - Loading certificates... + memuat sertifikat...
    )} diff --git a/src/Pages/internify/intern/components/ProjectTaskCard.tsx b/src/Pages/internify/intern/components/ProjectTaskCard.tsx index 42dc702..ef10285 100644 --- a/src/Pages/internify/intern/components/ProjectTaskCard.tsx +++ b/src/Pages/internify/intern/components/ProjectTaskCard.tsx @@ -30,7 +30,7 @@ export default function TaskCard({ task }: { task: InternTaskItem }) { : task.submission_status; return (
    navigate(`/intern/projects/${task.project_slug}/${task.slug}?type=${task.submission_type}`)} >
    diff --git a/src/Pages/internify/intern/components/ProjectTimeline.tsx b/src/Pages/internify/intern/components/ProjectTimeline.tsx index b523085..fde4be4 100644 --- a/src/Pages/internify/intern/components/ProjectTimeline.tsx +++ b/src/Pages/internify/intern/components/ProjectTimeline.tsx @@ -8,7 +8,7 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) today.setHours(0, 0, 0, 0); return (
    -
    +
    Timeline
      @@ -21,7 +21,7 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) return (
    • {index < tasks.length - 1 && ( -
      +
      )} {/* Dot */}
      -