Initial commit
This commit is contained in:
commit
28ec24b2c2
26 changed files with 4372 additions and 0 deletions
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
|
||||
dist
|
||||
.solid
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.vinxi
|
||||
app.config.timestamp_*.js
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# WASM file
|
||||
/src/assets/sql-wasm.wasm
|
||||
/src/assets/database.sqlite
|
36
README.md
Normal file
36
README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
## Usage
|
||||
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
||||
|
||||
## This project was created with the [Solid CLI](https://solid-cli.netlify.app)
|
30
db_layout.md
Normal file
30
db_layout.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
```mermaid
|
||||
flowchart LR
|
||||
A --> D
|
||||
B --> D
|
||||
D --> F
|
||||
F --> D
|
||||
E --> G
|
||||
G --> E
|
||||
C --> |I think this is the only way to match messages to groups and recipients| H
|
||||
I --> D
|
||||
|
||||
subgraph message
|
||||
A[message.from_recipient_id]
|
||||
B[message.to_recipient_id]
|
||||
C[message.thread_id]
|
||||
end
|
||||
subgraph recipient
|
||||
D[recipient._id]
|
||||
E[recipient.group_id]
|
||||
end
|
||||
subgraph groups
|
||||
F[groups.recipient_id]
|
||||
G[groups.group_id]
|
||||
end
|
||||
subgraph thread
|
||||
H[thread._id]
|
||||
I[thread.recipient_id]
|
||||
J@{ shape: braces, label: "Threads seem to be rooms in Signal" }
|
||||
end
|
||||
```
|
72
eslint.config.js
Normal file
72
eslint.config.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
// @ts-check
|
||||
|
||||
import eslint from "@eslint/js";
|
||||
import tsparser from "@typescript-eslint/parser";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
import solid from "eslint-plugin-solid/configs/typescript";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config([
|
||||
eslint.configs.recommended,
|
||||
// tseslint.configs.recommendedTypeChecked,
|
||||
// tseslint.configs.strictTypeChecked,
|
||||
// tseslint.configs.stylisticTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
...solid,
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
// Specifies the ESLint parser
|
||||
parserOptions: {
|
||||
ecmaVersion: 2024,
|
||||
// Allows for the parsing of modern ECMAScript features
|
||||
sourceType: "module",
|
||||
// Allows for the use of imports
|
||||
ecmaFeatures: {
|
||||
jsx: true, // Allows for the parsing of JSX
|
||||
},
|
||||
project: "./tsconfig.json",
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
"simple-import-sort": simpleImportSort,
|
||||
},
|
||||
rules: {
|
||||
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
|
||||
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
|
||||
"simple-import-sort/imports": [
|
||||
"error",
|
||||
{
|
||||
groups: [
|
||||
// solidjs
|
||||
["^solid-(js|start)", "^@solidjs/"],
|
||||
["^@?\\w"],
|
||||
// components imports
|
||||
["^~/components/?"],
|
||||
// other /src imports
|
||||
["^~/"],
|
||||
// Parent imports. Put `..` last.
|
||||
["^\\.\\.(?!/?$)", "^\\.\\./?$"],
|
||||
// Other relative imports. Put same-folder imports and `.` last.
|
||||
["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
|
||||
// types imports
|
||||
["^~/types$"],
|
||||
// Side effect imports.
|
||||
["^\\u0000"],
|
||||
// Style imports.
|
||||
["^.+\\.?(s?css)$"],
|
||||
],
|
||||
},
|
||||
],
|
||||
"simple-import-sort/exports": "error",
|
||||
// "@typescript-eslint/consistent-type-imports": "error",
|
||||
// "@typescript-eslint/consistent-type-exports": "error",
|
||||
},
|
||||
},
|
||||
]);
|
16
index.html
Normal file
16
index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
|
||||
<title>Solid App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
48
package.json
Normal file
48
package.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "vite-template-solid",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"postinstall": "cp ./node_modules/sql.js/dist/sql-wasm.wasm ./src/assets/sql-wasm.wasm"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||
"@typescript-eslint/parser": "^8.17.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-solid": "^0.14.4",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^6.0.3",
|
||||
"vite-plugin-solid": "^2.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kobalte/core": "^0.13.7",
|
||||
"@kobalte/tailwindcss": "^0.9.0",
|
||||
"@solid-primitives/refs": "^1.0.8",
|
||||
"@solidjs/router": "^0.15.1",
|
||||
"chart.js": "^4.4.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"solid-js": "^1.9.3",
|
||||
"sql.js": "^1.12.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
3158
pnpm-lock.yaml
generated
Normal file
3158
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
9
prettier.config.js
Normal file
9
prettier.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
semi: true,
|
||||
trailingComma: "all",
|
||||
singleQuote: false,
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
singleAttributePerLine: true,
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
};
|
33
src/App.module.css
Normal file
33
src/App.module.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
.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);
|
||||
}
|
||||
}
|
20
src/App.tsx
Normal file
20
src/App.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type Component } from "solid-js";
|
||||
import { Route } from "@solidjs/router";
|
||||
import { Home, Overview } from "./pages";
|
||||
|
||||
const App: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<Route
|
||||
path="/"
|
||||
component={Home}
|
||||
/>
|
||||
<Route
|
||||
path="/overview"
|
||||
component={Overview}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
141
src/app.css
Normal file
141
src/app.css
Normal file
|
@ -0,0 +1,141 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--info: 204 94% 94%;
|
||||
--info-foreground: 199 89% 48%;
|
||||
|
||||
--success: 149 80% 90%;
|
||||
--success-foreground: 160 84% 39%;
|
||||
|
||||
--warning: 48 96% 89%;
|
||||
--warning-foreground: 25 95% 53%;
|
||||
|
||||
--error: 0 93% 94%;
|
||||
--error-foreground: 0 84% 60%;
|
||||
|
||||
--ring: 240 5.9% 10%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark,
|
||||
[data-kb-theme="dark"] {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--info: 204 94% 94%;
|
||||
--info-foreground: 199 89% 48%;
|
||||
|
||||
--success: 149 80% 90%;
|
||||
--success-foreground: 160 84% 39%;
|
||||
|
||||
--warning: 48 96% 89%;
|
||||
--warning-foreground: 25 95% 53%;
|
||||
|
||||
--error: 0 93% 94%;
|
||||
--error-foreground: 0 84% 60%;
|
||||
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings:
|
||||
"rlig" 1,
|
||||
"calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.step {
|
||||
counter-increment: step;
|
||||
}
|
||||
|
||||
.step:before {
|
||||
@apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background;
|
||||
@apply ml-[-50px] mt-[-4px];
|
||||
content: counter(step);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 9999px;
|
||||
border: 4px solid transparent;
|
||||
background-clip: content-box;
|
||||
@apply bg-accent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
292
src/components/ui/charts.tsx
Normal file
292
src/components/ui/charts.tsx
Normal file
|
@ -0,0 +1,292 @@
|
|||
import type { Component } from "solid-js"
|
||||
import { createEffect, createSignal, mergeProps, on, onCleanup, onMount } from "solid-js"
|
||||
import { unwrap } from "solid-js/store"
|
||||
|
||||
import type { Ref } from "@solid-primitives/refs"
|
||||
import { mergeRefs } from "@solid-primitives/refs"
|
||||
import type {
|
||||
ChartComponent,
|
||||
ChartData,
|
||||
ChartItem,
|
||||
ChartOptions,
|
||||
Plugin as ChartPlugin,
|
||||
ChartType,
|
||||
ChartTypeRegistry,
|
||||
TooltipModel
|
||||
} from "chart.js"
|
||||
import {
|
||||
ArcElement,
|
||||
BarController,
|
||||
BarElement,
|
||||
BubbleController,
|
||||
CategoryScale,
|
||||
Chart,
|
||||
Colors,
|
||||
DoughnutController,
|
||||
Filler,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
PieController,
|
||||
PointElement,
|
||||
PolarAreaController,
|
||||
RadarController,
|
||||
RadialLinearScale,
|
||||
ScatterController,
|
||||
Tooltip
|
||||
} from "chart.js"
|
||||
|
||||
type TypedChartProps = {
|
||||
data: ChartData
|
||||
options?: ChartOptions
|
||||
plugins?: ChartPlugin[]
|
||||
ref?: Ref<HTMLCanvasElement | null>
|
||||
width?: number | undefined
|
||||
height?: number | undefined
|
||||
}
|
||||
|
||||
type ChartProps = TypedChartProps & {
|
||||
type: ChartType
|
||||
}
|
||||
|
||||
type ChartContext = {
|
||||
chart: Chart
|
||||
tooltip: TooltipModel<keyof ChartTypeRegistry>
|
||||
}
|
||||
|
||||
const BaseChart: Component<ChartProps> = (rawProps) => {
|
||||
const [canvasRef, setCanvasRef] = createSignal<HTMLCanvasElement | null>()
|
||||
const [chart, setChart] = createSignal<Chart>()
|
||||
|
||||
const props = mergeProps(
|
||||
{
|
||||
width: 512,
|
||||
height: 512,
|
||||
options: { responsive: true } as ChartOptions,
|
||||
plugins: [] as ChartPlugin[]
|
||||
},
|
||||
rawProps
|
||||
)
|
||||
|
||||
const init = () => {
|
||||
const ctx = canvasRef()?.getContext("2d") as ChartItem
|
||||
const config = unwrap(props)
|
||||
const chart = new Chart(ctx, {
|
||||
type: config.type,
|
||||
data: config.data,
|
||||
options: config.options,
|
||||
plugins: config.plugins
|
||||
})
|
||||
setChart(chart)
|
||||
}
|
||||
|
||||
onMount(() => init())
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.data,
|
||||
() => {
|
||||
chart()!.data = props.data
|
||||
chart()!.update()
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.options,
|
||||
() => {
|
||||
chart()!.options = props.options
|
||||
chart()!.update()
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
[() => props.width, () => props.height],
|
||||
() => {
|
||||
chart()!.resize(props.width, props.height)
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.type,
|
||||
() => {
|
||||
const dimensions = [chart()!.width, chart()!.height]
|
||||
chart()!.destroy()
|
||||
init()
|
||||
chart()!.resize(...dimensions)
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
chart()?.destroy()
|
||||
mergeRefs(props.ref, null)
|
||||
})
|
||||
|
||||
Chart.register(Colors, Filler, Legend, Tooltip)
|
||||
return (
|
||||
<canvas
|
||||
ref={mergeRefs(props.ref, (el) => setCanvasRef(el))}
|
||||
height={props.height}
|
||||
width={props.width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function showTooltip(context: ChartContext) {
|
||||
let el = document.getElementById("chartjs-tooltip")
|
||||
if (!el) {
|
||||
el = document.createElement("div")
|
||||
el.id = "chartjs-tooltip"
|
||||
document.body.appendChild(el)
|
||||
}
|
||||
|
||||
const model = context.tooltip
|
||||
if (model.opacity === 0 || !model.body) {
|
||||
el.style.opacity = "0"
|
||||
return
|
||||
}
|
||||
|
||||
el.className = `p-2 bg-card text-card-foreground rounded-lg border shadow-sm text-sm ${
|
||||
model.yAlign ?? `no-transform`
|
||||
}`
|
||||
|
||||
let content = ""
|
||||
|
||||
model.title.forEach((title) => {
|
||||
content += `<h3 class="font-semibold leading-none tracking-tight">${title}</h3>`
|
||||
})
|
||||
|
||||
content += `<div class="mt-1 text-muted-foreground">`
|
||||
const body = model.body.flatMap((body) => body.lines)
|
||||
body.forEach((line, i) => {
|
||||
const colors = model.labelColors[i]
|
||||
content += `
|
||||
<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>
|
||||
${line}
|
||||
</div>`
|
||||
})
|
||||
content += `</div>`
|
||||
|
||||
el.innerHTML = content
|
||||
|
||||
const pos = context.chart.canvas.getBoundingClientRect()
|
||||
el.style.opacity = "1"
|
||||
el.style.position = "absolute"
|
||||
el.style.left = `${pos.left + window.scrollX + model.caretX}px`
|
||||
el.style.top = `${pos.top + window.scrollY + model.caretY}px`
|
||||
el.style.pointerEvents = "none"
|
||||
}
|
||||
|
||||
function createTypedChart(
|
||||
type: ChartType,
|
||||
components: ChartComponent[]
|
||||
): Component<TypedChartProps> {
|
||||
const chartsWithScales: ChartType[] = ["bar", "line", "scatter"]
|
||||
const chartsWithLegends: ChartType[] = ["bar", "line"]
|
||||
|
||||
const options: ChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: chartsWithScales.includes(type)
|
||||
? {
|
||||
x: {
|
||||
border: { display: false },
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
border: {
|
||||
dash: [3],
|
||||
dashOffset: 3,
|
||||
display: false
|
||||
},
|
||||
grid: {
|
||||
color: "hsla(240, 3.8%, 46.1%, 0.4)"
|
||||
}
|
||||
}
|
||||
}
|
||||
: {},
|
||||
plugins: {
|
||||
legend: chartsWithLegends.includes(type)
|
||||
? {
|
||||
display: true,
|
||||
align: "end",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
boxWidth: 6,
|
||||
boxHeight: 6,
|
||||
color: "hsl(240, 3.8%, 46.1%)",
|
||||
font: { size: 14 }
|
||||
}
|
||||
}
|
||||
: { display: false },
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
external: (context) => showTooltip(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Chart.register(...components)
|
||||
return (props) => <BaseChart type={type} options={options} {...props} />
|
||||
}
|
||||
|
||||
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", [
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale
|
||||
])
|
||||
const PieChart = /* #__PURE__ */ createTypedChart("pie", [PieController, ArcElement])
|
||||
const PolarAreaChart = /* #__PURE__ */ createTypedChart("polarArea", [
|
||||
PolarAreaController,
|
||||
ArcElement,
|
||||
RadialLinearScale
|
||||
])
|
||||
const RadarChart = /* #__PURE__ */ createTypedChart("radar", [
|
||||
RadarController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
RadialLinearScale
|
||||
])
|
||||
const ScatterChart = /* #__PURE__ */ createTypedChart("scatter", [
|
||||
ScatterController,
|
||||
PointElement,
|
||||
LinearScale
|
||||
])
|
||||
|
||||
export {
|
||||
BaseChart as Chart,
|
||||
BarChart,
|
||||
BubbleChart,
|
||||
DonutChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
PolarAreaChart,
|
||||
RadarChart,
|
||||
ScatterChart
|
||||
}
|
89
src/db.ts
Normal file
89
src/db.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import initSqlJS, { type Database } from "sql.js";
|
||||
|
||||
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 SQL = await initSqlJS({
|
||||
locateFile: () => wasmURL,
|
||||
});
|
||||
|
||||
const file = await fetch(dbURL).then((res) => res.arrayBuffer());
|
||||
|
||||
const testDb = new SQL.Database(new Uint8Array(file));
|
||||
|
||||
export const [db, setDb] = createSignal<Database>(testDb);
|
||||
|
||||
const createStatement = (sql: string) => {
|
||||
return createMemo(() => {
|
||||
return db().prepare(sql);
|
||||
});
|
||||
};
|
||||
|
||||
export const roomOverviewStmt = createStatement(`
|
||||
SELECT
|
||||
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 = {
|
||||
recipient_id: number;
|
||||
active: 0 | 1;
|
||||
archived: 0 | 1;
|
||||
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(`
|
||||
SELECT
|
||||
COUNT(*) as message_count
|
||||
FROM
|
||||
message
|
||||
WHERE (message.from_recipient_id = :recipient_id AND message.body IS NOT NULL AND message.body != '')
|
||||
`);
|
13
src/index.css
Normal file
13
src/index.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
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;
|
||||
}
|
23
src/index.tsx
Normal file
23
src/index.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
/* @refresh reload */
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { Router } from "@solidjs/router";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
throw new Error(
|
||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?"
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
() => (
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
),
|
||||
root!
|
||||
);
|
53
src/lib/database.ts
Normal file
53
src/lib/database.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
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",
|
||||
];
|
44
src/lib/sql-queries.ts
Normal file
44
src/lib/sql-queries.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
// select messages from one chat / group
|
||||
`SELECT
|
||||
message.body,
|
||||
message.from_recipient_id,
|
||||
-- message."type" AS message_type,
|
||||
FROM
|
||||
message
|
||||
JOIN thread ON message.thread_id = thread._id
|
||||
JOIN recipient ON thread.recipient_id = recipient._id
|
||||
WHERE
|
||||
recipient._id = 1433
|
||||
AND
|
||||
message.body IS NOT NULL`;
|
||||
|
||||
// select messages from one chat / group with details of sender
|
||||
`SELECT
|
||||
message.body,
|
||||
message.from_recipient_id,
|
||||
-- message."type" AS message_type,
|
||||
sender.system_joined_name,
|
||||
sender.profile_joined_name
|
||||
FROM
|
||||
message
|
||||
JOIN thread ON message.thread_id = thread._id
|
||||
JOIN recipient AS r ON thread.recipient_id = r._id
|
||||
JOIN recipient AS sender ON message.from_recipient_id = sender._id
|
||||
WHERE
|
||||
r._id = 4
|
||||
AND message.body IS NOT NULL
|
||||
AND message.body != ''`;
|
||||
|
||||
// select all chats / groups with details
|
||||
`SELECT
|
||||
thread.recipient_id,
|
||||
thread.active,
|
||||
recipient.profile_joined_name,
|
||||
recipient.system_joined_name,
|
||||
groups.title
|
||||
FROM
|
||||
thread
|
||||
JOIN recipient ON thread.recipient_id = recipient._id
|
||||
LEFT JOIN groups ON recipient._id = groups.recipient_id
|
||||
WHERE (SELECT 1 FROM message WHERE message.thread_id = thread._id AND message.body IS NOT NULL)
|
||||
`;
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
32
src/pages/home.tsx
Normal file
32
src/pages/home.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { redirect, useNavigate, type RouteSectionProps } from "@solidjs/router";
|
||||
import { type Component, type JSX } from "solid-js";
|
||||
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();
|
||||
|
||||
reader.addEventListener("load", () => {
|
||||
const Uints = new Uint8Array(reader.result as ArrayBuffer);
|
||||
setDb(new SQL.Database(Uints));
|
||||
navigate("/overview");
|
||||
});
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".sqlite"
|
||||
onChange={onFileChange}
|
||||
></input>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
5
src/pages/index.tsx
Normal file
5
src/pages/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { lazy } from "solid-js";
|
||||
|
||||
export { Home } from "./home";
|
||||
|
||||
export const Overview = lazy(() => import("./overview"));
|
47
src/pages/overview.tsx
Normal file
47
src/pages/overview.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
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;
|
104
tailwind.config.js
Normal file
104
tailwind.config.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ["variant", [".dark &", '[data-kb-theme="dark"] &']],
|
||||
content: ["./src/**/*.{ts,tsx}"],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
info: {
|
||||
DEFAULT: "hsl(var(--info))",
|
||||
foreground: "hsl(var(--info-foreground))",
|
||||
},
|
||||
success: {
|
||||
DEFAULT: "hsl(var(--success))",
|
||||
foreground: "hsl(var(--success-foreground))",
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "hsl(var(--warning))",
|
||||
foreground: "hsl(var(--warning-foreground))",
|
||||
},
|
||||
error: {
|
||||
DEFAULT: "hsl(var(--error))",
|
||||
foreground: "hsl(var(--error-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
xl: "calc(var(--radius) + 4px)",
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--kb-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--kb-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
"content-show": {
|
||||
from: { opacity: 0, transform: "scale(0.96)" },
|
||||
to: { opacity: 1, transform: "scale(1)" },
|
||||
},
|
||||
"content-hide": {
|
||||
from: { opacity: 1, transform: "scale(1)" },
|
||||
to: { opacity: 0, transform: "scale(0.96)" },
|
||||
},
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"content-show": "content-show 0.2s ease-out",
|
||||
"content-hide": "content-hide 0.2s ease-out",
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@kobalte/tailwindcss")],
|
||||
};
|
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
},
|
||||
// "files": [
|
||||
// "./eslint.config.js",
|
||||
// "./prettier.config.js",
|
||||
// "./tailwind.config.js",
|
||||
// "./postcss.config.js",
|
||||
// ]
|
||||
}
|
13
ui.config.json
Normal file
13
ui.config.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://solid-ui.com/schema.json",
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"config": "tailwind.config.js",
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "~/components/ui",
|
||||
"utils": "~/lib/utils"
|
||||
}
|
||||
}
|
19
vite.config.ts
Normal file
19
vite.config.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import path from "path";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import solidPlugin from "vite-plugin-solid";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
build: {
|
||||
target: "esnext",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue