create-tinybase 0.1.5 → 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/README.md.hbs +1 -0
- package/templates/{eslint.config.js.hbs → client/eslint.config.js.hbs} +5 -0
- package/templates/client/index.html.hbs +182 -0
- package/templates/client/package.json.hbs +76 -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/package.json.hbs +21 -56
- 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/index.html.hbs +0 -55
- package/templates/src/App.tsx.hbs +0 -52
- package/templates/src/Buttons.tsx.hbs +0 -26
- package/templates/src/app.ts.hbs +0 -36
- package/templates/src/index.css.hbs +0 -138
- package/templates/src/index.tsx.hbs +0 -18
- /package/templates/{.prettierrc.hbs → client/.prettierrc.hbs} +0 -0
- /package/templates/{public → client/public}/favicon.svg +0 -0
- /package/templates/{tsconfig.json.hbs → client/tsconfig.json.hbs} +0 -0
- /package/templates/{vite.config.js.hbs → client/vite.config.js.hbs} +0 -0
package/cli.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{existsSync as
|
|
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=
|
|
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.
|
|
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.
|
|
26
|
+
"tinycreate": "^1.0.0"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=18.0.0"
|
package/templates/README.md.hbs
CHANGED
|
@@ -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>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}{{#if server}}-client{{/if}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
{{#list}}
|
|
6
|
+
{{includeFile template="client/index.html.hbs" output="client/index.html"}}
|
|
7
|
+
"dev": "vite"
|
|
8
|
+
{{#if typescript}}
|
|
9
|
+
"build": "tsc && vite build"
|
|
10
|
+
{{else}}
|
|
11
|
+
"build": "vite build"
|
|
12
|
+
{{/if}}
|
|
13
|
+
"preview": "vite preview"
|
|
14
|
+
{{#if prettier}}
|
|
15
|
+
"format": "prettier --write ."
|
|
16
|
+
"format:check": "prettier --check ."
|
|
17
|
+
{{/if}}
|
|
18
|
+
{{#if eslint}}
|
|
19
|
+
"lint": "eslint ."
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{/list}}
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
{{#list}}
|
|
25
|
+
"vite": "^7.1.3"
|
|
26
|
+
{{#if typescript}}
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
"@types/node": "^25.0.3"
|
|
29
|
+
{{#if react}}
|
|
30
|
+
"@types/react": "^19.2.7"
|
|
31
|
+
"@types/react-dom": "^19.2.3"
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{/if}}
|
|
34
|
+
{{#if react}}
|
|
35
|
+
"@vitejs/plugin-react": "^4.3.4"
|
|
36
|
+
{{/if}}
|
|
37
|
+
{{#if prettier}}
|
|
38
|
+
"prettier": "^3.7.4"
|
|
39
|
+
{{/if}}
|
|
40
|
+
{{#if eslint}}
|
|
41
|
+
"eslint": "^9.39.2"
|
|
42
|
+
{{#if typescript}}
|
|
43
|
+
"typescript-eslint": "^8.51.0"
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{#if react}}
|
|
46
|
+
"eslint-plugin-react": "7.37.5"
|
|
47
|
+
"eslint-plugin-react-hooks": "^7.0.1"
|
|
48
|
+
{{/if}}
|
|
49
|
+
{{/if}}
|
|
50
|
+
{{/list}}
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
{{#list}}
|
|
54
|
+
"tinybase": "^7.3.1"
|
|
55
|
+
{{#if react}}
|
|
56
|
+
"react": "^19.2.3"
|
|
57
|
+
"react-dom": "^19.2.3"
|
|
58
|
+
{{/if}}
|
|
59
|
+
{{#if sync}}
|
|
60
|
+
"reconnecting-websocket": "^4.4.0"
|
|
61
|
+
{{/if}}
|
|
62
|
+
{{/list}}
|
|
63
|
+
}
|
|
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,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;
|