diff --git a/src/App.tsx b/src/App.tsx index b70fb6f..a6b06fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,8 +95,8 @@ function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/Pages/Landing.tsx b/src/Pages/Landing.tsx index b83d4fe..3f8fd24 100644 --- a/src/Pages/Landing.tsx +++ b/src/Pages/Landing.tsx @@ -227,7 +227,7 @@ const Landing = () => { loading="eager" decoding="async" // @ts-ignore - fetchPriority="high" + fetchpriority="high" className="w-full h-full object-contain rounded-lg" /> diff --git a/src/Pages/internify/intern/ForumTab.tsx b/src/Pages/internify/intern/ForumTab.tsx index 1e46951..ace03e0 100644 --- a/src/Pages/internify/intern/ForumTab.tsx +++ b/src/Pages/internify/intern/ForumTab.tsx @@ -2,12 +2,12 @@ import { useParams } from 'react-router-dom'; import ProjectTimeline from './components/ProjectTimeline'; import TaskCard from './components/ProjectTaskCard'; import HeroProject from './components/Hero-project'; -import { useProjectTasks } from '../../../hooks/useProjectDetail'; +import { useProjectTasks } from '../../../hooks/useTasks'; import type { Project } from '../../../types/project.types'; export default function ForumTab({ project }: { project: Project }) { - const { id } = useParams<{id:string}>(); - const { tasks, loading, error } = useProjectTasks(id ?? ""); + const { slug } = useParams<{ slug: string }>(); + const { tasks, loading, error } = useProjectTasks(slug ?? ""); return (
{/* Hero banner */} @@ -26,19 +26,18 @@ export default function ForumTab({ project }: { project: Project }) { {error &&

{error}

} {/* Timeline + Task list */} -
-
- {/* */} +
+
+
- -
+
{tasks.length === 0 ? ( -

Belum ada task.

- ) : ( - tasks.map((task) => ( - - )) - )} +

Belum ada task.

+ ) : ( + tasks.map((task) => ( + + )) + )}
diff --git a/src/Pages/internify/intern/ParticipantTab.tsx b/src/Pages/internify/intern/ParticipantTab.tsx index c1de1eb..d851ac6 100644 --- a/src/Pages/internify/intern/ParticipantTab.tsx +++ b/src/Pages/internify/intern/ParticipantTab.tsx @@ -1,17 +1,17 @@ import { useParams } from 'react-router-dom'; -import { useProjectInterns } from '../../../hooks/useProjectDetail'; +import { } from '../../../hooks/useProjects'; import type { Project } from '../../../types/project.types'; export default function ParticipantsTab({ project }: { project: Project }) { - const { id } = useParams<{ id: string }>(); - const { interns, loading, error } = useProjectInterns(id ?? ""); + const { slug } = useParams<{ slug: string }>(); + const { interns, loading, error } = useProjectInterns(slug ?? ""); return (
{/* Mentor */}

Mentor

-
+
{project.admin.profile_picture ? ( )} - {project.admin.full_name} + {project.admin.full_name}
@@ -45,25 +45,25 @@ export default function ParticipantsTab({ project }: { project: Project }) {
{intern.avatar ? ( {intern.name} ) : (
)}
- {intern.name} + {intern.full_name} {intern.email}
- {intern.role} + {intern.kelompok_peminatan}
))} diff --git a/src/Pages/internify/intern/ProjectDetailPage.tsx b/src/Pages/internify/intern/ProjectDetailPage.tsx index 8f524cf..7b97d40 100644 --- a/src/Pages/internify/intern/ProjectDetailPage.tsx +++ b/src/Pages/internify/intern/ProjectDetailPage.tsx @@ -1,14 +1,13 @@ import { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { useProjectDetail } from '../../../hooks/useProjectDetail'; +// import { useProjectDetail } from '../../../hooks/useProjectDetail'; +import { useProjectDetail } from '../../../hooks/useProjects'; import ForumTab from './ForumTab'; import ParticipantsTab from './ParticipantTab'; -const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); - export default function ProjectDetailPage() { - const { id } = useParams<{ id: string }>(); - const { project, loading, error } = useProjectDetail(id ?? ""); + const { slug } = useParams<{ slug: string }>(); + const { project, loading, error } = useProjectDetail(slug ?? ""); const [activeTab, setActiveTab] = useState<'forum' | 'participants'>('forum'); // Fallback sederhana @@ -26,7 +25,7 @@ export default function ProjectDetailPage() {
{/* Tabs */} -
+
diff --git a/src/Pages/internify/intern/ProjectsPage.tsx b/src/Pages/internify/intern/ProjectsPage.tsx index 6ebbfc4..7733cf0 100644 --- a/src/Pages/internify/intern/ProjectsPage.tsx +++ b/src/Pages/internify/intern/ProjectsPage.tsx @@ -1,8 +1,8 @@ import ProjectCard from './components/ProjectsCard' -import { useProjects } from '../../../hooks/useProjects'; +import { useMyProjects } from '../../../hooks/useProjects'; export default function ProjectsPage() { - const { projects, loading, error } = useProjects(); + const { projects, loading, error } = useMyProjects(); if (loading) return (
@@ -22,7 +22,7 @@ export default function ProjectsPage() {
{/* Grid card */} -
+
{projects.map(project => ( ))} diff --git a/src/Pages/internify/intern/TaskSubmission.tsx b/src/Pages/internify/intern/TaskSubmission.tsx index 9bd5b76..f149654 100644 --- a/src/Pages/internify/intern/TaskSubmission.tsx +++ b/src/Pages/internify/intern/TaskSubmission.tsx @@ -2,71 +2,68 @@ import { useParams, useSearchParams } from "react-router-dom"; import { FileText, Info } from "lucide-react"; import TaskFormFile from "./components/TaskForm-file"; import TaskFormLink from "./components/TaskForm-link"; -import { mockProjects } from "../../../lib/mockProjects"; - -const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); +import { useTaskDetail } from "../../../hooks/useTasks"; export default function TaskSubmission() { const [searchParams] = useSearchParams(); - const { slug, TaskSlug } = useParams<{ slug: string; TaskSlug: string }>(); + const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); + const submissionType = (searchParams.get("type") ?? "file") as "file_upload" | "url_link"; + const { task, loading, error } = useTaskDetail(taskSlug!, slug); - const submissionType = (searchParams.get("type") ?? "file") as "file" | "link"; + const formatDeadline = (dateStr: string) => + new Date(dateStr).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }); + const formatTime = (dateStr: string) => + new Date(dateStr).toLocaleTimeString("id-ID", { hour: "2-digit", minute: "2-digit" }); - const project = mockProjects.find((p) => toSlug(p.name) === slug); - const task = project?.tasks.find((t) => toSlug(t.title) === TaskSlug); - const handleFileSubmit = (files: File[]) => { - const fileNames = files.map(f => f.name).join(', '); - console.log(`Submitting Files for Project ${slug}:`, fileNames); - alert(`File ${fileNames} berhasil dikirim!`); - }; - - const handleLinkSubmit = (link: string) => { - console.log(`Submitting Link for Project ${slug}:`, link); - alert(`Link berhasil dikirim!`); - }; + if (loading) return ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ); + if (error) return

