create-tinybase 1.0.1 → 1.1.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.
Files changed (43) hide show
  1. package/cli.js +2 -2
  2. package/package.json +2 -1
  3. package/screenshots/chat.png +0 -0
  4. package/screenshots/drawing.png +0 -0
  5. package/screenshots/game.png +0 -0
  6. package/screenshots/todos.png +0 -0
  7. package/templates/README.md.hbs +18 -19
  8. package/templates/client/.npmrc.hbs +1 -0
  9. package/templates/client/index.html.hbs +46 -9
  10. package/templates/client/package.json.hbs +20 -11
  11. package/templates/client/public/browser.svg +81 -0
  12. package/templates/client/public/js.svg +5 -0
  13. package/templates/client/public/pglite.svg +8 -0
  14. package/templates/client/public/react.svg +9 -0
  15. package/templates/client/public/sqlite.svg +58 -0
  16. package/templates/client/public/svelte.svg +23 -0
  17. package/templates/client/public/sync.svg +4 -0
  18. package/templates/client/public/ts.svg +6 -0
  19. package/templates/client/src/chat/App.svelte.hbs +31 -0
  20. package/templates/client/src/chat/Message.svelte.hbs +17 -0
  21. package/templates/client/src/chat/MessageInput.svelte.hbs +53 -0
  22. package/templates/client/src/chat/Messages.svelte.hbs +34 -0
  23. package/templates/client/src/chat/UsernameInput.svelte.hbs +45 -0
  24. package/templates/client/src/drawing/App.svelte.hbs +28 -0
  25. package/templates/client/src/drawing/BrushSize.svelte.hbs +45 -0
  26. package/templates/client/src/drawing/Canvas.svelte.hbs +145 -0
  27. package/templates/client/src/drawing/ColorPicker.svelte.hbs +39 -0
  28. package/templates/client/src/drawing/DrawingControls.svelte.hbs +22 -0
  29. package/templates/client/src/game/App.svelte.hbs +23 -0
  30. package/templates/client/src/game/Board.svelte.hbs +71 -0
  31. package/templates/client/src/game/Game.svelte.hbs +64 -0
  32. package/templates/client/src/game/GameStatus.svelte.hbs +37 -0
  33. package/templates/client/src/game/Square.svelte.hbs +25 -0
  34. package/templates/client/src/index.tsx.hbs +16 -5
  35. package/templates/client/src/shared/Loading.svelte.hbs +7 -0
  36. package/templates/client/src/shared/sqlite.ts.hbs +5 -2
  37. package/templates/client/src/todos/App.svelte.hbs +26 -0
  38. package/templates/client/src/todos/TodoInput.svelte.hbs +45 -0
  39. package/templates/client/src/todos/TodoItem.svelte.hbs +47 -0
  40. package/templates/client/src/todos/TodoList.svelte.hbs +29 -0
  41. package/templates/client/tsconfig.json.hbs +7 -1
  42. package/templates/client/vite-env.d.ts.hbs +11 -1
  43. package/templates/client/vite.config.js.hbs +7 -1
