Files
solution-erp/fe-user/src/components/DataTable.tsx
pqhuy1987 346bd5d644
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m54s
[CLAUDE] FE: content polish — typography + PageHeader + Button/Input/Table
Typography base (both apps):
- 14px base, line-height 1.55, letter-spacing -0.003em — Be Vietnam
  Pro render chắc hơn, dấu tiếng Việt rõ hơn
- h1/h2 tracking tighter (-0.018em), h1 weight 700, h2 weight 600
- Tabular numbers trong <table> (tự động align cột số)

PageHeader:
- Title text-[22px] thay vì text-xl — hierarchy mạnh hơn
- Border-bottom + pb-5 thay flat layout — content-area rõ vùng
- Description leading-relaxed + slate-500 — dễ đọc hơn

Button:
- shadow-sm + color-tinted shadow (brand/20, red/20) cho primary/
  danger — có chiều sâu
- active:translate-y-[0.5px] micro-press feedback
- Ring offset 2 (thay 1) + offset-white — focus ring tách rõ

Input/Select/Textarea:
- h-9 thay h-10 — phù hợp dense table layouts
- shadow-[inset_0_1px_0_...] — inset highlight tinh tế
- Focus: border-brand-500 + ring-brand-500/20 — 2 lớp chỉ báo
- Disabled: bg-slate-50 + opacity-70 — rõ disabled state

DataTable:
- rounded-xl + shadow-sm + border-200/80 — card feel nhẹ nhàng hơn
- Header: UPPERCASE text-[11px] tracking-wider — ERP enterprise look
- Row hover: bg-brand-50/40 (thay slate-50) — brand-tinted hover
- Padding tăng từ px-3 py-2 → px-4 py-2.5 — breathing room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:16:53 +07:00

155 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ReactNode } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/cn'
export type Column<T> = {
key: string
header: ReactNode
render: (row: T) => ReactNode
sortable?: boolean
width?: string
align?: 'left' | 'center' | 'right'
}
type Props<T> = {
columns: Column<T>[]
rows: T[]
getRowKey: (row: T) => string
isLoading?: boolean
empty?: ReactNode
sortBy?: string
sortDesc?: boolean
onSortChange?: (sortBy: string, sortDesc: boolean) => void
onRowClick?: (row: T) => void
}
export function DataTable<T>({
columns,
rows,
getRowKey,
isLoading,
empty,
sortBy,
sortDesc,
onSortChange,
onRowClick,
}: Props<T>) {
return (
<div className="overflow-auto rounded-xl border border-slate-200/80 bg-white shadow-sm">
<table className="w-full text-sm">
<thead className="bg-slate-50/60 text-slate-600">
<tr className="border-b border-slate-200/80">
{columns.map(c => (
<th
key={c.key}
className={cn(
'px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wider',
c.align === 'right' && 'text-right',
c.align === 'center' && 'text-center',
c.align !== 'right' && c.align !== 'center' && 'text-left',
c.width,
)}
>
{c.sortable && onSortChange ? (
<button
onClick={() => onSortChange(c.key, sortBy === c.key ? !sortDesc : false)}
className="inline-flex items-center gap-1 hover:text-slate-900"
>
{c.header}
{sortBy === c.key && (sortDesc ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronUp className="h-3.5 w-3.5" />)}
</button>
) : (
c.header
)}
</th>
))}
</tr>
</thead>
<tbody>
{isLoading &&
Array.from({ length: 5 }).map((_, i) => (
<tr key={`sk-${i}`} className="border-t border-slate-100">
{columns.map(c => (
<td key={c.key} className="px-4 py-3">
<div className="h-3 animate-pulse rounded bg-slate-100" />
</td>
))}
</tr>
))}
{!isLoading && rows.length === 0 && (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center text-sm text-slate-400">
{empty ?? 'Không có dữ liệu'}
</td>
</tr>
)}
{!isLoading &&
rows.map(row => (
<tr
key={getRowKey(row)}
className={cn(
'border-t border-slate-100 text-slate-700 transition-colors',
onRowClick && 'cursor-pointer hover:bg-brand-50/40',
)}
onClick={() => onRowClick?.(row)}
>
{columns.map(c => (
<td
key={c.key}
className={cn(
'px-4 py-2.5',
c.align === 'right' && 'text-right',
c.align === 'center' && 'text-center',
)}
>
{c.render(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
type PaginationProps = {
page: number
pageSize: number
total: number
onChange: (page: number) => void
}
export function Pagination({ page, pageSize, total, onChange }: PaginationProps) {
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const from = total === 0 ? 0 : (page - 1) * pageSize + 1
const to = Math.min(page * pageSize, total)
return (
<div className="flex items-center justify-between py-3 text-sm text-slate-600">
<span>
{from}{to} / {total}
</span>
<div className="flex gap-1">
<button
disabled={page <= 1}
onClick={() => onChange(page - 1)}
className="rounded-md border border-slate-300 bg-white px-3 py-1 disabled:opacity-50"
>
Trước
</button>
<span className="px-3 py-1">
Trang {page}/{totalPages}
</span>
<button
disabled={page >= totalPages}
onClick={() => onChange(page + 1)}
className="rounded-md border border-slate-300 bg-white px-3 py-1 disabled:opacity-50"
>
Sau
</button>
</div>
</div>
)
}