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.
Files changed (81) hide show
  1. package/README.md +62 -18
  2. package/cli.js +2 -2
  3. package/package.json +7 -4
  4. package/screenshots/chat.png +0 -0
  5. package/screenshots/drawing.png +0 -0
  6. package/screenshots/todos.png +0 -0
  7. package/templates/README.md.hbs +130 -12
  8. package/templates/client/eslint.config.js.hbs +7 -0
  9. package/templates/client/package.json.hbs +28 -8
  10. package/templates/client/src/chat/App.tsx.hbs +28 -22
  11. package/templates/client/src/chat/ChatStore.tsx.hbs +108 -27
  12. package/templates/client/src/chat/Message.tsx.hbs +8 -9
  13. package/templates/client/src/chat/MessageInput.tsx.hbs +7 -7
  14. package/templates/client/src/chat/Messages.tsx.hbs +5 -15
  15. package/templates/client/src/chat/SettingsStore.tsx.hbs +86 -9
  16. package/templates/client/src/chat/UsernameInput.tsx.hbs +6 -11
  17. package/templates/client/src/chat/app.ts.hbs +18 -18
  18. package/templates/client/src/chat/chatStore.ts.hbs +97 -35
  19. package/templates/client/src/chat/message.ts.hbs +4 -3
  20. package/templates/client/src/chat/messageInput.css.hbs +1 -1
  21. package/templates/client/src/chat/messageInput.ts.hbs +7 -8
  22. package/templates/client/src/chat/messages.ts.hbs +7 -12
  23. package/templates/client/src/chat/settingsStore.ts.hbs +65 -6
  24. package/templates/client/src/chat/usernameInput.css.hbs +1 -1
  25. package/templates/client/src/chat/usernameInput.ts.hbs +6 -6
  26. package/templates/client/src/chat/utils.ts.hbs +26 -0
  27. package/templates/client/src/drawing/App.tsx.hbs +26 -20
  28. package/templates/client/src/drawing/BrushSize.tsx.hbs +8 -11
  29. package/templates/client/src/drawing/Canvas.tsx.hbs +65 -73
  30. package/templates/client/src/drawing/CanvasStore.tsx.hbs +104 -18
  31. package/templates/client/src/drawing/ColorPicker.tsx.hbs +4 -11
  32. package/templates/client/src/drawing/DrawingControls.tsx.hbs +7 -7
  33. package/templates/client/src/drawing/SettingsStore.tsx.hbs +81 -8
  34. package/templates/client/src/drawing/app.ts.hbs +18 -8
  35. package/templates/client/src/drawing/brushSize.ts.hbs +12 -5
  36. package/templates/client/src/drawing/canvas.ts.hbs +84 -86
  37. package/templates/client/src/drawing/canvasStore.ts.hbs +93 -26
  38. package/templates/client/src/drawing/colorPicker.ts.hbs +3 -3
  39. package/templates/client/src/drawing/drawingControls.ts.hbs +7 -7
  40. package/templates/client/src/drawing/settingsStore.ts.hbs +63 -8
  41. package/templates/client/src/game/App.tsx.hbs +20 -16
  42. package/templates/client/src/game/Board.tsx.hbs +8 -8
  43. package/templates/client/src/game/Game.tsx.hbs +14 -21
  44. package/templates/client/src/game/GameStatus.tsx.hbs +5 -5
  45. package/templates/client/src/game/Square.tsx.hbs +6 -11
  46. package/templates/client/src/game/Store.tsx.hbs +106 -16
  47. package/templates/client/src/game/app.ts.hbs +17 -6
  48. package/templates/client/src/game/board.ts.hbs +7 -7
  49. package/templates/client/src/game/game.ts.hbs +12 -18
  50. package/templates/client/src/game/gameStatus.ts.hbs +3 -3
  51. package/templates/client/src/game/square.ts.hbs +4 -4
  52. package/templates/client/src/game/store.ts.hbs +95 -23
  53. package/templates/client/src/index.tsx.hbs +5 -7
  54. package/templates/client/src/shared/Button.tsx.hbs +3 -3
  55. package/templates/client/src/shared/Input.tsx.hbs +2 -2
  56. package/templates/client/src/shared/Loading.tsx.hbs +5 -0
  57. package/templates/client/src/shared/button.ts.hbs +2 -2
  58. package/templates/client/src/shared/config.ts.hbs +4 -6
  59. package/templates/client/src/shared/input.ts.hbs +2 -2
  60. package/templates/client/src/shared/loading.css.hbs +21 -0
  61. package/templates/client/src/shared/loading.ts.hbs +13 -0
  62. package/templates/client/src/shared/pglite.ts.hbs +10 -0
  63. package/templates/client/src/shared/sqlite.ts.hbs +17 -0
  64. package/templates/client/src/todos/App.tsx.hbs +22 -22
  65. package/templates/client/src/todos/Store.tsx.hbs +106 -23
  66. package/templates/client/src/todos/TodoInput.tsx.hbs +6 -8
  67. package/templates/client/src/todos/TodoItem.tsx.hbs +5 -6
  68. package/templates/client/src/todos/TodoList.tsx.hbs +5 -6
  69. package/templates/client/src/todos/app.ts.hbs +16 -10
  70. package/templates/client/src/todos/store.ts.hbs +94 -30
  71. package/templates/client/src/todos/todoInput.ts.hbs +5 -8
  72. package/templates/client/src/todos/todoItem.ts.hbs +3 -4
  73. package/templates/client/src/todos/todoList.ts.hbs +6 -8
  74. package/templates/client/vite-env.d.ts.hbs +4 -0
  75. package/templates/client/vite.config.js.hbs +53 -3
  76. package/templates/server/index.ts.hbs +43 -0
  77. package/templates/server/package.json.hbs +6 -7
  78. package/templates/server/wrangler.toml.hbs +1 -1
  79. package/templates/server/index-do.ts.hbs +0 -22
  80. package/templates/server/index-node.ts.hbs +0 -8
  81. /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
