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,11 +8,14 @@ import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||||
import solid from "eslint-plugin-solid/configs/typescript";
|
import solid from "eslint-plugin-solid/configs/typescript";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default tseslint.config([
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ["dist/**/*.ts", "dist/**", "**/*.mjs", "eslint.config.js", "**/*.js"],
|
||||||
|
},
|
||||||
|
[
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
// tseslint.configs.recommendedTypeChecked,
|
tseslint.configs.strictTypeChecked,
|
||||||
// tseslint.configs.strictTypeChecked,
|
tseslint.configs.stylisticTypeChecked,
|
||||||
// tseslint.configs.stylisticTypeChecked,
|
|
||||||
eslintPluginPrettierRecommended,
|
eslintPluginPrettierRecommended,
|
||||||
eslintConfigPrettier,
|
eslintConfigPrettier,
|
||||||
{
|
{
|
||||||
|
@ -69,4 +72,5 @@ export default tseslint.config([
|
||||||
// "@typescript-eslint/consistent-type-exports": "error",
|
// "@typescript-eslint/consistent-type-exports": "error",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
10
package.json
10
package.json
|
@ -8,7 +8,8 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"postinstall": "cp ./node_modules/sql.js/dist/sql-wasm.wasm ./src/assets/sql-wasm.wasm"
|
"postinstall": "cp ./node_modules/sql.js/dist/sql-wasm.wasm ./src/assets/sql-wasm.wasm",
|
||||||
|
"generate-db-types": "kysely-codegen --dialect=sqlite --url=./src/assets/database.sqlite"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -18,11 +19,13 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||||
"@typescript-eslint/parser": "^8.17.0",
|
"@typescript-eslint/parser": "^8.17.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"better-sqlite3": "^11.7.0",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-solid": "^0.14.4",
|
"eslint-plugin-solid": "^0.14.4",
|
||||||
|
"kysely-codegen": "^0.17.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||||
|
@ -37,9 +40,14 @@
|
||||||
"@kobalte/tailwindcss": "^0.9.0",
|
"@kobalte/tailwindcss": "^0.9.0",
|
||||||
"@solid-primitives/refs": "^1.0.8",
|
"@solid-primitives/refs": "^1.0.8",
|
||||||
"@solidjs/router": "^0.15.1",
|
"@solidjs/router": "^0.15.1",
|
||||||
|
"@tanstack/solid-table": "^8.20.5",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.4.7",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"kysely": "^0.27.5",
|
||||||
|
"kysely-wasm": "^0.7.0",
|
||||||
|
"lucide-solid": "^0.468.0",
|
||||||
"solid-js": "^1.9.3",
|
"solid-js": "^1.9.3",
|
||||||
"sql.js": "^1.12.0",
|
"sql.js": "^1.12.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
|
528
pnpm-lock.yaml
generated
528
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,33 +0,0 @@
|
||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: #b318f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { type Component } from "solid-js";
|
import { type Component } from "solid-js";
|
||||||
import { Route } from "@solidjs/router";
|
import { Route } from "@solidjs/router";
|
||||||
|
|
||||||
import { Home, Overview } from "./pages";
|
import { Home, Overview } from "./pages";
|
||||||
|
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -13,6 +16,7 @@ const App: Component = () => {
|
||||||
path="/overview"
|
path="/overview"
|
||||||
component={Overview}
|
component={Overview}
|
||||||
/>
|
/>
|
||||||
|
<Route path="/thread/:threadid" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
44
src/components/ui/badge.tsx
Normal file
44
src/components/ui/badge.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import type { Component, ComponentProps } from "solid-js";
|
||||||
|
import { splitProps } from "solid-js";
|
||||||
|
|
||||||
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border-transparent bg-primary text-primary-foreground",
|
||||||
|
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||||
|
outline: "text-foreground",
|
||||||
|
success: "border-success-foreground bg-success text-success-foreground",
|
||||||
|
warning: "border-warning-foreground bg-warning text-warning-foreground",
|
||||||
|
error: "border-error-foreground bg-error text-error-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
type BadgeProps = ComponentProps<"div"> &
|
||||||
|
VariantProps<typeof badgeVariants> & {
|
||||||
|
round?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Badge: Component<BadgeProps> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class", "variant", "round"]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={cn(badgeVariants({ variant: local.variant }), local.round && "rounded-full", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { BadgeProps };
|
||||||
|
export { Badge, badgeVariants };
|
51
src/components/ui/button.tsx
Normal file
51
src/components/ui/button.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import type { JSX, ValidComponent } from "solid-js";
|
||||||
|
import { splitProps } from "solid-js";
|
||||||
|
|
||||||
|
import * as ButtonPrimitive from "@kobalte/core/button";
|
||||||
|
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||||
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 px-3 text-xs",
|
||||||
|
lg: "h-11 px-8",
|
||||||
|
icon: "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
type ButtonProps<T extends ValidComponent = "button"> = ButtonPrimitive.ButtonRootProps<T> &
|
||||||
|
VariantProps<typeof buttonVariants> & { class?: string | undefined; children?: JSX.Element };
|
||||||
|
|
||||||
|
const Button = <T extends ValidComponent = "button">(props: PolymorphicProps<T, ButtonProps<T>>) => {
|
||||||
|
const [local, others] = splitProps(props as ButtonProps, ["variant", "size", "class"]);
|
||||||
|
return (
|
||||||
|
<ButtonPrimitive.Root
|
||||||
|
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { ButtonProps };
|
||||||
|
export { Button, buttonVariants };
|
|
@ -1,19 +1,19 @@
|
||||||
import type { Component } from "solid-js"
|
import type { Component } from "solid-js";
|
||||||
import { createEffect, createSignal, mergeProps, on, onCleanup, onMount } from "solid-js"
|
import { createEffect, createSignal, mergeProps, on, onCleanup, onMount } from "solid-js";
|
||||||
import { unwrap } from "solid-js/store"
|
import { unwrap } from "solid-js/store";
|
||||||
|
|
||||||
import type { Ref } from "@solid-primitives/refs"
|
import type { Ref } from "@solid-primitives/refs";
|
||||||
import { mergeRefs } from "@solid-primitives/refs"
|
import { mergeRefs } from "@solid-primitives/refs";
|
||||||
import type {
|
import type {
|
||||||
ChartComponent,
|
ChartComponent,
|
||||||
ChartData,
|
ChartData,
|
||||||
ChartItem,
|
ChartItem,
|
||||||
ChartOptions,
|
ChartOptions,
|
||||||
Plugin as ChartPlugin,
|
|
||||||
ChartType,
|
ChartType,
|
||||||
ChartTypeRegistry,
|
ChartTypeRegistry,
|
||||||
TooltipModel
|
Plugin as ChartPlugin,
|
||||||
} from "chart.js"
|
TooltipModel,
|
||||||
|
} from "chart.js";
|
||||||
import {
|
import {
|
||||||
ArcElement,
|
ArcElement,
|
||||||
BarController,
|
BarController,
|
||||||
|
@ -34,167 +34,166 @@ import {
|
||||||
RadarController,
|
RadarController,
|
||||||
RadialLinearScale,
|
RadialLinearScale,
|
||||||
ScatterController,
|
ScatterController,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from "chart.js"
|
} from "chart.js";
|
||||||
|
|
||||||
type TypedChartProps = {
|
interface TypedChartProps {
|
||||||
data: ChartData
|
data: ChartData;
|
||||||
options?: ChartOptions
|
options?: ChartOptions;
|
||||||
plugins?: ChartPlugin[]
|
plugins?: ChartPlugin[];
|
||||||
ref?: Ref<HTMLCanvasElement | null>
|
ref?: Ref<HTMLCanvasElement | null>;
|
||||||
width?: number | undefined
|
width?: number | undefined;
|
||||||
height?: number | undefined
|
height?: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartProps = TypedChartProps & {
|
type ChartProps = TypedChartProps & {
|
||||||
type: ChartType
|
type: ChartType;
|
||||||
}
|
};
|
||||||
|
|
||||||
type ChartContext = {
|
interface ChartContext {
|
||||||
chart: Chart
|
chart: Chart;
|
||||||
tooltip: TooltipModel<keyof ChartTypeRegistry>
|
tooltip: TooltipModel<keyof ChartTypeRegistry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseChart: Component<ChartProps> = (rawProps) => {
|
const BaseChart: Component<ChartProps> = (rawProps) => {
|
||||||
const [canvasRef, setCanvasRef] = createSignal<HTMLCanvasElement | null>()
|
const [canvasRef, setCanvasRef] = createSignal<HTMLCanvasElement | null>();
|
||||||
const [chart, setChart] = createSignal<Chart>()
|
const [chart, setChart] = createSignal<Chart>();
|
||||||
|
|
||||||
const props = mergeProps(
|
const props = mergeProps(
|
||||||
{
|
{
|
||||||
width: 512,
|
width: 512,
|
||||||
height: 512,
|
height: 512,
|
||||||
options: { responsive: true } as ChartOptions,
|
options: { responsive: true } as ChartOptions,
|
||||||
plugins: [] as ChartPlugin[]
|
plugins: [] as ChartPlugin[],
|
||||||
},
|
},
|
||||||
rawProps
|
rawProps,
|
||||||
)
|
);
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
const ctx = canvasRef()?.getContext("2d") as ChartItem
|
const ctx = canvasRef()?.getContext("2d") as ChartItem;
|
||||||
const config = unwrap(props)
|
const config = unwrap(props);
|
||||||
const chart = new Chart(ctx, {
|
const chart = new Chart(ctx, {
|
||||||
type: config.type,
|
type: config.type,
|
||||||
data: config.data,
|
data: config.data,
|
||||||
options: config.options,
|
options: config.options,
|
||||||
plugins: config.plugins
|
plugins: config.plugins,
|
||||||
})
|
});
|
||||||
setChart(chart)
|
setChart(chart);
|
||||||
}
|
};
|
||||||
|
|
||||||
onMount(() => init())
|
onMount(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
() => {
|
() => {
|
||||||
chart()!.data = props.data
|
chart()!.data = props.data;
|
||||||
chart()!.update()
|
chart()!.update();
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true },
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.options,
|
() => props.options,
|
||||||
() => {
|
() => {
|
||||||
chart()!.options = props.options
|
chart()!.options = props.options;
|
||||||
chart()!.update()
|
chart()!.update();
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true },
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
[() => props.width, () => props.height],
|
[() => props.width, () => props.height],
|
||||||
() => {
|
() => {
|
||||||
chart()!.resize(props.width, props.height)
|
chart()!.resize(props.width, props.height);
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true },
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.type,
|
() => props.type,
|
||||||
() => {
|
() => {
|
||||||
const dimensions = [chart()!.width, chart()!.height]
|
const dimensions = [chart()!.width, chart()!.height];
|
||||||
chart()!.destroy()
|
chart()!.destroy();
|
||||||
init()
|
init();
|
||||||
chart()!.resize(...dimensions)
|
chart()!.resize(...dimensions);
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true },
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
chart()?.destroy()
|
chart()?.destroy();
|
||||||
mergeRefs(props.ref, null)
|
mergeRefs(props.ref, null);
|
||||||
})
|
});
|
||||||
|
|
||||||
Chart.register(Colors, Filler, Legend, Tooltip)
|
Chart.register(Colors, Filler, Legend, Tooltip);
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
ref={mergeRefs(props.ref, (el) => setCanvasRef(el))}
|
ref={mergeRefs(props.ref, (el) => setCanvasRef(el))}
|
||||||
height={props.height}
|
height={props.height}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function showTooltip(context: ChartContext) {
|
function showTooltip(context: ChartContext) {
|
||||||
let el = document.getElementById("chartjs-tooltip")
|
let el = document.getElementById("chartjs-tooltip");
|
||||||
if (!el) {
|
if (!el) {
|
||||||
el = document.createElement("div")
|
el = document.createElement("div");
|
||||||
el.id = "chartjs-tooltip"
|
el.id = "chartjs-tooltip";
|
||||||
document.body.appendChild(el)
|
document.body.appendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = context.tooltip
|
const model = context.tooltip;
|
||||||
if (model.opacity === 0 || !model.body) {
|
if (model.opacity === 0 || !model.body) {
|
||||||
el.style.opacity = "0"
|
el.style.opacity = "0";
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.className = `p-2 bg-card text-card-foreground rounded-lg border shadow-sm text-sm ${
|
el.className = `p-2 bg-card text-card-foreground rounded-lg border shadow-sm text-sm ${
|
||||||
model.yAlign ?? `no-transform`
|
model.yAlign ?? `no-transform`
|
||||||
}`
|
}`;
|
||||||
|
|
||||||
let content = ""
|
let content = "";
|
||||||
|
|
||||||
model.title.forEach((title) => {
|
model.title.forEach((title) => {
|
||||||
content += `<h3 class="font-semibold leading-none tracking-tight">${title}</h3>`
|
content += `<h3 class="font-semibold leading-none tracking-tight">${title}</h3>`;
|
||||||
})
|
});
|
||||||
|
|
||||||
content += `<div class="mt-1 text-muted-foreground">`
|
content += `<div class="mt-1 text-muted-foreground">`;
|
||||||
const body = model.body.flatMap((body) => body.lines)
|
const body = model.body.flatMap((body) => body.lines);
|
||||||
body.forEach((line, i) => {
|
body.forEach((line, i) => {
|
||||||
const colors = model.labelColors[i]
|
const colors = model.labelColors[i];
|
||||||
content += `
|
content += `
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="inline-block h-2 w-2 mr-1 rounded-full border" style="background: ${colors.backgroundColor}; border-color: ${colors.borderColor}"></span>
|
<span class="inline-block h-2 w-2 mr-1 rounded-full border" style="background: ${colors.backgroundColor}; border-color: ${colors.borderColor}"></span>
|
||||||
${line}
|
${line}
|
||||||
</div>`
|
</div>`;
|
||||||
})
|
});
|
||||||
content += `</div>`
|
content += `</div>`;
|
||||||
|
|
||||||
el.innerHTML = content
|
el.innerHTML = content;
|
||||||
|
|
||||||
const pos = context.chart.canvas.getBoundingClientRect()
|
const pos = context.chart.canvas.getBoundingClientRect();
|
||||||
el.style.opacity = "1"
|
el.style.opacity = "1";
|
||||||
el.style.position = "absolute"
|
el.style.position = "absolute";
|
||||||
el.style.left = `${pos.left + window.scrollX + model.caretX}px`
|
el.style.left = `${pos.left + window.scrollX + model.caretX}px`;
|
||||||
el.style.top = `${pos.top + window.scrollY + model.caretY}px`
|
el.style.top = `${pos.top + window.scrollY + model.caretY}px`;
|
||||||
el.style.pointerEvents = "none"
|
el.style.pointerEvents = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTypedChart(
|
function createTypedChart(type: ChartType, components: ChartComponent[]): Component<TypedChartProps> {
|
||||||
type: ChartType,
|
const chartsWithScales: ChartType[] = ["bar", "line", "scatter"];
|
||||||
components: ChartComponent[]
|
const chartsWithLegends: ChartType[] = ["bar", "line"];
|
||||||
): Component<TypedChartProps> {
|
|
||||||
const chartsWithScales: ChartType[] = ["bar", "line", "scatter"]
|
|
||||||
const chartsWithLegends: ChartType[] = ["bar", "line"]
|
|
||||||
|
|
||||||
const options: ChartOptions = {
|
const options: ChartOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
@ -203,18 +202,18 @@ function createTypedChart(
|
||||||
? {
|
? {
|
||||||
x: {
|
x: {
|
||||||
border: { display: false },
|
border: { display: false },
|
||||||
grid: { display: false }
|
grid: { display: false },
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
border: {
|
border: {
|
||||||
dash: [3],
|
dash: [3],
|
||||||
dashOffset: 3,
|
dashOffset: 3,
|
||||||
display: false
|
display: false,
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
color: "hsla(240, 3.8%, 46.1%, 0.4)"
|
color: "hsla(240, 3.8%, 46.1%, 0.4)",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
@ -227,66 +226,61 @@ function createTypedChart(
|
||||||
boxWidth: 6,
|
boxWidth: 6,
|
||||||
boxHeight: 6,
|
boxHeight: 6,
|
||||||
color: "hsl(240, 3.8%, 46.1%)",
|
color: "hsl(240, 3.8%, 46.1%)",
|
||||||
font: { size: 14 }
|
font: { size: 14 },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
: { display: false },
|
: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
external: (context) => showTooltip(context)
|
external: (context) => {
|
||||||
}
|
showTooltip(context);
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Chart.register(...components);
|
||||||
|
return (props) => (
|
||||||
|
<BaseChart
|
||||||
|
type={type}
|
||||||
|
options={options}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Chart.register(...components)
|
const BarChart = /* #__PURE__ */ createTypedChart("bar", [BarController, BarElement, CategoryScale, LinearScale]);
|
||||||
return (props) => <BaseChart type={type} options={options} {...props} />
|
const BubbleChart = /* #__PURE__ */ createTypedChart("bubble", [BubbleController, PointElement, LinearScale]);
|
||||||
}
|
const DonutChart = /* #__PURE__ */ createTypedChart("doughnut", [DoughnutController, ArcElement]);
|
||||||
|
|
||||||
const BarChart = /* #__PURE__ */ createTypedChart("bar", [
|
|
||||||
BarController,
|
|
||||||
BarElement,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale
|
|
||||||
])
|
|
||||||
const BubbleChart = /* #__PURE__ */ createTypedChart("bubble", [
|
|
||||||
BubbleController,
|
|
||||||
PointElement,
|
|
||||||
LinearScale
|
|
||||||
])
|
|
||||||
const DonutChart = /* #__PURE__ */ createTypedChart("doughnut", [DoughnutController, ArcElement])
|
|
||||||
const LineChart = /* #__PURE__ */ createTypedChart("line", [
|
const LineChart = /* #__PURE__ */ createTypedChart("line", [
|
||||||
LineController,
|
LineController,
|
||||||
LineElement,
|
LineElement,
|
||||||
PointElement,
|
PointElement,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale
|
LinearScale,
|
||||||
])
|
]);
|
||||||
const PieChart = /* #__PURE__ */ createTypedChart("pie", [PieController, ArcElement])
|
const PieChart = /* #__PURE__ */ createTypedChart("pie", [PieController, ArcElement]);
|
||||||
const PolarAreaChart = /* #__PURE__ */ createTypedChart("polarArea", [
|
const PolarAreaChart = /* #__PURE__ */ createTypedChart("polarArea", [
|
||||||
PolarAreaController,
|
PolarAreaController,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
RadialLinearScale
|
RadialLinearScale,
|
||||||
])
|
]);
|
||||||
const RadarChart = /* #__PURE__ */ createTypedChart("radar", [
|
const RadarChart = /* #__PURE__ */ createTypedChart("radar", [
|
||||||
RadarController,
|
RadarController,
|
||||||
LineElement,
|
LineElement,
|
||||||
PointElement,
|
PointElement,
|
||||||
RadialLinearScale
|
RadialLinearScale,
|
||||||
])
|
]);
|
||||||
const ScatterChart = /* #__PURE__ */ createTypedChart("scatter", [
|
const ScatterChart = /* #__PURE__ */ createTypedChart("scatter", [ScatterController, PointElement, LinearScale]);
|
||||||
ScatterController,
|
|
||||||
PointElement,
|
|
||||||
LinearScale
|
|
||||||
])
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BaseChart as Chart,
|
|
||||||
BarChart,
|
BarChart,
|
||||||
BubbleChart,
|
BubbleChart,
|
||||||
|
BaseChart as Chart,
|
||||||
DonutChart,
|
DonutChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
PieChart,
|
PieChart,
|
||||||
PolarAreaChart,
|
PolarAreaChart,
|
||||||
RadarChart,
|
RadarChart,
|
||||||
ScatterChart
|
ScatterChart,
|
||||||
}
|
};
|
||||||
|
|
59
src/components/ui/checkbox.tsx
Normal file
59
src/components/ui/checkbox.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import type { ValidComponent } from "solid-js";
|
||||||
|
import { Match, splitProps, Switch } from "solid-js";
|
||||||
|
|
||||||
|
import * as CheckboxPrimitive from "@kobalte/core/checkbox";
|
||||||
|
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
type CheckboxRootProps<T extends ValidComponent = "div"> = CheckboxPrimitive.CheckboxRootProps<T> & {
|
||||||
|
class?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Checkbox = <T extends ValidComponent = "div">(props: PolymorphicProps<T, CheckboxRootProps<T>>) => {
|
||||||
|
const [local, others] = splitProps(props as CheckboxRootProps, ["class"]);
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
class={cn("items-top group relative flex space-x-2", local.class)}
|
||||||
|
{...others}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Input class="peer" />
|
||||||
|
<CheckboxPrimitive.Control class="size-4 shrink-0 rounded-sm border border-primary ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 data-[checked]:border-none data-[indeterminate]:border-none data-[checked]:bg-primary data-[indeterminate]:bg-primary data-[checked]:text-primary-foreground data-[indeterminate]:text-primary-foreground">
|
||||||
|
<CheckboxPrimitive.Indicator>
|
||||||
|
<Switch>
|
||||||
|
<Match when={!others.indeterminate}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path d="M5 12l5 5l10 -10" />
|
||||||
|
</svg>
|
||||||
|
</Match>
|
||||||
|
<Match when={others.indeterminate}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path d="M5 12l14 0" />
|
||||||
|
</svg>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Control>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Checkbox };
|
19
src/components/ui/label.tsx
Normal file
19
src/components/ui/label.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Component, ComponentProps } from "solid-js";
|
||||||
|
import { splitProps } from "solid-js";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
const Label: Component<ComponentProps<"label">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
class={cn(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
local.class,
|
||||||
|
)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Label };
|
91
src/components/ui/table.tsx
Normal file
91
src/components/ui/table.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import type { Component, ComponentProps } from "solid-js";
|
||||||
|
import { splitProps } from "solid-js";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
const Table: Component<ComponentProps<"table">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<div class="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
class={cn("w-full caption-bottom text-sm", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableHeader: Component<ComponentProps<"thead">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
class={cn("[&_tr]:border-b", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableBody: Component<ComponentProps<"tbody">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
class={cn("[&_tr:last-child]:border-0", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableFooter: Component<ComponentProps<"tfoot">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
class={cn("bg-primary font-medium text-primary-foreground", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableRow: Component<ComponentProps<"tr">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
class={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableHead: Component<ComponentProps<"th">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
class={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
local.class,
|
||||||
|
)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableCell: Component<ComponentProps<"td">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
class={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableCaption: Component<ComponentProps<"caption">> = (props) => {
|
||||||
|
const [local, others] = splitProps(props, ["class"]);
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
class={cn("mt-4 text-sm text-muted-foreground", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
|
147
src/components/ui/text-field.tsx
Normal file
147
src/components/ui/text-field.tsx
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import type { ValidComponent } from "solid-js";
|
||||||
|
import { mergeProps, splitProps } from "solid-js";
|
||||||
|
|
||||||
|
import type { PolymorphicProps } from "@kobalte/core";
|
||||||
|
import * as TextFieldPrimitive from "@kobalte/core/text-field";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
type TextFieldRootProps<T extends ValidComponent = "div"> = TextFieldPrimitive.TextFieldRootProps<T> & {
|
||||||
|
class?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextField = <T extends ValidComponent = "div">(props: PolymorphicProps<T, TextFieldRootProps<T>>) => {
|
||||||
|
const [local, others] = splitProps(props as TextFieldRootProps, ["class"]);
|
||||||
|
return (
|
||||||
|
<TextFieldPrimitive.Root
|
||||||
|
class={cn("flex flex-col gap-1", local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TextFieldInputProps<T extends ValidComponent = "input"> = TextFieldPrimitive.TextFieldInputProps<T> & {
|
||||||
|
class?: string | undefined;
|
||||||
|
type?:
|
||||||
|
| "button"
|
||||||
|
| "checkbox"
|
||||||
|
| "color"
|
||||||
|
| "date"
|
||||||
|
| "datetime-local"
|
||||||
|
| "email"
|
||||||
|
| "file"
|
||||||
|
| "hidden"
|
||||||
|
| "image"
|
||||||
|
| "month"
|
||||||
|
| "number"
|
||||||
|
| "password"
|
||||||
|
| "radio"
|
||||||
|
| "range"
|
||||||
|
| "reset"
|
||||||
|
| "search"
|
||||||
|
| "submit"
|
||||||
|
| "tel"
|
||||||
|
| "text"
|
||||||
|
| "time"
|
||||||
|
| "url"
|
||||||
|
| "week";
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextFieldInput = <T extends ValidComponent = "input">(rawProps: PolymorphicProps<T, TextFieldInputProps<T>>) => {
|
||||||
|
const props = mergeProps<TextFieldInputProps<T>[]>({ type: "text" }, rawProps);
|
||||||
|
const [local, others] = splitProps(props as TextFieldInputProps, ["type", "class"]);
|
||||||
|
return (
|
||||||
|
<TextFieldPrimitive.Input
|
||||||
|
type={local.type}
|
||||||
|
class={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[invalid]:border-error-foreground data-[invalid]:text-error-foreground",
|
||||||
|
local.class,
|
||||||
|
)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TextFieldTextAreaProps<T extends ValidComponent = "textarea"> = TextFieldPrimitive.TextFieldTextAreaProps<T> & {
|
||||||
|
class?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextFieldTextArea = <T extends ValidComponent = "textarea">(
|
||||||
|
props: PolymorphicProps<T, TextFieldTextAreaProps<T>>,
|
||||||
|
) => {
|
||||||
|
const [local, others] = splitProps(props as TextFieldTextAreaProps, ["class"]);
|
||||||
|
return (
|
||||||
|
<TextFieldPrimitive.TextArea
|
||||||
|
class={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
local.class,
|
||||||
|
)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
label: "data-[invalid]:text-destructive",
|
||||||
|
description: "font-normal text-muted-foreground",
|
||||||
|
error: "text-xs text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "label",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
type TextFieldLabelProps<T extends ValidComponent = "label"> = TextFieldPrimitive.TextFieldLabelProps<T> & {
|
||||||
|
class?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextFieldLabel = <T extends ValidComponent = "label">(props: PolymorphicProps<T, TextFieldLabelProps<T>>) => {
|
||||||
|
const [local, others] = splitProps(props as TextFieldLabelProps, ["class"]);
|
||||||
|
return (
|
||||||
|
<TextFieldPrimitive.Label
|
||||||
|
class={cn(labelVariants(), local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TextFieldDescriptionProps<T extends ValidComponent = "div"> = TextFieldPrimitive.TextFieldDescriptionProps<T> & {
|
||||||
|
class?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextFieldDescription = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, TextFieldDescriptionProps<T>>,
|
||||||
|
) => {
|
||||||
|
const [local, others] = splitProps(props as TextFieldDescriptionProps, ["class"]);
|
||||||
|
return (
|
||||||
|
<TextFieldPrimitive.Description
|
||||||
|
class={cn(labelVariants({ variant: "description" }), local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TextFieldErrorMessageProps<T extends ValidComponent = "div"> = TextFieldPrimitive.TextFieldErrorMessageProps<T> & {
|
||||||
|
class?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextFieldErrorMessage = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, TextFieldErrorMessageProps<T>>,
|
||||||
|
) => {
|
||||||
|
const [local, others] = splitProps(props as TextFieldErrorMessageProps, ["class"]);
|
||||||
|
return (
|
||||||
|
<TextFieldPrimitive.ErrorMessage
|
||||||
|
class={cn(labelVariants({ variant: "error" }), local.class)}
|
||||||
|
{...others}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TextField, TextFieldDescription, TextFieldErrorMessage, TextFieldInput, TextFieldLabel, TextFieldTextArea };
|
148
src/db.ts
148
src/db.ts
|
@ -1,8 +1,11 @@
|
||||||
|
import { type Accessor, createMemo, createSignal, DEV, type Setter } from "solid-js";
|
||||||
|
|
||||||
|
import { Kysely, type NotNull } from "kysely";
|
||||||
|
import type { DB } from "kysely-codegen";
|
||||||
|
import { SqlJsDialect } from "kysely-wasm";
|
||||||
import initSqlJS, { type Database } from "sql.js";
|
import initSqlJS, { type Database } from "sql.js";
|
||||||
|
|
||||||
import wasmURL from "./assets/sql-wasm.wasm?url";
|
import wasmURL from "./assets/sql-wasm.wasm?url";
|
||||||
import dbURL from "./assets/database.sqlite?url";
|
|
||||||
import { createMemo, createSignal } from "solid-js";
|
|
||||||
|
|
||||||
export const SELF_ID = 2;
|
export const SELF_ID = 2;
|
||||||
|
|
||||||
|
@ -10,80 +13,87 @@ export const SQL = await initSqlJS({
|
||||||
locateFile: () => wasmURL,
|
locateFile: () => wasmURL,
|
||||||
});
|
});
|
||||||
|
|
||||||
const file = await fetch(dbURL).then((res) => res.arrayBuffer());
|
let rawDb: Accessor<Database | undefined>, setRawDb: Setter<Database | undefined>;
|
||||||
|
|
||||||
|
if (DEV) {
|
||||||
|
const file = await import("./assets/database.sqlite?url").then((result) => {
|
||||||
|
return fetch(result.default).then((res) => res.arrayBuffer());
|
||||||
|
});
|
||||||
|
|
||||||
const testDb = new SQL.Database(new Uint8Array(file));
|
const testDb = new SQL.Database(new Uint8Array(file));
|
||||||
|
|
||||||
export const [db, setDb] = createSignal<Database>(testDb);
|
[rawDb, setRawDb] = createSignal<Database>(testDb);
|
||||||
|
} else {
|
||||||
|
[rawDb, setRawDb] = createSignal<Database>();
|
||||||
|
}
|
||||||
|
|
||||||
const createStatement = (sql: string) => {
|
export { rawDb as db, setRawDb as setDb };
|
||||||
return createMemo(() => {
|
|
||||||
return db().prepare(sql);
|
const sqlJsDialect = () => {
|
||||||
|
const currentDb = rawDb();
|
||||||
|
|
||||||
|
if (currentDb) {
|
||||||
|
return new SqlJsDialect({
|
||||||
|
database: currentDb,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roomOverviewStmt = createStatement(`
|
const kyselyDb = createMemo(() => {
|
||||||
SELECT
|
const currentSqlJsDialect = sqlJsDialect();
|
||||||
thread.recipient_id,
|
|
||||||
thread.active,
|
|
||||||
thread.archived,
|
|
||||||
recipient.profile_joined_name,
|
|
||||||
recipient.system_joined_name,
|
|
||||||
groups.title,
|
|
||||||
message_count,
|
|
||||||
last_message_date
|
|
||||||
FROM
|
|
||||||
thread
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT
|
|
||||||
thread_id,
|
|
||||||
COUNT(*) AS message_count
|
|
||||||
FROM
|
|
||||||
message
|
|
||||||
WHERE
|
|
||||||
message.body IS NOT NULL
|
|
||||||
AND message.body != ''
|
|
||||||
GROUP BY
|
|
||||||
thread_id
|
|
||||||
) message_counts ON message_counts.thread_id = thread._id
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT
|
|
||||||
thread_id,
|
|
||||||
max(date_sent) AS last_message_date
|
|
||||||
FROM
|
|
||||||
message
|
|
||||||
GROUP BY
|
|
||||||
thread_id
|
|
||||||
) last_messages ON last_messages.thread_id = thread._id
|
|
||||||
JOIN recipient ON thread.recipient_id = recipient._id
|
|
||||||
LEFT JOIN groups ON recipient._id = groups.recipient_id
|
|
||||||
WHERE
|
|
||||||
message_count > 0
|
|
||||||
`);
|
|
||||||
|
|
||||||
export type RoomOverviewColumn = {
|
if (!currentSqlJsDialect) {
|
||||||
recipient_id: number;
|
throw new Error("no db selected!");
|
||||||
active: 0 | 1;
|
}
|
||||||
archived: 0 | 1;
|
|
||||||
|
return new Kysely<DB>({
|
||||||
|
dialect: currentSqlJsDialect,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const threadOverviewQuery = kyselyDb()
|
||||||
|
.selectFrom("thread")
|
||||||
|
.innerJoin(
|
||||||
|
(eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom("message")
|
||||||
|
.select(["thread_id", kyselyDb().fn.countAll().as("message_count")])
|
||||||
|
.where((eb) => {
|
||||||
|
return eb.and([eb("message.body", "is not", null), eb("message.body", "is not", "")]);
|
||||||
|
})
|
||||||
|
.groupBy("message.thread_id")
|
||||||
|
.as("message"),
|
||||||
|
(join) => join.onRef("message.thread_id", "=", "thread._id"),
|
||||||
|
)
|
||||||
|
.innerJoin("recipient", "thread.recipient_id", "recipient._id")
|
||||||
|
.leftJoin("groups", "recipient._id", "groups.recipient_id")
|
||||||
|
.select([
|
||||||
|
"thread._id as thread_id",
|
||||||
|
"thread.recipient_id",
|
||||||
|
"thread.archived",
|
||||||
|
"recipient.profile_joined_name",
|
||||||
|
"recipient.system_joined_name",
|
||||||
|
"groups.title",
|
||||||
|
"message_count",
|
||||||
|
"thread.date as last_message_date",
|
||||||
|
])
|
||||||
|
.where("message_count", ">", 0)
|
||||||
|
.$narrowType<{
|
||||||
|
thread_id: NotNull;
|
||||||
|
archived: NotNull;
|
||||||
message_count: number;
|
message_count: number;
|
||||||
last_message_date: number;
|
}>();
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
profile_joined_name: string;
|
|
||||||
system_joined_name: string | null;
|
|
||||||
title: null;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
profile_joined_name: null;
|
|
||||||
system_joined_name: null;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const overallSentMessagesStmt = createStatement(`
|
console.log(threadOverviewQuery.compile());
|
||||||
SELECT
|
|
||||||
COUNT(*) as message_count
|
export const overallSentMessagesQuery = (recipientId: number) =>
|
||||||
FROM
|
kyselyDb()
|
||||||
message
|
.selectFrom("message")
|
||||||
WHERE (message.from_recipient_id = :recipient_id AND message.body IS NOT NULL AND message.body != '')
|
.select(kyselyDb().fn.countAll().as("message_count"))
|
||||||
`);
|
.where((eb) =>
|
||||||
|
eb.and([
|
||||||
|
eb("message.from_recipient_id", "=", recipientId),
|
||||||
|
eb("message.body", "is not", null),
|
||||||
|
eb("message.body", "!=", ""),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
|
||||||
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
|
||||||
'Helvetica Neue', sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
||||||
monospace;
|
|
||||||
}
|
|
|
@ -1,23 +1,26 @@
|
||||||
/* @refresh reload */
|
/* @refresh reload */
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
|
|
||||||
import "./index.css";
|
|
||||||
import App from "./App";
|
|
||||||
import { Router } from "@solidjs/router";
|
import { Router } from "@solidjs/router";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?"
|
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root) {
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
|
<div class="mx-auto max-w-screen-2xl">
|
||||||
<Router>
|
<Router>
|
||||||
<App />
|
<App />
|
||||||
</Router>
|
</Router>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
root!
|
root,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
const TABLES = [
|
|
||||||
"attachment",
|
|
||||||
"sqlite_sequence",
|
|
||||||
"avatar_picker",
|
|
||||||
"recipient",
|
|
||||||
"thread",
|
|
||||||
"message",
|
|
||||||
"call",
|
|
||||||
"call_link",
|
|
||||||
"cds",
|
|
||||||
"chat_colors",
|
|
||||||
"chat_folder",
|
|
||||||
"chat_folder_membership",
|
|
||||||
"distribution_list",
|
|
||||||
"distribution_list_member",
|
|
||||||
"donation_receipt",
|
|
||||||
"drafts",
|
|
||||||
"emoji_search",
|
|
||||||
"groups",
|
|
||||||
"group_membership",
|
|
||||||
"group_receipts",
|
|
||||||
"identities",
|
|
||||||
"in_app_payment",
|
|
||||||
"in_app_payment_subscriber",
|
|
||||||
"kyber_prekey",
|
|
||||||
"mention",
|
|
||||||
"message_fts",
|
|
||||||
"message_fts_data",
|
|
||||||
"message_fts_idx",
|
|
||||||
"message_fts_docsize",
|
|
||||||
"message_fts_config",
|
|
||||||
"msl_payload",
|
|
||||||
"msl_message",
|
|
||||||
"msl_recipient",
|
|
||||||
"name_collision",
|
|
||||||
"name_collision_membership",
|
|
||||||
"notification_profile",
|
|
||||||
"notification_profile_allowed_members",
|
|
||||||
"notification_profile_schedule",
|
|
||||||
"one_time_prekeys",
|
|
||||||
"payments",
|
|
||||||
"pending_pni_signature_message",
|
|
||||||
"pending_retry_receipts",
|
|
||||||
"reaction",
|
|
||||||
"remapped_recipients",
|
|
||||||
"remapped_threads",
|
|
||||||
"remote_megaphone",
|
|
||||||
"sender_key_shared",
|
|
||||||
"sender_keys",
|
|
||||||
"sessions",
|
|
||||||
"storage_key",
|
|
||||||
"story_sends",
|
|
||||||
];
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
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,12 +1,14 @@
|
||||||
import { redirect, useNavigate, type RouteSectionProps } from "@solidjs/router";
|
|
||||||
import { type Component, type JSX } from "solid-js";
|
import { type Component, type JSX } from "solid-js";
|
||||||
|
import { type RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||||
|
|
||||||
import { setDb, SQL } from "~/db";
|
import { setDb, SQL } from "~/db";
|
||||||
|
|
||||||
export const Home: Component<RouteSectionProps> = () => {
|
export const Home: Component<RouteSectionProps> = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onFileChange: JSX.ChangeEventHandler<HTMLInputElement, Event> = (event) => {
|
const onFileChange: JSX.ChangeEventHandler<HTMLInputElement, Event> = (event) => {
|
||||||
const file = event.currentTarget.files![0];
|
const file = event.currentTarget.files?.[0];
|
||||||
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.addEventListener("load", () => {
|
reader.addEventListener("load", () => {
|
||||||
|
@ -16,6 +18,7 @@ export const Home: Component<RouteSectionProps> = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,4 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import solidPlugin from "vite-plugin-solid";
|
import solidPlugin from "vite-plugin-solid";
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue