Skip to content

Commit

Permalink
table improvement (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremy-babylonlabs authored Dec 17, 2024
1 parent 3b06de3 commit 9fcfef8
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 141 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-clouds-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@babylonlabs-io/bbn-core-ui": patch
---

table improvement
173 changes: 63 additions & 110 deletions src/components/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { useState, useCallback } from "react";

import { Table } from "./";
import { ColumnProps, Table } from "./";
import { Avatar } from "../Avatar";

const meta: Meta<typeof Table> = {
Expand Down Expand Up @@ -37,7 +37,7 @@ const data: FinalityProvider[] = [
id: "2",
name: "Solv Protocol",
icon: "/images/fps/solv.jpeg",
status: "Active",
status: "Inactive",
btcPk: "1234...4321",
totalDelegation: 20,
commission: 3,
Expand All @@ -53,130 +53,83 @@ const data: FinalityProvider[] = [
},
];

export const Default: Story = {
render: () => {
const [tableData, setTableData] = useState(data.slice(0, 3));
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);

const handleLoadMore = async () => {
setLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1000));

const nextItems = data.slice(tableData.length, tableData.length + 3);
setTableData((prev) => [...prev, ...nextItems]);
setHasMore(tableData.length + nextItems.length < data.length);
setLoading(false);
};

const handleRowSelect = (row: FinalityProvider) => {
console.log(row);
};

return (
<div className="h-[150px]">
<Table
data={tableData}
hasMore={hasMore}
loading={loading}
onLoadMore={handleLoadMore}
onRowSelect={handleRowSelect}
columns={[
{
key: "name",
header: "Finality Provider",
render: (_, row) => (
<div className="flex items-center gap-2">
<Avatar size="small" url={row.icon} alt={row.name} />
<span className="text-primary-light">{row.name}</span>
</div>
),
sorter: (a, b) => a.name.localeCompare(b.name),
},
{
key: "status",
header: "Status",
},
{
key: "btcPk",
header: "BTC PK",
},
{
key: "totalDelegation",
header: "Total Delegation",
render: (value) => `${value} sBTC`,
sorter: (a, b) => a.totalDelegation - b.totalDelegation,
},
{
key: "commission",
header: "Commission",
render: (value) => `${value}%`,
sorter: (a, b) => a.commission - b.commission,
},
]}
/>
const columns: ColumnProps<FinalityProvider>[] = [
{
key: "name",
header: "Finality Provider",
render: (_: unknown, row: FinalityProvider) => (
<div className="flex items-center gap-2">
<Avatar size="small" url={row.icon} alt={row.name} />
<span>{row.name}</span>
</div>
);
),
sorter: (a, b) => a.name.localeCompare(b.name),
},
};
{
key: "status",
header: "Status",
},
{
key: "btcPk",
header: "BTC PK",
},
{
key: "totalDelegation",
header: "Total Delegation",
render: (_: unknown, row: FinalityProvider) => `${row.totalDelegation} sBTC`,
sorter: (a, b) => a.totalDelegation - b.totalDelegation,
},
{
key: "commission",
header: "Commission",
render: (_: unknown, row: FinalityProvider) => `${row.commission}%`,
sorter: (a, b) => a.commission - b.commission,
},
];

