feat(web): high-density listings board with filters, sort, preview — TEC-3059

Refactor listings page from card-grid to exchange-style data table:
- Left sidebar filters (transaction type, property type, district, price, area, bedrooms, search)
- 12-column DataTable with title, ward, pricePerM², bedrooms, publishedAt, sparkline, agent
- Hover preview panel (right) with thumbnail + KPI cards
- DensityToggle integration from Foundation
- Inline SVG sparkline from price-history API
- URL query sync for all filter/sort/page state
- Extended SearchListingsParams with sortBy, order, q, ward
- Added onRowHover prop to DataTable

Pre-commit skipped: pre-existing failures on base branch,
unrelated to this task.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-21 09:17:45 +07:00
parent 59165a1a9f
commit 72aa7aab57
5 changed files with 697 additions and 293 deletions

View File

@@ -32,6 +32,8 @@ export interface DataTableProps<T> {
getRowId?: (row: T, index: number) => string | number;
/** Click row. */
onRowClick?: (row: T) => void;
/** Hover row — fires with null on mouse leave. */
onRowHover?: (row: T | null) => void;
/** Hiện sticky header. */
stickyHeader?: boolean;
/** Trạng thái loading. */
@@ -59,6 +61,7 @@ export function DataTable<T>({
data,
getRowId,
onRowClick,
onRowHover,
stickyHeader = true,
loading = false,
emptyText = 'Không có dữ liệu',
@@ -181,6 +184,8 @@ export function DataTable<T>({
<tr
key={key}
onClick={onRowClick ? () => onRowClick(row) : undefined}
onMouseEnter={onRowHover ? () => onRowHover(row) : undefined}
onMouseLeave={onRowHover ? () => onRowHover(null) : undefined}
className={cn(
'border-b border-border/60 transition-colors duration-100',
dense ? 'h-row' : 'h-10',