diff --git a/package.json b/package.json
index 4c62b34..32f3a88 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"kysely": "^0.27.3",
"lowdb": "^7.0.1",
"prisma": "^5.7.0",
+ "solid-events": "^0.0.4",
"solid-icons": "^1.1.0",
"solid-js": "^1.8.22",
"unimport": "^3.7.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6167e5b..e562ac4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -47,6 +47,9 @@ importers:
prisma:
specifier: ^5.7.0
version: 5.18.0
+ solid-events:
+ specifier: ^0.0.4
+ version: 0.0.4
solid-icons:
specifier: ^1.1.0
version: 1.1.0(solid-js@1.8.22)
@@ -947,6 +950,11 @@ packages:
peerDependencies:
solid-js: ^1.8.6
+ '@solidjs/router@0.14.8':
+ resolution: {integrity: sha512-S+rD5Twp0820cM03wEIYtb7/4KN7Cfr3BP+qPIqb7IXO/SZ72tWqHEMQsmcjDbr4yVfpA+5Sq0Y+xcq09y1gQA==}
+ peerDependencies:
+ solid-js: ^1.8.6
+
'@solidjs/start@1.0.6':
resolution: {integrity: sha512-O5knaeqDBx+nKLJRm5ZJurnXZtIYBOwOreQ10APaVtVjKIKKRC5HxJ1Kwqg7atOQNNDgsF0pzhW218KseaZ1UA==}
@@ -2579,6 +2587,9 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ rxjs@7.8.1:
+ resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
+
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@@ -2671,6 +2682,9 @@ packages:
smob@1.5.0:
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
+ solid-events@0.0.4:
+ resolution: {integrity: sha512-ZpHHOf0X0PnWyp3ECnIrB8+VMVrDPlnkbVel2GpLhceJh39LWk/aLyKbYeMoUEM7TzhkTc6rbBN40VO3A3M3lA==}
+
solid-icons@1.1.0:
resolution: {integrity: sha512-IesTfr/F1ElVwH2E1110s2RPXH4pujKfSs+koT8rwuTAdleO5s26lNSpqJV7D1+QHooJj18mcOiz2PIKs0ic+A==}
peerDependencies:
@@ -2679,6 +2693,9 @@ packages:
solid-js@1.8.22:
resolution: {integrity: sha512-VBzN5j+9Y4rqIKEnK301aBk+S7fvFSTs9ljg+YEdFxjNjH0hkjXPiQRcws9tE5fUzMznSS6KToL5hwMfHDgpLA==}
+ solid-js@1.9.2:
+ resolution: {integrity: sha512-fe/K03nV+kMFJYhAOE8AIQHcGxB4rMIEoEyrulbtmf217NffbbwBqJnJI4ovt16e+kaIt0czE2WA7mP/pYN9yg==}
+
solid-refresh@0.6.3:
resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==}
peerDependencies:
@@ -3897,6 +3914,10 @@ snapshots:
dependencies:
solid-js: 1.8.22
+ '@solidjs/router@0.14.8(solid-js@1.9.2)':
+ dependencies:
+ solid-js: 1.9.2
+
'@solidjs/start@1.0.6(rollup@4.21.0)(solid-js@1.8.22)(vinxi@0.4.2(@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.4.2(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))
@@ -5758,6 +5779,10 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
+ rxjs@7.8.1:
+ dependencies:
+ tslib: 2.6.3
+
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
@@ -5849,6 +5874,12 @@ snapshots:
smob@1.5.0: {}
+ solid-events@0.0.4:
+ dependencies:
+ '@solidjs/router': 0.14.8(solid-js@1.9.2)
+ rxjs: 7.8.1
+ solid-js: 1.9.2
+
solid-icons@1.1.0(solid-js@1.8.22):
dependencies:
solid-js: 1.8.22
@@ -5859,6 +5890,12 @@ snapshots:
seroval: 1.1.1
seroval-plugins: 1.1.1(seroval@1.1.1)
+ solid-js@1.9.2:
+ dependencies:
+ csstype: 3.1.3
+ seroval: 1.1.1
+ seroval-plugins: 1.1.1(seroval@1.1.1)
+
solid-refresh@0.6.3(solid-js@1.8.22):
dependencies:
'@babel/generator': 7.25.0
diff --git a/prisma/dev.db b/prisma/dev.db
index 123a99f..7c59823 100644
Binary files a/prisma/dev.db and b/prisma/dev.db differ
diff --git a/src/components/Board.tsx b/src/components/Board.tsx
index f73f402..49d44f6 100644
--- a/src/components/Board.tsx
+++ b/src/components/Board.tsx
@@ -1,24 +1,8 @@
-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 { createSubjectStore } from "solid-events";
+import { For, createMemo } from "solid-js";
+import { AddColumn, Column, ColumnGap } from "./Column";
+import { Note } from "./Note";
+import { useBoardActions } from "./actions";
export enum DragTypes {
Note = "application/note",
@@ -39,211 +23,71 @@ export type BoardData = {
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;
- }
-
- createEffect(() => {
- const mutations = untrack(() => getMutations());
-
- const { notes, columns } = props.board;
- 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
- );
-
- if (!optimisticUpdates) return console.log(`Skipping optimistic update`);
-
- setBoardStore(
- produce((b) => {
- applyMutations(latestMutations, b.notes, b.columns);
- b.timestamp = Date.now();
- })
- );
- });
+ const {
+ onMoveColumn,
+ onMoveNote,
+ onCreateNote,
+ onCreateColumn,
+ onDeleteColumn,
+ onRenameColumn,
+ onDeleteNote,
+ onEditNote,
+ boardData,
+ } = useBoardActions();
+
+ const boardStore = createSubjectStore(
+ boardData,
+ onCreateNote(([note]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.notes.findIndex((n) => n.id === note.id);
+ if (index === -1) board.notes.push(note);
+ }),
+ onMoveNote(([note, column, order]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.notes.findIndex((n) => n.id === note);
+ if (index !== -1) {
+ board.notes[index].column = column;
+ board.notes[index].order = order;
+ }
+ }),
+ onEditNote(([id, content]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.notes.findIndex((n) => n.id === id);
+ if (index !== -1) board.notes[index].body = content;
+ }),
+ onDeleteNote(([id]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.notes.findIndex((n) => n.id === id);
+ if (index !== -1) board.notes.splice(index, 1);
+ }),
+ onCreateColumn(([id, boardId, name]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.columns.findIndex((c) => c.id === id);
+ if (index === -1)
+ board.columns.push({
+ id: id,
+ board: boardId,
+ title: name,
+ order: board.columns.length + 1,
+ });
+ }),
+ onRenameColumn(([id, name]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.columns.findIndex((c) => c.id === id);
+ if (index !== -1) board.columns[index].title = name;
+ }),
+ onMoveColumn(([id, order]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.columns.findIndex((c) => c.id === id);
+ if (index !== -1) board.columns[index].order = order;
+ }),
+ onDeleteColumn(([id]) => (board) => {
+ if (!optimisticUpdates) return;
+ const index = board.columns.findIndex((c) => c.id === id);
+ if (index !== -1) board.columns.splice(index, 1);
+ })
+ );
const sortedColumns = createMemo(() =>
boardStore.columns.slice().sort((a, b) => a.order - b.order)
@@ -285,73 +129,6 @@ export function Board(props: { board: BoardData }) {
);
}
-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].column = mut.column;
- notes[index].order = mut.order;
- }
- break;
- }
- case "editNote": {
- const index = notes.findIndex((n) => n.id === mut.id);
- if (index !== -1) 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].title = mut.title;
- break;
- }
- case "moveColumn": {
- const index = columns.findIndex((c) => c.id === mut.id);
- if (index !== -1) 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;
- }
- }
- }
-}
-
let optimisticUpdates = true;
if (typeof window !== "undefined") {
// disable optimistic updates in production for testing/demonstration purposes
diff --git a/src/components/Column.tsx b/src/components/Column.tsx
index cca6d73..8a10bea 100644
--- a/src/components/Column.tsx
+++ b/src/components/Column.tsx
@@ -15,6 +15,8 @@ import { AddNote, Note, NoteId, moveNote } from "./Note";
import { getAuthUser } from "~/lib/auth";
import { db } from "~/lib/db";
import { fetchBoard } from "~/lib";
+import { createEvent, createSubject, halt } from "solid-events";
+import { useBoardActions } from "./actions";
export const renameColumn = action(
async (id: ColumnId, name: string, timestamp: number) => {
@@ -88,14 +90,65 @@ export type Column = {
order: number;
};
+type BlurInput = FocusEvent & {
+ target: HTMLInputElement;
+};
export function Column(props: { column: Column; board: Board; notes: Note[] }) {
let parent: HTMLDivElement | undefined;
- const renameAction = useAction(renameColumn);
- const deleteAction = useAction(deleteColumn);
- const moveNoteAction = useAction(moveNote);
+ const { emitRenameColumn, emitDeleteColumn, emitMoveNote } =
+ useBoardActions();
+
+ const [onDragStart, emitDragStart] = createEvent