signalstats/src/pages/overview/overview-table.tsx
2024-12-12 18:44:44 +01:00

356 lines
9.8 KiB
TypeScript

import { type Component, createSignal, For, Match, Show, Switch } from "solid-js";
import { useNavigate } from "@solidjs/router";
import {
type ColumnFiltersState,
createColumnHelper,
createSolidTable,
type FilterFn,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortDirection,
type SortingState,
} from "@tanstack/solid-table";
import { intlFormatDistance } from "date-fns";
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-solid";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Checkbox } from "~/components/ui/checkbox";
import { Label } from "~/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { TextField, TextFieldInput } from "~/components/ui/text-field";
import { cn } from "~/lib/utils";
export interface RoomOverview {
threadId: number;
recipientId: number;
archived: boolean;
messageCount: number;
lastMessageDate: Date | undefined;
name: string;
isGroup: boolean;
}
const columnHelper = createColumnHelper<RoomOverview>();
const archivedFilterFn: FilterFn<RoomOverview> = (row, _columnId, filterValue) => {
if (filterValue === true) {
return true;
}
return !row.original.archived;
};
const SortingDisplay: Component<{ sorting: false | SortDirection; class?: string; activeClass?: string }> = (props) => {
return (
<Switch>
<Match when={props.sorting === false}>
<ArrowUpDown class={props.class} />
</Match>
<Match when={props.sorting === "asc"}>
<ArrowUp class={cn(props.class, props.activeClass)} />
</Match>
<Match when={props.sorting === "desc"}>
<ArrowDown class={cn(props.class, props.activeClass)} />
</Match>
</Switch>
);
};
export const columns = [
columnHelper.accessor("threadId", {}),
columnHelper.accessor("name", {
header: (props) => {
const sorting = () => props.column.getIsSorted();
return (
<Button
variant="ghost"
onClick={() => {
props.column.toggleSorting();
}}
>
Name
<SortingDisplay
sorting={sorting()}
class="ml-2 h-4 w-4"
activeClass="text-info-foreground"
/>
</Button>
);
},
cell: (props) => {
const isArchived = props.row.getValue("archived");
const isGroup = props.row.getValue("isGroup");
return (
<div class="flex w-full flex-row">
<span class="font-bold">{props.cell.getValue()}</span>
<Show when={isArchived || isGroup}>
<div class="ml-auto flex flex-row gap-2">
<Show when={isArchived}>
<Badge
variant="outline"
class="ml-auto"
>
Archived
</Badge>
</Show>
<Show when={isGroup}>
<Badge
variant="outline"
class="ml-auto"
>
Group
</Badge>
</Show>
</div>
</Show>
</div>
);
},
}),
columnHelper.accessor("messageCount", {
id: "messageCount",
header: (props) => {
const sorting = () => props.column.getIsSorted();
return (
<Button
variant="ghost"
onClick={() => {
props.column.toggleSorting();
}}
>
Number of messages
<SortingDisplay
sorting={sorting()}
class="ml-2 h-4 w-4"
activeClass="text-info-foreground"
/>
</Button>
);
},
sortingFn: "basic",
}),
columnHelper.accessor("lastMessageDate", {
header: (props) => {
const sorting = () => props.column.getIsSorted();
return (
<Button
variant="ghost"
onClick={() => {
props.column.toggleSorting();
}}
>
Date of last message
<SortingDisplay
sorting={sorting()}
class="ml-2 h-4 w-4"
activeClass="text-info-foreground"
/>
</Button>
);
},
sortingFn: "datetime",
cell: (props) => {
const value = props.cell.getValue();
if (value) {
return intlFormatDistance(new Date(value), new Date());
} else {
return "";
}
},
}),
columnHelper.accessor("archived", {
id: "archived",
header: "Archived",
cell: (props) => {
return (
<Show when={props.cell.getValue()}>
<Badge>Archived</Badge>
</Show>
);
},
filterFn: archivedFilterFn,
}),
columnHelper.accessor("isGroup", {
header: "Group",
cell: (props) => {
return (
<Show when={props.cell.getValue()}>
<Badge>Group</Badge>
</Show>
);
},
}),
];
interface OverviewTableProps {
data: RoomOverview[];
}
export const OverviewTable = (props: OverviewTableProps) => {
const [sorting, setSorting] = createSignal<SortingState>([
{
id: "messageCount",
desc: true,
},
]);
const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([
{
id: "archived",
value: false,
},
]);
const table = createSolidTable({
get data() {
return props.data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
state: {
get sorting() {
return sorting();
},
get columnFilters() {
return columnFilters();
},
columnVisibility: {
threadId: false,
archived: false,
isGroup: false,
},
},
initialState: {
pagination: {
pageIndex: 0,
pageSize: 25,
},
},
});
const navigate = useNavigate();
return (
<div>
<div class="flex flex-row items-center gap-x-4">
<div class="flex items-center py-4">
<TextField
value={(table.getColumn("name")?.getFilterValue() as string | undefined) ?? ""}
onChange={(value) => table.getColumn("name")?.setFilterValue(value)}
>
<TextFieldInput
placeholder="Filter by name..."
class="max-w-sm"
/>
</TextField>
</div>
<div class="flex items-start space-x-2">
<Checkbox
id="show-archived"
checked={(table.getColumn("archived")?.getFilterValue() as boolean | undefined) ?? false}
onChange={(value) => table.getColumn("archived")?.setFilterValue(value)}
/>
<div class="grid gap-1.5 leading-none">
<Label for="show-archived">Show archived chats</Label>
</div>
</div>
</div>
<Table class="border-separate border-spacing-0">
<TableHeader>
<For each={table.getHeaderGroups()}>
{(headerGroup) => (
<TableRow>
<For each={headerGroup.headers}>
{(header) => (
<TableHead
class="border-b border-r border-t first-of-type:rounded-tl-md first-of-type:border-l last-of-type:rounded-tr-md"
colSpan={header.colSpan}
>
<Show when={!header.isPlaceholder}>
{flexRender(header.column.columnDef.header, header.getContext())}
</Show>
</TableHead>
)}
</For>
</TableRow>
)}
</For>
</TableHeader>
<TableBody>
<Show
when={table.getRowModel().rows.length}
fallback={
<TableRow>
<TableCell
colSpan={columns.length}
class="h-24 border-b border-r text-center first-of-type:rounded-tl-md first-of-type:border-l last-of-type:rounded-br-md"
>
No results.
</TableCell>
</TableRow>
}
>
<For each={table.getRowModel().rows}>
{(row) => (
<TableRow
class="cursor-pointer [&:last-of-type>td:first-of-type]:rounded-bl-md [&:last-of-type>td:last-of-type]:rounded-br-md"
data-state={row.getIsSelected() && "selected"}
onClick={() => {
const threadId = row.original.threadId;
const isGroup = row.original.isGroup;
navigate(`/${isGroup ? "group" : "dm"}/${threadId.toString()}`);
}}
>
<For each={row.getVisibleCells()}>
{(cell) => (
<TableCell class="border-b border-r first-of-type:border-l">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
)}
</For>
</TableRow>
)}
</For>
</Show>
</TableBody>
</Table>
<div class="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => {
table.previousPage();
}}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
table.nextPage();
}}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
);
};