export const WithoutRowSelect: Story = {
export const Default: Story = {
render: () => {
const [tableData, setTableData] = useState(data.slice(0, 3));
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [selectedProvider, setSelectedProvider] = useState<FinalityProvider | null>(null);

const handleLoadMore = async () => {
const handleLoadMore = useCallback(async () => {
setLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1000));

const nextItems = data.slice(tableData.length, tableData.length + 3);
setTableData((prev) => [...prev, ...nextItems]);
setHasMore(tableData.length + nextItems.length < data.length);
setLoading(false);
};
}, [tableData]);

const handleRowSelect = useCallback((row: FinalityProvider | null) => {
setSelectedProvider(row);
}, []);

const isRowSelectable = useCallback((row: FinalityProvider) => {
return row.status === "Active";
}, []);

return (
<div className="h-[150px]">
<Table
data={tableData}
hasMore={hasMore}
loading={loading}
onLoadMore={handleLoadMore}
columns={[
{
key: "name",
header: "Finality Provider",
render: (_, row) => (
<div className="flex items-center gap-2">
<Avatar size="small" url={row.icon} alt={row.name} />
<span className="text-primary-light">{row.name}</span>
</div>
),
sorter: (a, b) => a.name.localeCompare(b.name),
},
{
key: "status",
header: "Status",
},
{
key: "btcPk",
header: "BTC PK",
},
{
key: "totalDelegation",
header: "Total Delegation",
render: (value) => `${value} sBTC`,
sorter: (a, b) => a.totalDelegation - b.totalDelegation,
},
{
key: "commission",
header: "Commission",
render: (value) => `${value}%`,
sorter: (a, b) => a.commission - b.commission,
},
]}
/>
<div className="space-y-4">
<div className="h-[150px]">
<Table
data={tableData}
hasMore={hasMore}
loading={loading}
onLoadMore={handleLoadMore}
columns={columns}
onRowSelect={handleRowSelect}
isRowSelectable={isRowSelectable}
/>
</div>
{selectedProvider && (
<div className="rounded bg-primary-contrast p-4">
Selected Provider: {selectedProvider.name} (Commission: {selectedProvider.commission}%)
</div>
)}
</div>
);
},
Expand Down
81 changes: 51 additions & 30 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useMemo, useState } from "react";
import { useRef, useMemo, useState, useCallback } from "react";
import { twJoin } from "tailwind-merge";
import { useTableScroll } from "@/hooks/useTableScroll";
import { TableContext, TableContextType } from "../../context/Table.context";
Expand All @@ -15,34 +15,48 @@ export function Table<T extends { id: string | number }>({
loading = false,
onLoadMore,
onRowSelect,
isRowSelectable,
...restProps
}: TableProps<T>) {
const tableRef = useRef<HTMLDivElement>(null);
const [hoveredColumn, setHoveredColumn] = useState<string | undefined>(undefined);
const [sortStates, setSortStates] = useState<{
[key: string]: { direction: "asc" | "desc" | null; priority: number };
}>({});
const [selectedRow, setSelectedRow] = useState<string | number | undefined>(undefined);
const [selectedRow, setSelectedRow] = useState<string | number | null>(null);

const { isScrolledTop } = useTableScroll(tableRef, {
onLoadMore,
hasMore,
loading,
});

const handleHoveredColumn = (column: string) => {
if (hoveredColumn === column) return;
setHoveredColumn(column);
};
const handleHoveredColumn = useCallback(
(column: string) => {
if (hoveredColumn !== column) {
setHoveredColumn(column);
}
},
[hoveredColumn],
);

const handleRowSelect = (row: T) => {
if (!onRowSelect) return;
if (selectedRow === row.id) return;
setSelectedRow(row.id);
onRowSelect(row);
};
const handleRowSelect = useCallback(
(row: T) => {
if (!onRowSelect) return;
if (isRowSelectable && !isRowSelectable(row)) return;
if (selectedRow === row.id) {
setSelectedRow(null);
onRowSelect(null);
return;
}

setSelectedRow(row.id);
onRowSelect(row);
},
[onRowSelect, isRowSelectable, selectedRow],
);

const handleColumnSort = (columnKey: string, sorter?: (a: T, b: T) => number) => {
const handleColumnSort = useCallback((columnKey: string, sorter?: (a: T, b: T) => number) => {
if (!sorter) return;

setSortStates((prev) => {
Expand Down Expand Up @@ -73,7 +87,7 @@ export function Table<T extends { id: string | number }>({
},
};
});
};
}, []);

const sortedData = useMemo(() => {
const activeSorters = Object.entries(sortStates)
Expand Down Expand Up @@ -125,22 +139,29 @@ export function Table<T extends { id: string | number }>({
</tr>
</thead>
<tbody className="bbn-table-body">
{sortedData.map((row) => (
<tr
key={row.id}
className={twJoin(selectedRow === row.id && "selected", onRowSelect && "cursor-pointer")}
onClick={() => handleRowSelect(row)}
>
{columns.map((column) => (
<Cell
key={column.key}
value={row[column.key as keyof T]}
columnName={column.key}
render={column.render ? (value) => column.render!(value, row) : undefined}
/>
))}
</tr>
))}
{sortedData.map((row) => {
const isSelectable = isRowSelectable ? isRowSelectable(row) : true;
return (
<tr
key={row.id}
className={twJoin(
selectedRow === row.id && "selected",
onRowSelect && isSelectable && "cursor-pointer",
!isSelectable && "opacity-50",
)}
onClick={() => handleRowSelect(row)}
>
{columns.map((column) => (
<Cell
key={column.key}
value={row[column.key as keyof T]}
columnName={column.key}
render={column.render ? (value) => column.render!(value, row) : undefined}
/>
))}
</tr>
);
})}
</tbody>
</table>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Table/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export interface TableProps<T extends { id: string | number }> {
hasMore?: boolean;
loading?: boolean;
onLoadMore?: () => void;
onRowSelect?: (row: T) => void;
onRowSelect?: (row: T | null) => void;
isRowSelectable?: (row: T) => boolean;
}

0 comments on commit 9fcfef8

Please sign in to comment.