create-fate 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/bin/create-fate.mjs +412 -0
  4. package/package.json +26 -3
  5. package/templates/fate/README.md +8 -0
  6. package/templates/fate/drizzle/.github/workflows/push.yml +27 -0
  7. package/templates/fate/drizzle/.oxfmtrc.jsonc +23 -0
  8. package/templates/fate/drizzle/.vite-hooks/pre-commit +1 -0
  9. package/templates/fate/drizzle/.vscode/extensions.json +12 -0
  10. package/templates/fate/drizzle/.vscode/settings.json +25 -0
  11. package/templates/fate/drizzle/AGENTS.md +55 -0
  12. package/templates/fate/drizzle/README.md +112 -0
  13. package/templates/fate/drizzle/_gitignore +20 -0
  14. package/templates/fate/drizzle/client/package.json +58 -0
  15. package/templates/fate/drizzle/client/pages/index.tsx +96 -0
  16. package/templates/fate/drizzle/client/pages/layout.tsx +104 -0
  17. package/templates/fate/drizzle/client/pages/login.tsx +107 -0
  18. package/templates/fate/drizzle/client/pages/post/[id].tsx +22 -0
  19. package/templates/fate/drizzle/client/pages/search.tsx +95 -0
  20. package/templates/fate/drizzle/client/public/icon.svg +1 -0
  21. package/templates/fate/drizzle/client/src/App.css +153 -0
  22. package/templates/fate/drizzle/client/src/lib/AvailableLanguages.tsx +5 -0
  23. package/templates/fate/drizzle/client/src/lib/cx.tsx +6 -0
  24. package/templates/fate/drizzle/client/src/lib/env.tsx +5 -0
  25. package/templates/fate/drizzle/client/src/ui/Badge.tsx +31 -0
  26. package/templates/fate/drizzle/client/src/ui/Button.tsx +108 -0
  27. package/templates/fate/drizzle/client/src/ui/Card.tsx +32 -0
  28. package/templates/fate/drizzle/client/src/ui/CommentCard.tsx +75 -0
  29. package/templates/fate/drizzle/client/src/ui/Error.tsx +19 -0
  30. package/templates/fate/drizzle/client/src/ui/H2.tsx +15 -0
  31. package/templates/fate/drizzle/client/src/ui/H3.tsx +20 -0
  32. package/templates/fate/drizzle/client/src/ui/Header.tsx +96 -0
  33. package/templates/fate/drizzle/client/src/ui/Input.tsx +17 -0
  34. package/templates/fate/drizzle/client/src/ui/Link.tsx +11 -0
  35. package/templates/fate/drizzle/client/src/ui/LocaleSwitcher.tsx +52 -0
  36. package/templates/fate/drizzle/client/src/ui/PostCard.tsx +284 -0
  37. package/templates/fate/drizzle/client/src/ui/Section.tsx +23 -0
  38. package/templates/fate/drizzle/client/src/ui/UserCard.tsx +131 -0
  39. package/templates/fate/drizzle/client/src/user/AuthClient.tsx +6 -0
  40. package/templates/fate/drizzle/client/translations/de_DE.json +689 -0
  41. package/templates/fate/drizzle/client/translations/ja_JP.json +689 -0
  42. package/templates/fate/drizzle/client/vite.config.ts +39 -0
  43. package/templates/fate/drizzle/client/void.json +29 -0
  44. package/templates/fate/drizzle/docker-compose.yml +11 -0
  45. package/templates/fate/drizzle/package.json +46 -0
  46. package/templates/fate/drizzle/pnpm-workspace.yaml +36 -0
  47. package/templates/fate/drizzle/public/fate-logo-dark.svg +4 -0
  48. package/templates/fate/drizzle/public/fate-logo.svg +4 -0
  49. package/templates/fate/drizzle/server/.env.example +5 -0
  50. package/templates/fate/drizzle/server/drizzle.config.ts +11 -0
  51. package/templates/fate/drizzle/server/package.json +48 -0
  52. package/templates/fate/drizzle/server/src/__tests__/index.test.tsx +5 -0
  53. package/templates/fate/drizzle/server/src/app.tsx +44 -0
  54. package/templates/fate/drizzle/server/src/drizzle/db.ts +25 -0
  55. package/templates/fate/drizzle/server/src/drizzle/queries.ts +89 -0
  56. package/templates/fate/drizzle/server/src/drizzle/schema.ts +174 -0
  57. package/templates/fate/drizzle/server/src/drizzle/seed.tsx +77 -0
  58. package/templates/fate/drizzle/server/src/drizzle/seedData.ts +205 -0
  59. package/templates/fate/drizzle/server/src/index.tsx +24 -0
  60. package/templates/fate/drizzle/server/src/lib/auth.ts +33 -0
  61. package/templates/fate/drizzle/server/src/lib/env.ts +3 -0
  62. package/templates/fate/drizzle/server/src/lib/origins.ts +18 -0
  63. package/templates/fate/drizzle/server/src/router.ts +15 -0
  64. package/templates/fate/drizzle/server/src/trpc/connection.ts +4 -0
  65. package/templates/fate/drizzle/server/src/trpc/context.ts +22 -0
  66. package/templates/fate/drizzle/server/src/trpc/init.ts +51 -0
  67. package/templates/fate/drizzle/server/src/trpc/routers/comment.ts +146 -0
  68. package/templates/fate/drizzle/server/src/trpc/routers/post.ts +99 -0
  69. package/templates/fate/drizzle/server/src/trpc/routers/user.ts +68 -0
  70. package/templates/fate/drizzle/server/src/trpc/views.ts +75 -0
  71. package/templates/fate/drizzle/server/src/user/SessionUser.tsx +17 -0
  72. package/templates/fate/drizzle/server/vite.config.ts +8 -0
  73. package/templates/fate/drizzle/tsconfig.json +33 -0
  74. package/templates/fate/drizzle/vite.config.ts +71 -0
  75. package/templates/fate/http/.github/workflows/push.yml +27 -0
  76. package/templates/fate/http/.oxfmtrc.jsonc +23 -0
  77. package/templates/fate/http/.vite-hooks/pre-commit +1 -0
  78. package/templates/fate/http/.vscode/extensions.json +12 -0
  79. package/templates/fate/http/.vscode/settings.json +25 -0
  80. package/templates/fate/http/AGENTS.md +55 -0
  81. package/templates/fate/http/README.md +112 -0
  82. package/templates/fate/http/_gitignore +20 -0
  83. package/templates/fate/http/client/package.json +56 -0
  84. package/templates/fate/http/client/pages/index.tsx +96 -0
  85. package/templates/fate/http/client/pages/layout.tsx +89 -0
  86. package/templates/fate/http/client/pages/login.tsx +107 -0
  87. package/templates/fate/http/client/pages/post/[id].tsx +22 -0
  88. package/templates/fate/http/client/pages/search.tsx +95 -0
  89. package/templates/fate/http/client/public/icon.svg +1 -0
  90. package/templates/fate/http/client/src/App.css +153 -0
  91. package/templates/fate/http/client/src/lib/AvailableLanguages.tsx +5 -0
  92. package/templates/fate/http/client/src/lib/cx.tsx +6 -0
  93. package/templates/fate/http/client/src/lib/env.tsx +5 -0
  94. package/templates/fate/http/client/src/ui/Badge.tsx +31 -0
  95. package/templates/fate/http/client/src/ui/Button.tsx +108 -0
  96. package/templates/fate/http/client/src/ui/Card.tsx +32 -0
  97. package/templates/fate/http/client/src/ui/CommentCard.tsx +75 -0
  98. package/templates/fate/http/client/src/ui/Error.tsx +19 -0
  99. package/templates/fate/http/client/src/ui/H2.tsx +15 -0
  100. package/templates/fate/http/client/src/ui/H3.tsx +20 -0
  101. package/templates/fate/http/client/src/ui/Header.tsx +96 -0
  102. package/templates/fate/http/client/src/ui/Input.tsx +17 -0
  103. package/templates/fate/http/client/src/ui/Link.tsx +11 -0
  104. package/templates/fate/http/client/src/ui/LocaleSwitcher.tsx +52 -0
  105. package/templates/fate/http/client/src/ui/PostCard.tsx +281 -0
  106. package/templates/fate/http/client/src/ui/Section.tsx +23 -0
  107. package/templates/fate/http/client/src/ui/UserCard.tsx +131 -0
  108. package/templates/fate/http/client/src/user/AuthClient.tsx +6 -0
  109. package/templates/fate/http/client/translations/de_DE.json +689 -0
  110. package/templates/fate/http/client/translations/ja_JP.json +689 -0
  111. package/templates/fate/http/client/vite.config.ts +40 -0
  112. package/templates/fate/http/client/void.json +29 -0
  113. package/templates/fate/http/docker-compose.yml +11 -0
  114. package/templates/fate/http/package.json +46 -0
  115. package/templates/fate/http/pnpm-workspace.yaml +36 -0
  116. package/templates/fate/http/public/fate-logo-dark.svg +4 -0
  117. package/templates/fate/http/public/fate-logo.svg +4 -0
  118. package/templates/fate/http/server/.env.example +5 -0
  119. package/templates/fate/http/server/drizzle.config.ts +11 -0
  120. package/templates/fate/http/server/package.json +46 -0
  121. package/templates/fate/http/server/src/__tests__/index.test.tsx +5 -0
  122. package/templates/fate/http/server/src/app.tsx +36 -0
  123. package/templates/fate/http/server/src/drizzle/db.ts +25 -0
  124. package/templates/fate/http/server/src/drizzle/queries.ts +89 -0
  125. package/templates/fate/http/server/src/drizzle/schema.ts +174 -0
  126. package/templates/fate/http/server/src/drizzle/seed.tsx +77 -0
  127. package/templates/fate/http/server/src/drizzle/seedData.ts +205 -0
  128. package/templates/fate/http/server/src/fate/context.ts +20 -0
  129. package/templates/fate/http/server/src/fate/server.ts +285 -0
  130. package/templates/fate/http/server/src/fate/views.ts +75 -0
  131. package/templates/fate/http/server/src/index.tsx +24 -0
  132. package/templates/fate/http/server/src/lib/auth.ts +33 -0
  133. package/templates/fate/http/server/src/lib/env.ts +3 -0
  134. package/templates/fate/http/server/src/lib/origins.ts +18 -0
  135. package/templates/fate/http/server/src/router.ts +2 -0
  136. package/templates/fate/http/server/src/user/SessionUser.tsx +17 -0
  137. package/templates/fate/http/server/vite.config.ts +8 -0
  138. package/templates/fate/http/tsconfig.json +33 -0
  139. package/templates/fate/http/vite.config.ts +71 -0
  140. package/templates/fate/prisma/.github/workflows/push.yml +27 -0
  141. package/templates/fate/prisma/.oxfmtrc.jsonc +23 -0
  142. package/templates/fate/prisma/.vite-hooks/pre-commit +1 -0
  143. package/templates/fate/prisma/.vscode/extensions.json +12 -0
  144. package/templates/fate/prisma/.vscode/settings.json +25 -0
  145. package/templates/fate/prisma/AGENTS.md +55 -0
  146. package/templates/fate/prisma/README.md +126 -0
  147. package/templates/fate/prisma/_gitignore +20 -0
  148. package/templates/fate/prisma/client/package.json +58 -0
  149. package/templates/fate/prisma/client/pages/index.tsx +96 -0
  150. package/templates/fate/prisma/client/pages/layout.tsx +94 -0
  151. package/templates/fate/prisma/client/pages/login.tsx +107 -0
  152. package/templates/fate/prisma/client/pages/post/[id].tsx +22 -0
  153. package/templates/fate/prisma/client/pages/search.tsx +95 -0
  154. package/templates/fate/prisma/client/public/icon.svg +1 -0
  155. package/templates/fate/prisma/client/src/App.css +153 -0
  156. package/templates/fate/prisma/client/src/lib/AvailableLanguages.tsx +5 -0
  157. package/templates/fate/prisma/client/src/lib/cx.tsx +6 -0
  158. package/templates/fate/prisma/client/src/lib/env.tsx +5 -0
  159. package/templates/fate/prisma/client/src/ui/Badge.tsx +31 -0
  160. package/templates/fate/prisma/client/src/ui/Button.tsx +108 -0
  161. package/templates/fate/prisma/client/src/ui/Card.tsx +32 -0
  162. package/templates/fate/prisma/client/src/ui/CommentCard.tsx +75 -0
  163. package/templates/fate/prisma/client/src/ui/Error.tsx +19 -0
  164. package/templates/fate/prisma/client/src/ui/H2.tsx +15 -0
  165. package/templates/fate/prisma/client/src/ui/H3.tsx +20 -0
  166. package/templates/fate/prisma/client/src/ui/Header.tsx +96 -0
  167. package/templates/fate/prisma/client/src/ui/Input.tsx +17 -0
  168. package/templates/fate/prisma/client/src/ui/Link.tsx +11 -0
  169. package/templates/fate/prisma/client/src/ui/LocaleSwitcher.tsx +52 -0
  170. package/templates/fate/prisma/client/src/ui/PostCard.tsx +281 -0
  171. package/templates/fate/prisma/client/src/ui/Section.tsx +23 -0
  172. package/templates/fate/prisma/client/src/ui/UserCard.tsx +131 -0
  173. package/templates/fate/prisma/client/src/user/AuthClient.tsx +6 -0
  174. package/templates/fate/prisma/client/translations/de_DE.json +689 -0
  175. package/templates/fate/prisma/client/translations/ja_JP.json +689 -0
  176. package/templates/fate/prisma/client/vite.config.ts +39 -0
  177. package/templates/fate/prisma/client/void.json +29 -0
  178. package/templates/fate/prisma/docker-compose.yml +11 -0
  179. package/templates/fate/prisma/package.json +47 -0
  180. package/templates/fate/prisma/pnpm-workspace.yaml +39 -0
  181. package/templates/fate/prisma/public/fate-logo-dark.svg +4 -0
  182. package/templates/fate/prisma/public/fate-logo.svg +4 -0
  183. package/templates/fate/prisma/server/.env.example +5 -0
  184. package/templates/fate/prisma/server/package.json +42 -0
  185. package/templates/fate/prisma/server/prisma.config.ts +21 -0
  186. package/templates/fate/prisma/server/src/__tests__/index.test.tsx +5 -0
  187. package/templates/fate/prisma/server/src/app.tsx +40 -0
  188. package/templates/fate/prisma/server/src/index.tsx +24 -0
  189. package/templates/fate/prisma/server/src/lib/auth.ts +31 -0
  190. package/templates/fate/prisma/server/src/lib/env.ts +3 -0
  191. package/templates/fate/prisma/server/src/lib/origins.ts +18 -0
  192. package/templates/fate/prisma/server/src/prisma/prisma.ts +9 -0
  193. package/templates/fate/prisma/server/src/prisma/schema.prisma +98 -0
  194. package/templates/fate/prisma/server/src/prisma/seed.tsx +71 -0
  195. package/templates/fate/prisma/server/src/prisma/seedData.ts +205 -0
  196. package/templates/fate/prisma/server/src/router.ts +14 -0
  197. package/templates/fate/prisma/server/src/trpc/connection.ts +4 -0
  198. package/templates/fate/prisma/server/src/trpc/context.ts +22 -0
  199. package/templates/fate/prisma/server/src/trpc/init.ts +8 -0
  200. package/templates/fate/prisma/server/src/trpc/routers/comment.ts +176 -0
  201. package/templates/fate/prisma/server/src/trpc/routers/post.ts +154 -0
  202. package/templates/fate/prisma/server/src/trpc/routers/user.ts +71 -0
  203. package/templates/fate/prisma/server/src/trpc/views.ts +77 -0
  204. package/templates/fate/prisma/server/src/user/SessionUser.tsx +17 -0
  205. package/templates/fate/prisma/server/vite.config.ts +8 -0
  206. package/templates/fate/prisma/tsconfig.json +33 -0
  207. package/templates/fate/prisma/vite.config.ts +72 -0
  208. package/templates/fate/void/.oxfmtrc.jsonc +14 -0
  209. package/templates/fate/void/README.md +55 -0
  210. package/templates/fate/void/auth.ts +22 -0
  211. package/templates/fate/void/db/migrations/20260508073357_initial_schema.sql +133 -0
  212. package/templates/fate/void/db/migrations/20260508120500_seed_void_demo.sql +170 -0
  213. package/templates/fate/void/db/migrations/meta/20260508073357_snapshot.json +868 -0
  214. package/templates/fate/void/db/migrations/meta/_journal.json +20 -0
  215. package/templates/fate/void/db/queries.ts +211 -0
  216. package/templates/fate/void/db/schema.ts +325 -0
  217. package/templates/fate/void/db/seed-auth.ts +31 -0
  218. package/templates/fate/void/db/seed.ts +163 -0
  219. package/templates/fate/void/middleware/01.shared.ts +29 -0
  220. package/templates/fate/void/package.json +65 -0
  221. package/templates/fate/void/pages/category/[id].tsx +23 -0
  222. package/templates/fate/void/pages/index.tsx +153 -0
  223. package/templates/fate/void/pages/layout.tsx +46 -0
  224. package/templates/fate/void/pages/login.tsx +103 -0
  225. package/templates/fate/void/pages/post/[id].tsx +20 -0
  226. package/templates/fate/void/pages/search.tsx +81 -0
  227. package/templates/fate/void/pnpm-workspace.yaml +31 -0
  228. package/templates/fate/void/public/.assetsignore +1 -0
  229. package/templates/fate/void/public/icon.svg +1 -0
  230. package/templates/fate/void/routes/fate-live.ts +8 -0
  231. package/templates/fate/void/routes/fate.ts +8 -0
  232. package/templates/fate/void/seedData.ts +535 -0
  233. package/templates/fate/void/src/App.css +130 -0
  234. package/templates/fate/void/src/fate/__tests__/commentPermissions.test.ts +31 -0
  235. package/templates/fate/void/src/fate/commentPermissions.ts +17 -0
  236. package/templates/fate/void/src/fate/context.ts +15 -0
  237. package/templates/fate/void/src/fate/live.ts +6 -0
  238. package/templates/fate/void/src/fate/server.ts +401 -0
  239. package/templates/fate/void/src/fate/views.ts +164 -0
  240. package/templates/fate/void/src/lib/cx.tsx +6 -0
  241. package/templates/fate/void/src/lib/formatLabel.tsx +6 -0
  242. package/templates/fate/void/src/lib/shared.ts +10 -0
  243. package/templates/fate/void/src/ui/Badge.tsx +31 -0
  244. package/templates/fate/void/src/ui/Button.tsx +92 -0
  245. package/templates/fate/void/src/ui/Card.tsx +29 -0
  246. package/templates/fate/void/src/ui/CategoryCard.tsx +105 -0
  247. package/templates/fate/void/src/ui/CommentCard.tsx +80 -0
  248. package/templates/fate/void/src/ui/CreatePost.tsx +111 -0
  249. package/templates/fate/void/src/ui/Error.tsx +19 -0
  250. package/templates/fate/void/src/ui/EventCard.tsx +152 -0
  251. package/templates/fate/void/src/ui/H2.tsx +15 -0
  252. package/templates/fate/void/src/ui/H3.tsx +20 -0
  253. package/templates/fate/void/src/ui/Header.tsx +96 -0
  254. package/templates/fate/void/src/ui/Input.tsx +33 -0
  255. package/templates/fate/void/src/ui/Link.tsx +11 -0
  256. package/templates/fate/void/src/ui/Number.tsx +5 -0
  257. package/templates/fate/void/src/ui/PostCard.tsx +294 -0
  258. package/templates/fate/void/src/ui/Section.tsx +23 -0
  259. package/templates/fate/void/src/ui/TagBadge.tsx +23 -0
  260. package/templates/fate/void/src/ui/UserCard.tsx +129 -0
  261. package/templates/fate/void/src/user/AuthClient.tsx +1 -0
  262. package/templates/fate/void/src/user/SessionUser.tsx +17 -0
  263. package/templates/fate/void/tsconfig.json +34 -0
  264. package/templates/fate/void/vite.config.ts +83 -0
  265. package/templates/fate/void/void.json +32 -0
  266. package/templates/fate/void/wrangler.jsonc +16 -0