@@ -0,0 +1,34 @@
1
+ {{includeFile template="client/src/chat/messages.css.hbs" output="client/src/messages.css"}}
2
+ {{includeFile template="client/src/chat/Message.svelte.hbs" output="client/src/Message.svelte"}}
3
+
4
+ <script{{#if typescript}} lang="ts"{{/if}}>
5
+ import './messages.css';
6
+ import {onMount} from 'svelte';
7
+ import Message from './Message.svelte';
8
+ import {chatStore} from './chatStore';
9
+ {{#if typescript}}import type {MessageRow} from './chatStore';{{/if}}
10
+
11
+ const getMessages = () =>
12
+ chatStore.getSortedRowIds('messages', 'timestamp').map((id) => ({
13
+ id,
14
+ row:
15
+ {{#if typescript}}chatStore.getRow('messages', id) as MessageRow{{else}}chatStore.getRow('messages', id){{/if}},
16
+ }));
17
+
18
+ let messages = $state(getMessages());
19
+
20
+ const updateMessages = () => {
21
+ messages = getMessages();
22
+ };
23
+
24
+ onMount(() => {
25
+ chatStore.addTablesListener(updateMessages);
26
+ updateMessages();
27
+ });
28
+ </script>
29
+
30
+ <div id="messages">
31
+ {#each messages as message (message.id)}
32
+ <Message message={message.row} />
33
+ {/each}
34
+ </div>
@@ -0,0 +1,45 @@
1
+ {{includeFile template="client/src/shared/input.css.hbs" output="client/src/input.css"}}
2
+ {{includeFile template="client/src/chat/usernameInput.css.hbs" output="client/src/usernameInput.css"}}
3
+
4
+ <script{{#if typescript}} lang="ts"{{/if}}>
5
+ import './input.css';
6
+ import './usernameInput.css';
7
+ import {onMount} from 'svelte';
8
+ import {settingsStore} from './settingsStore';
9
+
10
+ const getUsername = () =>
11
+ {{#if typescript}}(settingsStore.getValue('username') as string | undefined) ?? ''{{else}}settingsStore.getValue('username') ?? ''{{/if}};
12
+
13
+ let username = $state(getUsername());
14
+
15
+ const updateUsername = () => {
16
+ username = getUsername();
17
+ };
18
+
19
+ const handleInput = (event{{#if typescript}}: Event{{/if}}) => {
20
+ {{#if typescript}}
21
+ const input = event.currentTarget as HTMLInputElement;
22
+ const nextUsername = input.value;
23
+ {{else}}
24
+ const nextUsername = event.currentTarget.value;
25
+ {{/if}}
26
+ settingsStore.setValue('username', nextUsername);
27
+ username = nextUsername;
28
+ };
29
+
30
+ onMount(() => {
31
+ settingsStore.addValueListener('username', updateUsername);
32
+ updateUsername();
33
+ });
34
+ </script>
35
+
36
+ <div id="usernameInput">
37
+ <label for="usernameInputField">Your name:</label>
38
+ <input
39
+ id="usernameInputField"
40
+ type="text"
41
+ value={username}
42
+ placeholder="Enter your name"
43
+ oninput={handleInput}
44
+ />
45
+ </div>
@@ -0,0 +1,28 @@
1
+ {{includeFile template="client/src/drawing/settingsStore.ts.hbs" output="client/src/settingsStore.ts"}}
2
+ {{includeFile template="client/src/drawing/canvasStore.ts.hbs" output="client/src/canvasStore.ts"}}
3
+ {{includeFile template="client/src/shared/Loading.svelte.hbs" output="client/src/Loading.svelte"}}
4
+ {{includeFile template="client/src/drawing/DrawingControls.svelte.hbs" output="client/src/DrawingControls.svelte"}}
5
+ {{includeFile template="client/src/drawing/Canvas.svelte.hbs" output="client/src/Canvas.svelte"}}
6
+
7
+ <script{{#if typescript}} lang="ts"{{/if}}>
8
+ import {onMount} from 'svelte';
9
+ import Canvas from './Canvas.svelte';
10
+ import DrawingControls from './DrawingControls.svelte';
11
+ import Loading from './Loading.svelte';
12
+ import {canvasStoreReady} from './canvasStore';
13
+ import {settingsStoreReady} from './settingsStore';
14
+
15
+ let loading = $state(true);
16
+
17
+ onMount(async () => {
18
+ await Promise.all([settingsStoreReady, canvasStoreReady]);
19
+ loading = false;
20
+ });
21
+ </script>
22
+
23
+ {#if loading}
24
+ <Loading />
25
+ {:else}
26
+ <DrawingControls />
27
+ <Canvas />
28
+ {/if}
@@ -0,0 +1,45 @@
1
+ {{includeFile template="client/src/drawing/brushSize.css.hbs" output="client/src/brushSize.css"}}
2
+
3
+ <script{{#if typescript}} lang="ts"{{/if}}>
4
+ import './brushSize.css';
5
+ import {onMount} from 'svelte';
6
+ import {settingsStore} from './settingsStore';
7
+
8
+ const getBrushSize = () =>
9
+ {{#if typescript}}(settingsStore.getValue('brushSize') as number | undefined) ?? 5{{else}}settingsStore.getValue('brushSize') ?? 5{{/if}};
10
+
11
+ let size = $state(getBrushSize());
12
+
13
+ const updateSize = () => {
14
+ size = getBrushSize();
15
+ };
16
+
17
+ const handleInput = (event{{#if typescript}}: Event{{/if}}) => {
18
+ {{#if typescript}}
19
+ const input = event.currentTarget as HTMLInputElement;
20
+ const nextSize = parseInt(input.value);
21
+ {{else}}
22
+ const nextSize = parseInt(event.currentTarget.value);
23
+ {{/if}}
24
+ settingsStore.setValue('brushSize', nextSize);
25
+ size = nextSize;
26
+ };
27
+
28
+ onMount(() => {
29
+ settingsStore.addValueListener('brushSize', updateSize);
30
+ updateSize();
31
+ });
32
+ </script>
33
+
34
+ <div id="brushSize">
35
+ <label for="brushSizeSlider">Size:</label>
36
+ <input
37
+ id="brushSizeSlider"
38
+ type="range"
39
+ min="1"
40
+ max="50"
41
+ value={size}
42
+ oninput={handleInput}
43
+ />
44
+ <span id="brushSizeValue">{size}</span>
45
+ </div>
@@ -0,0 +1,145 @@
1
+ {{includeFile template="client/src/drawing/canvas.css.hbs" output="client/src/canvas.css"}}
2
+
3
+ <script{{#if typescript}} lang="ts"{{/if}}>
4
+ import './canvas.css';
5
+ import {onMount} from 'svelte';
6
+ import {getHlcFunctions} from 'tinybase';
7
+ import {canvasStore} from './canvasStore';
8
+ {{#if typescript}}import type {StrokeRow} from './canvasStore';{{/if}}
9
+ import {settingsStore} from './settingsStore';
10
+
11
+ const [getNextHlc] = getHlcFunctions();
12
+
13
+ let canvas{{#if typescript}}: HTMLCanvasElement | undefined{{/if}};
14
+ let isDrawing = false;
15
+ let currentStrokeId{{#if typescript}}: string | null{{/if}} = null;
16
+
17
+ const getPoint = (event{{#if typescript}}: MouseEvent | TouchEvent{{/if}}) => {
18
+ if (!canvas) {
19
+ return null;
20
+ }
21
+
22
+ const rect = canvas.getBoundingClientRect();
23
+ const clientX = 'touches' in event ? event.touches[0]?.clientX : event.clientX;
24
+ const clientY = 'touches' in event ? event.touches[0]?.clientY : event.clientY;
25
+
26
+ if (clientX == null || clientY == null) {
27
+ return null;
28
+ }
29
+
30
+ return {
31
+ x: clientX - rect.left,
32
+ y: clientY - rect.top,
33
+ };
34
+ };
35
+
36
+ const startStroke = (event{{#if typescript}}: MouseEvent | TouchEvent{{/if}}) => {
37
+ isDrawing = true;
38
+ currentStrokeId = getNextHlc();
39
+
40
+ const point = getPoint(event);
41
+ if (!point || !currentStrokeId) {
42
+ return;
43
+ }
44
+
45
+ const brushColor =
46
+ {{#if typescript}}(settingsStore.getValue('brushColor') as string | undefined) ?? '#d81b60'{{else}}settingsStore.getValue('brushColor') ?? '#d81b60'{{/if}};
47
+ const brushSize =
48
+ {{#if typescript}}(settingsStore.getValue('brushSize') as number | undefined) ?? 5{{else}}settingsStore.getValue('brushSize') ?? 5{{/if}};
49
+
50
+ canvasStore.setRow('strokes', currentStrokeId, {
51
+ color: brushColor,
52
+ size: brushSize,
53
+ points: JSON.stringify([point.x, point.y]),
54
+ });
55
+ };
56
+
57
+ const extendStroke = (event{{#if typescript}}: MouseEvent | TouchEvent{{/if}}) => {
58
+ if (!isDrawing || !currentStrokeId) {
59
+ return;
60
+ }
61
+
62
+ const point = getPoint(event);
63
+ if (!point) {
64
+ return;
65
+ }
66
+
67
+ const pointsArray = JSON.parse(
68
+ {{#if typescript}}(canvasStore.getCell('strokes', currentStrokeId, 'points') as string | undefined) ?? '[]'{{else}}canvasStore.getCell('strokes', currentStrokeId, 'points') ?? '[]'{{/if}},
69
+ ){{#if typescript}} as number[]{{/if}};
70
+ pointsArray.push(point.x, point.y);
71
+ canvasStore.setCell('strokes', currentStrokeId, 'points', JSON.stringify(pointsArray));
72
+ };
73
+
74
+ const endStroke = () => {
75
+ isDrawing = false;
76
+ currentStrokeId = null;
77
+ };
78
+
79
+ const handleTouchStart = (event{{#if typescript}}: TouchEvent{{/if}}) => {
80
+ event.preventDefault();
81
+ startStroke(event);
82
+ };
83
+
84
+ const handleTouchMove = (event{{#if typescript}}: TouchEvent{{/if}}) => {
85
+ event.preventDefault();
86
+ extendStroke(event);
87
+ };
88
+
89
+ onMount(() => {
90
+ const draw = () => {
91
+ if (!canvas) {
92
+ return;
93
+ }
94
+
95
+ const ctx = canvas.getContext('2d');
96
+ if (!ctx) {
97
+ return;
98
+ }
99
+
100
+ ctx.fillStyle = '#111';
101
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
102
+
103
+ canvasStore.getSortedRowIds('strokes').forEach((id) => {
104
+ const stroke =
105
+ {{#if typescript}}canvasStore.getRow('strokes', id) as StrokeRow{{else}}canvasStore.getRow('strokes', id){{/if}};
106
+ if (!stroke?.points) {
107
+ return;
108
+ }
109
+
110
+ const pointsArray = JSON.parse(stroke.points){{#if typescript}} as number[]{{/if}};
111
+ if (pointsArray.length < 2) {
112
+ return;
113
+ }
114
+
115
+ ctx.strokeStyle = stroke.color;
116
+ ctx.lineWidth = stroke.size * 2;
117
+ ctx.lineCap = 'round';
118
+ ctx.lineJoin = 'round';
119
+ ctx.beginPath();
120
+ ctx.moveTo(pointsArray[0], pointsArray[1]);
121
+ for (let i = 2; i < pointsArray.length; i += 2) {
122
+ ctx.lineTo(pointsArray[i], pointsArray[i + 1]);
123
+ }
124
+ ctx.stroke();
125
+ });
126
+ };
127
+
128
+ canvasStore.addTablesListener(draw);
129
+ draw();
130
+ });
131
+ </script>
132
+
133
+ <canvas
134
+ bind:this={canvas}
135
+ id="drawingCanvas"
136
+ width="600"
137
+ height="400"
138
+ onmousedown={startStroke}
139
+ onmousemove={extendStroke}
140
+ onmouseup={endStroke}
141
+ onmouseleave={endStroke}
142
+ ontouchstart={handleTouchStart}
143
+ ontouchmove={handleTouchMove}
144
+ ontouchend={endStroke}
145
+ ></canvas>
@@ -0,0 +1,39 @@
1
+ {{includeFile template="client/src/drawing/colorPicker.css.hbs" output="client/src/colorPicker.css"}}
2
+
3
+ <script{{#if typescript}} lang="ts"{{/if}}>
4
+ import './colorPicker.css';
5
+ import {onMount} from 'svelte';
6
+ import {settingsStore} from './settingsStore';
7
+
8
+ const colors = ['#d81b60', '#1976d2', '#388e3c', '#f57c00', '#7b1fa2', '#fff'];
9
+ const getCurrentColor = () =>
10
+ {{#if typescript}}(settingsStore.getValue('brushColor') as string | undefined) ?? '#d81b60'{{else}}settingsStore.getValue('brushColor') ?? '#d81b60'{{/if}};
11
+
12
+ let currentColor = $state(getCurrentColor());
13
+
14
+ const updateActive = () => {
15
+ currentColor = getCurrentColor();
16
+ };
17
+
18
+ const setColor = (color{{#if typescript}}: string{{/if}}) => {
19
+ settingsStore.setValue('brushColor', color);
20
+ };
21
+
22
+ onMount(() => {
23
+ settingsStore.addValueListener('brushColor', updateActive);
24
+ updateActive();
25
+ });
26
+ </script>
27
+
28
+ <div id="colorPicker">
29
+ {#each colors as color (color)}
30
+ <button
31
+ class="colorBtn"
32
+ class:active={currentColor === color}
33
+ style={`background: ${color}`}
34
+ onclick={() => setColor(color)}
35
+ aria-label={`Select ${color} brush`}
36
+ title={color}
37
+ ></button>
38
+ {/each}
39
+ </div>
@@ -0,0 +1,22 @@
1
+ {{includeFile template="client/src/shared/button.css.hbs" output="client/src/button.css"}}
2
+ {{includeFile template="client/src/drawing/drawingControls.css.hbs" output="client/src/drawingControls.css"}}
3
+ {{includeFile template="client/src/drawing/ColorPicker.svelte.hbs" output="client/src/ColorPicker.svelte"}}
4
+ {{includeFile template="client/src/drawing/BrushSize.svelte.hbs" output="client/src/BrushSize.svelte"}}
5
+
6
+ <script>
7
+ import './button.css';
8
+ import './drawingControls.css';
9
+ import BrushSize from './BrushSize.svelte';
10
+ import ColorPicker from './ColorPicker.svelte';
11
+ import {canvasStore} from './canvasStore';
12
+
13
+ const clearStrokes = () => {
14
+ canvasStore.delTable('strokes');
15
+ };
16
+ </script>
17
+
18
+ <div id="drawingControls">
19
+ <ColorPicker />
20
+ <BrushSize />
21
+ <button class="primary" onclick={clearStrokes}>Clear</button>
22
+ </div>
@@ -0,0 +1,23 @@
1
+ {{includeFile template="client/src/game/store.ts.hbs" output="client/src/store.ts"}}
2
+ {{includeFile template="client/src/shared/Loading.svelte.hbs" output="client/src/Loading.svelte"}}
3
+ {{includeFile template="client/src/game/Game.svelte.hbs" output="client/src/Game.svelte"}}
4
+
5
+ <script{{#if typescript}} lang="ts"{{/if}}>
6
+ import {onMount} from 'svelte';
7
+ import Game from './Game.svelte';
8
+ import Loading from './Loading.svelte';
9
+ import {storeReady} from './store';
10
+
11
+ let loading = $state(true);
12
+
13
+ onMount(async () => {
14
+ await storeReady;
15
+ loading = false;
16
+ });
17
+ </script>
18
+
19
+ {#if loading}
20
+ <Loading />
21
+ {:else}
22
+ <Game />
23
+ {/if}
@@ -0,0 +1,71 @@
1
+ {{includeFile template="client/src/game/board.css.hbs" output="client/src/board.css"}}
2
+ {{includeFile template="client/src/game/Square.svelte.hbs" output="client/src/Square.svelte"}}
3
+
4
+ <script{{#if typescript}} lang="ts"{{/if}}>
5
+ import './board.css';
6
+ import {onMount} from 'svelte';
7
+ import Square from './Square.svelte';
8
+ import {store} from './store';
9
+
10
+ const getSquares = () => {
11
+ const gameStatus =
12
+ {{#if typescript}}(store.getValue('gameStatus') as string | undefined) ?? 'playing'{{else}}store.getValue('gameStatus') ?? 'playing'{{/if}};
13
+ const winningLine =
14
+ {{#if typescript}}(store.getValue('winningLine') as string | undefined) ?? ''{{else}}store.getValue('winningLine') ?? ''{{/if}};
15
+ const currentPlayer =
16
+ {{#if typescript}}(store.getValue('currentPlayer') as string | undefined) ?? 'X'{{else}}store.getValue('currentPlayer') ?? 'X'{{/if}};
17
+ const winningPositions = new Set(
18
+ winningLine ? winningLine.split(',') : [],
19
+ );
20
+ const disabled = gameStatus !== 'playing';
21
+
22
+ return Array.from({length: 9}, (_, i) => {
23
+ const position = i.toString();
24
+ const value =
25
+ {{#if typescript}}(store.getCell('board', position, 'value') as string | undefined) ?? ''{{else}}store.getCell('board', position, 'value') ?? ''{{/if}};
26
+
27
+ return {
28
+ position,
29
+ value,
30
+ disabled: disabled || !!value,
31
+ winning: winningPositions.has(position),
32
+ currentPlayer,
33
+ };
34
+ });
35
+ };
36
+
37
+ let squares = $state(getSquares());
38
+
39
+ const updateBoard = () => {
40
+ squares = getSquares();
41
+ };
42
+
43
+ const handleSquareClick = (position{{#if typescript}}: string{{/if}}) => {
44
+ const gameStatus =
45
+ {{#if typescript}}(store.getValue('gameStatus') as string | undefined) ?? 'playing'{{else}}store.getValue('gameStatus') ?? 'playing'{{/if}};
46
+ const currentPlayer =
47
+ {{#if typescript}}(store.getValue('currentPlayer') as string | undefined) ?? 'X'{{else}}store.getValue('currentPlayer') ?? 'X'{{/if}};
48
+ const value = store.getCell('board', position, 'value');
49
+
50
+ if (gameStatus === 'playing' && !value && currentPlayer) {
51
+ store.setCell('board', position, 'value', currentPlayer);
52
+ }
53
+ };
54
+
55
+ onMount(() => {
56
+ store.addValuesListener(updateBoard);
57
+ store.addTableListener('board', updateBoard);
58
+ updateBoard();
59
+ });
60
+ </script>
61
+
62
+ <div id="board">
63
+ {#each squares as square (square.position)}
64
+ <Square
65
+ value={square.value}
66
+ disabled={square.disabled}
67
+ winning={square.winning}
68
+ handleClick={() => handleSquareClick(square.position)}
69
+ />
70
+ {/each}
71
+ </div>
@@ -0,0 +1,64 @@
1
+ {{includeFile template="client/src/shared/button.css.hbs" output="client/src/button.css"}}
2
+ {{includeFile template="client/src/game/Board.svelte.hbs" output="client/src/Board.svelte"}}
3
+ {{includeFile template="client/src/game/GameStatus.svelte.hbs" output="client/src/GameStatus.svelte"}}
4
+
5
+ <script>
6
+ import './button.css';
7
+ import {onMount} from 'svelte';
8
+ import Board from './Board.svelte';
9
+ import GameStatus from './GameStatus.svelte';
10
+ import {store} from './store';
11
+
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'],
16
+ ];
17
+
18
+ const checkGameState = () => {
19
+ const gameStatus = store.getValue('gameStatus');
20
+ if (gameStatus !== 'playing') {
21
+ return;
22
+ }
23
+
24
+ for (const line of WINNING_LINES) {
25
+ const [a, b, c] = line;
26
+ const cellA = store.getCell('board', a, 'value');
27
+ const cellB = store.getCell('board', b, 'value');
28
+ const cellC = store.getCell('board', c, 'value');
29
+
30
+ if (cellA && cellA === cellB && cellA === cellC) {
31
+ store.setValue('gameStatus', 'won');
32
+ store.setValue('winner', cellA);
33
+ store.setValue('winningLine', line.join(','));
34
+ return;
35
+ }
36
+ }
37
+
38
+ if (store.getRowCount('board') === 9) {
39
+ store.setValue('gameStatus', 'draw');
40
+ return;
41
+ }
42
+
43
+ const currentPlayer = store.getValue('currentPlayer');
44
+ store.setValue('currentPlayer', currentPlayer === 'X' ? 'O' : 'X');
45
+ };
46
+
47
+ const resetGame = () => {
48
+ store.delTable('board');
49
+ store.setValues({
50
+ currentPlayer: 'X',
51
+ gameStatus: 'playing',
52
+ });
53
+ };
54
+
55
+ onMount(() => {
56
+ store.addTableListener('board', checkGameState, true);
57
+ });
58
+ </script>
59
+
60
+ <GameStatus />
61
+ <Board />
62
+ <div style="text-align: center; margin-top: 2rem;">
63
+ <button class="primary" onclick={resetGame}>New Game</button>
64
+ </div>
@@ -0,0 +1,37 @@
1
+ {{includeFile template="client/src/game/gameStatus.css.hbs" output="client/src/gameStatus.css"}}
2
+
3
+ <script{{#if typescript}} lang="ts"{{/if}}>
4
+ import './gameStatus.css';
5
+ import {onMount} from 'svelte';
6
+ import {store} from './store';
7
+
8
+ const getStatus = () => ({
9
+ gameStatus:
10
+ {{#if typescript}}(store.getValue('gameStatus') as string | undefined) ?? 'playing'{{else}}store.getValue('gameStatus') ?? 'playing'{{/if}},
11
+ currentPlayer:
12
+ {{#if typescript}}(store.getValue('currentPlayer') as string | undefined) ?? 'X'{{else}}store.getValue('currentPlayer') ?? 'X'{{/if}},
13
+ winner:
14
+ {{#if typescript}}(store.getValue('winner') as string | undefined) ?? ''{{else}}store.getValue('winner') ?? ''{{/if}},
15
+ });
16
+
17
+ let status = $state(getStatus());
18
+
19
+ const updateStatus = () => {
20
+ status = getStatus();
21
+ };
22
+
23
+ onMount(() => {
24
+ store.addValuesListener(updateStatus);
25
+ updateStatus();
26
+ });
27
+ </script>
28
+
29
+ <div id="gameStatus">
30
+ {#if status.gameStatus === 'playing'}
31
+ Player <span class="player">{status.currentPlayer}</span>'s turn
32
+ {:else if status.gameStatus === 'won'}
33
+ Player <span class="winner">{status.winner}</span> wins!
34
+ {:else if status.gameStatus === 'draw'}
35
+ It's a draw!
36
+ {/if}
37
+ </div>
@@ -0,0 +1,25 @@
1
+ {{includeFile template="client/src/game/square.css.hbs" output="client/src/square.css"}}
2
+
3
+ <script{{#if typescript}} lang="ts"{{/if}}>
4
+ import './square.css';
5
+
6
+ let {
7
+ value,
8
+ disabled,
9
+ winning,
10
+ handleClick,
11
+ }{{#if typescript}}: {
12
+ value: string;
13
+ disabled: boolean;
14
+ winning: boolean;
15
+ handleClick: () => void;
16
+ }{{/if}} = $props();
17
+ </script>
18
+
19
+ <button
20
+ class={`square${disabled ? ' disabled' : ''}${winning ? ' winning' : ''}`}
21
+ onclick={handleClick}
22
+ disabled={disabled}
23
+ >
24
+ {value}
25
+ </button>
@@ -5,18 +5,29 @@
5
5
  }
6
6
  {{/if}}
7
7
 
8
+ {{#if vanilla}}
9
+ {{addImport "import {app} from './app';"}}
10
+ {{includeFile template="client/src/{{appType}}/app.ts.hbs" output="client/src/app.{{scriptExt}}"}}
11
+ addEventListener('load', () => {
12
+ app();
13
+ });
14
+ {{/if}}
8
15
  {{#if react}}
9
16
  {{addImport "import ReactDOM from 'react-dom/client';"}}
10
17
  {{addImport "import {App} from './App';"}}
11
- {{includeFile template="client/src/{{appType}}/App.tsx.hbs" output="client/src/App.{{ext}}"}}
18
+ {{includeFile template="client/src/{{appType}}/App.tsx.hbs" output="client/src/App.{{componentExt}}"}}
12
19
  addEventListener('load', () => {
13
20
  ReactDOM.createRoot(document.getElementById('app')!).render(
14
21
  <App />);
15
22
  });
16
- {{else}}
17
- {{addImport "import {app} from './app';"}}
18
- {{includeFile template="client/src/{{appType}}/app.ts.hbs" output="client/src/app.{{ext}}"}}
23
+ {{/if}}
24
+ {{#if svelte}}
25
+ {{addImport "import {mount} from 'svelte';"}}
26
+ {{addImport "import App from './App.svelte';"}}
27
+ {{includeFile template="client/src/{{appType}}/App.svelte.hbs" output="client/src/App.{{componentExt}}"}}
19
28
  addEventListener('load', () => {
20
- app();
29
+ mount(App, {
30
+ target: document.getElementById('app')!,
31
+ });
21
32
  });
22
33
  {{/if}}
@@ -0,0 +1,7 @@
1
+ <script>
2
+ import './loading.css';
3
+ </script>
4
+
5
+ {{includeFile template="client/src/shared/loading.css.hbs" output="client/src/loading.css"}}
6
+
7
+ <div id="loading"></div>
@@ -2,10 +2,13 @@
2
2
 
3
3
  let sqlite3Promise: Promise<Sqlite3Static> | null = null;
4
4
  let db: Database | null = null;
5
+ const initSqlite = sqlite3InitModule as (config?: {
6
+ locateFile?: () => string;
7
+ }) => Promise<Sqlite3Static>;
5
8
 
6
9
  export const getDb = async () => {
7
10
  if (!sqlite3Promise) {
8
- sqlite3Promise = sqlite3InitModule({
11
+ sqlite3Promise = initSqlite({
9
12
  locateFile: () => '/sqlite3.wasm',
10
13
  });
11
14
  }
@@ -14,4 +17,4 @@ let sqlite3Promise: Promise<Sqlite3Static> | null = null;
14
17
  db = new sqlite3.oo1.DB('file:local?vfs=kvvfs', 'c');
15
18
  }
16
19
  return {sqlite3: await sqlite3Promise, db};
17
- };
20
+ };
@@ -0,0 +1,26 @@
1
+ {{includeFile template="client/src/todos/store.ts.hbs" output="client/src/store.ts"}}
2
+ {{includeFile template="client/src/shared/Loading.svelte.hbs" output="client/src/Loading.svelte"}}
3
+ {{includeFile template="client/src/todos/TodoInput.svelte.hbs" output="client/src/TodoInput.svelte"}}
4
+ {{includeFile template="client/src/todos/TodoList.svelte.hbs" output="client/src/TodoList.svelte"}}
5
+
6
+ <script{{#if typescript}} lang="ts"{{/if}}>
7
+ import {onMount} from 'svelte';
8
+ import Loading from './Loading.svelte';
9
+ import TodoInput from './TodoInput.svelte';
10
+ import TodoList from './TodoList.svelte';
11
+ import {storeReady} from './store';
12
+
13
+ let loading = $state(true);
14
+
15
+ onMount(async () => {
16
+ await storeReady;
17
+ loading = false;
18
+ });
19
+ </script>
20
+
21
+ {#if loading}
22
+ <Loading />
23
+ {:else}
24
+ <TodoInput />
25
+ <TodoList />
26
+ {/if}