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.
Files changed (93) hide show
  1. package/cli.js +2 -2
  2. package/package.json +2 -2
  3. package/templates/{base/README.md.hbs → README.md.hbs} +1 -0
  4. package/templates/{base → client}/eslint.config.js.hbs +5 -0
  5. package/templates/client/index.html.hbs +182 -0
  6. package/templates/{base → client}/package.json.hbs +18 -2
  7. package/templates/client/public/favicon.svg +8 -0
  8. package/templates/client/src/chat/App.tsx.hbs +40 -0
  9. package/templates/client/src/chat/ChatStore.tsx.hbs +70 -0
  10. package/templates/client/src/chat/Message.tsx.hbs +21 -0
  11. package/templates/client/src/chat/MessageInput.tsx.hbs +42 -0
  12. package/templates/client/src/chat/Messages.tsx.hbs +29 -0
  13. package/templates/client/src/chat/SettingsStore.tsx.hbs +34 -0
  14. package/templates/client/src/chat/UsernameInput.tsx.hbs +22 -0
  15. package/templates/client/src/chat/app.ts.hbs +39 -0
  16. package/templates/client/src/chat/chatStore.ts.hbs +50 -0
  17. package/templates/client/src/chat/message.css.hbs +20 -0
  18. package/templates/client/src/chat/message.ts.hbs +27 -0
  19. package/templates/client/src/chat/messageInput.css.hbs +6 -0
  20. package/templates/client/src/chat/messageInput.ts.hbs +42 -0
  21. package/templates/client/src/chat/messages.css.hbs +6 -0
  22. package/templates/client/src/chat/messages.ts.hbs +33 -0
  23. package/templates/client/src/chat/settingsStore.ts.hbs +19 -0
  24. package/templates/client/src/chat/usernameInput.css.hbs +14 -0
  25. package/templates/client/src/chat/usernameInput.ts.hbs +30 -0
  26. package/templates/client/src/drawing/App.tsx.hbs +36 -0
  27. package/templates/client/src/drawing/BrushSize.tsx.hbs +22 -0
  28. package/templates/client/src/drawing/Canvas.tsx.hbs +100 -0
  29. package/templates/client/src/drawing/CanvasStore.tsx.hbs +62 -0
  30. package/templates/client/src/drawing/ColorPicker.tsx.hbs +24 -0
  31. package/templates/client/src/drawing/DrawingControls.tsx.hbs +24 -0
  32. package/templates/client/src/drawing/SettingsStore.tsx.hbs +36 -0
  33. package/templates/client/src/drawing/app.ts.hbs +20 -0
  34. package/templates/client/src/drawing/brushSize.css.hbs +21 -0
  35. package/templates/client/src/drawing/brushSize.ts.hbs +33 -0
  36. package/templates/client/src/drawing/canvas.css.hbs +8 -0
  37. package/templates/client/src/drawing/canvas.ts.hbs +103 -0
  38. package/templates/client/src/drawing/canvasStore.ts.hbs +42 -0
  39. package/templates/client/src/drawing/colorPicker.css.hbs +21 -0
  40. package/templates/client/src/drawing/colorPicker.ts.hbs +34 -0
  41. package/templates/client/src/drawing/drawingControls.css.hbs +12 -0
  42. package/templates/client/src/drawing/drawingControls.ts.hbs +26 -0
  43. package/templates/client/src/drawing/settingsStore.ts.hbs +21 -0
  44. package/templates/client/src/game/App.tsx.hbs +28 -0
  45. package/templates/client/src/game/Board.tsx.hbs +27 -0
  46. package/templates/client/src/game/Game.tsx.hbs +78 -0
  47. package/templates/client/src/game/GameStatus.tsx.hbs +21 -0
  48. package/templates/client/src/game/Square.tsx.hbs +23 -0
  49. package/templates/client/src/game/Store.tsx.hbs +67 -0
  50. package/templates/client/src/game/app.ts.hbs +12 -0
  51. package/templates/client/src/game/board.css.hbs +13 -0
  52. package/templates/client/src/game/board.ts.hbs +39 -0
  53. package/templates/client/src/game/game.ts.hbs +74 -0
  54. package/templates/client/src/game/gameStatus.css.hbs +21 -0
  55. package/templates/client/src/game/gameStatus.ts.hbs +27 -0
  56. package/templates/client/src/game/square.css.hbs +38 -0
  57. package/templates/client/src/game/square.ts.hbs +11 -0
  58. package/templates/client/src/game/store.ts.hbs +47 -0
  59. package/templates/client/src/index.tsx.hbs +24 -0
  60. package/templates/client/src/shared/Button.tsx.hbs +16 -0
  61. package/templates/client/src/shared/Input.tsx.hbs +16 -0
  62. package/templates/client/src/shared/button.css.hbs +25 -0
  63. package/templates/client/src/shared/button.ts.hbs +16 -0
  64. package/templates/client/src/shared/config.ts.hbs +9 -0
  65. package/templates/client/src/shared/input.css.hbs +22 -0
  66. package/templates/client/src/shared/input.ts.hbs +17 -0
  67. package/templates/client/src/todos/App.tsx.hbs +32 -0
  68. package/templates/client/src/todos/Store.tsx.hbs +70 -0
  69. package/templates/client/src/todos/TodoInput.tsx.hbs +30 -0
  70. package/templates/client/src/todos/TodoItem.tsx.hbs +20 -0
  71. package/templates/client/src/todos/TodoList.tsx.hbs +18 -0
  72. package/templates/client/src/todos/app.ts.hbs +23 -0
  73. package/templates/client/src/todos/store.ts.hbs +49 -0
  74. package/templates/client/src/todos/todoInput.css.hbs +9 -0
  75. package/templates/client/src/todos/todoInput.ts.hbs +38 -0
  76. package/templates/client/src/todos/todoItem.css.hbs +33 -0
  77. package/templates/client/src/todos/todoItem.ts.hbs +28 -0
  78. package/templates/client/src/todos/todoList.css.hbs +14 -0
  79. package/templates/client/src/todos/todoList.ts.hbs +38 -0
  80. package/templates/{base → client}/vite.config.js.hbs +5 -3
  81. package/templates/package.json.hbs +38 -0
  82. package/templates/server/index-do.ts.hbs +22 -0
  83. package/templates/server/index-node.ts.hbs +8 -0
  84. package/templates/server/package.json.hbs +51 -0
  85. package/templates/server/tsconfig.json.hbs +13 -0
  86. package/templates/server/wrangler.toml.hbs +12 -0
  87. package/templates/base/index.html.hbs +0 -17
  88. package/templates/base/tsconfig.node.json.hbs +0 -9
  89. package/templates/src/App.tsx.hbs +0 -24
  90. package/templates/src/index.css.hbs +0 -110
  91. package/templates/src/index.tsx.hbs +0 -48
  92. /package/templates/{base → client}/.prettierrc.hbs +0 -0
  93. /package/templates/{base → client}/tsconfig.json.hbs +0 -0
