create-tinybase 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -18
- package/cli.js +2 -2
- package/package.json +7 -4
- package/screenshots/chat.png +0 -0
- package/screenshots/drawing.png +0 -0
- package/screenshots/todos.png +0 -0
- package/templates/README.md.hbs +130 -12
- package/templates/client/eslint.config.js.hbs +7 -0
- package/templates/client/package.json.hbs +28 -8
- package/templates/client/src/chat/App.tsx.hbs +28 -22
- package/templates/client/src/chat/ChatStore.tsx.hbs +108 -27
- package/templates/client/src/chat/Message.tsx.hbs +8 -9
- package/templates/client/src/chat/MessageInput.tsx.hbs +7 -7
- package/templates/client/src/chat/Messages.tsx.hbs +5 -15
- package/templates/client/src/chat/SettingsStore.tsx.hbs +86 -9
- package/templates/client/src/chat/UsernameInput.tsx.hbs +6 -11
- package/templates/client/src/chat/app.ts.hbs +18 -18
- package/templates/client/src/chat/chatStore.ts.hbs +97 -35
- package/templates/client/src/chat/message.ts.hbs +4 -3
- package/templates/client/src/chat/messageInput.css.hbs +1 -1
- package/templates/client/src/chat/messageInput.ts.hbs +7 -8
- package/templates/client/src/chat/messages.ts.hbs +7 -12
- package/templates/client/src/chat/settingsStore.ts.hbs +65 -6
- package/templates/client/src/chat/usernameInput.css.hbs +1 -1
- package/templates/client/src/chat/usernameInput.ts.hbs +6 -6
- package/templates/client/src/chat/utils.ts.hbs +26 -0
- package/templates/client/src/drawing/App.tsx.hbs +26 -20
- package/templates/client/src/drawing/BrushSize.tsx.hbs +8 -11
- package/templates/client/src/drawing/Canvas.tsx.hbs +65 -73
- package/templates/client/src/drawing/CanvasStore.tsx.hbs +104 -18
- package/templates/client/src/drawing/ColorPicker.tsx.hbs +4 -11
- package/templates/client/src/drawing/DrawingControls.tsx.hbs +7 -7
- package/templates/client/src/drawing/SettingsStore.tsx.hbs +81 -8
- package/templates/client/src/drawing/app.ts.hbs +18 -8
- package/templates/client/src/drawing/brushSize.ts.hbs +12 -5
- package/templates/client/src/drawing/canvas.ts.hbs +84 -86
- package/templates/client/src/drawing/canvasStore.ts.hbs +93 -26
- package/templates/client/src/drawing/colorPicker.ts.hbs +3 -3
- package/templates/client/src/drawing/drawingControls.ts.hbs +7 -7
- package/templates/client/src/drawing/settingsStore.ts.hbs +63 -8
- package/templates/client/src/game/App.tsx.hbs +20 -16
- package/templates/client/src/game/Board.tsx.hbs +8 -8
- package/templates/client/src/game/Game.tsx.hbs +14 -21
- package/templates/client/src/game/GameStatus.tsx.hbs +5 -5
- package/templates/client/src/game/Square.tsx.hbs +6 -11
- package/templates/client/src/game/Store.tsx.hbs +106 -16
- package/templates/client/src/game/app.ts.hbs +17 -6
- package/templates/client/src/game/board.ts.hbs +7 -7
- package/templates/client/src/game/game.ts.hbs +12 -18
- package/templates/client/src/game/gameStatus.ts.hbs +3 -3
- package/templates/client/src/game/square.ts.hbs +4 -4
- package/templates/client/src/game/store.ts.hbs +95 -23
- package/templates/client/src/index.tsx.hbs +5 -7
- package/templates/client/src/shared/Button.tsx.hbs +3 -3
- package/templates/client/src/shared/Input.tsx.hbs +2 -2
- package/templates/client/src/shared/Loading.tsx.hbs +5 -0
- package/templates/client/src/shared/button.ts.hbs +2 -2
- package/templates/client/src/shared/config.ts.hbs +4 -6
- package/templates/client/src/shared/input.ts.hbs +2 -2
- package/templates/client/src/shared/loading.css.hbs +21 -0
- package/templates/client/src/shared/loading.ts.hbs +13 -0
- package/templates/client/src/shared/pglite.ts.hbs +10 -0
- package/templates/client/src/shared/sqlite.ts.hbs +17 -0
- package/templates/client/src/todos/App.tsx.hbs +22 -22
- package/templates/client/src/todos/Store.tsx.hbs +106 -23
- package/templates/client/src/todos/TodoInput.tsx.hbs +6 -8
- package/templates/client/src/todos/TodoItem.tsx.hbs +5 -6
- package/templates/client/src/todos/TodoList.tsx.hbs +5 -6
- package/templates/client/src/todos/app.ts.hbs +16 -10
- package/templates/client/src/todos/store.ts.hbs +94 -30
- package/templates/client/src/todos/todoInput.ts.hbs +5 -8
- package/templates/client/src/todos/todoItem.ts.hbs +3 -4
- package/templates/client/src/todos/todoList.ts.hbs +6 -8
- package/templates/client/vite-env.d.ts.hbs +4 -0
- package/templates/client/vite.config.js.hbs +53 -3
- package/templates/server/index.ts.hbs +43 -0
- package/templates/server/package.json.hbs +6 -7
- package/templates/server/wrangler.toml.hbs +1 -1
- package/templates/server/index-do.ts.hbs +0 -22
- package/templates/server/index-node.ts.hbs +0 -8
- /package/templates/client/{.prettierrc.hbs → .prettierrc.json.hbs} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{{includeFile template="client/src/chat/messageInput.css.hbs" output="client/src/messageInput.css"}}
|
|
2
|
-
import './messageInput.css';
|
|
3
|
-
import {type SettingsStore} from './settingsStore';
|
|
4
|
-
import {type ChatStore} from './chatStore';
|
|
2
|
+
{{addImport "import './messageInput.css';"}}
|
|
3
|
+
{{addImport "import {type SettingsStore} from './settingsStore';"}}
|
|
4
|
+
{{addImport "import {type ChatStore} from './chatStore';"}}
|
|
5
5
|
|
|
6
6
|
{{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
|
|
7
|
-
import {createButton} from './button';
|
|
7
|
+
{{addImport "import {createButton} from './button';"}}
|
|
8
8
|
|
|
9
9
|
{{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
|
|
10
|
-
import {createInput} from './input';
|
|
10
|
+
{{addImport "import {createInput} from './input';"}}
|
|
11
11
|
|
|
12
12
|
export const createMessageInput = (settingsStore: SettingsStore, chatStore: ChatStore): HTMLDivElement => {
|
|
13
13
|
const messageInputContainer = document.createElement('div');
|
|
@@ -20,12 +20,11 @@ const sendMessage = () => {
|
|
|
20
20
|
const text = messageInput.value.trim();
|
|
21
21
|
if (text) {
|
|
22
22
|
chatStore.addRow('messages', {
|
|
23
|
-
username: settingsStore.getValue('username'),
|
|
23
|
+
username: settingsStore.getValue('username') || 'Anonymous',
|
|
24
24
|
text,
|
|
25
25
|
timestamp: Date.now(),
|
|
26
26
|
});
|
|
27
27
|
messageInput.value = '';
|
|
28
|
-
messageInput.focus();
|
|
29
28
|
}
|
|
30
29
|
};
|
|
31
30
|
|
|
@@ -39,4 +38,4 @@ const sendButton = createButton('Send', sendMessage, 'primary');
|
|
|
39
38
|
messageInputContainer.appendChild(sendButton);
|
|
40
39
|
|
|
41
40
|
return messageInputContainer;
|
|
42
|
-
};
|
|
41
|
+
};
|
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
{{includeFile template="client/src/chat/messages.css.hbs" output="client/src/messages.css"}}
|
|
2
|
-
import './messages.css';
|
|
3
|
-
import {type ChatStore} from './chatStore';
|
|
2
|
+
{{addImport "import './messages.css';"}}
|
|
3
|
+
{{addImport "import {type ChatStore, type MessageRow} from './chatStore';"}}
|
|
4
4
|
|
|
5
5
|
{{includeFile template="client/src/chat/message.ts.hbs" output="client/src/message.{{ext}}"}}
|
|
6
|
-
import {createMessage} from './message';
|
|
6
|
+
{{addImport "import {createMessage} from './message';"}}
|
|
7
7
|
|
|
8
8
|
export const createMessages = (store: ChatStore): HTMLDivElement => {
|
|
9
9
|
const messagesContainer = document.createElement('div');
|
|
10
10
|
messagesContainer.id = 'messages';
|
|
11
11
|
|
|
12
12
|
const updateMessages = () => {
|
|
13
|
-
const
|
|
14
|
-
const sortedIds = Object.keys(messageRows).sort(
|
|
15
|
-
(a, b) => messageRows[a].timestamp - messageRows[b].timestamp,
|
|
16
|
-
);
|
|
13
|
+
const sortedIds = store.getSortedRowIds('messages', 'timestamp');
|
|
17
14
|
|
|
18
15
|
messagesContainer.innerHTML = '';
|
|
19
16
|
sortedIds.forEach((id) => {
|
|
20
|
-
const
|
|
21
|
-
const messageElement = createMessage(
|
|
17
|
+
const messageRow = store.getRow('messages', id) as MessageRow;
|
|
18
|
+
const messageElement = createMessage(messageRow);
|
|
22
19
|
messagesContainer.appendChild(messageElement);
|
|
23
20
|
});
|
|
24
21
|
};
|
|
25
22
|
|
|
26
|
-
// Initial render
|
|
27
23
|
updateMessages();
|
|
28
24
|
|
|
29
|
-
// Listen for changes
|
|
30
25
|
store.addTablesListener(updateMessages);
|
|
31
26
|
|
|
32
27
|
return messagesContainer;
|
|
33
|
-
};
|
|
28
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{{#if schemas}}
|
|
2
|
-
import {createStore} from 'tinybase/with-schemas';
|
|
2
|
+
{{addImport "import {createStore} from 'tinybase/with-schemas';"}}
|
|
3
3
|
{{else}}
|
|
4
|
-
import {createStore} from 'tinybase';
|
|
4
|
+
{{addImport "import {createStore} from 'tinybase';"}}
|
|
5
5
|
{{/if}}
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
{{includeFile template="client/src/chat/utils.ts.hbs" output="client/src/utils.ts"}}
|
|
8
|
+
{{addImport "import {getRandomName} from './utils';"}}
|
|
8
9
|
|
|
9
10
|
{{#if schemas}}
|
|
10
11
|
const VALUES_SCHEMA = {
|
|
@@ -13,7 +14,65 @@ const STORE_ID = 'settings';
|
|
|
13
14
|
|
|
14
15
|
{{/if}}
|
|
15
16
|
export const settingsStore = createStore(){{#if schemas}}
|
|
16
|
-
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
17
|
-
.setValue('username', 'Carol');
|
|
17
|
+
.setValuesSchema(VALUES_SCHEMA){{/if}};
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
let resolveSettingsReady: () => void;
|
|
20
|
+
export const settingsStoreReady = new Promise<void>((resolve) => {
|
|
21
|
+
resolveSettingsReady = resolve;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
{{#if persist}}
|
|
25
|
+
{{#if persistLocalStorage}}
|
|
26
|
+
{{#if schemas}}
|
|
27
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
|
|
28
|
+
{{else}}
|
|
29
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
|
|
30
|
+
{{/if}}
|
|
31
|
+
|
|
32
|
+
const settingsPersister = createLocalPersister(settingsStore, 'settings');
|
|
33
|
+
settingsPersister.load([{}, {username: getRandomName()}]).then(() => {
|
|
34
|
+
settingsPersister.startAutoSave();
|
|
35
|
+
resolveSettingsReady();
|
|
36
|
+
});
|
|
37
|
+
{{/if}}
|
|
38
|
+
{{#if persistSqlite}}
|
|
39
|
+
{{#if schemas}}
|
|
40
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
|
|
41
|
+
{{else}}
|
|
42
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
|
|
43
|
+
{{/if}}
|
|
44
|
+
{{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.ts"}}
|
|
45
|
+
{{addImport "import {getDb} from './sqlite';"}}
|
|
46
|
+
|
|
47
|
+
getDb().then(({sqlite3, db}) => {
|
|
48
|
+
const settingsPersister = createSqliteWasmPersister(settingsStore, sqlite3, db, 'settings');
|
|
49
|
+
settingsPersister.load([{}, {username: getRandomName()}]).then(() => {
|
|
50
|
+
settingsPersister.startAutoSave();
|
|
51
|
+
resolveSettingsReady();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
{{/if}}
|
|
55
|
+
{{#if persistPglite}}
|
|
56
|
+
{{#if schemas}}
|
|
57
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
|
|
58
|
+
{{else}}
|
|
59
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
|
|
60
|
+
{{/if}}
|
|
61
|
+
{{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
|
|
62
|
+
{{addImport "import {getPgLite} from './pglite';"}}
|
|
63
|
+
|
|
64
|
+
getPgLite().then((pgLite) => {
|
|
65
|
+
createPglitePersister(settingsStore, pgLite, 'settings').then((settingsPersister) => {
|
|
66
|
+
settingsPersister.load([{}, {username: getRandomName()}]).then(() => {
|
|
67
|
+
settingsPersister.startAutoSave();
|
|
68
|
+
resolveSettingsReady();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
{{/if}}
|
|
73
|
+
{{else}}
|
|
74
|
+
settingsStore.setValue('username', getRandomName());
|
|
75
|
+
resolveSettingsReady();
|
|
76
|
+
{{/if}}
|
|
77
|
+
|
|
78
|
+
export type SettingsStore = typeof settingsStore;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{{includeFile template="client/src/chat/usernameInput.css.hbs" output="client/src/usernameInput.css"}}
|
|
2
|
-
import './usernameInput.css';
|
|
3
|
-
import {type SettingsStore} from './settingsStore';
|
|
4
|
-
import {type ChatStore} from './chatStore';
|
|
2
|
+
{{addImport "import './usernameInput.css';"}}
|
|
3
|
+
{{addImport "import {type SettingsStore} from './settingsStore';"}}
|
|
4
|
+
{{addImport "import {type ChatStore} from './chatStore';"}}
|
|
5
5
|
|
|
6
6
|
{{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
|
|
7
|
-
import {createInput} from './input';
|
|
7
|
+
{{addImport "import {createInput} from './input';"}}
|
|
8
8
|
|
|
9
|
-
export const createUsernameInput = (settingsStore: SettingsStore,
|
|
9
|
+
export const createUsernameInput = (settingsStore: SettingsStore, _chatStore: ChatStore): HTMLDivElement => {
|
|
10
10
|
const container = document.createElement('div');
|
|
11
11
|
container.id = 'usernameInput';
|
|
12
12
|
|
|
@@ -27,4 +27,4 @@ usernameInput.value = settingsStore.getValue('username') as string;
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
return container;
|
|
30
|
-
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const NAMES = [
|
|
2
|
+
'Carol',
|
|
3
|
+
'Charlie',
|
|
4
|
+
'Casey',
|
|
5
|
+
'Cameron',
|
|
6
|
+
'Chris',
|
|
7
|
+
'Dana',
|
|
8
|
+
'Dakota',
|
|
9
|
+
'Drew',
|
|
10
|
+
'Devon',
|
|
11
|
+
'Dylan',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const getRandomName = (): string =>
|
|
15
|
+
NAMES[Math.floor(Math.random() * NAMES.length)];
|
|
16
|
+
|
|
17
|
+
export const getDefaultContent = (): [any, any] =>
|
|
18
|
+
[
|
|
19
|
+
{
|
|
20
|
+
messages: {
|
|
21
|
+
'1': {username: 'Alice', text: 'Hello!', timestamp: Date.now() - 60000},
|
|
22
|
+
'2': {username: 'Bob', text: 'Hi there!', timestamp: Date.now() - 30000},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{},
|
|
26
|
+
];
|
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import {StrictMode} from 'react';
|
|
1
|
+
{{addImport "import {StrictMode, useState} from 'react';"}}
|
|
2
|
+
{{includeFile template="client/src/shared/Loading.tsx.hbs" output="client/src/Loading.{{ext}}"}}
|
|
3
|
+
{{addImport "import {Loading} from './Loading';"}}
|
|
2
4
|
|
|
3
|
-
{{
|
|
4
|
-
|
|
5
|
-
{{else}}
|
|
6
|
-
import {Provider} from 'tinybase/ui-react';
|
|
7
|
-
{{/if}}
|
|
8
|
-
import {Inspector} from 'tinybase/ui-react-inspector';
|
|
5
|
+
{{addImport "import {Provider} from 'tinybase/ui-react';"}}
|
|
6
|
+
{{addImport "import {Inspector} from 'tinybase/ui-react-inspector';"}}
|
|
9
7
|
|
|
10
8
|
{{includeFile template="client/src/drawing/SettingsStore.tsx.hbs" output="client/src/SettingsStore.{{ext}}"}}
|
|
11
|
-
import {SettingsStore} from './SettingsStore';
|
|
9
|
+
{{addImport "import {SettingsStore} from './SettingsStore';"}}
|
|
12
10
|
|
|
13
11
|
{{includeFile template="client/src/drawing/CanvasStore.tsx.hbs" output="client/src/CanvasStore.{{ext}}"}}
|
|
14
|
-
import {CanvasStore} from './CanvasStore';
|
|
12
|
+
{{addImport "import {CanvasStore} from './CanvasStore';"}}
|
|
15
13
|
|
|
16
14
|
{{includeFile template="client/src/drawing/DrawingControls.tsx.hbs" output="client/src/DrawingControls.{{ext}}"}}
|
|
17
|
-
import {DrawingControls} from './DrawingControls';
|
|
15
|
+
{{addImport "import {DrawingControls} from './DrawingControls';"}}
|
|
18
16
|
|
|
19
17
|
{{includeFile template="client/src/drawing/Canvas.tsx.hbs" output="client/src/Canvas.{{ext}}"}}
|
|
20
|
-
import {Canvas} from './Canvas';
|
|
18
|
+
{{addImport "import {Canvas} from './Canvas';"}}
|
|
19
|
+
|
|
20
|
+
export const App = () => {
|
|
21
|
+
const [settingsReady, setSettingsReady] = useState(false);
|
|
22
|
+
const [canvasReady, setCanvasReady] = useState(false);
|
|
23
|
+
const loading = !settingsReady || !canvasReady;
|
|
21
24
|
|
|
22
|
-
const App = () => {
|
|
23
25
|
return (
|
|
24
26
|
<StrictMode>
|
|
25
27
|
<Provider>
|
|
26
|
-
<SettingsStore />
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
<SettingsStore onReady={()=> setSettingsReady(true)} />
|
|
29
|
+
<CanvasStore onReady={()=> setCanvasReady(true)} />
|
|
30
|
+
{loading ? (
|
|
31
|
+
<Loading />
|
|
32
|
+
) : (
|
|
33
|
+
<>
|
|
34
|
+
<DrawingControls />
|
|
35
|
+
<Canvas />
|
|
36
|
+
<Inspector />
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
31
39
|
</Provider>
|
|
32
40
|
</StrictMode>
|
|
33
41
|
);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export {App};
|
|
42
|
+
};
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
{{includeFile template="client/src/drawing/brushSize.css.hbs" output="client/src/brushSize.css"}}
|
|
2
|
-
import {
|
|
3
|
-
import './brushSize.css';
|
|
2
|
+
{{addImport "import {useValueState, STORE_ID} from './SettingsStore';"}}
|
|
3
|
+
{{addImport "import './brushSize.css';"}}
|
|
4
4
|
|
|
5
5
|
export const BrushSize = () => {
|
|
6
|
-
const size =
|
|
6
|
+
const [size, setSize] = useValueState('brushSize', STORE_ID);
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
[],
|
|
12
|
-
STORE_ID,
|
|
13
|
-
);
|
|
8
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
9
|
+
setSize(parseInt(e.target.value));
|
|
10
|
+
};
|
|
14
11
|
|
|
15
12
|
return (
|
|
16
13
|
<div id="brushSize">
|
|
17
14
|
<label>Size:</label>
|
|
18
|
-
<input type="range" min="1" max="50" value={size} onChange={
|
|
15
|
+
<input type="range" min="1" max="50" value={(size as number) ?? 5} onChange={handleChange} />
|
|
19
16
|
<span id="brushSizeValue">{size}</span>
|
|
20
17
|
</div>
|
|
21
18
|
);
|
|
22
|
-
};
|
|
19
|
+
};
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{{includeFile template="client/src/drawing/canvas.css.hbs" output="client/src/canvas.css"}}
|
|
2
|
-
import {useCallback, useEffect, useRef} from 'react';
|
|
3
|
-
import {useTable, useStore, STORE_ID as CANVAS_STORE_ID} from './CanvasStore';
|
|
4
|
-
import {useValues, STORE_ID as SETTINGS_STORE_ID} from './SettingsStore';
|
|
5
|
-
import {
|
|
6
|
-
import './canvas.css';
|
|
2
|
+
{{addImport "import {useCallback, useEffect, useRef} from 'react';"}}
|
|
3
|
+
{{addImport "import {useSortedRowIds, useTable, useStore, type StrokeRow, STORE_ID as CANVAS_STORE_ID} from './CanvasStore';"}}
|
|
4
|
+
{{addImport "import {useValues, STORE_ID as SETTINGS_STORE_ID} from './SettingsStore';"}}
|
|
5
|
+
{{addImport "import {getHlcFunctions} from 'tinybase';"}}
|
|
6
|
+
{{addImport "import './canvas.css';"}}
|
|
7
|
+
|
|
8
|
+
const [getNextHlc] = getHlcFunctions();
|
|
7
9
|
|
|
8
10
|
export const Canvas = () => {
|
|
9
11
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
10
12
|
const isDrawing = useRef(false);
|
|
11
13
|
const currentStrokeId = useRef<string | null>(null);
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
+
const {brushColor, brushSize} = useValues(SETTINGS_STORE_ID);
|
|
15
|
+
const sortedIds = useSortedRowIds('strokes', undefined, false, 0, undefined, CANVAS_STORE_ID);
|
|
14
16
|
const strokes = useTable('strokes', CANVAS_STORE_ID);
|
|
15
17
|
const store = useStore(CANVAS_STORE_ID);
|
|
16
18
|
|
|
@@ -24,77 +26,67 @@ const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
24
26
|
ctx.fillStyle = '#111';
|
|
25
27
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ctx.moveTo(points[0].x, points[0].y);
|
|
42
|
-
for (let i = 1; i < points.length; i++) { ctx.lineTo(points[i].x, points[i].y); } ctx.stroke(); } } }); }, [strokes]); useEffect(()=> {
|
|
43
|
-
draw();
|
|
44
|
-
}, [draw]);
|
|
45
|
-
|
|
46
|
-
const getPoint = (e: MouseEvent | TouchEvent) => {
|
|
47
|
-
const canvas = canvasRef.current;
|
|
48
|
-
if (!canvas) return null;
|
|
29
|
+
sortedIds.forEach((id) => {
|
|
30
|
+
const stroke = store?.getRow('strokes', id) as StrokeRow;
|
|
31
|
+
if (stroke?.points) {
|
|
32
|
+
const pointsArray = JSON.parse(stroke.points) as number[];
|
|
33
|
+
if (pointsArray.length >= 2) {
|
|
34
|
+
ctx.strokeStyle = stroke.color;
|
|
35
|
+
ctx.lineWidth = stroke.size * 2;
|
|
36
|
+
ctx.lineCap = 'round';
|
|
37
|
+
ctx.lineJoin = 'round';
|
|
38
|
+
ctx.beginPath();
|
|
39
|
+
ctx.moveTo(pointsArray[0], pointsArray[1]);
|
|
40
|
+
for (let i = 2; i < pointsArray.length; i +=2) { ctx.lineTo(pointsArray[i], pointsArray[i + 1]); } ctx.stroke(); } } }); }, [sortedIds, strokes, store]); useEffect(()=> {
|
|
41
|
+
draw();
|
|
42
|
+
}, [draw]);
|
|
49
43
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
const getPoint = (e: MouseEvent | TouchEvent) => {
|
|
45
|
+
const canvas = canvasRef.current;
|
|
46
|
+
if (!canvas) return null;
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
};
|
|
48
|
+
const rect = canvas.getBoundingClientRect();
|
|
49
|
+
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
50
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
return {
|
|
53
|
+
x: clientX - rect.left,
|
|
54
|
+
y: clientY - rect.top,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
58
|
+
const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
|
|
59
|
+
isDrawing.current = true;
|
|
60
|
+
currentStrokeId.current = getNextHlc();
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
const point = getPoint(e.nativeEvent);
|
|
63
|
+
if (point && store) {
|
|
64
|
+
store.setRow('strokes', currentStrokeId.current, {
|
|
65
|
+
color: brushColor,
|
|
66
|
+
size: brushSize,
|
|
67
|
+
points: JSON.stringify([point.x, point.y]),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
72
|
+
const handleMove = (e: React.MouseEvent | React.TouchEvent) => {
|
|
73
|
+
if (isDrawing.current && currentStrokeId.current && store) {
|
|
74
|
+
const point = getPoint(e.nativeEvent);
|
|
75
|
+
if (point) {
|
|
76
|
+
const pointsArray = JSON.parse(store.getCell('strokes', currentStrokeId.current, 'points') as string ?? '[]');
|
|
77
|
+
pointsArray.push(point.x, point.y);
|
|
78
|
+
store.setCell('strokes', currentStrokeId.current, 'points', JSON.stringify(pointsArray));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
83
|
+
const handleEnd = () => {
|
|
84
|
+
isDrawing.current = false;
|
|
85
|
+
currentStrokeId.current = null;
|
|
86
|
+
};
|
|
95
87
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
88
|
+
return (
|
|
89
|
+
<canvas ref={canvasRef} width={600} height={400} id="drawingCanvas" onMouseDown={handleStart} onMouseMove={handleMove} onMouseUp={handleEnd} onMouseLeave={handleEnd} onTouchStart={handleStart} onTouchMove={handleMove}
|
|
90
|
+
onTouchEnd={handleEnd} />
|
|
91
|
+
);
|
|
92
|
+
};
|
|
@@ -1,48 +1,127 @@
|
|
|
1
1
|
{{#if schemas}}
|
|
2
|
-
import {createMergeableStore} from 'tinybase/with-schemas';
|
|
3
|
-
import * as UiReact from 'tinybase/ui-react/with-schemas';
|
|
4
|
-
import {type NoValuesSchema} from 'tinybase/with-schemas';
|
|
2
|
+
{{addImport "import {createMergeableStore, type Row} from 'tinybase/with-schemas';"}}
|
|
3
|
+
{{addImport "import * as UiReact from 'tinybase/ui-react/with-schemas';"}}
|
|
4
|
+
{{addImport "import {type NoValuesSchema} from 'tinybase/with-schemas';"}}
|
|
5
5
|
{{else}}
|
|
6
|
-
import {createMergeableStore} from 'tinybase';
|
|
7
|
-
import {
|
|
6
|
+
{{addImport "import {createMergeableStore} from 'tinybase';"}}
|
|
7
|
+
{{addImport "import {useCreateMergeableStore, useProvideStore, useDelTableCallback, useRow, useSortedRowIds, useStore, useTable} from 'tinybase/ui-react';"}}
|
|
8
8
|
{{/if}}
|
|
9
|
-
import {getUniqueId} from 'tinybase';
|
|
10
9
|
|
|
11
10
|
export const STORE_ID = 'canvas';
|
|
12
11
|
|
|
13
12
|
{{#if schemas}}
|
|
14
13
|
const TABLES_SCHEMA = {
|
|
15
14
|
strokes: {
|
|
16
|
-
color: {type: 'string'},
|
|
17
|
-
size: {type: 'number'},
|
|
15
|
+
color: {type: 'string', default: ''},
|
|
16
|
+
size: {type: 'number', default: 1},
|
|
17
|
+
points: {type: 'string', default: '[]'},
|
|
18
18
|
},
|
|
19
19
|
} as const;
|
|
20
20
|
|
|
21
21
|
type Schemas = [typeof TABLES_SCHEMA, NoValuesSchema];
|
|
22
22
|
|
|
23
|
-
const {
|
|
23
|
+
const {useCreateMergeableStore, {{#if persist}}useCreatePersister, {{/if}}useDelTableCallback, useProvideStore, useRow, useSortedRowIds, useStore, useTable} = UiReact as UiReact.WithSchemas<Schemas>;
|
|
24
24
|
{{/if}}
|
|
25
25
|
|
|
26
|
-
export {
|
|
26
|
+
export type StrokeRow = {{#if schemas}}Row<typeof TABLES_SCHEMA, 'strokes'>{{else}}{color: string; size: number; points: string}{{/if}};
|
|
27
27
|
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export {useDelTableCallback, useRow, useSortedRowIds, useStore, useTable};
|
|
29
|
+
|
|
30
|
+
export const CanvasStore = ({onReady}: {onReady?: () => void}) => {
|
|
31
|
+
const store = useCreateMergeableStore(() =>
|
|
32
|
+
createMergeableStore(){{#if schemas}}
|
|
31
33
|
.setTablesSchema(TABLES_SCHEMA){{/if}}
|
|
32
34
|
.setDefaultContent([{strokes: {}}, {}]),
|
|
33
35
|
);
|
|
34
36
|
|
|
35
37
|
useProvideStore(STORE_ID, store);
|
|
36
38
|
|
|
39
|
+
{{#if persist}}
|
|
40
|
+
{{#if persistLocalStorage}}
|
|
41
|
+
{{#if schemas}}
|
|
42
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
|
|
43
|
+
{{else}}
|
|
44
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
|
|
45
|
+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
|
|
46
|
+
{{/if}}
|
|
47
|
+
|
|
48
|
+
useCreatePersister(
|
|
49
|
+
store,
|
|
50
|
+
(store) => createLocalPersister(store, STORE_ID),
|
|
51
|
+
[],
|
|
52
|
+
async (persister) => {
|
|
53
|
+
await persister.load();
|
|
54
|
+
await persister.startAutoSave();
|
|
55
|
+
onReady?.();
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
{{/if}}
|
|
59
|
+
{{#if persistSqlite}}
|
|
60
|
+
{{#if schemas}}
|
|
61
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
|
|
62
|
+
{{else}}
|
|
63
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
|
|
64
|
+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
|
|
65
|
+
{{/if}}
|
|
66
|
+
{{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.{{ext}}"}}
|
|
67
|
+
{{addImport "import {getDb} from './sqlite';"}}
|
|
68
|
+
|
|
69
|
+
useCreatePersister(
|
|
70
|
+
store,
|
|
71
|
+
async (store) => {
|
|
72
|
+
const {sqlite3, db} = await getDb();
|
|
73
|
+
return createSqliteWasmPersister(store, sqlite3, db, STORE_ID);
|
|
74
|
+
},
|
|
75
|
+
[],
|
|
76
|
+
async (persister) => {
|
|
77
|
+
await persister.load();
|
|
78
|
+
await persister.startAutoSave();
|
|
79
|
+
onReady?.();
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
{{/if}}
|
|
83
|
+
{{#if persistPglite}}
|
|
84
|
+
{{#if schemas}}
|
|
85
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
|
|
86
|
+
{{else}}
|
|
87
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
|
|
88
|
+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
|
|
89
|
+
{{/if}}
|
|
90
|
+
{{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
|
|
91
|
+
{{addImport "import {getPgLite} from './pglite';"}}
|
|
92
|
+
|
|
93
|
+
useCreatePersister(
|
|
94
|
+
store,
|
|
95
|
+
async (store) => {
|
|
96
|
+
const pgLite = await getPgLite();
|
|
97
|
+
return await createPglitePersister(store, pgLite, 'canvas');
|
|
98
|
+
},
|
|
99
|
+
[],
|
|
100
|
+
async (persister) => {
|
|
101
|
+
await persister.load();
|
|
102
|
+
await persister.startAutoSave();
|
|
103
|
+
onReady?.();
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
{{/if}}
|
|
107
|
+
{{/if}}
|
|
108
|
+
|
|
37
109
|
{{#if sync}}
|
|
38
110
|
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
39
111
|
{{addImport "import {SERVER} from './config';"}}
|
|
40
112
|
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
41
|
-
{{
|
|
42
|
-
|
|
43
|
-
{{
|
|
113
|
+
{{#if schemas}}
|
|
114
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client/with-schemas';"}}
|
|
115
|
+
{{else}}
|
|
116
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
117
|
+
{{/if}}
|
|
118
|
+
{{#if schemas}}
|
|
119
|
+
{{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react/with-schemas';"}}
|
|
120
|
+
{{else}}
|
|
121
|
+
{{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
|
|
122
|
+
{{/if}}
|
|
44
123
|
|
|
45
|
-
useCreateSynchronizer(store, async (store
|
|
124
|
+
useCreateSynchronizer(store, async (store) => {
|
|
46
125
|
const serverPathId = location.pathname;
|
|
47
126
|
const synchronizer = await createWsSynchronizer(
|
|
48
127
|
store,
|
|
@@ -51,12 +130,19 @@ useProvideStore(STORE_ID, store);
|
|
|
51
130
|
await synchronizer.startSync();
|
|
52
131
|
|
|
53
132
|
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
54
|
-
synchronizer.load().then(() =>
|
|
133
|
+
synchronizer.load().then(() => {
|
|
134
|
+
synchronizer.save();
|
|
135
|
+
{{#unless persist}}onReady?.();{{/unless}}
|
|
136
|
+
});
|
|
55
137
|
});
|
|
56
138
|
|
|
57
139
|
return synchronizer;
|
|
58
140
|
});
|
|
59
141
|
{{/if}}
|
|
60
142
|
|
|
143
|
+
{{#unless persist}}{{#unless sync}}
|
|
144
|
+
onReady?.();
|
|
145
|
+
{{/unless}}{{/unless}}
|
|
146
|
+
|
|
61
147
|
return null;
|
|
62
148
|
};
|