create-tinybase 0.2.4 → 0.3.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/README.md +62 -18
- package/cli.js +2 -2
- package/package.json +3 -3
- package/screenshots/chat.png +0 -0
- package/screenshots/todos.png +0 -0
- package/templates/README.md.hbs +13 -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-do.ts.hbs +10 -14
- package/templates/server/index-node.ts.hbs +3 -3
- package/templates/server/package.json.hbs +5 -5
- package/templates/server/wrangler.toml.hbs +1 -1
- /package/templates/client/{.prettierrc.hbs → .prettierrc.json.hbs} +0 -0
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
{{includeFile template="client/src/game/square.css.hbs" output="client/src/square.css"}}
|
|
2
|
-
import './square.css';
|
|
3
|
-
import {useCell,
|
|
2
|
+
{{addImport "import './square.css';"}}
|
|
3
|
+
{{addImport "import {useCell, useSetCellCallback, useValue, STORE_ID} from './Store';"}}
|
|
4
4
|
|
|
5
|
-
export const Square = ({position, disabled, winning}: {position:
|
|
6
|
-
const
|
|
7
|
-
const value = useCell('board', position.toString(), 'value', STORE_ID);
|
|
5
|
+
export const Square = ({position, disabled, winning}: {position: string; disabled: boolean; winning: boolean}) => {
|
|
6
|
+
const value = useCell('board', position, 'value', STORE_ID);
|
|
8
7
|
const currentPlayer = useValue('currentPlayer', STORE_ID);
|
|
9
8
|
|
|
10
9
|
const isDisabled = disabled || !!value;
|
|
11
10
|
|
|
12
|
-
const handleClick = () =>
|
|
13
|
-
if (!isDisabled) {
|
|
14
|
-
store.setCell('board', position.toString(), 'value', currentPlayer);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
11
|
+
const handleClick = useSetCellCallback('board', position, 'value', () => currentPlayer || 'X', [currentPlayer], STORE_ID);
|
|
17
12
|
|
|
18
13
|
return (
|
|
19
14
|
<button className={`square${isDisabled ? ' disabled' : '' }${winning ? ' winning' : '' }`} onClick={handleClick} disabled={isDisabled}>
|
|
20
15
|
{value || ''}
|
|
21
16
|
</button>
|
|
22
17
|
);
|
|
23
|
-
};
|
|
18
|
+
};
|
|
@@ -1,15 +1,20 @@
|
|
|
1
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';
|
|
2
|
+
{{addImport "import {createMergeableStore} from 'tinybase/with-schemas';"}}
|
|
3
|
+
{{addImport "import * as UiReact from 'tinybase/ui-react/with-schemas';"}}
|
|
5
4
|
{{else}}
|
|
6
|
-
import {createMergeableStore} from 'tinybase';
|
|
7
|
-
import {
|
|
5
|
+
{{addImport "import {createMergeableStore} from 'tinybase';"}}
|
|
6
|
+
{{addImport "import {useCreateMergeableStore, useProvideStore, useStore, useSetValuesCallback, useSetCellCallback, useTableListener, useCell, useValue} from 'tinybase/ui-react';"}}
|
|
8
7
|
{{/if}}
|
|
9
8
|
|
|
10
9
|
export const STORE_ID = 'game';
|
|
11
10
|
|
|
12
11
|
{{#if schemas}}
|
|
12
|
+
const TABLES_SCHEMA = {
|
|
13
|
+
board: {
|
|
14
|
+
value: {type: 'string', default: ''},
|
|
15
|
+
},
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
13
18
|
const VALUES_SCHEMA = {
|
|
14
19
|
currentPlayer: {type: 'string'},
|
|
15
20
|
gameStatus: {type: 'string'},
|
|
@@ -17,16 +22,17 @@ export const STORE_ID = 'game';
|
|
|
17
22
|
winningLine: {type: 'string', default: ''},
|
|
18
23
|
} as const;
|
|
19
24
|
|
|
20
|
-
type Schemas = [
|
|
25
|
+
type Schemas = [typeof TABLES_SCHEMA, typeof VALUES_SCHEMA];
|
|
21
26
|
|
|
22
|
-
const {
|
|
27
|
+
const {useCell, useCreateMergeableStore, {{#if persist}}useCreatePersister, {{/if}}useProvideStore, useSetCellCallback, useSetValuesCallback, useStore, useTableListener, useValue} = UiReact as UiReact.WithSchemas<Schemas>;
|
|
23
28
|
{{/if}}
|
|
24
29
|
|
|
25
|
-
export {useStore, useSetValuesCallback, useTableListener, useCell, useValue};
|
|
30
|
+
export {useStore, useSetValuesCallback, useSetCellCallback, useTableListener, useCell, useValue};
|
|
26
31
|
|
|
27
|
-
export const Store = () => {
|
|
28
|
-
const store =
|
|
29
|
-
createMergeableStore(
|
|
32
|
+
export const Store = ({onReady}: {onReady?: () => void}) => {
|
|
33
|
+
const store = useCreateMergeableStore(() =>
|
|
34
|
+
createMergeableStore(){{#if schemas}}
|
|
35
|
+
.setTablesSchema(TABLES_SCHEMA)
|
|
30
36
|
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
31
37
|
.setDefaultContent([
|
|
32
38
|
{},
|
|
@@ -39,15 +45,92 @@ gameStatus: 'playing',
|
|
|
39
45
|
|
|
40
46
|
useProvideStore(STORE_ID, store);
|
|
41
47
|
|
|
48
|
+
{{#if persist}}
|
|
49
|
+
{{#if persistLocalStorage}}
|
|
50
|
+
{{#if schemas}}
|
|
51
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
|
|
52
|
+
{{else}}
|
|
53
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
|
|
54
|
+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
|
|
55
|
+
{{/if}}
|
|
56
|
+
|
|
57
|
+
useCreatePersister(
|
|
58
|
+
store,
|
|
59
|
+
(store) => createLocalPersister(store, STORE_ID),
|
|
60
|
+
[],
|
|
61
|
+
async (persister) => {
|
|
62
|
+
await persister.load();
|
|
63
|
+
await persister.startAutoSave();
|
|
64
|
+
onReady?.();
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
{{/if}}
|
|
68
|
+
{{#if persistSqlite}}
|
|
69
|
+
{{#if schemas}}
|
|
70
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
|
|
71
|
+
{{else}}
|
|
72
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
|
|
73
|
+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
|
|
74
|
+
{{/if}}
|
|
75
|
+
{{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.{{ext}}"}}
|
|
76
|
+
{{addImport "import {getDb} from './sqlite';"}}
|
|
77
|
+
|
|
78
|
+
useCreatePersister(
|
|
79
|
+
store,
|
|
80
|
+
async (store) => {
|
|
81
|
+
const {sqlite3, db} = await getDb();
|
|
82
|
+
return createSqliteWasmPersister(store, sqlite3, db, STORE_ID);
|
|
83
|
+
},
|
|
84
|
+
[],
|
|
85
|
+
async (persister) => {
|
|
86
|
+
await persister.load();
|
|
87
|
+
await persister.startAutoSave();
|
|
88
|
+
onReady?.();
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
{{/if}}
|
|
92
|
+
{{#if persistPglite}}
|
|
93
|
+
{{#if schemas}}
|
|
94
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
|
|
95
|
+
{{else}}
|
|
96
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
|
|
97
|
+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
|
|
98
|
+
{{/if}}
|
|
99
|
+
{{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
|
|
100
|
+
{{addImport "import {getPgLite} from './pglite';"}}
|
|
101
|
+
|
|
102
|
+
useCreatePersister(
|
|
103
|
+
store,
|
|
104
|
+
async (store) => {
|
|
105
|
+
const pgLite = await getPgLite();
|
|
106
|
+
return await createPglitePersister(store, pgLite, 'game');
|
|
107
|
+
},
|
|
108
|
+
[],
|
|
109
|
+
async (persister) => {
|
|
110
|
+
await persister.load();
|
|
111
|
+
await persister.startAutoSave();
|
|
112
|
+
onReady?.();
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
{{/if}}
|
|
116
|
+
{{/if}}
|
|
117
|
+
|
|
42
118
|
{{#if sync}}
|
|
43
119
|
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
44
120
|
{{addImport "import {SERVER} from './config';"}}
|
|
45
121
|
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
46
|
-
{{
|
|
47
|
-
|
|
48
|
-
{{
|
|
122
|
+
{{#if schemas}}
|
|
123
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client/with-schemas';"}}
|
|
124
|
+
{{else}}
|
|
125
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
126
|
+
{{/if}}
|
|
127
|
+
{{#if schemas}}
|
|
128
|
+
{{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react/with-schemas';"}}
|
|
129
|
+
{{else}}
|
|
130
|
+
{{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
|
|
131
|
+
{{/if}}
|
|
49
132
|
|
|
50
|
-
useCreateSynchronizer(store, async (store
|
|
133
|
+
useCreateSynchronizer(store, async (store) => {
|
|
51
134
|
const serverPathId = location.pathname;
|
|
52
135
|
const synchronizer = await createWsSynchronizer(
|
|
53
136
|
store,
|
|
@@ -56,12 +139,19 @@ useProvideStore(STORE_ID, store);
|
|
|
56
139
|
await synchronizer.startSync();
|
|
57
140
|
|
|
58
141
|
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
59
|
-
synchronizer.load().then(() =>
|
|
142
|
+
synchronizer.load().then(() => {
|
|
143
|
+
synchronizer.save();
|
|
144
|
+
{{#unless persist}}onReady?.();{{/unless}}
|
|
145
|
+
});
|
|
60
146
|
});
|
|
61
147
|
|
|
62
148
|
return synchronizer;
|
|
63
149
|
});
|
|
64
150
|
{{/if}}
|
|
65
151
|
|
|
152
|
+
{{#unless persist}}{{#unless sync}}
|
|
153
|
+
onReady?.();
|
|
154
|
+
{{/unless}}{{/unless}}
|
|
155
|
+
|
|
66
156
|
return null;
|
|
67
157
|
};
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
{{includeFile template="client/src/game/store.ts.hbs" output="client/src/store.ts"}}
|
|
2
|
-
import {store} from './store';
|
|
2
|
+
{{addImport "import {store, storeReady} from './store';"}}
|
|
3
|
+
|
|
4
|
+
{{includeFile template="client/src/shared/loading.ts.hbs" output="client/src/loading.{{ext}}"}}
|
|
5
|
+
{{addImport "import {showLoading, hideLoading} from './loading';"}}
|
|
3
6
|
|
|
4
7
|
{{includeFile template="client/src/game/game.ts.hbs" output="client/src/game.{{ext}}"}}
|
|
5
|
-
import {createGame} from './game';
|
|
8
|
+
{{addImport "import {createGame} from './game';"}}
|
|
6
9
|
|
|
7
|
-
const app = () => {
|
|
10
|
+
export const app = async () => {
|
|
8
11
|
const appContainer = document.getElementById('app')!;
|
|
9
|
-
appContainer.appendChild(createGame(store));
|
|
10
|
-
};
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// Show loading spinner
|
|
14
|
+
const loadingDiv = showLoading(appContainer);
|
|
15
|
+
|
|
16
|
+
// Wait for store to be ready
|
|
17
|
+
await storeReady;
|
|
18
|
+
|
|
19
|
+
// Remove loading spinner
|
|
20
|
+
hideLoading(loadingDiv);
|
|
21
|
+
|
|
22
|
+
appContainer.appendChild(createGame(store));
|
|
23
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{{includeFile template="client/src/game/board.css.hbs" output="client/src/board.css"}}
|
|
2
|
-
import './board.css';
|
|
3
|
-
import type {GameStore} from './store';
|
|
2
|
+
{{addImport "import './board.css';"}}
|
|
3
|
+
{{addImport "import type {GameStore} from './store';"}}
|
|
4
4
|
|
|
5
5
|
{{includeFile template="client/src/game/square.ts.hbs" output="client/src/square.{{ext}}"}}
|
|
6
|
-
import {createSquare} from './square';
|
|
6
|
+
{{addImport "import {createSquare} from './square';"}}
|
|
7
7
|
|
|
8
8
|
export const createBoard = (store: GameStore): HTMLDivElement => {
|
|
9
9
|
const board = document.createElement('div');
|
|
@@ -14,13 +14,13 @@ const gameStatus = store.getValue('gameStatus');
|
|
|
14
14
|
const winningLine = store.getValue('winningLine');
|
|
15
15
|
const currentPlayer = store.getValue('currentPlayer');
|
|
16
16
|
const winningPositions = new Set(
|
|
17
|
-
winningLine ? winningLine.split(',').map(Number) : []
|
|
17
|
+
winningLine && typeof winningLine === 'string' ? winningLine.split(',').map(Number) : []
|
|
18
18
|
);
|
|
19
19
|
const disabled = gameStatus !== 'playing';
|
|
20
20
|
|
|
21
21
|
board.innerHTML = '';
|
|
22
|
-
for (let i = 0; i < 9; i++) { const cell=store.getCell('board', i.toString(), 'value' ); const square=createSquare(
|
|
23
|
-
if (gameStatus === 'playing' && !cell) {
|
|
22
|
+
for (let i = 0; i < 9; i++) { const cell=store.getCell('board', i.toString(), 'value' ); const square=createSquare(cell as string, ()=> {
|
|
23
|
+
if (gameStatus === 'playing' && !cell && currentPlayer) {
|
|
24
24
|
store.setCell('board', i.toString(), 'value', currentPlayer);
|
|
25
25
|
}
|
|
26
26
|
},
|
|
@@ -36,4 +36,4 @@ for (let i = 0; i < 9; i++) { const cell=store.getCell('board', i.toString(), 'v
|
|
|
36
36
|
render();
|
|
37
37
|
|
|
38
38
|
return board;
|
|
39
|
-
};
|
|
39
|
+
};
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type {GameStore} from './store';
|
|
1
|
+
{{addImport "import type {GameStore} from './store';"}}
|
|
2
2
|
|
|
3
3
|
{{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
|
|
4
|
-
import {createButton} from './button';
|
|
4
|
+
{{addImport "import {createButton} from './button';"}}
|
|
5
5
|
|
|
6
6
|
{{includeFile template="client/src/game/board.ts.hbs" output="client/src/board.{{ext}}"}}
|
|
7
|
-
import {createBoard} from './board';
|
|
7
|
+
{{addImport "import {createBoard} from './board';"}}
|
|
8
8
|
|
|
9
9
|
{{includeFile template="client/src/game/gameStatus.ts.hbs" output="client/src/gameStatus.{{ext}}"}}
|
|
10
|
-
import {createGameStatus} from './gameStatus';
|
|
10
|
+
{{addImport "import {createGameStatus} from './gameStatus';"}}
|
|
11
11
|
|
|
12
12
|
const WINNING_LINES = [
|
|
13
|
-
[0, 1, 2], [3, 4, 5], [6, 7, 8],
|
|
14
|
-
[0, 3, 6], [1, 4, 7], [2, 5, 8],
|
|
15
|
-
[0, 4, 8], [2, 4, 6],
|
|
13
|
+
['0', '1', '2'], ['3', '4', '5'], ['6', '7', '8'],
|
|
14
|
+
['0', '3', '6'], ['1', '4', '7'], ['2', '5', '8'],
|
|
15
|
+
['0', '4', '8'], ['2', '4', '6'],
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
export const createGame = (store: GameStore): HTMLDivElement => {
|
|
@@ -20,14 +20,11 @@ const checkGameState = () => {
|
|
|
20
20
|
const gameStatus = store.getValue('gameStatus');
|
|
21
21
|
if (gameStatus !== 'playing') return;
|
|
22
22
|
|
|
23
|
-
const board = store.getTable('board');
|
|
24
|
-
|
|
25
|
-
// Check for winner
|
|
26
23
|
for (const line of WINNING_LINES) {
|
|
27
24
|
const [a, b, c] = line;
|
|
28
|
-
const cellA = board
|
|
29
|
-
const cellB = board
|
|
30
|
-
const cellC = board
|
|
25
|
+
const cellA = store.getCell('board', a, 'value');
|
|
26
|
+
const cellB = store.getCell('board', b, 'value');
|
|
27
|
+
const cellC = store.getCell('board', c, 'value');
|
|
31
28
|
|
|
32
29
|
if (cellA && cellA === cellB && cellA === cellC) {
|
|
33
30
|
store.setValue('gameStatus', 'won');
|
|
@@ -37,14 +34,11 @@ return;
|
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
const filledCells = Object.values(board).filter((cell: any) => cell.value).length;
|
|
42
|
-
if (filledCells === 9) {
|
|
37
|
+
if (store.getRowCount('board') === 9) {
|
|
43
38
|
store.setValue('gameStatus', 'draw');
|
|
44
39
|
return;
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
// Switch player after each move
|
|
48
42
|
const currentPlayer = store.getValue('currentPlayer');
|
|
49
43
|
store.setValue('currentPlayer', currentPlayer === 'X' ? 'O' : 'X');
|
|
50
44
|
};
|
|
@@ -71,4 +65,4 @@ buttonContainer.appendChild(createButton('New Game', resetGame, 'primary'));
|
|
|
71
65
|
gameContainer.appendChild(buttonContainer);
|
|
72
66
|
|
|
73
67
|
return gameContainer;
|
|
74
|
-
};
|
|
68
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{{includeFile template="client/src/game/gameStatus.css.hbs" output="client/src/gameStatus.css"}}
|
|
2
|
-
import './gameStatus.css';
|
|
3
|
-
import type {GameStore} from './store';
|
|
2
|
+
{{addImport "import './gameStatus.css';"}}
|
|
3
|
+
{{addImport "import type {GameStore} from './store';"}}
|
|
4
4
|
|
|
5
5
|
export const createGameStatus = (store: GameStore): HTMLDivElement => {
|
|
6
6
|
const status = document.createElement('div');
|
|
@@ -24,4 +24,4 @@ store.addValuesListener(render);
|
|
|
24
24
|
render();
|
|
25
25
|
|
|
26
26
|
return status;
|
|
27
|
-
};
|
|
27
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{{includeFile template="client/src/game/square.css.hbs" output="client/src/square.css"}}
|
|
2
|
-
import './square.css';
|
|
2
|
+
{{addImport "import './square.css';"}}
|
|
3
3
|
|
|
4
|
-
export const createSquare = (
|
|
4
|
+
export const createSquare = (value: string | undefined, onClick: () => void, disabled: boolean, winning: boolean): HTMLButtonElement => {
|
|
5
5
|
const square = document.createElement('button');
|
|
6
6
|
square.className = `square${disabled ? ' disabled' : ''}${winning ? ' winning' : ''}`;
|
|
7
|
-
square.textContent = value
|
|
7
|
+
square.textContent = value ?? '';
|
|
8
8
|
square.disabled = disabled;
|
|
9
9
|
square.addEventListener('click', onClick);
|
|
10
10
|
return square;
|
|
11
|
-
};
|
|
11
|
+
};
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{{#if schemas}}
|
|
2
|
-
import {createMergeableStore} from 'tinybase/with-schemas';
|
|
2
|
+
{{addImport "import {createMergeableStore} from 'tinybase/with-schemas';"}}
|
|
3
3
|
{{else}}
|
|
4
|
-
import {createMergeableStore} from 'tinybase';
|
|
4
|
+
{{addImport "import {createMergeableStore} from 'tinybase';"}}
|
|
5
5
|
{{/if}}
|
|
6
6
|
|
|
7
|
-
const STORE_ID = 'game';
|
|
8
|
-
|
|
9
7
|
{{#if schemas}}
|
|
8
|
+
const TABLES_SCHEMA = {
|
|
9
|
+
board: {
|
|
10
|
+
value: {type: 'string', default: ''},
|
|
11
|
+
},
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
10
14
|
const VALUES_SCHEMA = {
|
|
11
15
|
currentPlayer: {type: 'string'},
|
|
12
16
|
gameStatus: {type: 'string'},
|
|
@@ -15,7 +19,8 @@ const STORE_ID = 'game';
|
|
|
15
19
|
} as const;
|
|
16
20
|
|
|
17
21
|
{{/if}}
|
|
18
|
-
export const store = createMergeableStore(
|
|
22
|
+
export const store = createMergeableStore(){{#if schemas}}
|
|
23
|
+
.setTablesSchema(TABLES_SCHEMA)
|
|
19
24
|
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
20
25
|
.setDefaultContent([
|
|
21
26
|
{},
|
|
@@ -25,23 +30,90 @@ gameStatus: 'playing',
|
|
|
25
30
|
},
|
|
26
31
|
]);
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
});
|
|
33
|
+
let resolveReady: () => void;
|
|
34
|
+
export const storeReady = new Promise<void>((resolve) => {
|
|
35
|
+
resolveReady = resolve;
|
|
44
36
|
});
|
|
45
|
-
{{/if}}
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
{{#if persist}}
|
|
39
|
+
{{#if persistLocalStorage}}
|
|
40
|
+
{{#if schemas}}
|
|
41
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
|
|
42
|
+
{{else}}
|
|
43
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
|
|
44
|
+
{{/if}}
|
|
45
|
+
|
|
46
|
+
const persister = createLocalPersister(store, 'game');
|
|
47
|
+
persister.load().then(() => {
|
|
48
|
+
persister.startAutoSave();
|
|
49
|
+
resolveReady();
|
|
50
|
+
});
|
|
51
|
+
{{/if}}
|
|
52
|
+
{{#if persistSqlite}}
|
|
53
|
+
{{#if schemas}}
|
|
54
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
|
|
55
|
+
{{else}}
|
|
56
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
|
|
57
|
+
{{/if}}
|
|
58
|
+
{{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.ts"}}
|
|
59
|
+
{{addImport "import {getDb} from './sqlite';"}}
|
|
60
|
+
|
|
61
|
+
getDb().then(({sqlite3, db}) => {
|
|
62
|
+
const persister = createSqliteWasmPersister(store, sqlite3, db, 'game');
|
|
63
|
+
persister.load().then(() => {
|
|
64
|
+
persister.startAutoSave();
|
|
65
|
+
resolveReady();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
{{/if}}
|
|
69
|
+
{{#if persistPglite}}
|
|
70
|
+
{{#if schemas}}
|
|
71
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
|
|
72
|
+
{{else}}
|
|
73
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
|
|
74
|
+
{{/if}}
|
|
75
|
+
{{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
|
|
76
|
+
{{addImport "import {getPgLite} from './pglite';"}}
|
|
77
|
+
|
|
78
|
+
getPgLite().then((pgLite) => {
|
|
79
|
+
createPglitePersister(store, pgLite, 'game').then((persister) => {
|
|
80
|
+
persister.load().then(() => {
|
|
81
|
+
persister.startAutoSave();
|
|
82
|
+
resolveReady();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
{{/if}}
|
|
87
|
+
{{/if}}
|
|
88
|
+
|
|
89
|
+
{{#if sync}}
|
|
90
|
+
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
91
|
+
{{addImport "import {SERVER} from './config';"}}
|
|
92
|
+
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
93
|
+
{{#if schemas}}
|
|
94
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client/with-schemas';"}}
|
|
95
|
+
{{else}}
|
|
96
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
97
|
+
{{/if}}
|
|
98
|
+
|
|
99
|
+
const serverPathId = location.pathname;
|
|
100
|
+
createWsSynchronizer(
|
|
101
|
+
store,
|
|
102
|
+
new ReconnectingWebSocket(SERVER + serverPathId),
|
|
103
|
+
).then(async (synchronizer) => {
|
|
104
|
+
await synchronizer.startSync();
|
|
105
|
+
|
|
106
|
+
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
107
|
+
synchronizer.load().then(() => {
|
|
108
|
+
synchronizer.save();
|
|
109
|
+
{{#unless persist}}resolveReady();{{/unless}}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
{{/if}}
|
|
114
|
+
|
|
115
|
+
{{#unless persist}}{{#unless sync}}
|
|
116
|
+
resolveReady();
|
|
117
|
+
{{/unless}}{{/unless}}
|
|
118
|
+
|
|
119
|
+
export type GameStore = typeof store;
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
{{#if sync}}
|
|
2
|
-
import {getUniqueId} from 'tinybase';
|
|
3
|
-
|
|
2
|
+
{{addImport "import {getUniqueId} from 'tinybase';"}}
|
|
4
3
|
if (location.pathname === '/') {
|
|
5
4
|
location.assign('/' + getUniqueId());
|
|
6
5
|
}
|
|
7
6
|
{{/if}}
|
|
7
|
+
|
|
8
8
|
{{#if react}}
|
|
9
|
-
import ReactDOM from 'react-dom/client';
|
|
10
|
-
import {App} from './App';
|
|
9
|
+
{{addImport "import ReactDOM from 'react-dom/client';"}}
|
|
10
|
+
{{addImport "import {App} from './App';"}}
|
|
11
11
|
{{includeFile template="client/src/{{appType}}/App.tsx.hbs" output="client/src/App.{{ext}}"}}
|
|
12
|
-
|
|
13
12
|
addEventListener('load', () => {
|
|
14
13
|
ReactDOM.createRoot(document.getElementById('app')!).render(
|
|
15
14
|
<App />);
|
|
16
15
|
});
|
|
17
16
|
{{else}}
|
|
18
|
-
import {app} from './app';
|
|
17
|
+
{{addImport "import {app} from './app';"}}
|
|
19
18
|
{{includeFile template="client/src/{{appType}}/app.ts.hbs" output="client/src/app.{{ext}}"}}
|
|
20
|
-
|
|
21
19
|
addEventListener('load', () => {
|
|
22
20
|
app();
|
|
23
21
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{{includeFile template="client/src/shared/button.css.hbs" output="client/src/button.css"}}
|
|
2
|
-
import './button.css';
|
|
3
|
-
import {ReactNode} from 'react';
|
|
2
|
+
{{addImport "import './button.css';"}}
|
|
3
|
+
{{addImport "import {ReactNode} from 'react';"}}
|
|
4
4
|
|
|
5
5
|
interface ButtonProps {
|
|
6
6
|
children: ReactNode;
|
|
@@ -13,4 +13,4 @@ export const Button = ({children, onClick, variant = 'default', type = 'button'}
|
|
|
13
13
|
<button onClick={onClick} type={type} className={variant==='primary' ? 'primary' : undefined}>
|
|
14
14
|
{children}
|
|
15
15
|
</button>
|
|
16
|
-
);
|
|
16
|
+
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{{includeFile template="client/src/shared/input.css.hbs" output="client/src/input.css"}}
|
|
2
|
-
import './input.css';
|
|
2
|
+
{{addImport "import './input.css';"}}
|
|
3
3
|
|
|
4
4
|
interface InputProps {
|
|
5
5
|
value: string;
|
|
@@ -13,4 +13,4 @@ export const Input = ({value, onChange, placeholder = '', autoFocus = false}: In
|
|
|
13
13
|
placeholder={placeholder}
|
|
14
14
|
autoFocus={autoFocus}
|
|
15
15
|
/>
|
|
16
|
-
);
|
|
16
|
+
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{{includeFile template="client/src/shared/button.css.hbs" output="client/src/button.css"}}
|
|
2
|
-
import './button.css';
|
|
2
|
+
{{addImport "import './button.css';"}}
|
|
3
3
|
|
|
4
4
|
export const createButton = (
|
|
5
5
|
text: string,
|
|
@@ -13,4 +13,4 @@ button.className = 'primary';
|
|
|
13
13
|
}
|
|
14
14
|
button.addEventListener('click', onClick);
|
|
15
15
|
return button;
|
|
16
|
-
};
|
|
16
|
+
};
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
{{#if
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const SERVER = 'ws://localhost:8787';
|
|
6
|
-
{{/if}}
|
|
1
|
+
{{#if (eq syncType "node")}}
|
|
2
|
+
export const SERVER = 'ws://localhost:8043';
|
|
3
|
+
{{else if (eq syncType "durable-objects")}}
|
|
4
|
+
export const SERVER = 'ws://localhost:8787';
|
|
7
5
|
{{else}}
|
|
8
6
|
export const SERVER = 'wss://vite.tinybase.org';
|
|
9
7
|
{{/if}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{{includeFile template="client/src/shared/input.css.hbs" output="client/src/input.css"}}
|
|
2
|
-
import './input.css';
|
|
2
|
+
{{addImport "import './input.css';"}}
|
|
3
3
|
|
|
4
4
|
export const createInput = (
|
|
5
5
|
placeholder: string = '',
|
|
@@ -14,4 +14,4 @@ if (onInput) {
|
|
|
14
14
|
input.addEventListener('input', () => onInput(input.value));
|
|
15
15
|
}
|
|
16
16
|
return input;
|
|
17
|
-
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#loading {
|
|
2
|
+
animation: spin 1s infinite linear;
|
|
3
|
+
height: 2rem;
|
|
4
|
+
margin: 40vh auto;
|
|
5
|
+
width: 2rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#loading::before {
|
|
9
|
+
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="2rem" viewBox="0 0 100 100">
|
|
10
|
+
<path d="M50 10A40 40 0 1 1 10 50" stroke="%23d81b60" fill="none" stroke-width="4" />
|
|
11
|
+
</svg>');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@keyframes spin {
|
|
15
|
+
from {
|
|
16
|
+
transform: rotate(0);
|
|
17
|
+
}
|
|
18
|
+
to {
|
|
19
|
+
transform: rotate(360deg);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{{includeFile template="client/src/shared/loading.css.hbs" output="client/src/loading.css"}}
|
|
2
|
+
{{addImport "import './loading.css';"}}
|
|
3
|
+
|
|
4
|
+
export const showLoading = (container: HTMLElement): HTMLElement => {
|
|
5
|
+
const loadingDiv = document.createElement('div');
|
|
6
|
+
loadingDiv.id = 'loading';
|
|
7
|
+
container.appendChild(loadingDiv);
|
|
8
|
+
return loadingDiv;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const hideLoading = (loadingDiv: HTMLElement): void => {
|
|
12
|
+
loadingDiv.remove();
|
|
13
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{{addImport "import {PGlite} from '@electric-sql/pglite';"}}
|
|
2
|
+
|
|
3
|
+
let pgLitePromise: Promise<PGlite> | null = null;
|
|
4
|
+
|
|
5
|
+
export const getPgLite = async (): Promise<PGlite> => {
|
|
6
|
+
if (!pgLitePromise) {
|
|
7
|
+
pgLitePromise = PGlite.create('idb://local');
|
|
8
|
+
}
|
|
9
|
+
return await pgLitePromise;
|
|
10
|
+
};
|