create-foldkit-app 0.30.1 → 0.31.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/dist/templates/base/FOLDKIT.md +3 -1
- package/dist/templates/examples/auth/src/main.ts +3 -3
- package/dist/templates/examples/auth/src/page/loggedIn/model.ts +2 -2
- package/dist/templates/examples/auth/src/page/loggedOut/model.ts +2 -2
- package/dist/templates/examples/auth/src/route.ts +19 -29
- package/dist/templates/examples/auth/src/scene.test.ts +2 -2
- package/dist/templates/examples/auth/src/update.ts +3 -4
- package/dist/templates/examples/form/src/main.ts +12 -21
- package/dist/templates/examples/interrupting-commands/src/scene.test.ts +2 -2
- package/dist/templates/examples/interrupting-commands/src/story.test.ts +7 -7
- package/dist/templates/examples/job-application/src/main.ts +2 -2
- package/dist/templates/examples/job-application/src/model.ts +7 -12
- package/dist/templates/examples/job-application/src/scene.test.ts +5 -11
- package/dist/templates/examples/job-application/src/story.test.ts +4 -4
- package/dist/templates/examples/job-application/src/update.ts +8 -4
- package/dist/templates/examples/job-application/src/view/review.ts +92 -94
- package/dist/templates/examples/managed-resource-layer/src/main.ts +23 -27
- package/dist/templates/examples/managed-resource-layer/src/scene.test.ts +3 -4
- package/dist/templates/examples/managed-resource-layer/src/story.test.ts +11 -20
- package/dist/templates/examples/map/src/main.fixtures.ts +2 -2
- package/dist/templates/examples/map/src/main.ts +23 -21
- package/dist/templates/examples/map/src/scene.test.ts +4 -2
- package/dist/templates/examples/map/src/story.test.ts +9 -10
- package/dist/templates/examples/query-sync/src/main.ts +30 -29
- package/dist/templates/examples/query-sync/src/scene.test.ts +8 -15
- package/dist/templates/examples/query-sync/src/story.test.ts +21 -8
- package/dist/templates/examples/route-transitions/src/main.ts +15 -25
- package/dist/templates/examples/route-transitions/src/route.ts +21 -26
- package/dist/templates/examples/route-transitions/src/story.test.ts +20 -26
- package/dist/templates/examples/routing/src/fileTree.ts +3 -3
- package/dist/templates/examples/routing/src/main.ts +2 -12
- package/dist/templates/examples/routing/src/page/people.ts +18 -16
- package/dist/templates/examples/routing/src/route.ts +18 -32
- package/dist/templates/examples/routing/src/scene.test.ts +12 -20
- package/dist/templates/examples/routing/src/story.test.ts +9 -6
- package/dist/templates/examples/shopping-cart/src/route.ts +11 -19
- package/dist/templates/examples/shopping-cart/src/scene.test.ts +10 -8
- package/dist/templates/examples/shopping-cart/src/story.test.ts +2 -2
- package/dist/templates/examples/ssg/src/route.ts +9 -8
- package/dist/templates/examples/state-machine/src/main.ts +68 -63
- package/dist/templates/examples/state-machine/src/story.test.ts +8 -7
- package/dist/templates/examples/state-machine/src/view.ts +7 -13
- package/dist/templates/examples/todo/src/main.ts +11 -17
- package/dist/templates/examples/todo/src/scene.test.ts +2 -2
- package/dist/templates/examples/todo/src/story.test.ts +11 -12
- package/dist/templates/examples/ui-showcase/src/main.ts +64 -85
- package/dist/templates/examples/ui-showcase/src/scene.test.ts +14 -30
- package/dist/templates/examples/ui-showcase/src/story.test.ts +2 -2
- package/dist/templates/examples/ui-showcase/src/ui/message.ts +0 -70
- package/dist/templates/examples/view-transitions/src/main.ts +1 -1
- package/dist/templates/examples/view-transitions/src/route.ts +9 -12
- package/dist/templates/examples/view-transitions/src/scene.test.ts +5 -14
- package/dist/templates/examples/websocket-chat/src/main.ts +29 -37
- package/dist/templates/examples/websocket-chat/src/scene.test.ts +18 -10
- package/dist/templates/examples/websocket-chat/src/story.test.ts +19 -15
- package/dist/templates/release.json +13 -13
- package/dist/templates/rendering/ssg/src/route.ts +9 -8
- package/dist/templates/rendering/ssg/src/scene.test.ts +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Array,
|
|
1
|
+
import { Array, Option, pipe } from 'effect'
|
|
2
2
|
import { File } from 'foldkit'
|
|
3
3
|
import type { Html, HtmlBuilder } from 'foldkit/html'
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ import { Button } from '@foldkit/ui'
|
|
|
6
6
|
|
|
7
7
|
import { Step } from '../domain'
|
|
8
8
|
import { Message } from '../message'
|
|
9
|
-
import type
|
|
9
|
+
import { type Model, Submission } from '../model'
|
|
10
10
|
import { Education, PersonalInfo, Skills, WorkHistory } from '../step'
|
|
11
11
|
import { employmentRange, pluralize } from './format'
|
|
12
12
|
|
|
@@ -257,99 +257,97 @@ const submissionSection = (
|
|
|
257
257
|
attentionSteps: ReadonlyArray<Step.Step>,
|
|
258
258
|
h: HtmlBuilder<Message>,
|
|
259
259
|
): Html =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
h.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
h.
|
|
274
|
-
|
|
275
|
-
['Submit Application'],
|
|
276
|
-
),
|
|
277
|
-
},
|
|
278
|
-
h,
|
|
279
|
-
),
|
|
280
|
-
],
|
|
281
|
-
),
|
|
282
|
-
Submitting: () =>
|
|
283
|
-
h.div(
|
|
284
|
-
[h.Class('pt-4')],
|
|
285
|
-
[
|
|
286
|
-
Button.view(
|
|
287
|
-
{
|
|
288
|
-
toView: attributes =>
|
|
289
|
-
h.button(
|
|
290
|
-
[
|
|
291
|
-
...attributes.button,
|
|
292
|
-
h.Class(
|
|
293
|
-
'w-full rounded-lg bg-indigo-400 px-4 py-3 text-sm font-semibold text-white cursor-wait',
|
|
294
|
-
),
|
|
295
|
-
],
|
|
296
|
-
['Submitting...'],
|
|
297
|
-
),
|
|
298
|
-
},
|
|
299
|
-
h,
|
|
300
|
-
),
|
|
301
|
-
],
|
|
302
|
-
),
|
|
303
|
-
SubmitSuccess: () =>
|
|
304
|
-
h.div(
|
|
305
|
-
[
|
|
306
|
-
h.Role('status'),
|
|
307
|
-
h.Class(
|
|
308
|
-
'mt-4 rounded-lg border border-green-200 bg-green-50 p-4 text-center',
|
|
309
|
-
),
|
|
310
|
-
],
|
|
311
|
-
[
|
|
312
|
-
h.p(
|
|
313
|
-
[h.Class('text-lg font-semibold text-green-800')],
|
|
314
|
-
['Application Submitted!'],
|
|
315
|
-
),
|
|
316
|
-
h.p(
|
|
317
|
-
[h.Class('text-sm text-green-600 mt-1')],
|
|
318
|
-
["Thank you for applying to work on Foldkit. We'll be in touch!"],
|
|
319
|
-
),
|
|
320
|
-
],
|
|
321
|
-
),
|
|
322
|
-
SubmitError: ({ error }) =>
|
|
323
|
-
h.div(
|
|
324
|
-
[h.Class('space-y-3 pt-4')],
|
|
325
|
-
[
|
|
326
|
-
h.div(
|
|
327
|
-
[
|
|
328
|
-
h.Role('alert'),
|
|
329
|
-
h.Class(
|
|
330
|
-
'rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700',
|
|
260
|
+
Submission.match(submission, {
|
|
261
|
+
NotSubmitted: () =>
|
|
262
|
+
h.div(
|
|
263
|
+
[h.Class('pt-4 space-y-2')],
|
|
264
|
+
[
|
|
265
|
+
...(shouldShowBlockedNotice
|
|
266
|
+
? [blockedNotice(attentionSteps, h)]
|
|
267
|
+
: []),
|
|
268
|
+
Button.view(
|
|
269
|
+
{
|
|
270
|
+
onClick: Message.ClickedSubmit(),
|
|
271
|
+
toView: attributes =>
|
|
272
|
+
h.button(
|
|
273
|
+
[...attributes.button, h.Class(submitButtonClass)],
|
|
274
|
+
['Submit Application'],
|
|
331
275
|
),
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
276
|
+
},
|
|
277
|
+
h,
|
|
278
|
+
),
|
|
279
|
+
],
|
|
280
|
+
),
|
|
281
|
+
Submitting: () =>
|
|
282
|
+
h.div(
|
|
283
|
+
[h.Class('pt-4')],
|
|
284
|
+
[
|
|
285
|
+
Button.view(
|
|
286
|
+
{
|
|
287
|
+
toView: attributes =>
|
|
288
|
+
h.button(
|
|
289
|
+
[
|
|
290
|
+
...attributes.button,
|
|
291
|
+
h.Class(
|
|
292
|
+
'w-full rounded-lg bg-indigo-400 px-4 py-3 text-sm font-semibold text-white cursor-wait',
|
|
293
|
+
),
|
|
294
|
+
],
|
|
295
|
+
['Submitting...'],
|
|
296
|
+
),
|
|
297
|
+
},
|
|
298
|
+
h,
|
|
299
|
+
),
|
|
300
|
+
],
|
|
301
|
+
),
|
|
302
|
+
SubmitSuccess: () =>
|
|
303
|
+
h.div(
|
|
304
|
+
[
|
|
305
|
+
h.Role('status'),
|
|
306
|
+
h.Class(
|
|
307
|
+
'mt-4 rounded-lg border border-green-200 bg-green-50 p-4 text-center',
|
|
308
|
+
),
|
|
309
|
+
],
|
|
310
|
+
[
|
|
311
|
+
h.p(
|
|
312
|
+
[h.Class('text-lg font-semibold text-green-800')],
|
|
313
|
+
['Application Submitted!'],
|
|
314
|
+
),
|
|
315
|
+
h.p(
|
|
316
|
+
[h.Class('text-sm text-green-600 mt-1')],
|
|
317
|
+
["Thank you for applying to work on Foldkit. We'll be in touch!"],
|
|
318
|
+
),
|
|
319
|
+
],
|
|
320
|
+
),
|
|
321
|
+
SubmitError: ({ error }) =>
|
|
322
|
+
h.div(
|
|
323
|
+
[h.Class('space-y-3 pt-4')],
|
|
324
|
+
[
|
|
325
|
+
h.div(
|
|
326
|
+
[
|
|
327
|
+
h.Role('alert'),
|
|
328
|
+
h.Class(
|
|
329
|
+
'rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700',
|
|
330
|
+
),
|
|
331
|
+
],
|
|
332
|
+
[error],
|
|
333
|
+
),
|
|
334
|
+
...(shouldShowBlockedNotice
|
|
335
|
+
? [blockedNotice(attentionSteps, h)]
|
|
336
|
+
: []),
|
|
337
|
+
Button.view(
|
|
338
|
+
{
|
|
339
|
+
onClick: Message.ClickedSubmit(),
|
|
340
|
+
toView: attributes =>
|
|
341
|
+
h.button(
|
|
342
|
+
[...attributes.button, h.Class(submitButtonClass)],
|
|
343
|
+
['Try Again'],
|
|
344
|
+
),
|
|
345
|
+
},
|
|
346
|
+
h,
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
),
|
|
350
|
+
})
|
|
353
351
|
|
|
354
352
|
export const review = (
|
|
355
353
|
model: Model,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { Command, ManagedResource, Runtime, type Update } from 'foldkit'
|
|
12
12
|
import { Document, Html, HtmlBuilder } from 'foldkit/html'
|
|
13
13
|
import { defineMessageUnion } from 'foldkit/message'
|
|
14
|
-
import {
|
|
14
|
+
import { defineTaggedUnion } from 'foldkit/schema'
|
|
15
15
|
import { evo } from 'foldkit/struct'
|
|
16
16
|
|
|
17
17
|
import { BrowserCrypto } from '@effect/platform-browser'
|
|
@@ -47,17 +47,12 @@ type EngineService = ManagedResource.ServiceOf<typeof Engine>
|
|
|
47
47
|
|
|
48
48
|
// MODEL
|
|
49
49
|
|
|
50
|
-
export const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
EngineOff,
|
|
57
|
-
EngineBooting,
|
|
58
|
-
EngineReady,
|
|
59
|
-
EngineFailed,
|
|
60
|
-
])
|
|
50
|
+
export const EngineState = defineTaggedUnion({
|
|
51
|
+
Off: {},
|
|
52
|
+
Booting: {},
|
|
53
|
+
Ready: { engineId: S.String },
|
|
54
|
+
Failed: { reason: S.String },
|
|
55
|
+
})
|
|
61
56
|
type EngineState = typeof EngineState.Type
|
|
62
57
|
|
|
63
58
|
export const Model = S.Struct({
|
|
@@ -103,21 +98,23 @@ export const Compute = Command.define('Compute', {
|
|
|
103
98
|
export const update = (model: Model, message: Message) =>
|
|
104
99
|
Message.match<Update.Return<Model, Message, EngineService>>(message, {
|
|
105
100
|
ClickedStartEngine: () => ({
|
|
106
|
-
model: evo(model, { engine: () =>
|
|
101
|
+
model: evo(model, { engine: () => EngineState.Booting() }),
|
|
107
102
|
}),
|
|
108
103
|
|
|
109
104
|
ClickedStopEngine: () => ({
|
|
110
|
-
model: evo(model, { engine: () =>
|
|
105
|
+
model: evo(model, { engine: () => EngineState.Off() }),
|
|
111
106
|
}),
|
|
112
107
|
|
|
113
108
|
StartedEngine: ({ engineId }) => ({
|
|
114
|
-
model: evo(model, {
|
|
109
|
+
model: evo(model, {
|
|
110
|
+
engine: () => EngineState.Ready({ engineId }),
|
|
111
|
+
}),
|
|
115
112
|
}),
|
|
116
113
|
|
|
117
114
|
StoppedEngine: () => ({ model }),
|
|
118
115
|
|
|
119
116
|
FailedStartEngine: ({ reason }) => ({
|
|
120
|
-
model: evo(model, { engine: () =>
|
|
117
|
+
model: evo(model, { engine: () => EngineState.Failed({ reason }) }),
|
|
121
118
|
}),
|
|
122
119
|
|
|
123
120
|
ClickedCompute: () => {
|
|
@@ -139,7 +136,7 @@ export const update = (model: Model, message: Message) =>
|
|
|
139
136
|
|
|
140
137
|
export const init: Runtime.ApplicationInit<Model, Message> = () => ({
|
|
141
138
|
model: {
|
|
142
|
-
engine:
|
|
139
|
+
engine: EngineState.Off(),
|
|
143
140
|
computeCount: 0,
|
|
144
141
|
maybeSquareResult: Option.none(),
|
|
145
142
|
},
|
|
@@ -153,8 +150,8 @@ export const managedResources = ManagedResource.make<Model, Message>()(
|
|
|
153
150
|
resource: Engine,
|
|
154
151
|
modelToMaybeRequirements: model =>
|
|
155
152
|
M.value(model.engine).pipe(
|
|
156
|
-
M.tag('
|
|
157
|
-
M.tag('
|
|
153
|
+
M.tag('Booting', 'Ready', () => Option.some(null)),
|
|
154
|
+
M.tag('Off', 'Failed', () => Option.none()),
|
|
158
155
|
M.exhaustive,
|
|
159
156
|
),
|
|
160
157
|
acquire: () =>
|
|
@@ -203,19 +200,19 @@ const engineStatusView = (
|
|
|
203
200
|
h: HtmlBuilder<Message>,
|
|
204
201
|
): Html => {
|
|
205
202
|
const status = M.value(engine).pipe(
|
|
206
|
-
M.tag('
|
|
203
|
+
M.tag('Off', () => ({
|
|
207
204
|
colorClassName: 'text-gray-500',
|
|
208
205
|
text: 'Engine is off.',
|
|
209
206
|
})),
|
|
210
|
-
M.tag('
|
|
207
|
+
M.tag('Booting', () => ({
|
|
211
208
|
colorClassName: 'text-amber-600',
|
|
212
209
|
text: 'Booting engine...',
|
|
213
210
|
})),
|
|
214
|
-
M.tag('
|
|
211
|
+
M.tag('Ready', ({ engineId }) => ({
|
|
215
212
|
colorClassName: 'text-green-600',
|
|
216
213
|
text: `Engine ready: ${engineId}`,
|
|
217
214
|
})),
|
|
218
|
-
M.tag('
|
|
215
|
+
M.tag('Failed', ({ reason }) => ({
|
|
219
216
|
colorClassName: 'text-red-600',
|
|
220
217
|
text: `Engine failed: ${reason}`,
|
|
221
218
|
})),
|
|
@@ -230,12 +227,12 @@ const engineControlsView = (
|
|
|
230
227
|
h: HtmlBuilder<Message>,
|
|
231
228
|
): Html => {
|
|
232
229
|
const controls = M.value(engine).pipe(
|
|
233
|
-
M.tag('
|
|
230
|
+
M.tag('Booting', 'Ready', () => ({
|
|
234
231
|
label: 'Stop engine',
|
|
235
232
|
message: Message.ClickedStopEngine(),
|
|
236
233
|
colorClassName: 'bg-red-500 hover:bg-red-600',
|
|
237
234
|
})),
|
|
238
|
-
M.tag('
|
|
235
|
+
M.tag('Off', 'Failed', () => ({
|
|
239
236
|
label: 'Start engine',
|
|
240
237
|
message: Message.ClickedStartEngine(),
|
|
241
238
|
colorClassName: 'bg-green-500 hover:bg-green-600',
|
|
@@ -269,8 +266,7 @@ const squareResultView = (
|
|
|
269
266
|
return h.div([h.Class('text-gray-800')], [text])
|
|
270
267
|
}
|
|
271
268
|
|
|
272
|
-
const isEngineReady = (engine: EngineState): boolean =>
|
|
273
|
-
engine._tag === 'EngineReady'
|
|
269
|
+
const isEngineReady = (engine: EngineState): boolean => engine._tag === 'Ready'
|
|
274
270
|
|
|
275
271
|
export const view = (model: Model, h: HtmlBuilder<Message>): Document => {
|
|
276
272
|
const isComputeDisabled = !isEngineReady(model.engine)
|
|
@@ -13,8 +13,7 @@ import { describe, test } from 'vitest'
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
Compute,
|
|
16
|
-
|
|
17
|
-
EngineReady,
|
|
16
|
+
EngineState,
|
|
18
17
|
Message,
|
|
19
18
|
Model,
|
|
20
19
|
managedResources,
|
|
@@ -23,13 +22,13 @@ import {
|
|
|
23
22
|
} from './main'
|
|
24
23
|
|
|
25
24
|
const offModel = Model.make({
|
|
26
|
-
engine:
|
|
25
|
+
engine: EngineState.Off(),
|
|
27
26
|
computeCount: 0,
|
|
28
27
|
maybeSquareResult: Option.none(),
|
|
29
28
|
})
|
|
30
29
|
|
|
31
30
|
const readyModel = Model.make({
|
|
32
|
-
engine:
|
|
31
|
+
engine: EngineState.Ready({ engineId: 'engine-1' }),
|
|
33
32
|
computeCount: 2,
|
|
34
33
|
maybeSquareResult: Option.none(),
|
|
35
34
|
})
|
|
@@ -2,38 +2,29 @@ import { Option } from 'effect'
|
|
|
2
2
|
import { Command, given, message, model, story } from 'foldkit/story'
|
|
3
3
|
import { describe, expect, test } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
Compute,
|
|
7
|
-
EngineBooting,
|
|
8
|
-
EngineFailed,
|
|
9
|
-
EngineOff,
|
|
10
|
-
EngineReady,
|
|
11
|
-
Message,
|
|
12
|
-
type Model,
|
|
13
|
-
update,
|
|
14
|
-
} from './main'
|
|
5
|
+
import { Compute, EngineState, Message, type Model, update } from './main'
|
|
15
6
|
|
|
16
7
|
const offModel: Model = {
|
|
17
|
-
engine:
|
|
8
|
+
engine: EngineState.Off(),
|
|
18
9
|
computeCount: 0,
|
|
19
10
|
maybeSquareResult: Option.none(),
|
|
20
11
|
}
|
|
21
12
|
|
|
22
13
|
const readyModel: Model = {
|
|
23
|
-
engine:
|
|
14
|
+
engine: EngineState.Ready({ engineId: 'engine-1' }),
|
|
24
15
|
computeCount: 2,
|
|
25
16
|
maybeSquareResult: Option.none(),
|
|
26
17
|
}
|
|
27
18
|
|
|
28
19
|
describe('update', () => {
|
|
29
20
|
describe('engine lifecycle', () => {
|
|
30
|
-
test('ClickedStartEngine requests the engine by entering
|
|
21
|
+
test('ClickedStartEngine requests the engine by entering Booting', () => {
|
|
31
22
|
story(
|
|
32
23
|
update,
|
|
33
24
|
given(offModel),
|
|
34
25
|
message(Message.ClickedStartEngine()),
|
|
35
26
|
model(model => {
|
|
36
|
-
expect(model.engine._tag).toBe('
|
|
27
|
+
expect(model.engine._tag).toBe('Booting')
|
|
37
28
|
}),
|
|
38
29
|
)
|
|
39
30
|
})
|
|
@@ -41,23 +32,23 @@ describe('update', () => {
|
|
|
41
32
|
test('StartedEngine marks the engine ready with its id', () => {
|
|
42
33
|
story(
|
|
43
34
|
update,
|
|
44
|
-
given({ ...offModel, engine:
|
|
35
|
+
given({ ...offModel, engine: EngineState.Booting() }),
|
|
45
36
|
message(Message.StartedEngine({ engineId: 'engine-7' })),
|
|
46
37
|
model(model => {
|
|
47
38
|
expect(model.engine).toStrictEqual(
|
|
48
|
-
|
|
39
|
+
EngineState.Ready({ engineId: 'engine-7' }),
|
|
49
40
|
)
|
|
50
41
|
}),
|
|
51
42
|
)
|
|
52
43
|
})
|
|
53
44
|
|
|
54
|
-
test('ClickedStopEngine releases the engine by entering
|
|
45
|
+
test('ClickedStopEngine releases the engine by entering Off', () => {
|
|
55
46
|
story(
|
|
56
47
|
update,
|
|
57
48
|
given(readyModel),
|
|
58
49
|
message(Message.ClickedStopEngine()),
|
|
59
50
|
model(model => {
|
|
60
|
-
expect(model.engine._tag).toBe('
|
|
51
|
+
expect(model.engine._tag).toBe('Off')
|
|
61
52
|
}),
|
|
62
53
|
)
|
|
63
54
|
})
|
|
@@ -76,11 +67,11 @@ describe('update', () => {
|
|
|
76
67
|
test('FailedStartEngine records the failure reason', () => {
|
|
77
68
|
story(
|
|
78
69
|
update,
|
|
79
|
-
given({ ...offModel, engine:
|
|
70
|
+
given({ ...offModel, engine: EngineState.Booting() }),
|
|
80
71
|
message(Message.FailedStartEngine({ reason: 'boot timeout' })),
|
|
81
72
|
model(model => {
|
|
82
73
|
expect(model.engine).toStrictEqual(
|
|
83
|
-
|
|
74
|
+
EngineState.Failed({ reason: 'boot timeout' }),
|
|
84
75
|
)
|
|
85
76
|
}),
|
|
86
77
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Option } from 'effect'
|
|
2
2
|
|
|
3
3
|
import { Location, featuredLocations } from './locations'
|
|
4
|
-
import {
|
|
4
|
+
import { GeolocateState, type Model } from './main'
|
|
5
5
|
|
|
6
6
|
export const initialModel: Model = {
|
|
7
7
|
locations: featuredLocations,
|
|
@@ -11,7 +11,7 @@ export const initialModel: Model = {
|
|
|
11
11
|
maybeBounds: Option.none(),
|
|
12
12
|
maybeSelectedLocationId: Option.none(),
|
|
13
13
|
maybeUserLocation: Option.none(),
|
|
14
|
-
geolocateState:
|
|
14
|
+
geolocateState: GeolocateState.Idle(),
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const mountedModel: Model = {
|
|
@@ -11,12 +11,12 @@ import {
|
|
|
11
11
|
Stream,
|
|
12
12
|
String,
|
|
13
13
|
} from 'effect'
|
|
14
|
-
import { Command, Mount, Runtime, Subscription,
|
|
14
|
+
import { Command, Mount, Runtime, Subscription, Update } from 'foldkit'
|
|
15
15
|
import * as Dom from 'foldkit/dom'
|
|
16
16
|
import type { Document, Html } from 'foldkit/html'
|
|
17
17
|
import { HtmlBuilder } from 'foldkit/html'
|
|
18
18
|
import { defineMessageUnion } from 'foldkit/message'
|
|
19
|
-
import {
|
|
19
|
+
import { defineTaggedUnion } from 'foldkit/schema'
|
|
20
20
|
import { evo } from 'foldkit/struct'
|
|
21
21
|
import type { Map as MapInstance } from 'maplibre-gl'
|
|
22
22
|
|
|
@@ -45,16 +45,12 @@ type Bounds = typeof Bounds.Type
|
|
|
45
45
|
const LngLat = S.Struct({ lng: S.Number, lat: S.Number })
|
|
46
46
|
type LngLat = typeof LngLat.Type
|
|
47
47
|
|
|
48
|
-
export const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
GeolocateLocating,
|
|
55
|
-
GeolocateFailed,
|
|
56
|
-
])
|
|
57
|
-
type GeolocateState = typeof GeolocateState.Type
|
|
48
|
+
export const GeolocateState = defineTaggedUnion({
|
|
49
|
+
Idle: {},
|
|
50
|
+
Locating: {},
|
|
51
|
+
Failed: { reason: S.String },
|
|
52
|
+
})
|
|
53
|
+
export type GeolocateState = typeof GeolocateState.Type
|
|
58
54
|
|
|
59
55
|
export const Model = S.Struct({
|
|
60
56
|
locations: S.Array(Location),
|
|
@@ -254,19 +250,23 @@ export const update = (model: Model, message: Message) =>
|
|
|
254
250
|
}),
|
|
255
251
|
|
|
256
252
|
ClickedFindMe: () => ({
|
|
257
|
-
model: evo(model, {
|
|
253
|
+
model: evo(model, {
|
|
254
|
+
geolocateState: () => GeolocateState.Locating(),
|
|
255
|
+
}),
|
|
258
256
|
commands: [LockBodyScroll(), Geolocate()],
|
|
259
257
|
}),
|
|
260
258
|
|
|
261
259
|
DismissedGeolocate: () => ({
|
|
262
|
-
model: evo(model, {
|
|
260
|
+
model: evo(model, {
|
|
261
|
+
geolocateState: () => GeolocateState.Idle(),
|
|
262
|
+
}),
|
|
263
263
|
commands: [UnlockBodyScroll()],
|
|
264
264
|
}),
|
|
265
265
|
|
|
266
266
|
SucceededGeolocate: ({ lng, lat }) => ({
|
|
267
267
|
model: evo(model, {
|
|
268
268
|
maybeUserLocation: () => Option.some({ lng, lat }),
|
|
269
|
-
geolocateState: () =>
|
|
269
|
+
geolocateState: () => GeolocateState.Idle(),
|
|
270
270
|
}),
|
|
271
271
|
commands: [
|
|
272
272
|
UnlockBodyScroll(),
|
|
@@ -280,7 +280,9 @@ export const update = (model: Model, message: Message) =>
|
|
|
280
280
|
}),
|
|
281
281
|
|
|
282
282
|
FailedGeolocate: ({ reason }) => ({
|
|
283
|
-
model: evo(model, {
|
|
283
|
+
model: evo(model, {
|
|
284
|
+
geolocateState: () => GeolocateState.Failed({ reason }),
|
|
285
|
+
}),
|
|
284
286
|
}),
|
|
285
287
|
|
|
286
288
|
SucceededFlyTo: () => ({ model }),
|
|
@@ -301,7 +303,7 @@ export const init: Runtime.ApplicationInit<Model, Message> = () => ({
|
|
|
301
303
|
maybeBounds: Option.none(),
|
|
302
304
|
maybeSelectedLocationId: Option.none(),
|
|
303
305
|
maybeUserLocation: Option.none(),
|
|
304
|
-
geolocateState:
|
|
306
|
+
geolocateState: GeolocateState.Idle(),
|
|
305
307
|
},
|
|
306
308
|
commands: [FocusSearchInput()],
|
|
307
309
|
})
|
|
@@ -584,7 +586,7 @@ const locationListItemView =
|
|
|
584
586
|
}
|
|
585
587
|
|
|
586
588
|
const footerView = (model: Model, h: HtmlBuilder<Message>): Html => {
|
|
587
|
-
const isLocating = model.geolocateState._tag === '
|
|
589
|
+
const isLocating = model.geolocateState._tag === 'Locating'
|
|
588
590
|
return h.div(
|
|
589
591
|
[h.Class('border-t border-slate-200 px-5 py-3 space-y-2')],
|
|
590
592
|
[
|
|
@@ -680,10 +682,10 @@ const geolocateOverlayView = (
|
|
|
680
682
|
): Html =>
|
|
681
683
|
M.value(state).pipe(
|
|
682
684
|
M.tagsExhaustive({
|
|
683
|
-
|
|
684
|
-
|
|
685
|
+
Idle: () => h.empty,
|
|
686
|
+
Locating: () =>
|
|
685
687
|
geolocateOverlayShellView(geolocateLocatingContentView(h), h),
|
|
686
|
-
|
|
688
|
+
Failed: ({ reason }) =>
|
|
687
689
|
geolocateOverlayShellView(geolocateFailedContentView(reason, h), h),
|
|
688
690
|
}),
|
|
689
691
|
)
|
|
@@ -16,7 +16,7 @@ import { describe, test } from 'vitest'
|
|
|
16
16
|
import {
|
|
17
17
|
FlyTo,
|
|
18
18
|
Geolocate,
|
|
19
|
-
|
|
19
|
+
GeolocateState,
|
|
20
20
|
LockBodyScroll,
|
|
21
21
|
Message,
|
|
22
22
|
MountMap,
|
|
@@ -93,7 +93,9 @@ describe('view', () => {
|
|
|
93
93
|
{ update, view },
|
|
94
94
|
given({
|
|
95
95
|
...mountedModel,
|
|
96
|
-
geolocateState:
|
|
96
|
+
geolocateState: GeolocateState.Failed({
|
|
97
|
+
reason: 'Permission denied',
|
|
98
|
+
}),
|
|
97
99
|
}),
|
|
98
100
|
acknowledgeMapMount,
|
|
99
101
|
expect(role('button', { name: 'Dismiss' })).toExist(),
|
|
@@ -5,8 +5,7 @@ import { expect, test } from 'vitest'
|
|
|
5
5
|
import {
|
|
6
6
|
FlyTo,
|
|
7
7
|
Geolocate,
|
|
8
|
-
|
|
9
|
-
GeolocateLocating,
|
|
8
|
+
GeolocateState,
|
|
10
9
|
LockBodyScroll,
|
|
11
10
|
Message,
|
|
12
11
|
UnlockBodyScroll,
|
|
@@ -126,7 +125,7 @@ test('clicking find-me transitions to the locating state and emits Geolocate', (
|
|
|
126
125
|
given(initialModel),
|
|
127
126
|
message(Message.ClickedFindMe()),
|
|
128
127
|
model(model => {
|
|
129
|
-
expect(model.geolocateState._tag).toBe('
|
|
128
|
+
expect(model.geolocateState._tag).toBe('Locating')
|
|
130
129
|
}),
|
|
131
130
|
Command.expectHas(LockBodyScroll, Geolocate),
|
|
132
131
|
Command.resolve(LockBodyScroll, Message.CompletedLockBodyScroll()),
|
|
@@ -142,11 +141,11 @@ test('a successful geolocation result clears the locating state and flies the ma
|
|
|
142
141
|
update,
|
|
143
142
|
given({
|
|
144
143
|
...mountedModel,
|
|
145
|
-
geolocateState:
|
|
144
|
+
geolocateState: GeolocateState.Locating(),
|
|
146
145
|
}),
|
|
147
146
|
message(Message.SucceededGeolocate({ lng: 2.35, lat: 48.85 })),
|
|
148
147
|
model(model => {
|
|
149
|
-
expect(model.geolocateState._tag).toBe('
|
|
148
|
+
expect(model.geolocateState._tag).toBe('Idle')
|
|
150
149
|
expect(model.maybeUserLocation).toStrictEqual(
|
|
151
150
|
Option.some({ lng: 2.35, lat: 48.85 }),
|
|
152
151
|
)
|
|
@@ -162,12 +161,12 @@ test('a failed geolocation result surfaces the reason in the geolocate state', (
|
|
|
162
161
|
update,
|
|
163
162
|
given({
|
|
164
163
|
...initialModel,
|
|
165
|
-
geolocateState:
|
|
164
|
+
geolocateState: GeolocateState.Locating(),
|
|
166
165
|
}),
|
|
167
166
|
message(Message.FailedGeolocate({ reason: 'Permission denied' })),
|
|
168
167
|
model(model => {
|
|
169
|
-
expect(model.geolocateState._tag).toBe('
|
|
170
|
-
if (model.geolocateState._tag === '
|
|
168
|
+
expect(model.geolocateState._tag).toBe('Failed')
|
|
169
|
+
if (model.geolocateState._tag === 'Failed') {
|
|
171
170
|
expect(model.geolocateState.reason).toBe('Permission denied')
|
|
172
171
|
}
|
|
173
172
|
}),
|
|
@@ -179,11 +178,11 @@ test('dismissing the geolocate overlay returns to idle', () => {
|
|
|
179
178
|
update,
|
|
180
179
|
given({
|
|
181
180
|
...initialModel,
|
|
182
|
-
geolocateState:
|
|
181
|
+
geolocateState: GeolocateState.Failed({ reason: 'Timed out' }),
|
|
183
182
|
}),
|
|
184
183
|
message(Message.DismissedGeolocate()),
|
|
185
184
|
model(model => {
|
|
186
|
-
expect(model.geolocateState._tag).toBe('
|
|
185
|
+
expect(model.geolocateState._tag).toBe('Idle')
|
|
187
186
|
}),
|
|
188
187
|
Command.resolve(UnlockBodyScroll, Message.CompletedUnlockBodyScroll()),
|
|
189
188
|
)
|