responsive and new date time picker
All checks were successful
Build and Release / release (push) Successful in 28s

This commit is contained in:
2026-05-02 16:04:45 +07:00
parent ff70fc78f5
commit 7ec7fda0b4
9 changed files with 462 additions and 159 deletions

View File

@@ -5,6 +5,7 @@ import ComponentCard from "@/components/common/ComponentCard";
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
import Pagination from "@/components/tables/Pagination";
import Swal from "sweetalert2";
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
import ApplicationTable, {
AppSortColumn,
@@ -266,39 +267,15 @@ export default function HistorianApplicationPage() {
</div>
<div>
<label className="block mb-2 text-sm font-medium">T ngày</label>
<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>
<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>
<label className="block mb-2 text-sm font-medium">Thời gian</label>
<CustomDateRangePicker
onFilterChange={(sDate, eDate, sTime, eTime) => {
setFromDate(sDate);
setToDate(eDate);
setFromTime(sTime);
setToTime(eTime);
}}
/>
</div>
<div>

View File

@@ -5,6 +5,7 @@ import ComponentCard from "@/components/common/ComponentCard";
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
import Pagination from "@/components/tables/Pagination";
import { toast } from "sonner";
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
import MediaTable, {
MediaItem,
MediaSortColumn,
@@ -335,38 +336,15 @@ export default function AssetsPage() {
</select>
</div>
<div>
<label className="block mb-2 text-sm font-medium">T ngày</label>
<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>
<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>
<label className="block mb-2 text-sm font-medium">Thời gian</label>
<CustomDateRangePicker
onFilterChange={(sDate, eDate, sTime, eTime) => {
setFromDate(sDate);
setToDate(eDate);
setFromTime(sTime);
setToTime(eTime);
}}
/>
</div>
<div>
<label className="block mb-2 text-sm font-medium">
@@ -437,13 +415,13 @@ export default function AssetsPage() {
{index >= 0 && tableData?.data && tableData.data[index] && (
<div
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
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
onClick={() => setIndex(-1)}
className="absolute -top-12 right-0 md:-right-12 text-white/70 hover:text-white transition-colors p-2"

View File

@@ -21,6 +21,8 @@ import { useRouter } from "next/navigation";
import { LIMIT_ITEM_TABLE } from "../../../../../../constant";
import { ProjectsResponse } from "@/interface/project";
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
const formatDateTimeToISO = (
dateStr: string,
timeStr: string,
@@ -41,7 +43,6 @@ export default function ProjectsPage(_props: {
LIMIT_ITEM_TABLE.toString(),
);
// Filters state
const [searchTerm, setSearchTerm] = useState<string>("");
const [statusFilter, setStatusFilter] = useState<string>("");
const [userIdsFilter, setUserIdsFilter] = useState<string>("");
@@ -50,6 +51,8 @@ export default function ProjectsPage(_props: {
const [toDate, setToDate] = useState<string>("");
const [toTime, setToTime] = useState<string>("");
const [resetKey, setResetKey] = useState<number>(0);
const [debouncedParams, setDebouncedParams] = useState({
search: "",
limit: LIMIT_ITEM_TABLE,
@@ -77,6 +80,14 @@ export default function ProjectsPage(_props: {
setToDate("");
setToTime("");
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(() => {
@@ -174,23 +185,38 @@ 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">
<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" />
<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="PUBLIC">PUBLIC</option>
<option value="PRIVATE">PRIVATE</option>
<option value="ARCHIVE">ARCHIVE</option>
</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>
<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" />
</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" />
<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>
<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">
<option value="">Tất cả trạng thái</option>
<option value="PUBLIC">PUBLIC</option>
<option value="PRIVATE">PRIVATE</option>
<option value="ARCHIVE">ARCHIVE</option>
</select>
</div>
<div>
<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>
<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" />
</div>
<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>
</ComponentCard>
@@ -209,7 +235,6 @@ export default function ProjectsPage(_props: {
onSort={handleSort}
sortBy={sortBy}
sortOrder={sortOrder}
onViewDetails={handleViewDetails}
/>
</div>

View File

@@ -15,6 +15,7 @@ import {
import { useEffect, useState, useCallback } from "react";
import Pagination from "@/components/tables/Pagination";
import { LIMIT_ITEM_TABLE } from "../../../../../../constant";
import CustomDateRangePicker from "@/components/common/CustomDateRangePicker";
export type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
@@ -325,42 +326,16 @@ export default function UserTable() {
</select>
</div>
{/* Từ ngày */}
<div>
<label className="block mb-2 text-sm font-medium">T ngày</label>
<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>
{/* Đế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>
<label className="block mb-2 text-sm font-medium">Thời gian</label>
<CustomDateRangePicker
onFilterChange={(sDate, eDate, sTime, eTime) => {
setFromDate(sDate);
setToDate(eDate);
setFromTime(sTime);
setToTime(eTime);
}}
/>
</div>
{/* Limit */}

View File

@@ -44,7 +44,7 @@ export default function AdminLayout({
<Backdrop />
{/* Main Content Area */}
<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 */}
<AppHeader />

View File

@@ -89,7 +89,7 @@ export default function SignInForm() {
return (
<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
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"
@@ -97,7 +97,7 @@ export default function SignInForm() {
<ChevronLeftIcon />
Back to dashboard
</Link>
</div>
</div> */}
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
<div>
<div className="mb-5 sm:mb-8">

View File

@@ -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="w-full max-w-md sm:pt-10 mx-auto mb-5">
<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"
>
<ChevronLeftIcon />
Back to dashboard
Back to Signin
</Link>
</div>
@@ -284,7 +284,7 @@ export default function SignUpForm() {
<div
className={
formData.password.length > 0 &&
!isValidPassword(formData.password)
!isValidPassword(formData.password)
? "opacity-50 cursor-not-allowed"
: ""
}

View 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>
);
}

View File

@@ -45,7 +45,7 @@ export default function ProjectsTable({
const formatDate = (dateString: string | null | undefined) => {
if (!dateString) return "-";
const date = new Date(dateString);
return `Updated on ${date.toLocaleDateString("vi-VN", {
return `${date.toLocaleDateString("vi-VN", {
day: "2-digit",
month: "short",
year: "numeric",
@@ -92,11 +92,10 @@ export default function ProjectsTable({
return (
<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"
}`}
className={`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>
@@ -104,18 +103,32 @@ export default function ProjectsTable({
};
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="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">
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-[#0d1117]">
<div className="max-w-full overflow-x-auto custom-scrollbar">
<div className="min-w-[700px]">
<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]">
<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:
</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" />
</div>
<div className="w-40 shrink-0">
<SortButton column="updated_at" label="Cập nhật" />
</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 className="flex flex-col divide-y divide-gray-200 dark:divide-gray-800">
@@ -123,12 +136,13 @@ export default function ProjectsTable({
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"
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
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">
{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">
<span className="text-[10px] font-bold text-gray-500 dark:text-gray-300 leading-none">
{item.user?.display_name?.charAt(0)?.toUpperCase() ||
"U"}
{item.user?.display_name?.charAt(0)?.toUpperCase() || "U"}
</span>
</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">
{item.user?.display_name || "Unknown"}
</span>
@@ -159,21 +172,25 @@ export default function ProjectsTable({
/
</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}
</h3>
<div className="shrink-0 w-20 flex justify-start">
<div className="shrink-0 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="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>
</div>
<div className="flex items-center md:w-[120px] md:justify-end shrink-0">
<div className="flex -space-x-2 overflow-hidden">
{item.members && item.members.length > 0 ? (
<>
@@ -217,6 +234,7 @@ export default function ProjectsTable({
)}
</div>
</div>
</div>
))
) : (
@@ -225,6 +243,8 @@ export default function ProjectsTable({
</div>
)}
</div>
</div>
</div>
</div>
);
}