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,130 @@
1
+ @import 'tailwindcss';
2
+
3
+ body {
4
+ font-family:
5
+ system-ui,
6
+ -apple-system,
7
+ BlinkMacSystemFont,
8
+ 'Segoe UI',
9
+ 'Open Sans',
10
+ 'Helvetica Neue',
11
+ sans-serif;
12
+
13
+ background-color: hsl(var(--background));
14
+ color: hsl(var(--foreground));
15
+ }
16
+
17
+ @theme {
18
+ --color-accent: hsl(var(--accent));
19
+ --color-accent-foreground: hsl(var(--accent-foreground));
20
+
21
+ --color-background: hsl(var(--background));
22
+ --color-foreground: hsl(var(--foreground));
23
+ --color-border: hsl(var(--border));
24
+ --color-ring: hsl(var(--ring));
25
+
26
+ --color-card: hsl(var(--card));
27
+ --color-card-foreground: hsl(var(--card-foreground));
28
+
29
+ --color-muted: hsl(var(--muted));
30
+ --color-muted-foreground: hsl(var(--muted-foreground));
31
+
32
+ --color-secondary: hsl(var(--secondary));
33
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
34
+
35
+ --color-destructive: hsl(var(--destructive));
36
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
37
+ }
38
+
39
+ @layer utilities {
40
+ .text-balance {
41
+ text-wrap: balance;
42
+ }
43
+ }
44
+
45
+ @layer base {
46
+ :root {
47
+ --background: 0 0% 100%;
48
+ --foreground: 0 0% 3.9%;
49
+ --card: 0 0% 100%;
50
+ --card-foreground: 0 0% 3.9%;
51
+ --popover: 0 0% 100%;
52
+ --popover-foreground: 0 0% 3.9%;
53
+ --primary: 0 0% 9%;
54
+ --primary-foreground: 0 0% 98%;
55
+ --secondary: 0 0% 96.1%;
56
+ --secondary-foreground: 0 0% 9%;
57
+ --muted: 0 0% 96.1%;
58
+ --muted-foreground: 0 0 40%;
59
+ --accent: 0 0% 96.1%;
60
+ --accent-foreground: 0 0% 9%;
61
+ --destructive: 0 84.2% 60.2%;
62
+ --destructive-foreground: 0 0% 98%;
63
+ --border: 0 0% 89.8%;
64
+ --input: 0 0% 89.8%;
65
+ --ring: 0 0% 3.9%;
66
+ --chart-1: 12 76% 61%;
67
+ --chart-2: 173 58% 39%;
68
+ --chart-3: 197 37% 24%;
69
+ --chart-4: 43 74% 66%;
70
+ --chart-5: 27 87% 67%;
71
+ --radius: 0.5rem;
72
+ --sidebar-background: 0 0% 98%;
73
+ --sidebar-foreground: 240 5.3% 26.1%;
74
+ --sidebar-primary: 240 5.9% 10%;
75
+ --sidebar-primary-foreground: 0 0% 98%;
76
+ --sidebar-accent: 240 4.8% 95.9%;
77
+ --sidebar-accent-foreground: 240 5.9% 10%;
78
+ --sidebar-border: 220 13% 91%;
79
+ --sidebar-ring: 217.2 91.2% 59.8%;
80
+ }
81
+ @media (prefers-color-scheme: dark) {
82
+ :root {
83
+ --background: 0 0% 3.9%;
84
+ --foreground: 0 0% 98%;
85
+ --card: 0 0% 3.9%;
86
+ --card-foreground: 0 0% 98%;
87
+ --popover: 0 0% 3.9%;
88
+ --popover-foreground: 0 0% 98%;
89
+ --primary: 0 0% 98%;
90
+ --primary-foreground: 0 0% 9%;
91
+ --secondary: 0 0% 14.9%;
92
+ --secondary-foreground: 0 0% 98%;
93
+ --muted: 0 0% 14.9%;
94
+ --muted-foreground: 0 0% 63.9%;
95
+ --accent: 0 0% 14.9%;
96
+ --accent-foreground: 0 0% 98%;
97
+ --destructive: 0 62.8% 30.6%;
98
+ --destructive-foreground: 0 0% 98%;
99
+ --border: 0 0% 14.9%;
100
+ --input: 0 0% 14.9%;
101
+ --ring: 0 0% 83.1%;
102
+ --chart-1: 220 70% 50%;
103
+ --chart-2: 160 60% 45%;
104
+ --chart-3: 30 80% 55%;
105
+ --chart-4: 280 65% 60%;
106
+ --chart-5: 340 75% 55%;
107
+ --sidebar-background: 240 5.9% 10%;
108
+ --sidebar-foreground: 240 4.8% 95.9%;
109
+ --sidebar-primary: 224.3 76.3% 48%;
110
+ --sidebar-primary-foreground: 0 0% 100%;
111
+ --sidebar-accent: 240 3.7% 15.9%;
112
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
113
+ --sidebar-border: 240 3.7% 15.9%;
114
+ --sidebar-ring: 217.2 91.2% 59.8%;
115
+ }
116
+ }
117
+
118
+ * {
119
+ @apply border-border;
120
+ }
121
+ }
122
+
123
+ textarea {
124
+ resize: none;
125
+ }
126
+
127
+ .squircle {
128
+ border-radius: 32px;
129
+ corner-shape: squircle;
130
+ }
@@ -0,0 +1,31 @@
1
+ import { FateRequestError } from '@nkzw/fate/server';
2
+ import { expect, test } from 'vite-plus/test';
3
+ import { assertCanDeleteComment, getCommentDeleteFetchSelection } from '../commentPermissions.ts';
4
+
5
+ test('allows comment authors to delete their own comments', () => {
6
+ expect(() => assertCanDeleteComment({ authorId: 'user-1' }, { id: 'user-1' })).not.toThrow();
7
+ });
8
+
9
+ test('rejects anonymous comment deletion with a protocol error', () => {
10
+ expect(() => assertCanDeleteComment({ authorId: 'user-1' }, null)).toThrow(
11
+ new FateRequestError('UNAUTHORIZED', 'You must be logged in.'),
12
+ );
13
+ });
14
+
15
+ test('rejects deleting another users comment with a protocol error', () => {
16
+ expect(() => assertCanDeleteComment({ authorId: 'user-1' }, { id: 'user-2' })).toThrow(
17
+ new FateRequestError('FORBIDDEN', 'You can only delete your own comments.'),
18
+ );
19
+ });
20
+
21
+ test('rejects deleting orphaned comments with a protocol error', () => {
22
+ expect(() => assertCanDeleteComment({ authorId: null }, { id: 'user-1' })).toThrow(
23
+ new FateRequestError('FORBIDDEN', 'You can only delete your own comments.'),
24
+ );
25
+ });
26
+
27
+ test('excludes post selections from the initial delete comment fetch', () => {
28
+ expect(
29
+ getCommentDeleteFetchSelection(['content', 'id', 'post', 'post.commentCount', 'post.title']),
30
+ ).toEqual(['content', 'id']);
31
+ });
@@ -0,0 +1,17 @@
1
+ import { FateRequestError } from '@nkzw/fate/server';
2
+
3
+ type CommentOwner = Readonly<{ authorId?: string | null }>;
4
+ type SessionUser = Readonly<{ id: string }> | null | undefined;
5
+
6
+ export const assertCanDeleteComment = (comment: CommentOwner, user: SessionUser) => {
7
+ if (!user) {
8
+ throw new FateRequestError('UNAUTHORIZED', 'You must be logged in.');
9
+ }
10
+
11
+ if (!comment.authorId || comment.authorId !== user.id) {
12
+ throw new FateRequestError('FORBIDDEN', 'You can only delete your own comments.');
13
+ }
14
+ };
15
+
16
+ export const getCommentDeleteFetchSelection = (select: ReadonlyArray<string>): Array<string> =>
17
+ select.filter((path) => path !== 'post' && !path.startsWith('post.'));
@@ -0,0 +1,15 @@
1
+ import { getSession } from 'void/auth';
2
+ import { db } from 'void/db';
3
+ import { toSessionUser } from '../user/SessionUser.tsx';
4
+
5
+ export const createContext = async ({ request }: { request: Request }) => {
6
+ const session = getSession();
7
+
8
+ return {
9
+ db,
10
+ headers: request.headers,
11
+ sessionUser: session?.user ? toSessionUser(session.user) : null,
12
+ };
13
+ };
14
+
15
+ export type AppContext = Awaited<ReturnType<typeof createContext>>;
@@ -0,0 +1,6 @@
1
+ import { defineLiveStream } from 'void/live';
2
+
3
+ export const fateStream = defineLiveStream({
4
+ allowAnonymousControl: true,
5
+ id: 'fate',
6
+ });
@@ -0,0 +1,401 @@
1
+ import {
2
+ createFateServer,
3
+ createSourcePlan,
4
+ FateRequestError,
5
+ getNestedSelection,
6
+ getScopedArgs,
7
+ hasNestedSelection,
8
+ } from '@nkzw/fate/server';
9
+ import { createDrizzleSourceAdapter, type DrizzleQueryExtra } from '@nkzw/fate/server/drizzle';
10
+ import { createVoidFateLive } from 'void-fate/server';
11
+ import { db, eq, like } from 'void/db';
12
+ import { z } from 'zod';
13
+ import {
14
+ type CommentItem,
15
+ type PostItem,
16
+ createCommentRecord,
17
+ createPostRecord,
18
+ deleteCommentRecord,
19
+ likePostRecord,
20
+ unlikePostRecord,
21
+ } from '../../db/queries.ts';
22
+ import schema, { comment, user as userTable } from '../../db/schema.ts';
23
+ import { assertCanDeleteComment, getCommentDeleteFetchSelection } from './commentPermissions.ts';
24
+ import { createContext, type AppContext } from './context.ts';
25
+ import { commentDataView, postDataView, Root, userDataView, type Post } from './views.ts';
26
+
27
+ type ResolverOptions<Input> = {
28
+ ctx: AppContext;
29
+ input: Input;
30
+ select: Array<string>;
31
+ };
32
+
33
+ type ListResolverOptions = ResolverOptions<{
34
+ args?: Record<string, unknown>;
35
+ }>;
36
+
37
+ const source = createDrizzleSourceAdapter<AppContext>({
38
+ db,
39
+ schema,
40
+ views: Root,
41
+ });
42
+ export const fateLive = createVoidFateLive();
43
+ export const { live } = fateLive;
44
+
45
+ const requireUser = (ctx: AppContext) => {
46
+ if (!ctx.sessionUser) {
47
+ throw new FateRequestError('UNAUTHORIZED', 'You must be logged in.');
48
+ }
49
+
50
+ return ctx.sessionUser;
51
+ };
52
+
53
+ export const fateServer = createFateServer({
54
+ context: ({ request }) => createContext({ request }),
55
+ lists: {
56
+ commentSearch: {
57
+ resolve: async ({ ctx, input, select }: ListResolverOptions) => {
58
+ const query = input.args?.query;
59
+ if (typeof query !== 'string' || !query.trim().length) {
60
+ return {
61
+ items: [],
62
+ pagination: { hasNext: false, hasPrevious: false },
63
+ };
64
+ }
65
+
66
+ if (query.trim().length > 1) {
67
+ await new Promise((resolve) => setTimeout(resolve, 500));
68
+ }
69
+
70
+ const first = typeof input.args?.first === 'number' ? input.args.first : 20;
71
+ const cursor = typeof input.args?.after === 'string' ? input.args.after : undefined;
72
+ const nodes = await source.resolveConnection({
73
+ ctx,
74
+ cursor,
75
+ direction: 'forward',
76
+ extra: {
77
+ where: like(
78
+ comment.content,
79
+ `%${query.trim()}%`,
80
+ ) as unknown as DrizzleQueryExtra['where'],
81
+ },
82
+ input: { args: input.args, select },
83
+ skip: cursor ? 1 : undefined,
84
+ take: first + 1,
85
+ view: commentDataView,
86
+ });
87
+ const hasNext = nodes.length > first;
88
+ const items = nodes.slice(0, first).map((node) => ({
89
+ cursor: String(node.id),
90
+ node,
91
+ }));
92
+
93
+ return {
94
+ items,
95
+ pagination: {
96
+ hasNext,
97
+ hasPrevious: Boolean(cursor),
98
+ nextCursor: items.at(-1)?.cursor,
99
+ previousCursor: cursor ? items[0]?.cursor : undefined,
100
+ },
101
+ };
102
+ },
103
+ type: 'Comment',
104
+ },
105
+ },
106
+ live,
107
+ mutations: {
108
+ 'comment.add': {
109
+ input: z.object({
110
+ content: z.string().min(1, 'Content is required'),
111
+ postId: z.string().min(1, 'Post id is required'),
112
+ }),
113
+ resolve: async ({
114
+ ctx,
115
+ input,
116
+ select,
117
+ }: ResolverOptions<{ content: string; postId: string }>) => {
118
+ const user = requireUser(ctx);
119
+ const post = await source.resolveById({
120
+ ctx,
121
+ id: input.postId,
122
+ input: { select: ['id'] },
123
+ view: postDataView,
124
+ });
125
+
126
+ if (!post) {
127
+ throw new Error('Post not found.');
128
+ }
129
+
130
+ const commentId = await createCommentRecord({
131
+ authorId: user.id,
132
+ content: input.content,
133
+ postId: input.postId,
134
+ });
135
+
136
+ if (!commentId) {
137
+ throw new Error('Failed to create comment.');
138
+ }
139
+
140
+ const created = (await source.resolveById({
141
+ ctx,
142
+ id: commentId,
143
+ input: {
144
+ select: select.filter((field) => field !== 'post' && !field.startsWith('post.')),
145
+ },
146
+ view: commentDataView,
147
+ })) as (CommentItem & { post?: PostItem | null }) | null;
148
+
149
+ if (!created) {
150
+ throw new Error('Comment not found.');
151
+ }
152
+
153
+ const liveComment = await source.resolveById({
154
+ ctx,
155
+ id: commentId,
156
+ input: {
157
+ select: ['author.id', 'author.name', 'author.username', 'content', 'id'],
158
+ },
159
+ view: commentDataView,
160
+ });
161
+
162
+ if (hasNestedSelection(select, 'post')) {
163
+ created.post = (await source.resolveById<PostItem>({
164
+ ctx,
165
+ id: input.postId,
166
+ input: {
167
+ args: getScopedArgs(undefined, 'post'),
168
+ select: getNestedSelection(select, 'post'),
169
+ },
170
+ view: postDataView,
171
+ })) as PostItem | null;
172
+ }
173
+
174
+ live
175
+ .connection('Post.comments', { id: input.postId })
176
+ .appendNode('Comment', commentId, { node: liveComment ?? created });
177
+ live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
178
+
179
+ return created as CommentItem & { post?: { commentCount: number } };
180
+ },
181
+ type: 'Comment',
182
+ },
183
+ 'comment.delete': {
184
+ input: z.object({
185
+ id: z.string().min(1, 'Comment id is required'),
186
+ }),
187
+ resolve: async ({ ctx, input, select }: ResolverOptions<{ id: string }>) => {
188
+ const plan = createSourcePlan({
189
+ ctx,
190
+ select,
191
+ source: source.getSource(commentDataView),
192
+ });
193
+ const commentSelection = getCommentDeleteFetchSelection(select);
194
+
195
+ const existing = await source.fetchById<CommentItem>({
196
+ ctx,
197
+ extra: { extraFields: ['authorId', 'postId'] },
198
+ id: input.id,
199
+ plan: createSourcePlan({
200
+ ctx,
201
+ select: commentSelection,
202
+ source: source.getSource(commentDataView),
203
+ }),
204
+ });
205
+
206
+ if (!existing) {
207
+ throw new FateRequestError('NOT_FOUND', 'Comment not found.');
208
+ }
209
+
210
+ assertCanDeleteComment(existing, ctx.sessionUser);
211
+
212
+ await deleteCommentRecord(input.id);
213
+
214
+ if (existing.postId && hasNestedSelection(select, 'post')) {
215
+ const post = await source.fetchById<PostItem>({
216
+ ctx,
217
+ id: existing.postId,
218
+ plan: createSourcePlan({
219
+ args: getScopedArgs(undefined, 'post'),
220
+ ctx,
221
+ select: getNestedSelection(select, 'post'),
222
+ source: source.getSource(postDataView),
223
+ }),
224
+ });
225
+
226
+ existing.post = post;
227
+ }
228
+
229
+ const resolved = (await plan.resolve(existing)) as CommentItem & {
230
+ post?: { commentCount: number };
231
+ };
232
+
233
+ if (existing.postId) {
234
+ live.connection('Post.comments', { id: existing.postId }).deleteEdge('Comment', input.id);
235
+ live.update('Post', existing.postId, { changed: ['commentCount', 'comments'] });
236
+ }
237
+
238
+ return resolved;
239
+ },
240
+ type: 'Comment',
241
+ },
242
+ 'post.add': {
243
+ input: z.object({
244
+ content: z.string().min(1, 'Content is required'),
245
+ title: z.string().min(1, 'Title is required'),
246
+ }),
247
+ resolve: async ({
248
+ ctx,
249
+ input,
250
+ select,
251
+ }: ResolverOptions<{ content: string; title: string }>) => {
252
+ const user = requireUser(ctx);
253
+ const postId = await createPostRecord({
254
+ authorId: user.id,
255
+ content: input.content,
256
+ title: input.title,
257
+ });
258
+
259
+ if (!postId) {
260
+ throw new Error('Failed to create post.');
261
+ }
262
+
263
+ const post = await source.resolveById({
264
+ ctx,
265
+ id: postId,
266
+ input: { select },
267
+ view: postDataView,
268
+ });
269
+
270
+ if (!post) {
271
+ throw new Error('Post not found.');
272
+ }
273
+
274
+ live.connection('posts').prependNode('Post', postId);
275
+
276
+ return post as Post;
277
+ },
278
+ type: 'Post',
279
+ },
280
+ 'post.like': {
281
+ input: z.object({
282
+ error: z.enum(['boundary', 'callSite']).optional(),
283
+ id: z.string().min(1, 'Post id is required.'),
284
+ slow: z.boolean().optional(),
285
+ }),
286
+ resolve: async ({
287
+ ctx,
288
+ input,
289
+ select,
290
+ }: ResolverOptions<{ error?: 'boundary' | 'callSite'; id: string; slow?: boolean }>) => {
291
+ if (input.slow) {
292
+ await new Promise((resolve) => setTimeout(resolve, 2000));
293
+ }
294
+
295
+ if (input.error === 'boundary') {
296
+ throw new Error('Simulated error.');
297
+ } else if (input.error === 'callSite') {
298
+ await new Promise((resolve) => setTimeout(resolve, 200));
299
+ throw new Error('Gotta pay up.');
300
+ }
301
+
302
+ const updated = await likePostRecord(input.id);
303
+ if (!updated) {
304
+ throw new Error('Post not found.');
305
+ }
306
+
307
+ const post = await source.resolveById({
308
+ ctx,
309
+ id: input.id,
310
+ input: { select },
311
+ view: postDataView,
312
+ });
313
+
314
+ if (!post) {
315
+ throw new Error('Post not found.');
316
+ }
317
+
318
+ live.update('Post', input.id, { changed: ['likes'], data: post });
319
+
320
+ return post as Post;
321
+ },
322
+ type: 'Post',
323
+ },
324
+ 'post.unlike': {
325
+ input: z.object({
326
+ id: z.string().min(1, 'Post id is required.'),
327
+ }),
328
+ resolve: async ({ ctx, input, select }: ResolverOptions<{ id: string }>) => {
329
+ const updated = await unlikePostRecord(input.id);
330
+ if (!updated) {
331
+ throw new Error('Post not found.');
332
+ }
333
+
334
+ const post = await source.resolveById({
335
+ ctx,
336
+ id: input.id,
337
+ input: { select },
338
+ view: postDataView,
339
+ });
340
+
341
+ if (!post) {
342
+ throw new Error('Post not found.');
343
+ }
344
+
345
+ live.update('Post', input.id, { changed: ['likes'], data: post });
346
+
347
+ return post as Post;
348
+ },
349
+ type: 'Post',
350
+ },
351
+ 'user.update': {
352
+ input: z.object({
353
+ name: z
354
+ .string()
355
+ .trim()
356
+ .min(2, 'Name must be at least 2 characters.')
357
+ .max(50, 'Name must be at most 32 characters.'),
358
+ }),
359
+ resolve: async ({ ctx, input, select }: ResolverOptions<{ name: string }>) => {
360
+ const user = requireUser(ctx);
361
+
362
+ await db
363
+ .update(userTable)
364
+ .set({ name: input.name, updatedAt: new Date() })
365
+ .where(eq(userTable.id, user.id));
366
+
367
+ const updated = await source.resolveById({
368
+ ctx,
369
+ id: user.id,
370
+ input: { select },
371
+ view: userDataView,
372
+ });
373
+
374
+ if (!updated) {
375
+ throw new Error('User not found.');
376
+ }
377
+
378
+ return updated;
379
+ },
380
+ type: 'User',
381
+ },
382
+ },
383
+ queries: {
384
+ viewer: {
385
+ resolve: ({ ctx, select }: Omit<ResolverOptions<unknown>, 'input'>) =>
386
+ ctx.sessionUser
387
+ ? source.resolveById({
388
+ ctx,
389
+ id: ctx.sessionUser.id,
390
+ input: { select },
391
+ view: Root.viewer,
392
+ })
393
+ : null,
394
+ type: 'User',
395
+ },
396
+ },
397
+ roots: Root,
398
+ sources: source,
399
+ });
400
+
401
+ export * from './views.ts';