import { useMemo, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import type { SortState } from '../types/sort'; import { Chip } from './Chip'; import { Table, type TableHeader } from './Table'; type UserRow = { id: string; name: string; role: 'ADMIN' | 'EDITOR' | 'AUTHOR'; status: 'Active' | 'Pending'; posts: number; }; const rows: UserRow[] = [ { id: '1', name: 'Beatrice Rosa', role: 'ADMIN', status: 'Active', posts: 48 }, { id: '2', name: 'Luca Valli', role: 'EDITOR', status: 'Active', posts: 26 }, { id: '3', name: 'Marta Bellini', role: 'AUTHOR', status: 'Pending', posts: 4 }, { id: '4', name: 'Giulia Fontana', role: 'AUTHOR', status: 'Active', posts: 12 }, { id: '5', name: 'Andrea Pini', role: 'EDITOR', status: 'Pending', posts: 9 }, { id: '6', name: 'Sofia Denti', role: 'AUTHOR', status: 'Active', posts: 7 }, { id: '7', name: 'Marco Serra', role: 'AUTHOR', status: 'Active', posts: 18 }, { id: '8', name: 'Elena Neri', role: 'EDITOR', status: 'Active', posts: 31 }, ]; const headers: TableHeader[] = [ { id: 'name', label: 'Name', value: (row) => row.name, sortable: true, sortField: 'name', cellClassName: 'table-cell-primary', }, { id: 'role', label: 'Role', value: (row) => row.role, sortable: true, sortField: 'role', }, { id: 'status', label: 'Status', value: (row) => ( {row.status} ), }, { id: 'posts', label: 'Posts', value: (row) => row.posts, sortable: true, sortField: 'posts', }, ]; type UsersTableProps = { data: UserRow[]; isLoading?: boolean; emptyMessage?: string; sorting?: SortState | null; onSortChange?: (field: string) => void; pagination?: { page: number; pageSize: number; total: number; totalPages: number; onPageChange: (page: number) => void; onPageSizeChange?: (pageSize: number) => void; }; }; function UsersTable(props: Readonly) { return ( headers={headers} data={props.data} rowKey={(row) => row.id} isLoading={props.isLoading} emptyMessage={props.emptyMessage} sorting={props.sorting} onSortChange={props.onSortChange} pagination={props.pagination} /> ); } function sortRows(data: UserRow[], sorting: SortState | null): UserRow[] { if (!sorting) { return data; } const sorted = [...data]; sorted.sort((a, b) => { const left = a[sorting.field as keyof UserRow]; const right = b[sorting.field as keyof UserRow]; if (left === right) { return 0; } if (typeof left === 'number' && typeof right === 'number') { return sorting.direction === 'asc' ? left - right : right - left; } return sorting.direction === 'asc' ? String(left).localeCompare(String(right)) : String(right).localeCompare(String(left)); }); return sorted; } const meta = { title: 'Components/Table', component: UsersTable, tags: ['autodocs'], parameters: { docs: { description: { component: 'Generic data table with loading/empty states, optional sorting controls, and optional pagination footer.', }, }, }, argTypes: { data: { description: 'Rows rendered in the table body.', control: 'object', table: { type: { summary: 'UserRow[]' } }, }, isLoading: { description: 'When true, shows the loading indicator row.', control: 'boolean', table: { type: { summary: 'boolean' } }, }, emptyMessage: { description: 'Message shown when `data` is empty and `isLoading` is false.', control: 'text', table: { type: { summary: 'string' } }, }, sorting: { description: 'Current sort state object. Use `null` for no active sorting.', control: 'object', table: { type: { summary: "{ field: string; direction: 'asc' | 'desc' } | null" } }, }, onSortChange: { description: 'Callback fired when a sortable header is clicked.', action: 'sort changed', table: { type: { summary: '(field: string) => void' } }, }, pagination: { description: 'Pagination config object. When omitted, pagination footer is hidden.', control: 'object', table: { type: { summary: '{ page; pageSize; total; totalPages; onPageChange; onPageSizeChange? }', }, }, }, }, args: { data: rows, }, } satisfies Meta; export default meta; type Story = StoryObj; export const WithRows: Story = {}; export const Loading: Story = { args: { isLoading: true, }, }; export const Empty: Story = { args: { data: [], emptyMessage: 'No users found', }, }; export const InteractiveSortingAndPagination: Story = { render: () => { const [sorting, setSorting] = useState({ field: 'name', direction: 'asc', }); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(5); const sorted = useMemo(() => sortRows(rows, sorting), [sorting]); const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize)); const safePage = Math.min(page, totalPages); const start = (safePage - 1) * pageSize; const pagedRows = sorted.slice(start, start + pageSize); return ( { setPage(1); setSorting((prev) => { if (!prev || prev.field !== field) { return { field, direction: 'asc' }; } if (prev.direction === 'asc') { return { field, direction: 'desc' }; } return null; }); }} pagination={{ page: safePage, pageSize, total: sorted.length, totalPages, onPageChange: setPage, onPageSizeChange: (next) => { setPage(1); setPageSize(next); }, }} /> ); }, };