Update all hooks (Projects, Tasks, Certificates), implement backend for Projects. HomePage on Mentors
This commit is contained in:
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
||||
export default function Header({ toggleSidebar }: { toggleSidebar: () => void }) {
|
||||
const nav = useNavigate();
|
||||
return (
|
||||
<header className='sticky top-0 flex w-full justify-between items-center py-2 px-12 bg-white border-b border-gray-300 z-10'>
|
||||
<header className='sticky top-0 flex w-full justify-between items-center py-4 px-12 bg-white border-b border-gray-300 z-10'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
@@ -21,9 +21,9 @@ export default function Header({ toggleSidebar }: { toggleSidebar: () => void })
|
||||
|
||||
<div className="flex flex-row gap-6 items-center">
|
||||
<button onClick={()=>nav("notifications")}>
|
||||
<Bell className="w-6 h-6 hover:text-red-800"/>
|
||||
<Bell className="w-5 h-5 hover:text-red-800"/>
|
||||
</button>
|
||||
<div className="bg-blue-500 rounded-xl w-9 h-9">
|
||||
<div className="bg-red rounded-full w-9 h-9">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function InternifyLogin() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.post("/auth-api/login", { email, password });
|
||||
|
||||
console.log(response.data.data)
|
||||
const { token } = response.data.data;
|
||||
document.cookie = `token=${token}; path=/; SameSite=Strict`;
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function InternifyLogin() {
|
||||
const role = decoded?.role;
|
||||
if (role === "intern") {
|
||||
navigate("/intern");
|
||||
} else if (role === "admin") {
|
||||
} else if (role === "mentor" || role === "admin") {
|
||||
navigate("/mentor");
|
||||
} else {
|
||||
setError("Role user tidak dikenali.");
|
||||
@@ -59,7 +59,7 @@ export default function InternifyLogin() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex flex-col justify-between items-center py-8 px-4 font-sans selection:bg-red-200">
|
||||
<div className="min-h-screen bg-background flex flex-col justify-between items-center py-8 px-4 font-sans selection:bg-red-foreground">
|
||||
<div className="hidden md:block"></div>
|
||||
|
||||
<div className="w-full max-w-[440px] flex flex-col items-center">
|
||||
@@ -72,7 +72,7 @@ export default function InternifyLogin() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md bg-white rounded-2xl border border-gray-100 p-8 md:p-12">
|
||||
<div className="max-w-md bg-white rounded-2xl border border-box-border p-8 md:p-12">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-1">Welcome back</h2>
|
||||
<p className="text-sm text-gray-500 mb-6">Access your dashboard with your credentials.</p>
|
||||
|
||||
@@ -147,14 +147,14 @@ export default function InternifyLogin() {
|
||||
<div className="mt-1">
|
||||
<div className="relative flex py-2 items-center">
|
||||
<div className="flex-grow border-t border-gray-100"></div>
|
||||
<span className="flex-shrink mx-4 text-[10px] font-bold tracking-wider text-gray-400 uppercase">
|
||||
<span className="flex-shrink mx-4 text-[10px] font-bold tracking-wider text-font-shade uppercase">
|
||||
SUPPORT
|
||||
</span>
|
||||
<div className="flex-grow border-t border-gray-100"></div>
|
||||
</div>
|
||||
|
||||
<button className="w-full mt-2 flex items-center justify-center gap-2 border border-gray-200 rounded-full py-1.5 text-xs text-gray-600 hover:bg-gray-50 transition-colors font-medium">
|
||||
<svg className="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<button className="w-full mt-2 flex items-center justify-center gap-2 border border-gray-200 rounded-full py-1.5 text-xs text-font-shade hover:bg-gray-50 transition-colors font-medium">
|
||||
<svg className="w-3.5 h-3.5 text-font-shade" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Frequently Asked Questions
|
||||
|
||||
@@ -1,106 +1,94 @@
|
||||
import { useState } from "react";
|
||||
import { Check, Download, Info } from "lucide-react";
|
||||
import { LuFileSpreadsheet } from "react-icons/lu"; // Alternatif icon task dari react-icons
|
||||
import { LuFileSpreadsheet } from "react-icons/lu";
|
||||
import { useNotifications } from "../../hooks/useNotification";
|
||||
|
||||
const initialNotifications = [
|
||||
{
|
||||
id: 1,
|
||||
type: "new-task",
|
||||
title: "New Task Assigned",
|
||||
time: "Just now",
|
||||
isNew: true,
|
||||
group: "TODAY",
|
||||
description: "Mentor Sarah Johnson has pushed a new task: User Persona Analysis"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "achievement",
|
||||
title: "Certificate Ready",
|
||||
time: "2 hours ago",
|
||||
isNew: false,
|
||||
group: "TODAY",
|
||||
description: "Your certificate for UI Engineering Phase 1 is now available for download!"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "system",
|
||||
title: "System Maintenance",
|
||||
time: "2 days ago",
|
||||
isNew: false,
|
||||
group: "YESTERDAY",
|
||||
description: "Internify will undergo scheduled maintenance this Sunday from 02:00 AM to 04:00 AM GMT+7."
|
||||
}
|
||||
];
|
||||
function getGroup(created_at: string): "TODAY" | "YESTERDAY" | "OLDER" {
|
||||
const date = new Date(created_at);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (diffDays === 0) return "TODAY";
|
||||
if (diffDays === 1) return "YESTERDAY";
|
||||
return "OLDER";
|
||||
}
|
||||
|
||||
function getTimeLabel(created_at: string): string {
|
||||
const date = new Date(created_at);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins} minutes ago`;
|
||||
if (diffHours < 24) return `${diffHours} hours ago`;
|
||||
return `${diffDays} days ago`;
|
||||
}
|
||||
|
||||
function getNotifType(title: string): "new-task" | "achievement" | "system" {
|
||||
const lower = title.toLowerCase();
|
||||
if (lower.includes("task") || lower.includes("tugas")) return "new-task";
|
||||
if (lower.includes("certificate") || lower.includes("sertifikat")) return "achievement";
|
||||
return "system";
|
||||
}
|
||||
|
||||
export default function NotificationList() {
|
||||
const [notifications, setNotifications] = useState(initialNotifications);
|
||||
const { notifications, loading, error, markAllRead, markOneRead } = useNotifications();
|
||||
|
||||
const handleMarkAllRead = () => {
|
||||
setNotifications(prev => prev.map(n => ({ ...n, isNew: false })));
|
||||
alert("All notifications marked as read!");
|
||||
};
|
||||
const todayNotifs = notifications.filter(n => getGroup(n.created_at) === "TODAY");
|
||||
const yesterdayNotifs = notifications.filter(n => getGroup(n.created_at) === "YESTERDAY");
|
||||
const olderNotifs = notifications.filter(n => getGroup(n.created_at) === "OLDER");
|
||||
|
||||
if (loading) return (
|
||||
<div className="space-y-3">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error) return <p className="text-red-500 text-sm">{error}</p>;
|
||||
|
||||
const renderNotifCard = (item: typeof notifications[0], isOld = false) => {
|
||||
const type = getNotifType(item.title);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="page-title">Notifications</h1>
|
||||
<p className="page-title-desc">
|
||||
Keep track of your interns' progress and system updates.
|
||||
</p>
|
||||
</div>
|
||||
{/* <button
|
||||
onClick={handleMarkAllRead}
|
||||
className="flex items-center gap-1.5 self-start sm:self-center px-4 py-2 border border-gray-200 rounded-lg text-xs font-semibold text-gray-700 bg-white hover:bg-gray-50 transition-colors shadow-sm"
|
||||
>
|
||||
<Check className="w-4 h-4 text-green-600 stroke-[3]" />
|
||||
Mark all as read
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
{/* List Group Section */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Render Group TODAY */}
|
||||
<div className="space-y-3">
|
||||
{notifications.filter(n => n.group === "TODAY").map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`relative flex items-start gap-4 p-5 bg-white border border-gray-100 rounded-xl shadow-[0_2px_10px_rgba(0,0,0,0.01)] ${
|
||||
item.type === 'new-task' ? 'border-l-4 border-l-[#B30000]' : ''
|
||||
}`}
|
||||
onClick={() => { if (!item.is_read) markOneRead(item.id); }}
|
||||
className={`relative flex items-start gap-4 p-5 border rounded-xl cursor-pointer transition-colors
|
||||
${isOld ? "bg-gray-50 border-gray-100" : "bg-white border-gray-100 shadow-[0_2px_10px_rgba(0,0,0,0.01)]"}
|
||||
${type === "new-task" && !isOld ? "border-l-4 border-l-[#B30000]" : ""}
|
||||
`}
|
||||
>
|
||||
{/* Icon Container */}
|
||||
{/* Icon */}
|
||||
<div className={`p-2.5 rounded-xl ${
|
||||
item.type === 'new-task' ? 'bg-red-50 text-red-600' : 'bg-gray-100 text-gray-600'
|
||||
type === "new-task" ? "bg-red-50 text-red-600"
|
||||
: type === "achievement" ? "bg-green-50 text-green-600"
|
||||
: "bg-gray-200 text-gray-500"
|
||||
}`}>
|
||||
{item.type === 'new-task' && <LuFileSpreadsheet className="w-5 h-5" />}
|
||||
{item.type === 'achievement' && <Check className="w-5 h-5 stroke-[2.5]" />}
|
||||
{type === "new-task" && <LuFileSpreadsheet className="w-5 h-5" />}
|
||||
{type === "achievement" && <Check className="w-5 h-5 stroke-[2.5]" />}
|
||||
{type === "system" && <Info className="w-5 h-5" />}
|
||||
</div>
|
||||
|
||||
{/* Content text */}
|
||||
{/* Content */}
|
||||
<div className="flex-grow space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{item.isNew && (
|
||||
{!item.is_read && (
|
||||
<span className="bg-[#B30000] text-white font-extrabold text-[9px] px-2 py-0.5 rounded uppercase tracking-wider">
|
||||
NEW
|
||||
</span>
|
||||
)}
|
||||
<h3 className="text-sm font-bold text-gray-900">{item.title}</h3>
|
||||
<h3 className={`text-sm font-bold ${isOld ? "text-gray-700" : "text-gray-900"}`}>
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Custom description coloring logic */}
|
||||
<p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
|
||||
{item.type === 'new-task' ? (
|
||||
<>Mentor <span className="font-semibold text-gray-900">Sarah Johnson</span> has pushed a new task: <span className="italic font-semibold text-gray-800">User Persona Analysis</span></>
|
||||
) : item.type === 'achievement' ? (
|
||||
<>Your certificate for <span className="font-semibold text-gray-900">UI Engineering Phase 1</span> is now available for download!</>
|
||||
) : item.description}
|
||||
{item.message}
|
||||
</p>
|
||||
|
||||
{/* Conditional Actions */}
|
||||
{item.type === 'new-task' && (
|
||||
{/* Actions */}
|
||||
{type === "new-task" && !isOld && (
|
||||
<div className="flex items-center gap-2 pt-3">
|
||||
<button className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors">
|
||||
View Task
|
||||
@@ -110,8 +98,7 @@ export default function NotificationList() {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'achievement' && (
|
||||
{type === "achievement" && !isOld && (
|
||||
<button className="flex items-center gap-1 text-xs font-bold text-[#B30000] hover:underline pt-2">
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
Download Certificate
|
||||
@@ -121,39 +108,59 @@ export default function NotificationList() {
|
||||
|
||||
{/* Time */}
|
||||
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap self-start md:absolute md:top-5 md:right-5">
|
||||
{item.time}
|
||||
{getTimeLabel(item.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{/* Header Section YESTERDAY */}
|
||||
<div className="space-y-3 pt-2">
|
||||
<h2 className="text-[10px] font-bold tracking-widest text-gray-400 uppercase">
|
||||
YESTERDAY
|
||||
</h2>
|
||||
|
||||
{notifications.filter(n => n.group === "YESTERDAY").map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="relative flex items-start gap-4 p-5 bg-gray-50 border border-gray-100 rounded-xl"
|
||||
>
|
||||
<div className="p-2.5 bg-gray-200 text-gray-500 rounded-xl">
|
||||
<Info className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-grow space-y-1">
|
||||
<h3 className="text-sm font-bold text-gray-700">{item.title}</h3>
|
||||
<p className="text-xs text-gray-500 font-medium leading-relaxed max-w-2xl">
|
||||
{item.description}
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="page-title">Notifications</h1>
|
||||
<p className="page-title-desc">
|
||||
Keep track of your interns' progress and system updates.
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-400 font-medium whitespace-nowrap md:absolute md:top-5 md:right-5">
|
||||
{item.time}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={markAllRead}
|
||||
className="flex items-center gap-1.5 self-start sm:self-center px-4 py-2 border border-gray-200 rounded-lg text-xs font-semibold text-gray-700 bg-white hover:bg-gray-50 transition-colors shadow-sm"
|
||||
>
|
||||
<Check className="w-4 h-4 text-green-600 stroke-[3]" />
|
||||
Mark all as read
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{notifications.length === 0 && (
|
||||
<p className="text-sm text-gray-400">Belum ada notifikasi.</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* TODAY */}
|
||||
{todayNotifs.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-[10px] font-bold tracking-widest text-gray-400 uppercase">Today</h2>
|
||||
{todayNotifs.map(n => renderNotifCard(n))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* YESTERDAY */}
|
||||
{yesterdayNotifs.length > 0 && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<h2 className="text-[10px] font-bold tracking-widest text-gray-400 uppercase">Yesterday</h2>
|
||||
{yesterdayNotifs.map(n => renderNotifCard(n, true))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OLDER */}
|
||||
{olderNotifs.length > 0 && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<h2 className="text-[10px] font-bold tracking-widest text-gray-400 uppercase">Older</h2>
|
||||
{olderNotifs.map(n => renderNotifCard(n, true))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { } from '../../../hooks/useProjects';
|
||||
import type { Project } from '../../../types/project.types';
|
||||
|
||||
export default function ParticipantsTab({ project }: { project: Project }) {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { interns, loading, error } = useProjectInterns(slug ?? "");
|
||||
import type { ProjectDetail, ProjectMember } from '../../../types/project.types';
|
||||
|
||||
export default function ParticipantsTab({ project }: { project: ProjectDetail }) {
|
||||
return (
|
||||
<div>
|
||||
{/* Mentor */}
|
||||
@@ -30,44 +25,29 @@ export default function ParticipantsTab({ project }: { project: Project }) {
|
||||
{/* Members */}
|
||||
<div>
|
||||
<h3 className="font-bold text-font-shade mb-3.5">Members</h3>
|
||||
|
||||
{loading && (
|
||||
<div className="flex flex-col gap-3">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="h-10 bg-gray-100 rounded animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
|
||||
{!loading && !error && interns.map((intern, idx) => (
|
||||
{project.members.map((member: ProjectMember, idx) => (
|
||||
<div
|
||||
key={intern.id}
|
||||
key={member.id}
|
||||
className={`flex items-center justify-between py-3 ${
|
||||
idx < interns.length - 1 ? 'border-b border-box-border' : ''
|
||||
idx < project.members.length - 1 ? 'border-b border-box-border' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{intern.avatar ? (
|
||||
<img
|
||||
src={intern.avatar}
|
||||
alt={intern.full_name}
|
||||
className="w-7 h-7 rounded-full object-cover"
|
||||
/>
|
||||
{member.avatar ? (
|
||||
<img src={member.avatar} alt={member.full_name} className="w-7 h-7 rounded-full object-cover" />
|
||||
) : (
|
||||
<div className="w-7 h-7 rounded-full bg-gray-200 flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-[#333]">{intern.full_name}</span>
|
||||
<span className="text-[11px] text-gray-400">{intern.email}</span>
|
||||
<span className="text-sm text-[#333]">{member.full_name}</span>
|
||||
<span className="text-[11px] text-gray-400">{member.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[13px] text-[#555] font-semibold">{intern.kelompok_peminatan}</span>
|
||||
<span className="text-[13px] text-[#555] font-semibold">{member.kelompok_peminatan}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!loading && !error && interns.length === 0 && (
|
||||
{project.members.length === 0 && (
|
||||
<p className="text-sm text-gray-400">Belum ada members.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
// import { useProjectDetail } from '../../../hooks/useProjectDetail';
|
||||
import { useProjectDetail } from '../../../hooks/useProjects';
|
||||
import ForumTab from './ForumTab';
|
||||
import ParticipantsTab from './ParticipantTab';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useMyTasks } from '../../../../hooks/useProjects';
|
||||
//SEMENTARA USERSERVICES BELUM KUBUAT
|
||||
import api from '../../../../lib/api';
|
||||
|
||||
const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
|
||||
const MONTH_NAMES = [
|
||||
@@ -16,19 +18,29 @@ function getFirstDayOfMonth(year: number, month: number) {
|
||||
return new Date(year, month, 1).getDay()
|
||||
}
|
||||
|
||||
const HomeCalendar = () => {
|
||||
interface roleUser {
|
||||
role: "mentor" | "intern" | "admin";
|
||||
}
|
||||
|
||||
export default function HomeCalendar() {
|
||||
const [today, setToday] = useState(new Date());
|
||||
const [role, setRole] = useState<"mentor" | "intern" | "admin" | null>(null);
|
||||
const { tasks } = useMyTasks();
|
||||
|
||||
const DEADLINES = tasks.map(task => ({
|
||||
date: new Date(task.deadline_at),
|
||||
label: task.title,
|
||||
}));
|
||||
useEffect(() => {
|
||||
api.get("/auth-api/me")
|
||||
.then((res) => setRole(res.data.data.role))
|
||||
.catch((err) => console.error("Failed to fetch user:", err));
|
||||
}, []);
|
||||
|
||||
const DEADLINES = role === "intern"
|
||||
? tasks.map(task => ({ date: new Date(task.deadline_at), label: task.title, }))
|
||||
: [];
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setToday(new Date())
|
||||
}, 60 * 1000) // update tiap 1 menit
|
||||
}, 60 * 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
@@ -188,5 +200,3 @@ const HomeCalendar = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeCalendar
|
||||
@@ -14,7 +14,7 @@ const ProjectCard = (project: Project) => {
|
||||
const Icon = getDynamicIcon(project.project_icon);
|
||||
|
||||
|
||||
const tasksDone = -1;
|
||||
const tasksDone = 1;
|
||||
const tasksTotal = project.total_tasks;
|
||||
|
||||
return (
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function SidebarIntern({ isOpen, closeSidebar }: SidebarProps) {
|
||||
{/* Brand & Tombol Close untuk Mobile */}
|
||||
<div className="flex items-center justify-between px-2 pb-10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-[10px] bg-red-600 flex items-center justify-center flex-shrink-0"></div>
|
||||
<div className="w-9 h-9 rounded-[10px] bg-red flex items-center justify-center flex-shrink-0"></div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-gray-900 leading-tight">Internify</p>
|
||||
<p className="text-[11px] text-gray-400">LMS Portal</p>
|
||||
|
||||
@@ -47,7 +47,7 @@ export const MentorLayout = () => {
|
||||
/>
|
||||
)}
|
||||
<main className="container">
|
||||
{/* <Breadcrumbs/> */}
|
||||
<Breadcrumbs/>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { mockProjects } from '../../../lib/mockProjects';
|
||||
import MentorProjectCard from './components/MentorProjectCard';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CreateProjectModal from './components/CreateProjectDialog';
|
||||
import { useProjects } from '../../../hooks/useProjects';
|
||||
|
||||
export default function MentorProjectsPage(){
|
||||
|
||||
export default function MentorProjectsPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { projects, loading, error } = useProjects("active");
|
||||
|
||||
const openDialog = () => {
|
||||
setIsModalOpen(true);
|
||||
@@ -15,8 +17,18 @@ export default function MentorProjectsPage(){
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
if (loading) return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="h-16 bg-gray-100 rounded-lg animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error) return <p className="text-danger text-md">{error}</p>;
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="page-title">List Projects</h1>
|
||||
@@ -35,8 +47,8 @@ export default function MentorProjectsPage(){
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-stretch">
|
||||
{mockProjects.map(project => (
|
||||
<MentorProjectCard key={project.id} {...project} tasks={project.tasks} />
|
||||
{projects.map(project => (
|
||||
<MentorProjectCard key={project.id} {...project} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const initialHistoryData = [
|
||||
];
|
||||
|
||||
const MentorCertificatePage = () => {
|
||||
// State Form Management
|
||||
const [selectedIntern, setSelectedIntern] = useState("JonathanKristina@gmail.com");
|
||||
const [selectedProject, setSelectedProject] = useState("Internify Project (Web Development)");
|
||||
const [issueDate, setIssueDate] = useState("2024-10-24");
|
||||
|
||||
@@ -8,7 +8,7 @@ const HomeMentorPage = () => {
|
||||
<HomeHeader />
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<div className="bg-white border border-box-border rounded-xl p-6">
|
||||
<HomeMentorProject />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import type { Project } from '../../../lib/mockProjects';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ProjectTimeline from '../intern/components/ProjectTimeline';
|
||||
import HeroProject from '../intern/components/Hero-project';
|
||||
import MentorTaskCard from './components/MentorTaskCard';
|
||||
import { useProjectTasks } from '../../../hooks/useTasks';
|
||||
import type { Project } from '../../../types/project.types';
|
||||
|
||||
export default function MentorForumTab({ project }: { project: Project }) {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { tasks, loading, error } = useProjectTasks(slug ?? "");
|
||||
return (
|
||||
<div>
|
||||
{/* Hero banner */}
|
||||
<HeroProject title={project.name} description={project.description} />
|
||||
<HeroProject title={project.project_name} description={project.description} />
|
||||
|
||||
{/* Timeline + Task list */}
|
||||
<div className="flex gap-7 items-start">
|
||||
<div className="w-1/5 bg-white rounded-xl p-5 shadow-md shrink-0">
|
||||
<ProjectTimeline tasks={project.tasks} />
|
||||
<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-4/5">
|
||||
{project.tasks.map((task, idx) => (
|
||||
<MentorTaskCard
|
||||
key={idx}
|
||||
task={task}
|
||||
projectName={project.name} />
|
||||
))}
|
||||
<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) => (
|
||||
<MentorTaskCard key={task.id} task={task} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import type { Project, Intern } from '../../../lib/mockProjects';
|
||||
import { Plus } from 'lucide-react';
|
||||
import ManageInternsModal from './components/AddInternsDialog';
|
||||
import type { ProjectDetail, ProjectMember } from '../../../types/project.types';
|
||||
|
||||
export default function InternsTab({ project }: { project: Project }) {
|
||||
export default function InternsTab({ project }: { project: ProjectDetail }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -15,8 +15,17 @@ export default function InternsTab({ project }: { project: Project }) {
|
||||
</h3>
|
||||
<div className="border-b border-gray-300 pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{project.admin.profile_picture ? (
|
||||
<img
|
||||
src={project.admin.profile_picture}
|
||||
alt={project.admin.full_name}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200" />
|
||||
)}
|
||||
<span className=" text-sm text-[#333]">
|
||||
{project.mentor}
|
||||
{project.admin.full_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,19 +46,19 @@ export default function InternsTab({ project }: { project: Project }) {
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{project.interns.map((intern: Intern, idx: number) => (
|
||||
{project.members.map((member: ProjectMember, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center justify-between py-3 ${idx < project.interns.length - 1 ? 'border-b border-gray-300' : ''
|
||||
className={`flex items-center justify-between py-3 ${idx < project.members.length - 1 ? 'border-b border-gray-300' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className=" text-sm text-[#333]">
|
||||
{intern.name}
|
||||
{member.full_name}
|
||||
</span>
|
||||
</div>
|
||||
<span className=" text-[13px] text-[#555] font-semibold">
|
||||
{intern.role}
|
||||
{member.position}
|
||||
</span>
|
||||
<div>
|
||||
<button className='flex items-center gap-1 text-[14px] bg-red-600 text-white p-1 rounded-xl'>
|
||||
@@ -62,6 +71,8 @@ export default function InternsTab({ project }: { project: Project }) {
|
||||
{isModalOpen && <ManageInternsModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
projectId={project.id}
|
||||
initialMembers={project.members}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { mockProjects } from '../../../lib/mockProjects';
|
||||
import { useProjectDetail } from '../../../hooks/useProjects';
|
||||
import MentorForumTab from './ProjectForumTab';
|
||||
import InternsTab from './ProjectsInternTab';
|
||||
import TaskTab from './TaskTab';
|
||||
|
||||
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
export default function ProjectDetailPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const [activeTab, setActiveTab] = useState<'forum' | 'participants' | 'Task'>('forum');
|
||||
const { project, loading, error } = useProjectDetail(slug ?? "");
|
||||
|
||||
const project = mockProjects.find((p) => toSlug(p.name) === slug);
|
||||
if (loading) return <p className="p-10 text-gray-400">Memuat project...</p>;
|
||||
if (error) return <p className="p-10 text-red-500">{error}</p>;
|
||||
if (!project) return <p className="p-10 text-gray-400">Project tidak ditemukan.</p>
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
import type { Project, Task } from '../../../lib/mockData';
|
||||
import { useProjectTasks, useCreateTask, useDeleteTask } from '../../../hooks/useTasks';
|
||||
import type { ProjectDetail } from '../../../types/project.types';
|
||||
import type { ProjectTask } from '../../../types/task.types';
|
||||
import { ClipboardList, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CreateTaskModal from './components/CreateTaskDialog';
|
||||
import EditTaskModal from './components/EditTaskDialog';
|
||||
|
||||
export default function TaskTab({ project }: { project: Project }) {
|
||||
|
||||
export default function TaskTab({ project }: { project: ProjectDetail }) {
|
||||
const { tasks, loading, error, refetch } = useProjectTasks(String(project.id));
|
||||
const { remove } = useDeleteTask();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<ProjectTask | null>(null);
|
||||
|
||||
const handleEditTask = (taskTitle: string) => {
|
||||
console.log('Edit task:', taskTitle);
|
||||
const handleDeleteTask = async (taskId: number) => {
|
||||
const ok = await remove(taskId, String(project.id));
|
||||
if (ok) refetch();
|
||||
};
|
||||
|
||||
const handleDeleteTask = (taskTitle: string) => {
|
||||
console.log('Delete task:', taskTitle);
|
||||
};
|
||||
if (loading) return (
|
||||
<div className="flex flex-col gap-3 p-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="h-12 bg-gray-100 rounded-lg animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
if (error) return <p className="text-red-500 text-sm p-6">{error}</p>;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100 min-h-[400px]">
|
||||
{/* Header Section */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-3xl font-bold text-slate-800 tracking-tight">Tasks</h2>
|
||||
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="bg-[#C0392B] hover:bg-[#A93226] text-white px-6 py-2.5 rounded-full font-bold text-sm transition-colors shadow-sm"
|
||||
@@ -30,39 +38,31 @@ export default function TaskTab({ project }: { project: Project }) {
|
||||
Add Task
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr className="border-gray-200/80 mb-4" />
|
||||
|
||||
{/* Task List Section */}
|
||||
<div className="flex flex-col">
|
||||
{project.tasks && project.tasks.length > 0 ? (
|
||||
project.tasks.map((task: Task, idx: number) => (
|
||||
{tasks.length > 0 ? (
|
||||
tasks.map((task, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center justify-between py-4 group ${idx < project.tasks.length - 1 ? 'border-b border-gray-100' : ''
|
||||
key={task.id}
|
||||
className={`flex items-center justify-between py-4 group ${
|
||||
idx < tasks.length - 1 ? 'border-b border-gray-100' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Task Title & Icon */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<ClipboardList size={22} className="text-slate-600 shrink-0" />
|
||||
<span className="text-base text-slate-700 font-medium truncate">
|
||||
{task.title}
|
||||
</span>
|
||||
<span className="text-base text-slate-700 font-medium truncate">{task.title}</span>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons (Edit & Delete) */}
|
||||
<div className="flex items-center gap-4 text-slate-600 shrink-0">
|
||||
<button
|
||||
onClick={() => setEditModalOpen(true)}
|
||||
onClick={() => { setSelectedTask(task); setEditModalOpen(true); }}
|
||||
className="hover:text-blue-600 transition-colors p-1"
|
||||
title="Edit Task"
|
||||
>
|
||||
<Pencil size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteTask(task.title)}
|
||||
onClick={() => handleDeleteTask(task.id)}
|
||||
className="hover:text-red-600 transition-colors p-1"
|
||||
title="Delete Task"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
@@ -75,18 +75,23 @@ export default function TaskTab({ project }: { project: Project }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isModalOpen &&
|
||||
|
||||
{isModalOpen && (
|
||||
<CreateTaskModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
projectId={String(project.id)}
|
||||
onSuccess={() => { setIsModalOpen(false); refetch(); }}
|
||||
/>
|
||||
}
|
||||
{editModalOpen &&
|
||||
)}
|
||||
{editModalOpen && selectedTask && (
|
||||
<EditTaskModal
|
||||
isOpen={editModalOpen}
|
||||
onClose={() => setEditModalOpen(false)}
|
||||
onClose={() => { setEditModalOpen(false); setSelectedTask(null); }}
|
||||
task={selectedTask}
|
||||
onSuccess={() => { setEditModalOpen(false); setSelectedTask(null); refetch(); }}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,34 @@
|
||||
// ManageInternsModal.tsx
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { X, Search, Plus, Trash2 } from "lucide-react";
|
||||
import { Search, Plus, Trash2 } from "lucide-react";
|
||||
import { useMahasiswa } from "../../../../hooks/useInterns";
|
||||
import { useAssignMember } from '../../../../hooks/useProjects';
|
||||
import type { ProjectMember } from '../../../../types/project.types';
|
||||
import type { Mahasiswa } from '../../../../types/project.types';
|
||||
|
||||
interface ManageInternsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: number;
|
||||
initialMembers: ProjectMember[];
|
||||
}
|
||||
|
||||
export default function ManageInternsModal({ isOpen, onClose }: ManageInternsModalProps) {
|
||||
export default function ManageInternsModal({ isOpen, onClose, projectId, initialMembers }: ManageInternsModalProps) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("Jonathankristin@gmail.com");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [invitedList, setInvitedList] = useState<ProjectMember[]>(initialMembers);
|
||||
|
||||
const [invitedList, setInvitedList] = useState([
|
||||
{ id: 1, email: "Jonathankristina@gmail.com", initials: "JK" }
|
||||
]);
|
||||
const { mahasiswa, loading: loadingMahasiswa } = useMahasiswa();
|
||||
const { assign, loading: assigning } = useAssignMember();
|
||||
|
||||
// Data Rekomendasi/Hasil Pencarian (Simulasi list bawah yang ada ikon plus)
|
||||
const [searchResult, setSearchResult] = useState([
|
||||
{ id: 2, email: "Jonathankristin@gmail.com", initials: "JK" }
|
||||
]);
|
||||
// Sync saat modal dibuka
|
||||
useEffect(() => {
|
||||
if (isOpen) setInvitedList(initialMembers);
|
||||
}, [isOpen, initialMembers]);
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
|
||||
if (isOpen) {
|
||||
dialog.showModal();
|
||||
document.body.style.overflow = "hidden";
|
||||
@@ -30,12 +36,7 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
|
||||
dialog.close();
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
|
||||
const handleCancel = (e: Event) => {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCancel = (e: Event) => { e.preventDefault(); onClose(); };
|
||||
dialog.addEventListener("cancel", handleCancel);
|
||||
return () => {
|
||||
dialog.removeEventListener("cancel", handleCancel);
|
||||
@@ -43,38 +44,66 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Filter mahasiswa yang belum jadi member & sesuai search
|
||||
const invitedIds = new Set(invitedList.map(m => m.id));
|
||||
const searchResult = mahasiswa.filter(m =>
|
||||
!invitedIds.has(m.id) &&
|
||||
(m.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
`${m.nama_depan} ${m.nama_belakang}`.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
const handleAssign = async (intern: Mahasiswa) => {
|
||||
const result = await assign({ id_project: projectId, id_user: intern.id });
|
||||
if (result) {
|
||||
setInvitedList(prev => [...prev, {
|
||||
id: intern.id,
|
||||
id_user: intern.id,
|
||||
full_name: `${intern.nama_depan} ${intern.nama_belakang}`,
|
||||
email: intern.email,
|
||||
avatar: "",
|
||||
kelompok_peminatan: intern.jurusan,
|
||||
professional_bio: "",
|
||||
position: intern.role,
|
||||
} as ProjectMember]);
|
||||
}
|
||||
};
|
||||
|
||||
const getInitials = (m: Mahasiswa) =>
|
||||
`${m.nama_depan[0]}${m.nama_belakang[0]}`.toUpperCase();
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className="custom-dialog"
|
||||
>
|
||||
<dialog ref={dialogRef} className="custom-dialog">
|
||||
<div className="px-3 py-5">
|
||||
{/* Search Input Area */}
|
||||
<div className="relative flex items-center mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search by email..."
|
||||
placeholder="Cari nama atau email..."
|
||||
className="w-full pl-4 pr-10 py-3 bg-white border border-gray-200 rounded-xl text-sm placeholder-gray-400 text-gray-700 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium shadow-sm"
|
||||
/>
|
||||
<Search className="absolute right-3.5 w-4 h-4 text-gray-400" />
|
||||
</div>
|
||||
|
||||
{/* List Container (Scrollable jika data banyak) */}
|
||||
<div className="overflow-y-auto space-y-3 pr-1 flex-1">
|
||||
|
||||
{/* List 1: Intern Yang Sudah Tergabung / Ditambahkan */}
|
||||
{invitedList.map((intern) => (
|
||||
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
|
||||
{/* Sudah jadi member */}
|
||||
{invitedList.map((member) => (
|
||||
<div key={member.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
{member.avatar ? (
|
||||
<img src={member.avatar} alt={member.full_name} className="w-9 h-9 rounded-full object-cover" />
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded-full bg-gray-200 border border-gray-300 flex items-center justify-center text-xs font-bold text-gray-500">
|
||||
{intern.initials}
|
||||
{member.full_name.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-gray-700">{member.full_name}</span>
|
||||
<span className="text-xs text-gray-400">{member.email}</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setInvitedList(invitedList.filter(i => i.id !== intern.id))}
|
||||
onClick={() => setInvitedList(prev => prev.filter(i => i.id !== member.id))}
|
||||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -82,27 +111,35 @@ export default function ManageInternsModal({ isOpen, onClose }: ManageInternsMod
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* List 2: Hasil Pencarian / Belum Ditambahkan */}
|
||||
{searchResult.map((intern) => (
|
||||
{/* Hasil pencarian — belum jadi member */}
|
||||
{loadingMahasiswa ? (
|
||||
<p className="text-sm text-gray-400 text-center py-2">Memuat...</p>
|
||||
) : (
|
||||
searchResult.map((intern) => (
|
||||
<div key={intern.id} className="flex items-center justify-between p-2 rounded-xl hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-gray-200 border border-gray-300 flex items-center justify-center text-xs font-bold text-gray-500">
|
||||
{intern.initials}
|
||||
{getInitials(intern)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-gray-700">{intern.nama_depan} {intern.nama_belakang}</span>
|
||||
<span className="text-xs text-gray-400">{intern.email}</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-600">{intern.email}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setInvitedList([...invitedList, intern]);
|
||||
setSearchResult(searchResult.filter(i => i.id !== intern.id));
|
||||
}}
|
||||
className="p-2 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||
onClick={() => handleAssign(intern)}
|
||||
disabled={assigning}
|
||||
className="p-2 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
|
||||
{!loadingMahasiswa && searchQuery && searchResult.length === 0 && (
|
||||
<p className="text-sm text-gray-400 text-center py-2">Tidak ada mahasiswa ditemukan.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1,187 +1,112 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { X, FileText, Link } from "lucide-react";
|
||||
import { useCreateTask } from '../../../../hooks/useTasks';
|
||||
import type { SubmissionType } from '../../../../types/task.types';
|
||||
|
||||
interface CreateTaskModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: string;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function CreateTaskModal({ isOpen, onClose }: CreateTaskModalProps) {
|
||||
export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess }: CreateTaskModalProps) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const { create, loading } = useCreateTask(projectId);
|
||||
|
||||
// Form States
|
||||
const [taskTitle, setTaskTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [deadlineDate, setDeadlineDate] = useState("");
|
||||
const [specificTime, setSpecificTime] = useState("");
|
||||
const [submissionType, setSubmissionType] = useState<"file" | "url">("file");
|
||||
const [submissionType, setSubmissionType] = useState<SubmissionType>("file_upload");
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
|
||||
if (isOpen) {
|
||||
dialog.showModal();
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
dialog.close();
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
|
||||
const handleCancel = (e: Event) => {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (isOpen) { dialog.showModal(); document.body.style.overflow = "hidden"; }
|
||||
else { dialog.close(); document.body.style.overflow = "unset"; }
|
||||
const handleCancel = (e: Event) => { e.preventDefault(); onClose(); };
|
||||
dialog.addEventListener("cancel", handleCancel);
|
||||
return () => {
|
||||
dialog.removeEventListener("cancel", handleCancel);
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
return () => { dialog.removeEventListener("cancel", handleCancel); document.body.style.overflow = "unset"; };
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const payload = { taskTitle, description, deadlineDate, specificTime, submissionType };
|
||||
console.log("Saving Task:", payload);
|
||||
alert("Task Saved Successfully!");
|
||||
onClose();
|
||||
const result = await create({
|
||||
title: taskTitle,
|
||||
description,
|
||||
deadline_date: deadlineDate,
|
||||
specific_time: specificTime,
|
||||
submission_type: submissionType,
|
||||
});
|
||||
if (result) onSuccess();
|
||||
};
|
||||
|
||||
// JSX sama persis, hanya ganti handleSubmit dan tambah disabled pada button submit:
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className="custom-dialog p-[1.5rem]"
|
||||
>
|
||||
{/* Header Modal */}
|
||||
<dialog ref={dialogRef} className="custom-dialog p-[1.5rem]">
|
||||
<div className="dialog-header">
|
||||
<div>
|
||||
<h2 className="text-[26px] font-bold text-[#B30000] tracking-tight">Create New Task</h2>
|
||||
<p className="text-xs text-gray-400 font-medium mt-0.5">
|
||||
Fill in the details for the next intern assignment.
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 font-medium mt-0.5">Fill in the details for the next intern assignment.</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors">
|
||||
<X className="w-5 h-5 stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body Form Modal (Scrollable jika layar pendek) */}
|
||||
<form onSubmit={handleSubmit} className="px-6 space-y-4 overflow-y-auto flex-1">
|
||||
|
||||
{/* Task Title */}
|
||||
<div className="">
|
||||
<div>
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g., Design High-Fidelity Prototypes"
|
||||
value={taskTitle}
|
||||
<input type="text" required placeholder="e.g., Design High-Fidelity Prototypes" value={taskTitle}
|
||||
onChange={(e) => setTaskTitle(e.target.value)}
|
||||
className="w-full px-3 py-4 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||
/>
|
||||
className="w-full px-3 py-4 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Description</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder="Outline the requirements and objectives..."
|
||||
value={description}
|
||||
<textarea rows={4} placeholder="Outline the requirements and objectives..." value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed"
|
||||
/>
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed" />
|
||||
</div>
|
||||
|
||||
{/* Deadline Grid (Date & Time) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Deadline Date</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
value={deadlineDate}
|
||||
onChange={(e) => setDeadlineDate(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||
/>
|
||||
<input type="date" required value={deadlineDate} onChange={(e) => setDeadlineDate(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time (Jam)</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="time"
|
||||
required
|
||||
value={specificTime}
|
||||
onChange={(e) => setSpecificTime(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||
/>
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time</label>
|
||||
<input type="time" required value={specificTime} onChange={(e) => setSpecificTime(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submission Type Card Radio */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Submission Type</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
|
||||
{/* Opsi 1: File Upload */}
|
||||
<div
|
||||
onClick={() => setSubmissionType("file")}
|
||||
{(["file_upload", "url_link"] as SubmissionType[]).map((type) => (
|
||||
<div key={type} onClick={() => setSubmissionType(type)}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
|
||||
submissionType === "file"
|
||||
? "border-[#B30000] bg-[#B30000] text-white font-semibold"
|
||||
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === "file" ? "border-white bg-white" : "border-gray-300"}`}>
|
||||
{submissionType === "file" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||
submissionType === type ? "border-[#B30000] bg-[#B30000] text-white font-semibold" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}>
|
||||
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === type ? "border-white bg-white" : "border-gray-300"}`}>
|
||||
{submissionType === type && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||
</div>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span className="text-xs">File Upload</span>
|
||||
{type === "file_upload" ? <FileText className="w-4 h-4" /> : <Link className="w-4 h-4" />}
|
||||
<span className="text-xs">{type === "file_upload" ? "File Upload" : "URL Link"}</span>
|
||||
</div>
|
||||
|
||||
{/* Opsi 2: URL Link */}
|
||||
<div
|
||||
onClick={() => setSubmissionType("url")}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
|
||||
submissionType === "url"
|
||||
? "border-[#B30000] bg-[#B30000] text-white font-semibold"
|
||||
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === "url" ? "border-white bg-white" : "border-gray-300"}`}>
|
||||
{submissionType === "url" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||
</div>
|
||||
<Link className="w-4 h-4" />
|
||||
<span className="text-xs">URL Link</span>
|
||||
</div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-gray-100 my-2" />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-3 border border-gray-200 rounded-xl text-xs font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<button type="button" onClick={onClose}
|
||||
className="p-3 border border-gray-200 rounded-xl text-xs font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="p-3 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md"
|
||||
>
|
||||
Save Task
|
||||
<button type="submit" disabled={loading}
|
||||
className="p-3 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md disabled:opacity-50">
|
||||
{loading ? "Saving..." : "Save Task"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</dialog>
|
||||
);
|
||||
|
||||
@@ -1,187 +1,118 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { X, FileText, Link } from "lucide-react";
|
||||
import { useUpdateTask } from '../../../../hooks/useTasks';
|
||||
import type { ProjectTask, SubmissionType } from '../../../../types/task.types';
|
||||
|
||||
interface EditTaskModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
task: ProjectTask;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function EditTaskModal({ isOpen, onClose }: EditTaskModalProps) {
|
||||
export default function EditTaskModal({ isOpen, onClose, task, onSuccess }: EditTaskModalProps) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const { update, loading } = useUpdateTask();
|
||||
|
||||
// Form States
|
||||
const [taskTitle, setTaskTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [deadlineDate, setDeadlineDate] = useState("");
|
||||
const [specificTime, setSpecificTime] = useState("");
|
||||
const [submissionType, setSubmissionType] = useState<"file" | "url">("file");
|
||||
const [taskTitle, setTaskTitle] = useState(task.title);
|
||||
const [description, setDescription] = useState(task.description);
|
||||
const [deadlineDate, setDeadlineDate] = useState(task.deadline_at.split("T")[0]);
|
||||
const [specificTime, setSpecificTime] = useState(task.deadline_at.split("T")[1]?.slice(0, 5) ?? "");
|
||||
const [submissionType, setSubmissionType] = useState<SubmissionType>(task.submission_type);
|
||||
|
||||
// Reset tiap task berubah
|
||||
useEffect(() => {
|
||||
setTaskTitle(task.title);
|
||||
setDescription(task.description);
|
||||
setDeadlineDate(task.deadline_at.split("T")[0]);
|
||||
setSpecificTime(task.deadline_at.split("T")[1]?.slice(0, 5) ?? "");
|
||||
setSubmissionType(task.submission_type);
|
||||
}, [task]);
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
|
||||
if (isOpen) {
|
||||
dialog.showModal();
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
dialog.close();
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
|
||||
const handleCancel = (e: Event) => {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (isOpen) { dialog.showModal(); document.body.style.overflow = "hidden"; }
|
||||
else { dialog.close(); document.body.style.overflow = "unset"; }
|
||||
const handleCancel = (e: Event) => { e.preventDefault(); onClose(); };
|
||||
dialog.addEventListener("cancel", handleCancel);
|
||||
return () => {
|
||||
dialog.removeEventListener("cancel", handleCancel);
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
return () => { dialog.removeEventListener("cancel", handleCancel); document.body.style.overflow = "unset"; };
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const payload = { taskTitle, description, deadlineDate, specificTime, submissionType };
|
||||
console.log("Saving Task:", payload);
|
||||
alert("Task Saved Successfully!");
|
||||
onClose();
|
||||
const result = await update(task.id, {
|
||||
title: taskTitle,
|
||||
description,
|
||||
deadline_date: deadlineDate,
|
||||
specific_time: specificTime,
|
||||
submission_type: submissionType,
|
||||
}, String(task.id_project));
|
||||
if (result) onSuccess();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className="custom-dialog p-[1.5rem]"
|
||||
>
|
||||
{/* Header Modal */}
|
||||
<dialog ref={dialogRef} className="custom-dialog p-[1.5rem]">
|
||||
<div className="dialog-header">
|
||||
<div>
|
||||
<h2 className="text-[26px] font-bold text-[#B30000] tracking-tight">Edut Task</h2>
|
||||
<p className="text-xs text-gray-400 font-medium mt-0.5">
|
||||
Fill in the details for the next intern assignment.
|
||||
</p>
|
||||
<h2 className="text-[26px] font-bold text-[#B30000] tracking-tight">Edit Task</h2>
|
||||
<p className="text-xs text-gray-400 font-medium mt-0.5">Update the details for this assignment.</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors">
|
||||
<X className="w-5 h-5 stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body Form Modal (Scrollable jika layar pendek) */}
|
||||
<form onSubmit={handleSubmit} className="px-6 space-y-4 overflow-y-auto flex-1">
|
||||
|
||||
{/* Task Title */}
|
||||
<div className="">
|
||||
<div>
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Task Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g., Design High-Fidelity Prototypes"
|
||||
value={taskTitle}
|
||||
onChange={(e) => setTaskTitle(e.target.value)}
|
||||
className="w-full px-3 py-4 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||
/>
|
||||
<input type="text" required value={taskTitle} onChange={(e) => setTaskTitle(e.target.value)}
|
||||
className="w-full px-3 py-4 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Description</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder="Outline the requirements and objectives..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed"
|
||||
/>
|
||||
<textarea rows={4} value={description} onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000] resize-none leading-relaxed" />
|
||||
</div>
|
||||
|
||||
{/* Deadline Grid (Date & Time) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Deadline Date</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
value={deadlineDate}
|
||||
onChange={(e) => setDeadlineDate(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||
/>
|
||||
<input type="date" required value={deadlineDate} onChange={(e) => setDeadlineDate(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time (Jam)</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="time"
|
||||
required
|
||||
value={specificTime}
|
||||
onChange={(e) => setSpecificTime(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]"
|
||||
/>
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Specific Time</label>
|
||||
<input type="time" required value={specificTime} onChange={(e) => setSpecificTime(e.target.value)}
|
||||
className="w-full pl-3 pr-9 py-3 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-800 focus:outline-none focus:border-[#B30000] focus:ring-1 focus:ring-[#B30000]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submission Type Card Radio */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-700 tracking-wide">Submission Type</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
|
||||
{/* Opsi 1: File Upload */}
|
||||
<div
|
||||
onClick={() => setSubmissionType("file")}
|
||||
{(["file_upload", "url_link"] as SubmissionType[]).map((type) => (
|
||||
<div key={type} onClick={() => setSubmissionType(type)}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
|
||||
submissionType === "file"
|
||||
? "border-[#B30000] bg-[#B30000] text-white font-semibold"
|
||||
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === "file" ? "border-white bg-white" : "border-gray-300"}`}>
|
||||
{submissionType === "file" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||
submissionType === type ? "border-[#B30000] bg-[#B30000] text-white font-semibold" : "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}>
|
||||
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === type ? "border-white bg-white" : "border-gray-300"}`}>
|
||||
{submissionType === type && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||
</div>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span className="text-xs">File Upload</span>
|
||||
{type === "file_upload" ? <FileText className="w-4 h-4" /> : <Link className="w-4 h-4" />}
|
||||
<span className="text-xs">{type === "file_upload" ? "File Upload" : "URL Link"}</span>
|
||||
</div>
|
||||
|
||||
{/* Opsi 2: URL Link */}
|
||||
<div
|
||||
onClick={() => setSubmissionType("url")}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl border cursor-pointer transition-all ${
|
||||
submissionType === "url"
|
||||
? "border-[#B30000] bg-[#B30000] text-white font-semibold"
|
||||
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className={`w-3.5 h-3.5 rounded-full border flex items-center justify-center ${submissionType === "url" ? "border-white bg-white" : "border-gray-300"}`}>
|
||||
{submissionType === "url" && <div className="w-1.5 h-1.5 bg-[#B30000] rounded-full" />}
|
||||
</div>
|
||||
<Link className="w-4 h-4" />
|
||||
<span className="text-xs">URL Link</span>
|
||||
</div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-gray-100 my-2" />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-3 border border-gray-200 rounded-xl text-xs font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<button type="button" onClick={onClose}
|
||||
className="p-3 border border-gray-200 rounded-xl text-xs font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="p-3 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md"
|
||||
>
|
||||
Save Task
|
||||
<button type="submit" disabled={loading}
|
||||
className="p-3 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-xl transition-colors shadow-md disabled:opacity-50">
|
||||
{loading ? "Saving..." : "Save Task"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</dialog>
|
||||
);
|
||||
|
||||
@@ -3,10 +3,12 @@ import { mockProjects } from '../../../../lib/mockData'
|
||||
import { useState } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import CreateProjectModal from './CreateProjectDialog';
|
||||
import { useProjects } from '../../../../hooks/useProjects';
|
||||
|
||||
const HomeMentorProject = () => {
|
||||
const navigate = useNavigate();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { projects, loading, error } = useProjects();
|
||||
const openDialog = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
@@ -26,22 +28,22 @@ const HomeMentorProject = () => {
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{mockProjects.map((project, index) => {
|
||||
return (
|
||||
<div className="flex flex-col max-h-[210px] overflow-y-auto gap-3 pr-1">
|
||||
{loading && <p className="text-sm text-gray-400 px-5">Memuat...</p>}
|
||||
{error && <p className="text-sm text-red-500 px-5">{error}</p>}
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => navigate(`/intern/projects/${project.name}`)}
|
||||
className="flex items-center gap-4 border border-gray-200 rounded-lg px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors duration-150"
|
||||
key={project.id}
|
||||
onClick={() => navigate(`/mentor/projects/${project.slug}`)}
|
||||
className="flex items-center gap-4 border border-box-border rounded-lg px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors duration-150"
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-gray-300`}>
|
||||
<div className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-gray-300">
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-800">{project.name}</p>
|
||||
<p className="text-sm font-semibold text-gray-800">{project.project_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
{isModalOpen && (
|
||||
<CreateProjectModal
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
import { Trash2, UsersRound } from 'lucide-react'
|
||||
import type { Intern, Task } from '../../../../lib/mockProjects'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import type { LucideProps } from 'lucide-react';
|
||||
import type { Project } from '../../../../types/project.types';
|
||||
|
||||
interface ProjectCardProps {
|
||||
id: number | string
|
||||
name: string
|
||||
interns: Intern[]
|
||||
tasks: Task[]
|
||||
thumbnailUrl?: string
|
||||
}
|
||||
const getDynamicIcon = (iconName: string) => {
|
||||
const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1);
|
||||
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<LucideProps>>)[formatted];
|
||||
return Icon ?? LucideIcons.FolderOpen;
|
||||
};
|
||||
|
||||
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
export default function MentorProjectCard({
|
||||
id,
|
||||
name,
|
||||
interns,
|
||||
thumbnailUrl
|
||||
}: ProjectCardProps) {
|
||||
export default function MentorProjectCard(project: Project) {
|
||||
const navigate = useNavigate();
|
||||
const handleCardClick = () => {
|
||||
navigate(`/mentor/projects/${toSlug(name)}`)
|
||||
navigate(`/mentor/projects/${toSlug(project.slug)}`)
|
||||
};
|
||||
const Icon = getDynamicIcon(project.project_icon);
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
console.log('Delete project dengan id:', id)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onClick={handleCardClick}
|
||||
className="flex flex-col bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 w-[180px] min-h-[270px]hover:bg-gray-50 transition-colors">
|
||||
{/* Thumbnail */}
|
||||
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden shrink-0">
|
||||
{thumbnailUrl && (
|
||||
<img src={thumbnailUrl} alt={name} className="w-full h-full object-cover" />
|
||||
)}
|
||||
<div className="h-[150px] bg-gradient-to-tr from-red-700 to-red-800 overflow-hidden shrink-0 flex items-center justify-center">
|
||||
<Icon className="w-12 h-12 text-white opacity-80" />
|
||||
</div>
|
||||
|
||||
{/* Content*/}
|
||||
<div className="p-3 flex flex-col gap-2 flex-1 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-semibold text-gray-800 leading-tight line-clamp-2">
|
||||
{name}
|
||||
{project.project_name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +42,7 @@ export default function MentorProjectCard({
|
||||
<div className="flex items-center gap-2 mt-auto">
|
||||
<UsersRound className='text-gray-400 w-4 h-4' />
|
||||
<span className="text-xs text-gray-500 truncate">
|
||||
{interns.length}/{interns.length}
|
||||
{project.total_members}/{project.total_members}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import type { Task } from "../../../../lib/mockProjects";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { ProjectTask } from "../../../../types/task.types";
|
||||
|
||||
const formatDate = (date: Date) =>
|
||||
const formatDate = (date: string) =>
|
||||
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
|
||||
const formatTime = (dateStr: string) =>
|
||||
new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
interface MentorTaskCardProps {
|
||||
task: Task;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export default function MentorTaskCard({ task, projectName }: MentorTaskCardProps) {
|
||||
export default function MentorTaskCard({ task }: {task: ProjectTask}) {
|
||||
const navigate = useNavigate();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
|
||||
const handleCardClick = () => {
|
||||
// Pastikan jika projectName belum ter-render, proses navigasi tidak bikin crash
|
||||
if (!projectName || !task?.title) return;
|
||||
|
||||
const projectSlug = toSlug(projectName);
|
||||
const taskSlug = toSlug(task.title);
|
||||
|
||||
// Rute mutlak menuju target kamu: /mentor/projects/:slug/:taskSlug
|
||||
navigate(`/mentor/projects/${projectSlug}/${taskSlug}`);
|
||||
navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`)
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -32,7 +25,7 @@ export default function MentorTaskCard({ task, projectName }: MentorTaskCardProp
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[11px] text-[#888] mb-1">
|
||||
Deadline : {task.deadline ? `${formatDate(task.deadline.date)} at ${task.deadline.time}` : '-'}
|
||||
Deadline : {task.deadline_at ? `${formatDate(task.deadline_at)} at ${formatTime(task.deadline_at)}` : '-'}
|
||||
</div>
|
||||
<div className="font-bold text-[15px] text-[#1a1a1a] mb-1">
|
||||
{task.title}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, ChevronRight, X } from 'lucide-react';
|
||||
import { Home, FolderKanban, Award, Settings, HelpCircle, LogOut, X } from 'lucide-react';
|
||||
|
||||
const navItems = [
|
||||
// belum kutambahin
|
||||
@@ -39,7 +39,7 @@ export default function SidebarMentor({ isOpen, closeSidebar }: SidebarProps) {
|
||||
return (
|
||||
<aside
|
||||
className={`
|
||||
bg-white border-r border-gray-300 flex flex-col py-5 px-3 gap-1 overflow-y-auto flex-shrink-0 transition-transform duration-300 ease-in-out
|
||||
bg-white border-r border-gray-300 flex flex-col py-5 px-1 gap-1 overflow-y-auto flex-shrink-0 transition-transform duration-300 ease-in-out
|
||||
md:sticky md:top-[50px] md:h-[calc(100vh-73px)] md:w-[250px] md:translate-x-0 md:z-8
|
||||
fixed top-0 left-0 h-[calc(100vh-50px)] w-[270px] z-40 pt-5
|
||||
${isOpen ? "translate-x-0" : "-translate-x-full"}
|
||||
@@ -71,10 +71,10 @@ export default function SidebarMentor({ isOpen, closeSidebar }: SidebarProps) {
|
||||
<button
|
||||
key={path}
|
||||
onClick={() => 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
|
||||
className={`flex items-center gap-2.5 w-full px-3 py-2 text-[13.5px] rounded-lg font-medium text-left transition-colors
|
||||
${isActive
|
||||
? "bg-base-foreground text-base font-semibold border-l-4 border-base"
|
||||
: "text-font hover:bg-base-foreground hover:text-base"
|
||||
? "bg-red-foreground text-red font-semibold border-l-6 border-red"
|
||||
: "text-font hover:bg-red-foreground"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
||||
@@ -99,10 +99,10 @@ export default function SidebarMentor({ 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"
|
||||
? "bg-red-50 text-red-600 font-semibold border-l-6 border-red "
|
||||
: danger
|
||||
? "text-font hover:bg-base-foreground hover:text-base"
|
||||
: "text-font hover:bg-base-foreground hover:text-base"
|
||||
? "text-font hover:bg-base-foreground"
|
||||
: "text-font hover:bg-red-foreground"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-[18px] h-[18px] flex-shrink-0" />
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// hooks/useMahasiswa.ts
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Mahasiswa } from '../types/project.types';
|
||||
import { getMahasiswa } from '../services/MahasiswaService';
|
||||
|
||||
export const useMahasiswa = () => {
|
||||
const [mahasiswa, setMahasiswa] = useState<Mahasiswa[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getMahasiswa()
|
||||
.then(setMahasiswa)
|
||||
.catch(() => setError('Gagal memuat data mahasiswa.'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return { mahasiswa, loading, error };
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
// hooks/useNotifications.ts
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { Notification } from "../types/certificate.types";
|
||||
import {
|
||||
getNotifications,
|
||||
markAllNotificationsRead,
|
||||
markNotificationRead,
|
||||
} from "../services/NotificationService";
|
||||
|
||||
export const useNotifications = () => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setLoading(true);
|
||||
getNotifications()
|
||||
.then(setNotifications)
|
||||
.catch(() => setError("Gagal memuat notifikasi."))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => { refetch(); }, [refetch]);
|
||||
|
||||
const markAllRead = async () => {
|
||||
try {
|
||||
await markAllNotificationsRead();
|
||||
setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
|
||||
} catch { setError("Gagal menandai semua notifikasi."); }
|
||||
};
|
||||
|
||||
const markOneRead = async (id: number) => {
|
||||
try {
|
||||
await markNotificationRead(id);
|
||||
setNotifications(prev =>
|
||||
prev.map(n => n.id === id ? { ...n, is_read: true } : n)
|
||||
);
|
||||
} catch { setError("Gagal menandai notifikasi."); }
|
||||
};
|
||||
|
||||
const unreadCount = notifications.filter(n => !n.is_read).length;
|
||||
|
||||
return { notifications, unreadCount, loading, error, refetch, markAllRead, markOneRead };
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
// hooks/useCertificates.ts
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { Certificate, CertificateVerifyResult } from "../types/certificate.types";
|
||||
import {
|
||||
claimCertificate,
|
||||
getMyCertificates,
|
||||
getAllCertificates,
|
||||
getCertificateDetail,
|
||||
getCertificatesByProject,
|
||||
verifyCertificate,
|
||||
verifyCertificateByUuid,
|
||||
uploadCertificateTemplate,
|
||||
} from "../services/CertificateService";
|
||||
|
||||
// GET /certificate-api/my-certificates
|
||||
export const useMyCertificates = () => {
|
||||
const [certificates, setCertificates] = useState<Certificate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getMyCertificates()
|
||||
.then(setCertificates)
|
||||
.catch(() => setError("Gagal memuat sertifikat."))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return { certificates, loading, error };
|
||||
};
|
||||
|
||||
// GET /certificate-api/all
|
||||
export const useAllCertificates = () => {
|
||||
const [certificates, setCertificates] = useState<Certificate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getAllCertificates()
|
||||
.then(setCertificates)
|
||||
.catch(() => setError("Gagal memuat semua sertifikat."))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return { certificates, loading, error };
|
||||
};
|
||||
|
||||
// GET /certificate-api/detail/{id}
|
||||
export const useCertificateDetail = (id: number) => {
|
||||
const [certificate, setCertificate] = useState<Certificate | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
getCertificateDetail(id)
|
||||
.then(setCertificate)
|
||||
.catch(() => setError("Gagal memuat detail sertifikat."))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
return { certificate, loading, error };
|
||||
};
|
||||
|
||||
// GET /certificate-api/project/{id_project}
|
||||
export const useProjectCertificates = (id_project: number) => {
|
||||
const [certificates, setCertificates] = useState<Certificate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id_project) return;
|
||||
getCertificatesByProject(id_project)
|
||||
.then(setCertificates)
|
||||
.catch(() => setError("Gagal memuat sertifikat project."))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id_project]);
|
||||
|
||||
return { certificates, loading, error };
|
||||
};
|
||||
|
||||
// GET /certificate-api/verify & verify-uuid (public, on-demand)
|
||||
export const useVerifyCertificate = () => {
|
||||
const [result, setResult] = useState<CertificateVerifyResult | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const verifyByNo = useCallback(async (certificate_no: string) => {
|
||||
setLoading(true); setError(null);
|
||||
try {
|
||||
const res = await verifyCertificate(certificate_no);
|
||||
setResult(res);
|
||||
return res;
|
||||
} catch { setError("Sertifikat tidak valid atau tidak ditemukan."); return null; }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
const verifyByUuid = useCallback(async (uuid: string) => {
|
||||
setLoading(true); setError(null);
|
||||
try {
|
||||
const res = await verifyCertificateByUuid(uuid);
|
||||
setResult(res);
|
||||
return res;
|
||||
} catch { setError("Sertifikat tidak valid atau tidak ditemukan."); return null; }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
return { result, loading, error, verifyByNo, verifyByUuid };
|
||||
};
|
||||
|
||||
// POST /certificate-api/claim
|
||||
export const useClaimCertificate = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const claim = async (id_project: number) => {
|
||||
setLoading(true); setError(null);
|
||||
try { return await claimCertificate(id_project); }
|
||||
catch { setError("Gagal claim sertifikat."); return null; }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return { claim, loading, error };
|
||||
};
|
||||
|
||||
// POST /certificate-api/projects/{id_project}/template
|
||||
export const useUploadCertificateTemplate = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const upload = async (id_project: number, file: File) => {
|
||||
setLoading(true); setError(null);
|
||||
try { return await uploadCertificateTemplate(id_project, file); }
|
||||
catch { setError("Gagal upload template."); return null; }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return { upload, loading, error };
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export const useProjectTasks = (projectId: string) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const refetch = useCallback(() => {
|
||||
if (!projectId) return;
|
||||
setLoading(true);
|
||||
getProjectTasks(projectId)
|
||||
@@ -29,7 +29,9 @@ export const useProjectTasks = (projectId: string) => {
|
||||
.finally(() => setLoading(false));
|
||||
}, [projectId]);
|
||||
|
||||
return { tasks, loading, error };
|
||||
useEffect(() => { refetch(); }, [refetch]);
|
||||
|
||||
return { tasks, loading, error, refetch }; // tambah refetch
|
||||
};
|
||||
|
||||
// GET /task-api/tasks/{id}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// services/CertificateService.ts
|
||||
import api from "../lib/api";
|
||||
import type {
|
||||
Certificate,
|
||||
CertificateVerifyResult,
|
||||
CertificateTemplate,
|
||||
} from "../types/certificate.types";
|
||||
|
||||
// POST /certificate-api/claim
|
||||
export const claimCertificate = async (id_project: number): Promise<Certificate> => {
|
||||
const res = await api.post("/certificate-api/claim", { id_project });
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// GET /certificate-api/my-certificates
|
||||
export const getMyCertificates = async (): Promise<Certificate[]> => {
|
||||
const res = await api.get("/certificate-api/my-certificates");
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// GET /certificate-api/all (admin only)
|
||||
export const getAllCertificates = async (): Promise<Certificate[]> => {
|
||||
const res = await api.get("/certificate-api/all");
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// GET /certificate-api/detail/{id}
|
||||
export const getCertificateDetail = async (id: number): Promise<Certificate> => {
|
||||
const res = await api.get(`/certificate-api/detail/${id}`);
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// GET /certificate-api/project/{id_project} (admin only)
|
||||
export const getCertificatesByProject = async (id_project: number): Promise<Certificate[]> => {
|
||||
const res = await api.get(`/certificate-api/project/${id_project}`);
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// GET /certificate-api/verify?certificate_no=... (public)
|
||||
export const verifyCertificate = async (certificate_no: string): Promise<CertificateVerifyResult> => {
|
||||
const res = await api.get("/certificate-api/verify", {
|
||||
params: { certificate_no },
|
||||
});
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// GET /certificate-api/verify-uuid/{uuid} (public)
|
||||
export const verifyCertificateByUuid = async (uuid: string): Promise<CertificateVerifyResult> => {
|
||||
const res = await api.get(`/certificate-api/verify-uuid/${uuid}`);
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// POST /certificate-api/projects/{id_project}/template
|
||||
export const uploadCertificateTemplate = async (
|
||||
id_project: number,
|
||||
file: File
|
||||
): Promise<CertificateTemplate> => {
|
||||
const formData = new FormData();
|
||||
formData.append("template", file);
|
||||
const res = await api.post(`/certificate-api/projects/${id_project}/template`, formData);
|
||||
return res.data.data;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
// services/mahasiswa.service.ts
|
||||
import api from '../lib/api';
|
||||
import type { Mahasiswa, MahasiswaResponse } from "../types/project.types"
|
||||
|
||||
export const getMahasiswa = async (): Promise<Mahasiswa[]> => {
|
||||
const response = await api.get<MahasiswaResponse>('/mahasiswa-api/get');
|
||||
return response.data.data;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// services/NotificationService.ts
|
||||
import api from "../lib/api";
|
||||
import type { Notification } from "../types/certificate.types";
|
||||
|
||||
// GET /notification-api
|
||||
export const getNotifications = async (): Promise<Notification[]> => {
|
||||
const res = await api.get("/notification-api");
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
// PATCH /notification-api/read-all
|
||||
export const markAllNotificationsRead = async (): Promise<void> => {
|
||||
await api.patch("/notification-api/read-all");
|
||||
};
|
||||
|
||||
// PATCH /notification-api/{id}/read
|
||||
export const markNotificationRead = async (id: number): Promise<Notification> => {
|
||||
const res = await api.patch(`/notification-api/${id}/read`);
|
||||
return res.data.data;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import api from "../lib/api";
|
||||
|
||||
// POST auth-api/login
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// types/certificate.types.ts
|
||||
|
||||
export interface CertificateUser {
|
||||
id: number;
|
||||
full_name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface CertificateProject {
|
||||
id: number;
|
||||
project_name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Certificate {
|
||||
id: number;
|
||||
id_project: number;
|
||||
id_user: number;
|
||||
certificate_no: string;
|
||||
issued_at: string;
|
||||
user: CertificateUser;
|
||||
project: CertificateProject;
|
||||
}
|
||||
|
||||
export interface CertificateVerifyResult {
|
||||
uuid: string;
|
||||
intern_name: string;
|
||||
project_name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CertificateTemplate {
|
||||
id_project: number;
|
||||
certificate_template: string;
|
||||
}
|
||||
|
||||
// Notification
|
||||
export interface Notification {
|
||||
id: number;
|
||||
id_user: number;
|
||||
id_admin: number | null;
|
||||
title: string;
|
||||
message: string;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export interface ProjectAdmin {
|
||||
}
|
||||
|
||||
export interface ProjectMember {
|
||||
avatar: string;
|
||||
id: number;
|
||||
id_user: number;
|
||||
status?: MemberStatus;
|
||||
@@ -118,3 +119,22 @@ export interface ProjectMember {
|
||||
position: string;
|
||||
kelompok_peminatan: string;
|
||||
}
|
||||
|
||||
export interface Mahasiswa {
|
||||
id: number;
|
||||
nama_depan: string;
|
||||
nama_belakang: string;
|
||||
email: string;
|
||||
kontak: string;
|
||||
jurusan: string;
|
||||
role: string;
|
||||
cv_path: string;
|
||||
portofolio_path: string;
|
||||
motivasi: string;
|
||||
relevant_skills: string;
|
||||
}
|
||||
|
||||
export interface MahasiswaResponse {
|
||||
message: string;
|
||||
data: Mahasiswa[];
|
||||
}
|
||||
Reference in New Issue
Block a user