Skip to main content

Table List Component Pattern

This document describes the standardized pattern for creating table/list components in the FlowPOS frontend-pwa application.

Overview

All table/list components should follow a consistent pattern that includes:

  • Sorting functionality with visual indicators
  • Responsive design (full width on large screens, mobile cards)
  • Consistent styling (padding, hover effects, sticky actions column)
  • Reusable sorting hook for state management

Pattern Components

1. Sorting Hook (useTableSort)

The useTableSort hook manages sorting state and provides utilities for sorting data.

Location: src/hooks/useTableSort.ts

Usage in Parent Component:

import { useTableSort } from "@/hooks/useTableSort";

export default function MyPage() {
const { sortField, sortDirection, handleSort } = useTableSort();

// ... rest of component
}

Props to pass to List component:

  • sortField={sortField}
  • sortDirection={sortDirection}
  • onSort={handleSort}

2. List Component Structure

Required Props Interface

export interface MyListProps {
items: ItemType[];
sortField: string;
sortDirection: "asc" | "desc";
onSort: (field: string) => void;
// ... other props (onEdit, onDelete, etc.)
}

Required Imports

import {
ArrowDown,
ArrowUp,
ArrowUpDown,
Search as SearchIcon,
X,
} from "lucide-react";
import { getSortedData, type SortConfig } from "@/hooks/useTableSort";

Component Structure

  1. Sort Icon Function:
const getSortIcon = (field: string) => {
if (sortField !== field) {
return <ArrowUpDown className="w-4 h-4 opacity-50" />;
}
return sortDirection === "asc" ? (
<ArrowUp className="w-4 h-4" />
) : (
<ArrowDown className="w-4 h-4" />
);
};
  1. Sort Configuration:
const sortConfig: SortConfig<ItemType> = {
name: (item) => item.name || "",
email: (item) => item.email || "",
// Add all sortable fields
};

const sortedItems = getSortedData(items, sortConfig, sortField, sortDirection);
  1. Container Styling:
return (
<div className="w-full space-y-4">
{/* Header with search and create button */}
{/* Table/Cards */}
</div>
);
  1. Table Header with Sort Buttons:
<th className="px-4 py-3 text-left text-sm font-semibold">
<button
type="button"
className="flex items-center gap-2 hover:bg-muted/80 transition-colors rounded px-1 py-1 -mx-1 -my-1"
onClick={() => onSort("fieldName")}
aria-label={`Sort by ${t("translation.key")}`}
>
{t("translation.key")}
{getSortIcon("fieldName")}
</button>
</th>
  1. Table Body Styling:
<tr className="hover:bg-muted/50 transition">
<td className="px-4 py-3 text-sm text-foreground">
{/* Content */}
</td>
</tr>
  1. Sticky Actions Column:
<th className="px-4 py-3 text-right text-sm font-semibold sticky right-0 bg-muted z-10 shadow-[-2px_0_4px_rgba(0,0,0,0.1)] no-print-col">
{t("common.actions")}
</th>

<td className="px-4 py-3 text-sm sticky right-0 bg-card shadow-[-2px_0_4px_rgba(0,0,0,0.1)] no-print-col">
{/* Action buttons */}
</td>
  1. Empty State:
{sortedItems.length === 0 ? (
<div className="bg-card rounded-lg border border-border">
<div className="px-4 py-8 text-center text-muted-foreground">
{t("noItems")}
</div>
</div>
) : (
// Table/Cards
)}
  1. Mobile Cards:
<div className="md:hidden flex flex-col gap-4 p-4">
{sortedItems.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
{t("noItems")}
</div>
) : (
sortedItems.map((item) => (
<div
key={item.id}
className="border border-border rounded-xl p-4 shadow-sm bg-card text-card-foreground"
>
{/* Card content */}
</div>
))
)}
</div>

3. Parent Page Component

Required State

// Sorting state
const [sortField, setSortField] = useState<string>("");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");

Sorting Handler

const handleSort = (field: string) => {
if (sortField === field) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
} else {
setSortField(field);
setSortDirection("asc");
}
};

Container Styling

return (
<div className="w-full max-w-full px-4 py-6 md:px-6 lg:px-8">
{/* Form or List component */}
</div>
);

Styling Standards

Table Styling

  • Header padding: px-4 py-3
  • Cell padding: px-4 py-3
  • Text size: text-sm
  • Hover effect: hover:bg-muted/50
  • Header background: bg-muted text-muted-foreground
  • Border: divide-y divide-border

Responsive Design

  • Desktop table: hidden md:table
  • Mobile cards: md:hidden
  • Container: w-full for full width on large screens
  • Responsive padding: px-4 py-6 md:px-6 lg:px-8

Actions Column

  • Sticky positioning: sticky right-0
  • Background: bg-muted (header), bg-card (cells)
  • Shadow: shadow-[-2px_0_4px_rgba(0,0,0,0.1)]
  • Z-index: z-10 (header only)

Example: Complete Implementation

See the following components for reference implementations:

  • CustomerList.tsx - Full example with multiple sortable columns
  • SupplierList.tsx - Similar pattern
  • ProductList.tsx - Complex example with many columns
  • BrandList.tsx - Simple example with single column
  • ModelList.tsx - Two-column example

Checklist for New List Components

  • Import sorting icons from lucide-react
  • Import getSortedData and SortConfig from @/hooks/useTableSort
  • Add sortField, sortDirection, onSort to props interface
  • Implement getSortIcon function
  • Create sortConfig object with all sortable fields
  • Use getSortedData to get sorted items
  • Add sort buttons to all sortable column headers
  • Apply consistent styling (py-3, hover:bg-muted/50, etc.)
  • Add sticky actions column with proper styling
  • Implement responsive mobile cards
  • Update parent page with sorting state and handler
  • Make container full width (w-full max-w-full)
  • Test sorting on all columns
  • Test responsive behavior

Migration Guide

To migrate an existing list component:

  1. Add sorting props to component interface
  2. Import sorting utilities and icons
  3. Add sorting state to parent component
  4. Implement sort handler in parent
  5. Add sort icon function to list component
  6. Create sort config and use getSortedData
  7. Update table headers with sort buttons
  8. Apply styling standards (padding, hover, sticky column)
  9. Update container to full width
  10. Test thoroughly

Notes

  • Sorting is client-side only (sorts the current page of data)
  • The pattern uses TypeScript for type safety
  • All components should be accessible (aria-labels on sort buttons)
  • Print styles are handled with no-print-col class
  • Mobile cards should show all important information