create-tinybase 0.1.5 → 0.2.1

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