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,11 +1,9 @@
|
|
|
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
|
-
const STORE_ID = 'settings';
|
|
8
|
-
|
|
9
7
|
{{#if schemas}}
|
|
10
8
|
const VALUES_SCHEMA = {
|
|
11
9
|
brushColor: {type: 'string'},
|
|
@@ -14,8 +12,65 @@ const STORE_ID = 'settings';
|
|
|
14
12
|
|
|
15
13
|
{{/if}}
|
|
16
14
|
export const settingsStore = createStore(){{#if schemas}}
|
|
17
|
-
.setValuesSchema(VALUES_SCHEMA){{/if}}
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
.setValuesSchema(VALUES_SCHEMA){{/if}};
|
|
16
|
+
|
|
17
|
+
let resolveSettingsReady: () => void;
|
|
18
|
+
export const settingsStoreReady = new Promise<void>((resolve) => {
|
|
19
|
+
resolveSettingsReady = resolve;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
{{#if persist}}
|
|
23
|
+
{{#if persistLocalStorage}}
|
|
24
|
+
{{#if schemas}}
|
|
25
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
|
|
26
|
+
{{else}}
|
|
27
|
+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
|
|
28
|
+
{{/if}}
|
|
29
|
+
|
|
30
|
+
const settingsPersister = createLocalPersister(settingsStore, 'settings');
|
|
31
|
+
settingsPersister.load([{}, {brushColor: '#d81b60', brushSize: 5}]).then(() => {
|
|
32
|
+
settingsPersister.startAutoSave();
|
|
33
|
+
resolveSettingsReady();
|
|
34
|
+
});
|
|
35
|
+
{{/if}}
|
|
36
|
+
{{#if persistSqlite}}
|
|
37
|
+
{{#if schemas}}
|
|
38
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
|
|
39
|
+
{{else}}
|
|
40
|
+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
|
|
41
|
+
{{/if}}
|
|
42
|
+
{{includeFile template="client/src/shared/sqlite.ts.hbs" output="client/src/sqlite.ts"}}
|
|
43
|
+
{{addImport "import {getDb} from './sqlite';"}}
|
|
44
|
+
|
|
45
|
+
getDb().then(({sqlite3, db}) => {
|
|
46
|
+
const settingsPersister = createSqliteWasmPersister(settingsStore, sqlite3, db, 'settings');
|
|
47
|
+
settingsPersister.load([{}, {brushColor: '#d81b60', brushSize: 5}]).then(() => {
|
|
48
|
+
settingsPersister.startAutoSave();
|
|
49
|
+
resolveSettingsReady();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
{{/if}}
|
|
53
|
+
{{#if persistPglite}}
|
|
54
|
+
{{#if schemas}}
|
|
55
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
|
|
56
|
+
{{else}}
|
|
57
|
+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
|
|
58
|
+
{{/if}}
|
|
59
|
+
{{includeFile template="client/src/shared/pglite.ts.hbs" output="client/src/pglite.{{ext}}"}}
|
|
60
|
+
{{addImport "import {getPgLite} from './pglite';"}}
|
|
61
|
+
|
|
62
|
+
getPgLite().then((pgLite) => {
|
|
63
|
+
createPglitePersister(settingsStore, pgLite, 'settings').then((settingsPersister) => {
|
|
64
|
+
settingsPersister.load([{}, {brushColor: '#d81b60', brushSize: 5}]).then(() => {
|
|
65
|
+
settingsPersister.startAutoSave();
|
|
66
|
+
resolveSettingsReady();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
{{/if}}
|
|
71
|
+
{{else}}
|
|
72
|
+
settingsStore.setValue('brushColor', '#d81b60').setValue('brushSize', 5);
|
|
73
|
+
resolveSettingsReady();
|
|
74
|
+
{{/if}}
|
|
20
75
|
|
|
21
|
-
export type SettingsStore = typeof settingsStore;
|
|
76
|
+
export type SettingsStore = typeof settingsStore;
|
|
@@ -1,28 +1,32 @@
|
|
|
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/game/Store.tsx.hbs" output="client/src/Store.{{ext}}"}}
|
|
11
|
-
import {Store} from './Store';
|
|
9
|
+
{{addImport "import {Store} from './Store';"}}
|
|
12
10
|
|
|
13
11
|
{{includeFile template="client/src/game/Game.tsx.hbs" output="client/src/Game.{{ext}}"}}
|
|
14
|
-
import {Game} from './Game';
|
|
12
|
+
{{addImport "import {Game} from './Game';"}}
|
|
13
|
+
|
|
14
|
+
export const App = () => {
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
15
16
|
|
|
16
|
-
const App = () => {
|
|
17
17
|
return (
|
|
18
18
|
<StrictMode>
|
|
19
19
|
<Provider>
|
|
20
|
-
<Store />
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
<Store onReady={()=> setLoading(false)} />
|
|
21
|
+
{loading ? (
|
|
22
|
+
<Loading />
|
|
23
|
+
) : (
|
|
24
|
+
<>
|
|
25
|
+
<Game />
|
|
26
|
+
<Inspector />
|
|
27
|
+
</>
|
|
28
|
+
)}
|
|
23
29
|
</Provider>
|
|
24
30
|
</StrictMode>
|
|
25
31
|
);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export {App};
|
|
32
|
+
};
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{{includeFile template="client/src/game/board.css.hbs" output="client/src/board.css"}}
|
|
2
|
-
import './board.css';
|
|
3
|
-
import {useMemo} from 'react';
|
|
4
|
-
import {useValue, STORE_ID} from './Store';
|
|
2
|
+
{{addImport "import './board.css';"}}
|
|
3
|
+
{{addImport "import {useMemo} from 'react';"}}
|
|
4
|
+
{{addImport "import {useValue, STORE_ID} from './Store';"}}
|
|
5
5
|
|
|
6
6
|
{{includeFile template="client/src/game/Square.tsx.hbs" output="client/src/Square.{{ext}}"}}
|
|
7
|
-
import {Square} from './Square';
|
|
7
|
+
{{addImport "import {Square} from './Square';"}}
|
|
8
8
|
|
|
9
9
|
export const Board = () => {
|
|
10
10
|
const gameStatus = useValue('gameStatus', STORE_ID);
|
|
11
11
|
const winningLine = useValue('winningLine', STORE_ID);
|
|
12
12
|
|
|
13
13
|
const winningPositions = useMemo(() => {
|
|
14
|
-
if (!winningLine) return new Set();
|
|
15
|
-
return new Set(winningLine.split(',')
|
|
14
|
+
if (!winningLine || typeof winningLine !== 'string') return new Set();
|
|
15
|
+
return new Set(winningLine.split(','));
|
|
16
16
|
}, [winningLine]);
|
|
17
17
|
|
|
18
18
|
const disabled = gameStatus !== 'playing';
|
|
@@ -20,8 +20,8 @@ const disabled = gameStatus !== 'playing';
|
|
|
20
20
|
return (
|
|
21
21
|
<div id="board">
|
|
22
22
|
{Array.from({length: 9}, (_, i) => (
|
|
23
|
-
<Square key={i} position={i} disabled={disabled} winning={winningPositions.has(i)} />
|
|
23
|
+
<Square key={i} position={i.toString()} disabled={disabled} winning={winningPositions.has(i.toString())} />
|
|
24
24
|
))}
|
|
25
25
|
</div>
|
|
26
26
|
);
|
|
27
|
-
};
|
|
27
|
+
};
|
|
@@ -1,37 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
{{addImport "import {useSetValuesCallback, useTableListener, STORE_ID} from './Store';"}}
|
|
2
2
|
|
|
3
3
|
{{includeFile template="client/src/shared/Button.tsx.hbs" output="client/src/Button.{{ext}}"}}
|
|
4
|
-
import {Button} from './Button';
|
|
4
|
+
{{addImport "import {Button} from './Button';"}}
|
|
5
5
|
|
|
6
6
|
{{includeFile template="client/src/game/Board.tsx.hbs" output="client/src/Board.{{ext}}"}}
|
|
7
|
-
import {Board} from './Board';
|
|
7
|
+
{{addImport "import {Board} from './Board';"}}
|
|
8
8
|
|
|
9
9
|
{{includeFile template="client/src/game/GameStatus.tsx.hbs" output="client/src/GameStatus.{{ext}}"}}
|
|
10
|
-
import {GameStatus} from './GameStatus';
|
|
10
|
+
{{addImport "import {GameStatus} 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 Game = () => {
|
|
19
|
-
const store = useStore(STORE_ID);
|
|
20
|
-
|
|
21
19
|
useTableListener(
|
|
22
20
|
'board',
|
|
23
|
-
() => {
|
|
21
|
+
(store) => {
|
|
24
22
|
const gameStatus = store.getValue('gameStatus');
|
|
25
23
|
if (gameStatus !== 'playing') return;
|
|
26
24
|
|
|
27
|
-
const board = store.getTable('board');
|
|
28
|
-
|
|
29
|
-
// Check for winner
|
|
30
25
|
for (const line of WINNING_LINES) {
|
|
31
26
|
const [a, b, c] = line;
|
|
32
|
-
const cellA = board
|
|
33
|
-
const cellB = board
|
|
34
|
-
const cellC = board
|
|
27
|
+
const cellA = store.getCell('board', a, 'value');
|
|
28
|
+
const cellB = store.getCell('board', b, 'value');
|
|
29
|
+
const cellC = store.getCell('board', c, 'value');
|
|
35
30
|
|
|
36
31
|
if (cellA && cellA === cellB && cellA === cellC) {
|
|
37
32
|
store.setValue('gameStatus', 'won');
|
|
@@ -41,14 +36,11 @@ return;
|
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
const filledCells = Object.values(board).filter((cell: any) => cell.value).length;
|
|
46
|
-
if (filledCells === 9) {
|
|
39
|
+
if (store.getRowCount('board') === 9) {
|
|
47
40
|
store.setValue('gameStatus', 'draw');
|
|
48
41
|
return;
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
// Switch player after each move
|
|
52
44
|
const currentPlayer = store.getValue('currentPlayer');
|
|
53
45
|
store.setValue('currentPlayer', currentPlayer === 'X' ? 'O' : 'X');
|
|
54
46
|
},
|
|
@@ -57,13 +49,14 @@ true,
|
|
|
57
49
|
STORE_ID,);
|
|
58
50
|
|
|
59
51
|
const resetGame = useSetValuesCallback(
|
|
60
|
-
() => {
|
|
52
|
+
(_, store) => {
|
|
61
53
|
store.delTable('board');
|
|
62
54
|
return {currentPlayer: 'X',
|
|
63
55
|
gameStatus: 'playing',
|
|
64
56
|
};
|
|
65
57
|
},
|
|
66
58
|
[],
|
|
59
|
+
STORE_ID,
|
|
67
60
|
);
|
|
68
61
|
|
|
69
62
|
return (
|
|
@@ -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 {useValue, STORE_ID} from './Store';
|
|
2
|
+
{{addImport "import './gameStatus.css';"}}
|
|
3
|
+
{{addImport "import {useValue, STORE_ID} from './Store';"}}
|
|
4
4
|
|
|
5
5
|
export const GameStatus = () => {
|
|
6
6
|
const gameStatus = useValue('gameStatus', STORE_ID);
|
|
@@ -10,12 +10,12 @@ const winner = useValue('winner', STORE_ID);
|
|
|
10
10
|
return (
|
|
11
11
|
<div id="gameStatus">
|
|
12
12
|
{gameStatus === 'playing' && (
|
|
13
|
-
<>Player <span className="player">{currentPlayer}</span
|
|
13
|
+
<>Player <span className="player">{currentPlayer}</span>'s turn</>
|
|
14
14
|
)}
|
|
15
15
|
{gameStatus === 'won' && (
|
|
16
16
|
<>Player <span className="winner">{winner}</span> wins!</>
|
|
17
17
|
)}
|
|
18
|
-
{gameStatus === 'draw' && <>It
|
|
18
|
+
{gameStatus === 'draw' && <>It's a draw!</>}
|
|
19
19
|
</div>
|
|
20
20
|
);
|
|
21
|
-
};
|
|
21
|
+
};
|
|
@@ -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
|
+
};
|