356 lines
9.8 KiB
TypeScript
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>
|
|
);
|
|
};
|