@@ -0,0 +1,164 @@
1
+ import { computed, count, dataView, field, list, type Entity } from '@nkzw/fate/server';
2
+ import type {
3
+ CategoryItem,
4
+ CommentItem,
5
+ EventAttendeeItem,
6
+ EventItem,
7
+ PostItem,
8
+ } from '../../db/queries.ts';
9
+ import type { CategoryRow, TagRow, UserRow } from '../../db/schema.ts';
10
+ import type { AppContext } from './context.ts';
11
+
12
+ export const userDataView = dataView<UserRow>('User')({
13
+ email: computed<UserRow, string | null, AppContext>({
14
+ authorize: ({ id }, context) => context?.sessionUser?.id === id,
15
+ resolve: (_item, deps) => (deps.email as string | null) ?? null,
16
+ select: {
17
+ email: field('email'),
18
+ },
19
+ }),
20
+ id: true,
21
+ name: true,
22
+ username: true,
23
+ });
24
+
25
+ export const tagDataView = dataView<TagRow>('Tag')({
26
+ description: true,
27
+ id: true,
28
+ name: true,
29
+ });
30
+
31
+ export const categorySummaryDataView = dataView<CategoryRow>('Category')({
32
+ id: true,
33
+ name: true,
34
+ });
35
+
36
+ const basePost = {
37
+ author: userDataView,
38
+ category: categorySummaryDataView,
39
+ commentCount: computed<PostItem, number>({
40
+ resolve: (_item, deps) => (deps.count as number) ?? 0,
41
+ select: {
42
+ count: count('comments'),
43
+ },
44
+ }),
45
+ content: true,
46
+ id: true,
47
+ likes: true,
48
+ title: true,
49
+ } as const;
50
+
51
+ export const postSummaryDataView = dataView<PostItem>('Post')({
52
+ ...basePost,
53
+ tags: list(tagDataView),
54
+ });
55
+
56
+ export const commentDataView = dataView<CommentItem>('Comment')({
57
+ author: userDataView,
58
+ content: true,
59
+ id: true,
60
+ post: postSummaryDataView,
61
+ });
62
+
63
+ export const postDataView = dataView<PostItem>('Post')({
64
+ ...basePost,
65
+ comments: list(commentDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
66
+ tags: list(tagDataView),
67
+ });
68
+
69
+ export const categoryDataView = dataView<CategoryItem>('Category')({
70
+ description: true,
71
+ id: true,
72
+ name: true,
73
+ postCount: computed<CategoryItem, number>({
74
+ resolve: (_item, deps) => (deps.count as number) ?? 0,
75
+ select: {
76
+ count: count('posts'),
77
+ },
78
+ }),
79
+ posts: list(postDataView),
80
+ });
81
+
82
+ export const eventAttendeeDataView = dataView<EventAttendeeItem>('EventAttendee')({
83
+ id: true,
84
+ notes: true,
85
+ status: true,
86
+ user: userDataView,
87
+ });
88
+
89
+ export const eventDataView = dataView<EventItem>('Event')({
90
+ attendees: list(eventAttendeeDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
91
+ attendingCount: computed<EventItem, number>({
92
+ resolve: (_item, deps) => (deps.count as number) ?? 0,
93
+ select: {
94
+ count: count('attendees', {
95
+ where: { status: 'GOING' },
96
+ }),
97
+ },
98
+ }),
99
+ capacity: true,
100
+ description: true,
101
+ endAt: true,
102
+ host: userDataView,
103
+ id: true,
104
+ livestreamUrl: true,
105
+ location: true,
106
+ name: true,
107
+ startAt: true,
108
+ topics: true,
109
+ type: true,
110
+ });
111
+
112
+ export type User = Entity<typeof userDataView, 'User'>;
113
+ export type Tag = Entity<typeof tagDataView, 'Tag'>;
114
+ export type Comment = Entity<
115
+ typeof commentDataView,
116
+ 'Comment',
117
+ {
118
+ author: User;
119
+ post: Post;
120
+ }
121
+ >;
122
+ export type EventAttendee = Entity<
123
+ typeof eventAttendeeDataView,
124
+ 'EventAttendee',
125
+ {
126
+ user: User;
127
+ }
128
+ >;
129
+ export type Post = Entity<
130
+ typeof postDataView,
131
+ 'Post',
132
+ {
133
+ author: User;
134
+ category: Category | null;
135
+ comments: Array<Comment>;
136
+ tags: Array<Tag>;
137
+ }
138
+ >;
139
+ export type Category = Entity<
140
+ typeof categoryDataView,
141
+ 'Category',
142
+ {
143
+ posts: Array<Post>;
144
+ }
145
+ >;
146
+ export type Event = Entity<
147
+ typeof eventDataView,
148
+ 'Event',
149
+ {
150
+ attendees: Array<EventAttendee>;
151
+ host: User;
152
+ }
153
+ >;
154
+
155
+ export const Root = {
156
+ categories: list(categoryDataView, { orderBy: { createdAt: 'asc', id: 'asc' } }),
157
+ commentSearch: {
158
+ procedure: 'search',
159
+ view: list(commentDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
160
+ },
161
+ events: list(eventDataView, { orderBy: [{ startAt: 'asc' }, { id: 'asc' }] }),
162
+ posts: list(postDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
163
+ viewer: userDataView,
164
+ };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export default function cx(...inputs: Array<ClassValue>) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,6 @@
1
+ export default function formatLabel(value: string) {
2
+ return value
3
+ .split('_')
4
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
5
+ .join(' ');
6
+ }
@@ -0,0 +1,10 @@
1
+ export type SharedData = {
2
+ auth: {
3
+ user: {
4
+ email: string;
5
+ id: string;
6
+ name: string;
7
+ } | null;
8
+ };
9
+ origin: string;
10
+ };
@@ -0,0 +1,31 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import { HTMLAttributes } from 'react';
3
+ import cx from '../lib/cx.tsx';
4
+
5
+ const badgeVariants = cva(
6
+ 'squircle inline-flex items-center border px-1.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none',
7
+ {
8
+ defaultVariants: {
9
+ variant: 'default',
10
+ },
11
+ variants: {
12
+ variant: {
13
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80 border-transparent',
14
+ destructive:
15
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
16
+ outline: 'text-foreground',
17
+ secondary:
18
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
19
+ },
20
+ },
21
+ },
22
+ );
23
+
24
+ export interface BadgeProps
25
+ extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
26
+
27
+ function Badge({ className, variant, ...props }: BadgeProps) {
28
+ return <div className={cx(badgeVariants({ variant }), className)} {...props} />;
29
+ }
30
+
31
+ export { Badge, badgeVariants };
@@ -0,0 +1,92 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import {
4
+ type ButtonHTMLAttributes,
5
+ type MouseEvent,
6
+ type ReactNode,
7
+ useOptimistic,
8
+ useTransition,
9
+ } from 'react';
10
+ import { useFormStatus } from 'react-dom';
11
+ import cx from '../lib/cx.tsx';
12
+
13
+ const buttonVariants = cva(
14
+ 'squircle inline-flex cursor-pointer items-center justify-center gap-2 text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
15
+ {
16
+ defaultVariants: {
17
+ size: 'default',
18
+ variant: 'default',
19
+ },
20
+ variants: {
21
+ size: {
22
+ default: 'h-10 px-3 py-2 active:pt-[11px] active:pb-[9px]',
23
+ icon: 'h-10 w-10',
24
+ lg: 'squircle h-11 px-6 active:pt-[11px] active:pb-[9px]',
25
+ sm: 'squircle h-9 px-2 active:pt-[11px] active:pb-[9px]',
26
+ },
27
+ variant: {
28
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
29
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
30
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
31
+ link: 'text-primary underline-offset-4 hover:underline',
32
+ outline: 'border-input border bg-background hover:bg-accent hover:text-accent-foreground',
33
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
34
+ },
35
+ },
36
+ },
37
+ );
38
+
39
+ const Button = ({
40
+ action,
41
+ asChild = false,
42
+ children,
43
+ className,
44
+ disabled,
45
+ onClick: initialOnClick,
46
+ pendingPlaceholder = '...',
47
+ size,
48
+ type,
49
+ variant,
50
+ ...props
51
+ }: ButtonHTMLAttributes<HTMLButtonElement> &
52
+ VariantProps<typeof buttonVariants> & {
53
+ action?: () => Promise<unknown> | void;
54
+ asChild?: boolean;
55
+ pendingPlaceholder?: ReactNode;
56
+ }) => {
57
+ const Component = asChild ? Slot : 'button';
58
+
59
+ const [optimisticIsPending, setOptimisticIsPending] = useOptimistic(false);
60
+ const [transitionIsPending, startTransition] = useTransition();
61
+ const { pending: formIsPending } = useFormStatus();
62
+
63
+ const onClick = (event: MouseEvent<HTMLButtonElement>) => {
64
+ initialOnClick?.(event);
65
+
66
+ if (!action || event.defaultPrevented) {
67
+ return;
68
+ }
69
+
70
+ event.preventDefault();
71
+ startTransition(async () => {
72
+ setOptimisticIsPending(true);
73
+ await action();
74
+ });
75
+ };
76
+
77
+ const isPending = transitionIsPending || optimisticIsPending || formIsPending;
78
+
79
+ return (
80
+ <Component
81
+ className={cx(buttonVariants({ className, size, variant }))}
82
+ disabled={disabled || isPending || undefined}
83
+ onClick={initialOnClick || action ? onClick : undefined}
84
+ type={type}
85
+ {...props}
86
+ >
87
+ {isPending ? pendingPlaceholder : children}
88
+ </Component>
89
+ );
90
+ };
91
+
92
+ export { Button, buttonVariants };
@@ -0,0 +1,29 @@
1
+ import { VStack } from '@nkzw/stack';
2
+ import cx from '../lib/cx.tsx';
3
+
4
+ export default function Card({
5
+ children,
6
+ className,
7
+ }: {
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ }) {
11
+ return (
12
+ <div
13
+ className={cx(
14
+ 'squircle relative overflow-hidden border border-gray-200/70 bg-white/85 shadow-[0_16px_70px_-32px_rgba(15,23,42,0.45)] transition duration-200 ease-out hover:shadow-[0_24px_80px_-38px_rgba(15,23,42,0.55)] dark:border-neutral-800 dark:bg-neutral-900/80',
15
+ className,
16
+ )}
17
+ >
18
+ <div className="pointer-events-none absolute inset-0 bg-linear-to-br from-blue-500/10 via-transparent to-indigo-500/15 opacity-30 transition duration-200" />
19
+ <VStack
20
+ className="relative z-10 flex h-full w-full"
21
+ gap={16}
22
+ horizontalPadding={20}
23
+ verticalPadding={16}
24
+ >
25
+ {children}
26
+ </VStack>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,105 @@
1
+ import Stack, { VStack } from '@nkzw/stack';
2
+ import { useView, view, ViewRef } from 'react-fate';
3
+ import type { Category, Post } from '../fate/views.ts';
4
+ import { Badge } from '../ui/Badge.tsx';
5
+ import Card from '../ui/Card.tsx';
6
+ import TagBadge, { TagView } from '../ui/TagBadge.tsx';
7
+ import { UserView } from '../ui/UserCard.tsx';
8
+ import H3 from './H3.tsx';
9
+ import Link from './Link.tsx';
10
+
11
+ const CategoryPostView = view<Post>()({
12
+ author: UserView,
13
+ id: true,
14
+ likes: true,
15
+ tags: {
16
+ items: {
17
+ node: TagView,
18
+ },
19
+ },
20
+ title: true,
21
+ });
22
+
23
+ const CategoryPost = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
24
+ const post = useView(CategoryPostView, postRef);
25
+ const author = useView(UserView, post.author);
26
+ const tags = post.tags?.items ?? [];
27
+
28
+ return (
29
+ <VStack gap key={post.id}>
30
+ <Stack alignCenter between gap={12}>
31
+ <Link href={`/post/${post.id}`}>
32
+ <span className="font-medium text-blue-600 no-underline hover:underline dark:text-blue-200">
33
+ {post.title}
34
+ </span>
35
+ </Link>
36
+ <span className="text-xs text-muted-foreground">{post.likes} likes</span>
37
+ </Stack>
38
+ <Stack alignCenter gap wrap>
39
+ <span className="text-xs text-muted-foreground">
40
+ {author?.name ? `by ${author.name}` : 'By an anonymous collaborator'}
41
+ </span>
42
+ {tags.length ? (
43
+ <Stack gap wrap>
44
+ {tags.map(({ node }) => (
45
+ <TagBadge key={node.id} tag={node} />
46
+ ))}
47
+ </Stack>
48
+ ) : null}
49
+ </Stack>
50
+ </VStack>
51
+ );
52
+ };
53
+
54
+ export const CategoryView = view<Category>()({
55
+ description: true,
56
+ id: true,
57
+ name: true,
58
+ postCount: true,
59
+ posts: {
60
+ items: {
61
+ cursor: true,
62
+ node: CategoryPostView,
63
+ },
64
+ pagination: {
65
+ hasNext: true,
66
+ nextCursor: true,
67
+ },
68
+ },
69
+ });
70
+
71
+ export default function CategoryCard({ category: categoryRef }: { category: ViewRef<'Category'> }) {
72
+ const category = useView(CategoryView, categoryRef);
73
+ const posts = category.posts?.items ?? [];
74
+
75
+ return (
76
+ <Card key={category.id}>
77
+ <VStack gap={12}>
78
+ <Stack alignCenter between gap={12}>
79
+ <div>
80
+ <Link href={`/category/${category.id}`}>
81
+ <H3>{category.name}</H3>
82
+ </Link>
83
+ <p className="text-sm text-muted-foreground">{category.description}</p>
84
+ </div>
85
+ <Badge className="text-nowrap" variant="outline">
86
+ {category.postCount} posts
87
+ </Badge>
88
+ </Stack>
89
+ <VStack gap={12}>
90
+ {posts.map(({ cursor, node }) => {
91
+ if (cursor !== node.id) {
92
+ throw new Error(`fate: Cursor '${cursor}' does not match node ID '${node.id}'.`);
93
+ }
94
+ return <CategoryPost key={node.id} post={node} />;
95
+ })}
96
+ </VStack>
97
+ {category.posts?.pagination?.hasNext ? (
98
+ <span className="text-sm text-muted-foreground">
99
+ More posts available in this category...
100
+ </span>
101
+ ) : null}
102
+ </VStack>
103
+ </Card>
104
+ );
105
+ }
@@ -0,0 +1,80 @@
1
+ import Stack from '@nkzw/stack';
2
+ import { useShared } from '@void/react';
3
+ import { X } from 'lucide-react';
4
+ import { useFateClient, useView, view, ViewRef } from 'react-fate';
5
+ import type { Comment } from '../fate/views.ts';
6
+ import type { SharedData } from '../lib/shared.ts';
7
+ import { Button } from './Button.tsx';
8
+ import Link from './Link.tsx';
9
+
10
+ export const CommentView = view<Comment>()({
11
+ author: {
12
+ id: true,
13
+ name: true,
14
+ username: true,
15
+ },
16
+ content: true,
17
+ id: true,
18
+ });
19
+
20
+ export const CommentViewWithPostCount = view<Comment>()({
21
+ ...CommentView,
22
+ post: { commentCount: true },
23
+ });
24
+
25
+ export default function CommentCard({
26
+ comment: commentRef,
27
+ link,
28
+ post,
29
+ }: {
30
+ comment: ViewRef<'Comment'>;
31
+ link?: boolean;
32
+ post: { commentCount: number; id: string; title: string };
33
+ }) {
34
+ const fate = useFateClient();
35
+ const { auth } = useShared<SharedData>();
36
+ const comment = useView(CommentView, commentRef);
37
+ const { author } = comment;
38
+ const canDelete = auth.user?.id === author?.id;
39
+
40
+ return (
41
+ <div
42
+ className="group squircle border border-gray-200/80 bg-gray-100/50 p-3 text-sm dark:border-neutral-800 dark:bg-neutral-900/40"
43
+ key={comment.id}
44
+ >
45
+ <Stack between gap={16}>
46
+ <p className="font-medium text-gray-900 dark:text-gray-200">
47
+ {author?.name ?? 'Anonymous'}
48
+ </p>
49
+ {canDelete ? (
50
+ <Button
51
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
52
+ onClick={async () => {
53
+ await fate.mutations.comment.delete({
54
+ delete: true,
55
+ input: { id: comment.id },
56
+ optimistic: {
57
+ post: { commentCount: post.commentCount - 1, id: post.id },
58
+ },
59
+ view: view<Comment>()({
60
+ id: true,
61
+ post: { commentCount: true },
62
+ }),
63
+ });
64
+ }}
65
+ size="sm"
66
+ variant="ghost"
67
+ >
68
+ <X size={14} />
69
+ </Button>
70
+ ) : null}
71
+ </Stack>
72
+ <p className="text-foreground/80">{comment.content}</p>
73
+ {link && (
74
+ <Link className="text-blue-500 underline hover:no-underline" href={`/post/${post.id}`}>
75
+ {post.title}
76
+ </Link>
77
+ )}
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,111 @@
1
+ import Stack, { VStack } from '@nkzw/stack';
2
+ import { KeyboardEvent, startTransition, useActionState, useState } from 'react';
3
+ import { useFateClient, useView, ViewRef } from 'react-fate';
4
+ import { Button } from '../ui/Button.tsx';
5
+ import Card from './Card.tsx';
6
+ import H3 from './H3.tsx';
7
+ import Input, { CheckBox } from './Input.tsx';
8
+ import { PostView } from './PostCard.tsx';
9
+ import { UserCardView } from './UserCard.tsx';
10
+
11
+ export default function CreatePost({ user: userRef }: { user: ViewRef<'User'> | null }) {
12
+ const fate = useFateClient();
13
+ const user = useView(UserCardView, userRef);
14
+ const [contentValue, setContentValue] = useState('');
15
+ const [titleValue, setTitleValue] = useState('');
16
+ const [missingOptimisticContent, setMissingOptimisticContent] = useState(false);
17
+ const [missingMutationSelection, setMissingMutationSelection] = useState(false);
18
+
19
+ const [, createPost, isPending] = useActionState(async () => {
20
+ const content = contentValue.trim();
21
+ const title = titleValue.trim();
22
+
23
+ if (!content || !title || !user) {
24
+ return;
25
+ }
26
+
27
+ const result = await fate.mutations.post.add({
28
+ input: { content, title },
29
+ insert: 'before',
30
+ optimistic: missingOptimisticContent
31
+ ? {
32
+ author: user,
33
+ comments: [],
34
+ id: `optimistic:${Date.now().toString(36)}`,
35
+ title,
36
+ }
37
+ : {
38
+ author: user,
39
+ commentCount: 0,
40
+ comments: [],
41
+ content,
42
+ id: `optimistic:${Date.now().toString(36)}`,
43
+ likes: 0,
44
+ title,
45
+ },
46
+ ...(missingMutationSelection ? null : { view: PostView }),
47
+ });
48
+
49
+ setContentValue('');
50
+ setTitleValue('');
51
+
52
+ return result;
53
+ }, null);
54
+
55
+ const maybeSubmitPost = (event: KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
56
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
57
+ startTransition(createPost);
58
+ }
59
+ };
60
+
61
+ const postingIsDisabled =
62
+ isPending || titleValue.trim().length === 0 || contentValue.trim().length === 0;
63
+
64
+ return (
65
+ <Card>
66
+ <VStack action={createPost} as="form" gap={16}>
67
+ <H3>Create a Post</H3>
68
+ <Input
69
+ className="w-full"
70
+ disabled={isPending}
71
+ onChange={(event) => setTitleValue(event.target.value)}
72
+ onKeyDown={maybeSubmitPost}
73
+ placeholder="Post Title"
74
+ value={titleValue}
75
+ />
76
+ <textarea
77
+ className="squircle border-input flex min-h-20 w-full border bg-background px-3 py-2 text-sm text-foreground shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-offset-background focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:bg-neutral-900/40"
78
+ disabled={isPending}
79
+ onChange={(event) => setContentValue(event.target.value)}
80
+ onKeyDown={maybeSubmitPost}
81
+ placeholder={'Share your thoughts about fate...'}
82
+ value={contentValue}
83
+ />
84
+ <Stack alignCenter between className="text-sm" gap={16}>
85
+ <span className="font-bold">Mutation Debug Options</span>
86
+ <Stack alignCenter as="label" gap>
87
+ <CheckBox
88
+ checked={missingMutationSelection}
89
+ disabled={isPending}
90
+ onChange={(event) => setMissingMutationSelection(event.target.checked)}
91
+ />
92
+ Missing mutation selection
93
+ </Stack>
94
+ <Stack alignCenter as="label" gap>
95
+ <CheckBox
96
+ checked={missingOptimisticContent}
97
+ disabled={isPending}
98
+ onChange={(event) => setMissingOptimisticContent(event.target.checked)}
99
+ />
100
+ Missing optimistic content
101
+ </Stack>
102
+ </Stack>
103
+ <Stack alignCenter end gap={16}>
104
+ <Button disabled={postingIsDisabled} size="sm" type="submit" variant="secondary">
105
+ Post comment
106
+ </Button>
107
+ </Stack>
108
+ </VStack>
109
+ </Card>
110
+ );
111
+ }
@@ -0,0 +1,19 @@
1
+ import { VStack } from '@nkzw/stack';
2
+
3
+ export default function Error({ error: initialError }: { error: unknown }) {
4
+ const error = initialError instanceof Error ? (initialError as Error) : null;
5
+ return (
6
+ <VStack gap={12}>
7
+ <h3 className="text-xl font-semibold text-red-700">Error</h3>
8
+ <code>{error ? error.stack || `fate Error: ${error.message}` : String(initialError)}</code>
9
+ <div>
10
+ <a
11
+ className="cursor-pointer underline decoration-gray-300 hover:no-underline"
12
+ onClick={() => window.location.reload()}
13
+ >
14
+ Try again
15
+ </a>
16
+ </div>
17
+ </VStack>
18
+ );
19
+ }