create-tinybase 0.1.4 → 0.2.0
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/cli.js +2 -2
- package/package.json +2 -2
- package/templates/{base/README.md.hbs → README.md.hbs} +1 -0
- package/templates/{base → client}/eslint.config.js.hbs +5 -0
- package/templates/client/index.html.hbs +182 -0
- package/templates/{base → client}/package.json.hbs +18 -2
- package/templates/client/public/favicon.svg +8 -0
- package/templates/client/src/chat/App.tsx.hbs +40 -0
- package/templates/client/src/chat/ChatStore.tsx.hbs +70 -0
- package/templates/client/src/chat/Message.tsx.hbs +21 -0
- package/templates/client/src/chat/MessageInput.tsx.hbs +42 -0
- package/templates/client/src/chat/Messages.tsx.hbs +29 -0
- package/templates/client/src/chat/SettingsStore.tsx.hbs +34 -0
- package/templates/client/src/chat/UsernameInput.tsx.hbs +22 -0
- package/templates/client/src/chat/app.ts.hbs +39 -0
- package/templates/client/src/chat/chatStore.ts.hbs +50 -0
- package/templates/client/src/chat/message.css.hbs +20 -0
- package/templates/client/src/chat/message.ts.hbs +27 -0
- package/templates/client/src/chat/messageInput.css.hbs +6 -0
- package/templates/client/src/chat/messageInput.ts.hbs +42 -0
- package/templates/client/src/chat/messages.css.hbs +6 -0
- package/templates/client/src/chat/messages.ts.hbs +33 -0
- package/templates/client/src/chat/settingsStore.ts.hbs +19 -0
- package/templates/client/src/chat/usernameInput.css.hbs +14 -0
- package/templates/client/src/chat/usernameInput.ts.hbs +30 -0
- package/templates/client/src/drawing/App.tsx.hbs +36 -0
- package/templates/client/src/drawing/BrushSize.tsx.hbs +22 -0
- package/templates/client/src/drawing/Canvas.tsx.hbs +100 -0
- package/templates/client/src/drawing/CanvasStore.tsx.hbs +62 -0
- package/templates/client/src/drawing/ColorPicker.tsx.hbs +24 -0
- package/templates/client/src/drawing/DrawingControls.tsx.hbs +24 -0
- package/templates/client/src/drawing/SettingsStore.tsx.hbs +36 -0
- package/templates/client/src/drawing/app.ts.hbs +20 -0
- package/templates/client/src/drawing/brushSize.css.hbs +21 -0
- package/templates/client/src/drawing/brushSize.ts.hbs +33 -0
- package/templates/client/src/drawing/canvas.css.hbs +8 -0
- package/templates/client/src/drawing/canvas.ts.hbs +103 -0
- package/templates/client/src/drawing/canvasStore.ts.hbs +42 -0
- package/templates/client/src/drawing/colorPicker.css.hbs +21 -0
- package/templates/client/src/drawing/colorPicker.ts.hbs +34 -0
- package/templates/client/src/drawing/drawingControls.css.hbs +12 -0
- package/templates/client/src/drawing/drawingControls.ts.hbs +26 -0
- package/templates/client/src/drawing/settingsStore.ts.hbs +21 -0
- package/templates/client/src/game/App.tsx.hbs +28 -0
- package/templates/client/src/game/Board.tsx.hbs +27 -0
- package/templates/client/src/game/Game.tsx.hbs +78 -0
- package/templates/client/src/game/GameStatus.tsx.hbs +21 -0
- package/templates/client/src/game/Square.tsx.hbs +23 -0
- package/templates/client/src/game/Store.tsx.hbs +67 -0
- package/templates/client/src/game/app.ts.hbs +12 -0
- package/templates/client/src/game/board.css.hbs +13 -0
- package/templates/client/src/game/board.ts.hbs +39 -0
- package/templates/client/src/game/game.ts.hbs +74 -0
- package/templates/client/src/game/gameStatus.css.hbs +21 -0
- package/templates/client/src/game/gameStatus.ts.hbs +27 -0
- package/templates/client/src/game/square.css.hbs +38 -0
- package/templates/client/src/game/square.ts.hbs +11 -0
- package/templates/client/src/game/store.ts.hbs +47 -0
- package/templates/client/src/index.tsx.hbs +24 -0
- package/templates/client/src/shared/Button.tsx.hbs +16 -0
- package/templates/client/src/shared/Input.tsx.hbs +16 -0
- package/templates/client/src/shared/button.css.hbs +25 -0
- package/templates/client/src/shared/button.ts.hbs +16 -0
- package/templates/client/src/shared/config.ts.hbs +9 -0
- package/templates/client/src/shared/input.css.hbs +22 -0
- package/templates/client/src/shared/input.ts.hbs +17 -0
- package/templates/client/src/todos/App.tsx.hbs +32 -0
- package/templates/client/src/todos/Store.tsx.hbs +70 -0
- package/templates/client/src/todos/TodoInput.tsx.hbs +30 -0
- package/templates/client/src/todos/TodoItem.tsx.hbs +20 -0
- package/templates/client/src/todos/TodoList.tsx.hbs +18 -0
- package/templates/client/src/todos/app.ts.hbs +23 -0
- package/templates/client/src/todos/store.ts.hbs +49 -0
- package/templates/client/src/todos/todoInput.css.hbs +9 -0
- package/templates/client/src/todos/todoInput.ts.hbs +38 -0
- package/templates/client/src/todos/todoItem.css.hbs +33 -0
- package/templates/client/src/todos/todoItem.ts.hbs +28 -0
- package/templates/client/src/todos/todoList.css.hbs +14 -0
- package/templates/client/src/todos/todoList.ts.hbs +38 -0
- package/templates/{base → client}/vite.config.js.hbs +5 -3
- package/templates/package.json.hbs +38 -0
- package/templates/server/index-do.ts.hbs +22 -0
- package/templates/server/index-node.ts.hbs +8 -0
- package/templates/server/package.json.hbs +51 -0
- package/templates/server/tsconfig.json.hbs +13 -0
- package/templates/server/wrangler.toml.hbs +12 -0
- package/templates/base/index.html.hbs +0 -17
- package/templates/base/tsconfig.node.json.hbs +0 -9
- package/templates/src/App.tsx.hbs +0 -24
- package/templates/src/index.css.hbs +0 -110
- package/templates/src/index.tsx.hbs +0 -48
- /package/templates/{base → client}/.prettierrc.hbs +0 -0
- /package/templates/{base → client}/tsconfig.json.hbs +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{includeFile template="client/src/chat/message.css.hbs" output="client/src/message.css"}}
|
|
2
|
+
import './message.css';
|
|
3
|
+
|
|
4
|
+
export const createMessage = (username: string, text: string, timestamp: number): HTMLDivElement => {
|
|
5
|
+
const time = new Date(timestamp).toLocaleTimeString();
|
|
6
|
+
|
|
7
|
+
const messageDiv = document.createElement('div');
|
|
8
|
+
messageDiv.className = 'message';
|
|
9
|
+
|
|
10
|
+
const usernameSpan = document.createElement('span');
|
|
11
|
+
usernameSpan.className = 'username';
|
|
12
|
+
usernameSpan.textContent = `${username}:`;
|
|
13
|
+
|
|
14
|
+
const textSpan = document.createElement('span');
|
|
15
|
+
textSpan.className = 'text';
|
|
16
|
+
textSpan.textContent = text;
|
|
17
|
+
|
|
18
|
+
const timeSpan = document.createElement('span');
|
|
19
|
+
timeSpan.className = 'time';
|
|
20
|
+
timeSpan.textContent = time;
|
|
21
|
+
|
|
22
|
+
messageDiv.appendChild(usernameSpan);
|
|
23
|
+
messageDiv.appendChild(textSpan);
|
|
24
|
+
messageDiv.appendChild(timeSpan);
|
|
25
|
+
|
|
26
|
+
return messageDiv;
|
|
27
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
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';
|
|
5
|
+
|
|
6
|
+
{{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
|
|
7
|
+
import {createButton} from './button';
|
|
8
|
+
|
|
9
|
+
{{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
|
|
10
|
+
import {createInput} from './input';
|
|
11
|
+
|
|
12
|
+
export const createMessageInput = (settingsStore: SettingsStore, chatStore: ChatStore): HTMLDivElement => {
|
|
13
|
+
const messageInputContainer = document.createElement('div');
|
|
14
|
+
messageInputContainer.id = 'messageInput';
|
|
15
|
+
|
|
16
|
+
const messageInput = createInput('Type a message...');
|
|
17
|
+
messageInputContainer.appendChild(messageInput);
|
|
18
|
+
|
|
19
|
+
const sendMessage = () => {
|
|
20
|
+
const text = messageInput.value.trim();
|
|
21
|
+
if (text) {
|
|
22
|
+
chatStore.addRow('messages', {
|
|
23
|
+
username: settingsStore.getValue('username'),
|
|
24
|
+
text,
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
});
|
|
27
|
+
messageInput.value = '';
|
|
28
|
+
messageInput.focus();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
messageInput.addEventListener('keypress', (e) => {
|
|
33
|
+
if (e.key === 'Enter') {
|
|
34
|
+
sendMessage();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const sendButton = createButton('Send', sendMessage, 'primary');
|
|
39
|
+
messageInputContainer.appendChild(sendButton);
|
|
40
|
+
|
|
41
|
+
return messageInputContainer;
|
|
42
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{{includeFile template="client/src/chat/messages.css.hbs" output="client/src/messages.css"}}
|
|
2
|
+
import './messages.css';
|
|
3
|
+
import {type ChatStore} from './chatStore';
|
|
4
|
+
|
|
5
|
+
{{includeFile template="client/src/chat/message.ts.hbs" output="client/src/message.{{ext}}"}}
|
|
6
|
+
import {createMessage} from './message';
|
|
7
|
+
|
|
8
|
+
export const createMessages = (store: ChatStore): HTMLDivElement => {
|
|
9
|
+
const messagesContainer = document.createElement('div');
|
|
10
|
+
messagesContainer.id = 'messages';
|
|
11
|
+
|
|
12
|
+
const updateMessages = () => {
|
|
13
|
+
const messageRows = store.getTable('messages');
|
|
14
|
+
const sortedIds = Object.keys(messageRows).sort(
|
|
15
|
+
(a, b) => messageRows[a].timestamp - messageRows[b].timestamp,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
messagesContainer.innerHTML = '';
|
|
19
|
+
sortedIds.forEach((id) => {
|
|
20
|
+
const msg = messageRows[id];
|
|
21
|
+
const messageElement = createMessage(msg.username, msg.text, msg.timestamp);
|
|
22
|
+
messagesContainer.appendChild(messageElement);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Initial render
|
|
27
|
+
updateMessages();
|
|
28
|
+
|
|
29
|
+
// Listen for changes
|
|
30
|
+
store.addTablesListener(updateMessages);
|
|
31
|
+
|
|
32
|
+
return messagesContainer;
|
|
33
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{{#if schemas}}
|
|
2
|
+
import {createStore} from 'tinybase/with-schemas';
|
|
3
|
+
{{else}}
|
|
4
|
+
import {createStore} from 'tinybase';
|
|
5
|
+
{{/if}}
|
|
6
|
+
|
|
7
|
+
const STORE_ID = 'settings';
|
|
8
|
+
|
|
9
|
+
{{#if schemas}}
|
|
10
|
+
const VALUES_SCHEMA = {
|
|
11
|
+
username: {type: 'string'},
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
{{/if}}
|
|
15
|
+
export const settingsStore = createStore(){{#if schemas}}
|
|
16
|
+
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
17
|
+
.setValue('username', 'Carol');
|
|
18
|
+
|
|
19
|
+
export type SettingsStore = typeof settingsStore;
|
|
@@ -0,0 +1,30 @@
|
|
|
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';
|
|
5
|
+
|
|
6
|
+
{{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
|
|
7
|
+
import {createInput} from './input';
|
|
8
|
+
|
|
9
|
+
export const createUsernameInput = (settingsStore: SettingsStore, chatStore: ChatStore): HTMLDivElement => {
|
|
10
|
+
const container = document.createElement('div');
|
|
11
|
+
container.id = 'usernameInput';
|
|
12
|
+
|
|
13
|
+
const label = document.createElement('label');
|
|
14
|
+
label.textContent = 'Your name:';
|
|
15
|
+
|
|
16
|
+
const usernameInput = createInput('Enter your name', settingsStore.getValue('username') as string);
|
|
17
|
+
|
|
18
|
+
container.appendChild(label);
|
|
19
|
+
container.appendChild(usernameInput);
|
|
20
|
+
|
|
21
|
+
usernameInput.addEventListener('input', () => {
|
|
22
|
+
settingsStore.setValue('username', usernameInput.value);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
settingsStore.addValuesListener(() => {
|
|
26
|
+
usernameInput.value = settingsStore.getValue('username') as string;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return container;
|
|
30
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {StrictMode} from 'react';
|
|
2
|
+
|
|
3
|
+
{{#if schemas}}
|
|
4
|
+
import {Provider} from 'tinybase/ui-react/with-schemas';
|
|
5
|
+
{{else}}
|
|
6
|
+
import {Provider} from 'tinybase/ui-react';
|
|
7
|
+
{{/if}}
|
|
8
|
+
import {Inspector} from 'tinybase/ui-react-inspector';
|
|
9
|
+
|
|
10
|
+
{{includeFile template="client/src/drawing/SettingsStore.tsx.hbs" output="client/src/SettingsStore.{{ext}}"}}
|
|
11
|
+
import {SettingsStore} from './SettingsStore';
|
|
12
|
+
|
|
13
|
+
{{includeFile template="client/src/drawing/CanvasStore.tsx.hbs" output="client/src/CanvasStore.{{ext}}"}}
|
|
14
|
+
import {CanvasStore} from './CanvasStore';
|
|
15
|
+
|
|
16
|
+
{{includeFile template="client/src/drawing/DrawingControls.tsx.hbs" output="client/src/DrawingControls.{{ext}}"}}
|
|
17
|
+
import {DrawingControls} from './DrawingControls';
|
|
18
|
+
|
|
19
|
+
{{includeFile template="client/src/drawing/Canvas.tsx.hbs" output="client/src/Canvas.{{ext}}"}}
|
|
20
|
+
import {Canvas} from './Canvas';
|
|
21
|
+
|
|
22
|
+
const App = () => {
|
|
23
|
+
return (
|
|
24
|
+
<StrictMode>
|
|
25
|
+
<Provider>
|
|
26
|
+
<SettingsStore />
|
|
27
|
+
<CanvasStore />
|
|
28
|
+
<DrawingControls />
|
|
29
|
+
<Canvas />
|
|
30
|
+
<Inspector />
|
|
31
|
+
</Provider>
|
|
32
|
+
</StrictMode>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export {App};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{{includeFile template="client/src/drawing/brushSize.css.hbs" output="client/src/brushSize.css"}}
|
|
2
|
+
import {useValue, useSetValueCallback, STORE_ID} from './SettingsStore';
|
|
3
|
+
import './brushSize.css';
|
|
4
|
+
|
|
5
|
+
export const BrushSize = () => {
|
|
6
|
+
const size = useValue('brushSize', STORE_ID) ?? 5;
|
|
7
|
+
|
|
8
|
+
const setSize = useSetValueCallback(
|
|
9
|
+
'brushSize',
|
|
10
|
+
(e: React.ChangeEvent<HTMLInputElement>) => parseInt(e.target.value),
|
|
11
|
+
[],
|
|
12
|
+
STORE_ID,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div id="brushSize">
|
|
17
|
+
<label>Size:</label>
|
|
18
|
+
<input type="range" min="1" max="50" value={size} onChange={setSize} />
|
|
19
|
+
<span id="brushSizeValue">{size}</span>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
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 {getUniqueId} from 'tinybase';
|
|
6
|
+
import './canvas.css';
|
|
7
|
+
|
|
8
|
+
export const Canvas = () => {
|
|
9
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
10
|
+
const isDrawing = useRef(false);
|
|
11
|
+
const currentStrokeId = useRef<string | null>(null);
|
|
12
|
+
const pointIndex = useRef(0);
|
|
13
|
+
const brush = useValues(SETTINGS_STORE_ID);
|
|
14
|
+
const strokes = useTable('strokes', CANVAS_STORE_ID);
|
|
15
|
+
const store = useStore(CANVAS_STORE_ID);
|
|
16
|
+
|
|
17
|
+
const draw = useCallback(() => {
|
|
18
|
+
const canvas = canvasRef.current;
|
|
19
|
+
if (!canvas) return;
|
|
20
|
+
|
|
21
|
+
const ctx = canvas.getContext('2d');
|
|
22
|
+
if (!ctx) return;
|
|
23
|
+
|
|
24
|
+
ctx.fillStyle = '#111';
|
|
25
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
26
|
+
|
|
27
|
+
Object.entries(strokes).forEach(([id, stroke]) => {
|
|
28
|
+
if (stroke) {
|
|
29
|
+
const points: Array<{x: number; y: number}> = [];
|
|
30
|
+
let i = 0;
|
|
31
|
+
while (stroke[`x${i}`] !== undefined && stroke[`y${i}`] !== undefined) {
|
|
32
|
+
points.push({x: stroke[`x${i}`] as number, y: stroke[`y${i}`] as number});
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
if (points.length > 0) {
|
|
36
|
+
ctx.strokeStyle = stroke.color as string;
|
|
37
|
+
ctx.lineWidth = (stroke.size as number) * 2;
|
|
38
|
+
ctx.lineCap = 'round';
|
|
39
|
+
ctx.lineJoin = 'round';
|
|
40
|
+
ctx.beginPath();
|
|
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;
|
|
49
|
+
|
|
50
|
+
const rect = canvas.getBoundingClientRect();
|
|
51
|
+
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
52
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
x: clientX - rect.left,
|
|
56
|
+
y: clientY - rect.top,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
|
|
61
|
+
isDrawing.current = true;
|
|
62
|
+
pointIndex.current = 0;
|
|
63
|
+
currentStrokeId.current = getUniqueId();
|
|
64
|
+
|
|
65
|
+
store.setRow('strokes', currentStrokeId.current, {
|
|
66
|
+
color: brush.brushColor,
|
|
67
|
+
size: brush.brushSize,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const point = getPoint(e.nativeEvent);
|
|
71
|
+
if (point) {
|
|
72
|
+
store.setCell('strokes', currentStrokeId.current, 'x0', point.x);
|
|
73
|
+
store.setCell('strokes', currentStrokeId.current, 'y0', point.y);
|
|
74
|
+
pointIndex.current = 1;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleMove = (e: React.MouseEvent | React.TouchEvent) => {
|
|
79
|
+
if (isDrawing.current && currentStrokeId.current) {
|
|
80
|
+
const point = getPoint(e.nativeEvent);
|
|
81
|
+
if (point) {
|
|
82
|
+
const idx = pointIndex.current;
|
|
83
|
+
store.setCell('strokes', currentStrokeId.current, `x${idx}`, point.x);
|
|
84
|
+
store.setCell('strokes', currentStrokeId.current, `y${idx}`, point.y);
|
|
85
|
+
pointIndex.current++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleEnd = () => {
|
|
91
|
+
isDrawing.current = false;
|
|
92
|
+
currentStrokeId.current = null;
|
|
93
|
+
pointIndex.current = 0;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<canvas ref={canvasRef} width={600} height={400} id="drawingCanvas" onMouseDown={handleStart} onMouseMove={handleMove} onMouseUp={handleEnd} onMouseLeave={handleEnd} onTouchStart={handleStart} onTouchMove={handleMove}
|
|
98
|
+
onTouchEnd={handleEnd} />
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
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';
|
|
5
|
+
{{else}}
|
|
6
|
+
import {createMergeableStore} from 'tinybase';
|
|
7
|
+
import {useCreateStore, useProvideStore, useAddRowCallback, useDelTableCallback, useRow, useRowIds, useStore, useTable} from 'tinybase/ui-react';
|
|
8
|
+
{{/if}}
|
|
9
|
+
import {getUniqueId} from 'tinybase';
|
|
10
|
+
|
|
11
|
+
export const STORE_ID = 'canvas';
|
|
12
|
+
|
|
13
|
+
{{#if schemas}}
|
|
14
|
+
const TABLES_SCHEMA = {
|
|
15
|
+
strokes: {
|
|
16
|
+
color: {type: 'string'},
|
|
17
|
+
size: {type: 'number'},
|
|
18
|
+
},
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
type Schemas = [typeof TABLES_SCHEMA, NoValuesSchema];
|
|
22
|
+
|
|
23
|
+
const {useCreateStore, useProvideStore, useAddRowCallback, useDelTableCallback, useRow, useRowIds, useStore, useTable} = UiReact as UiReact.WithSchemas<Schemas>;
|
|
24
|
+
{{/if}}
|
|
25
|
+
|
|
26
|
+
export {useAddRowCallback, useDelTableCallback, useRow, useRowIds, useStore, useTable};
|
|
27
|
+
|
|
28
|
+
export const CanvasStore = () => {
|
|
29
|
+
const store = useCreateStore(() =>
|
|
30
|
+
createMergeableStore(STORE_ID){{#if schemas}}
|
|
31
|
+
.setTablesSchema(TABLES_SCHEMA){{/if}}
|
|
32
|
+
.setDefaultContent([{strokes: {}}, {}]),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
useProvideStore(STORE_ID, store);
|
|
36
|
+
|
|
37
|
+
{{#if sync}}
|
|
38
|
+
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
39
|
+
{{addImport "import {SERVER} from './config';"}}
|
|
40
|
+
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
41
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
42
|
+
{{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
|
|
43
|
+
{{addImport "import type {MergeableStore} from 'tinybase';"}}
|
|
44
|
+
|
|
45
|
+
useCreateSynchronizer(store, async (store: MergeableStore) => {
|
|
46
|
+
const serverPathId = location.pathname;
|
|
47
|
+
const synchronizer = await createWsSynchronizer(
|
|
48
|
+
store,
|
|
49
|
+
new ReconnectingWebSocket(SERVER + serverPathId),
|
|
50
|
+
);
|
|
51
|
+
await synchronizer.startSync();
|
|
52
|
+
|
|
53
|
+
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
54
|
+
synchronizer.load().then(() => synchronizer.save());
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return synchronizer;
|
|
58
|
+
});
|
|
59
|
+
{{/if}}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{{includeFile template="client/src/drawing/colorPicker.css.hbs" output="client/src/colorPicker.css"}}
|
|
2
|
+
import './colorPicker.css';
|
|
3
|
+
import {useValue, useSetValueCallback, STORE_ID} from './SettingsStore';
|
|
4
|
+
|
|
5
|
+
export const ColorPicker = () => {
|
|
6
|
+
const colors = ['#d81b60', '#1976d2', '#388e3c', '#f57c00', '#7b1fa2', '#fff'];
|
|
7
|
+
const currentColor = useValue('brushColor', STORE_ID);
|
|
8
|
+
|
|
9
|
+
const setColor = useSetValueCallback(
|
|
10
|
+
'brushColor',
|
|
11
|
+
(color: string) => color,
|
|
12
|
+
[],
|
|
13
|
+
STORE_ID,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div id="colorPicker">
|
|
18
|
+
{colors.map((color) => (
|
|
19
|
+
<button key={color} className={`colorBtn${currentColor===color ? ' active' : '' }`} style=\{{background: color}} onClick={()=> setColor(color)}
|
|
20
|
+
/>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{{includeFile template="client/src/drawing/drawingControls.css.hbs" output="client/src/drawingControls.css"}}
|
|
2
|
+
import './drawingControls.css';
|
|
3
|
+
import {useDelTableCallback, STORE_ID} from './CanvasStore';
|
|
4
|
+
|
|
5
|
+
{{includeFile template="client/src/shared/Button.tsx.hbs" output="client/src/Button.{{ext}}"}}
|
|
6
|
+
import {Button} from './Button';
|
|
7
|
+
|
|
8
|
+
{{includeFile template="client/src/drawing/ColorPicker.tsx.hbs" output="client/src/ColorPicker.{{ext}}"}}
|
|
9
|
+
import {ColorPicker} from './ColorPicker';
|
|
10
|
+
|
|
11
|
+
{{includeFile template="client/src/drawing/BrushSize.tsx.hbs" output="client/src/BrushSize.{{ext}}"}}
|
|
12
|
+
import {BrushSize} from './BrushSize';
|
|
13
|
+
|
|
14
|
+
export const DrawingControls = () => {
|
|
15
|
+
const clearStrokes = useDelTableCallback('strokes', () => null, [], STORE_ID);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div id="drawingControls">
|
|
19
|
+
<ColorPicker />
|
|
20
|
+
<BrushSize />
|
|
21
|
+
<Button onClick={clearStrokes} variant="primary">Clear</Button>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{{#if schemas}}
|
|
2
|
+
import {createStore} from 'tinybase/with-schemas';
|
|
3
|
+
import * as UiReact from 'tinybase/ui-react/with-schemas';
|
|
4
|
+
import {type NoTablesSchema} from 'tinybase/with-schemas';
|
|
5
|
+
{{else}}
|
|
6
|
+
import {createStore} from 'tinybase';
|
|
7
|
+
import {useCreateStore, useProvideStore, useValue, useSetValueCallback, useValues} from 'tinybase/ui-react';
|
|
8
|
+
{{/if}}
|
|
9
|
+
|
|
10
|
+
export const STORE_ID = 'settings';
|
|
11
|
+
|
|
12
|
+
{{#if schemas}}
|
|
13
|
+
const VALUES_SCHEMA = {
|
|
14
|
+
brushColor: {type: 'string'},
|
|
15
|
+
brushSize: {type: 'number'},
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
type Schemas = [NoTablesSchema, typeof VALUES_SCHEMA];
|
|
19
|
+
|
|
20
|
+
const {useCreateStore, useProvideStore, useValue, useSetValueCallback, useValues} = UiReact as UiReact.WithSchemas<Schemas>;
|
|
21
|
+
{{/if}}
|
|
22
|
+
|
|
23
|
+
export {useValue, useSetValueCallback, useValues};
|
|
24
|
+
|
|
25
|
+
export const SettingsStore = () => {
|
|
26
|
+
const store = useCreateStore(() =>
|
|
27
|
+
createStore(){{#if schemas}}
|
|
28
|
+
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
29
|
+
.setValue('brushColor', '#d81b60')
|
|
30
|
+
.setValue('brushSize', 5),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
useProvideStore(STORE_ID, store);
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{{includeFile template="client/src/drawing/settingsStore.ts.hbs" output="client/src/settingsStore.ts"}}
|
|
2
|
+
import {settingsStore} from './settingsStore';
|
|
3
|
+
|
|
4
|
+
{{includeFile template="client/src/drawing/canvasStore.ts.hbs" output="client/src/canvasStore.ts"}}
|
|
5
|
+
import {canvasStore} from './canvasStore';
|
|
6
|
+
|
|
7
|
+
{{includeFile template="client/src/drawing/drawingControls.ts.hbs" output="client/src/drawingControls.{{ext}}"}}
|
|
8
|
+
import {createDrawingControls} from './drawingControls';
|
|
9
|
+
|
|
10
|
+
{{includeFile template="client/src/drawing/canvas.ts.hbs" output="client/src/canvas.{{ext}}"}}
|
|
11
|
+
import {createCanvas} from './canvas';
|
|
12
|
+
|
|
13
|
+
const app = () => {
|
|
14
|
+
const appContainer = document.getElementById('app')!;
|
|
15
|
+
|
|
16
|
+
appContainer.appendChild(createDrawingControls(settingsStore, canvasStore));
|
|
17
|
+
appContainer.appendChild(createCanvas(settingsStore, canvasStore));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export {app};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#brushSize {
|
|
2
|
+
display: flex;
|
|
3
|
+
gap: 0.5rem;
|
|
4
|
+
align-items: center;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
#brushSize label {
|
|
8
|
+
white-space: nowrap;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#brushSize input {
|
|
12
|
+
width: 100px;
|
|
13
|
+
vertical-align: middle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#brushSizeValue {
|
|
17
|
+
width: 2rem;
|
|
18
|
+
min-width: 2rem;
|
|
19
|
+
text-align: center;
|
|
20
|
+
display: inline-block;
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{{includeFile template="client/src/drawing/brushSize.css.hbs" output="client/src/brushSize.css"}}
|
|
2
|
+
import './brushSize.css';
|
|
3
|
+
import type {Store as SettingsStore} from './settingsStore';
|
|
4
|
+
|
|
5
|
+
export const createBrushSize = (store: SettingsStore): HTMLDivElement => {
|
|
6
|
+
const container = document.createElement('div');
|
|
7
|
+
container.id = 'brushSize';
|
|
8
|
+
|
|
9
|
+
const label = document.createElement('label');
|
|
10
|
+
label.textContent = 'Size:';
|
|
11
|
+
|
|
12
|
+
const sizeSlider = document.createElement('input');
|
|
13
|
+
sizeSlider.type = 'range';
|
|
14
|
+
sizeSlider.min = '1';
|
|
15
|
+
sizeSlider.max = '50';
|
|
16
|
+
sizeSlider.value = '5';
|
|
17
|
+
|
|
18
|
+
const sizeLabel = document.createElement('span');
|
|
19
|
+
sizeLabel.id = 'brushSizeValue';
|
|
20
|
+
sizeLabel.textContent = '5';
|
|
21
|
+
|
|
22
|
+
sizeSlider.addEventListener('input', () => {
|
|
23
|
+
const size = parseInt(sizeSlider.value);
|
|
24
|
+
store.setValue('brushSize', size);
|
|
25
|
+
sizeLabel.textContent = `${size}`;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
container.appendChild(label);
|
|
29
|
+
container.appendChild(sizeSlider);
|
|
30
|
+
container.appendChild(sizeLabel);
|
|
31
|
+
|
|
32
|
+
return container;
|
|
33
|
+
};
|