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