Perbaikan pada landing page: Gambar Feedback menjadi statis

Taskform type link: saat diupdate sudah diarahkan ke submission status
Settings Page: Penambahan data user di samping gambar profile_picture
Penambahan warning dialog pada saat melakukan archiveProject dan CompletedProject pada list mentor project
Input durasi projek format string pada SertificateGenerator
This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-08 18:24:56 +07:00
parent 9470d30a9a
commit 328bcbcdc2
15 changed files with 456 additions and 146 deletions
+8 -13
View File
@@ -57,10 +57,11 @@ const Landing = () => {
return feedbacks[displayIndex];
}, [feedbacks, displayIndex]);
const currentImgUrl = useMemo(() => {
if (!currentFeedback) return "";
return getFeedbackImageUrl(currentFeedback.image_path);
}, [currentFeedback]);
// Gambar statis: selalu pakai foto dari feedback pertama
const staticImgUrl = useMemo(() => {
if (feedbacks.length === 0) return "";
return getFeedbackImageUrl(feedbacks[0].image_path);
}, [feedbacks]);
useEffect(() => {
// Fetch internships
@@ -217,17 +218,11 @@ const Landing = () => {
<div
className={`w-[230px] h-[320px] sm:w-[260px] sm:h-[340px] lg:w-[280px] lg:h-[360px]
rounded-xl shadow-xl bg-white/70 backdrop-blur p-3 flex items-center justify-center
transition-opacity duration-[450ms] ease-in-out
${fadeIn ? "opacity-100" : "opacity-0"}`}
`}
>
<img
key={currentImgUrl}
src={currentImgUrl}
alt={currentFeedback.nama}
loading="eager"
decoding="async"
// @ts-ignore
fetchpriority="high"
src={staticImgUrl}
alt="Humic Internship"
className="w-full h-full object-contain rounded-lg"
/>
</div>
+1 -1
View File
@@ -155,7 +155,7 @@ export default function MentorSettingsPage() {
</p>
{photoFile && (
<p className="text-xs text-blue-500 mb-2 font-medium">
📎 {photoFile.name}
{photoFile.name}
</p>
)}
<div className="flex items-center gap-2">
@@ -13,7 +13,12 @@ interface TaskFormLinkProps {
initialSubmission?: TaskSubmissionData | null;
}
export default function TaskFormLink({ taskId, projectId, deadline, initialSubmission }: TaskFormLinkProps) {
export default function TaskFormLink({
taskId,
projectId,
deadline,
initialSubmission
}: TaskFormLinkProps) {
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission, projectId);
const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? "");
const [isEditing, setIsEditing] = useState(false);
@@ -28,6 +33,7 @@ export default function TaskFormLink({ taskId, projectId, deadline, initialSubmi
return;
}
try {
await customToast.promise(
isEditing ? updateLink(submission!.id, inputLink) : submitLink(inputLink),
{
@@ -47,6 +53,10 @@ export default function TaskFormLink({ taskId, projectId, deadline, initialSubmi
}),
}
);
setIsEditing(false);
} catch {
// error sudah ditangani toast, tetap di form
}
};
const handleDelete = async () => {
@@ -2,7 +2,6 @@ import MentorProjectCard from './components/MentorProjectCard';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import CreateProjectModal from './components/CreateProjectDialog';
import { customToast } from '../../utils/showToast';
import { useProjectsByRole } from '../../../hooks/useListProjects';
export default function MentorProjectsPage() {
@@ -50,7 +49,12 @@ export default function MentorProjectsPage() {
<div className="flex flex-wrap gap-4 items-stretch">
{projects.map(project => (
<MentorProjectCard key={project.id} {...project} />
<MentorProjectCard
key={project.id}
{...project}
onArchived={refetch}
onCompleted={refetch}
/>
))}
</div>
+12 -7
View File
@@ -45,14 +45,14 @@ export default function CertificateDetail() {
description: err instanceof Error ? err.message : "Terjadi kesalahan saat upload.",
}),
}
).then(async(result) => {
).then(async (result) => {
if (result) {
setUploadSuccess(true);
setSelectedFile(null);
if (fileInputRef.current) fileInputRef.current.value = "";
await refetch();
}
}).catch(() => {});
}).catch(() => { });
};
const handleClearFile = () => {
@@ -97,23 +97,28 @@ export default function CertificateDetail() {
Administrate, issue, and track professional certifications for all interns.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-8 gap-6 items-start">
<div className="grid grid-cols-1 lg:grid-cols-8 gap-5 items-start">
{/* Left Column */}
<div className="space-y-4 lg:col-span-3">
<h2 className="text-3xl font-extrabold text-gray-900 leading-tight">
{project.project_name}
</h2>
<div className="flex items-center gap-2 text-sm text-gray-400 font-medium">
<div className="flex items-center justify-between gap-2 text-sm text-gray-400 font-medium">
<div className="flex items-center gap-2">
<CalendarDays className="w-4 h-4 flex-shrink-0" />
{new Date(project.start_date).toLocaleDateString("en-US", { month: "short", year: "numeric" })}
{" - "}
{new Date(project.end_date).toLocaleDateString("en-US", { month: "short", year: "numeric" })}
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 tracking-wide">
<h4 className="text-[8px] w-fit p-2 font-bold text-font tracking-wide uppercase rounded-xl border border-card-outline">
Status: {project.status}
</h4>
</div>
<div className="space-y-3">
<label className="text-xs font-bold text-gray-700 tracking-wide mt-1">
Certificate Template
</label>
{/* Drop Zone */}
<div className="relative">
<div
@@ -0,0 +1,108 @@
import { useEffect, useRef } from "react";
import { Archive, X } from "lucide-react";
interface ArchiveProjectDialogProps {
isOpen: boolean;
projectName: string;
loading?: boolean;
onConfirm: () => void;
onClose: () => void;
}
export default function ArchiveProjectDialog({
isOpen,
projectName,
loading = false,
onConfirm,
onClose,
}: ArchiveProjectDialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
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();
};
dialog.addEventListener("cancel", handleCancel);
return () => {
dialog.removeEventListener("cancel", handleCancel);
document.body.style.overflow = "unset";
};
}, [isOpen, onClose]);
return (
<dialog ref={dialogRef} className="custom-dialog max-w-sm w-full">
{/* Header */}
<div className="dialog-header">
<div className="flex items-center gap-2.5 px-3">
<div className="w-9 h-9 rounded-full bg-yellow-100 flex items-center justify-center shrink-0">
<Archive className="w-4 h-4 text-yellow-600" />
</div>
<div>
<h2 className="text-base font-bold text-gray-900 tracking-tight">
Arsipkan Project
</h2>
<p className="text-xs text-gray-400 font-medium">
Tindakan ini akan menonaktifkan project
</p>
</div>
</div>
<button
onClick={onClose}
disabled={loading}
className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors mt-1 disabled:opacity-50"
>
<X className="w-5 h-5 stroke-[2.5]" />
</button>
</div>
{/* Body */}
<div className="p-5 space-y-4">
<p className="text-sm text-gray-600 leading-relaxed">
Apakah kamu yakin ingin mengarsipkan project{" "}
<span className="font-semibold text-gray-900">"{projectName}"</span>?
</p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg px-4 py-3">
<p className="text-xs text-yellow-700 leading-relaxed">
<span className="font-bold">Perhatian:</span> Project yang diarsipkan
tidak akan bisa diakses oleh intern dan tidak muncul di daftar aktif.
Kamu masih bisa melihatnya di tab Archived.
</p>
</div>
{/* Actions */}
<div className="flex flex-col sm:flex-row items-center gap-2.5 pt-1">
<button
type="button"
onClick={onClose}
disabled={loading}
className="w-full sm:flex-1 py-2.5 border border-gray-200 rounded-xl text-sm font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50"
>
Batal
</button>
<button
type="button"
onClick={onConfirm}
disabled={loading}
className="w-full sm:flex-1 py-2.5 bg-yellow-500 hover:bg-yellow-600 disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2"
>
<Archive className="w-4 h-4" />
{loading ? "Mengarsipkan..." : "Ya, Arsipkan"}
</button>
</div>
</div>
</dialog>
);
}
@@ -0,0 +1,109 @@
import { useEffect, useRef } from "react";
import { ClipboardCheck, X } from "lucide-react";
interface CompleteProjectDialogProps {
isOpen: boolean;
projectName: string;
loading?: boolean;
onConfirm: () => void;
onClose: () => void;
}
export default function CompleteProjectDialog({
isOpen,
projectName,
loading = false,
onConfirm,
onClose,
}: CompleteProjectDialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
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();
};
dialog.addEventListener("cancel", handleCancel);
return () => {
dialog.removeEventListener("cancel", handleCancel);
document.body.style.overflow = "unset";
};
}, [isOpen, onClose]);
return (
<dialog ref={dialogRef} className="custom-dialog max-w-sm w-full">
{/* Header */}
<div className="dialog-header">
<div className="flex items-center gap-2.5 px-3">
<div className="w-9 h-9 rounded-full bg-green-100 flex items-center justify-center shrink-0">
<ClipboardCheck className="w-4 h-4 text-green-600" />
</div>
<div>
<h2 className="text-base font-bold text-gray-900 tracking-tight">
Selesaikan Project
</h2>
<p className="text-xs text-gray-400 font-medium">
Tandai project ini sebagai completed
</p>
</div>
</div>
<button
onClick={onClose}
disabled={loading}
className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-50 hover:text-gray-600 transition-colors mt-1 disabled:opacity-50"
>
<X className="w-5 h-5 stroke-[2.5]" />
</button>
</div>
{/* Body */}
<div className="p-5 space-y-4">
<p className="text-sm text-gray-600 leading-relaxed">
Apakah kamu yakin ingin menyelesaikan project{" "}
<span className="font-semibold text-gray-900">"{projectName}"</span>?
</p>
<div className="bg-green-50 border border-green-200 rounded-lg px-4 py-3">
<p className="text-xs text-green-700 leading-relaxed">
<span className="font-bold">Perhatian:</span> Project yang diselesaikan
akan ditandai sebagai <span className="font-semibold">Completed</span> dan
tidak bisa dikembalikan ke status aktif. Pastikan semua task dan
evaluasi intern sudah selesai sebelum melanjutkan.
</p>
</div>
{/* Actions */}
<div className="flex flex-col sm:flex-row items-center gap-2.5 pt-1">
<button
type="button"
onClick={onClose}
disabled={loading}
className="w-full sm:flex-1 py-2.5 border border-gray-200 rounded-xl text-sm font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50"
>
Batal
</button>
<button
type="button"
onClick={onConfirm}
disabled={loading}
className="w-full sm:flex-1 py-2.5 bg-green-600 hover:bg-green-700 disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2"
>
<ClipboardCheck className="w-4 h-4" />
{loading ? "Menyelesaikan..." : "Ya, Selesaikan"}
</button>
</div>
</div>
</dialog>
);
}
@@ -14,7 +14,6 @@ interface CreateTaskModalProps {
export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess }: CreateTaskModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
const { create, loading } = useCreateTask(projectId);
const [taskTitle, setTaskTitle] = useState("");
const [description, setDescription] = useState("");
const [deadlineDate, setDeadlineDate] = useState("");
@@ -24,11 +23,30 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
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 = async (e: React.FormEvent) => {
@@ -40,11 +58,17 @@ export default function CreateTaskModal({ isOpen, onClose, projectId, onSuccess
specific_time: specificTime,
submission_type: submissionType,
});
if (result) {
customToast.success('Task created', `"${taskTitle}" has been successfully added to the project.`);
customToast.success(
'Task created',
`"${taskTitle}" has been successfully added to the project.`);
onSuccess();
} else {
customToast.error('Failed to create task', 'An error occurred while saving the task. Please try again.');
customToast.error(
'Failed to create task',
'An error occurred while saving the task. Please try again.');
}
};
return (
@@ -185,10 +185,10 @@ export default function EligibleInternTable({
{/* Column Headers */}
<div className="grid grid-cols-[16px_1fr_96px_88px_60px] items-center gap-x-3 px-5 py-2.5 bg-gray-50/70 border-b border-gray-100">
<div />
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Intern Name</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider text-center">Task Progress</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider text-center">Status</span>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-wider text-center">Action</span>
<span className="text-[10px] font-bold text-gray-400 uppercase">Intern Name</span>
<span className="text-[10px] font-bold text-gray-400 uppercase text-center">Task Progress</span>
<span className="text-[10px] font-bold text-gray-400 uppercase text-center">Status</span>
<span className="text-[10px] font-bold text-gray-400 uppercase text-center">Action</span>
</div>
{/* Rows */}
@@ -1,10 +1,13 @@
import { Trash2, UsersRound } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react';
import { ClipboardCheck, Trash2, UsersRound } from 'lucide-react';
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';
import { useArchiveProject } from '../../../../hooks/useProjects';
import { useArchiveProject, useCompleteProject } from '../../../../hooks/useProjects';
import { customToast } from '../../../utils/showToast';
import ArchiveProjectDialog from './ArchiveProjectDialog';
import CompleteProjectDialog from './CompleteProjectDialog';
const getDynamicIcon = (iconName: string) => {
const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1);
@@ -14,49 +17,93 @@ const getDynamicIcon = (iconName: string) => {
interface MentorProjectCardProps extends Project {
onArchived?: () => void;
onCompleted?: () => void;
}
export default function MentorProjectCard({ onArchived, ...project} : MentorProjectCardProps) {
export default function MentorProjectCard({ onArchived, onCompleted, ...project }: MentorProjectCardProps) {
const navigate = useNavigate();
const { archive, loading } = useArchiveProject();
const { archive, loading: archiveLoading } = useArchiveProject();
const { complete, loading: completeLoading } = useCompleteProject();
const [showArchiveDialog, setShowArchiveDialog] = useState(false);
const [showCompleteDialog, setShowCompleteDialog] = useState(false);
const handleCardClick = () => {
navigate(`/mentor/projects/${project.slug}`)
navigate(`/mentor/projects/${project.slug}`);
};
const Icon = getDynamicIcon(project.project_icon);
const handleDeleteClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
/* ── Archive ─────────────────────────────────────────────── */
const handleArchiveButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setShowArchiveDialog(true);
};
const confirmed = window.confirm(`Arsipkan project "${project.project_name}"?`);
if (!confirmed) return;
const handleArchiveConfirm = async () => {
const result = await archive(project.id);
setShowArchiveDialog(false);
if (result) {
customToast.success(
"Berhasil!",
"Projek Berhasil Diarsipkan (archived)"
)
customToast.success("Berhasil!", "Projek berubah status menjadi Archived (Diarsipkan");
onArchived?.();
} else {
customToast.error(
"Gagal!",
"Projek gagal diarsipkan (archived)"
)
customToast.error("Gagal!", "Projek gagal diarsipkan (archived)");
}
};
/* ── Complete ────────────────────────────────────────────── */
const handleCompleteButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setShowCompleteDialog(true);
};
const handleCompleteConfirm = async () => {
const result = await complete(project.id);
setShowCompleteDialog(false);
if (result) {
customToast.success("Berhasil!", "Projek berubah status menjadi completed (selesai)");
onCompleted?.();
} else {
customToast.error("Gagal!", "Projek gagal diselesaikan (completed)");
}
};
return (
<>
{/* Archive Confirmation Dialog */}
<ArchiveProjectDialog
isOpen={showArchiveDialog}
projectName={project.project_name}
loading={archiveLoading}
onConfirm={handleArchiveConfirm}
onClose={() => setShowArchiveDialog(false)}
/>
{/* Complete Confirmation Dialog */}
<CompleteProjectDialog
isOpen={showCompleteDialog}
projectName={project.project_name}
loading={completeLoading}
onConfirm={handleCompleteConfirm}
onClose={() => setShowCompleteDialog(false)}
/>
{/* Card */}
<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">
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 cursor-pointer"
>
{/* Thumbnail */}
<div style={{ backgroundColor: project.background_color ?? '#dc2626' }} className="h-[150px] overflow-hidden shrink-0 flex items-center justify-center">
<div
style={{ backgroundColor: project.background_color ?? '#dc2626' }}
className="h-[150px] overflow-hidden shrink-0 flex items-center justify-center"
>
<Icon className="w-12 h-12 text-white opacity-80" />
</div>
{/* Content*/}
{/* 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">
@@ -66,11 +113,13 @@ export default function MentorProjectCard({ onArchived, ...project} : MentorProj
{/* Interns Count */}
<div className="flex items-center gap-2 mt-auto">
<UsersRound className='text-gray-400 w-4 h-4' />
<UsersRound className="text-gray-400 w-4 h-4" />
<span className="text-xs text-gray-500 truncate">
{project.total_members}/{project.total_members}
</span>
</div>
{/* Status Badge */}
<div className="flex items-center text-xs text-gray-500 gap-1">
{project.status === "active" ? (
<span className="px-2 py-0.5 bg-green-100 text-green-800 rounded-full">Active</span>
@@ -84,18 +133,31 @@ export default function MentorProjectCard({ onArchived, ...project} : MentorProj
</div>
</div>
{/* Footer */}
<div className="border-t border-gray-300 text-gray-400 flex justify-end p-2 bg-gray-50/50">
{/* Footer Actions */}
<div className="border-t border-gray-300 text-gray-400 flex justify-end gap-1 p-2 bg-gray-50/50">
{/* Complete Button */}
<button
onClick={handleDeleteClick}
disabled={loading}
className="hover:text-red-600 transition-colors disabled:opacity-50"
aria-label={`Archive ${project.project_name}`}
onClick={handleCompleteButtonClick}
disabled={completeLoading}
className="p-1 rounded hover:text-green-600 hover:bg-green-50 transition-colors disabled:opacity-50"
aria-label={`Selesaikan ${project.project_name}`}
title="Selesaikan project"
>
<ClipboardCheck className="w-4 h-4" />
</button>
{/* Archive Button */}
<button
onClick={handleArchiveButtonClick}
disabled={archiveLoading}
className="p-1 rounded hover:text-yellow-600 hover:bg-yellow-50 transition-colors disabled:opacity-50"
aria-label={`Arsipkan ${project.project_name}`}
title="Arsipkan project"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
)
</>
);
}
@@ -40,7 +40,7 @@ export default function SidebarMentor({ isOpen, closeSidebar }: SidebarProps) {
<aside
className={`
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
lg:sticky lg:top-[50px] lg:h-[calc(100vh-73px)] lg:w-[250px] lg:translate-x-0 lg:z-8
fixed top-0 left-0 h-[calc(100vh-50px)] w-[270px] z-40 pt-5
${isOpen ? "translate-x-0" : "-translate-x-full"}
`}
@@ -57,7 +57,7 @@ export default function SidebarMentor({ isOpen, closeSidebar }: SidebarProps) {
<button
onClick={closeSidebar}
className="p-1 text-gray-500 hover:bg-gray-100 rounded-md md:hidden"
className="p-1 text-gray-500 hover:bg-gray-100 rounded-lg lg:hidden"
>
<X className="w-5 h-5" />
</button>
+2 -2
View File
@@ -82,10 +82,10 @@ export async function generateCertificate(
//Durasi Project — besar (Grenze)
ctx.font = `400 48px "Grenze", serif`;
ctx.fillStyle = "#090909";
ctx.fillText("20 April - 20 Juni 2025", canvas.width * 0.68, canvas.height * 0.66);
ctx.fillText(ProjectDuration, canvas.width * 0.68, canvas.height * 0.66);
// QR code — kalau verifyUrl diisi
const qrDataUrl = await QRCode.toDataURL('http://localhost:5172/verify-certificate/' + verifyUrl, {
const qrDataUrl = await QRCode.toDataURL('http://localhost:5173/verify-certificate/' + verifyUrl, {
width: 200,
margin: 1,
});
+2 -2
View File
@@ -268,10 +268,10 @@ export const useCompleteProject = () => {
} finally {
setLoading(false);
}
};
return { complete, loading, error };
}
}
};
// POST /project-api/assign-member
export const useAssignMember = () => {
+8 -15
View File
@@ -82,20 +82,19 @@ dialog.custom-dialog {
bottom: 0;
left: 0;
margin: auto;
display: flex;
display: none; /* default tertutup: sembunyikan */
flex-direction: column;
width: calc(100% - 2rem);
max-width: 540px;
max-height: 90vh;
border-radius: 1rem; /* 16px */
border: 1px solid #f3f4f6; /* border-gray-100 */
border-radius: 1rem;
border: 1px solid #f3f4f6;
background-color: #ffffff;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); /* shadow-2xl */
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
/* Animasi (Masuk) */
dialog.custom-dialog[open] {
display: flex; /* hanya tampil saat terbuka */
animation: fadeInZoom 200ms ease-out forwards;
}
@@ -127,14 +126,8 @@ dialog.custom-dialog::backdrop {
border-bottom: 1px 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) {
@media (min-width: 1024px) {
.dialog-header {
/* Perubahan padding untuk layar desktop (md:px-8 md:py-5) */
padding-left: 2rem; /* 32px */
+1 -1
View File
@@ -89,7 +89,7 @@ export const archiveProject = async (
export const completeProject = async (
id: string | number
): Promise<ProjectDetail> => {
const res = await api.post(`/project-api/complete/${id}`);
const res = await api.patch(`/project-api/complete/${id}`);
return res.data.data;
};