- .setValue('brushColor', '#d81b60')
19
- .setValue('brushSize', 5);
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
- {{#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';
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
- <Game />
22
- <Inspector />
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(',').map(Number));
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 {useStore, useSetValuesCallback, useTableListener, STORE_ID} from './Store';
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], // rows
14
- [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
15
- [0, 4, 8], [2, 4, 6], // diagonals
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[a]?.value;
33
- const cellB = board[b]?.value;
34
- const cellC = board[c]?.value;
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
- // Check for draw
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>'s turn</>
13
+ <>Player <span className="player">{currentPlayer}</span>&apos;s turn</>
14
14
  )}
15
15
  {gameStatus === 'won' && (
16
16
  <>Player <span className="winner">{winner}</span> wins!</>
17
17
  )}
18
- {gameStatus === 'draw' && <>It's a draw!</>}
18
+ {gameStatus === 'draw' && <>It&apos;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, useStore, useValue, STORE_ID} from './Store';
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: number; disabled: boolean; winning: boolean}) => {
6
- const store = useStore(STORE_ID);
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 {useCreateStore, useProvideStore, useStore, useSetValuesCallback, useTableListener, useCell, useValue} from 'tinybase/ui-react';
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 = [NoTablesSchema, typeof VALUES_SCHEMA];
25
+ type Schemas = [typeof TABLES_SCHEMA, typeof VALUES_SCHEMA];
21
26
 
22
- const {useCreateStore, useProvideStore, useStore, useSetValuesCallback, useTableListener, useCell, useValue} = UiReact as UiReact.WithSchemas<Schemas>;
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 = useCreateStore(() =>
29
- createMergeableStore(STORE_ID){{#if schemas}}
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
- {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
47
- {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
48
- {{addImport "import type {MergeableStore} from 'tinybase';"}}
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: MergeableStore) => {
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(() => synchronizer.save());
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
- export {app};
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( i, cell, ()=> {
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], // rows
14
- [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
15
- [0, 4, 8], [2, 4, 6], // diagonals
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[a]?.value;
29
- const cellB = board[b]?.value;
30
- const cellC = board[c]?.value;
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
- // Check for draw
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 = (position: number, value: string | undefined, onClick: () => void, disabled: boolean, winning: boolean): HTMLButtonElement => {
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
+ };