create-tinybase 0.1.5 → 0.2.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/LICENSE +21 -0
- package/README.md +227 -0
- package/cli.js +2 -2
- package/package.json +6 -3
- package/screenshots/chat.png +0 -0
- package/screenshots/drawing.png +0 -0
- package/screenshots/game.png +0 -0
- package/screenshots/todos.png +0 -0
- package/templates/README.md.hbs +1 -0
- package/templates/{eslint.config.js.hbs → client/eslint.config.js.hbs} +5 -0
- package/templates/client/index.html.hbs +182 -0
- package/templates/client/package.json.hbs +76 -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/package.json.hbs +21 -56
- 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/index.html.hbs +0 -55
- package/templates/src/App.tsx.hbs +0 -52
- package/templates/src/Buttons.tsx.hbs +0 -26
- package/templates/src/app.ts.hbs +0 -36
- package/templates/src/index.css.hbs +0 -138
- package/templates/src/index.tsx.hbs +0 -18
- /package/templates/{.prettierrc.hbs → client/.prettierrc.hbs} +0 -0
- /package/templates/{public → client/public}/favicon.svg +0 -0
- /package/templates/{tsconfig.json.hbs → client/tsconfig.json.hbs} +0 -0
- /package/templates/{vite.config.js.hbs → client/vite.config.js.hbs} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{{#if schemas}}
|
|
2
|
+
import {createMergeableStore} 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 {createMergeableStore} from 'tinybase';
|
|
7
|
+
import {useCreateStore, useProvideStore, useStore, useSetValuesCallback, useTableListener, useCell, useValue} from 'tinybase/ui-react';
|
|
8
|
+
{{/if}}
|
|
9
|
+
|
|
10
|
+
export const STORE_ID = 'game';
|
|
11
|
+
|
|
12
|
+
{{#if schemas}}
|
|
13
|
+
const VALUES_SCHEMA = {
|
|
14
|
+
currentPlayer: {type: 'string'},
|
|
15
|
+
gameStatus: {type: 'string'},
|
|
16
|
+
winner: {type: 'string', default: ''},
|
|
17
|
+
winningLine: {type: 'string', default: ''},
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
type Schemas = [NoTablesSchema, typeof VALUES_SCHEMA];
|
|
21
|
+
|
|
22
|
+
const {useCreateStore, useProvideStore, useStore, useSetValuesCallback, useTableListener, useCell, useValue} = UiReact as UiReact.WithSchemas<Schemas>;
|
|
23
|
+
{{/if}}
|
|
24
|
+
|
|
25
|
+
export {useStore, useSetValuesCallback, useTableListener, useCell, useValue};
|
|
26
|
+
|
|
27
|
+
export const Store = () => {
|
|
28
|
+
const store = useCreateStore(() =>
|
|
29
|
+
createMergeableStore(STORE_ID){{#if schemas}}
|
|
30
|
+
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
31
|
+
.setDefaultContent([
|
|
32
|
+
{},
|
|
33
|
+
{
|
|
34
|
+
currentPlayer: 'X',
|
|
35
|
+
gameStatus: 'playing',
|
|
36
|
+
},
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
useProvideStore(STORE_ID, store);
|
|
41
|
+
|
|
42
|
+
{{#if sync}}
|
|
43
|
+
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
44
|
+
{{addImport "import {SERVER} from './config';"}}
|
|
45
|
+
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
46
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
47
|
+
{{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
|
|
48
|
+
{{addImport "import type {MergeableStore} from 'tinybase';"}}
|
|
49
|
+
|
|
50
|
+
useCreateSynchronizer(store, async (store: MergeableStore) => {
|
|
51
|
+
const serverPathId = location.pathname;
|
|
52
|
+
const synchronizer = await createWsSynchronizer(
|
|
53
|
+
store,
|
|
54
|
+
new ReconnectingWebSocket(SERVER + serverPathId),
|
|
55
|
+
);
|
|
56
|
+
await synchronizer.startSync();
|
|
57
|
+
|
|
58
|
+
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
59
|
+
synchronizer.load().then(() => synchronizer.save());
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return synchronizer;
|
|
63
|
+
});
|
|
64
|
+
{{/if}}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{{includeFile template="client/src/game/store.ts.hbs" output="client/src/store.ts"}}
|
|
2
|
+
import {store} from './store';
|
|
3
|
+
|
|
4
|
+
{{includeFile template="client/src/game/game.ts.hbs" output="client/src/game.{{ext}}"}}
|
|
5
|
+
import {createGame} from './game';
|
|
6
|
+
|
|
7
|
+
const app = () => {
|
|
8
|
+
const appContainer = document.getElementById('app')!;
|
|
9
|
+
appContainer.appendChild(createGame(store));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export {app};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#board {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: repeat(3, 100px);
|
|
4
|
+
grid-template-rows: repeat(3, 100px);
|
|
5
|
+
gap: 1px;
|
|
6
|
+
background: var(--border);
|
|
7
|
+
border: 2px solid var(--border);
|
|
8
|
+
border-radius: 0.5rem;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
margin: 0 auto 2rem;
|
|
11
|
+
width: fit-content;
|
|
12
|
+
padding: 1px;
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{{includeFile template="client/src/game/board.css.hbs" output="client/src/board.css"}}
|
|
2
|
+
import './board.css';
|
|
3
|
+
import type {GameStore} from './store';
|
|
4
|
+
|
|
5
|
+
{{includeFile template="client/src/game/square.ts.hbs" output="client/src/square.{{ext}}"}}
|
|
6
|
+
import {createSquare} from './square';
|
|
7
|
+
|
|
8
|
+
export const createBoard = (store: GameStore): HTMLDivElement => {
|
|
9
|
+
const board = document.createElement('div');
|
|
10
|
+
board.id = 'board';
|
|
11
|
+
|
|
12
|
+
const render = () => {
|
|
13
|
+
const gameStatus = store.getValue('gameStatus');
|
|
14
|
+
const winningLine = store.getValue('winningLine');
|
|
15
|
+
const currentPlayer = store.getValue('currentPlayer');
|
|
16
|
+
const winningPositions = new Set(
|
|
17
|
+
winningLine ? winningLine.split(',').map(Number) : []
|
|
18
|
+
);
|
|
19
|
+
const disabled = gameStatus !== 'playing';
|
|
20
|
+
|
|
21
|
+
board.innerHTML = '';
|
|
22
|
+
for (let i = 0; i < 9; i++) { const cell=store.getCell('board', i.toString(), 'value' ); const square=createSquare( i, cell, ()=> {
|
|
23
|
+
if (gameStatus === 'playing' && !cell) {
|
|
24
|
+
store.setCell('board', i.toString(), 'value', currentPlayer);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
disabled || !!cell,
|
|
28
|
+
winningPositions.has(i)
|
|
29
|
+
);
|
|
30
|
+
board.appendChild(square);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
store.addValuesListener(render);
|
|
35
|
+
store.addTableListener('board', render);
|
|
36
|
+
render();
|
|
37
|
+
|
|
38
|
+
return board;
|
|
39
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type {GameStore} from './store';
|
|
2
|
+
|
|
3
|
+
{{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
|
|
4
|
+
import {createButton} from './button';
|
|
5
|
+
|
|
6
|
+
{{includeFile template="client/src/game/board.ts.hbs" output="client/src/board.{{ext}}"}}
|
|
7
|
+
import {createBoard} from './board';
|
|
8
|
+
|
|
9
|
+
{{includeFile template="client/src/game/gameStatus.ts.hbs" output="client/src/gameStatus.{{ext}}"}}
|
|
10
|
+
import {createGameStatus} from './gameStatus';
|
|
11
|
+
|
|
12
|
+
const WINNING_LINES = [
|
|
13
|
+
[0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
|
|
14
|
+
[0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
|
|
15
|
+
[0, 4, 8], [2, 4, 6], // diagonals
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const createGame = (store: GameStore): HTMLDivElement => {
|
|
19
|
+
const checkGameState = () => {
|
|
20
|
+
const gameStatus = store.getValue('gameStatus');
|
|
21
|
+
if (gameStatus !== 'playing') return;
|
|
22
|
+
|
|
23
|
+
const board = store.getTable('board');
|
|
24
|
+
|
|
25
|
+
// Check for winner
|
|
26
|
+
for (const line of WINNING_LINES) {
|
|
27
|
+
const [a, b, c] = line;
|
|
28
|
+
const cellA = board[a]?.value;
|
|
29
|
+
const cellB = board[b]?.value;
|
|
30
|
+
const cellC = board[c]?.value;
|
|
31
|
+
|
|
32
|
+
if (cellA && cellA === cellB && cellA === cellC) {
|
|
33
|
+
store.setValue('gameStatus', 'won');
|
|
34
|
+
store.setValue('winner', cellA);
|
|
35
|
+
store.setValue('winningLine', line.join(','));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for draw
|
|
41
|
+
const filledCells = Object.values(board).filter((cell: any) => cell.value).length;
|
|
42
|
+
if (filledCells === 9) {
|
|
43
|
+
store.setValue('gameStatus', 'draw');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Switch player after each move
|
|
48
|
+
const currentPlayer = store.getValue('currentPlayer');
|
|
49
|
+
store.setValue('currentPlayer', currentPlayer === 'X' ? 'O' : 'X');
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
store.addTableListener('board', checkGameState, true);
|
|
53
|
+
|
|
54
|
+
const resetGame = () => {
|
|
55
|
+
store.delTable('board');
|
|
56
|
+
store.setValues({
|
|
57
|
+
currentPlayer: 'X',
|
|
58
|
+
gameStatus: 'playing',
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const gameContainer = document.createElement('div');
|
|
63
|
+
|
|
64
|
+
gameContainer.appendChild(createGameStatus(store));
|
|
65
|
+
gameContainer.appendChild(createBoard(store));
|
|
66
|
+
|
|
67
|
+
const buttonContainer = document.createElement('div');
|
|
68
|
+
buttonContainer.style.textAlign = 'center';
|
|
69
|
+
buttonContainer.style.marginTop = '2rem';
|
|
70
|
+
buttonContainer.appendChild(createButton('New Game', resetGame, 'primary'));
|
|
71
|
+
gameContainer.appendChild(buttonContainer);
|
|
72
|
+
|
|
73
|
+
return gameContainer;
|
|
74
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#gameStatus {
|
|
2
|
+
text-align: center;
|
|
3
|
+
font-size: 1.5rem;
|
|
4
|
+
font-weight: 800;
|
|
5
|
+
margin: 2rem 0 1rem;
|
|
6
|
+
color: var(--accent);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#gameStatus .winner {
|
|
10
|
+
color: var(--accent);
|
|
11
|
+
animation: pulse 1s ease-in-out infinite;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@keyframes pulse {
|
|
15
|
+
0%, 100% {
|
|
16
|
+
opacity: 1;
|
|
17
|
+
}
|
|
18
|
+
50% {
|
|
19
|
+
opacity: 0.7;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{includeFile template="client/src/game/gameStatus.css.hbs" output="client/src/gameStatus.css"}}
|
|
2
|
+
import './gameStatus.css';
|
|
3
|
+
import type {GameStore} from './store';
|
|
4
|
+
|
|
5
|
+
export const createGameStatus = (store: GameStore): HTMLDivElement => {
|
|
6
|
+
const status = document.createElement('div');
|
|
7
|
+
status.id = 'gameStatus';
|
|
8
|
+
|
|
9
|
+
const render = () => {
|
|
10
|
+
const gameStatus = store.getValue('gameStatus');
|
|
11
|
+
const currentPlayer = store.getValue('currentPlayer');
|
|
12
|
+
const winner = store.getValue('winner');
|
|
13
|
+
|
|
14
|
+
if (gameStatus === 'playing') {
|
|
15
|
+
status.innerHTML = `Player <span class="player">${currentPlayer}</span>'s turn`;
|
|
16
|
+
} else if (gameStatus === 'won') {
|
|
17
|
+
status.innerHTML = `Player <span class="winner">${winner}</span> wins!`;
|
|
18
|
+
} else if (gameStatus === 'draw') {
|
|
19
|
+
status.textContent = "It's a draw!";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
store.addValuesListener(render);
|
|
24
|
+
render();
|
|
25
|
+
|
|
26
|
+
return status;
|
|
27
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.square {
|
|
2
|
+
width: 100px;
|
|
3
|
+
height: 100px;
|
|
4
|
+
background: var(--bg2);
|
|
5
|
+
border: none;
|
|
6
|
+
font-size: 2.5rem;
|
|
7
|
+
font-weight: 800;
|
|
8
|
+
color: var(--fg);
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
transition: all 0.15s ease;
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
padding: 0;
|
|
15
|
+
margin: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.square:hover:not(.square.disabled) {
|
|
19
|
+
background: var(--bg);
|
|
20
|
+
outline: 2px solid var(--accent);
|
|
21
|
+
outline-offset: -2px;
|
|
22
|
+
z-index: 1;
|
|
23
|
+
position: relative;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.square:disabled {
|
|
27
|
+
cursor: not-allowed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.square.disabled {
|
|
31
|
+
cursor: not-allowed;
|
|
32
|
+
opacity: 0.7;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.square.winning {
|
|
36
|
+
background: var(--accent);
|
|
37
|
+
color: var(--bg);
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{{includeFile template="client/src/game/square.css.hbs" output="client/src/square.css"}}
|
|
2
|
+
import './square.css';
|
|
3
|
+
|
|
4
|
+
export const createSquare = (position: number, value: string | undefined, onClick: () => void, disabled: boolean, winning: boolean): HTMLButtonElement => {
|
|
5
|
+
const square = document.createElement('button');
|
|
6
|
+
square.className = `square${disabled ? ' disabled' : ''}${winning ? ' winning' : ''}`;
|
|
7
|
+
square.textContent = value || '';
|
|
8
|
+
square.disabled = disabled;
|
|
9
|
+
square.addEventListener('click', onClick);
|
|
10
|
+
return square;
|
|
11
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{{#if schemas}}
|
|
2
|
+
import {createMergeableStore} from 'tinybase/with-schemas';
|
|
3
|
+
{{else}}
|
|
4
|
+
import {createMergeableStore} from 'tinybase';
|
|
5
|
+
{{/if}}
|
|
6
|
+
|
|
7
|
+
const STORE_ID = 'game';
|
|
8
|
+
|
|
9
|
+
{{#if schemas}}
|
|
10
|
+
const VALUES_SCHEMA = {
|
|
11
|
+
currentPlayer: {type: 'string'},
|
|
12
|
+
gameStatus: {type: 'string'},
|
|
13
|
+
winner: {type: 'string', default: ''},
|
|
14
|
+
winningLine: {type: 'string', default: ''},
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
{{/if}}
|
|
18
|
+
export const store = createMergeableStore(STORE_ID){{#if schemas}}
|
|
19
|
+
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
20
|
+
.setDefaultContent([
|
|
21
|
+
{},
|
|
22
|
+
{
|
|
23
|
+
currentPlayer: 'X',
|
|
24
|
+
gameStatus: 'playing',
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
{{#if sync}}
|
|
29
|
+
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
30
|
+
{{addImport "import {SERVER} from './config';"}}
|
|
31
|
+
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
32
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
33
|
+
|
|
34
|
+
const serverPathId = location.pathname;
|
|
35
|
+
createWsSynchronizer(
|
|
36
|
+
store,
|
|
37
|
+
new ReconnectingWebSocket(SERVER + serverPathId),
|
|
38
|
+
).then(async (synchronizer) => {
|
|
39
|
+
await synchronizer.startSync();
|
|
40
|
+
|
|
41
|
+
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
42
|
+
synchronizer.load().then(() => synchronizer.save());
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
{{/if}}
|
|
46
|
+
|
|
47
|
+
export type GameStore = typeof store;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{{#if sync}}
|
|
2
|
+
import {getUniqueId} from 'tinybase';
|
|
3
|
+
|
|
4
|
+
if (location.pathname === '/') {
|
|
5
|
+
location.assign('/' + getUniqueId());
|
|
6
|
+
}
|
|
7
|
+
{{/if}}
|
|
8
|
+
{{#if react}}
|
|
9
|
+
import ReactDOM from 'react-dom/client';
|
|
10
|
+
import {App} from './App';
|
|
11
|
+
{{includeFile template="client/src/{{appType}}/App.tsx.hbs" output="client/src/App.{{ext}}"}}
|
|
12
|
+
|
|
13
|
+
addEventListener('load', () => {
|
|
14
|
+
ReactDOM.createRoot(document.getElementById('app')!).render(
|
|
15
|
+
<App />);
|
|
16
|
+
});
|
|
17
|
+
{{else}}
|
|
18
|
+
import {app} from './app';
|
|
19
|
+
{{includeFile template="client/src/{{appType}}/app.ts.hbs" output="client/src/app.{{ext}}"}}
|
|
20
|
+
|
|
21
|
+
addEventListener('load', () => {
|
|
22
|
+
app();
|
|
23
|
+
});
|
|
24
|
+
{{/if}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{includeFile template="client/src/shared/button.css.hbs" output="client/src/button.css"}}
|
|
2
|
+
import './button.css';
|
|
3
|
+
import {ReactNode} from 'react';
|
|
4
|
+
|
|
5
|
+
interface ButtonProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
variant?: 'default' | 'primary';
|
|
9
|
+
type?: 'button' | 'submit';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Button = ({children, onClick, variant = 'default', type = 'button'}: ButtonProps) => (
|
|
13
|
+
<button onClick={onClick} type={type} className={variant==='primary' ? 'primary' : undefined}>
|
|
14
|
+
{children}
|
|
15
|
+
</button>
|
|
16
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{includeFile template="client/src/shared/input.css.hbs" output="client/src/input.css"}}
|
|
2
|
+
import './input.css';
|
|
3
|
+
|
|
4
|
+
interface InputProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
autoFocus?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Input = ({value, onChange, placeholder = '', autoFocus = false}: InputProps) => (
|
|
12
|
+
<input type="text" value={value} onChange={(e)=> onChange(e.target.value)}
|
|
13
|
+
placeholder={placeholder}
|
|
14
|
+
autoFocus={autoFocus}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
button {
|
|
2
|
+
padding: 0.5rem 1rem;
|
|
3
|
+
border: 1px solid #666;
|
|
4
|
+
border-radius: 0.25rem;
|
|
5
|
+
background: #222;
|
|
6
|
+
color: #fff;
|
|
7
|
+
font-family: inherit;
|
|
8
|
+
font-weight: 800;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
margin: 0.5rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
button:hover {
|
|
14
|
+
border-color: #d81b60;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
button.primary {
|
|
18
|
+
background: #d81b60;
|
|
19
|
+
border: none;
|
|
20
|
+
padding: 0.75rem 1.5rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
button.primary:hover {
|
|
24
|
+
background: #c2185b;
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{includeFile template="client/src/shared/button.css.hbs" output="client/src/button.css"}}
|
|
2
|
+
import './button.css';
|
|
3
|
+
|
|
4
|
+
export const createButton = (
|
|
5
|
+
text: string,
|
|
6
|
+
onClick: () => void,
|
|
7
|
+
variant: 'default' | 'primary' = 'default',
|
|
8
|
+
): HTMLButtonElement => {
|
|
9
|
+
const button = document.createElement('button');
|
|
10
|
+
button.textContent = text;
|
|
11
|
+
if (variant === 'primary') {
|
|
12
|
+
button.className = 'primary';
|
|
13
|
+
}
|
|
14
|
+
button.addEventListener('click', onClick);
|
|
15
|
+
return button;
|
|
16
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
input[type="text"] {
|
|
2
|
+
padding: 0.5rem 0.75rem;
|
|
3
|
+
background: var(--bg);
|
|
4
|
+
border: 1px solid var(--border);
|
|
5
|
+
border-radius: 0.375rem;
|
|
6
|
+
color: var(--fg);
|
|
7
|
+
font-family: inherit;
|
|
8
|
+
font-size: 1rem;
|
|
9
|
+
line-height: 1.5;
|
|
10
|
+
width: 100%;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
align-self: center;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
input[type="text"]:focus {
|
|
16
|
+
outline: none;
|
|
17
|
+
border-color: var(--accent);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
input[type="text"]::placeholder {
|
|
21
|
+
color: var(--fg2);
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{{includeFile template="client/src/shared/input.css.hbs" output="client/src/input.css"}}
|
|
2
|
+
import './input.css';
|
|
3
|
+
|
|
4
|
+
export const createInput = (
|
|
5
|
+
placeholder: string = '',
|
|
6
|
+
value: string = '',
|
|
7
|
+
onInput?: (value: string) => void,
|
|
8
|
+
): HTMLInputElement => {
|
|
9
|
+
const input = document.createElement('input');
|
|
10
|
+
input.type = 'text';
|
|
11
|
+
input.placeholder = placeholder;
|
|
12
|
+
input.value = value;
|
|
13
|
+
if (onInput) {
|
|
14
|
+
input.addEventListener('input', () => onInput(input.value));
|
|
15
|
+
}
|
|
16
|
+
return input;
|
|
17
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
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/todos/Store.tsx.hbs" output="client/src/Store.{{ext}}"}}
|
|
11
|
+
import {Store} from './Store';
|
|
12
|
+
|
|
13
|
+
{{includeFile template="client/src/todos/TodoInput.tsx.hbs" output="client/src/TodoInput.{{ext}}"}}
|
|
14
|
+
import {TodoInput} from './TodoInput';
|
|
15
|
+
|
|
16
|
+
{{includeFile template="client/src/todos/TodoList.tsx.hbs" output="client/src/TodoList.{{ext}}"}}
|
|
17
|
+
import {TodoList} from './TodoList';
|
|
18
|
+
|
|
19
|
+
const App = () => {
|
|
20
|
+
return (
|
|
21
|
+
<StrictMode>
|
|
22
|
+
<Provider>
|
|
23
|
+
<Store />
|
|
24
|
+
<TodoInput />
|
|
25
|
+
<TodoList />
|
|
26
|
+
<Inspector />
|
|
27
|
+
</Provider>
|
|
28
|
+
</StrictMode>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export {App};
|
|
@@ -0,0 +1,70 @@
|
|
|
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, useDelRowCallback, useRow, useRowIds, useSetPartialRowCallback} from 'tinybase/ui-react';
|
|
8
|
+
{{/if}}
|
|
9
|
+
|
|
10
|
+
export const STORE_ID = 'todos';
|
|
11
|
+
|
|
12
|
+
{{#if schemas}}
|
|
13
|
+
const TODOS_SCHEMA = {
|
|
14
|
+
todos: {
|
|
15
|
+
text: {type: 'string'},
|
|
16
|
+
completed: {type: 'boolean'},
|
|
17
|
+
},
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
type Schemas = [typeof TODOS_SCHEMA, NoValuesSchema];
|
|
21
|
+
|
|
22
|
+
const {useCreateStore, useProvideStore, useAddRowCallback, useDelRowCallback, useRow, useRowIds, useSetPartialRowCallback} = UiReact as UiReact.WithSchemas<Schemas>;
|
|
23
|
+
{{/if}}
|
|
24
|
+
|
|
25
|
+
export {useAddRowCallback, useDelRowCallback, useRow, useRowIds, useSetPartialRowCallback};
|
|
26
|
+
|
|
27
|
+
export const Store = () => {
|
|
28
|
+
const store = useCreateStore(() =>
|
|
29
|
+
createMergeableStore(STORE_ID){{#if schemas}}
|
|
30
|
+
.setTablesSchema(TODOS_SCHEMA){{/if}}
|
|
31
|
+
.setDefaultContent([
|
|
32
|
+
{
|
|
33
|
+
todos: {
|
|
34
|
+
'1': {text: 'Learn TinyBase', completed: false},
|
|
35
|
+
'2': {text: 'Build an app', completed: false},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{},
|
|
39
|
+
]),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
useProvideStore(STORE_ID, store);
|
|
43
|
+
|
|
44
|
+
{{#if sync}}
|
|
45
|
+
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
46
|
+
{{addImport "import {SERVER} from './config';"}}
|
|
47
|
+
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
48
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
49
|
+
{{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
|
|
50
|
+
{{addImport "import type {MergeableStore} from 'tinybase';"}}
|
|
51
|
+
|
|
52
|
+
useCreateSynchronizer(store, async (store: MergeableStore) => {
|
|
53
|
+
const serverPathId = location.pathname;
|
|
54
|
+
const synchronizer = await createWsSynchronizer(
|
|
55
|
+
store,
|
|
56
|
+
new ReconnectingWebSocket(SERVER + serverPathId),
|
|
57
|
+
);
|
|
58
|
+
await synchronizer.startSync();
|
|
59
|
+
|
|
60
|
+
// If the websocket reconnects in the future, do another explicit sync.
|
|
61
|
+
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
62
|
+
synchronizer.load().then(() => synchronizer.save());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return synchronizer;
|
|
66
|
+
});
|
|
67
|
+
{{/if}}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{includeFile template="client/src/todos/todoInput.css.hbs" output="client/src/todoInput.css"}}
|
|
2
|
+
import './todoInput.css';
|
|
3
|
+
import {useState} from 'react';
|
|
4
|
+
import {useAddRowCallback, STORE_ID} from './Store';
|
|
5
|
+
|
|
6
|
+
{{includeFile template="client/src/shared/Button.tsx.hbs" output="client/src/Button.{{ext}}"}}
|
|
7
|
+
import {Button} from './Button';
|
|
8
|
+
|
|
9
|
+
{{includeFile template="client/src/shared/Input.tsx.hbs" output="client/src/Input.{{ext}}"}}
|
|
10
|
+
import {Input} from './Input';
|
|
11
|
+
|
|
12
|
+
export const TodoInput = () => {
|
|
13
|
+
const [text, setText] = useState('');
|
|
14
|
+
const addRow = useAddRowCallback('todos', () => ({text, completed: false}), [text], STORE_ID);
|
|
15
|
+
|
|
16
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
if (text.trim()) {
|
|
19
|
+
addRow();
|
|
20
|
+
setText('');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<form id="todoInput" onSubmit={handleSubmit}>
|
|
26
|
+
<Input value={text} onChange={setText} placeholder="What needs to be done?" autoFocus />
|
|
27
|
+
<Button type="submit" variant="primary">Add</Button>
|
|
28
|
+
</form>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{{includeFile template="client/src/todos/todoItem.css.hbs" output="client/src/todoItem.css"}}
|
|
2
|
+
import './todoItem.css';
|
|
3
|
+
import {useDelRowCallback, useRow, useSetPartialRowCallback, STORE_ID} from './Store';
|
|
4
|
+
|
|
5
|
+
{{includeFile template="client/src/shared/Button.tsx.hbs" output="client/src/Button.{{ext}}"}}
|
|
6
|
+
import {Button} from './Button';
|
|
7
|
+
|
|
8
|
+
export const TodoItem = ({rowId}: {rowId: string}) => {
|
|
9
|
+
const todo = useRow('todos', rowId, STORE_ID);
|
|
10
|
+
const setPartialRow = useSetPartialRowCallback('todos', rowId, (e: React.ChangeEvent<HTMLInputElement>) => ({completed: e.target.checked}), [], STORE_ID);
|
|
11
|
+
const delRow = useDelRowCallback('todos', rowId, STORE_ID);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={`todoItem${todo.completed ? ' completed' : '' }`}>
|
|
15
|
+
<input type="checkbox" checked={todo.completed} onChange={setPartialRow} id={`todo-${rowId}`} />
|
|
16
|
+
<label htmlFor={`todo-${rowId}`}>{todo.text}</label>
|
|
17
|
+
<Button onClick={delRow}>Delete</Button>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
};
|