{error}

; + if (!task) return null; return ( -
- {/* Header Card */} -
-
-
-

- {task?.title} -

- - Due {task?.deadline.label} at {task?.deadline.time} - -
+
+
+
+

{task.title}

+ + Due {formatDeadline(task.deadline_at)} at {formatTime(task.deadline_at)} +
- {/* Task Description Card */}
-

Task Description

-

- {task?.description} -

+

Task Description

+

{task.description}

- {/* Submit Area Card */}
-
- -

Save Changes

+
+ +

Submit Task

- {/* Render form sesuai tipe dari query param */} - {submissionType === "file" ? ( - + {submissionType === "file_upload" ? ( + ) : ( - + )} - {/* Info Box */}
- -

+ +

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

diff --git a/src/Pages/internify/intern/components/CertificateAvailabe.tsx b/src/Pages/internify/intern/components/CertificateAvailabe.tsx index 983f4c3..028f62d 100644 --- a/src/Pages/internify/intern/components/CertificateAvailabe.tsx +++ b/src/Pages/internify/intern/components/CertificateAvailabe.tsx @@ -34,19 +34,11 @@ export default function CertificateAvailable({ data }: { data: CertificateData } {[ { label: "Nama Program", value: certificate.programName }, { label: "Durasi", value: certificate.duration }, - { - label: "Nilai Akhir", - value: ( - - {certificate.finalScore}/100 ({certificate.grade}) - - ), - }, { label: "Status", value: ( - {certificate.status} + VERIFIED ), }, diff --git a/src/Pages/internify/intern/components/HomeCalendar.tsx b/src/Pages/internify/intern/components/HomeCalendar.tsx index f09d578..5ac0110 100644 --- a/src/Pages/internify/intern/components/HomeCalendar.tsx +++ b/src/Pages/internify/intern/components/HomeCalendar.tsx @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react' import { ChevronLeft, ChevronRight } from 'lucide-react' -import { DEADLINES } from '../../../../lib/mockDeadline' - +import { useMyTasks } from '../../../../hooks/useProjects'; const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] const MONTH_NAMES = [ @@ -18,7 +17,13 @@ function getFirstDayOfMonth(year: number, month: number) { } const HomeCalendar = () => { - const [today, setToday] = useState(new Date()) + const [today, setToday] = useState(new Date()); + const { tasks } = useMyTasks(); + + const DEADLINES = tasks.map(task => ({ + date: new Date(task.deadline_at), + label: task.title, + })); useEffect(() => { const interval = setInterval(() => { diff --git a/src/Pages/internify/intern/components/ProjectTaskCard.tsx b/src/Pages/internify/intern/components/ProjectTaskCard.tsx index 64688f0..299dd57 100644 --- a/src/Pages/internify/intern/components/ProjectTaskCard.tsx +++ b/src/Pages/internify/intern/components/ProjectTaskCard.tsx @@ -1,5 +1,5 @@ import { useNavigate, useParams } from "react-router-dom"; -import type { ProjectTask } from "../../../../types/project.types"; +import type { ProjectTask } from "../../../../types/task.types"; const formatDate = (dateStr: string) => new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); @@ -7,32 +7,34 @@ const formatDate = (dateStr: string) => const formatTime = (dateStr: string) => new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); -export default function TaskCard({ task, projectName }: { task: ProjectTask; projectName: string }) { +export default function TaskCard({ task }: { task: ProjectTask }) { const navigate = useNavigate(); - const {id}= useParams<{ id: string }>(); + const { slug } = useParams<{ slug: string }>(); return ( -
+
navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`)} + >
-
+
Deadline : {formatDate(task.deadline_at)} at {formatTime(task.deadline_at)}
{task.title}
-
+
{task.description}
-
+
Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"}
-
+
+ +
diff --git a/src/Pages/internify/intern/components/ProjectTimeline.tsx b/src/Pages/internify/intern/components/ProjectTimeline.tsx index a22313a..a2b0225 100644 --- a/src/Pages/internify/intern/components/ProjectTimeline.tsx +++ b/src/Pages/internify/intern/components/ProjectTimeline.tsx @@ -1,50 +1,43 @@ -import type { Task } from "../../../../lib/mockProjects"; -import { ArrowDownNarrowWide } from "lucide-react"; +import type { ProjectTask } from "../../../../types/project.types"; -const formatDate = (date: Date) => - new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); +const formatDate = (dateStr: string) => + new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short' }); -export default function ProjectTimeline({ tasks }: { tasks: Task[] }) { - const today = new Date(); - today.setHours(0, 0, 0, 0); +export default function ProjectTimeline({ tasks }: { tasks: ProjectTask[] }) { + const today = new Date(); + today.setHours(0, 0, 0, 0); - return ( -
-
- - Timeline - - -
+ return ( +
+
+ Timeline +
+
    + {tasks.map((task) => { + const d = new Date(task.deadline_at); + d.setHours(0, 0, 0, 0); + const isToday = d.getTime() === today.getTime(); + const isPast = d < today; + const active = isToday || isPast; -
      - {tasks.map((task, idx) => { - const d = new Date(task.deadline.date); - d.setHours(0, 0, 0, 0); - const isToday = d.getTime() === today.getTime(); - const isPast = d < today; - const active = isToday || isPast; - - return ( -
    • - {/* Dot */} -
      -
      - {isToday ? 'Hari ini' : formatDate(task.deadline.date)} -
      -
      - {task.title} -
      -
    • - ); - })} -
    -
- ); + return ( +
  • + {/* Dot */} +
    +
    + {isToday ? 'Hari ini' : formatDate(task.deadline_at)} {/* ← format ISO string */} +
    +
    + {task.title} +
    +
  • + ); + })} + +
    + ); } \ No newline at end of file diff --git a/src/Pages/internify/intern/components/ProjectsCard.tsx b/src/Pages/internify/intern/components/ProjectsCard.tsx index c2ce585..2cbea7c 100644 --- a/src/Pages/internify/intern/components/ProjectsCard.tsx +++ b/src/Pages/internify/intern/components/ProjectsCard.tsx @@ -3,8 +3,6 @@ import * as LucideIcons from 'lucide-react'; import type { LucideProps } from 'lucide-react'; import type { Project } from '../../../../types/project.types'; -const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); - const getDynamicIcon = (iconName: string) => { const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1); const Icon = (LucideIcons as unknown as Record>)[formatted]; @@ -21,7 +19,7 @@ const ProjectCard = (project: Project) => { return (
    navigate(`/intern/projects/${project.id}`)} + onClick={() => 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" > {/* Thumbnail */} @@ -61,7 +59,7 @@ const ProjectCard = (project: Project) => { style={{ width: tasksTotal > 0 ? `${(tasksDone / tasksTotal) * 100}%` : '0%' }} />
    - + {tasksDone}/{tasksTotal}
    @@ -70,10 +68,6 @@ const ProjectCard = (project: Project) => { {/* Button */}
    - - {/* Tombol Close silang (X): Hanya muncul di HP saat sidebar terbuka */}
    @@ -148,19 +112,16 @@ if (isSubmitted) { ) : (
    -

    - Click to upload or drag & drop -

    +

    Click to upload or drag & drop

    PDF, DOCX, or ZIP (Max. 10MB)

    )}
    - diff --git a/src/Pages/internify/intern/components/TaskForm-link.tsx b/src/Pages/internify/intern/components/TaskForm-link.tsx index 478133f..618682d 100644 --- a/src/Pages/internify/intern/components/TaskForm-link.tsx +++ b/src/Pages/internify/intern/components/TaskForm-link.tsx @@ -1,80 +1,76 @@ import { useState } from "react"; -import { Link, FileText } from "lucide-react"; +import { Link } from "lucide-react"; import { LuSendHorizontal } from "react-icons/lu"; import SubmitStatusTable from "./SubmissionStatusTable"; +import { useSubmission } from "../../../../hooks/useTasks"; +import type { TaskSubmissionData } from "../../../../types/task.types"; interface TaskFormLinkProps { - onSubmitSuccess: (link: string) => void; + taskId: string; deadline?: Date; + initialSubmission?: TaskSubmissionData | null; } -export default function TaskFormLink({ onSubmitSuccess, deadline }: TaskFormLinkProps) { - const [submissionLink, setSubmissionLink] = useState(""); - const [isSubmitted, setIsSubmitted] = useState(false); +export default function TaskFormLink({ taskId, deadline, initialSubmission }: TaskFormLinkProps) { + const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission); + const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? ""); + const [isEditing, setIsEditing] = useState(false); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!submissionLink.trim()) { - // alert("Silakan masukkan link tugas Anda terlebih dahulu."); - return; - } - setIsSubmitted(true); // ini hilang - onSubmitSuccess(submissionLink); + if (!inputLink.trim()) return; + const res = isEditing + ? await updateLink(submission!.id, inputLink) + : await submitLink(inputLink); + if (res) { setIsEditing(false); } }; - const handleEditSubmission = () => { - setIsSubmitted(false); - }; + const handleDelete = async () => { + if (!confirm("Yakin ingin menghapus submission ini?")) return; + const ok = await remove(submission!.id); + if (ok) setInputLink(""); +}; - const handleDeleteSubmission = () => { - if (confirm("Are you sure you want to delete this submission?")) { - setSubmissionLink(""); - setIsSubmitted(false); - } - }; - - if (isSubmitted) { - return ( - - ); -} + if (submission && !isEditing) { + return ( + setIsEditing(true)} + onDelete={handleDelete} + /> + ); + } return (
    -
    -
    diff --git a/src/Pages/internify/intern/components/TaskTimeline.tsx b/src/Pages/internify/intern/components/TaskTimeline.tsx index 446b50e..5206e25 100644 --- a/src/Pages/internify/intern/components/TaskTimeline.tsx +++ b/src/Pages/internify/intern/components/TaskTimeline.tsx @@ -1,8 +1,8 @@ -import { useInternTasks } from "../../../../hooks/useProjects"; -import type { InternTask } from "../../../../types/project.types"; +import { useMyTasks } from "../../../../hooks/useProjects"; +import type { InternTaskItem } from "../../../../types/project.types"; const TaskTimeline = () => { - const { tasks, loading, error } = useInternTasks(); + const { tasks, loading, error } = useMyTasks(); const today = new Date(); const isOverdue = (dateStr: string) => new Date(dateStr) < today; @@ -23,7 +23,7 @@ const TaskTimeline = () => {
      - {tasks.map((item: InternTask) => { + {tasks.map((item: InternTaskItem) => { const overdue = isOverdue(item.deadline_at); const deadlineDate = new Date(item.deadline_at); return ( diff --git a/src/Pages/internify/layout.tsx b/src/Pages/internify/layout.tsx index 0379c1c..2c05d3e 100644 --- a/src/Pages/internify/layout.tsx +++ b/src/Pages/internify/layout.tsx @@ -17,7 +17,7 @@ export const InternLayout = () => { {isSidebarOpen && (
      )} @@ -47,7 +47,7 @@ export const MentorLayout = () => { /> )}
      - + {/* */}
      diff --git a/src/hooks/useProjectDetail.ts b/src/hooks/useProjectDetail.ts deleted file mode 100644 index 836a819..0000000 --- a/src/hooks/useProjectDetail.ts +++ /dev/null @@ -1,59 +0,0 @@ -// hooks/useProjectDetail.ts -/** - * Bagian Detail dari Project: Ketika User sudah masuk ke dalam Project Card - * Komponen yang masuk: IDProject, ParticipantTab, ForumTab, Submissions - */ -import { useEffect, useState } from "react"; -import { getProjectById, getProjectInterns, getProjectTasks } from "../services/ProjectService"; -import type { Project, Intern, ProjectTask } from "../types/project.types"; - -export const useProjectDetail = (id: string) => { - const [project, setProject] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (!id) return; - setLoading(true); - getProjectById(id) - .then(setProject) - .catch(() => setError("Gagal memuat detail project.")) - .finally(() => setLoading(false)); - }, [id]); - - return { project, loading, error }; -}; - -export const useProjectInterns = (projectId: string) => { - const [interns, setInterns] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (!projectId) return; - setLoading(true); - getProjectInterns(projectId) - .then(setInterns) - .catch(() => setError("Gagal memuat participants.")) - .finally(() => setLoading(false)); - }, [projectId]); - - return { interns, loading, error }; -}; - -export const useProjectTasks = (projectId: string) => { - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (!projectId) return; - setLoading(true); - getProjectTasks(projectId) - .then(setTasks) - .catch(() => setError("Gagal memuat tasks.")) - .finally(() => setLoading(false)); - }, [projectId]); - - return { tasks, loading, error }; -}; \ No newline at end of file diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index f3cec5e..5cfd188 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -1,34 +1,109 @@ -import { useEffect, useState } from "react"; -// Projects -import { getMyProjects } from "../services/ProjectService"; -import type { Project } from "../types/project.types"; -// Tasks -import { getMyTasks } from "../services/ProjectService"; -import type { InternTask } from "../types/project.types"; +// hooks/useProject.ts +import { useState, useEffect, useCallback } from "react"; +import type { + Project, + ProjectDetail, + InternTaskItem, + InternDetail, + CreateProjectPayload, + UpdateProjectPayload, + AssignMemberPayload, + ProjectMember +} from "../types/project.types"; +import { + getProjects, + getProjectById, + getMyProjects, + getMyTasks, + getMentorProjects, + getAllInterns, + createProject, + updateProject, + archiveProject, + assignMember, + getProjectMembers +} from "../services/ProjectService"; -export const useProjects = () => { +// GET /project-api/get +export const useProjects = (status?: "active" | "completed" | "archived") => { const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const refetch = useCallback(() => { + setLoading(true); + getProjects(status) + .then(setProjects) + .catch(() => setError("Gagal memuat projects.")) + .finally(() => setLoading(false)); + }, [status]); + + useEffect(() => { refetch(); }, [refetch]); + + return { projects, loading, error, refetch }; +}; + +// GET /project-api/get/{id} +export const useProjectDetail = (id: string) => { + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const refetch = useCallback(() => { + if (!id) return; + setLoading(true); + getProjectById(id) + .then(setProject) + .catch(() => setError("Gagal memuat detail project.")) + .finally(() => setLoading(false)); + }, [id]); + + useEffect(() => { refetch(); }, [refetch]); + + return { project, loading, error, refetch }; +}; + +//Khusus Ambil Intern dari Project/{id} +export const useProjectInterns = (projectId: string) => { + const [interns, setInterns] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { + if (!projectId) return; setLoading(true); + getProjectMembers(projectId) + .then(setInterns) + .catch(() => setError("Gagal memuat participants.")) + .finally(() => setLoading(false)); + }, [projectId]); + + return { interns, loading, error }; +}; + +// GET /project-api/my-projects +export const useMyProjects = () => { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { getMyProjects() .then(setProjects) - .catch(() => setError("Gagal memuat project.")) + .catch(() => setError("Gagal memuat my projects.")) .finally(() => setLoading(false)); }, []); return { projects, loading, error }; }; -export const useInternTasks = () => { - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(false); +// GET /project-api/my-tasks +export const useMyTasks = () => { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { - setLoading(true); getMyTasks() .then(setTasks) .catch(() => setError("Gagal memuat tasks.")) @@ -38,3 +113,118 @@ export const useInternTasks = () => { return { tasks, loading, error }; }; +// GET /project-api/mentor-projects +export const useMentorProjects = () => { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + getMentorProjects() + .then(setProjects) + .catch(() => setError("Gagal memuat mentor projects.")) + .finally(() => setLoading(false)); + }, []); + + return { projects, loading, error }; +}; + +// GET /project-api/interns +export const useAllInterns = () => { + const [interns, setInterns] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + getAllInterns() + .then(setInterns) + .catch(() => setError("Gagal memuat data interns.")) + .finally(() => setLoading(false)); + }, []); + + return { interns, loading, error }; +}; + +// POST /project-api/add +export const useCreateProject = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const create = async (payload: CreateProjectPayload) => { + setLoading(true); + setError(null); + try { + return await createProject(payload); + } catch { + setError("Gagal membuat project."); + return null; + } finally { + setLoading(false); + } + }; + + return { create, loading, error }; +}; + +// PATCH /project-api/update/{id} +export const useUpdateProject = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const update = async (id: string | number, payload: UpdateProjectPayload) => { + setLoading(true); + setError(null); + try { + return await updateProject(id, payload); + } catch { + setError("Gagal mengupdate project."); + return null; + } finally { + setLoading(false); + } + }; + + return { update, loading, error }; +}; + +// DELETE /project-api/delete/{id} +export const useArchiveProject = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const archive = async (id: string | number) => { + setLoading(true); + setError(null); + try { + return await archiveProject(id); + } catch { + setError("Gagal mengarsipkan project."); + return null; + } finally { + setLoading(false); + } + }; + + return { archive, loading, error }; +}; + +// POST /project-api/assign-member +export const useAssignMember = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const assign = async (payload: AssignMemberPayload) => { + setLoading(true); + setError(null); + try { + return await assignMember(payload); + } catch { + setError("Gagal assign member."); + return null; + } finally { + setLoading(false); + } + }; + + return { assign, loading, error }; +}; \ No newline at end of file diff --git a/src/hooks/useTasks.ts b/src/hooks/useTasks.ts new file mode 100644 index 0000000..49cc841 --- /dev/null +++ b/src/hooks/useTasks.ts @@ -0,0 +1,180 @@ +// hooks/useTasks.ts +import { useState, useEffect, useCallback } from "react"; +import type { ProjectTask, ProjectTaskDetail, CreateTaskPayload, UpdateTaskPayload, TaskSubmissionData } from "../types/task.types"; +import { + getProjectTasks, + getTaskById, + createTask, + updateTask, + deleteTask, + submitTaskFile, + submitTaskLink, + updateSubmissionFile, + updateSubmissionLink, + deleteSubmission, +} from "../services/TaskService"; + +// GET /task-api/projects/{id_project}/tasks +export const useProjectTasks = (projectId: string) => { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!projectId) return; + setLoading(true); + getProjectTasks(projectId) + .then(setTasks) + .catch(() => setError("Gagal memuat task.")) + .finally(() => setLoading(false)); + }, [projectId]); + + return { tasks, loading, error }; +}; + +// GET /task-api/tasks/{id} +export const useTaskDetail = (taskId: string, projectId?: string) => { + const [task, setTask] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const refetch = useCallback(() => { + if (!taskId) return; + setLoading(true); + getTaskById(taskId, projectId) + .then(setTask) + .catch(() => setError("Gagal memuat detail task.")) + .finally(() => setLoading(false)); + }, [taskId, projectId]); + + useEffect(() => { refetch(); }, [refetch]); + + return { task, loading, error, refetch }; +}; + +// POST /task-api/projects/{id_project}/tasks +export const useCreateTask = (projectId: string) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const create = async (payload: CreateTaskPayload): Promise => { + setLoading(true); + setError(null); + try { + return await createTask(projectId, payload); + } catch { + setError("Gagal membuat task."); + return null; + } finally { + setLoading(false); + } + }; + + return { create, loading, error }; +}; + +// PATCH /task-api/tasks/{id} +export const useUpdateTask = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const update = async ( + taskId: string | number, + payload: UpdateTaskPayload, + projectId?: string + ) => { + setLoading(true); + setError(null); + try { + return await updateTask(taskId, payload, projectId); + } catch { + setError("Gagal mengupdate task."); + return null; + } finally { + setLoading(false); + } + }; + + return { update, loading, error }; +}; + +// DELETE /task-api/tasks/{id} +export const useDeleteTask = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const remove = async (taskId: string | number, projectId?: string) => { + setLoading(true); + setError(null); + try { + await deleteTask(taskId, projectId); + return true; + } catch { + setError("Gagal menghapus task."); + return false; + } finally { + setLoading(false); + } + }; + + return { remove, loading, error }; +}; + +// POST + PATCH + DELETE submission — digabung dalam satu hook per task +export const useSubmission = (taskId: string, initialSubmission?: TaskSubmissionData | null, projectId?: string) => { + const [submission, setSubmission] = useState(initialSubmission ?? null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const submitFile = async (file: File) => { + setLoading(true); setError(null); + try { + const res = await submitTaskFile(taskId, file, projectId); + if (res) setSubmission(res); + return res; + } catch { setError("Gagal submit file."); return null; } + finally { setLoading(false); } + }; + + const submitLink = async (url: string) => { + setLoading(true); setError(null); + try { + const res = await submitTaskLink(taskId, url, projectId); + if (res) setSubmission(res); + return res; + } catch { setError("Gagal submit link."); return null; } + finally { setLoading(false); } + }; + + const updateFile = async (submissionId: number, file: File) => { + setLoading(true); setError(null); + try { + const res = await updateSubmissionFile(submissionId, file); + if (res) setSubmission(res); + return res; + } catch { setError("Gagal update submission."); return null; } + finally { setLoading(false); } + }; + + const updateLink = async (submissionId: number, url: string) => { + setLoading(true); setError(null); + try { + const res = await updateSubmissionLink(submissionId, url); + if (res) setSubmission(res); + return res; + } catch { setError("Gagal update submission."); return null; } + finally { setLoading(false); } + }; + + const remove = async (submissionId: number) => { + setLoading(true); setError(null); + try { + await deleteSubmission(submissionId); + setSubmission(null); + return true; + } catch { setError("Gagal hapus submission."); return false; } + finally { setLoading(false); } + }; + + return { submission, submitFile, submitLink, updateFile, updateLink, remove, loading, error }; +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css index ae5add1..a6d3e55 100644 --- a/src/index.css +++ b/src/index.css @@ -31,8 +31,8 @@ --color-primary-foreground: #ffffff; --color-primary-hover: #84281c; - --color-base: #e82222; - --color-base-foreground: #fef2f2; + --color-red: #e82222; + --color-red-foreground: #fef2f2; --color-border: #a8a9aa; @@ -43,14 +43,11 @@ } .container { - flex: 1; - min-width: 0; /* penting! mencegah overflow */ + min-width: 0; padding-inline: calc(var(--spacing) * 14); padding-block: calc(var(--spacing) * 12); background-color: var(--color-background); - display: flex; flex-direction: column; - gap: 13px; min-height: 100vh; } @@ -130,6 +127,12 @@ dialog.custom-dialog::backdrop { border-bottom: 2px solid #b3b8c0; /* border-gray-100 */ } +@media (max-width: 1024px) { + .container { + padding-inline: calc(var(--spacing) * 16); + } +} + /* Breakpoint Medium (md:) - Aktif saat lebar layar >= 768px */ @media (min-width: 768px) { .dialog-header { @@ -139,4 +142,9 @@ dialog.custom-dialog::backdrop { padding-top: 1.25rem; /* 20px */ padding-bottom: 1.25rem;/* 20px */ } + + .container { + padding-inline: calc(var(--spacing) * 14); + padding-block: calc(var(--spacing) * 10); + } } \ No newline at end of file diff --git a/src/services/ProjectService.ts b/src/services/ProjectService.ts index 6f025c9..3192758 100644 --- a/src/services/ProjectService.ts +++ b/src/services/ProjectService.ts @@ -1,29 +1,96 @@ +// services/ProjectService.ts import api from "../lib/api"; -import type { Project, InternTask, Intern, ProjectTask } from "../types/project.types"; +import type { + Project, + ProjectDetail, + InternTaskItem, + InternDetail, + CreateProjectPayload, + UpdateProjectPayload, + AssignMemberPayload, + MemberAssignment, + ProjectMember +} from "../types/project.types"; -export const getMyProjects = async (): Promise => { - const res = await api.get("/project-api/my-projects"); +// POST /project-api/add +export const createProject = async ( + payload: CreateProjectPayload +): Promise => { + const res = await api.post("/project-api/add", payload); return res.data.data; }; -export const getProjectById = async (id: string): Promise => { - const res = await api.get(`/project-api/get/${id}`); - return res.data.data; -}; - -export const getMyTasks = async (): Promise => { - const res = await api.get("/project-api/my-tasks"); - return res.data.data; -}; - -export const getProjectInterns = async (projectId: string): Promise => { - const res = await api.get(`/project-api/interns`, { - params: { projectId } +// GET /project-api/get +export const getProjects = async ( + status?: "active" | "completed" | "archived" +): Promise => { + const res = await api.get("/project-api/get", { + params: status ? { status } : undefined, }); return res.data.data; }; -export const getProjectTasks = async (projectId: string): Promise => { - const res = await api.get(`/task-api/projects/${projectId}/tasks`); +// GET /project-api/get/{id} → ambil members saja +export const getProjectMembers = async ( + id: string | number +): Promise => { + const res = await api.get(`/project-api/get/${id}`); + return res.data.data.members; +}; + +// GET /project-api/get/{id} +export const getProjectById = async ( + id: string | number +): Promise => { + const res = await api.get(`/project-api/get/${id}`); + return res.data.data; +}; + +// PATCH /project-api/update/{id} +export const updateProject = async ( + id: string | number, + payload: UpdateProjectPayload +): Promise => { + const res = await api.patch(`/project-api/update/${id}`, payload); + return res.data.data; +}; + +// DELETE /project-api/delete/{id} — soft delete (archive) +export const archiveProject = async ( + id: string | number +): Promise => { + const res = await api.delete(`/project-api/delete/${id}`); + return res.data.data; +}; + +// GET /project-api/my-projects (intern only) +export const getMyProjects = async (): Promise => { + const res = await api.get("/project-api/my-projects"); + return res.data.data; +}; + +// GET /project-api/my-tasks (intern only) +export const getMyTasks = async (): Promise => { + const res = await api.get("/project-api/my-tasks"); + return res.data.data; +}; + +// GET /project-api/mentor-projects (admin/mentor only) +export const getMentorProjects = async (): Promise => { + const res = await api.get("/project-api/mentor-projects"); + return res.data.data; +}; + +// GET /project-api/interns (admin/mentor only) +export const getAllInterns = async (): Promise => { + const res = await api.get("/project-api/interns"); + return res.data.data; +}; + +// POST /project-api/assign-member (admin/mentor only) +export const assignMember = async ( + payload: AssignMemberPayload +): Promise => { + const res = await api.post("/project-api/assign-member", payload); return res.data.data; }; \ No newline at end of file diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts new file mode 100644 index 0000000..3d6b0c1 --- /dev/null +++ b/src/services/TaskService.ts @@ -0,0 +1,108 @@ +// services/TaskService.ts +import api from "../lib/api"; +import type { + ProjectTask, + ProjectTaskDetail, + CreateTaskPayload, + UpdateTaskPayload, + TaskSubmissionData, +} from "../types/task.types"; + +// ── Task ────────────────────────────────────────────── + +export const createTask = async ( + projectId: string | number, + payload: CreateTaskPayload +): Promise => { + const res = await api.post(`/task-api/projects/${projectId}/tasks`, payload); + return res.data.data; +}; + +export const getProjectTasks = async ( + projectId: string | number +): Promise => { + const res = await api.get(`/task-api/projects/${projectId}/tasks`); + return res.data.data; +}; + +export const getTaskById = async ( + taskId: string | number, + projectId?: string | number +): Promise => { + const res = await api.get(`/task-api/tasks/${taskId}`, { + params: projectId ? { project: projectId } : undefined, + }); + return res.data.data; +}; + +export const updateTask = async ( + taskId: string | number, + payload: UpdateTaskPayload, + projectId?: string | number +): Promise => { + const res = await api.patch(`/task-api/tasks/${taskId}`, payload, { + params: projectId ? { project: projectId } : undefined, + }); + return res.data.data; +}; + +export const deleteTask = async ( + taskId: string | number, + projectId?: string | number +): Promise => { + await api.delete(`/task-api/tasks/${taskId}`, { + params: projectId ? { project: projectId } : undefined, + }); +}; + +// ── Submission ──────────────────────────────────────── + +export const submitTaskFile = async ( + taskId: string | number, + file: File, + projectId?: string | number +): Promise => { + const formData = new FormData(); + formData.append("submission_file", file); + const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, { + params: projectId ? { project: projectId } : undefined, + }); + return res.data.data; +}; + +export const submitTaskLink = async ( + taskId: string | number, + url_link: string, + projectId?: string | number +): Promise => { + const formData = new FormData(); + formData.append("url_link", url_link); + const res = await api.post(`/task-api/tasks/${taskId}/submissions`, formData, { + params: projectId ? { project: projectId } : undefined, + }); + return res.data.data; +}; + +export const updateSubmissionFile = async ( + submissionId: number, + file: File +): Promise => { + const formData = new FormData(); + formData.append("submission_file", file); + const res = await api.patch(`/task-api/submissions/${submissionId}`, formData); + return res.data.data; +}; + +export const updateSubmissionLink = async ( + submissionId: number, + url_link: string +): Promise => { + const formData = new FormData(); + formData.append("url_link", url_link); + const res = await api.patch(`/task-api/submissions/${submissionId}`, formData); + return res.data.data; +}; + +export const deleteSubmission = async (submissionId: number): Promise => { + await api.delete(`/task-api/submissions/${submissionId}`); +}; \ No newline at end of file diff --git a/src/services/UserService.ts b/src/services/UserService.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/types/project.types.ts b/src/types/project.types.ts index e363e02..3db67ef 100644 --- a/src/types/project.types.ts +++ b/src/types/project.types.ts @@ -1,21 +1,34 @@ -// Admin/Mentor Projects +// types/project.types.ts + +export type ProjectStatus = "active" | "completed" | "archived"; +export type MemberStatus = "active" | "inactive"; + export interface ProjectAdmin { id: number; full_name: string; email: string; - profile_picture: string; - professional_bio: string; + profile_picture: string | null; + professional_bio: string | null; } -// Project General +export interface ProjectMember { + id: number; + id_user: number; + status?: MemberStatus; + created_at?: string; + updated_at?: string; +} + +// Response GET /project-api/get dan GET /project-api/get/{id} export interface Project { id: number; + slug: string; project_icon: string; project_name: string; description: string; start_date: string; end_date: string; - status: "active" | "inactive" | "completed"; + status: ProjectStatus; admin: ProjectAdmin; total_members: number; total_tasks: number; @@ -23,31 +36,35 @@ export interface Project { updated_at: string; } -// -export interface TaskSubmissionDetails { - id: number; - file_path: string | null; - url_link: string | null; - submitted_at: string; - updated_at: string; +// Detail project (includes members array) +export interface ProjectDetail extends Project { + members: ProjectMember[]; } -// Task General: ALL TASK COMBINED -export interface InternTask { +// Response GET /project-api/my-tasks +export interface InternTaskItem { id: number; + slug: string; id_project: number; + project_slug: string; project_name: string; project_icon: string; title: string; description: string; deadline_at: string; - submission_type: "url_link" | "file"; - submission_status: "submitted" | "pending" | "late"; - submission_details: TaskSubmissionDetails | null; + submission_type: "file_upload" | "url_link"; + submission_status: "pending" | "submitted" | "done" | "overdue"; + submission_details: { + id: number; + file_path: string | null; + url_link: string | null; + submitted_at: string; + updated_at: string; + } | null; } -// Intern -export interface Intern { +// Response GET /project-api/interns +export interface InternDetail { id: number; name: string; email: string; @@ -57,14 +74,47 @@ export interface Intern { avatar: string | null; } -//Project Task -export interface ProjectTask { +// Payload POST /project-api/add +export interface CreateProjectPayload { + project_icon: string; + project_name: string; + description: string; + start_date: string; // "YYYY-MM-DD" + end_date: string; // "YYYY-MM-DD" + member_emails?: string[]; +} + +// Payload PATCH /project-api/update/{id} +export interface UpdateProjectPayload { + project_icon?: string; + project_name?: string; + description?: string; + start_date?: string; + end_date?: string; + status?: ProjectStatus; +} + +// Payload POST /project-api/assign-member +export interface AssignMemberPayload { + id_project: number; + id_user: number; +} + +// Response assign-member +export interface MemberAssignment { id: number; id_project: number; - title: string; - description: string; - deadline_at: string; - submission_type: "file_upload" | "url_link"; + id_user: number; + status: MemberStatus; created_at: string; updated_at: string; +} + +export interface ProjectMember { + id: number; + full_name: string; + email: string; + professional_bio: string | null; + position: string; + kelompok_peminatan: string; } \ No newline at end of file diff --git a/src/types/task.types.ts b/src/types/task.types.ts index e69de29..4fc6b02 100644 --- a/src/types/task.types.ts +++ b/src/types/task.types.ts @@ -0,0 +1,69 @@ +// types/task.types.ts + +export type SubmissionType = "file_upload" | "url_link"; +export type DisplayStatus = "pending" | "submitted" | "done" | "overdue"; + +export interface ProjectTask { + id: number; + id_project: number; + slug: string; + title: string; + description: string; + deadline_at: string; + submission_type: SubmissionType; + created_at: string; + updated_at: string; +} + +export interface TaskSubmissionData { + id: number; + id_task: number; + id_user: number; + file_path: string | null; + url_link: string | null; + submitted_at: string; + updated_at: string; +} + +export interface ProjectTaskDetail extends ProjectTask { + project: { + id: number; + project_name: string; + project_icon: string; + }; + display_status: DisplayStatus; + my_submission: TaskSubmissionData | null; +} + +export interface AdminTaskDetail extends ProjectTask { + project: { + id: number; + project_name: string; + project_icon: string; + }; + submission_summary: { + total_members: number; + total_submitted: number; + total_done: number; + total_pending: number; + total_overdue: number; + }; + submissions: Array<{ + id_user: number; + full_name: string; + file_path: string | null; + url_link: string | null; + submitted_at: string | null; + updated_at: string | null; + }>; +} + +export interface CreateTaskPayload { + title: string; + description: string; + deadline_date: string; // "YYYY-MM-DD" + specific_time: string; // "HH:mm" + submission_type: SubmissionType; +} + +export interface UpdateTaskPayload extends Partial {} \ No newline at end of file diff --git a/src/types/user.types.ts b/src/types/user.types.ts new file mode 100644 index 0000000..e69de29