diff --git a/app.config.ts b/app.config.ts index 5de386a..01ecda3 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,12 +1,21 @@ import { defineConfig } from "@solidjs/start/config"; import unocss from "unocss/vite"; +import { client, router } from "./socket"; -export default defineConfig({ +const app = defineConfig({ + ssr: false, server: { preset: "netlify", + experimental: { + websocket: true, + }, }, vite: { - plugins: [unocss()], + plugins: [unocss(), client()], ssr: { external: ["@prisma/client"] }, }, }); + +app.addRouter(router); + +export default app; diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..7af5848 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index 9023bd3..6b79871 100644 --- a/package.json +++ b/package.json @@ -19,23 +19,31 @@ }, "dependencies": { "@formkit/auto-animate": "^0.8.1", + "@kobalte/core": "^0.13.7", "@libsql/client": "^0.6.1", "@prisma/adapter-libsql": "^5.14.0", "@prisma/client": "^5.7.0", + "@solid-primitives/memo": "^1.3.10", + "@solid-primitives/mouse": "^2.0.20", + "@solid-primitives/rootless": "^1.4.5", + "@solid-primitives/scheduled": "^1.4.4", "@solidjs/meta": "^0.29.3", "@solidjs/router": "^0.13.3", "@solidjs/start": "^1.0.1", "@unocss/reset": "^0.58.5", + "@vinxi/plugin-directives": "^0.4.3", "better-sqlite3": "^9.4.3", "immer": "^10.0.4", "kysely": "^0.27.3", "lowdb": "^7.0.1", "prisma": "^5.7.0", + "rxjs-for-await": "^1.0.0", "solid-icons": "^1.1.0", "solid-js": "^1.8.17", "unimport": "^3.7.1", + "unique-names-generator": "^4.7.1", "unocss-preset-theme": "^0.12.0", - "vinxi": "^0.3.11" + "vinxi": "^0.3.14" }, "engines": { "node": ">=18" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f39a50e..6e0daf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,10 +28,13 @@ importers: version: 0.13.6(solid-js@1.8.21) '@solidjs/start': specifier: ^1.0.1 - version: 1.0.6(solid-js@1.8.21)(vinxi@0.3.14)(vite@5.4.2) + version: 1.0.6(rollup@4.21.0)(solid-js@1.8.21)(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) '@unocss/reset': specifier: ^0.58.5 version: 0.58.9 + '@vinxi/plugin-directives': + specifier: ^0.4.3 + version: 0.4.3(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)) better-sqlite3: specifier: ^9.4.3 version: 9.6.0 @@ -47,6 +50,9 @@ importers: prisma: specifier: ^5.7.0 version: 5.18.0 + rxjs-for-await: + specifier: ^1.0.0 + version: 1.0.0(rxjs@7.8.1) solid-icons: specifier: ^1.1.0 version: 1.1.0(solid-js@1.8.21) @@ -60,8 +66,8 @@ importers: specifier: ^0.12.0 version: 0.12.0(@unocss/core@0.62.2) vinxi: - specifier: ^0.3.11 - version: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0) + specifier: ^0.3.14 + version: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6) devDependencies: '@tailwindcss/forms': specifier: ^0.5.7 @@ -74,7 +80,7 @@ importers: version: 20.16.1 '@unscatty/unocss-preset-daisy': specifier: ^1.0.0 - version: 1.0.0(unocss@0.58.9) + version: 1.0.0(unocss@0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))) autoprefixer: specifier: ^10.4.16 version: 10.4.20(postcss@8.4.41) @@ -86,7 +92,7 @@ importers: version: 3.4.10 unocss: specifier: ^0.58.5 - version: 0.58.9(postcss@8.4.41)(vite@5.4.2) + version: 0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) packages: @@ -1111,6 +1117,11 @@ packages: peerDependencies: vinxi: ^0.4.0 + '@vinxi/plugin-directives@0.4.3': + resolution: {integrity: sha512-Ey+TRIwyk8871PKhQel8NyZ9B6N0Tvhjo1QIttTyrV0d7BfUpri5GyGygmBY7fHClSE/vqaNCCZIKpTL3NJAEg==} + peerDependencies: + vinxi: ^0.4.3 + '@vinxi/server-components@0.4.1': resolution: {integrity: sha512-rMS+RCGr1tujO1xWgILMLpOWIyw2OwDO46EtkuhTfqaVgLLt/w7+hxzOnh4s3O9sXoKKuUswtj9/MpQQkFoMOQ==} peerDependencies: @@ -2056,7 +2067,6 @@ packages: libsql@0.3.19: resolution: {integrity: sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA==} - cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] lilconfig@2.1.0: @@ -2580,6 +2590,14 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs-for-await@1.0.0: + resolution: {integrity: sha512-MJhvf1vtQaljd5wlzsasvOjcohVogzkHkUI0gFE9nGhZ15/fT2vR1CjkLEh37oRqWwpv11vHo5D+sLM+Aw9Y8g==} + peerDependencies: + rxjs: ^7.0.0 + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -3742,7 +3760,7 @@ snapshots: async-mutex: 0.5.0 '@prisma/client@5.18.0(prisma@5.18.0)': - dependencies: + optionalDependencies: prisma: 5.18.0 '@prisma/debug@5.18.0': {} @@ -3772,8 +3790,9 @@ snapshots: '@rollup/plugin-alias@5.1.0(rollup@4.21.0)': dependencies: - rollup: 4.21.0 slash: 4.0.0 + optionalDependencies: + rollup: 4.21.0 '@rollup/plugin-commonjs@25.0.8(rollup@4.21.0)': dependencies: @@ -3783,6 +3802,7 @@ snapshots: glob: 8.1.0 is-reference: 1.2.1 magic-string: 0.30.11 + optionalDependencies: rollup: 4.21.0 '@rollup/plugin-inject@5.0.5(rollup@4.21.0)': @@ -3790,11 +3810,13 @@ snapshots: '@rollup/pluginutils': 5.1.0(rollup@4.21.0) estree-walker: 2.0.2 magic-string: 0.30.11 + optionalDependencies: rollup: 4.21.0 '@rollup/plugin-json@6.1.0(rollup@4.21.0)': dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.21.0) + optionalDependencies: rollup: 4.21.0 '@rollup/plugin-node-resolve@15.2.3(rollup@4.21.0)': @@ -3805,20 +3827,23 @@ snapshots: is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.8 + optionalDependencies: rollup: 4.21.0 '@rollup/plugin-replace@5.0.7(rollup@4.21.0)': dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.21.0) magic-string: 0.30.11 + optionalDependencies: rollup: 4.21.0 '@rollup/plugin-terser@0.4.4(rollup@4.21.0)': dependencies: - rollup: 4.21.0 serialize-javascript: 6.0.2 smob: 1.5.0 terser: 5.31.6 + optionalDependencies: + rollup: 4.21.0 '@rollup/pluginutils@4.2.1': dependencies: @@ -3830,6 +3855,7 @@ snapshots: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 + optionalDependencies: rollup: 4.21.0 '@rollup/rollup-android-arm-eabi@4.21.0': @@ -3890,11 +3916,11 @@ snapshots: dependencies: solid-js: 1.8.21 - '@solidjs/start@1.0.6(solid-js@1.8.21)(vinxi@0.3.14)(vite@5.4.2)': + '@solidjs/start@1.0.6(rollup@4.21.0)(solid-js@1.8.21)(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))': dependencies: - '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14) - '@vinxi/server-components': 0.4.1(vinxi@0.3.14) - '@vinxi/server-functions': 0.4.1(vinxi@0.3.14) + '@vinxi/plugin-directives': 0.4.3(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)) + '@vinxi/server-components': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)) + '@vinxi/server-functions': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)) defu: 6.1.4 error-stack-parser: 2.1.4 glob: 10.4.5 @@ -3905,8 +3931,8 @@ snapshots: shikiji: 0.9.19 source-map-js: 1.2.0 terracotta: 1.0.5(solid-js@1.8.21) - vite-plugin-inspect: 0.7.42(vite@5.4.2) - vite-plugin-solid: 2.10.2(solid-js@1.8.21)(vite@5.4.2) + vite-plugin-inspect: 0.7.42(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) + vite-plugin-solid: 2.10.2(solid-js@1.8.21)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) transitivePeerDependencies: - '@nuxt/kit' - '@testing-library/jest-dom' @@ -3973,16 +3999,17 @@ snapshots: dependencies: '@types/node': 20.16.1 - '@unocss/astro@0.58.9(vite@5.4.2)': + '@unocss/astro@0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))': dependencies: '@unocss/core': 0.58.9 '@unocss/reset': 0.58.9 - '@unocss/vite': 0.58.9(vite@5.4.2) - vite: 5.4.2(@types/node@20.16.1) + '@unocss/vite': 0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) + optionalDependencies: + vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6) transitivePeerDependencies: - rollup - '@unocss/cli@0.58.9': + '@unocss/cli@0.58.9(rollup@4.21.0)': dependencies: '@ampproject/remapping': 2.3.0 '@rollup/pluginutils': 5.1.0(rollup@4.21.0) @@ -4118,7 +4145,7 @@ snapshots: dependencies: '@unocss/core': 0.58.9 - '@unocss/vite@0.58.9(vite@5.4.2)': + '@unocss/vite@0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))': dependencies: '@ampproject/remapping': 2.3.0 '@rollup/pluginutils': 5.1.0(rollup@4.21.0) @@ -4130,11 +4157,11 @@ snapshots: chokidar: 3.6.0 fast-glob: 3.3.2 magic-string: 0.30.11 - vite: 5.4.2(@types/node@20.16.1) + vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6) transitivePeerDependencies: - rollup - '@unscatty/unocss-preset-daisy@1.0.0(unocss@0.58.9)': + '@unscatty/unocss-preset-daisy@1.0.0(unocss@0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)))': dependencies: '@tailwindcss/nesting': 0.0.0-insiders.565cd3e(postcss@8.4.41) '@unocss/rule-utils': 0.57.7 @@ -4143,7 +4170,7 @@ snapshots: parsel-js: 1.1.2 postcss: 8.4.41 postcss-js: 4.0.1(postcss@8.4.41) - unocss: 0.58.9(postcss@8.4.41)(vite@5.4.2) + unocss: 0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) '@vercel/nft@0.26.5': dependencies: @@ -4185,7 +4212,7 @@ snapshots: transitivePeerDependencies: - uWebSockets.js - '@vinxi/plugin-directives@0.4.1(vinxi@0.3.14)': + '@vinxi/plugin-directives@0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))': dependencies: '@babel/parser': 7.25.3 acorn: 8.12.1 @@ -4196,29 +4223,42 @@ snapshots: magicast: 0.2.11 recast: 0.23.9 tslib: 2.6.3 - vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0) + vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6) - '@vinxi/server-components@0.4.1(vinxi@0.3.14)': + '@vinxi/plugin-directives@0.4.3(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))': dependencies: - '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14) + '@babel/parser': 7.25.3 acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) acorn-loose: 8.4.0 acorn-typescript: 1.4.13(acorn@8.12.1) astring: 1.8.6 magicast: 0.2.11 recast: 0.23.9 - vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0) + tslib: 2.6.3 + vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6) - '@vinxi/server-functions@0.4.1(vinxi@0.3.14)': + '@vinxi/server-components@0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))': dependencies: - '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14) + '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)) acorn: 8.12.1 acorn-loose: 8.4.0 acorn-typescript: 1.4.13(acorn@8.12.1) astring: 1.8.6 magicast: 0.2.11 recast: 0.23.9 - vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0) + vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6) + + '@vinxi/server-functions@0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))': + dependencies: + '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)) + acorn: 8.12.1 + acorn-loose: 8.4.0 + acorn-typescript: 1.4.13(acorn@8.12.1) + astring: 1.8.6 + magicast: 0.2.11 + recast: 0.23.9 + vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6) abbrev@1.1.1: {} @@ -4582,7 +4622,7 @@ snapshots: undici-types: 5.28.4 db0@0.1.4(@libsql/client@0.6.2)(better-sqlite3@9.6.0): - dependencies: + optionalDependencies: '@libsql/client': 0.6.2 better-sqlite3: 9.6.0 @@ -5562,8 +5602,9 @@ snapshots: postcss-load-config@4.0.2(postcss@8.4.41): dependencies: lilconfig: 3.1.2 - postcss: 8.4.41 yaml: 2.5.0 + optionalDependencies: + postcss: 8.4.41 postcss-nested@5.0.6(postcss@8.4.41): dependencies: @@ -5714,9 +5755,10 @@ snapshots: dependencies: open: 8.4.2 picomatch: 2.3.1 - rollup: 4.21.0 source-map: 0.7.4 yargs: 17.7.2 + optionalDependencies: + rollup: 4.21.0 rollup@4.21.0: dependencies: @@ -5748,6 +5790,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs-for-await@1.0.0(rxjs@7.8.1): + dependencies: + rxjs: 7.8.1 + + rxjs@7.8.1: + dependencies: + tslib: 2.6.3 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -6120,10 +6170,10 @@ snapshots: '@unocss/core': 0.62.2 '@unocss/rule-utils': 0.58.9 - unocss@0.58.9(postcss@8.4.41)(vite@5.4.2): + unocss@0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)): dependencies: - '@unocss/astro': 0.58.9(vite@5.4.2) - '@unocss/cli': 0.58.9 + '@unocss/astro': 0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) + '@unocss/cli': 0.58.9(rollup@4.21.0) '@unocss/core': 0.58.9 '@unocss/extractor-arbitrary-variants': 0.58.9 '@unocss/postcss': 0.58.9(postcss@8.4.41) @@ -6141,8 +6191,9 @@ snapshots: '@unocss/transformer-compile-class': 0.58.9 '@unocss/transformer-directives': 0.58.9 '@unocss/transformer-variant-group': 0.58.9 - '@unocss/vite': 0.58.9(vite@5.4.2) - vite: 5.4.2(@types/node@20.16.1) + '@unocss/vite': 0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) + optionalDependencies: + vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6) transitivePeerDependencies: - postcss - rollup @@ -6160,14 +6211,15 @@ snapshots: anymatch: 3.1.3 chokidar: 3.6.0 destr: 2.0.3 - h3: 1.11.1 - ioredis: 5.4.1 + h3: 1.12.0 listhen: 1.7.2 lru-cache: 10.4.3 mri: 1.2.0 node-fetch-native: 1.6.4 ofetch: 1.3.4 ufo: 1.5.4 + optionalDependencies: + ioredis: 5.4.1 transitivePeerDependencies: - uWebSockets.js @@ -6202,7 +6254,7 @@ snapshots: validate-html-nesting@1.2.2: {} - vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0): + vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6): dependencies: '@babel/core': 7.25.2 '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) @@ -6236,7 +6288,7 @@ snapshots: unctx: 2.3.1 unenv: 1.10.0 unstorage: 1.10.2(ioredis@5.4.1) - vite: 5.4.2(@types/node@20.16.1) + vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6) zod: 3.23.8 transitivePeerDependencies: - '@azure/app-configuration' @@ -6270,7 +6322,7 @@ snapshots: - uWebSockets.js - xml2js - vite-plugin-inspect@0.7.42(vite@5.4.2): + vite-plugin-inspect@0.7.42(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.0(rollup@4.21.0) @@ -6280,12 +6332,12 @@ snapshots: open: 9.1.0 picocolors: 1.0.1 sirv: 2.0.4 - vite: 5.4.2(@types/node@20.16.1) + vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6) transitivePeerDependencies: - rollup - supports-color - vite-plugin-solid@2.10.2(solid-js@1.8.21)(vite@5.4.2): + vite-plugin-solid@2.10.2(solid-js@1.8.21)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)): dependencies: '@babel/core': 7.25.2 '@types/babel__core': 7.20.5 @@ -6293,23 +6345,24 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.8.21 solid-refresh: 0.6.3(solid-js@1.8.21) - vite: 5.4.2(@types/node@20.16.1) - vitefu: 0.2.5(vite@5.4.2) + vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6) + vitefu: 0.2.5(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)) transitivePeerDependencies: - supports-color - vite@5.4.2(@types/node@20.16.1): + vite@5.4.2(@types/node@20.16.1)(terser@5.31.6): dependencies: - '@types/node': 20.16.1 esbuild: 0.21.5 postcss: 8.4.41 rollup: 4.21.0 optionalDependencies: + '@types/node': 20.16.1 fsevents: 2.3.3 + terser: 5.31.6 - vitefu@0.2.5(vite@5.4.2): - dependencies: - vite: 5.4.2(@types/node@20.16.1) + vitefu@0.2.5(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)): + optionalDependencies: + vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6) web-streams-polyfill@3.3.3: {} diff --git a/prisma/dev.db b/prisma/dev.db index 123a99f..5ff34e9 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/socket/imports/compiler/babel.ts b/socket/imports/compiler/babel.ts new file mode 100644 index 0000000..1d3736b --- /dev/null +++ b/socket/imports/compiler/babel.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as babel from "@babel/core"; +import { ImportPluginOptions } from "."; + +const specificImports = [ + "createMemo", + "createRoot", + "createSignal", + "createEffect", + "from", + "observable", + "untrack", + "onCleanup", +]; + +export function createTransform$(opts?: ImportPluginOptions) { + return function transform$({ + types: t, + template: temp, + }: { + types: typeof babel.types; + template: typeof babel.template; + }): babel.PluginObj { + return { + visitor: { + ImportDeclaration(path) { + if (path.node.source.value === "solid-js") { + const specificSpecifiers = path.node.specifiers.filter( + (specifier) => + t.isImportSpecifier(specifier) && + specificImports.includes((specifier.imported as any).name) + ); + const otherSpecifiers = path.node.specifiers.filter( + (specifier) => + t.isImportSpecifier(specifier) && + !specificImports.includes((specifier.imported as any).name) + ); + if (specificSpecifiers.length > 0) { + const newImportDeclaration = t.importDeclaration( + specificSpecifiers, + t.stringLiteral("solid-js/dist/solid") + ); + path.insertAfter(newImportDeclaration); + if (otherSpecifiers.length > 0) { + path.node.specifiers = otherSpecifiers; + } else { + path.remove(); + } + } + } + }, + }, + }; + }; +} + +export async function compilepImports( + code: string, + id: string, + opts?: ImportPluginOptions +) { + try { + const plugins: babel.ParserOptions["plugins"] = ["typescript", "jsx"]; + const transform$ = createTransform$(opts); + const transformed = await babel.transformAsync(code, { + presets: [["@babel/preset-typescript"], ...(opts?.babel?.presets ?? [])], + parserOpts: { + plugins, + }, + plugins: [[transform$], ...(opts?.babel?.plugins ?? [])], + filename: id, + }); + if (transformed) { + if (opts?.log) { + console.log(id, transformed.code); + } + return { + code: transformed.code ?? "", + map: transformed.map, + }; + } + return null; + } catch (e) { + console.error("err$$", e); + return null; + } +} diff --git a/socket/imports/compiler/index.ts b/socket/imports/compiler/index.ts new file mode 100644 index 0000000..5db11b6 --- /dev/null +++ b/socket/imports/compiler/index.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type babel from "@babel/core"; +import { type FilterPattern } from "@rollup/pluginutils"; +export { compilepImports } from "./babel"; + +export type ImportPluginOptions = { + babel?: babel.TransformOptions; + filter?: { + include?: FilterPattern; + exclude?: FilterPattern; + }; + log?: boolean; +}; + +export * from "./babel"; diff --git a/socket/imports/index.ts b/socket/imports/index.ts new file mode 100644 index 0000000..8c205b3 --- /dev/null +++ b/socket/imports/index.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Plugin } from "vite"; +import { compilepImports, type ImportPluginOptions } from "./compiler"; +import { repushPlugin, getFilter } from "./utils"; + +export function importsPlugin(opts?: ImportPluginOptions): Plugin { + const filter = getFilter(opts?.filter); + const plugin: Plugin = { + enforce: "pre", + name: "imports", + async transform(code, id) { + if (!filter(id)) { + return code; + } + if (id.endsWith(".ts") || id.endsWith(".tsx")) { + return await compilepImports(code, id, opts); + } + return undefined; + }, + configResolved(config) { + repushPlugin(config.plugins as Plugin[], plugin, [ + "vite-server-references", + "solid", + "vinxi:routes", + ]); + }, + }; + return plugin; +} diff --git a/socket/imports/utils.ts b/socket/imports/utils.ts new file mode 100644 index 0000000..495a017 --- /dev/null +++ b/socket/imports/utils.ts @@ -0,0 +1,54 @@ +import { createFilter, FilterPattern } from "@rollup/pluginutils"; +import { Plugin } from "vite"; + +export const DEFAULT_INCLUDE = "{src,socket}/**/*.{jsx,tsx,ts,js,mjs,cjs}"; +export const DEFAULT_EXCLUDE = "node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}"; + +export function getFileName(_filename: string): string { + if (_filename.includes("?")) { + // might be useful for the future + const [actualId] = _filename.split("?"); + return actualId; + } + return _filename; +} + +export const getFilter = (f?: { + include?: FilterPattern; + exclude?: FilterPattern; +}) => { + const filter = createFilter( + f?.include ?? DEFAULT_INCLUDE, + f?.exclude ?? DEFAULT_EXCLUDE + ); + return (id: string) => { + const actualName = getFileName(id); + return filter(actualName); + }; +}; + +// From: https://github.com/bluwy/whyframe/blob/master/packages/jsx/src/index.js#L27-L37 +export function repushPlugin( + plugins: Plugin[], + plugin: Plugin | string, + pluginNames: string[] +): void { + const namesSet = new Set(pluginNames); + const name = typeof plugin === "string" ? plugin : plugin.name; + const currentPlugin = plugins.find((e) => e.name === name)!; + let baseIndex = -1; + let targetIndex = -1; + for (let i = 0, len = plugins.length; i < len; i += 1) { + const current = plugins[i]; + if (namesSet.has(current.name) && baseIndex === -1) { + baseIndex = i; + } + if (current.name === name) { + targetIndex = i; + } + } + if (baseIndex !== -1 && targetIndex !== -1 && baseIndex < targetIndex) { + plugins.splice(targetIndex, 1); + plugins.splice(baseIndex, 0, currentPlugin); + } +} diff --git a/socket/index.ts b/socket/index.ts new file mode 100644 index 0000000..bdb789e --- /dev/null +++ b/socket/index.ts @@ -0,0 +1,23 @@ +import { normalize } from "vinxi/lib/path"; +export { client } from "./plugin/client"; +import { server } from "./plugin/server"; +import { fileURLToPath } from "url"; +import { importsPlugin } from "./imports"; + +export const router = { + name: "socket-fns", + type: "http", + base: "/_ws", + handler: "./socket/plugin/server-handler.ts", + target: "server", + plugins: () => [ + server({ + runtime: normalize( + fileURLToPath( + new URL("./socket/plugin/server-runtime.js", import.meta.url) + ) + ), + }), + importsPlugin(), + ], +}; diff --git a/socket/lib/client.tsx b/socket/lib/client.tsx new file mode 100644 index 0000000..710334b --- /dev/null +++ b/socket/lib/client.tsx @@ -0,0 +1,232 @@ +import { from as rxFrom, mergeMap, Observable, tap } from "rxjs"; +import { + createSeriazliedMemo, + SerializedMemo, + SerializedRef, + SerializedThing, + WsMessage, + WsMessageDown, + WsMessageUp, +} from "./shared"; +import { + createComputed, + createEffect, + createMemo, + createSignal, + from, + getOwner, + onCleanup, + runWithOwner, + untrack, +} from "solid-js"; +import { createAsync } from "@solidjs/router"; +import { createLazyMemo } from "@solid-primitives/memo"; +import { createCallback } from "@solid-primitives/rootless"; + +const globalWsPromise = new Promise((resolve) => { + const ws = new WebSocket("ws://localhost:3000/_ws"); + ws.onopen = () => resolve(ws); +}); + +export type Listener = (ev: { data: any }) => any; +export type SimpleWs = { + removeEventListener(type: "message", listener: Listener): void; + addEventListener(type: "message", listener: Listener): void; + send(data: string): void; +}; + +function wsRpc(message: WsMessageUp, wsPromise: Promise) { + const id = crypto.randomUUID() as string; + + return new Promise<{ value: T; dispose: () => void }>(async (res, rej) => { + const ws = await wsPromise; + + function dispose() { + ws.send( + JSON.stringify({ type: "dispose", id } satisfies WsMessage) + ); + } + + function handler(event: { data: string }) { + // console.log(`handler ${id}`, message, { data: event.data }); + const data = JSON.parse(event.data) as WsMessage>; + if (data.id === id && data.type === "value") { + res({ value: data.value, dispose }); + ws.removeEventListener("message", handler); + } + } + + ws.addEventListener("message", handler); + ws.send( + JSON.stringify({ ...message, id } satisfies WsMessage) + ); + }); +} + +function wsSub(message: WsMessageUp, wsPromise: Promise) { + const id = crypto.randomUUID(); + + return rxFrom(Promise.resolve(wsPromise)).pipe( + mergeMap((ws) => { + return new Observable((obs) => { + // console.log(`attaching sub handler`); + function handler(event: { data: string }) { + const data = JSON.parse(event.data) as WsMessage>; + // console.log(`data`, data, id); + if (data.id === id && data.type === "value") obs.next(data.value); + } + + ws.addEventListener("message", handler); + ws.send( + JSON.stringify({ ...message, id } satisfies WsMessage) + ); + + return () => { + // console.log(`detaching sub handler`); + ws.removeEventListener("message", handler); + }; + }); + }) + ); +} + +export function createRef( + ref: SerializedRef, + wsPromise: Promise +) { + return (...input: any[]) => + wsRpc( + { + type: "invoke", + ref, + input, + }, + wsPromise + ).then(({ value }) => value); +} + +export function createSocketMemoConsumer( + ref: SerializedMemo, + wsPromise: Promise +) { + // console.log({ ref }); + const memo = createLazyMemo( + () => + from( + wsSub( + { + type: "subscribe", + ref, + }, + wsPromise + ) + ), + () => ref.initial + ); + + return () => { + const memoValue = memo()(); + // console.log({ memoValue }); + return memoValue; + }; +} + +type SerializedValue = SerializedThing | Record; + +const deserializeValue = (value: SerializedValue) => { + if (value.__type === "ref") { + return createRef(value, globalWsPromise); + } else if (value.__type === "memo") { + return createSocketMemoConsumer(value, globalWsPromise); + } else { + return Object.entries(value).reduce((res, [name, value]) => { + return { + ...res, + [name]: + value.__type === "ref" + ? createRef(value, globalWsPromise) + : value.__type === "memo" + ? createSocketMemoConsumer(value, globalWsPromise) + : value, + }; + }, {} as any); + } +}; + +export function createEndpoint( + name: string, + input?: any, + wsPromise = globalWsPromise +) { + const inputScope = crypto.randomUUID(); + const serializedInput = + input?.type === "memo" + ? createSeriazliedMemo({ + name: `input`, + scope: inputScope, + initial: untrack(input), + }) + : input; + // console.log({ serializedInput }); + + const scopePromise = wsRpc( + { type: "create", name, input: serializedInput }, + wsPromise + ); + + if (input?.type === "memo") { + const [inputSignal, setInput] = createSignal(input()); + createComputed(() => setInput(input())); + + const onSubscribe = createCallback( + (ws: SimpleWs, data: WsMessage>) => { + createEffect(() => { + const value = inputSignal(); + // console.log(`sending input update to server`, value, input); + ws.send( + JSON.stringify({ + type: "value", + id: data.id, + value, + } satisfies WsMessage) + ); + }); + } + ); + + const onWs = createCallback((ws: SimpleWs) => { + function handler(event: { data: string }) { + const data = JSON.parse(event.data) as WsMessage>; + + if (data.type === "subscribe" && data.ref.scope === inputScope) { + onSubscribe(ws, data); + } + } + ws.addEventListener("message", handler); + onCleanup(() => ws.removeEventListener("message", handler)); + }); + + wsPromise.then(onWs); + } + + onCleanup(() => { + // console.log(`cleanup endpoint`); + scopePromise.then(({ dispose }) => dispose()); + }); + + const scope = createAsync(() => scopePromise); + const deserializedScope = createMemo( + () => scope() && deserializeValue(scope()!.value) + ); + + return new Proxy((() => {}) as any, { + get(_, path) { + const res = deserializedScope()?.[path]; + return res || (() => {}); + }, + apply(_, __, args) { + const res = deserializedScope()?.(...args); + return res; + }, + }); +} diff --git a/socket/lib/server.tsx b/socket/lib/server.tsx new file mode 100644 index 0000000..ebd7884 --- /dev/null +++ b/socket/lib/server.tsx @@ -0,0 +1,233 @@ +import { + createSeriazliedMemo, + SerializedMemo, + SerializedRef, + SerializedStream, + SerializedThing, + WsMessage, + WsMessageDown, + WsMessageUp, +} from "./shared"; +import { + createMemo, + createRoot, + createSignal, + observable, + onCleanup, + untrack, +} from "solid-js"; +import { getManifest } from "vinxi/manifest"; + +export type Callable = (arg: unknown) => T | Promise; + +export type Endpoint = ( + input: I +) => Callable | Record>; +export type Endpoints = Record>; + +export type SimplePeer = { + id: string; + send(message: any): void; +}; + +export class LiveSolidServer { + private closures = new Map void }>(); + observers = new Map(); + + constructor(public peer: SimplePeer) {} + + send(message: WsMessage>) { + // console.log(`send`, message); + this.peer.send(JSON.stringify(message)); + } + + handleMessage(message: WsMessage) { + if (message.type === "create") { + this.create(message.id, message.name, message.input); + } + + if (message.type === "subscribe") { + this.subscribe(message.id, message.ref); + } + + if (message.type === "dispose") { + this.dispose(message.id); + } + + if (message.type === "invoke") { + this.invoke(message.id, message.ref, message.input); + } + + if (message.type === "value") { + this.observers.get(message.id)?.(message.value); + } + } + + async create(id: string, name: string, input?: SerializedThing) { + const [filepath, functionName] = name.split("#"); + // @ts-expect-error + const module = await getManifest(import.meta.env.ROUTER_NAME).chunks[ + filepath + ].import(); + const endpoint = module[functionName]; + + if (!endpoint) throw new Error(`Endpoint ${name} not found`); + + const { payload, disposal } = createRoot((disposal) => { + const deserializedInput = + input?.__type === "memo" + ? createSocketMemoConsumer(input, this) + : input; + + const payload = endpoint(deserializedInput); + + return { payload, disposal }; + }); + + this.closures.set(id, { payload, disposal }); + + if (typeof payload === "function") { + if (payload.type === "memo") { + const value = createSeriazliedMemo({ + name, + scope: id, + initial: untrack(payload), + }); + this.send({ value, id, type: "value" }); + } else { + const value = createSeriazliedRef({ + name, + scope: id, + }); + this.send({ value, id, type: "value" }); + } + } else { + const value = Object.entries(payload).reduce((res, [name, value]) => { + return { + ...res, + [name]: + typeof value === "function" + ? // @ts-expect-error + value.type === "memo" + ? createSeriazliedMemo({ + name, + scope: id, + initial: untrack(() => value()), + }) + : createSeriazliedRef({ name, scope: id }) + : value, + }; + }, {} as Record); + this.send({ value, id, type: "value" }); + } + } + + async invoke(id: string, ref: SerializedRef, input: any[]) { + const closure = this.closures.get(ref.scope); + if (!closure) throw new Error(`Callable ${ref.scope} not found`); + const { payload } = closure; + + if (typeof payload === "function") { + const response = await payload(...input); + this.send({ id, value: response, type: "value" }); + } else { + const response = await payload[ref.name](...input); + this.send({ id, value: response, type: "value" }); + } + } + + dispose(id: string) { + // console.log(`Disposing ${id}`); + const closure = this.closures.get(id); + if (closure) { + closure.disposal(); + this.closures.delete(id); + } + } + + subscribe(id: string, ref: SerializedMemo) { + // console.log(`subscribe`, ref); + + const closure = this.closures.get(ref.scope); + if (!closure) throw new Error(`Callable ${ref.scope} not found`); + const { payload } = closure; + + const func = typeof payload === "function" ? payload : payload[ref.name]; + + const response$ = observable(func); + const sub = response$.subscribe((value) => { + this.send({ id, value, type: "value" }); + }); + this.closures.set(id, { payload: sub, disposal: () => sub.unsubscribe() }); + } + + stream(stream: SerializedStream) {} + + cleanup() { + for (const [key, closure] of this.closures.entries()) { + // console.log(`Disposing ${key}`); + closure.disposal(); + this.closures.delete(key); + } + } +} + +function createSeriazliedRef( + opts: Omit +): SerializedRef { + return { ...opts, __type: "ref" }; +} + +export function createSocketFn( + fn: () => (i?: I) => O +): () => (i?: I) => Promise; + +export function createSocketFn( + fn: () => Record O> +): () => Record Promise>; + +export function createSocketFn( + fn: () => ((i: I) => O) | Record O> +): () => ((i: I) => Promise) | Record Promise> { + return fn as any; +} + +function createLazyMemo( + calc: (prev: T | undefined) => T, + value?: T +): () => T { + let isReading = false, + isStale: boolean | undefined = true; + + const [track, trigger] = createSignal(void 0, { equals: false }), + memo = createMemo( + (p) => (isReading ? calc(p) : ((isStale = !track()), p)), + value as T, + { equals: false } + ); + + return (): T => { + isReading = true; + if (isStale) isStale = trigger(); + const v = memo(); + isReading = false; + return v; + }; +} + +export function createSocketMemoConsumer( + ref: SerializedMemo, + server: LiveSolidServer +) { + const inputSubId = crypto.randomUUID(); + + const memo = createLazyMemo(() => { + const [get, set] = createSignal(ref.initial); + server.observers.set(inputSubId, set); + server.send({ type: "subscribe", id: inputSubId, ref }); + onCleanup(() => server.observers.delete(inputSubId)); + return get; + }); + + return () => memo()(); +} diff --git a/socket/lib/shared.tsx b/socket/lib/shared.tsx new file mode 100644 index 0000000..9d2fc0b --- /dev/null +++ b/socket/lib/shared.tsx @@ -0,0 +1,68 @@ +export type WsMessage = T & { id: string }; + +export type WsMessageUp = + | { + type: "create"; + name: string; + input?: I; + } + | { + type: "subscribe"; + ref: SerializedMemo; + } + | { + type: "dispose"; + } + | { + type: "invoke"; + ref: SerializedRef; + input?: I; + } + | { + type: "value"; + value: I; + }; + +export type WsMessageDown = + | { + type: "value"; + value: T; + } + | { + type: "subscribe"; + ref: SerializedMemo; + }; + +export type SerializedRef = { + __type: "ref"; + name: string; + scope: string; +}; + +export type SerializedMemo = { + __type: "memo"; + name: string; + scope: string; + initial: O; +}; + +export type SerializedThing = SerializedRef | SerializedMemo; + +export type SerializedStream = { + __type: "stream"; + name: string; + scope: string; + value: O; +}; + +export function createSeriazliedMemo( + opts: Omit +): SerializedMemo { + return { ...opts, __type: "memo" }; +} + +export function createSocketMemo(source: () => T): () => T | undefined { + // @ts-expect-error + source.type = "memo"; + return source; +} diff --git a/socket/plugin/client-runtime.js b/socket/plugin/client-runtime.js new file mode 100644 index 0000000..f01217e --- /dev/null +++ b/socket/plugin/client-runtime.js @@ -0,0 +1,6 @@ +import { createEndpoint } from "../lib/client"; + +export function createServerReference(fn, id, name) { + // console.log("createServerReference", id, name); + return (input) => createEndpoint(`${id}#${name}`, input); +} diff --git a/socket/plugin/client.js b/socket/plugin/client.js new file mode 100644 index 0000000..f68c97e --- /dev/null +++ b/socket/plugin/client.js @@ -0,0 +1,58 @@ +import { directives, shimExportsPlugin } from "@vinxi/plugin-directives"; +import { fileURLToPath } from "url"; +import { chunkify } from "vinxi/lib/chunks"; +import { normalize } from "vinxi/lib/path"; + +import { CLIENT_REFERENCES_MANIFEST } from "./constants.js"; + +export function client({ + runtime = normalize( + fileURLToPath(new URL("./socket/plugin/client-runtime.js", import.meta.url)) + ), + manifest = CLIENT_REFERENCES_MANIFEST, +} = {}) { + const serverModules = new Set(); + const clientModules = new Set(); + return [ + directives({ + hash: chunkify, + runtime, + transforms: [ + shimExportsPlugin({ + runtime: { + module: runtime, + function: "createServerReference", + }, + onModuleFound: (mod) => { + serverModules.add(mod); + }, + hash: chunkify, + apply: (code, id, options) => { + return !options.ssr; + }, + pragma: "use socket", + }), + ], + onReference(type, reference) { + if (type === "server") { + serverModules.add(reference); + } else { + clientModules.add(reference); + } + }, + }), + { + name: "references-manifest", + generateBundle() { + this.emitFile({ + fileName: manifest, + type: "asset", + source: JSON.stringify({ + server: [...serverModules], + client: [...clientModules], + }), + }); + }, + }, + ]; +} diff --git a/socket/plugin/constants.js b/socket/plugin/constants.js new file mode 100644 index 0000000..51cb6bb --- /dev/null +++ b/socket/plugin/constants.js @@ -0,0 +1 @@ +export const CLIENT_REFERENCES_MANIFEST = `server-functions-manifest.json`; diff --git a/socket/plugin/server-handler.ts b/socket/plugin/server-handler.ts new file mode 100644 index 0000000..f9b0e26 --- /dev/null +++ b/socket/plugin/server-handler.ts @@ -0,0 +1,26 @@ +import { eventHandler } from "vinxi/http"; +import { LiveSolidServer } from "../lib/server"; +import { WsMessage, WsMessageUp } from "../lib/shared"; + +const clients = new Map(); + +export default eventHandler({ + handler() {}, + websocket: { + open(peer) { + clients.set(peer.id, new LiveSolidServer(peer)); + }, + message(peer, e) { + const message = JSON.parse(e.text()) as WsMessage; + const client = clients.get(peer.id); + if (!client) return; + client.handleMessage(message); + }, + async close(peer) { + const client = clients.get(peer.id); + if (!client) return; + client.cleanup(); + clients.delete(peer.id); + }, + }, +}); diff --git a/socket/plugin/server-runtime.ts b/socket/plugin/server-runtime.ts new file mode 100644 index 0000000..41b478c --- /dev/null +++ b/socket/plugin/server-runtime.ts @@ -0,0 +1,4 @@ +export function createServerReference(fn, id, name) { + // console.log(`server runtime reference`, id, name); + return fn; +} diff --git a/socket/plugin/server.js b/socket/plugin/server.js new file mode 100644 index 0000000..5a0f6f0 --- /dev/null +++ b/socket/plugin/server.js @@ -0,0 +1,92 @@ +import { directives, wrapExportsPlugin } from "@vinxi/plugin-directives"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { chunkify } from "vinxi/lib/chunks"; +import { handlerModule, join, normalize } from "vinxi/lib/path"; + +import { CLIENT_REFERENCES_MANIFEST } from "./constants.js"; + +export function serverTransform({ runtime }) { + return directives({ + hash: chunkify, + runtime: runtime, + transforms: [ + wrapExportsPlugin({ + runtime: { + module: runtime, + function: "createServerReference", + }, + hash: chunkify, + apply: (code, id, options) => { + return options.ssr; + }, + pragma: "use socket", + }), + ], + onReference(type, reference) {}, + }); +} + +/** + * + * @returns {import('vinxi').Plugin} + */ +export const serverBuild = ({ client, manifest }) => { + let input; + return { + name: "server-functions:build", + enforce: "post", + apply: "build", + config(config, env) { + // @ts-ignore + const router = config.router; + // @ts-ignore + const app = config.app; + + const rscRouter = app.getRouter(client); + + const serverFunctionsManifest = JSON.parse( + readFileSync(join(rscRouter.outDir, rscRouter.base, manifest), "utf-8") + ); + + input = { + entry: handlerModule(router), + ...Object.fromEntries( + serverFunctionsManifest.server.map((key) => { + return [chunkify(key), key]; + }) + ), + }; + + return { + build: { + rollupOptions: { + output: { + chunkFileNames: "[name].mjs", + entryFileNames: "[name].mjs", + }, + treeshake: true, + }, + }, + }; + }, + + configResolved(config) { + config.build.rollupOptions.input = input; + }, + }; +}; + +/** + * + * @returns {import('vinxi').Plugin[]} + */ +export function server({ + client = "client", + manifest = CLIENT_REFERENCES_MANIFEST, + runtime = normalize( + fileURLToPath(new URL("./server-runtime.js", import.meta.url)) + ), +} = {}) { + return [serverTransform({ runtime }), serverBuild({ client, manifest })]; +} diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 7382307..4c0e3d1 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -1,24 +1,5 @@ -import { Action, useSubmissions } from "@solidjs/router"; -import { For, batch, createEffect, createMemo, untrack } from "solid-js"; -import { createStore, produce, reconcile } from "solid-js/store"; -import { - AddColumn, - Column, - ColumnGap, - ColumnId, - createColumn, - deleteColumn, - moveColumn, - renameColumn, -} from "./Column"; -import { - Note, - NoteId, - createNote, - deleteNote, - editNote, - moveNote, -} from "./Note"; +import { Column } from "./Column"; +import { Note } from "./Note"; export enum DragTypes { Note = "application/note", @@ -38,334 +19,3 @@ export type BoardData = { columns: Column[]; notes: Note[]; }; - -type Mutation = - | { - type: "createNote"; - id: NoteId; - column: ColumnId; - board: BoardId; - body: string; - order: number; - timestamp: number; - } - | { - type: "editNote"; - id: NoteId; - content: string; - timestamp: number; - } - | { - type: "moveNote"; - id: NoteId; - column: ColumnId; - order: number; - timestamp: number; - } - | { - type: "deleteNote"; - id: NoteId; - timestamp: number; - } - | { - type: "createColumn"; - id: ColumnId; - board: string; - title: string; - timestamp: number; - } - | { - type: "renameColumn"; - id: ColumnId; - title: string; - timestamp: number; - } - | { - type: "moveColumn"; - id: ColumnId; - order: number; - timestamp: number; - } - | { - type: "deleteColumn"; - id: ColumnId; - timestamp: number; - }; - -export function Board(props: { board: BoardData }) { - const [boardStore, setBoardStore] = createStore({ - columns: props.board.columns, - notes: props.board.notes, - timestamp: 0, - }); - - const createNoteSubmission = useSubmissions(createNote); - const editNoteSubmission = useSubmissions(editNote); - const moveNoteSubmission = useSubmissions(moveNote); - const deleteNoteSubmission = useSubmissions(deleteNote); - const createColumnSubmission = useSubmissions(createColumn); - const renameColumnSubmission = useSubmissions(renameColumn); - const moveColumnSubmission = useSubmissions(moveColumn); - const deleteColumnSubmission = useSubmissions(deleteColumn); - - function getMutations() { - const mutations: Mutation[] = []; - - for (const note of createNoteSubmission.values()) { - if (!note.pending) continue; - const [{ id, column, body, order, timestamp }] = note.input; - mutations.push({ - type: "createNote", - board: props.board.board.id, - id, - column, - body, - order, - timestamp, - }); - } - - for (const note of editNoteSubmission.values()) { - if (!note.pending) continue; - const [id, content, timestamp] = note.input; - mutations.push({ - type: "editNote", - id, - content, - timestamp, - }); - } - - for (const note of moveNoteSubmission.values()) { - if (!note.pending) continue; - const [id, column, order, timestamp] = note.input; - mutations.push({ - type: "moveNote", - id, - column, - order, - timestamp, - }); - } - - for (const note of deleteNoteSubmission.values()) { - if (!note.pending) continue; - const [id, timestamp] = note.input; - mutations.push({ - type: "deleteNote", - id, - timestamp, - }); - } - - for (const column of createColumnSubmission.values()) { - if (!column.pending) continue; - const [id, board, title, timestamp] = column.input; - mutations.push({ - type: "createColumn", - id, - board, - title, - timestamp, - }); - } - - for (const column of renameColumnSubmission.values()) { - if (!column.pending) continue; - const [id, title, timestamp] = column.input; - mutations.push({ - type: "renameColumn", - id, - title, - timestamp, - }); - } - - for (const column of moveColumnSubmission.values()) { - if (!column.pending) continue; - const [id, order, timestamp] = column.input; - mutations.push({ - type: "moveColumn", - id, - order, - timestamp, - }); - } - - for (const column of deleteColumnSubmission.values()) { - if (!column.pending) continue; - const [id, timestamp] = column.input; - mutations.push({ - type: "deleteColumn", - id, - timestamp, - }); - } - - return mutations; - } - - // current approach - // createEffect(() => { - // const mutations = getMutations(); - - // const newNotes = [...props.board.notes]; - // const newColumns = [...props.board.columns]; - - // applyMutations(mutations, newNotes, newColumns); - - // batch(() => { - // setBoardStore("notes", reconcile(newNotes)); - // setBoardStore("columns", reconcile(newColumns)); - // }); - // }); - - createEffect(() => { - const mutations = untrack(() => getMutations()); - - const notes = [...props.board.notes]; - const columns = [...props.board.columns]; - applyMutations(mutations, notes, columns); - - console.log( - `got server data, reset the board with mutations`, - ...mutations - ); - - batch(() => { - setBoardStore("notes", reconcile(notes)); - setBoardStore("columns", reconcile(columns)); - setBoardStore("timestamp", Date.now()); - }); - }); - - createEffect(() => { - const mutations = getMutations(); - const prevTimestamp = untrack(() => boardStore.timestamp); - const latestMutations = mutations.filter( - (m) => m.timestamp > prevTimestamp - ); - - console.log( - `found submission, apply optimistic update with mutations`, - ...latestMutations - ); - - setBoardStore( - produce((b) => { - applyMutations(latestMutations, b.notes, b.columns); - b.timestamp = Date.now(); - }) - ); - }); - - const sortedColumns = createMemo(() => - boardStore.columns.slice().sort((a, b) => a.order - b.order) - ); - - let scrollContainerRef: HTMLDivElement | undefined; - - return ( -
{ - scrollContainerRef = el; - }} - class="pb-8 h-[calc(100vh-160px)] min-w-full overflow-x-auto overflow-y-hidden flex flex-start items-start flex-nowrap" - > - - - {(column, i) => ( - <> - - - - )} - - { - scrollContainerRef && - (scrollContainerRef.scrollLeft = scrollContainerRef.scrollWidth); - }} - /> -
- ); -} - -function applyMutations( - mutations: Mutation[], - notes: Note[], - columns: Column[] -) { - for (const mut of mutations.sort((a, b) => a.timestamp - b.timestamp)) { - switch (mut.type) { - case "createNote": { - const index = notes.findIndex((n) => n.id === mut.id); - if (index === -1) - notes.push({ - id: mut.id, - column: mut.column, - body: mut.body, - order: mut.order, - board: mut.board, - }); - break; - } - case "moveNote": { - const index = notes.findIndex((n) => n.id === mut.id); - if (index !== -1) - notes[index] = { - ...notes[index], - column: mut.column, - order: mut.order, - }; - break; - } - case "editNote": { - const index = notes.findIndex((n) => n.id === mut.id); - if (index !== -1) notes[index] = { ...notes[index], body: mut.content }; - break; - } - case "deleteNote": { - const index = notes.findIndex((n) => n.id === mut.id); - if (index !== -1) notes.splice(index, 1); - break; - } - case "createColumn": { - const index = columns.findIndex((c) => c.id === mut.id); - if (index === -1) - columns.push({ - id: mut.id, - board: mut.board, - title: mut.title, - order: columns.length + 1, - }); - break; - } - case "renameColumn": { - const index = columns.findIndex((c) => c.id === mut.id); - if (index !== -1) - columns[index] = { ...columns[index], title: mut.title }; - break; - } - case "moveColumn": { - const index = columns.findIndex((c) => c.id === mut.id); - if (index !== -1) - columns[index] = { ...columns[index], order: mut.order }; - break; - } - case "deleteColumn": { - const index = columns.findIndex((c) => c.id === mut.id); - if (index !== -1) columns.splice(index, 1); - break; - } - } - } -} diff --git a/src/components/Column.tsx b/src/components/Column.tsx index 020ba53..e4b8568 100644 --- a/src/components/Column.tsx +++ b/src/components/Column.tsx @@ -1,4 +1,3 @@ -import { action, useAction } from "@solidjs/router"; import { BsPlus, BsTrash } from "solid-icons/bs"; import { RiEditorDraggable } from "solid-icons/ri"; import { @@ -9,74 +8,9 @@ import { createSignal, onMount, } from "solid-js"; -import { type Board, type BoardId, DragTypes } from "./Board"; import { getIndexBetween } from "~/lib/utils"; -import { AddNote, Note, NoteId, moveNote } from "./Note"; -import { getAuthUser } from "~/lib/auth"; -import { db } from "~/lib/db"; - -export const renameColumn = action( - async (id: ColumnId, name: string, timestamp: number) => { - "use server"; - const accountId = await getAuthUser(); - - await db.column.update({ - where: { id, Board: { accountId } }, - data: { name }, - }); - - return true; - } -); - -export const createColumn = action( - async (id: ColumnId, board: BoardId, name: string, timestamp: number) => { - "use server"; - - const accountId = await getAuthUser(); - - let columnCount = await db.column.count({ - where: { boardId: +board, Board: { accountId } }, - }); - await db.column.create({ - data: { - id, - boardId: +board, - name, - order: columnCount + 1, - }, - }); - - return true; - }, - "create-column" -); - -export const moveColumn = action( - async (id: ColumnId, order: number, timestamp: number) => { - "use server"; - const accountId = await getAuthUser(); - - await db.column.update({ - where: { id, Board: { accountId } }, - data: { order }, - }); - - return; - }, - "create-column" -); - -export const deleteColumn = action(async (id: ColumnId, timestamp: number) => { - "use server"; - const accountId = await getAuthUser(); - - await db.column.delete({ - where: { id, Board: { accountId } }, - }); - - return true; -}, "create-column"); +import { type BoardId, DragTypes } from "./Board"; +import { AddNote, Note, NoteId } from "./Note"; export type ColumnId = string & { __brand?: "ColumnId" }; @@ -87,13 +21,24 @@ export type Column = { order: number; }; -export function Column(props: { column: Column; board: Board; notes: Note[] }) { +export function Column(props: { + boardId: string; + column: Column; + notes: Note[]; + renameColumn: (columnId: ColumnId, name: string) => void; + deleteColumn: (columnId: ColumnId) => void; + moveNote: (noteId: NoteId, column: ColumnId, order: number) => void; + createNote: ( + noteId: NoteId, + column: ColumnId, + body: string, + order: number + ) => void; + editNote: (noteId: NoteId, body: string) => void; + deleteNote: (noteId: NoteId) => void; +}) { let parent: HTMLDivElement | undefined; - const renameAction = useAction(renameColumn); - const deleteAction = useAction(deleteColumn); - const moveNoteAction = useAction(moveNote); - const [acceptDrop, setAcceptDrop] = createSignal(false); const filteredNotes = createMemo(() => @@ -130,14 +75,13 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) { | NoteId | undefined; if (noteId && !filteredNotes().find((n) => n.id === noteId)) { - moveNoteAction( + props.moveNote( noteId, props.column.id, getIndexBetween( filteredNotes()[filteredNotes().length - 1]?.order, undefined - ), - new Date().getTime() + ) ); } } @@ -154,11 +98,7 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) { required onBlur={(e) => { if (e.target.reportValidity()) { - renameAction( - props.column.id, - e.target.value, - new Date().getTime() - ); + props.renameColumn(props.column.id, e.target.value); } }} onKeyDown={(e) => { @@ -170,7 +110,7 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) { /> @@ -185,14 +125,18 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) { note={n} previous={filteredNotes()[i() - 1]} next={filteredNotes()[i() + 1]} + moveNote={props.moveNote} + editNote={props.editNote} + deleteNote={props.deleteNote} /> )} { parent && (parent.scrollTop = parent.scrollHeight); }} @@ -201,9 +145,13 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) { ); } -export function ColumnGap(props: { left?: Column; right?: Column }) { +export function ColumnGap(props: { + left?: Column; + right?: Column; + moveColumn: (columnId: ColumnId, order: number) => void; +}) { const [active, setActive] = createSignal(false); - const moveColumnAction = useAction(moveColumn); + return (
void }) { +export function AddColumn(props: { + board: BoardId; + createColumn: (columnId: ColumnId, title: string) => void; +}) { const [active, setActive] = createSignal(false); - const addColumn = useAction(createColumn); - let inputRef: HTMLInputElement | undefined; let plusRef: HTMLButtonElement | undefined; @@ -267,14 +216,11 @@ export function AddColumn(props: { board: BoardId; onAdd: () => void }) {
( e.preventDefault(), - addColumn( + props.createColumn( crypto.randomUUID() as ColumnId, - props.board, - inputRef?.value ?? "Column", - new Date().getTime() + inputRef?.value ?? "Column" ), - inputRef && (inputRef.value = ""), - props.onAdd() + inputRef && (inputRef.value = "") )} class="flex flex-col space-y-2 card bg-slate-100 p-2 w-full max-w-[300px]" onFocusOut={(e) => { diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx index d4c5bc5..beb74e0 100644 --- a/src/components/Logo.tsx +++ b/src/components/Logo.tsx @@ -88,7 +88,7 @@ export function Logo(props: { class?: string }) { /> - Strello + MultiStrello
); diff --git a/src/components/Note.tsx b/src/components/Note.tsx index 61c609a..c8114b2 100644 --- a/src/components/Note.tsx +++ b/src/components/Note.tsx @@ -1,4 +1,3 @@ -import { action, useAction } from "@solidjs/router"; import { BsPlus, BsTrash } from "solid-icons/bs"; import { RiEditorDraggable } from "solid-icons/ri"; import { Match, Switch, createSignal } from "solid-js"; @@ -8,106 +7,6 @@ import { getIndexBetween } from "~/lib/utils"; import { getAuthUser } from "~/lib/auth"; import { db } from "~/lib/db"; -export const createNote = action( - async ({ - id, - column, - body, - order, - timestamp, - board, - }: { - id: NoteId; - board: BoardId; - column: ColumnId; - body: string; - order: number; - timestamp: number; - }) => { - "use server"; - const accountId = await getAuthUser(); - const mutation = { - id: String(id), - title: String(body), - order, - boardId: +board, - columnId: String(column), - }; - - await db.item.upsert({ - where: { - id: mutation.id, - Board: { - accountId, - }, - }, - create: mutation, - update: mutation, - }); - - return true; - }, - "create-item" -); - -export const editNote = action( - async (id: NoteId, content: string, timestamp: number) => { - "use server"; - const accountId = await getAuthUser(); - const mutation = { - id: String(id), - title: String(content), - }; - - await db.item.update({ - where: { - id: mutation.id, - Board: { - accountId, - }, - }, - data: mutation, - }); - - return true; - }, - "edit-item" -); - -export const moveNote = action( - async (note: NoteId, column: ColumnId, order: number, timestamp: number) => { - "use server"; - const accountId = await getAuthUser(); - const mutation = { - id: String(note), - columnId: String(column), - order, - }; - - await db.item.update({ - where: { - id: mutation.id, - Board: { - accountId, - }, - }, - data: mutation, - }); - - return true; - }, - "move-item" -); - -export const deleteNote = action(async (id: NoteId, timestamp: number) => { - "use server"; - const accountId = await getAuthUser(); - - await db.item.delete({ where: { id, Board: { accountId } } }); - - return true; -}, "delete-card"); - export type NoteId = string & { __brand?: "NoteId" }; export type Note = { @@ -118,11 +17,14 @@ export type Note = { body: string; }; -export function Note(props: { note: Note; previous?: Note; next?: Note }) { - const updateAction = useAction(editNote); - const deleteAction = useAction(deleteNote); - const moveNoteAction = useAction(moveNote); - +export function Note(props: { + note: Note; + previous?: Note; + next?: Note; + moveNote: (noteId: NoteId, column: ColumnId, order: number) => void; + editNote: (noteId: NoteId, body: string) => void; + deleteNote: (noteId: NoteId) => void; +}) { let input: HTMLTextAreaElement | undefined; const [isBeingDragged, setIsBeingDragged] = createSignal(false); @@ -189,11 +91,10 @@ export function Note(props: { note: Note; previous?: Note; next?: Note }) { if (props.previous && props.previous?.id === noteId) { break action; } - moveNoteAction( + props.moveNote( noteId, props.note.column, - getIndexBetween(props.previous?.order, props.note.order), - new Date().getTime() + getIndexBetween(props.previous?.order, props.note.order) ); } @@ -201,11 +102,10 @@ export function Note(props: { note: Note; previous?: Note; next?: Note }) { if (props.previous && props.next?.id === noteId) { break action; } - moveNoteAction( + props.moveNote( noteId, props.note.column, - getIndexBetween(props.note.order, props.next?.order), - new Date().getTime() + getIndexBetween(props.note.order, props.next?.order) ); } } @@ -224,18 +124,14 @@ export function Note(props: { note: Note; previous?: Note; next?: Note }) { resize: "none", }} onBlur={(e) => - updateAction( - props.note.id, - (e.target as HTMLTextAreaElement).value, - new Date().getTime() - ) + props.editNote(props.note.id, (e.target as HTMLTextAreaElement).value) } > {`${props.note.body}`} @@ -248,9 +144,14 @@ export function AddNote(props: { length: number; onAdd: () => void; board: BoardId; + createNote: ( + noteId: NoteId, + column: ColumnId, + body: string, + order: number + ) => void; }) { const [active, setActive] = createSignal(false); - const addNote = useAction(createNote); let inputRef: HTMLInputElement | undefined; @@ -262,20 +163,18 @@ export function AddNote(props: { class="flex flex-col space-y-2 card w-full" onSubmit={(e) => { e.preventDefault(); - const body = inputRef?.value.trim() ?? 'Note' - if (body === '') { - inputRef?.setCustomValidity('Please fill out this field.'); + const body = inputRef?.value.trim() ?? "Note"; + if (body === "") { + inputRef?.setCustomValidity("Please fill out this field."); inputRef?.reportValidity(); return; } - addNote({ - id: crypto.randomUUID() as NoteId, - board: props.board, - column: props.column, + props.createNote( + crypto.randomUUID() as NoteId, + props.column, body, - order: props.length + 1, - timestamp: new Date().getTime(), - }); + props.length + 1 + ); inputRef && (inputRef.value = ""); props.onAdd(); }} diff --git a/src/components/Presence.tsx b/src/components/Presence.tsx new file mode 100644 index 0000000..b560475 --- /dev/null +++ b/src/components/Presence.tsx @@ -0,0 +1,109 @@ +import { createComputed, createEffect, createSignal, For } from "solid-js"; +import { PresenceUser, usePresence } from "./board-data"; +import { createSocketMemo } from "../../socket/lib/shared"; +import { + createPositionToElement, + useMousePosition, +} from "@solid-primitives/mouse"; +import { debounce } from "@solid-primitives/scheduled"; +import { createStore, reconcile } from "solid-js/store"; +import { Tooltip } from "@kobalte/core/tooltip"; +import { RiDevelopmentCursorLine } from "solid-icons/ri"; + +export function Presence() { + const pos = useMousePosition(); + const users = usePresence(createSocketMemo(() => pos)); + const [presenceStore, setPresenceStore] = createStore([]); + + createComputed(() => + setPresenceStore(reconcile(Object.values(users() || {}))) + ); + + return ( +
+ + {(user) => { + createEffect(() => { + console.log(user.name, user.x, user.y); + }); + return ( + + +
+ {user.name + .split(" ") + .map((n) => n[0]) + .join("")} +
+
+ + + + {user.name} + + +
+ ); + }} +
+ + {(user) => { + return ( +
+ +
+ ); + }} +
+
+ ); +} + +function createDebouncedMousePos(ref: () => HTMLElement | undefined) { + const pos = useMousePosition(); + const relative = createPositionToElement(ref, () => pos); + const [debouncedPos, setDebouncedPos] = createSignal<{ + x: number; + y: number; + }>(); + const trigger = debounce( + (pos: { x: number; y: number }) => setDebouncedPos(pos), + 5 + ); + createEffect(() => { + const { x, y } = relative; + x && y && trigger({ x, y }); + }); + return debouncedPos; +} diff --git a/src/components/board-data.ts b/src/components/board-data.ts new file mode 100644 index 0000000..d490b8c --- /dev/null +++ b/src/components/board-data.ts @@ -0,0 +1,224 @@ +"use socket"; + +import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"; +import { BoardData, BoardId } from "./Board"; +import { + uniqueNamesGenerator, + adjectives, + colors, + animals, +} from "unique-names-generator"; +import { createSocketMemo } from "../../socket/lib/shared"; +import { ColumnId } from "./Column"; +import { NoteId } from "./Note"; + +const [boards, setBoards] = createSignal>({}); + +export const useBoards = () => { + return { + boards: createSocketMemo(boards), + async createBoard(title: string, color: string) { + const boardId = `${Object.keys(boards()).length + 1}`; + setBoards((b) => ({ + ...b, + [boardId]: { + board: { id: boardId, title, color }, + columns: [], + notes: [], + }, + })); + return boardId; + }, + deleteBoard(boardId: BoardId) { + setBoards((b) => { + const { [boardId]: _, ...rest } = b; + return rest; + }); + }, + }; +}; + +export const useBoard = (boardId: () => string | undefined) => { + createEffect(() => console.log(`boardId`, boardId())); + function moveColumn(columnId: ColumnId, order: number) { + const id = boardId(); + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + columns: b[id].columns.map((c) => + c.id === columnId ? { ...c, order } : c + ), + }, + })); + } + + function renameColumn(columnId: ColumnId, name: string) { + const id = boardId(); + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + columns: b[id].columns.map((c) => + c.id === columnId ? { ...c, name } : c + ), + }, + })); + } + + function deleteColumn(columnId: ColumnId) { + const id = boardId(); + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + columns: b[id].columns.filter((c) => c.id !== columnId), + }, + })); + } + + function createColumn(columnId: ColumnId, title: string) { + const id = boardId() as BoardId; + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + columns: [ + ...b[id].columns, + { + id: columnId, + order: b[id].columns.length + 1, + board: id, + title, + }, + ], + }, + })); + } + + function moveNote(noteId: NoteId, column: ColumnId, order: number) { + console.log(`moveNote`, noteId, column, order); + const id = boardId(); + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + notes: b[id].notes.map((n) => + n.id === noteId ? { ...n, column, order } : n + ), + }, + })); + } + + function createNote( + noteId: NoteId, + column: ColumnId, + body: string, + order: number + ) { + const id = boardId() as BoardId; + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + notes: [ + ...b[id].notes, + { + id: noteId, + column, + body, + order, + board: id, + }, + ], + }, + })); + } + + function editNote(noteId: NoteId, content: string) { + const id = boardId() as BoardId; + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + notes: b[id].notes.map((n) => + n.id === noteId ? { ...n, body: content } : n + ), + }, + })); + } + + function deleteNote(noteId: NoteId) { + const id = boardId() as BoardId; + if (!id) return; + setBoards((b) => ({ + ...b, + [id]: { + ...b[id], + notes: b[id].notes.filter((n) => n.id !== noteId), + }, + })); + } + + return { + board: createSocketMemo(() => + boardId() ? boards()[boardId()!] : undefined + ), + async setBoard(data: BoardData) { + setBoards((b) => ({ ...b, [data.board.id]: data })); + }, + moveColumn, + moveNote, + renameColumn, + deleteColumn, + createColumn, + createNote, + editNote, + deleteNote, + }; +}; + +export type PresenceUser = { + name: string; + x: number; + y: number; + color: string; +}; + +const [users, setUsers] = createSignal>({}); + +export const usePresence = ( + mousePos: () => { x: number; y: number } | undefined +) => { + const id = crypto.randomUUID(); + const color = Math.floor(Math.random() * 16777215).toString(16); + const name = uniqueNamesGenerator({ + dictionaries: [adjectives, colors, animals], + style: "capital", + separator: " ", + }); + + createEffect(() => { + const { x, y } = mousePos() || {}; + console.log(name, x, y); + x && y && setUsers((u) => ({ ...u, [id]: { name, x, y, color } })); + }); + + onCleanup(() => { + setUsers(({ [id]: _, ...rest }) => rest); + }); + + const otherUsers = createMemo(() => { + const { [id]: _, ...rest } = users(); + return rest; + }); + + return createSocketMemo(otherUsers); +}; diff --git a/src/lib/db.ts b/src/lib/db.ts index 0c2c97c..6031213 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,14 +1,6 @@ import { PrismaClient } from "@prisma/client"; -import { PrismaLibSQL } from "@prisma/adapter-libsql"; -import { createClient } from "@libsql/client"; -const libsql = createClient({ - url: `${process.env.TURSO_DATABASE_URL}`, - authToken: `${process.env.TURSO_AUTH_TOKEN}`, -}); - -const adapter = new PrismaLibSQL(libsql); -const db = new PrismaClient({ adapter }); +const db = new PrismaClient(); process.on("beforeExit", () => { db.$disconnect(); diff --git a/src/routes/board/[id].tsx b/src/routes/board/[id].tsx index ca78470..bc149c5 100644 --- a/src/routes/board/[id].tsx +++ b/src/routes/board/[id].tsx @@ -1,106 +1,82 @@ import { Title } from "@solidjs/meta"; -import { - RouteDefinition, - RouteSectionProps, - action, - cache, - createAsync, - redirect, - useAction, - useSubmission, -} from "@solidjs/router"; -import { Show } from "solid-js"; -import { Board, BoardData } from "~/components/Board"; +import { RouteSectionProps } from "@solidjs/router"; +import { createComputed, createEffect, createMemo, For, Show } from "solid-js"; +import { useBoard } from "~/components/board-data"; import EditableText from "~/components/EditableText"; -import { getAuthUser } from "~/lib/auth"; -import { db } from "~/lib/db"; - -const fetchBoard = cache(async (boardId: number) => { - "use server"; - const accountId = await getAuthUser(); - - const boardFromDataBase = await db.board.findUnique({ - where: { - id: boardId, - accountId, - }, - include: { - items: true, - columns: { orderBy: { order: "asc" } }, - }, - }); - - if (!boardFromDataBase) throw redirect("/"); - - // mapping the db to what the board expects - return { - board: { - id: String(boardFromDataBase.id), - title: boardFromDataBase.name, - color: boardFromDataBase.color, - }, - notes: - boardFromDataBase.items.map((note) => ({ - ...note, - board: String(note.boardId), - column: note.columnId, - body: note.title || "", - })) || [], - columns: - boardFromDataBase.columns.map((column) => ({ - ...column, - board: String(column.boardId), - title: column.name, - })) || [], - } satisfies BoardData; -}, "get-board-data"); - -const updateBoardName = action(async (boardId: number, name: string) => { - "use server"; - const accountId = await getAuthUser(); - - return db.board.update({ - where: { id: boardId, accountId }, - data: { name }, - }); -}, "update-board-name"); - -export const route: RouteDefinition = { - load: (props) => fetchBoard(+props.params.id), -}; +import { createSocketMemo } from "../../../socket/lib/shared"; +import { AddColumn, Column, ColumnGap } from "~/components/Column"; +import { createStore } from "solid-js/store"; +import { reconcile } from "solid-js/store"; +import { Presence } from "~/components/Presence"; export default function Page(props: RouteSectionProps) { - const board = createAsync(() => fetchBoard(+props.params.id)); - const submission = useSubmission(updateBoardName); - const updateBoardNameAction = useAction(updateBoardName); + const boardId = createSocketMemo(() => props.params.id); + const serverBoard = useBoard(boardId); + let scrollContainerRef: HTMLDivElement | undefined; return ( - - {(board) => ( -
- {board().board.title} | Strello + + {(board) => { + const [boardStore, setBoardStore] = createStore(board()); + createComputed(() => setBoardStore(reconcile(board()))); + + const sortedColumns = createMemo( + () => + boardStore.columns.slice().sort((a, b) => a.order - b.order) || [] + ); -

- - updateBoardNameAction(+props.params.id, value) - } - /> -

+ return ( +
+ {board().board.title} | Strello + +

+ {}} + /> +

-
- -
-
- )} +
+ + + {(column, i) => ( + <> + + + + )} + + serverBoard.createColumn(...p)} + /> +
+
+ ); + }}
); } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 32e0136..e2afe95 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,88 +1,15 @@ import { Title } from "@solidjs/meta"; -import { - action, - cache, - createAsync, - redirect, - useSubmission, - useSubmissions, - type RouteDefinition, -} from "@solidjs/router"; +import { A, useNavigate, type RouteDefinition } from "@solidjs/router"; import { BsTrash } from "solid-icons/bs"; import { For, Show, onMount } from "solid-js"; -import { getUser } from "~/lib"; -import { getSession } from "~/lib/auth"; -import { db } from "~/lib/db"; - -const addBoard = action(async (formData: FormData) => { - "use server"; - - const session = await getSession(); - const userId = session.data.userId; - const name = String(formData.get("name")); - const color = String(formData.get("color")); - - const board = await db.board.create({ - data: { - accountId: userId, - name, - color, - }, - }); - - return redirect(`/board/${board.id}`); -}, "add-board"); - -const deleteBoard = action(async (boardId: number) => { - "use server"; - const session = await getSession(); - const userId = session.data.userId; - - await db.board.delete({ - where: { id: boardId, accountId: userId }, - }); -}, "delete-board"); - -const getBoards = cache(async () => { - "use server"; - const session = await getSession(); - const userId = session.data.userId; - - return db.board.findMany({ - where: { - accountId: userId, - }, - }); -}, "get-boards"); +import { useBoards } from "~/components/board-data"; export const route = { - load: () => { - getUser(); - getBoards(); - }, + load: () => {}, } satisfies RouteDefinition; export default function Home() { - const user = createAsync(() => getUser(), { deferStream: true }); - const serverBoards = createAsync(() => getBoards()); - const addBoardSubmission = useSubmission(addBoard); - const deleteBoardSubmissions = useSubmissions(deleteBoard); - - const boards = () => { - if (deleteBoardSubmissions.pending) { - const deletedBoards: number[] = []; - - for (const sub of deleteBoardSubmissions) { - deletedBoards.push(sub.input[0]); - } - - return serverBoards()?.filter( - (board) => !deletedBoards.includes(board.id) - ); - } - - return serverBoards(); - }; + const serverBoards = useBoards(); let inputRef: HTMLInputElement | undefined; @@ -90,95 +17,106 @@ export default function Home() { inputRef?.focus(); }); + const nav = useNavigate(); + + const boardsList = () => Object.values(serverBoards.boards() || {}); + return (
- - Boards | Strello - -
- -
-

- New Board -

- -
- -
+ Boards | Strello + +
+ { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const name = formData.get("name") as string; + const color = formData.get("color") as string; + const boardId = await serverBoards.createBoard(name, color); + nav(`/board/${boardId}`); + }} + class="max-w-md" + > +
+

+ New Board +

+ +
+
-
-
- - -
-
+
+
+ +
- -
-

Boards

- +
+ +
+

Boards

+
- +
); } diff --git a/tsconfig.json b/tsconfig.json index 205d785..c2e8309 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,19 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - "allowJs": true, - "strict": true, - "noEmit": true, - "types": [ - "vinxi/client", - "node" - ], - "isolatedModules": true, - "paths": { - "~/*": [ - "./src/*" - ] - } - } -} \ No newline at end of file + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/client", "node"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +}