@weft-js/create 0.1.1 → 0.1.2

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 (127) hide show
  1. package/README.md +60 -10
  2. package/bin/create-weft.mjs +62 -14
  3. package/package.json +26 -16
  4. package/scripts/tarballs.mjs +56 -6
  5. package/src/data.mjs +90 -18
  6. package/src/index.mjs +210 -51
  7. package/src/stack/pages.mjs +151 -0
  8. package/src/stack/schema.mjs +195 -0
  9. package/src/stack.mjs +102 -313
  10. package/src/templates.mjs +174 -0
  11. package/src/wizard.mjs +65 -16
  12. package/templates/admin/files/drizzle/.schema-hash +1 -0
  13. package/templates/admin/files/drizzle/0000_admin.sql +136 -0
  14. package/templates/admin/files/drizzle/meta/0000_snapshot.json +771 -0
  15. package/templates/admin/files/drizzle/meta/_journal.json +13 -0
  16. package/templates/admin/files/public/favicon.svg +1 -0
  17. package/templates/admin/files/src/auth.server.ts +24 -0
  18. package/templates/admin/files/src/components/AuthPage.tsx +33 -0
  19. package/templates/admin/files/src/components/Failed.tsx +19 -0
  20. package/templates/admin/files/src/components/Logo.tsx +16 -0
  21. package/templates/admin/files/src/components/OrderDetail.tsx +93 -0
  22. package/templates/admin/files/src/components/OrdersFilters.tsx +44 -0
  23. package/templates/admin/files/src/components/OrdersTable.tsx +144 -0
  24. package/templates/admin/files/src/components/Overview.tsx +113 -0
  25. package/templates/admin/files/src/components/StatusBadge.tsx +12 -0
  26. package/templates/admin/files/src/db/schema.ts +68 -0
  27. package/templates/admin/files/src/lib/orders.ts +60 -0
  28. package/templates/admin/files/src/routes/404.tsx +20 -0
  29. package/templates/admin/files/src/routes/_error.tsx +20 -0
  30. package/templates/admin/files/src/routes/_layout.tsx +6 -0
  31. package/templates/admin/files/src/routes/admin/_layout.tsx +89 -0
  32. package/templates/admin/files/src/routes/admin/index.tsx +45 -0
  33. package/templates/admin/files/src/routes/admin/nested/_layout.tsx +35 -0
  34. package/templates/admin/files/src/routes/admin/nested/index.tsx +77 -0
  35. package/templates/admin/files/src/routes/admin/orders/[id].tsx +24 -0
  36. package/templates/admin/files/src/routes/admin/orders/index.tsx +28 -0
  37. package/templates/admin/files/src/routes/admin/per-row.tsx +81 -0
  38. package/templates/admin/files/src/routes/admin/slow.tsx +74 -0
  39. package/templates/admin/files/src/routes/index.tsx +13 -0
  40. package/templates/admin/files/src/routes/login.tsx +53 -0
  41. package/templates/admin/files/src/routes/signup.tsx +60 -0
  42. package/templates/admin/files/src/server/orders.ts +193 -0
  43. package/templates/admin/files/src/server/reports.ts +92 -0
  44. package/templates/admin/files/src/server/session.ts +42 -0
  45. package/templates/admin/files/src/theme.css +43 -0
  46. package/templates/admin/files/weft.config.ts +31 -0
  47. package/templates/admin/template.json +22 -0
  48. package/templates/catalog/files/drizzle/.schema-hash +1 -0
  49. package/templates/catalog/files/drizzle/0000_catalog.sql +47 -0
  50. package/templates/catalog/files/drizzle/meta/0000_snapshot.json +263 -0
  51. package/templates/catalog/files/drizzle/meta/_journal.json +13 -0
  52. package/templates/catalog/files/public/favicon.svg +1 -0
  53. package/templates/catalog/files/seed.sql +13 -0
  54. package/templates/catalog/files/src/components/AddToCart.tsx +22 -0
  55. package/templates/catalog/files/src/components/Aisles.tsx +31 -0
  56. package/templates/catalog/files/src/components/CartLines.tsx +96 -0
  57. package/templates/catalog/files/src/components/CartLink.tsx +29 -0
  58. package/templates/catalog/files/src/components/Failed.tsx +19 -0
  59. package/templates/catalog/files/src/components/Featured.tsx +19 -0
  60. package/templates/catalog/files/src/components/Filters.tsx +40 -0
  61. package/templates/catalog/files/src/components/Logo.tsx +16 -0
  62. package/templates/catalog/files/src/components/ProductArt.tsx +42 -0
  63. package/templates/catalog/files/src/components/ProductCard.tsx +42 -0
  64. package/templates/catalog/files/src/components/ProductList.tsx +65 -0
  65. package/templates/catalog/files/src/components/Related.tsx +19 -0
  66. package/templates/catalog/files/src/components/SiteFooter.tsx +9 -0
  67. package/templates/catalog/files/src/components/SiteHeader.tsx +16 -0
  68. package/templates/catalog/files/src/db/schema.ts +53 -0
  69. package/templates/catalog/files/src/lib/catalog.ts +35 -0
  70. package/templates/catalog/files/src/routes/404.tsx +18 -0
  71. package/templates/catalog/files/src/routes/_error.tsx +18 -0
  72. package/templates/catalog/files/src/routes/_layout.tsx +14 -0
  73. package/templates/catalog/files/src/routes/c/[slug].tsx +53 -0
  74. package/templates/catalog/files/src/routes/cart.tsx +29 -0
  75. package/templates/catalog/files/src/routes/index.tsx +84 -0
  76. package/templates/catalog/files/src/routes/p/[slug].tsx +67 -0
  77. package/templates/catalog/files/src/server/cart.ts +125 -0
  78. package/templates/catalog/files/src/server/catalog.ts +107 -0
  79. package/templates/catalog/files/src/theme.css +159 -0
  80. package/templates/catalog/files/weft.config.ts +30 -0
  81. package/templates/catalog/template.json +20 -0
  82. package/templates/landing/files/.weft/plugins.lock.json +42 -0
  83. package/templates/landing/files/assets/fonts/LICENSE-figtree.txt +93 -0
  84. package/templates/landing/files/assets/fonts/figtree.woff2 +0 -0
  85. package/templates/landing/files/assets/hero.webp +0 -0
  86. package/templates/landing/files/drizzle/.schema-hash +1 -0
  87. package/templates/landing/files/drizzle/0000_landing.sql +6 -0
  88. package/templates/landing/files/drizzle/meta/0000_snapshot.json +60 -0
  89. package/templates/landing/files/drizzle/meta/_journal.json +13 -0
  90. package/templates/landing/files/public/favicon.svg +1 -0
  91. package/templates/landing/files/src/components/SiteFooter.tsx +24 -0
  92. package/templates/landing/files/src/components/SiteHeader.tsx +23 -0
  93. package/templates/landing/files/src/components/WaitlistForm.tsx +36 -0
  94. package/templates/landing/files/src/db/schema.ts +7 -0
  95. package/templates/landing/files/src/routes/404.tsx +24 -0
  96. package/templates/landing/files/src/routes/_error.tsx +24 -0
  97. package/templates/landing/files/src/routes/_layout.tsx +14 -0
  98. package/templates/landing/files/src/routes/index.tsx +130 -0
  99. package/templates/landing/files/src/routes/joined.tsx +30 -0
  100. package/templates/landing/files/src/routes/pricing.tsx +131 -0
  101. package/templates/landing/files/src/routes/privacy.tsx +56 -0
  102. package/templates/landing/files/src/server/waitlist.ts +13 -0
  103. package/templates/landing/files/src/theme.css +108 -0
  104. package/templates/landing/files/weft.config.ts +44 -0
  105. package/templates/landing/template.json +17 -0
  106. package/templates/todo/files/drizzle/.schema-hash +1 -0
  107. package/templates/todo/files/drizzle/0000_todo.sql +82 -0
  108. package/templates/todo/files/drizzle/meta/0000_snapshot.json +462 -0
  109. package/templates/todo/files/drizzle/meta/_journal.json +13 -0
  110. package/templates/todo/files/public/favicon.svg +1 -0
  111. package/templates/todo/files/src/components/AuthPage.tsx +33 -0
  112. package/templates/todo/files/src/components/Logo.tsx +14 -0
  113. package/templates/todo/files/src/components/NewTodo.tsx +27 -0
  114. package/templates/todo/files/src/components/TodoList.tsx +101 -0
  115. package/templates/todo/files/src/db/schema.ts +18 -0
  116. package/templates/todo/files/src/lib/views.ts +13 -0
  117. package/templates/todo/files/src/routes/404.tsx +20 -0
  118. package/templates/todo/files/src/routes/_error.tsx +20 -0
  119. package/templates/todo/files/src/routes/_layout.tsx +6 -0
  120. package/templates/todo/files/src/routes/index.tsx +13 -0
  121. package/templates/todo/files/src/routes/login.tsx +53 -0
  122. package/templates/todo/files/src/routes/signup.tsx +57 -0
  123. package/templates/todo/files/src/routes/todos.tsx +87 -0
  124. package/templates/todo/files/src/server/todos.ts +98 -0
  125. package/templates/todo/files/src/theme.css +71 -0
  126. package/templates/todo/files/weft.config.ts +30 -0
  127. package/templates/todo/template.json +18 -0
@@ -0,0 +1,462 @@
1
+ {
2
+ "id": "9dc5327c-fae6-40d0-8aff-8ef8bd46fd13",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.todos": {
8
+ "name": "todos",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "user_id": {
19
+ "name": "user_id",
20
+ "type": "varchar(255)",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "title": {
25
+ "name": "title",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "done": {
31
+ "name": "done",
32
+ "type": "boolean",
33
+ "primaryKey": false,
34
+ "notNull": true,
35
+ "default": false
36
+ },
37
+ "created_at": {
38
+ "name": "created_at",
39
+ "type": "timestamp with time zone",
40
+ "primaryKey": false,
41
+ "notNull": true,
42
+ "default": "now()"
43
+ }
44
+ },
45
+ "indexes": {
46
+ "todos_user": {
47
+ "name": "todos_user",
48
+ "columns": [
49
+ {
50
+ "expression": "user_id",
51
+ "isExpression": false,
52
+ "asc": true,
53
+ "nulls": "last"
54
+ },
55
+ {
56
+ "expression": "created_at",
57
+ "isExpression": false,
58
+ "asc": true,
59
+ "nulls": "last"
60
+ },
61
+ {
62
+ "expression": "id",
63
+ "isExpression": false,
64
+ "asc": true,
65
+ "nulls": "last"
66
+ }
67
+ ],
68
+ "isUnique": false,
69
+ "concurrently": false,
70
+ "method": "btree",
71
+ "with": {}
72
+ }
73
+ },
74
+ "foreignKeys": {
75
+ "todos_user_id_user_id_fk": {
76
+ "name": "todos_user_id_user_id_fk",
77
+ "tableFrom": "todos",
78
+ "tableTo": "user",
79
+ "columnsFrom": [
80
+ "user_id"
81
+ ],
82
+ "columnsTo": [
83
+ "id"
84
+ ],
85
+ "onDelete": "cascade",
86
+ "onUpdate": "no action"
87
+ }
88
+ },
89
+ "compositePrimaryKeys": {},
90
+ "uniqueConstraints": {},
91
+ "policies": {},
92
+ "checkConstraints": {},
93
+ "isRLSEnabled": false
94
+ },
95
+ "public.account": {
96
+ "name": "account",
97
+ "schema": "",
98
+ "columns": {
99
+ "id": {
100
+ "name": "id",
101
+ "type": "text",
102
+ "primaryKey": true,
103
+ "notNull": true
104
+ },
105
+ "accountId": {
106
+ "name": "accountId",
107
+ "type": "text",
108
+ "primaryKey": false,
109
+ "notNull": true
110
+ },
111
+ "providerId": {
112
+ "name": "providerId",
113
+ "type": "text",
114
+ "primaryKey": false,
115
+ "notNull": true
116
+ },
117
+ "userId": {
118
+ "name": "userId",
119
+ "type": "text",
120
+ "primaryKey": false,
121
+ "notNull": true
122
+ },
123
+ "accessToken": {
124
+ "name": "accessToken",
125
+ "type": "text",
126
+ "primaryKey": false,
127
+ "notNull": false
128
+ },
129
+ "refreshToken": {
130
+ "name": "refreshToken",
131
+ "type": "text",
132
+ "primaryKey": false,
133
+ "notNull": false
134
+ },
135
+ "idToken": {
136
+ "name": "idToken",
137
+ "type": "text",
138
+ "primaryKey": false,
139
+ "notNull": false
140
+ },
141
+ "accessTokenExpiresAt": {
142
+ "name": "accessTokenExpiresAt",
143
+ "type": "timestamp with time zone",
144
+ "primaryKey": false,
145
+ "notNull": false
146
+ },
147
+ "refreshTokenExpiresAt": {
148
+ "name": "refreshTokenExpiresAt",
149
+ "type": "timestamp with time zone",
150
+ "primaryKey": false,
151
+ "notNull": false
152
+ },
153
+ "scope": {
154
+ "name": "scope",
155
+ "type": "text",
156
+ "primaryKey": false,
157
+ "notNull": false
158
+ },
159
+ "password": {
160
+ "name": "password",
161
+ "type": "text",
162
+ "primaryKey": false,
163
+ "notNull": false
164
+ },
165
+ "createdAt": {
166
+ "name": "createdAt",
167
+ "type": "timestamp with time zone",
168
+ "primaryKey": false,
169
+ "notNull": true
170
+ },
171
+ "updatedAt": {
172
+ "name": "updatedAt",
173
+ "type": "timestamp with time zone",
174
+ "primaryKey": false,
175
+ "notNull": true
176
+ }
177
+ },
178
+ "indexes": {},
179
+ "foreignKeys": {
180
+ "account_userId_user_id_fk": {
181
+ "name": "account_userId_user_id_fk",
182
+ "tableFrom": "account",
183
+ "tableTo": "user",
184
+ "columnsFrom": [
185
+ "userId"
186
+ ],
187
+ "columnsTo": [
188
+ "id"
189
+ ],
190
+ "onDelete": "cascade",
191
+ "onUpdate": "no action"
192
+ }
193
+ },
194
+ "compositePrimaryKeys": {},
195
+ "uniqueConstraints": {},
196
+ "policies": {},
197
+ "checkConstraints": {},
198
+ "isRLSEnabled": false
199
+ },
200
+ "public.jwks": {
201
+ "name": "jwks",
202
+ "schema": "",
203
+ "columns": {
204
+ "id": {
205
+ "name": "id",
206
+ "type": "text",
207
+ "primaryKey": true,
208
+ "notNull": true
209
+ },
210
+ "publicKey": {
211
+ "name": "publicKey",
212
+ "type": "text",
213
+ "primaryKey": false,
214
+ "notNull": true
215
+ },
216
+ "privateKey": {
217
+ "name": "privateKey",
218
+ "type": "text",
219
+ "primaryKey": false,
220
+ "notNull": true
221
+ },
222
+ "createdAt": {
223
+ "name": "createdAt",
224
+ "type": "timestamp with time zone",
225
+ "primaryKey": false,
226
+ "notNull": true
227
+ },
228
+ "expiresAt": {
229
+ "name": "expiresAt",
230
+ "type": "timestamp with time zone",
231
+ "primaryKey": false,
232
+ "notNull": false
233
+ },
234
+ "alg": {
235
+ "name": "alg",
236
+ "type": "text",
237
+ "primaryKey": false,
238
+ "notNull": false
239
+ },
240
+ "crv": {
241
+ "name": "crv",
242
+ "type": "text",
243
+ "primaryKey": false,
244
+ "notNull": false
245
+ }
246
+ },
247
+ "indexes": {},
248
+ "foreignKeys": {},
249
+ "compositePrimaryKeys": {},
250
+ "uniqueConstraints": {},
251
+ "policies": {},
252
+ "checkConstraints": {},
253
+ "isRLSEnabled": false
254
+ },
255
+ "public.session": {
256
+ "name": "session",
257
+ "schema": "",
258
+ "columns": {
259
+ "id": {
260
+ "name": "id",
261
+ "type": "text",
262
+ "primaryKey": true,
263
+ "notNull": true
264
+ },
265
+ "expiresAt": {
266
+ "name": "expiresAt",
267
+ "type": "timestamp with time zone",
268
+ "primaryKey": false,
269
+ "notNull": true
270
+ },
271
+ "token": {
272
+ "name": "token",
273
+ "type": "text",
274
+ "primaryKey": false,
275
+ "notNull": true
276
+ },
277
+ "createdAt": {
278
+ "name": "createdAt",
279
+ "type": "timestamp with time zone",
280
+ "primaryKey": false,
281
+ "notNull": true
282
+ },
283
+ "updatedAt": {
284
+ "name": "updatedAt",
285
+ "type": "timestamp with time zone",
286
+ "primaryKey": false,
287
+ "notNull": true
288
+ },
289
+ "ipAddress": {
290
+ "name": "ipAddress",
291
+ "type": "text",
292
+ "primaryKey": false,
293
+ "notNull": false
294
+ },
295
+ "userAgent": {
296
+ "name": "userAgent",
297
+ "type": "text",
298
+ "primaryKey": false,
299
+ "notNull": false
300
+ },
301
+ "userId": {
302
+ "name": "userId",
303
+ "type": "text",
304
+ "primaryKey": false,
305
+ "notNull": true
306
+ }
307
+ },
308
+ "indexes": {},
309
+ "foreignKeys": {
310
+ "session_userId_user_id_fk": {
311
+ "name": "session_userId_user_id_fk",
312
+ "tableFrom": "session",
313
+ "tableTo": "user",
314
+ "columnsFrom": [
315
+ "userId"
316
+ ],
317
+ "columnsTo": [
318
+ "id"
319
+ ],
320
+ "onDelete": "cascade",
321
+ "onUpdate": "no action"
322
+ }
323
+ },
324
+ "compositePrimaryKeys": {},
325
+ "uniqueConstraints": {
326
+ "session_token_unique": {
327
+ "name": "session_token_unique",
328
+ "nullsNotDistinct": false,
329
+ "columns": [
330
+ "token"
331
+ ]
332
+ }
333
+ },
334
+ "policies": {},
335
+ "checkConstraints": {},
336
+ "isRLSEnabled": false
337
+ },
338
+ "public.user": {
339
+ "name": "user",
340
+ "schema": "",
341
+ "columns": {
342
+ "id": {
343
+ "name": "id",
344
+ "type": "text",
345
+ "primaryKey": true,
346
+ "notNull": true
347
+ },
348
+ "name": {
349
+ "name": "name",
350
+ "type": "text",
351
+ "primaryKey": false,
352
+ "notNull": true
353
+ },
354
+ "email": {
355
+ "name": "email",
356
+ "type": "text",
357
+ "primaryKey": false,
358
+ "notNull": true
359
+ },
360
+ "emailVerified": {
361
+ "name": "emailVerified",
362
+ "type": "boolean",
363
+ "primaryKey": false,
364
+ "notNull": true
365
+ },
366
+ "image": {
367
+ "name": "image",
368
+ "type": "text",
369
+ "primaryKey": false,
370
+ "notNull": false
371
+ },
372
+ "createdAt": {
373
+ "name": "createdAt",
374
+ "type": "timestamp with time zone",
375
+ "primaryKey": false,
376
+ "notNull": true
377
+ },
378
+ "updatedAt": {
379
+ "name": "updatedAt",
380
+ "type": "timestamp with time zone",
381
+ "primaryKey": false,
382
+ "notNull": true
383
+ }
384
+ },
385
+ "indexes": {},
386
+ "foreignKeys": {},
387
+ "compositePrimaryKeys": {},
388
+ "uniqueConstraints": {
389
+ "user_email_unique": {
390
+ "name": "user_email_unique",
391
+ "nullsNotDistinct": false,
392
+ "columns": [
393
+ "email"
394
+ ]
395
+ }
396
+ },
397
+ "policies": {},
398
+ "checkConstraints": {},
399
+ "isRLSEnabled": false
400
+ },
401
+ "public.verification": {
402
+ "name": "verification",
403
+ "schema": "",
404
+ "columns": {
405
+ "id": {
406
+ "name": "id",
407
+ "type": "text",
408
+ "primaryKey": true,
409
+ "notNull": true
410
+ },
411
+ "identifier": {
412
+ "name": "identifier",
413
+ "type": "text",
414
+ "primaryKey": false,
415
+ "notNull": true
416
+ },
417
+ "value": {
418
+ "name": "value",
419
+ "type": "text",
420
+ "primaryKey": false,
421
+ "notNull": true
422
+ },
423
+ "expiresAt": {
424
+ "name": "expiresAt",
425
+ "type": "timestamp with time zone",
426
+ "primaryKey": false,
427
+ "notNull": true
428
+ },
429
+ "createdAt": {
430
+ "name": "createdAt",
431
+ "type": "timestamp with time zone",
432
+ "primaryKey": false,
433
+ "notNull": false
434
+ },
435
+ "updatedAt": {
436
+ "name": "updatedAt",
437
+ "type": "timestamp with time zone",
438
+ "primaryKey": false,
439
+ "notNull": false
440
+ }
441
+ },
442
+ "indexes": {},
443
+ "foreignKeys": {},
444
+ "compositePrimaryKeys": {},
445
+ "uniqueConstraints": {},
446
+ "policies": {},
447
+ "checkConstraints": {},
448
+ "isRLSEnabled": false
449
+ }
450
+ },
451
+ "enums": {},
452
+ "schemas": {},
453
+ "sequences": {},
454
+ "roles": {},
455
+ "policies": {},
456
+ "views": {},
457
+ "_meta": {
458
+ "columns": {},
459
+ "schemas": {},
460
+ "tables": {}
461
+ }
462
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1793581546736,
9
+ "tag": "0000_todo",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#1f6f5c"/><path d="M8 17l5.5 5.5L24 10" fill="none" stroke="#fff" stroke-width="3.5"/></svg>
@@ -0,0 +1,33 @@
1
+ import { AuthScreen, Heading, Stack, Text } from '@weft-js/warp'
2
+ import { Logo } from '#/components/Logo'
3
+
4
+ export function AuthPage({
5
+ title,
6
+ lede,
7
+ children,
8
+ }: {
9
+ title: string
10
+ lede: string
11
+ children?: unknown
12
+ }) {
13
+ return (
14
+ <AuthScreen
15
+ name="Daybook"
16
+ logo={<Logo />}
17
+ quote="A short list, kept honestly, beats a long one kept in your head."
18
+ >
19
+ <Stack gap="group">
20
+ <span class="auth-mark">
21
+ <Logo /> Daybook
22
+ </span>
23
+ <Stack gap="tight">
24
+ <Heading level={1} role="title">
25
+ {title}
26
+ </Heading>
27
+ <Text subtle>{lede}</Text>
28
+ </Stack>
29
+ {children}
30
+ </Stack>
31
+ </AuthScreen>
32
+ )
33
+ }
@@ -0,0 +1,14 @@
1
+ /** Daybook's mark: a tick on a ruled square. */
2
+ export function Logo() {
3
+ return (
4
+ <svg class="logo" viewBox="0 0 32 32" aria-hidden="true">
5
+ <rect width="32" height="32" fill="var(--warp-color-accent-solid)" />
6
+ <path
7
+ d="M8 17l5.5 5.5L24 10"
8
+ fill="none"
9
+ stroke="var(--warp-color-accent-on-solid)"
10
+ stroke-width="3.5"
11
+ />
12
+ </svg>
13
+ )
14
+ }
@@ -0,0 +1,27 @@
1
+ import { Form, type FormApi } from '@weft-js/start'
2
+ import { Alert, Button, Input } from '@weft-js/warp'
3
+ import { state } from '@weft-js/core'
4
+ import { addTodo } from '#/server/todos'
5
+
6
+ export function NewTodo() {
7
+ const id = state(crypto.randomUUID())
8
+ return (
9
+ <Form of={addTodo} class="new-todo" onSuccess={() => (id.value = crypto.randomUUID())}>
10
+ {(form: FormApi) => (
11
+ <>
12
+ <input type="hidden" name="id" value={id.value} />
13
+ <div class="new-todo-row">
14
+ <Input label="New todo" autocomplete="off" required {...form.field('title', 'text')} />
15
+ <Button type="submit">Add</Button>
16
+ </div>
17
+ <form.Error name="title" class="form-error" />
18
+ {form.message.value && (
19
+ <Alert tone="critical" title="That wasn't saved" live>
20
+ Your todo is still in the box. Try adding it again in a moment.
21
+ </Alert>
22
+ )}
23
+ </>
24
+ )}
25
+ </Form>
26
+ )
27
+ }
@@ -0,0 +1,101 @@
1
+ import { CheckCircleIcon, InboxIcon, XIcon } from '@weft-js/warp-icons'
2
+ import { Button, Checkbox, Heading, Progress, Skeleton, Stack, Text, toast } from '@weft-js/warp'
3
+ import { derived } from '@weft-js/core'
4
+ import type { View } from '#/lib/views'
5
+ import { deleteTodo, listTodos, toggleTodo } from '#/server/todos'
6
+
7
+ const unsaved = () =>
8
+ toast.show({
9
+ tone: 'critical',
10
+ title: "That didn't save",
11
+ description: 'Your list is back as it was. Try again in a moment.',
12
+ })
13
+
14
+ const blank = {
15
+ all: {
16
+ icon: InboxIcon,
17
+ title: 'Nothing here yet',
18
+ body: 'Add the first thing on your mind above.',
19
+ },
20
+ active: {
21
+ icon: CheckCircleIcon,
22
+ title: 'All caught up',
23
+ body: 'Nothing left to do. Enjoy it, or add something new.',
24
+ },
25
+ done: {
26
+ icon: InboxIcon,
27
+ title: 'Nothing finished yet',
28
+ body: 'Tick something off and it will land here.',
29
+ },
30
+ }
31
+
32
+ function TodoRow({ todo }: { todo: { id: string; title: string; done: boolean } }) {
33
+ return (
34
+ <li class="todo">
35
+ <Checkbox
36
+ checked={todo.done}
37
+ onChange={(e: Event) =>
38
+ toggleTodo({ id: todo.id, done: (e.currentTarget as HTMLInputElement).checked }).catch(
39
+ unsaved,
40
+ )
41
+ }
42
+ >
43
+ {todo.title}
44
+ </Checkbox>
45
+ <Button
46
+ emphasis="ghost"
47
+ tone="neutral"
48
+ size="sm"
49
+ icon={XIcon}
50
+ label={`Delete ${todo.title}`}
51
+ onClick={() => deleteTodo({ id: todo.id }).catch(unsaved)}
52
+ />
53
+ </li>
54
+ )
55
+ }
56
+
57
+ export function TodoList({ view }: { view: View }) {
58
+ const list = listTodos(() => ({ view }))
59
+ const shown = derived(() => list.value?.rows ?? [])
60
+ const left = derived(() => list.value?.left ?? 0)
61
+ const empty = derived(() => blank[view])
62
+
63
+ // Another tab, or another device, may have changed the list while this one was in the background.
64
+ return (
65
+ <Stack gap="group">
66
+ <div class="tally">
67
+ <Text>{left.value} left</Text>
68
+ {view === 'all' && shown.value.length > 0 && (
69
+ <Progress
70
+ label="Done so far"
71
+ value={((shown.value.length - left.value) / shown.value.length) * 100}
72
+ />
73
+ )}
74
+ </div>
75
+ {shown.value.length > 0 ? (
76
+ <ul class="todos" aria-label="Todos">
77
+ {shown.value.map((todo) => (
78
+ <TodoRow key={todo.id} todo={todo} />
79
+ ))}
80
+ </ul>
81
+ ) : (
82
+ <div class="blank">
83
+ <empty.value.icon />
84
+ <Heading level={2}>{empty.value.title}</Heading>
85
+ <Text subtle>{empty.value.body}</Text>
86
+ </div>
87
+ )}
88
+ </Stack>
89
+ )
90
+ }
91
+
92
+ export function TodoListLoading() {
93
+ return (
94
+ <Stack gap="group" aria-busy="true" aria-label="Loading your list">
95
+ <Skeleton width="5rem" />
96
+ <div class="todos">
97
+ <Skeleton lines={6} height="1.5rem" />
98
+ </div>
99
+ </Stack>
100
+ )
101
+ }
@@ -0,0 +1,18 @@
1
+ import { boolean, index, pgTable, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
2
+ import { user } from './auth'
3
+
4
+ export * from './auth'
5
+
6
+ export const todos = pgTable(
7
+ 'todos',
8
+ {
9
+ id: uuid('id').primaryKey().defaultRandom(),
10
+ userId: varchar('user_id', { length: 255 })
11
+ .notNull()
12
+ .references(() => user.id, { onDelete: 'cascade' }),
13
+ title: text('title').notNull(),
14
+ done: boolean('done').notNull().default(false),
15
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
16
+ },
17
+ (table) => [index('todos_user').on(table.userId, table.createdAt, table.id)],
18
+ )
@@ -0,0 +1,13 @@
1
+ export const views = {
2
+ all: { label: 'All', title: 'Everything', href: '/todos' },
3
+ active: { label: 'Active', title: 'Still to do', href: '/todos?filter=active' },
4
+ done: { label: 'Done', title: 'Finished', href: '/todos?filter=done' },
5
+ }
6
+
7
+ export type View = keyof typeof views
8
+ export const viewNames = ['all', 'active', 'done'] as const
9
+
10
+ export const viewOf = (url: string): View => {
11
+ const filter = new URL(url).searchParams.get('filter')
12
+ return filter === 'active' || filter === 'done' ? filter : 'all'
13
+ }