package/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{dirname as c,join as p}from"path";import{createCLI as l}from"tinycreate";import{fileURLToPath as u}from"url";const m=c(u(import.meta.url)),g={welcomeMessage:`\u{1F389} Welcome to TinyBase!
3
- `,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinybase-app",validate:e=>e.length>0?!0:"Project name is required"},{type:"select",name:"language",message:"Language:",choices:[{title:"TypeScript",value:"typescript"},{title:"JavaScript",value:"javascript"}],initial:0},{type:"select",name:"framework",message:"Framework:",choices:[{title:"React",value:"react"},{title:"Vanilla",value:"vanilla"}],initial:0},{type:"confirm",name:"prettier",message:"Include Prettier?",initial:!1},{type:"confirm",name:"eslint",message:"Include ESLint?",initial:!1}],createContext:e=>{const{projectName:r,language:s,framework:i,prettier:n,eslint:a}=e,t=s==="typescript",o=i==="react";return{projectName:r,language:s,framework:i,prettier:n,eslint:a,typescript:t,react:o,ext:t?o?"tsx":"ts":o?"jsx":"js"}},createDirectories:async e=>{const{mkdir:r}=await import("fs/promises"),{join:s}=await import("path");await r(s(e,"src"),{recursive:!0}),await r(s(e,"public"),{recursive:!0})},getFiles:e=>{const{typescript:r,react:s,ext:i,prettier:n,eslint:a}=e,t=[{template:"base/package.json.hbs",output:"package.json",prettier:!0},{template:"base/index.html.hbs",output:"index.html",prettier:!0},{template:"base/README.md.hbs",output:"README.md",prettier:!0},{template:"src/index.css.hbs",output:"src/index.css",prettier:!0},{template:"src/index.tsx.hbs",output:`src/index.${i}`,prettier:!0,transpile:!0}];return n&&t.push({template:"base/.prettierrc.hbs",output:".prettierrc",prettier:!0}),a&&t.push({template:"base/eslint.config.js.hbs",output:"eslint.config.js",prettier:!0}),s&&t.push({template:"src/App.tsx.hbs",output:`src/App.${i}`,prettier:!0,transpile:!0},{template:"base/vite.config.js.hbs",output:"vite.config.js",prettier:!0}),r&&t.push({template:"base/tsconfig.json.hbs",output:"tsconfig.json",prettier:!0},{template:"base/tsconfig.node.json.hbs",output:"tsconfig.node.json",prettier:!0}),t},templateRoot:p(m,"templates"),onSuccess:e=>{console.log("Next steps:"),console.log(` cd ${e}`),console.log(" npm install"),console.log(" npm run dev")}};l(g).catch(e=>{console.error(e),process.exit(1)});
2
+ import{existsSync as v}from"fs";import{dirname as y,join as p}from"path";import{createCLI as f}from"tinycreate";import{fileURLToPath as h}from"url";const w=y(h(import.meta.url)),j={welcomeMessage:`\u{1F389} Welcome to TinyBase!
3
+ `,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinybase-app",validate:e=>{if(e.length===0)return"Project name is required";const t=p(process.cwd(),e);return v(t)?`Directory "${e}" already exists. Please choose a different name.`:!0}},{type:"select",name:"language",message:"Language:",choices:[{title:"TypeScript",value:"typescript"},{title:"JavaScript",value:"javascript"}],initial:0},{type:"select",name:"framework",message:"Framework:",choices:[{title:"React",value:"react"},{title:"Vanilla",value:"vanilla"}],initial:0},{type:"select",name:"appType",message:"App type:",choices:[{title:"Todo app",value:"todos"},{title:"Chat app",value:"chat"},{title:"Drawing app",value:"drawing"},{title:"Tic-tac-toe game",value:"game"}],initial:0},{type:(e,t)=>t.language==="typescript"?"confirm":null,name:"schemas",message:"Include store schemas?",initial:!1},{type:"confirm",name:"sync",message:"Enable synchronization?",initial:!0},{type:(e,t)=>t.sync?"confirm":null,name:"server",message:"Add code for server?",initial:!1},{type:(e,t)=>t.server?"select":null,name:"serverType",message:"Server type:",choices:[{title:"Node",value:"node"},{title:"Durable Objects",value:"durable-objects"}],initial:0},{type:"confirm",name:"prettier",message:"Include Prettier?",initial:!1},{type:"confirm",name:"eslint",message:"Include ESLint?",initial:!1}],createContext:e=>{const{projectName:t,language:s,framework:n,appType:r,prettier:m,eslint:u,schemas:o,sync:c,server:l,serverType:g}=e,a=s==="typescript",d=!a,i=n==="react";return{projectName:t,language:s,framework:n,appType:r,prettier:m,eslint:u,schemas:a&&(o===!0||o==="true"),sync:c===!0||c==="true",server:l===!0||l==="true",serverType:g||"node",typescript:a,javascript:d,react:i,ext:a?i?"tsx":"ts":i?"jsx":"js"}},createDirectories:async(e,t)=>{const{mkdir:s}=await import("fs/promises"),{join:n}=await import("path"),r=t.server;await s(n(e,"client/src"),{recursive:!0}),await s(n(e,"client/public"),{recursive:!0}),r&&await s(n(e,"server"),{recursive:!0})},getFiles:e=>{const t=e.server,s=[{template:"README.md.hbs",output:"README.md",prettier:!0},{template:"client/package.json.hbs",output:"client/package.json",prettier:!0}];return t&&s.push({template:"package.json.hbs",output:"package.json",prettier:!0}),s},processIncludedFile:(e,t)=>{const{javascript:s}=t,n=e.prettier??/\.(js|jsx|ts|tsx|css|json|html|md)$/.test(e.output),r=e.transpile??(/\.(ts|tsx)\.hbs$/.test(e.template)&&s===!0);return{...e,prettier:n,transpile:r}},templateRoot:p(w,"templates"),installCommand:"{pm} install",devCommand:"{pm} run dev",onSuccess:e=>{console.log("Next steps:"),console.log(` cd ${e}/client`),console.log(" npm install"),console.log(" npm run dev")}};f(j).catch(e=>{console.error(e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-tinybase",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "author": "jamesgpearce",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,7 +23,7 @@
23
23
  "create-tinybase": "cli.js"
24
24
  },
25
25
  "dependencies": {
26
- "tinycreate": "^0.1.0"
26
+ "tinycreate": "^1.0.0"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=18.0.0"
@@ -5,6 +5,7 @@ A TinyBase app built with {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}
5
5
  ## Getting Started
6
6
 
7
7
  ```bash
8
+ cd client
8
9
  npm install
9
10
  npm run dev
10
11
  ```
@@ -9,6 +9,11 @@
9
9
  export default [
10
10
  {{#if typescript}}
11
11
  ...tseslint.configs.recommended,
12
+ {
13
+ rules: {
14
+ '@typescript-eslint/no-unused-vars': 'warn',
15
+ },
16
+ },
12
17
  {{else}}
13
18
  {
14
19
  rules: {
@@ -0,0 +1,182 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;800&display=swap" />
10
+ {{includeFile template="client/public/favicon.svg" output="client/public/favicon.svg"}}
11
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
12
+ <title>
13
+ TinyBase {{#if (eq appType "chat")}}Chat{{else if (eq appType "drawing")}}Drawing{{else if (eq appType "game")}}Game{{else}}Todos{{/if}}
14
+ </title>
15
+ <style>
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ body {
21
+ --hue: 270;
22
+ --fg: oklch(85% 0.01 var(--hue));
23
+ --fg2: oklch(60% 0.01 var(--hue));
24
+ --bg: oklch(20% 0.01 var(--hue));
25
+ --bg2: oklch(25% 0.01 var(--hue));
26
+ --border: oklch(30% 0.01 var(--hue));
27
+ --bg-header: oklch(30% 0.008 var(--hue) / 0.5);
28
+ --accent: #d81b60;
29
+ color: var(--fg);
30
+ background: var(--bg);
31
+ font-family: 'Inter', sans-serif;
32
+ user-select: none;
33
+ margin: 0;
34
+ padding: 0;
35
+ }
36
+
37
+ #appContainer {
38
+ display: flex;
39
+ flex-direction: column;
40
+ min-height: 100vh;
41
+ }
42
+
43
+ #topBar {
44
+ background: var(--bg-header);
45
+ border-bottom: 1px solid var(--border);
46
+ box-shadow: 0 1px 2px 0 #0007;
47
+ padding: 0.75rem 1.5rem;
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 1rem;
51
+ position: sticky;
52
+ top: 0;
53
+ z-index: 100;
54
+ backdrop-filter: blur(4px);
55
+ }
56
+
57
+ #topBarLogo {
58
+ height: 2rem;
59
+ width: 2rem;
60
+ flex-shrink: 0;
61
+ }
62
+
63
+ #topBarTitle {
64
+ font-size: 1.1rem;
65
+ font-weight: 600;
66
+ color: #fff;
67
+ flex: 1;
68
+ }
69
+
70
+ #topBarInfo {
71
+ position: relative;
72
+ }
73
+
74
+ #infoIcon {
75
+ width: 1.25rem;
76
+ height: 1.25rem;
77
+ border: 2px solid var(--fg2);
78
+ border-radius: 50%;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ font-size: 0.75rem;
83
+ font-weight: 800;
84
+ color: var(--fg2);
85
+ cursor: help;
86
+ transition: all 0.2s;
87
+ user-select: none;
88
+ }
89
+
90
+ #infoIcon:hover {
91
+ border-color: var(--accent);
92
+ color: var(--accent);
93
+ }
94
+
95
+ #infoIcon:hover #infoTooltip {
96
+ opacity: 1;
97
+ visibility: visible;
98
+ transform: translateY(0);
99
+ }
100
+
101
+ #infoTooltip {
102
+ position: absolute;
103
+ right: 0;
104
+ top: calc(100% + 0.5rem);
105
+ background: var(--bg2);
106
+ border: 1px solid var(--border);
107
+ border-radius: 0.375rem;
108
+ padding: 0.75rem 1rem;
109
+ font-size: 0.85rem;
110
+ font-weight: 400;
111
+ width: 20rem;
112
+ white-space: normal;
113
+ color: var(--fg);
114
+ line-height: 1.5;
115
+ opacity: 0;
116
+ visibility: hidden;
117
+ transform: translateY(-0.25rem);
118
+ transition: all 0.2s;
119
+ pointer-events: none;
120
+ box-shadow: 0 1px 2px 0 #0007;
121
+ }
122
+
123
+ #infoTooltip a {
124
+ color: var(--fg);
125
+ text-decoration: underline;
126
+ }
127
+
128
+ #infoTooltip::before {
129
+ content: '';
130
+ position: absolute;
131
+ right: 0.75rem;
132
+ top: -0.375rem;
133
+ width: 0.75rem;
134
+ height: 0.75rem;
135
+ background: var(--bg2);
136
+ border-left: 1px solid var(--border);
137
+ border-top: 1px solid var(--border);
138
+ transform: rotate(45deg);
139
+ }
140
+
141
+ #app {
142
+ max-width: 60rem;
143
+ width: 100%;
144
+ margin: 0 auto;
145
+ padding: 2rem 1.5rem;
146
+ }
147
+ </style>
148
+ </head>
149
+
150
+ <body>
151
+ <div id="appContainer">
152
+ <div id="topBar">
153
+ <img src="/favicon.svg" id="topBarLogo" alt="TinyBase" />
154
+ <div id="topBarTitle">
155
+ TinyBase {{#if (eq appType "chat")}}Chat{{else if (eq appType "drawing")}}Drawing{{else if (eq appType "game")}}Game{{else}}Todos{{/if}}
156
+ </div>
157
+ <div id="topBarInfo">
158
+ <div id="infoIcon">
159
+ i
160
+ <div id="infoTooltip">
161
+ {{#if (eq appType "todos")}}
162
+ A simple todo list application demonstrating TinyBase's reactive data management with CRUD operations.
163
+ {{else if (eq appType "chat")}}
164
+ A real-time chat application demonstrating TinyBase's reactive data management.
165
+ {{else if (eq appType "drawing")}}
166
+ A collaborative drawing canvas showcasing TinyBase's state synchronization.
167
+ {{else if (eq appType "game")}}
168
+ A tic-tac-toe game demonstrating turn-based logic and computed game state.
169
+ {{/if}}
170
+ <br><br>
171
+ Built with {{#if typescript}}TypeScript{{#if schemas}} (using typed store schemas){{/if}}{{else}}JavaScript{{/if}}{{#if react}} and React{{/if}}.
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ <div id="app"></div>
177
+ </div>
178
+ {{includeFile template="client/src/index.tsx.hbs" output="client/src/index.{{ext}}"}}
179
+ <script type="module" src="/src/index.{{ext}}"></script>
180
+ </body>
181
+
182
+ </html>
@@ -1,8 +1,9 @@
1
1
  {
2
- "name": "{{projectName}}",
2
+ "name": "{{projectName}}{{#if server}}-client{{/if}}",
3
3
  "version": "1.0.0",
4
4
  "scripts": {
5
5
  {{#list}}
6
+ {{includeFile template="client/index.html.hbs" output="client/index.html"}}
6
7
  "dev": "vite"
7
8
  {{#if typescript}}
8
9
  "build": "tsc && vite build"
@@ -55,6 +56,21 @@
55
56
  "react": "^19.2.3"
56
57
  "react-dom": "^19.2.3"
57
58
  {{/if}}
59
+ {{#if sync}}
60
+ "reconnecting-websocket": "^4.4.0"
61
+ {{/if}}
58
62
  {{/list}}
59
63
  }
60
- }
64
+ }
65
+ {{#if prettier}}
66
+ {{includeFile template="client/.prettierrc.hbs" output="client/.prettierrc"}}
67
+ {{/if}}
68
+ {{#if eslint}}
69
+ {{includeFile template="client/eslint.config.js.hbs" output="client/eslint.config.js"}}
70
+ {{/if}}
71
+ {{#if react}}
72
+ {{includeFile template="client/vite.config.js.hbs" output="client/vite.config.js"}}
73
+ {{/if}}
74
+ {{#if typescript}}
75
+ {{includeFile template="client/tsconfig.json.hbs" output="client/tsconfig.json"}}
76
+ {{/if}}
@@ -0,0 +1,8 @@
1
+ <svg viewBox="0 0 680 680" xmlns="http://www.w3.org/2000/svg" style="width:680px;height:680px">
2
+ <path stroke="#fff" stroke-width="80" fill="none"
3
+ d="M340 617a84 241 90 11.01 0zM131 475a94 254 70 10428-124 114 286 70 01-428 124zm0-140a94 254 70 10428-124 114 286 70 01-428 124zm-12-127a94 254 70 00306 38 90 260 90 01-306-38zm221 3a74 241 90 11.01 0z" />
4
+ <path fill="#d81b60"
5
+ d="M131 475a94 254 70 10428-124 114 286 70 01-428 124zm0-140a94 254 70 10428-124 114 286 70 01-428 124z" />
6
+ <path
7
+ d="M249 619a94 240 90 00308-128 114 289 70 01-308 128zM119 208a94 254 70 00306 38 90 260 90 01-306-38zm221 3a74 241 90 11.01 0z" />
8
+ </svg>
@@ -0,0 +1,40 @@
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/chat/SettingsStore.tsx.hbs" output="client/src/SettingsStore.{{ext}}"}}
11
+ import {SettingsStore} from './SettingsStore';
12
+
13
+ {{includeFile template="client/src/chat/ChatStore.tsx.hbs" output="client/src/ChatStore.{{ext}}"}}
14
+ import {ChatStore} from './ChatStore';
15
+
16
+ {{includeFile template="client/src/chat/UsernameInput.tsx.hbs" output="client/src/UsernameInput.{{ext}}"}}
17
+ import {UsernameInput} from './UsernameInput';
18
+
19
+ {{includeFile template="client/src/chat/Messages.tsx.hbs" output="client/src/Messages.{{ext}}"}}
20
+ import {Messages} from './Messages';
21
+
22
+ {{includeFile template="client/src/chat/MessageInput.tsx.hbs" output="client/src/MessageInput.{{ext}}"}}
23
+ import {MessageInput} from './MessageInput';
24
+
25
+ const App = () => {
26
+ return (
27
+ <StrictMode>
28
+ <Provider>
29
+ <SettingsStore />
30
+ <ChatStore />
31
+ <UsernameInput />
32
+ <Messages />
33
+ <MessageInput />
34
+ <Inspector />
35
+ </Provider>
36
+ </StrictMode>
37
+ );
38
+ };
39
+
40
+ 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, useRow, useRowIds, useStore} from 'tinybase/ui-react';
8
+ {{/if}}
9
+
10
+ export const STORE_ID = 'chat';
11
+
12
+ {{#if schemas}}
13
+ const TABLES_SCHEMA = {
14
+ messages: {
15
+ username: {type: 'string'},
16
+ text: {type: 'string'},
17
+ timestamp: {type: 'number'},
18
+ },
19
+ } as const;
20
+
21
+ type Schemas = [typeof TABLES_SCHEMA, NoValuesSchema];
22
+
23
+ const {useCreateStore, useProvideStore, useAddRowCallback, useRow, useRowIds, useStore} = UiReact as UiReact.WithSchemas<Schemas>;
24
+ {{/if}}
25
+
26
+ export {useAddRowCallback, useRow, useRowIds, useStore};
27
+
28
+ export const ChatStore = () => {
29
+ const store = useCreateStore(() =>
30
+ createMergeableStore(STORE_ID){{#if schemas}}
31
+ .setTablesSchema(TABLES_SCHEMA){{/if}}
32
+ .setDefaultContent([
33
+ {
34
+ messages: {
35
+ '1': {username: 'Alice', text: 'Hello!', timestamp: Date.now() - 60000},
36
+ '2': {username: 'Bob', text: 'Hi there!', timestamp: Date.now() - 30000},
37
+ },
38
+ },
39
+ {},
40
+ ]),
41
+ );
42
+
43
+ useProvideStore(STORE_ID, store);
44
+
45
+ {{#if sync}}
46
+ {{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
47
+ {{addImport "import {SERVER} from './config';"}}
48
+ {{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
49
+ {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
50
+ {{addImport "import {useCreateSynchronizer} from 'tinybase/ui-react';"}}
51
+ {{addImport "import type {MergeableStore} from 'tinybase';"}}
52
+
53
+ useCreateSynchronizer(store, async (store: MergeableStore) => {
54
+ const serverPathId = location.pathname;
55
+ const synchronizer = await createWsSynchronizer(
56
+ store,
57
+ new ReconnectingWebSocket(SERVER + serverPathId),
58
+ );
59
+ await synchronizer.startSync();
60
+
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,21 @@
1
+ import {useRow, STORE_ID} from './ChatStore';
2
+
3
+ {{includeFile template="client/src/chat/message.css.hbs" output="client/src/message.css"}}
4
+ import './message.css';
5
+
6
+ interface MessageProps {
7
+ rowId: string;
8
+ }
9
+
10
+ export const Message = ({rowId}: MessageProps) => {
11
+ const row = useRow('messages', rowId, 'chat');
12
+ const time = new Date((row as any).timestamp).toLocaleTimeString();
13
+
14
+ return (
15
+ <div className="message">
16
+ <span className="username">{(row as any).username}:</span>
17
+ <span className="text">{(row as any).text}</span>
18
+ <span className="time">{time}</span>
19
+ </div>
20
+ );
21
+ };
@@ -0,0 +1,42 @@
1
+ {{includeFile template="client/src/chat/messageInput.css.hbs" output="client/src/messageInput.css"}}
2
+ import {useState} from 'react';
3
+ import {useAddRowCallback, STORE_ID as CHAT_STORE_ID} from './ChatStore';
4
+ import {useValue, STORE_ID as SETTINGS_STORE_ID} from './SettingsStore';
5
+ import './messageInput.css';
6
+
7
+ {{includeFile template="client/src/shared/Button.tsx.hbs" output="client/src/Button.{{ext}}"}}
8
+ import {Button} from './Button';
9
+
10
+ {{includeFile template="client/src/shared/Input.tsx.hbs" output="client/src/Input.{{ext}}"}}
11
+ import {Input} from './Input';
12
+
13
+ export const MessageInput = () => {
14
+ const [message, setMessage] = useState('');
15
+ const username = useValue('username', SETTINGS_STORE_ID) || 'Anonymous';
16
+
17
+ const handleSend = useAddRowCallback(
18
+ 'messages',
19
+ () => ({
20
+ username,
21
+ text: message,
22
+ timestamp: Date.now(),
23
+ }),
24
+ [message, username],
25
+ CHAT_STORE_ID,
26
+ );
27
+
28
+ const onSubmit = (e: React.FormEvent) => {
29
+ e.preventDefault();
30
+ if (message.trim()) {
31
+ handleSend();
32
+ setMessage('');
33
+ }
34
+ };
35
+
36
+ return (
37
+ <form onSubmit={onSubmit} id="messageInput">
38
+ <Input value={message} onChange={setMessage} placeholder="Type a message..." autoFocus />
39
+ <Button type="submit" variant="primary">Send</Button>
40
+ </form>
41
+ );
42
+ };
@@ -0,0 +1,29 @@
1
+ import {useMemo} from 'react';
2
+ import {useRowIds, useStore, STORE_ID} from './ChatStore';
3
+
4
+ {{includeFile template="client/src/chat/messages.css.hbs" output="client/src/messages.css"}}
5
+ import './messages.css';
6
+
7
+ {{includeFile template="client/src/chat/Message.tsx.hbs" output="client/src/Message.{{ext}}"}}
8
+ import {Message} from './Message';
9
+
10
+ export const Messages = () => {
11
+ const store = useStore(STORE_ID);
12
+ const messageIds = useRowIds('messages', STORE_ID);
13
+
14
+ const sortedIds = useMemo(() => {
15
+ return [...messageIds].sort((a, b) => {
16
+ const rowA = store.getRow('messages', a) as any;
17
+ const rowB = store.getRow('messages', b) as any;
18
+ return rowA.timestamp - rowB.timestamp;
19
+ });
20
+ }, [messageIds, store]);
21
+
22
+ return (
23
+ <div id="messages">
24
+ {sortedIds.map((id) => (
25
+ <Message key={id} rowId={id} />
26
+ ))}
27
+ </div>
28
+ );
29
+ };
@@ -0,0 +1,34 @@
1
+ {{#if schemas}}
2
+ import {createStore} 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 {createStore} from 'tinybase';
7
+ import {useCreateStore, useProvideStore, useValue, useSetValueCallback} from 'tinybase/ui-react';
8
+ {{/if}}
9
+
10
+ export const STORE_ID = 'settings';
11
+
12
+ {{#if schemas}}
13
+ const VALUES_SCHEMA = {
14
+ username: {type: 'string'},
15
+ } as const;
16
+
17
+ type Schemas = [NoTablesSchema, typeof VALUES_SCHEMA];
18
+
19
+ const {useCreateStore, useProvideStore, useValue, useSetValueCallback} = UiReact as UiReact.WithSchemas<Schemas>;
20
+ {{/if}}
21
+
22
+ export {useValue, useSetValueCallback};
23
+
24
+ export const SettingsStore = () => {
25
+ const store = useCreateStore(() =>
26
+ createStore(){{#if schemas}}
27
+ .setValuesSchema(VALUES_SCHEMA){{/if}}
28
+ .setValue('username', 'Carol'),
29
+ );
30
+
31
+ useProvideStore(STORE_ID, store);
32
+
33
+ return null;
34
+ };
@@ -0,0 +1,22 @@
1
+ {{includeFile template="client/src/chat/usernameInput.css.hbs" output="client/src/usernameInput.css"}}
2
+ import {useValue, useSetValueCallback, STORE_ID} from './SettingsStore';
3
+ import './usernameInput.css';
4
+
5
+ {{includeFile template="client/src/shared/Input.tsx.hbs" output="client/src/Input.{{ext}}"}}
6
+ import {Input} from './Input';
7
+
8
+ export const UsernameInput = () => {
9
+ const username = useValue('username', STORE_ID) || '';
10
+ const setUsername = useSetValueCallback(
11
+ 'username',
12
+ (_e) => _e,[],
13
+ STORE_ID,
14
+ );
15
+
16
+ return (
17
+ <div id="usernameInput">
18
+ <label>Your name:</label>
19
+ <Input value={username} onChange={setUsername} placeholder="Enter your name" />
20
+ </div>
21
+ );
22
+ };
@@ -0,0 +1,39 @@
1
+ {{includeFile template="client/src/chat/settingsStore.ts.hbs" output="client/src/settingsStore.ts"}}
2
+ import {settingsStore} from './settingsStore';
3
+
4
+ {{includeFile template="client/src/chat/chatStore.ts.hbs" output="client/src/chatStore.ts"}}
5
+ import {chatStore} from './chatStore';
6
+
7
+ {{includeFile template="client/src/shared/button.ts.hbs" output="client/src/button.{{ext}}"}}
8
+ import {createButton} from './button';
9
+
10
+ {{includeFile template="client/src/shared/input.ts.hbs" output="client/src/input.{{ext}}"}}
11
+ import {createInput} from './input';
12
+
13
+ {{includeFile template="client/src/chat/message.ts.hbs" output="client/src/message.{{ext}}"}}
14
+ import {createMessage} from './message';
15
+
16
+ {{includeFile template="client/src/chat/messages.ts.hbs" output="client/src/messages.{{ext}}"}}
17
+ import {createMessages} from './messages';
18
+
19
+ {{includeFile template="client/src/chat/usernameInput.ts.hbs" output="client/src/usernameInput.{{ext}}"}}
20
+ import {createUsernameInput} from './usernameInput';
21
+
22
+ {{includeFile template="client/src/chat/messageInput.ts.hbs" output="client/src/messageInput.{{ext}}"}}
23
+ import {createMessageInput} from './messageInput';
24
+
25
+ const app = () => {
26
+ const appContainer = document.getElementById('app')!;
27
+
28
+ appContainer.appendChild(createUsernameInput(settingsStore, chatStore));
29
+ appContainer.appendChild(createMessages(chatStore));
30
+
31
+ const messageInputContainer = createMessageInput(settingsStore, chatStore);
32
+ appContainer.appendChild(messageInputContainer);
33
+
34
+ // Focus the input after it's in the DOM
35
+ const messageInput = messageInputContainer.querySelector('input')!;
36
+ messageInput.focus();
37
+ };
38
+
39
+ export {app};
@@ -0,0 +1,50 @@
1
+ {{#if schemas}}
2
+ import {createMergeableStore} from 'tinybase/with-schemas';
3
+ {{else}}
4
+ import {createMergeableStore} from 'tinybase';
5
+ {{/if}}
6
+
7
+ const STORE_ID = 'chat';
8
+
9
+ {{#if schemas}}
10
+ const TABLES_SCHEMA = {
11
+ messages: {
12
+ username: {type: 'string'},
13
+ text: {type: 'string'},
14
+ timestamp: {type: 'number'},
15
+ },
16
+ } as const;
17
+
18
+ {{/if}}
19
+ export const chatStore = createMergeableStore(STORE_ID){{#if schemas}}
20
+ .setTablesSchema(TABLES_SCHEMA){{/if}}
21
+ .setDefaultContent([
22
+ {
23
+ messages: {
24
+ '1': {username: 'Alice', text: 'Hello!', timestamp: Date.now() - 60000},
25
+ '2': {username: 'Bob', text: 'Hi there!', timestamp: Date.now() - 30000},
26
+ },
27
+ },
28
+ {},
29
+ ]);
30
+
31
+ {{#if sync}}
32
+ {{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
33
+ {{addImport "import {SERVER} from './config';"}}
34
+ {{addImport "import ReconnectingWebSocket from 'reconnecting-websocket';"}}
35
+ {{addImport "import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';"}}
36
+
37
+ const serverPathId = location.pathname;
38
+ createWsSynchronizer(
39
+ chatStore,
40
+ new ReconnectingWebSocket(SERVER + serverPathId),
41
+ ).then(async (synchronizer) => {
42
+ await synchronizer.startSync();
43
+
44
+ synchronizer.getWebSocket().addEventListener('open', () => {
45
+ synchronizer.load().then(() => synchronizer.save());
46
+ });
47
+ });
48
+ {{/if}}
49
+
50
+ export type ChatStore = typeof chatStore;
@@ -0,0 +1,20 @@
1
+ .message {
2
+ margin-bottom: 0.75rem;
3
+ display: flex;
4
+ flex-wrap: wrap;
5
+ gap: 0.5rem;
6
+ }
7
+
8
+ .message .username {
9
+ font-weight: 800;
10
+ color: #d81b60;
11
+ }
12
+
13
+ .message .text {
14
+ flex: 1;
15
+ }
16
+
17
+ .message .time {
18
+ font-size: 0.8rem;
19
+ color: #666;
20
+ }