responsive and new date time picker
All checks were successful
Build and Release / release (push) Successful in 28s
All checks were successful
Build and Release / release (push) Successful in 28s
This commit is contained in:
@@ -5,6 +5,7 @@ import ComponentCard from "@/components/common/ComponentCard";
|
|||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||||
import Pagination from "@/components/tables/Pagination";
|
import Pagination from "@/components/tables/Pagination";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
|
||||||
|
|
||||||
import ApplicationTable, {
|
import ApplicationTable, {
|
||||||
AppSortColumn,
|
AppSortColumn,
|
||||||
@@ -266,39 +267,15 @@ export default function HistorianApplicationPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block mb-2 text-sm font-medium">Từ ngày</label>
|
<label className="block mb-2 text-sm font-medium">Thời gian</label>
|
||||||
<div className="flex gap-2">
|
<CustomDateRangePicker
|
||||||
<input
|
onFilterChange={(sDate, eDate, sTime, eTime) => {
|
||||||
type="date"
|
setFromDate(sDate);
|
||||||
value={fromDate}
|
setToDate(eDate);
|
||||||
onChange={(e) => setFromDate(e.target.value)}
|
setFromTime(sTime);
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
setToTime(eTime);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={fromTime}
|
|
||||||
onChange={(e) => setFromTime(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 text-sm font-medium">Đến ngày</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={toDate}
|
|
||||||
onChange={(e) => setToDate(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={toTime}
|
|
||||||
onChange={(e) => setToTime(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ComponentCard from "@/components/common/ComponentCard";
|
|||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
||||||
import Pagination from "@/components/tables/Pagination";
|
import Pagination from "@/components/tables/Pagination";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
|
||||||
import MediaTable, {
|
import MediaTable, {
|
||||||
MediaItem,
|
MediaItem,
|
||||||
MediaSortColumn,
|
MediaSortColumn,
|
||||||
@@ -335,38 +336,15 @@ export default function AssetsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block mb-2 text-sm font-medium">Từ ngày</label>
|
<label className="block mb-2 text-sm font-medium">Thời gian</label>
|
||||||
<div className="flex gap-2">
|
<CustomDateRangePicker
|
||||||
<input
|
onFilterChange={(sDate, eDate, sTime, eTime) => {
|
||||||
type="date"
|
setFromDate(sDate);
|
||||||
value={fromDate}
|
setToDate(eDate);
|
||||||
onChange={(e) => setFromDate(e.target.value)}
|
setFromTime(sTime);
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
setToTime(eTime);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={fromTime}
|
|
||||||
onChange={(e) => setFromTime(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 text-sm font-medium">Đến ngày</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={toDate}
|
|
||||||
onChange={(e) => setToDate(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={toTime}
|
|
||||||
onChange={(e) => setToTime(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block mb-2 text-sm font-medium">
|
<label className="block mb-2 text-sm font-medium">
|
||||||
@@ -437,13 +415,13 @@ export default function AssetsPage() {
|
|||||||
{index >= 0 && tableData?.data && tableData.data[index] && (
|
{index >= 0 && tableData?.data && tableData.data[index] && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm transition-opacity"
|
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm transition-opacity"
|
||||||
onClick={() => setIndex(-1)} // Click ra vùng tối sẽ đóng
|
onClick={() => setIndex(-1)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative flex flex-col items-center justify-center p-4 max-w-[90vw] max-h-[90vh]"
|
className="relative flex flex-col items-center justify-center p-4 max-w-[90vw] max-h-[90vh]"
|
||||||
onClick={(e) => e.stopPropagation()} // Bấm vào ảnh không bị đóng
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Nút X đóng Modal */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIndex(-1)}
|
onClick={() => setIndex(-1)}
|
||||||
className="absolute -top-12 right-0 md:-right-12 text-white/70 hover:text-white transition-colors p-2"
|
className="absolute -top-12 right-0 md:-right-12 text-white/70 hover:text-white transition-colors p-2"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { useRouter } from "next/navigation";
|
|||||||
import { LIMIT_ITEM_TABLE } from "../../../../../../constant";
|
import { LIMIT_ITEM_TABLE } from "../../../../../../constant";
|
||||||
import { ProjectsResponse } from "@/interface/project";
|
import { ProjectsResponse } from "@/interface/project";
|
||||||
|
|
||||||
|
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
|
||||||
|
|
||||||
const formatDateTimeToISO = (
|
const formatDateTimeToISO = (
|
||||||
dateStr: string,
|
dateStr: string,
|
||||||
timeStr: string,
|
timeStr: string,
|
||||||
@@ -41,7 +43,6 @@ export default function ProjectsPage(_props: {
|
|||||||
LIMIT_ITEM_TABLE.toString(),
|
LIMIT_ITEM_TABLE.toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filters state
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [statusFilter, setStatusFilter] = useState<string>("");
|
const [statusFilter, setStatusFilter] = useState<string>("");
|
||||||
const [userIdsFilter, setUserIdsFilter] = useState<string>("");
|
const [userIdsFilter, setUserIdsFilter] = useState<string>("");
|
||||||
@@ -50,6 +51,8 @@ export default function ProjectsPage(_props: {
|
|||||||
const [toDate, setToDate] = useState<string>("");
|
const [toDate, setToDate] = useState<string>("");
|
||||||
const [toTime, setToTime] = useState<string>("");
|
const [toTime, setToTime] = useState<string>("");
|
||||||
|
|
||||||
|
const [resetKey, setResetKey] = useState<number>(0);
|
||||||
|
|
||||||
const [debouncedParams, setDebouncedParams] = useState({
|
const [debouncedParams, setDebouncedParams] = useState({
|
||||||
search: "",
|
search: "",
|
||||||
limit: LIMIT_ITEM_TABLE,
|
limit: LIMIT_ITEM_TABLE,
|
||||||
@@ -77,6 +80,14 @@ export default function ProjectsPage(_props: {
|
|||||||
setToDate("");
|
setToDate("");
|
||||||
setToTime("");
|
setToTime("");
|
||||||
setPage(1);
|
setPage(1);
|
||||||
|
setResetKey(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateFilterChange = (startD: string, endD: string, startT: string, endT: string) => {
|
||||||
|
setFromDate(startD);
|
||||||
|
setToDate(endD);
|
||||||
|
setFromTime(startT);
|
||||||
|
setToTime(endT);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -174,24 +185,39 @@ export default function ProjectsPage(_props: {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 text-sm font-medium">Tìm kiếm</label>
|
||||||
<input type="text" placeholder="Tên dự án, ID..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
<input type="text" placeholder="Tên dự án, ID..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 text-sm font-medium">Trạng thái</label>
|
||||||
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="w-full px-3 py-2 bg-white dark:bg-gray-900 border rounded-lg cursor-pointer outline-none focus:border-brand-500">
|
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="w-full px-3 py-2 bg-white dark:bg-gray-900 border rounded-lg cursor-pointer outline-none focus:border-brand-500">
|
||||||
<option value="">Tất cả trạng thái</option>
|
<option value="">Tất cả trạng thái</option>
|
||||||
<option value="PUBLIC">PUBLIC</option>
|
<option value="PUBLIC">PUBLIC</option>
|
||||||
<option value="PRIVATE">PRIVATE</option>
|
<option value="PRIVATE">PRIVATE</option>
|
||||||
<option value="ARCHIVE">ARCHIVE</option>
|
<option value="ARCHIVE">ARCHIVE</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" placeholder="IDs người dùng (cách nhau bởi dấu phẩy)" value={userIdsFilter} onChange={(e) => setUserIdsFilter(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input type="date" value={fromDate} onChange={(e) => setFromDate(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
|
||||||
<input type="time" value={fromTime} onChange={(e) => setFromTime(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
|
||||||
<input type="date" value={toDate} onChange={(e) => setToDate(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
<div>
|
||||||
<input type="time" value={toTime} onChange={(e) => setToTime(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
<label className="block mb-2 text-sm font-medium">ID người dùng</label>
|
||||||
|
<input type="text" placeholder="IDs (cách nhau bởi dấu phẩy)" value={userIdsFilter} onChange={(e) => setUserIdsFilter(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 text-sm font-medium">Thời gian</label>
|
||||||
|
<CustomDateRangePicker
|
||||||
|
key={resetKey}
|
||||||
|
onFilterChange={handleDateFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 text-sm font-medium">Hiển thị (Limit)</label>
|
||||||
<input type="number" value={limitInput} onChange={(e) => setLimitInput(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
<input type="number" value={limitInput} onChange={(e) => setLimitInput(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ComponentCard>
|
</ComponentCard>
|
||||||
|
|
||||||
<ComponentCard
|
<ComponentCard
|
||||||
@@ -209,7 +235,6 @@ export default function ProjectsPage(_props: {
|
|||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
|
|
||||||
onViewDetails={handleViewDetails}
|
onViewDetails={handleViewDetails}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import Pagination from "@/components/tables/Pagination";
|
import Pagination from "@/components/tables/Pagination";
|
||||||
import { LIMIT_ITEM_TABLE } from "../../../../../../constant";
|
import { LIMIT_ITEM_TABLE } from "../../../../../../constant";
|
||||||
|
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
|
||||||
|
|
||||||
export type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
|
export type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
|
||||||
|
|
||||||
@@ -325,42 +326,16 @@ export default function UserTable() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Từ ngày */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block mb-2 text-sm font-medium">Từ ngày</label>
|
<label className="block mb-2 text-sm font-medium">Thời gian</label>
|
||||||
<div className="flex gap-2">
|
<CustomDateRangePicker
|
||||||
<input
|
onFilterChange={(sDate, eDate, sTime, eTime) => {
|
||||||
type="date"
|
setFromDate(sDate);
|
||||||
value={fromDate}
|
setToDate(eDate);
|
||||||
onChange={(e) => setFromDate(e.target.value)}
|
setFromTime(sTime);
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
setToTime(eTime);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={fromTime}
|
|
||||||
onChange={(e) => setFromTime(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Đến ngày */}
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 text-sm font-medium">Đến ngày</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={toDate}
|
|
||||||
onChange={(e) => setToDate(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={toTime}
|
|
||||||
onChange={(e) => setToTime(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Limit */}
|
{/* Limit */}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function AdminLayout({
|
|||||||
<Backdrop />
|
<Backdrop />
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`}
|
className={`flex-1 min-w-0 transition-all duration-300 ease-in-out ${mainContentMargin}`}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function SignInForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 lg:w-1/2 w-full">
|
<div className="flex flex-col flex-1 lg:w-1/2 w-full">
|
||||||
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
{/* <div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||||
@@ -97,7 +97,7 @@ export default function SignInForm() {
|
|||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
Back to dashboard
|
Back to dashboard
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div> */}
|
||||||
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
|
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-5 sm:mb-8">
|
<div className="mb-5 sm:mb-8">
|
||||||
|
|||||||
@@ -131,11 +131,11 @@ export default function SignUpForm() {
|
|||||||
<div className="flex flex-col flex-1 lg:w-1/2 w-full overflow-y-auto no-scrollbar">
|
<div className="flex flex-col flex-1 lg:w-1/2 w-full overflow-y-auto no-scrollbar">
|
||||||
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/signin"
|
||||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
Back to dashboard
|
Back to Signin
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
328
src/components/common/CustomDateRangePicker.tsx
Normal file
328
src/components/common/CustomDateRangePicker.tsx
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
interface CustomDateRangePickerProps {
|
||||||
|
onFilterChange: (
|
||||||
|
startDate: string,
|
||||||
|
endDate: string,
|
||||||
|
startTime: string,
|
||||||
|
endTime: string
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DAYS_OF_WEEK = ["Th2", "Th3", "Th4", "Th5", "Th6", "Th7", "CN"];
|
||||||
|
const MONTHS = [
|
||||||
|
"Tháng 1", "Tháng 2", "Tháng 3", "Tháng 4", "Tháng 5", "Tháng 6",
|
||||||
|
"Tháng 7", "Tháng 8", "Tháng 9", "Tháng 10", "Tháng 11", "Tháng 12"
|
||||||
|
];
|
||||||
|
|
||||||
|
const getStartOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
|
const formatDateToString = (date: Date | null) => {
|
||||||
|
if (!date) return "";
|
||||||
|
const d = date.getDate().toString().padStart(2, "0");
|
||||||
|
const m = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const y = date.getFullYear();
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CustomDateRangePicker({ onFilterChange }: CustomDateRangePickerProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [currentMonthDate, setCurrentMonthDate] = useState(getStartOfDay(new Date()));
|
||||||
|
|
||||||
|
// LOGIC MỚI: Tách biệt rõ ràng state Từ ngày và Đến ngày
|
||||||
|
const [startTimestamp, setStartTimestamp] = useState<number | null>(null);
|
||||||
|
const [endTimestamp, setEndTimestamp] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// History dùng để theo dõi thứ tự click, phục vụ "Cửa sổ trượt" giữ 2 ngày gần nhất
|
||||||
|
const [history, setHistory] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const [startTime, setStartTime] = useState<string>("00:00");
|
||||||
|
const [endTime, setEndTime] = useState<string>("23:59");
|
||||||
|
|
||||||
|
const startDate = startTimestamp ? new Date(startTimestamp) : null;
|
||||||
|
const endDate = endTimestamp ? new Date(endTimestamp) : null;
|
||||||
|
|
||||||
|
// Click outside to close
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Update parent
|
||||||
|
useEffect(() => {
|
||||||
|
const sDate = startDate ? formatDateToString(startDate) : "";
|
||||||
|
const eDate = endDate ? formatDateToString(endDate) : "";
|
||||||
|
const sTime = startDate ? startTime : "";
|
||||||
|
const eTime = endDate ? endTime : "";
|
||||||
|
onFilterChange(sDate, eDate, sTime, eTime);
|
||||||
|
}, [startTimestamp, endTimestamp, startTime, endTime]);
|
||||||
|
|
||||||
|
const { daysInMonth, emptyDays } = useMemo(() => {
|
||||||
|
const year = currentMonthDate.getFullYear();
|
||||||
|
const month = currentMonthDate.getMonth();
|
||||||
|
const days = new Date(year, month + 1, 0).getDate();
|
||||||
|
let firstDayIndex = new Date(year, month, 1).getDay() - 1;
|
||||||
|
if (firstDayIndex < 0) firstDayIndex = 6;
|
||||||
|
return { daysInMonth: days, emptyDays: firstDayIndex };
|
||||||
|
}, [currentMonthDate]);
|
||||||
|
|
||||||
|
const handleDayClick = (day: number) => {
|
||||||
|
const clickedTime = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), day).getTime();
|
||||||
|
|
||||||
|
if (clickedTime === startTimestamp) {
|
||||||
|
setStartTimestamp(null);
|
||||||
|
setHistory(prev => prev.filter(t => t !== clickedTime));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (clickedTime === endTimestamp) {
|
||||||
|
setEndTimestamp(null);
|
||||||
|
setHistory(prev => prev.filter(t => t !== clickedTime));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newStart = startTimestamp;
|
||||||
|
let newEnd = endTimestamp;
|
||||||
|
let newHistory = [...history];
|
||||||
|
|
||||||
|
if (newStart && newEnd) {
|
||||||
|
const oldest = newHistory[0];
|
||||||
|
if (oldest === newStart) newStart = null;
|
||||||
|
else if (oldest === newEnd) newEnd = null;
|
||||||
|
newHistory.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newStart && !newEnd) {
|
||||||
|
newEnd = clickedTime;
|
||||||
|
}
|
||||||
|
else if (!newStart && newEnd) {
|
||||||
|
if (clickedTime < newEnd) {
|
||||||
|
newStart = clickedTime;
|
||||||
|
} else {
|
||||||
|
newStart = newEnd;
|
||||||
|
newEnd = clickedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (newStart && !newEnd) {
|
||||||
|
if (clickedTime > newStart) {
|
||||||
|
newEnd = clickedTime;
|
||||||
|
} else {
|
||||||
|
newEnd = newStart;
|
||||||
|
newStart = clickedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newHistory.push(clickedTime);
|
||||||
|
setStartTimestamp(newStart);
|
||||||
|
setEndTimestamp(newEnd);
|
||||||
|
setHistory(newHistory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManualDateChange = (val: string, target: 'start' | 'end') => {
|
||||||
|
const newTime = val ? new Date(val).getTime() : null;
|
||||||
|
|
||||||
|
if (target === 'start') {
|
||||||
|
const oldTime = startTimestamp;
|
||||||
|
setStartTimestamp(newTime);
|
||||||
|
setHistory(prev => {
|
||||||
|
let h = prev;
|
||||||
|
if (oldTime) h = h.filter(t => t !== oldTime);
|
||||||
|
if (newTime) h = [...h, newTime];
|
||||||
|
return h.slice(-2);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const oldTime = endTimestamp;
|
||||||
|
setEndTimestamp(newTime);
|
||||||
|
setHistory(prev => {
|
||||||
|
let h = prev;
|
||||||
|
if (oldTime) h = h.filter(t => t !== oldTime);
|
||||||
|
if (newTime) h = [...h, newTime];
|
||||||
|
return h.slice(-2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextMonth = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth() + 1, 1));
|
||||||
|
const prevMonth = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth() - 1, 1));
|
||||||
|
const nextYear = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear() + 1, currentMonthDate.getMonth(), 1));
|
||||||
|
const prevYear = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear() - 1, currentMonthDate.getMonth(), 1));
|
||||||
|
|
||||||
|
const renderDateBox = (date: Date | null) => {
|
||||||
|
if (!date) return "mm/dd/yyyy";
|
||||||
|
const d = date.getDate().toString().padStart(2, "0");
|
||||||
|
const m = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const y = date.getFullYear();
|
||||||
|
return `${m}/${d}/${y}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelected = (day: number) => {
|
||||||
|
const d = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), day).getTime();
|
||||||
|
return d === startTimestamp || d === endTimestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInRange = (day: number) => {
|
||||||
|
const d = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), day).getTime();
|
||||||
|
if (startTimestamp && endTimestamp && d > startTimestamp && d < endTimestamp) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full" ref={containerRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="flex items-center justify-between w-full px-4 py-2 text-sm border rounded-lg bg-white dark:bg-gray-800 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-500 transition-all outline-none text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{(!startDate && !endDate)
|
||||||
|
? "mm/dd/yyyy - mm/dd/yyyy"
|
||||||
|
: `${renderDateBox(startDate)} - ${renderDateBox(endDate)}`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-gray-500" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute top-12 left-0 z-50 w-[320px] p-4 bg-[#22272e] border border-gray-700 rounded-xl shadow-2xl text-gray-200">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span className="font-semibold mx-auto">Ngày</span>
|
||||||
|
<button onClick={() => setIsOpen(false)} className="absolute right-4 text-gray-400 hover:text-white">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mb-4 text-sm">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={prevYear} className="p-1 hover:bg-gray-700 rounded text-gray-400 hover:text-white">«</button>
|
||||||
|
<button onClick={prevMonth} className="p-1 hover:bg-gray-700 rounded text-gray-400 hover:text-white">‹</button>
|
||||||
|
</div>
|
||||||
|
<span className="font-medium">
|
||||||
|
{MONTHS[currentMonthDate.getMonth()]} {currentMonthDate.getFullYear()}
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={nextMonth} className="p-1 hover:bg-gray-700 rounded text-gray-400 hover:text-white">›</button>
|
||||||
|
<button onClick={nextYear} className="p-1 hover:bg-gray-700 rounded text-gray-400 hover:text-white">»</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-y-2 mb-6 text-center text-sm">
|
||||||
|
{DAYS_OF_WEEK.map(day => (
|
||||||
|
<div key={day} className="font-medium text-gray-400">{day}</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{Array.from({ length: emptyDays }).map((_, i) => (
|
||||||
|
<div key={`empty-${i}`} className="text-gray-600"></div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{Array.from({ length: daysInMonth }).map((_, i) => {
|
||||||
|
const day = i + 1;
|
||||||
|
const selected = isSelected(day);
|
||||||
|
const inRange = isInRange(day);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={day}
|
||||||
|
onClick={() => handleDayClick(day)}
|
||||||
|
className={`w-8 h-8 mx-auto flex items-center justify-center rounded-md transition-colors
|
||||||
|
${selected ? 'bg-blue-600 text-white font-semibold' : ''}
|
||||||
|
${inRange && !selected ? 'bg-blue-900/50 text-blue-200' : ''}
|
||||||
|
${!selected && !inRange ? 'hover:bg-gray-700 text-gray-300' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-gray-700 my-4" />
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-400 mb-2">Từ ngày</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!startTimestamp}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.target.checked) {
|
||||||
|
setStartTimestamp(null);
|
||||||
|
setHistory(prev => prev.filter(t => t !== startTimestamp));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-4 h-4 rounded border-gray-600 bg-transparent text-blue-500 focus:ring-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
disabled={!startTimestamp}
|
||||||
|
value={startDate ? formatDateToString(startDate) : ""}
|
||||||
|
onChange={(e) => handleManualDateChange(e.target.value, 'start')}
|
||||||
|
className="flex-1 px-3 py-1.5 bg-[#2d333b] border border-gray-600 rounded-md text-sm outline-none focus:border-blue-500 disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
disabled={!startTimestamp}
|
||||||
|
value={startTime}
|
||||||
|
onChange={(e) => setStartTime(e.target.value)}
|
||||||
|
className="w-[100px] px-3 py-1.5 bg-[#2d333b] border border-gray-600 rounded-md text-sm outline-none focus:border-blue-500 disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="00:00">00:00</option>
|
||||||
|
<option value="08:00">08:00</option>
|
||||||
|
<option value="12:00">12:00</option>
|
||||||
|
<option value="21:22">21:22</option>
|
||||||
|
<option value="23:59">23:59</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-400 mb-2">Đến ngày</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!endTimestamp}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.target.checked) {
|
||||||
|
setEndTimestamp(null);
|
||||||
|
setHistory(prev => prev.filter(t => t !== endTimestamp));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-4 h-4 rounded border-gray-600 bg-transparent text-blue-500 focus:ring-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
disabled={!endTimestamp}
|
||||||
|
value={endDate ? formatDateToString(endDate) : ""}
|
||||||
|
onChange={(e) => handleManualDateChange(e.target.value, 'end')}
|
||||||
|
className="flex-1 px-3 py-1.5 bg-[#2d333b] border border-gray-600 rounded-md text-sm outline-none focus:border-blue-500 disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
disabled={!endTimestamp}
|
||||||
|
value={endTime}
|
||||||
|
onChange={(e) => setEndTime(e.target.value)}
|
||||||
|
className="w-[100px] px-3 py-1.5 bg-[#2d333b] border border-gray-600 rounded-md text-sm outline-none focus:border-blue-500 disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="00:00">00:00</option>
|
||||||
|
<option value="08:00">08:00</option>
|
||||||
|
<option value="12:00">12:00</option>
|
||||||
|
<option value="21:22">21:22</option>
|
||||||
|
<option value="23:59">23:59</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ export default function ProjectsTable({
|
|||||||
const formatDate = (dateString: string | null | undefined) => {
|
const formatDate = (dateString: string | null | undefined) => {
|
||||||
if (!dateString) return "-";
|
if (!dateString) return "-";
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return `Updated on ${date.toLocaleDateString("vi-VN", {
|
return `${date.toLocaleDateString("vi-VN", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -92,8 +92,7 @@ export default function ProjectsTable({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => onSort(column)}
|
onClick={() => onSort(column)}
|
||||||
className={`w-20 text-sm font-medium text-left hover:text-blue-500 transition-colors ${
|
className={`text-sm font-medium text-left hover:text-blue-500 transition-colors ${isActive
|
||||||
isActive
|
|
||||||
? "text-blue-600 dark:text-blue-400"
|
? "text-blue-600 dark:text-blue-400"
|
||||||
: "text-gray-500 dark:text-gray-400"
|
: "text-gray-500 dark:text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -104,18 +103,32 @@ export default function ProjectsTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-[#0d1117] min-w-[700px]">
|
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-[#0d1117]">
|
||||||
<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]">
|
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
||||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300 w-40">
|
<div className="min-w-[700px]">
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-4 shrink-0">
|
<div className="flex items-center px-5 py-3 border-b border-gray-200 dark:border-gray-800 bg-gray-50/50 dark:bg-[#161b22]">
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400 w-20">
|
|
||||||
|
<div className="flex-1 pr-4 flex items-center gap-3">
|
||||||
|
<span className="text-sm font-medium text-gray-400 dark:text-gray-500">
|
||||||
Sắp xếp:
|
Sắp xếp:
|
||||||
</span>
|
</span>
|
||||||
<SortButton column="title" label="Tên" />
|
<SortButton column="title" label="Tên dự án" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-40 shrink-0">
|
||||||
<SortButton column="created_at" label="Ngày tạo" />
|
<SortButton column="created_at" label="Ngày tạo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-40 shrink-0">
|
||||||
<SortButton column="updated_at" label="Cập nhật" />
|
<SortButton column="updated_at" label="Cập nhật" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-[120px] shrink-0 text-right">
|
||||||
|
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Thành viên
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
|
<div className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
@@ -123,12 +136,13 @@ export default function ProjectsTable({
|
|||||||
data.map((item) => (
|
data.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
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"
|
className="group flex flex-col p-5 md:flex-row md:items-center gap-3 md:gap-0 hover:bg-gray-50 dark:hover:bg-[#161b22] transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex-1 pr-4 max-w-full md:max-w-[75%]">
|
|
||||||
|
<div className="flex-1 pr-4 min-w-0">
|
||||||
<div
|
<div
|
||||||
onClick={() => onViewDetails(item.id)}
|
onClick={() => onViewDetails(item.id)}
|
||||||
className="flex items-center gap-2 mb-2 cursor-pointer hover:underline"
|
className="flex items-center gap-2 cursor-pointer hover:underline w-full"
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6 shrink-0 flex items-center justify-center">
|
<div className="w-6 h-6 shrink-0 flex items-center justify-center">
|
||||||
{item.user?.avatar_url ? (
|
{item.user?.avatar_url ? (
|
||||||
@@ -143,13 +157,12 @@ export default function ProjectsTable({
|
|||||||
) : (
|
) : (
|
||||||
<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">
|
<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">
|
<span className="text-[10px] font-bold text-gray-500 dark:text-gray-300 leading-none">
|
||||||
{item.user?.display_name?.charAt(0)?.toUpperCase() ||
|
{item.user?.display_name?.charAt(0)?.toUpperCase() || "U"}
|
||||||
"U"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center max-w-[250px]">
|
<div className="flex items-center max-w-[200px]">
|
||||||
<span className="text-[14px] font-medium text-gray-700 dark:text-gray-300 truncate">
|
<span className="text-[14px] font-medium text-gray-700 dark:text-gray-300 truncate">
|
||||||
{item.user?.display_name || "Unknown"}
|
{item.user?.display_name || "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
@@ -159,21 +172,25 @@ export default function ProjectsTable({
|
|||||||
/
|
/
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h3 className="text-[14px] font-semibold text-blue-600 dark:text-[#58a6ff] truncate max-w-[300px]">
|
<h3 className="text-[14px] font-semibold text-blue-600 dark:text-[#58a6ff] truncate max-w-[250px]">
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="shrink-0 w-20 flex justify-start">
|
<div className="shrink-0 flex justify-start">
|
||||||
{getStatusBadge(item.project_status)}
|
{getStatusBadge(item.project_status)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-500 dark:text-[#8b949e] h-5">
|
<div className="md:w-40 shrink-0 text-xs text-gray-500 dark:text-[#8b949e]">
|
||||||
|
<span>{formatDate(item.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:w-40 shrink-0 text-xs text-gray-500 dark:text-[#8b949e]">
|
||||||
<span>{formatDate(item.updated_at)}</span>
|
<span>{formatDate(item.updated_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center mt-4 md:mt-0 w-[120px] justify-end shrink-0">
|
<div className="flex items-center md:w-[120px] md:justify-end shrink-0">
|
||||||
<div className="flex -space-x-2 overflow-hidden">
|
<div className="flex -space-x-2 overflow-hidden">
|
||||||
{item.members && item.members.length > 0 ? (
|
{item.members && item.members.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
@@ -217,6 +234,7 @@ export default function ProjectsTable({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@@ -226,5 +244,7 @@ export default function ProjectsTable({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user