create-tinybase 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +2 -2
- package/package.json +2 -2
- package/templates/{base/README.md.hbs → README.md.hbs} +1 -0
- package/templates/{base → client}/eslint.config.js.hbs +5 -0
- package/templates/client/index.html.hbs +182 -0
- package/templates/{base → client}/package.json.hbs +18 -2
- package/templates/client/public/favicon.svg +8 -0
- package/templates/client/src/chat/App.tsx.hbs +40 -0
- package/templates/client/src/chat/ChatStore.tsx.hbs +70 -0
- package/templates/client/src/chat/Message.tsx.hbs +21 -0
- package/templates/client/src/chat/MessageInput.tsx.hbs +42 -0
- package/templates/client/src/chat/Messages.tsx.hbs +29 -0
- package/templates/client/src/chat/SettingsStore.tsx.hbs +34 -0
- package/templates/client/src/chat/UsernameInput.tsx.hbs +22 -0
- package/templates/client/src/chat/app.ts.hbs +39 -0
- package/templates/client/src/chat/chatStore.ts.hbs +50 -0
- package/templates/client/src/chat/message.css.hbs +20 -0
- package/templates/client/src/chat/message.ts.hbs +27 -0
- package/templates/client/src/chat/messageInput.css.hbs +6 -0
- package/templates/client/src/chat/messageInput.ts.hbs +42 -0
- package/templates/client/src/chat/messages.css.hbs +6 -0
- package/templates/client/src/chat/messages.ts.hbs +33 -0
- package/templates/client/src/chat/settingsStore.ts.hbs +19 -0
- package/templates/client/src/chat/usernameInput.css.hbs +14 -0
- package/templates/client/src/chat/usernameInput.ts.hbs +30 -0
- package/templates/client/src/drawing/App.tsx.hbs +36 -0
- package/templates/client/src/drawing/BrushSize.tsx.hbs +22 -0
- package/templates/client/src/drawing/Canvas.tsx.hbs +100 -0
- package/templates/client/src/drawing/CanvasStore.tsx.hbs +62 -0
- package/templates/client/src/drawing/ColorPicker.tsx.hbs +24 -0
- package/templates/client/src/drawing/DrawingControls.tsx.hbs +24 -0
- package/templates/client/src/drawing/SettingsStore.tsx.hbs +36 -0
- package/templates/client/src/drawing/app.ts.hbs +20 -0
- package/templates/client/src/drawing/brushSize.css.hbs +21 -0
- package/templates/client/src/drawing/brushSize.ts.hbs +33 -0
- package/templates/client/src/drawing/canvas.css.hbs +8 -0
- package/templates/client/src/drawing/canvas.ts.hbs +103 -0
- package/templates/client/src/drawing/canvasStore.ts.hbs +42 -0
- package/templates/client/src/drawing/colorPicker.css.hbs +21 -0
- package/templates/client/src/drawing/colorPicker.ts.hbs +34 -0
- package/templates/client/src/drawing/drawingControls.css.hbs +12 -0
- package/templates/client/src/drawing/drawingControls.ts.hbs +26 -0
- package/templates/client/src/drawing/settingsStore.ts.hbs +21 -0
- package/templates/client/src/game/App.tsx.hbs +28 -0
- package/templates/client/src/game/Board.tsx.hbs +27 -0
- package/templates/client/src/game/Game.tsx.hbs +78 -0
- package/templates/client/src/game/GameStatus.tsx.hbs +21 -0
- package/templates/client/src/game/Square.tsx.hbs +23 -0
- package/templates/client/src/game/Store.tsx.hbs +67 -0
- package/templates/client/src/game/app.ts.hbs +12 -0
- package/templates/client/src/game/board.css.hbs +13 -0
- package/templates/client/src/game/board.ts.hbs +39 -0
- package/templates/client/src/game/game.ts.hbs +74 -0
- package/templates/client/src/game/gameStatus.css.hbs +21 -0
- package/templates/client/src/game/gameStatus.ts.hbs +27 -0
- package/templates/client/src/game/square.css.hbs +38 -0
- package/templates/client/src/game/square.ts.hbs +11 -0
- package/templates/client/src/game/store.ts.hbs +47 -0
- package/templates/client/src/index.tsx.hbs +24 -0
- package/templates/client/src/shared/Button.tsx.hbs +16 -0
- package/templates/client/src/shared/Input.tsx.hbs +16 -0
- package/templates/client/src/shared/button.css.hbs +25 -0
- package/templates/client/src/shared/button.ts.hbs +16 -0
- package/templates/client/src/shared/config.ts.hbs +9 -0
- package/templates/client/src/shared/input.css.hbs +22 -0
- package/templates/client/src/shared/input.ts.hbs +17 -0
- package/templates/client/src/todos/App.tsx.hbs +32 -0
- package/templates/client/src/todos/Store.tsx.hbs +70 -0
- package/templates/client/src/todos/TodoInput.tsx.hbs +30 -0
- package/templates/client/src/todos/TodoItem.tsx.hbs +20 -0
- package/templates/client/src/todos/TodoList.tsx.hbs +18 -0
- package/templates/client/src/todos/app.ts.hbs +23 -0
- package/templates/client/src/todos/store.ts.hbs +49 -0
- package/templates/client/src/todos/todoInput.css.hbs +9 -0
- package/templates/client/src/todos/todoInput.ts.hbs +38 -0
- package/templates/client/src/todos/todoItem.css.hbs +33 -0
- package/templates/client/src/todos/todoItem.ts.hbs +28 -0
- package/templates/client/src/todos/todoList.css.hbs +14 -0
- package/templates/client/src/todos/todoList.ts.hbs +38 -0
- package/templates/{base → client}/vite.config.js.hbs +5 -3
- package/templates/package.json.hbs +38 -0
- package/templates/server/index-do.ts.hbs +22 -0
- package/templates/server/index-node.ts.hbs +8 -0
- package/templates/server/package.json.hbs +51 -0
- package/templates/server/tsconfig.json.hbs +13 -0
- package/templates/server/wrangler.toml.hbs +12 -0
- package/templates/base/index.html.hbs +0 -17
- package/templates/base/tsconfig.node.json.hbs +0 -9
- package/templates/src/App.tsx.hbs +0 -24
- package/templates/src/index.css.hbs +0 -110
- package/templates/src/index.tsx.hbs +0 -48
- /package/templates/{base → client}/.prettierrc.hbs +0 -0
- /package/templates/{base → client}/tsconfig.json.hbs +0 -0
|
@@ -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,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
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{{includeFile template="client/src/todos/todoList.css.hbs" output="client/src/todoList.css"}}
|
|
2
|
+
import './todoList.css';
|
|
3
|
+
import {useRowIds, STORE_ID} from './Store';
|
|
4
|
+
|
|
5
|
+
{{includeFile template="client/src/todos/TodoItem.tsx.hbs" output="client/src/TodoItem.{{ext}}"}}
|
|
6
|
+
import {TodoItem} from './TodoItem';
|
|
7
|
+
|
|
8
|
+
export const TodoList = () => {
|
|
9
|
+
const todoIds = useRowIds('todos', STORE_ID);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div id="todoList">
|
|
13
|
+
{todoIds.map((id) => (
|
|
14
|
+
<TodoItem key={id} rowId={id} />
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{{includeFile template="client/src/todos/store.ts.hbs" output="client/src/store.ts"}}
|
|
2
|
+
import {store} from './store';
|
|
3
|
+
|
|
4
|
+
{{includeFile template="client/src/todos/todoInput.ts.hbs" output="client/src/todoInput.{{ext}}"}}
|
|
5
|
+
import {createTodoInput} from './todoInput';
|
|
6
|
+
|
|
7
|
+
{{includeFile template="client/src/todos/todoList.ts.hbs" output="client/src/todoList.{{ext}}"}}
|
|
8
|
+
import {createTodoList} from './todoList';
|
|
9
|
+
|
|
10
|
+
const app = () => {
|
|
11
|
+
const appContainer = document.getElementById('app')!;
|
|
12
|
+
|
|
13
|
+
const todoInputContainer = createTodoInput(store);
|
|
14
|
+
appContainer.appendChild(todoInputContainer);
|
|
15
|
+
|
|
16
|
+
// Focus the input after it's in the DOM
|
|
17
|
+
const input = todoInputContainer.querySelector('input')!;
|
|
18
|
+
input.focus();
|
|
19
|
+
|
|
20
|
+
appContainer.appendChild(createTodoList(store));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export {app};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{{#if schemas}}
|
|
2
|
+
import {createMergeableStore} from 'tinybase/with-schemas';
|
|
3
|
+
{{else}}
|
|
4
|
+
import {createMergeableStore} from 'tinybase';
|
|
5
|
+
{{/if}}
|
|
6
|
+
|
|
7
|
+
const STORE_ID = 'todos';
|
|
8
|
+
|
|
9
|
+
{{#if schemas}}
|
|
10
|
+
const TODOS_SCHEMA = {
|
|
11
|
+
todos: {
|
|
12
|
+
text: {type: 'string'},
|
|
13
|
+
completed: {type: 'boolean'},
|
|
14
|
+
},
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
{{/if}}
|
|
18
|
+
export const store = createMergeableStore(STORE_ID){{#if schemas}}
|
|
19
|
+
.setTablesSchema(TODOS_SCHEMA){{/if}}
|
|
20
|
+
.setDefaultContent([
|
|
21
|
+
{
|
|
22
|
+
todos: {
|
|
23
|
+
'1': {text: 'Learn TinyBase', completed: false},
|
|
24
|
+
'2': {text: 'Build an app', completed: false},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{},
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
{{#if sync}}
|
|
31
|
+
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
|
|
32
|
+
{{addImport "import {SERVER} from './config';"}}
|
|
33
|
+
{{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
|
|
34
|
+
{{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
|
|
35
|
+
|
|
36
|
+
const serverPathId = location.pathname;
|
|
37
|
+
createWsSynchronizer(
|
|
38
|
+
store,
|
|
39
|
+
new ReconnectingWebSocket(SERVER + serverPathId),
|
|
40
|
+
).then(async (synchronizer) => {
|
|
41
|
+
await synchronizer.startSync();
|
|
42
|
+
|
|
43
|
+
synchronizer.getWebSocket().addEventListener('open', () => {
|
|
44
|
+
synchronizer.load().then(() => synchronizer.save());
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
{{/if}}
|
|
48
|
+
|
|
49
|
+
export type TodosStore = typeof store;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{{includeFile template="client/src/todos/todoInput.css.hbs" output="client/src/todoInput.css"}}
|
|
2
|
+
import './todoInput.css';
|
|
3
|
+
import {type TodosStore} from './store';
|
|
4
|
+
|
|
5
|
+
{{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
|
|
6
|
+
import {createButton} from './button';
|
|
7
|
+
|
|
8
|
+
{{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
|
|
9
|
+
import {createInput} from './input';
|
|
10
|
+
|
|
11
|
+
export const createTodoInput = (store: TodosStore): HTMLDivElement => {
|
|
12
|
+
const container = document.createElement('div');
|
|
13
|
+
container.id = 'todoInput';
|
|
14
|
+
|
|
15
|
+
const input = createInput('What needs to be done?');
|
|
16
|
+
|
|
17
|
+
const addTodo = () => {
|
|
18
|
+
const text = input.value.trim();
|
|
19
|
+
if (text) {
|
|
20
|
+
store.addRow('todos', {text, completed: false});
|
|
21
|
+
input.value = '';
|
|
22
|
+
input.focus();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
input.addEventListener('keypress', (e) => {
|
|
27
|
+
if (e.key === 'Enter') {
|
|
28
|
+
addTodo();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const addButton = createButton('Add', addTodo, 'primary');
|
|
33
|
+
|
|
34
|
+
container.appendChild(input);
|
|
35
|
+
container.appendChild(addButton);
|
|
36
|
+
|
|
37
|
+
return container;
|
|
38
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.todoItem {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.75rem;
|
|
5
|
+
padding: 0.75rem;
|
|
6
|
+
border-bottom: 1px solid var(--border);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.todoItem:last-child {
|
|
10
|
+
border-bottom: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.todoItem input[type="checkbox"] {
|
|
14
|
+
width: 1.25rem;
|
|
15
|
+
height: 1.25rem;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
flex-shrink: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.todoItem label {
|
|
21
|
+
flex: 1;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
user-select: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.todoItem.completed label {
|
|
27
|
+
text-decoration: line-through;
|
|
28
|
+
opacity: 0.6;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.todoItem button {
|
|
32
|
+
flex-shrink: 0;
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{{includeFile template="client/src/todos/todoItem.css.hbs" output="client/src/todoItem.css"}}
|
|
2
|
+
import './todoItem.css';
|
|
3
|
+
|
|
4
|
+
{{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
|
|
5
|
+
import {createButton} from './button';
|
|
6
|
+
|
|
7
|
+
export const createTodoItem = (id: string, text: string, completed: boolean, onToggle: () => void, onDelete: () => void): HTMLDivElement => {
|
|
8
|
+
const item = document.createElement('div');
|
|
9
|
+
item.className = `todoItem${completed ? ' completed' : ''}`;
|
|
10
|
+
|
|
11
|
+
const checkbox = document.createElement('input');
|
|
12
|
+
checkbox.type = 'checkbox';
|
|
13
|
+
checkbox.checked = completed;
|
|
14
|
+
checkbox.id = `todo-${id}`;
|
|
15
|
+
checkbox.addEventListener('change', onToggle);
|
|
16
|
+
|
|
17
|
+
const label = document.createElement('label');
|
|
18
|
+
label.textContent = text;
|
|
19
|
+
label.htmlFor = `todo-${id}`;
|
|
20
|
+
|
|
21
|
+
const deleteBtn = createButton('Delete', onDelete);
|
|
22
|
+
|
|
23
|
+
item.appendChild(checkbox);
|
|
24
|
+
item.appendChild(label);
|
|
25
|
+
item.appendChild(deleteBtn);
|
|
26
|
+
|
|
27
|
+
return item;
|
|
28
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#todoList {
|
|
2
|
+
border: 1px solid var(--border);
|
|
3
|
+
border-radius: 0.375rem;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
background: var(--bg2);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#todoList:empty::before {
|
|
9
|
+
content: 'No todos yet. Add one above!';
|
|
10
|
+
display: block;
|
|
11
|
+
padding: 2rem;
|
|
12
|
+
text-align: center;
|
|
13
|
+
color: var(--fg2);
|
|
14
|
+
}
|