update: projectsmanage
All checks were successful
Build and Release / release (push) Successful in 29s

This commit is contained in:
2026-04-29 12:08:09 +07:00
parent d65a71ba1d
commit a2bab73e50
5 changed files with 812 additions and 233 deletions

View File

@@ -1,12 +1,6 @@
"use client";
import {
Table,
TableBody,
TableCell,
TableHeader,
TableRow,
} from "../ui/table";
import Image from "next/image";
import Badge from "../ui/badge/Badge";
export type ProjectSortColumn = "created_at" | "updated_at" | "title";
@@ -19,12 +13,18 @@ export interface ProjectItem {
owner_id: string;
created_at: string;
updated_at: string;
user:{
user: {
id: string;
display_name: string;
email:string;
avatar_url:string;
}
email: string;
avatar_url: string;
};
members?: {
user_id: string;
role: string;
display_name: string;
avatar_url: string;
}[];
}
interface ProjectsTableProps {
@@ -32,10 +32,7 @@ interface ProjectsTableProps {
onSort: (column: ProjectSortColumn) => void;
sortBy?: ProjectSortColumn;
sortOrder?: "asc" | "desc";
onUpdate: (item: ProjectItem) => void;
onDelete: (id: string) => void;
onTransferOwner: (item: ProjectItem) => void;
onViewDetails: (id: string) => void; // Để xem commits, members...
onViewDetails: (id: string) => void;
}
export default function ProjectsTable({
@@ -43,21 +40,16 @@ export default function ProjectsTable({
onSort,
sortBy,
sortOrder,
onUpdate,
onDelete,
onTransferOwner,
onViewDetails,
}: ProjectsTableProps) {
const formatDate = (dateString: string | null | undefined) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleDateString("vi-VN", {
return `Updated on ${date.toLocaleDateString("vi-VN", {
day: "2-digit",
month: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
})}`;
};
const getStatusBadge = (status: ProjectItem["project_status"]) => {
@@ -89,101 +81,150 @@ export default function ProjectsTable({
}
};
const SortIcon = ({ column }: { column: ProjectSortColumn }) => {
const SortButton = ({
column,
label,
}: {
column: ProjectSortColumn;
label: string;
}) => {
const isActive = sortBy === column;
return (
<div className="flex flex-col ml-2 opacity-50 cursor-pointer hover:opacity-100">
<svg
className={`w-3 h-3 ${isActive && sortOrder === "asc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 15l7-7 7 7" />
</svg>
<svg
className={`w-3 h-3 -mt-1 ${isActive && sortOrder === "desc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M19 9l-7 7-7-7" />
</svg>
</div>
<button
onClick={() => onSort(column)}
className={`w-20 text-sm font-medium text-left hover:text-blue-500 transition-colors ${
isActive
? "text-blue-600 dark:text-blue-400"
: "text-gray-500 dark:text-gray-400"
}`}
>
{label} {isActive && (sortOrder === "asc" ? "↑" : "↓")}
</button>
);
};
return (
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03]">
<div className="max-w-full overflow-x-auto">
<div className="min-w-[1000px]">
<Table>
<TableHeader className="border-b border-gray-100 dark:border-white/[0.05]">
<TableRow>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("title")}>
Tiêu đ <SortIcon column="title" />
</div>
</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">Trạng thái</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">Chủ sở hữu</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("created_at")}>
Ngày tạo <SortIcon column="created_at" />
</div>
</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
<div className="flex items-center cursor-pointer select-none" onClick={() => onSort("updated_at")}>
Cập nhật <SortIcon column="updated_at" />
</div>
</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-center text-gray-500 text-theme-xs dark:text-gray-400 min-w-[200px]">Thao tác</TableCell>
</TableRow>
</TableHeader>
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05]">
{data.length > 0 ? (
data.map((item) => (
<TableRow key={item.id} className="hover:bg-gray-50/50 dark:hover:bg-white/[0.01] transition-colors">
<TableCell className="px-5 py-4 text-start font-medium text-theme-sm truncate max-w-[250px]">{item.title}</TableCell>
<TableCell className="px-5 py-4 text-start">{getStatusBadge(item.project_status)}</TableCell>
<TableCell className="px-5 py-4 text-start text-theme-sm">{item.user?.display_name}</TableCell>
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">{formatDate(item.created_at)}</TableCell>
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">{formatDate(item.updated_at)}</TableCell>
<TableCell className="px-5 py-4 text-center">
<div className="flex items-center justify-center gap-3">
<button onClick={() => onViewDetails(item.id)} title="Chi tiết" className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button onClick={() => onUpdate(item)} title="Cập nhật" className="text-blue-500 hover:text-blue-700 dark:hover:text-blue-400">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button onClick={() => onTransferOwner(item)} title="Chuyển quyền sở hữu" className="text-green-500 hover:text-green-700 dark:hover:text-green-400">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</button>
<button onClick={() => onDelete(item.id)} title="Xóa" className="text-red-500 hover:text-red-700 dark:hover:text-red-400">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="px-5 py-20 text-center text-gray-500 italic">
Không tìm thấy dự án nào
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-[#0d1117] min-w-[700px]">
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200 dark:border-gray-800 bg-gray-50/50 dark:bg-[#161b22]">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300 w-40">
</span>
<div className="flex items-center gap-4 shrink-0">
<span className="text-sm text-gray-500 dark:text-gray-400 w-20">
Sắp xếp:
</span>
<SortButton column="title" label="Tên" />
<SortButton column="created_at" label="Ngày tạo" />
<SortButton column="updated_at" label="Cập nhật" />
</div>
</div>
<div className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
{data.length > 0 ? (
data.map((item) => (
<div
key={item.id}
className="group flex flex-col p-5 md:flex-row md:items-center justify-between hover:bg-gray-50 dark:hover:bg-[#161b22] transition-colors"
>
<div className="flex-1 pr-4 max-w-full md:max-w-[75%]">
<div
onClick={() => onViewDetails(item.id)}
className="flex items-center gap-2 mb-2 cursor-pointer hover:underline"
>
<div className="w-6 h-6 shrink-0 flex items-center justify-center">
{item.user?.avatar_url ? (
<div className="relative w-6 h-6 rounded-full overflow-hidden border border-gray-200 dark:border-gray-800">
<Image
src={item.user.avatar_url}
alt="avatar"
fill
className="object-cover rounded-full"
/>
</div>
) : (
<div className="w-6 h-6 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center border border-gray-300 dark:border-gray-600">
<span className="text-[10px] font-bold text-gray-500 dark:text-gray-300 leading-none">
{item.user?.display_name?.charAt(0)?.toUpperCase() ||
"U"}
</span>
</div>
)}
</div>
<div className="flex items-center max-w-[250px]">
<span className="text-lg font-medium text-gray-700 dark:text-gray-300 truncate">
{item.user?.display_name || "Unknown"}
</span>
</div>
<span className="text-lg text-gray-400 dark:text-gray-600 shrink-0">
/
</span>
<h3 className="text-lg font-semibold text-blue-600 dark:text-[#58a6ff] truncate max-w-[300px]">
{item.title}
</h3>
<div className="shrink-0 w-20 flex justify-start">
{getStatusBadge(item.project_status)}
</div>
</div>
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-500 dark:text-[#8b949e] h-5">
<span>{formatDate(item.updated_at)}</span>
</div>
</div>
<div className="flex items-center mt-4 md:mt-0 w-[120px] justify-end shrink-0">
<div className="flex -space-x-2 overflow-hidden">
{item.members && item.members.length > 0 ? (
<>
{item.members.slice(0, 4).map((m, index) =>
m.avatar_url ? (
<Image
key={index}
src={m.avatar_url}
alt={m.display_name}
width={32}
height={32}
title={m.display_name}
className="inline-block w-8 h-8 rounded-full object-cover ring-2 ring-white group-hover:ring-gray-50 dark:ring-[#0d1117] dark:group-hover:ring-[#161b22] transition-colors"
/>
) : (
<div
key={index}
title={m.display_name}
className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 ring-2 ring-white group-hover:ring-gray-50 dark:ring-[#0d1117] dark:group-hover:ring-[#161b22] transition-colors"
>
<span className="text-xs font-medium text-gray-500 dark:text-gray-300">
{m.display_name?.charAt(0)?.toUpperCase() || "U"}
</span>
</div>
),
)}
{item.members.length > 4 && (
<div
title="Những người khác"
className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 ring-2 ring-white group-hover:ring-gray-50 dark:ring-[#0d1117] dark:group-hover:ring-[#161b22] transition-colors z-10"
>
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
+{item.members.length - 4}
</span>
</div>
)}
</>
) : (
<span className="text-sm text-gray-400 dark:text-gray-600 italic"></span>
)}
</div>
</div>
</div>
))
) : (
<div className="p-10 text-center text-gray-500 dark:text-gray-400 italic">
Không tìm thấy dự án nào
</div>
)}
</div>
</div>
);
}
}