From c2aacb505f86f28ddebae2f756ad23e12ab7153e Mon Sep 17 00:00:00 2001 From: "Kemas M. Aryadary Rasyad" <112590882+aryadary0706@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:20:15 +0700 Subject: [PATCH] feat: Refactor Mentor Settings Page and improve user profile handling - Removed MentorSettingsPage component to streamline user profile management. - Updated ProjectForumTab and ProjectsInternTab for better UI/UX. - Enhanced ProjectsPage loading state with a spinner. - Improved TaskTab to handle loading states and error messages more effectively. - Added new utility functions for user initials and file URL resolution. - Updated UserService to normalize user data and handle profile updates more efficiently. - Enhanced CSS styles for better visual consistency across components. - Added new properties to CurrentUser type for better user data management. --- src/Layout/Footer-Internify.tsx | 23 +- src/Layout/Header-internify.tsx | 23 +- src/Pages/internify/HomeCalendar.tsx | 19 +- src/Pages/internify/LoginInternify.tsx | 4 +- src/Pages/internify/NotificationList.tsx | 7 +- src/Pages/internify/SettingsPage.tsx | 132 ++++++--- src/Pages/internify/UpdateProfileDialogue.tsx | 7 - src/Pages/internify/VerificationCert.tsx | 31 ++- src/Pages/internify/intern/HomeInternPage.tsx | 2 +- .../internify/intern/InternCertDetail.tsx | 19 +- src/Pages/internify/intern/InternCertList.tsx | 16 +- src/Pages/internify/intern/ParticipantTab.tsx | 18 +- src/Pages/internify/intern/ProjectsPage.tsx | 51 ++-- .../intern/components/HomeProjectList.tsx | 2 +- .../intern/components/ProjectTaskCard.tsx | 7 +- .../intern/components/ProjectTimeline.tsx | 8 +- .../intern/components/ProjectsCard.tsx | 31 +-- .../intern/components/TaskTimeline.tsx | 7 +- src/Pages/internify/layout.tsx | 4 +- .../internify/mentor/BaseProjectsPage.tsx | 99 ++++--- .../internify/mentor/CertificateForm.tsx | 2 +- src/Pages/internify/mentor/InternsPage.tsx | 53 ++-- .../mentor/MentorProjectDetailPage.tsx | 4 +- .../internify/mentor/MentorSettingsPage.tsx | 262 ------------------ .../internify/mentor/ProjectForumTab.tsx | 6 +- .../internify/mentor/ProjectsInternTab.tsx | 75 ++--- src/Pages/internify/mentor/ProjectsPage.tsx | 16 +- src/Pages/internify/mentor/TaskTab.tsx | 68 ++++- .../internify/mentor/ViewInternSubmission.tsx | 2 +- .../mentor/components/CreateTaskDialog.tsx | 4 +- .../mentor/components/HomeMentorProject.tsx | 18 +- .../mentor/components/InternsTable.tsx | 1 - .../mentor/components/MentorProjectCard.tsx | 38 +-- src/hooks/useUser.ts | 106 +++++-- src/index.css | 52 ++-- src/services/UserService.ts | 58 ++-- src/types/user.types.ts | 6 +- 37 files changed, 613 insertions(+), 668 deletions(-) delete mode 100644 src/Pages/internify/mentor/MentorSettingsPage.tsx diff --git a/src/Layout/Footer-Internify.tsx b/src/Layout/Footer-Internify.tsx index 0248862..d77dc05 100644 --- a/src/Layout/Footer-Internify.tsx +++ b/src/Layout/Footer-Internify.tsx @@ -1,15 +1,15 @@ -import { Briefcase, Globe, Users } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { FaInstagram, FaLinkedin, FaRegEnvelope } from "react-icons/fa"; +import logoHumic from "../assets/logo.png" const footerLinks = { Internship: [ { label: "Beranda", path: "/" }, - { label: "Tentang", path: "/tentang" }, - { label: "Life At Humic", path: "/life" }, + { label: "Tentang", path: "/about-us" }, + { label: "Life At Humic", path: "/our-developer" }, ], - "Internify Logo": [ - { label: "Humic Website", path: "/humic" }, + External: [ + { label: "Humic Website", path: "https://humic.telkomuniversity.ac.id/" }, { label: "Our Activity", path: "/activity" }, { label: "Life At Humic", path: "/life-humic" }, { label: "Our Developer", path: "/developer" }, @@ -17,9 +17,9 @@ const footerLinks = { }; const socials = [ - { label: "Instagram", icon: FaInstagram, href: "https://placeholder.com" }, - { label: "LinkedIn", icon: FaLinkedin, href: "https://placeholder.com" }, - { label: "Email", icon: FaRegEnvelope, href: "https://placeholder.com" }, + { label: "Instagram", icon: FaInstagram, href: "https://www.instagram.com/humicengineering/" }, + { label: "LinkedIn", icon: FaLinkedin, href: "https://www.linkedin.com/company/humic-engineering/" }, + { label: "Email", icon: FaRegEnvelope, href: "https://humic.telkomuniversity.ac.id/" }, ]; @@ -54,7 +54,7 @@ export function FooterIntern() { {/* Link columns */}
{Object.entries(footerLinks).map(([title, links]) => ( -
+

{title}

    {links.map(({ label, path }) => ( @@ -71,6 +71,11 @@ export function FooterIntern() {
))}
+ logo humic ); } diff --git a/src/Layout/Header-internify.tsx b/src/Layout/Header-internify.tsx index 3b6a78b..58768ae 100644 --- a/src/Layout/Header-internify.tsx +++ b/src/Layout/Header-internify.tsx @@ -1,25 +1,17 @@ import { Bell, Menu } from "lucide-react" -import { useEffect, useState } from "react"; +import { useState } from "react"; import humiclogo from "../assets/logo.png"; import { useNavigate } from "react-router-dom"; -import { useCurrentUser, resolveFileUrl, DEFAULT_AVATAR } from "../hooks/useUser"; - -function getInitials(fullName: string | undefined | null): string { - if (!fullName) return "U"; - const parts = fullName.trim().split(/\s+/).filter(Boolean); - if (parts.length === 0) return "U"; - if (parts.length === 1) return parts[0].charAt(0).toUpperCase(); - return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase(); -} +import { useCurrentUser, resolveFileUrl, getInitials } from "../hooks/useUser"; +import { useNotifications } from "../hooks/useNotification"; export default function Header({ toggleSidebar }: { toggleSidebar: () => void }) { const nav = useNavigate(); const { user } = useCurrentUser(); const [imageError, setImageError] = useState(false); - - const displayName = user?.full_name?.trim() || [user?.nama_depan, user?.nama_belakang].filter(Boolean).join(" ") || ""; - + const { unreadCount } = useNotifications(); const photoUrl = resolveFileUrl(user?.profile_picture); + const displayName = user?.full_name?.trim() || [user?.nama_depan, user?.nama_belakang].filter(Boolean).join(" ") || ""; const initials = getInitials(displayName); const hasPhoto = !!photoUrl && !imageError; @@ -40,8 +32,11 @@ export default function Header({ toggleSidebar }: { toggleSidebar: () => void })
- {/* Gambar Profil */} diff --git a/src/Pages/internify/HomeCalendar.tsx b/src/Pages/internify/HomeCalendar.tsx index 1368adf..26330e5 100644 --- a/src/Pages/internify/HomeCalendar.tsx +++ b/src/Pages/internify/HomeCalendar.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { ChevronLeft, ChevronRight } from 'lucide-react' +import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react' import { useDeadlines } from '../../hooks/useTasks'; const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] @@ -16,6 +16,9 @@ function getFirstDayOfMonth(year: number, month: number) { return new Date(year, month, 1).getDay() } +const formatDate = (dateInput: Date | string) => + new Date(dateInput).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }); + export default function HomeCalendar() { const [today, setToday] = useState(new Date()); const { deadlines, loading } = useDeadlines(); @@ -82,22 +85,24 @@ export default function HomeCalendar() { const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7 const leadingDays = Array.from({ length: totalCells - firstDay - daysInMonth }, (_, i) => i + 1) - // Bold dates (weekends or special) + // Tanggal Akhir Pekan const isBold = (day: number) => { const d = new Date(currentYear, currentMonth, day).getDay() - return d === 0 || d === 6 // Sun or Sat + return d === 0 || d === 6 } if (loading) { return ( -
-

Loading calendar...

+
+
+ +
) } return ( -
+
{/* Header */}

@@ -180,7 +185,7 @@ export default function HomeCalendar() { {upcomingDeadlines.map((d, i) => (
  • - {d.label} + {formatDate(d.date)} - {d.label}
  • ))} diff --git a/src/Pages/internify/LoginInternify.tsx b/src/Pages/internify/LoginInternify.tsx index a8997e2..3426a3c 100644 --- a/src/Pages/internify/LoginInternify.tsx +++ b/src/Pages/internify/LoginInternify.tsx @@ -68,7 +68,7 @@ export default function InternifyLogin() {

    {/* MAIN CONTENT */} -
    +

    Welcome back

    Access your dashboard with your credentials.

    @@ -142,7 +142,7 @@ export default function InternifyLogin() {
    diff --git a/src/Pages/internify/NotificationList.tsx b/src/Pages/internify/NotificationList.tsx index 702a20d..3a4c999 100644 --- a/src/Pages/internify/NotificationList.tsx +++ b/src/Pages/internify/NotificationList.tsx @@ -6,6 +6,7 @@ import { io } from "socket.io-client"; import * as NotificationService from "../../services/NotificationService"; import type { UINotification, BackendNotification } from "../../types/notification.types"; import mailRafiki from '../../assets/Mail-rafiki.svg' +import { customToast } from "../utils/showToast"; const getCookieToken = () => { return document.cookie @@ -70,9 +71,9 @@ export default function NotificationList() { try { await NotificationService.markAllAsRead(); setNotifications([]); - alert("All notifications marked as read!"); - } catch (err) { - console.error("Failed to mark all as read:", err); + customToast.success("All notifications marked as read!"); + } catch (err:any) { + customToast.error("Failed to mark all as read", err?.message); } }; diff --git a/src/Pages/internify/SettingsPage.tsx b/src/Pages/internify/SettingsPage.tsx index eae3778..49cdf8e 100644 --- a/src/Pages/internify/SettingsPage.tsx +++ b/src/Pages/internify/SettingsPage.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from "react"; -import { useCurrentUser, useUpdateProfile, resolveFileUrl, DEFAULT_AVATAR } from "../../hooks/useUser"; +import { useCurrentUser, useUpdateProfile, resolveFileUrl, getInitials } from "../../hooks/useUser"; import { customToast } from "../utils/showToast"; import { Pen } from "lucide-react"; import UpdateProfileDialog from "./UpdateProfileDialogue"; @@ -8,7 +8,9 @@ export default function SettingsPage() { const { user, loading: userLoading } = useCurrentUser(); const { save, loading: saving, msgRef } = useUpdateProfile(); const [confirmOpen, setConfirmOpen] = useState(false); + const isIntern = user?.role === "intern"; + const hasPendingChanges = isIntern && (user?.changes ?? 0) > 0; const [formData, setFormData] = useState({ fullName: "", @@ -16,9 +18,13 @@ export default function SettingsPage() { bio: "", }); + const [formErrors, setFormErrors] = useState({ + fullName: "", + email: "", + }); + const [photoFile, setPhotoFile] = useState(null); const [photoPreview, setPhotoPreview] = useState(null); - const [removePhoto, setRemovePhoto] = useState(false); const [imageError, setImageError] = useState(false); const fileInputRef = useRef(null); @@ -26,13 +32,13 @@ export default function SettingsPage() { if (!user) return; setFormData({ - fullName: user.full_name || `${user.nama_depan} ${user.nama_belakang}`.trim(), + fullName: user.full_name || "", email: user.email || "", bio: user.professional_bio || "", }); - setPhotoPreview(resolveFileUrl(user.profile_picture) ?? DEFAULT_AVATAR); - setRemovePhoto(false); + setPhotoFile(null); + setPhotoPreview(resolveFileUrl(user.profile_picture)); setImageError(false); }, [user]); @@ -46,7 +52,6 @@ export default function SettingsPage() { if (!file) return; setPhotoFile(file); - setRemovePhoto(false); setImageError(false); const reader = new FileReader(); @@ -54,29 +59,45 @@ export default function SettingsPage() { reader.readAsDataURL(file); }; - const handleRemovePhoto = () => { + const handleCancelPhoto = () => { setPhotoFile(null); - setRemovePhoto(true); - setPhotoPreview(DEFAULT_AVATAR); + setPhotoPreview(resolveFileUrl(user?.profile_picture)); setImageError(false); if (fileInputRef.current) fileInputRef.current.value = ""; }; + const validateForm = () => { + const errors = { fullName: "", email: "" }; + + if (!formData.fullName.trim()) errors.fullName = "Full name is required."; + if (!formData.email.trim()) errors.email = "Email address is required."; + + setFormErrors(errors); + return !errors.fullName && !errors.email; + }; + const handleSaveChanges = async () => { setConfirmOpen(false); + if (!validateForm()) return; + + const emailChanged = + formData.email.trim().toLowerCase() !== (user?.email ?? "").trim().toLowerCase(); + try { await customToast.promise( save({ full_name: formData.fullName, email: formData.email, professional_bio: formData.bio, - profile_picture: photoFile ?? (removePhoto ? DEFAULT_AVATAR : undefined), + profile_picture: !isIntern && photoFile ? photoFile : undefined, }), { loading: "Menyimpan perubahan...", success: () => ({ title: "Perubahan berhasil disimpan!", - description: msgRef.current.success ?? "", + description: emailChanged + ? "Email berubah, silakan login ulang..." + : msgRef.current.success ?? "", }), error: () => ({ title: "Gagal menyimpan perubahan!", @@ -84,22 +105,36 @@ export default function SettingsPage() { }), } ); + + // email berubah -> sesi lama tidak valid lagi, paksa login ulang + if (emailChanged) { + document.cookie = "token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + setTimeout(() => { + window.location.href = "/login-internify"; + }, 1200); + return; + } + + setPhotoFile(null); + if (fileInputRef.current) fileInputRef.current.value = ""; } catch { } }; - if (userLoading) { - return ( -
    - Loading profile... -
    - ); - } - const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setConfirmOpen(true); }; + if (userLoading) { + return ( +
    + {[...Array(3)].map((_, i) => ( +
    + ))} +
    + ); + } + return (
    {/* Header Title */} @@ -123,35 +158,32 @@ export default function SettingsPage() { {/* Card Body & Form */}
    - - {/* Profile Photo Row */}
    - {/* Avatar dengan badge edit */}
    -
    - {photoPreview && !imageError && ( +
    + {photoPreview && !imageError ? ( Profile setImageError(true)} /> + ) : ( +

    {getInitials(user?.full_name) || "U"}

    )}
    - {/* Badge edit icon */} - {!isIntern && + {!isIntern && ( - } + )}
    - {/* Info & Action Buttons */} {!isIntern ? (

    Profile Photo

    @@ -159,31 +191,35 @@ export default function SettingsPage() { JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K.

    {photoFile && ( -

    - {photoFile.name} -

    +

    {photoFile.name}

    )}
    ) : (

    Nama

    -

    {user?.full_name || formData.fullName || 'User'}

    +

    + {user?.full_name || formData.fullName || "User"} +

    +

    + {user.kelompok_peminatan} +

    )} @@ -210,8 +246,17 @@ export default function SettingsPage() { name="fullName" value={formData.fullName} onChange={handleChange} - className="w-full px-3 py-2 border border-box-border rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 transition-colors bg-white font-medium" + disabled={hasPendingChanges} + className={`w-full px-3 py-2 border rounded-lg text-sm text-gray-800 focus:outline-none transition-colors bg-white font-medium disabled:bg-gray-100 disabled:text-gray-500 ${formErrors.fullName ? "border-red-600 focus:border-red-600 focus:ring-1 focus:ring-red-600" : "border-box-border focus:border-red-600 focus:ring-1 focus:ring-red-600"}`} /> + {formErrors.fullName && ( +

    {formErrors.fullName}

    + )} + {hasPendingChanges && ( +

    + Nama hanya bisa diubah satu kali dan sudah pernah diubah. +

    + )}
    @@ -249,7 +300,7 @@ export default function SettingsPage() { @@ -257,6 +308,7 @@ export default function SettingsPage() {
    + { const dialog = dialogRef.current; if (!dialog) return; @@ -42,12 +39,8 @@ export default function UpdateProfileDialog({ return ( dialog center horizontal & vertical. - // hidden + open:flex -> hanya tampil kalau attribute [open] ada, - // supaya lifecycle showModal()/close() tetap dihormati. className="hidden open:flex fixed inset-0 m-auto h-fit max-h-[90vh] w-[90vw] max-w-md flex-col overflow-hidden rounded-xl border border-box-border p-0 shadow-xl backdrop:bg-black/40 backdrop:backdrop-blur-[2px]" onClick={(e) => { - // klik di area backdrop (target === dialog itu sendiri) -> tutup if (e.target === dialogRef.current && !loading) onClose(); }} > diff --git a/src/Pages/internify/VerificationCert.tsx b/src/Pages/internify/VerificationCert.tsx index 39884f1..b0e79a6 100644 --- a/src/Pages/internify/VerificationCert.tsx +++ b/src/Pages/internify/VerificationCert.tsx @@ -1,5 +1,6 @@ import { useParams } from "react-router-dom"; import { useCertificateVerification } from "../../hooks/useCertificates"; +import logoHumic from "../../assets/logo.png" export default function VerificationCert() { const { uuid } = useParams<{ uuid: string }>(); @@ -7,7 +8,7 @@ export default function VerificationCert() { if (loading) { return ( -
    +

    Memverifikasi sertifikat...

    ); @@ -15,9 +16,14 @@ export default function VerificationCert() { if (error || !verification) { return ( -
    -

    Sertifikat Tidak Valid

    -

    +

    + Humic Internify +

    Maaf, ID Sertifikat anda masukkan tidak valid

    +

    Sertifikat dengan UUID ini tidak dapat ditemukan atau sudah tidak berlaku.

    @@ -25,22 +31,26 @@ export default function VerificationCert() { } return ( -
    -
    +
    + Humic Internify

    Sertifikat Valid

    - Nama Intern + Nama Intern

    {verification.intern_name}

    - Proyek + Proyek

    {verification.project_name}

    - Tanggal Terbit + Tanggal Terbit

    {new Date(verification.created_at).toLocaleDateString("id-ID", { day: "numeric", @@ -50,11 +60,10 @@ export default function VerificationCert() {

    - UUID + UUID

    {verification.uuid}

    -
    ); } \ No newline at end of file diff --git a/src/Pages/internify/intern/HomeInternPage.tsx b/src/Pages/internify/intern/HomeInternPage.tsx index c523941..b7c8365 100644 --- a/src/Pages/internify/intern/HomeInternPage.tsx +++ b/src/Pages/internify/intern/HomeInternPage.tsx @@ -8,7 +8,7 @@ export default function HomeInternPage() {
    -
    +
    diff --git a/src/Pages/internify/intern/InternCertDetail.tsx b/src/Pages/internify/intern/InternCertDetail.tsx index 15c3cd8..4a673b7 100644 --- a/src/Pages/internify/intern/InternCertDetail.tsx +++ b/src/Pages/internify/intern/InternCertDetail.tsx @@ -9,7 +9,7 @@ import { useProjectDetail } from "../../../hooks/useProjects"; import { useMyCertificates } from "../../../hooks/useCertificates"; import type { Project } from "../../../types/project.types"; -export default function InternCertificateDetail () { +export default function InternCertificateDetail() { const { slug } = useParams<{ slug: string }>(); const location = useLocation(); const projectFromList = location.state?.project as Project | undefined; @@ -43,8 +43,19 @@ export default function InternCertificateDetail () { if (isLoading) { return ( -
    - Memuat Data... +
    +
    +

    Certificates

    +

    View and download internship certificates

    +
    + +
    + {/* Kolom kiri: certificate box */} +
    + + {/* Kolom kanan: history box */} +
    +
    ); } @@ -61,7 +72,7 @@ export default function InternCertificateDetail () { ) : ( @@ -17,10 +16,7 @@ function CertCardSkeleton() {
    {/* Info */} -
    -
    -
    -
    +
    ); @@ -37,22 +33,17 @@ export default function SertificateList() { }; return (
    - {/* Header */}

    Certificate History

    All your earned internship certificates

    - - {/* Loading Skeleton */} {loading && (
    - {[...Array(6)].map((_, i) => ( + {[...Array(projects.length)].map((_, i) => ( ))}
    )} - - {/* Error */} {error && !loading && (
    @@ -60,15 +51,12 @@ export default function SertificateList() {
    )} - {/* Empty */} {!loading && !error && projects.length === 0 && (
    empty image

    Tidak ada projek yang berlangsung.

    )} - - {/* Grid */} {!loading && !error && projects.length > 0 && (
    {projects.map((item: Project) => ( diff --git a/src/Pages/internify/intern/ParticipantTab.tsx b/src/Pages/internify/intern/ParticipantTab.tsx index c71e596..8db811a 100644 --- a/src/Pages/internify/intern/ParticipantTab.tsx +++ b/src/Pages/internify/intern/ParticipantTab.tsx @@ -1,7 +1,12 @@ import { Users } from 'lucide-react'; import type { ProjectDetail, ProjectMember } from '../../../types/project.types'; +import { useState } from 'react'; +import { resolveFileUrl, getInitials } from '../../../hooks/useUser'; export default function ParticipantsTab({ project }: { project: ProjectDetail }) { + const [ImageError, setImageError] = useState(false); + const Avatar = resolveFileUrl(project.admin.profile_picture); + const hasAvatar = !!Avatar && !ImageError; return (
    {/* Mentor */} @@ -9,14 +14,19 @@ export default function ParticipantsTab({ project }: { project: ProjectDetail })

    Mentor

    - {project.admin.profile_picture ? ( + {hasAvatar ? ( {project.admin.full_name} setImageError(true)} /> ) : ( -
    +
    + + {getInitials(project.admin.full_name)} + +
    )} {project.admin.full_name}
    diff --git a/src/Pages/internify/intern/ProjectsPage.tsx b/src/Pages/internify/intern/ProjectsPage.tsx index afe36fe..f535ee9 100644 --- a/src/Pages/internify/intern/ProjectsPage.tsx +++ b/src/Pages/internify/intern/ProjectsPage.tsx @@ -2,18 +2,37 @@ import ProjectCard from './components/ProjectsCard' import { useMyProjects } from '../../../hooks/useProjects'; import TeamPana from '../../../assets/Team-pana.svg' +function ProjectCardSkeleton() { + return ( +
    + ) +} + export default function ProjectsPage() { const { projects, loading, error } = useMyProjects(); - if (loading) return ( -
    - {[...Array(3)].map((_, i) => ( -
    - ))} -
    - ); + const renderContent = () => { + if (loading) { + return [...Array(5)].map((_, i) => ); + } - if (error) return

    {error}

    ; + if (error) { + return

    {error}

    ; + } + + if (projects.length === 0) { + return ( +
    + +

    Kamu belum tergabung ke projek apapun

    +
    + ); + } + + return projects.map(project => ( + + )); + }; return (
    @@ -22,19 +41,9 @@ export default function ProjectsPage() {

    View assigned projects and track their progress.

    - {projects.length === 0 ? ( -
    - -

    Kamu belum tergabung ke projek apapun

    -
    - ) : ( - // Grid card -
    - {projects.map(project => ( - - ))} -
    - )} +
    + {renderContent()} +
    ) } \ No newline at end of file diff --git a/src/Pages/internify/intern/components/HomeProjectList.tsx b/src/Pages/internify/intern/components/HomeProjectList.tsx index 9b05ca6..0373def 100644 --- a/src/Pages/internify/intern/components/HomeProjectList.tsx +++ b/src/Pages/internify/intern/components/HomeProjectList.tsx @@ -20,7 +20,7 @@ const HomeProjectList = () => { if (error) return

    {error}

    ; return ( -
    +

    Projects

    {projects.length === 0 ? ( diff --git a/src/Pages/internify/intern/components/ProjectTaskCard.tsx b/src/Pages/internify/intern/components/ProjectTaskCard.tsx index cddb41a..42dc702 100644 --- a/src/Pages/internify/intern/components/ProjectTaskCard.tsx +++ b/src/Pages/internify/intern/components/ProjectTaskCard.tsx @@ -34,6 +34,12 @@ export default function TaskCard({ task }: { task: InternTaskItem }) { onClick={() => navigate(`/intern/projects/${task.project_slug}/${task.slug}?type=${task.submission_type}`)} >
    +
    + + {statusLabels[displayStatus]} + +
    Deadline : {formatDate(task.deadline_at)} at {formatTime(task.deadline_at)}
    @@ -48,7 +54,6 @@ export default function TaskCard({ task }: { task: InternTaskItem }) {
    -
    -
    +
    +
    Timeline
      @@ -19,13 +19,13 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] }) const isPast = d < today; const active = isToday || isPast; return ( -
    • +
    • {index < tasks.length - 1 && (
      )} {/* Dot */}
      diff --git a/src/Pages/internify/intern/components/ProjectsCard.tsx b/src/Pages/internify/intern/components/ProjectsCard.tsx index 4fd59d2..1f0b8ce 100644 --- a/src/Pages/internify/intern/components/ProjectsCard.tsx +++ b/src/Pages/internify/intern/components/ProjectsCard.tsx @@ -1,39 +1,40 @@ import { useNavigate } from 'react-router-dom'; import { getProjectIcon } from '../../../../lib/ProjectIcons'; import type { Project } from '../../../../types/project.types'; +import { resolveFileUrl } from '../../../../hooks/useUser'; const ProjectCard = (project: Project) => { const navigate = useNavigate(); const Icon = getProjectIcon(project.project_icon); - - - const tasksDone = project.task_done; + const Avatar = resolveFileUrl(project.admin.profile_picture); + const tasksDone = project.task_done; const tasksTotal = project.total_tasks; return (
      navigate(`/intern/projects/${project.slug}`)} - className=" rounded-xl overflow-hidden shadow-sm border border-gray-100 w-full flex flex-col cursor-pointer hover:shadow-md transition-shadow duration-200" + 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 */} -
      +
      {/* Content */} -
      +
      - {/* Project Name */} -

      +

      {project.project_name}

      - {/* Mentor */}
      - {project.admin.profile_picture ? ( + {Avatar ? ( {project.admin.full_name} @@ -44,7 +45,6 @@ const ProjectCard = (project: Project) => { {project.admin.email}
      - {/* Progress bar */}
      {
      - {/* Button */} -
      -
      diff --git a/src/Pages/internify/intern/components/TaskTimeline.tsx b/src/Pages/internify/intern/components/TaskTimeline.tsx index 1472efd..61f3ccc 100644 --- a/src/Pages/internify/intern/components/TaskTimeline.tsx +++ b/src/Pages/internify/intern/components/TaskTimeline.tsx @@ -8,6 +8,9 @@ const TaskTimeline = () => { const isOverdue = (dateStr: string) => new Date(dateStr) < today; + const formatDate = (dateInput: Date | string) => + new Date(dateInput).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); + if (loading) return (
      {[...Array(3)].map((_, i) => ( @@ -19,7 +22,7 @@ const TaskTimeline = () => { if (error) return

      {error}

      ; return ( -
      +

      Timeline Tugas

      {tasks.length === 0 ? ( @@ -50,7 +53,7 @@ const TaskTimeline = () => {

      - {deadlineDate.toLocaleDateString("id-ID")} AT{" "} + {formatDate(deadlineDate)} AT{" "} {deadlineDate.toLocaleTimeString("id-ID", { hour: "2-digit", minute: "2-digit", diff --git a/src/Pages/internify/layout.tsx b/src/Pages/internify/layout.tsx index 47611a1..c79af1a 100644 --- a/src/Pages/internify/layout.tsx +++ b/src/Pages/internify/layout.tsx @@ -21,7 +21,7 @@ export const InternLayout = () => { onClick={closeSidebar} /> )} -

      +
      @@ -46,7 +46,7 @@ export const MentorLayout = () => { onClick={closeSidebar} /> )} -
      +
      diff --git a/src/Pages/internify/mentor/BaseProjectsPage.tsx b/src/Pages/internify/mentor/BaseProjectsPage.tsx index 53e0c6d..e120453 100644 --- a/src/Pages/internify/mentor/BaseProjectsPage.tsx +++ b/src/Pages/internify/mentor/BaseProjectsPage.tsx @@ -5,67 +5,78 @@ import CreateProjectModal from './components/CreateProjectDialog'; import { useProjectsByRole } from '../../../hooks/useListProjects'; import TeamPana from '../../../assets/Team-pana.svg'; +function MentorProjectCardSkeleton() { + return ( +
      +
      + ) +} + export default function MentorProjectsPage() { const [isModalOpen, setIsModalOpen] = useState(false); const { projects, loading, error, refetch } = useProjectsByRole(); - const openDialog = () => { - setIsModalOpen(true); - }; - const closeDialog = () => { - setIsModalOpen(false); - }; + const openDialog = () => setIsModalOpen(true); + const closeDialog = () => setIsModalOpen(false); const handleRefetch = () => { closeDialog(); refetch(); - } + }; - if (loading) return ( -
      - {[...Array(3)].map((_, i) => ( -
      - ))} -
      - ); - if (error) return

      {error}

      ; + const renderContent = () => { + if (loading) { + return ( +
      + {[...Array(4)].map((_, i) => ( + + ))} +
      + ); + } + + if (error) return

      {error}

      ; + + if (projects.length === 0) { + return ( +
      + +

      + Belum ada project, buat project dengan klik 'Add' +

      +
      + ); + } + + return projects.map(project => ( + + )); + }; return (

      List Projects

      -

      Create, manage, and monitor ongoing projects and assignments.

      -
      -
      - +

      + Create, manage, and monitor ongoing projects and assignments. +

      +
      -
      - - {projects.length === 0 && ( -
      - -

      Belum ada project, Buat project dengan kik 'add'

      -
      - )} - - {projects.map(project => ( - - ))} -
      +
      {renderContent()}
      {isModalOpen && ( ); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/Pages/internify/mentor/CertificateForm.tsx b/src/Pages/internify/mentor/CertificateForm.tsx index 74d22a3..71bdfd3 100644 --- a/src/Pages/internify/mentor/CertificateForm.tsx +++ b/src/Pages/internify/mentor/CertificateForm.tsx @@ -192,7 +192,7 @@ export default function CertificateDetail() { > {uploading ? ( <> - + Mengupload... ) : ( diff --git a/src/Pages/internify/mentor/InternsPage.tsx b/src/Pages/internify/mentor/InternsPage.tsx index 0210e26..bb3184a 100644 --- a/src/Pages/internify/mentor/InternsPage.tsx +++ b/src/Pages/internify/mentor/InternsPage.tsx @@ -66,18 +66,10 @@ export default function InternsManagement() { } }; - if (loading) return ( -
      - {[...Array(5)].map((_, i) => ( -
      - ))} -
      - ); - if (error) return

      {error}

      ; return ( -
      +
      {/* Header Title */}

      Interns Management

      @@ -119,7 +111,7 @@ export default function InternsManagement() { ))} -
      @@ -130,11 +122,10 @@ export default function InternsManagement() {
      - openAssignFor(userId)} - onDelete={handleDeleteIntern} - /> + {loading ? ( +
      + Memuat Data... +
      + ) : ( + openAssignFor(userId)} + onDelete={handleDeleteIntern} + /> + ) + } - {isModalOpen && + { + isModalOpen && } -
      +
      ); } \ No newline at end of file diff --git a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx index 6f1c37d..db9d123 100644 --- a/src/Pages/internify/mentor/MentorProjectDetailPage.tsx +++ b/src/Pages/internify/mentor/MentorProjectDetailPage.tsx @@ -25,7 +25,7 @@ export default function MentorProjectsDetailPage() { }; if (loading) return ( -
      +
      {[...Array(3)].map((_, i) => (
      ))} @@ -47,7 +47,7 @@ export default function MentorProjectsDetailPage() { return (
      {/* Header Utama */} -
      +

      {task.title}

      -
      - - {/* Info & Action Buttons */} -
      -

      Profile Photo

      -

      - JPG, GIF or PNG. Recommended size 800x800px. Max size of 800K. -

      - {photoFile && ( -

      - šŸ“Ž {photoFile.name} -

      - )} -
      - - -
      -
      - - {/* Hidden file input */} - -
      - -
      - - {/* Grid Inputs */} -
      -
      - - -
      -
      - - -
      -
      - - {/* Professional Bio */} -
      - -