Skip to content

Commit

Permalink
feat(GamesTable): Add row filter
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyCoffee committed Apr 2, 2024
1 parent 9d37be4 commit 622f12d
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 9 deletions.
91 changes: 91 additions & 0 deletions src/pages/overview/FilterFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
DeepKeys,
Row,
RowData,
Table,
TableFeature,
} from "@tanstack/react-table"

type FilterState = string | null
interface FilterTableState {
filter: FilterState
}

interface FilterInstance {
setFilter: (filter: FilterState) => void
clearFilter: () => void
isFiltering: () => boolean
}

interface FilterRow {
isHidden: () => boolean
}

/* eslint-disable @typescript-eslint/no-empty-interface, @typescript-eslint/no-unused-vars */
declare module "@tanstack/react-table" {
interface TableState extends FilterTableState {}
interface Table<TData extends RowData> extends FilterInstance {}
interface Row<TData extends RowData> extends FilterRow {}
}
/* eslint-enable */

const normalizeString = (text: string) =>
text.replace(/[^a-zA-Z0-9-]/g, "").toLowerCase()

const find = (data: unknown[], filter: string) => {
const text = JSON.stringify(data)
return normalizeString(text).includes(normalizeString(filter))
}

const getDeepData = (row: Row<unknown>, keys: string[]) =>
keys.map(key =>
key.split(".").reduce((acc, key) => {
if (acc == null) return ""
switch (typeof acc) {
case "object":
return (acc as Record<string, unknown>)[key]
case "string":
case "number":
case "boolean":
return acc.toString()
default:
return ""
}
}, row.original)
)

export const FilterFeature = <TData extends RowData>(
filterKeys: DeepKeys<TData>[]
): TableFeature<RowData> => ({
getInitialState: (state): FilterTableState => {
return {
filter: null,
...state,
}
},

createTable: <TData extends RowData>(table: Table<TData>) => {
table.setFilter = filter => {
table.setState(old => ({ ...old, filter }))
}
table.clearFilter = () => {
table.setFilter(null)
}
table.isFiltering = () => {
const { filter } = table.getState()
return Boolean(filter)
}
},

createRow: <TData extends RowData>(row: Row<TData>, table: Table<TData>) => {
row.isHidden = () => {
const { filter } = table.getState()
if (filter == null) return false
const filterData = getDeepData(
row as Row<unknown>,
filterKeys as string[]
)
return !find(filterData, filter)
}
},
})
4 changes: 3 additions & 1 deletion src/pages/overview/GamesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Game } from "~/data/games"
import { usePlayers } from "~/data/players"
import { textColor } from "~/utils/colors"

import { FilterFeature } from "./FilterFeature"
import { GamesTableBody } from "./GamesTableBody"
import { GamesTableFooter } from "./GamesTableFooter"
import { GamesTableHeader } from "./GamesTableHeader"
Expand Down Expand Up @@ -112,6 +113,7 @@ export const GamesTable = ({ data, onEdit, onDelete }: GamesTableProps) => {
const columns = useColumns()

const table = useReactTable<Game>({
_features: [FilterFeature<Game>(["name", "date", "player.name"])],
data,
columns,
defaultColumn: {
Expand All @@ -130,7 +132,7 @@ export const GamesTable = ({ data, onEdit, onDelete }: GamesTableProps) => {
<NativeTable.Root>
<GamesTableHeader table={table} />
<GamesTableBody table={table} onEdit={onEdit} onDelete={onDelete} />
<GamesTableFooter games={data} />
<GamesTableFooter table={table} games={data} />
</NativeTable.Root>
)
}
5 changes: 4 additions & 1 deletion src/pages/overview/GamesTableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ export const GamesTableBody = ({
return (
<NativeTable.Body>
{table.getRowModel().rows.map(row => (
<NativeTable.Row key={row.id}>
<NativeTable.Row
key={row.id}
className={cn(row.isHidden() && "hidden")}
>
{row.getVisibleCells().map(cell => (
<DataCell key={cell.id} cell={cell} />
))}
Expand Down
13 changes: 12 additions & 1 deletion src/pages/overview/GamesTableFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Table as TableInstance } from "@tanstack/react-table"
import { Fragment } from "react/jsx-runtime"

import { Swatch } from "~/components/Swatch"
Expand Down Expand Up @@ -31,10 +32,20 @@ const getGamesByPlayers = (games: Game[]) =>
}
}, {})

export const GamesTableFooter = ({ games }: { games: Game[] }) => {
export const GamesTableFooter = ({
games,
table,
}: {
games: Game[]
table: TableInstance<Game>
}) => {
const playerStats = useGamePlayerStats()
const gamesByPlayers = getGamesByPlayers(games)

if (table.isFiltering()) {
return null
}

return (
<Table.Footer>
<Table.Row>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/overview/GamesTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const GamesTableHeader = ({ table }: { table: Table<Game> }) => {
))}

{headers.every(({ subHeaders }) => subHeaders.length === 0) ? (
<TableHeaderActions />
<TableHeaderActions table={table} />
) : (
<NativeTable.Head className="h-0" />
)}
Expand Down
50 changes: 45 additions & 5 deletions src/pages/overview/TableActions.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Dispatch } from "react"
import { Dispatch, useState } from "react"

import { Row } from "@tanstack/react-table"
import { PenBox, Trash } from "lucide-react"
import { Row, Table } from "@tanstack/react-table"
import { Filter, PenBox, Trash } from "lucide-react"

import { IconButton } from "~/components/IconButton"
import { Input } from "~/components/ui/input"
import { Table as NativeTable } from "~/components/ui/table"
import { Game } from "~/data/games"
import { cn } from "~/utils/utils"
Expand Down Expand Up @@ -43,13 +44,52 @@ export const TableRowActions = ({
</NativeTable.Cell>
)

export const TableHeaderActions = () => (
const TableFilter = ({ table }: { table: Table<Game> }) => {
const [filtering, setFiltering] = useState(false)

const toggle = () => {
setFiltering(prev => !prev)
table.clearFilter()
}

return (
<>
{filtering && (
<div className="absolute right-[calc(100%-theme(width.2))] flex">
<div className="inline-block h-10 w-2 shrink-0 bg-gradient-to-r from-transparent to-background" />
<div className="bg-background">
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
className="mx-2 w-40 shadow-medium"
placeholder="Filter"
value={table.getState().filter ?? ""}
onChange={({ target }) => table.setFilter(target.value)}
onKeyDown={({ key }) => key === "Escape" && toggle()}
/>
</div>
</div>
)}
<IconButton
filled={filtering}
icon={Filter}
onClick={toggle}
title="Toggle filter"
/>
</>
)
}

export const TableHeaderActions = ({ table }: { table: Table<Game> }) => (
<NativeTable.Head
className={cn("sticky right-0 overflow-visible bg-transparent px-0 py-1")}
className={cn(
"sticky right-0 z-20 overflow-visible bg-transparent px-0 py-1"
)}
>
<div className="ml-auto flex w-max items-center">
<div className="inline-block h-10 w-2 shrink-0 bg-gradient-to-r from-transparent to-background" />
<div className="inline-flex justify-end bg-background pr-1">
<TableFilter table={table} />
<AddGame />
</div>
</div>
Expand Down

0 comments on commit 622f12d

Please sign in to comment.