feat: typed database with kysely, updated config
This commit is contained in:
parent
28ec24b2c2
commit
67da0a72db
24 changed files with 1656 additions and 434 deletions
8
src/pages/chat/index.tsx
Normal file
8
src/pages/chat/index.tsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import type { Component } from "solid-js";
|
||||
import type { RouteSectionProps } from "@solidjs/router";
|
||||
|
||||
export const Chat: Component<RouteSectionProps> = (props) => {
|
||||
const threadId = () => props.params.threadid;
|
||||
|
||||
return threadId();
|
||||
};
|
|
@ -1,21 +1,24 @@
|
|||
import { redirect, useNavigate, type RouteSectionProps } from "@solidjs/router";
|
||||
import { type Component, type JSX } from "solid-js";
|
||||
import { type RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
|
||||
import { setDb, SQL } from "~/db";
|
||||
|
||||
export const Home: Component<RouteSectionProps> = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFileChange: JSX.ChangeEventHandler<HTMLInputElement, Event> = (event) => {
|
||||
const file = event.currentTarget.files![0];
|
||||
const reader = new FileReader();
|
||||
const file = event.currentTarget.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener("load", () => {
|
||||
const Uints = new Uint8Array(reader.result as ArrayBuffer);
|
||||
setDb(new SQL.Database(Uints));
|
||||
navigate("/overview");
|
||||
});
|
||||
reader.addEventListener("load", () => {
|
||||
const Uints = new Uint8Array(reader.result as ArrayBuffer);
|
||||
setDb(new SQL.Database(Uints));
|
||||
navigate("/overview");
|
||||
});
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import type { RouteSectionProps } from "@solidjs/router";
|
||||
import type { Component } from "solid-js";
|
||||
import { overallSentMessagesStmt, roomOverviewStmt, SELF_ID, type RoomOverviewColumn } from "~/db";
|
||||
|
||||
type RoomOverview = {
|
||||
recipientId: number;
|
||||
active: boolean;
|
||||
archived: boolean;
|
||||
messageCount: number;
|
||||
lastMessageDate: number;
|
||||
title: string;
|
||||
isGroup: boolean;
|
||||
}[];
|
||||
|
||||
export const Overview: Component<RouteSectionProps> = () => {
|
||||
const allSelfSentMessagesCount = overallSentMessagesStmt().getAsObject({
|
||||
":recipient_id": SELF_ID,
|
||||
});
|
||||
|
||||
const roomOverviewRaw: RoomOverviewColumn[] = [];
|
||||
|
||||
while (roomOverviewStmt().step()) {
|
||||
roomOverviewRaw.push(roomOverviewStmt().getAsObject() as RoomOverviewColumn);
|
||||
}
|
||||
|
||||
roomOverviewStmt().free();
|
||||
|
||||
const roomOverview: RoomOverview = roomOverviewRaw.map((column) => {
|
||||
const isGroup = Boolean(column.title);
|
||||
|
||||
return {
|
||||
recipientId: column.recipient_id,
|
||||
active: Boolean(column.active),
|
||||
archived: Boolean(column.archived),
|
||||
messageCount: column.message_count,
|
||||
lastMessageDate: column.last_message_date,
|
||||
title: isGroup ? column.title! : (column.system_joined_name ?? column.profile_joined_name)!,
|
||||
isGroup,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(roomOverview);
|
||||
|
||||
return <p>All messages: {allSelfSentMessagesCount.message_count as number}</p>;
|
||||
};
|
||||
|
||||
export default Overview;
|
49
src/pages/overview/index.tsx
Normal file
49
src/pages/overview/index.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { type Component, createResource, Show } from "solid-js";
|
||||
import type { RouteSectionProps } from "@solidjs/router";
|
||||
|
||||
import { overallSentMessagesQuery, SELF_ID, threadOverviewQuery } from "~/db";
|
||||
|
||||
import { OverviewTable, type RoomOverview } from "./overview-table";
|
||||
|
||||
export const Overview: Component<RouteSectionProps> = () => {
|
||||
const [allSelfSentMessagesCount] = createResource(() => overallSentMessagesQuery(SELF_ID).executeTakeFirstOrThrow());
|
||||
|
||||
const [roomOverview] = createResource<RoomOverview[]>(async () => {
|
||||
return (await threadOverviewQuery.execute()).map((row) => {
|
||||
const isGroup = row.title !== null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const name = (
|
||||
isGroup
|
||||
? row.title
|
||||
: /* seems possible that it is an empty string */ !row.system_joined_name
|
||||
? row.profile_joined_name
|
||||
: row.system_joined_name
|
||||
)!;
|
||||
|
||||
return {
|
||||
threadId: row.thread_id,
|
||||
recipientId: row.recipient_id,
|
||||
archived: Boolean(row.archived),
|
||||
messageCount: row.message_count,
|
||||
lastMessageDate: row.last_message_date ? new Date(row.last_message_date) : undefined,
|
||||
name,
|
||||
isGroup,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>All messages: {allSelfSentMessagesCount()?.message_count as number}</p>
|
||||
<Show
|
||||
when={!roomOverview.loading}
|
||||
fallback="Loading..."
|
||||
>
|
||||
<OverviewTable data={roomOverview()!} />;
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
347
src/pages/overview/overview-table.tsx
Normal file
347
src/pages/overview/overview-table.tsx
Normal file
|
@ -0,0 +1,347 @@
|
|||
import { type Component, createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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="[&: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"}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue