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,107 @@
1
+ import Stack, { VStack } from '@nkzw/stack';
2
+ import { useRouter } from '@void/react';
3
+ import { fbs } from 'fbtee';
4
+ import { ExternalLinkIcon } from 'lucide-react';
5
+ import { useActionState, useEffect, useState } from 'react';
6
+ import { AsyncButton } from '../src/ui/Button.tsx';
7
+ import Card from '../src/ui/Card.tsx';
8
+ import H2 from '../src/ui/H2.tsx';
9
+ import Input from '../src/ui/Input.tsx';
10
+ import Section from '../src/ui/Section.tsx';
11
+ import AuthClient from '../src/user/AuthClient.tsx';
12
+
13
+ export default function LoginPage() {
14
+ const [email, setEmail] = useState('');
15
+ const [password, setPassword] = useState('');
16
+ const router = useRouter();
17
+ const { data: session } = AuthClient.useSession();
18
+
19
+ const [, signInAction] = useActionState(async () => {
20
+ await AuthClient.signIn.email(
21
+ {
22
+ email,
23
+ password,
24
+ },
25
+ {
26
+ onError: () => {},
27
+ onRequest: () => {},
28
+ onSuccess: () => {
29
+ router.flushAll();
30
+ void router.visit('/', { replace: true });
31
+ },
32
+ },
33
+ );
34
+
35
+ return null;
36
+ }, null);
37
+
38
+ useEffect(() => {
39
+ if (session) {
40
+ router.flushAll();
41
+ void router.visit('/', { replace: true });
42
+ }
43
+ }, [router, session]);
44
+
45
+ if (session) {
46
+ return null;
47
+ }
48
+
49
+ return (
50
+ <Section>
51
+ <VStack center gap={16}>
52
+ <H2 className="pl-5">
53
+ <fbt desc="Sign In headline">Sign In</fbt>
54
+ </H2>
55
+ <Stack gap={32} wrap>
56
+ <Card className="w-84">
57
+ <Stack gap vertical>
58
+ <VStack action={signInAction} as="form" gap={12}>
59
+ <Input
60
+ className="w-48"
61
+ name="email"
62
+ onChange={(e) => setEmail(e.target.value)}
63
+ placeholder={fbs('email', 'Email placeholder')}
64
+ type="email"
65
+ value={email}
66
+ />
67
+ <Input
68
+ className="w-48"
69
+ name="password"
70
+ onChange={(e) => setPassword(e.target.value)}
71
+ placeholder={fbs('password', 'Password placeholder')}
72
+ type="password"
73
+ value={password}
74
+ />
75
+ <div>
76
+ <AsyncButton type="submit" variant="outline">
77
+ <fbt desc="Sign in button">Sign In</fbt>
78
+ </AsyncButton>
79
+ </div>
80
+ </VStack>
81
+ </Stack>
82
+ </Card>
83
+ <Card className="w-84">
84
+ <p>
85
+ <fbt desc="Login details">
86
+ Try one of the
87
+ <Stack
88
+ alignCenter
89
+ as="a"
90
+ className="inline-flex! px-1 underline hover:no-underline"
91
+ gap={4}
92
+ href="https://github.com/nkzw-tech/fate/blob/main/packages/create-fate/templates/fate/prisma/server/src/prisma/seedData.ts#L1"
93
+ rel="noreferrer"
94
+ target="_blank"
95
+ >
96
+ Example Accounts
97
+ <ExternalLinkIcon className="h-4 w-4" />
98
+ </Stack>{' '}
99
+ in the seed data.
100
+ </fbt>
101
+ </p>
102
+ </Card>
103
+ </Stack>
104
+ </VStack>
105
+ </Section>
106
+ );
107
+ }
@@ -0,0 +1,22 @@
1
+ import { useParams } from '@void/react';
2
+ import { useRequest } from 'react-fate';
3
+ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
4
+ import Section from '../../src/ui/Section.tsx';
5
+
6
+ export default function PostPage() {
7
+ const { id } = useParams<{ id: string }>();
8
+
9
+ if (!id) {
10
+ throw new Error('fate: Post ID is required.');
11
+ }
12
+
13
+ const { post } = useRequest({
14
+ post: { id, view: PostView },
15
+ });
16
+
17
+ return (
18
+ <Section>
19
+ <PostCard detail post={post} />
20
+ </Section>
21
+ );
22
+ }
@@ -0,0 +1,95 @@
1
+ import type { Comment, Post } from '@app/server/src/trpc/views.ts';
2
+ import Stack, { VStack } from '@nkzw/stack';
3
+ import { fbs } from 'fbtee';
4
+ import { Suspense, useDeferredValue, useState } from 'react';
5
+ import { ErrorBoundary } from 'react-error-boundary';
6
+ import { useRequest, useView, view, ViewRef } from 'react-fate';
7
+ import cx from '../src/lib/cx.tsx';
8
+ import Card from '../src/ui/Card.tsx';
9
+ import CommentCard, { CommentView } from '../src/ui/CommentCard.tsx';
10
+ import Error from '../src/ui/Error.tsx';
11
+ import Input from '../src/ui/Input.tsx';
12
+ import Section from '../src/ui/Section.tsx';
13
+
14
+ const CommentPostView = view<Post>()({
15
+ commentCount: true,
16
+ id: true,
17
+ title: true,
18
+ });
19
+
20
+ const CommentSearchView = view<Comment>()({
21
+ ...CommentView,
22
+ id: true,
23
+ post: CommentPostView,
24
+ });
25
+
26
+ const CommentResult = ({ comment: commentRef }: { comment: ViewRef<'Comment'> }) => {
27
+ const comment = useView(CommentSearchView, commentRef);
28
+ const post = useView(CommentPostView, comment.post);
29
+
30
+ return <CommentCard comment={comment} link post={post} />;
31
+ };
32
+
33
+ const SearchResults = ({ isStale, query }: { isStale: boolean; query: string }) => {
34
+ const { commentSearch } = useRequest({
35
+ commentSearch: { args: { query }, list: CommentSearchView },
36
+ });
37
+
38
+ if (commentSearch.length === 0) {
39
+ return (
40
+ <p>
41
+ <fbt desc="No matches text">
42
+ No matches for{' '}
43
+ <i>
44
+ &quot;<fbt:param name="query">{query}</fbt:param>&quot;
45
+ </i>
46
+ </fbt>
47
+ </p>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <VStack className={cx(isStale && 'opacity-50')} gap={12}>
53
+ {commentSearch.map((comment) => (
54
+ <CommentResult comment={comment} key={comment.id} />
55
+ ))}
56
+ </VStack>
57
+ );
58
+ };
59
+
60
+ export default function SearchPage() {
61
+ const [query, setQuery] = useState('');
62
+ const deferredQuery = useDeferredValue(query);
63
+ const isStale = query !== deferredQuery;
64
+
65
+ return (
66
+ <Section>
67
+ <Card>
68
+ <Stack alignCenter between gap={16}>
69
+ <Input
70
+ className="w-64"
71
+ onChange={(e) => setQuery(e.target.value)}
72
+ placeholder={fbs('Search comments...', 'Search input placeholder')}
73
+ ref={(ref) => ref?.focus()}
74
+ value={query}
75
+ />
76
+ <div className="text-xs text-muted-foreground">
77
+ <fbt desc="Slowdown label explanation">500ms artificial slowdown</fbt>
78
+ </div>
79
+ </Stack>
80
+
81
+ <ErrorBoundary FallbackComponent={Error}>
82
+ <Suspense
83
+ fallback={
84
+ <h2>
85
+ <fbt desc="Text for thinking/loading screen">Thinking…</fbt>
86
+ </h2>
87
+ }
88
+ >
89
+ {query.trim().length > 0 ? <SearchResults isStale={isStale} query={query} /> : null}
90
+ </Suspense>
91
+ </ErrorBoundary>
92
+ </Card>
93
+ </Section>
94
+ );
95
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMax" viewBox="0 0 160 160"><path d="M108.301 17.465h.272c1.797-.005 3.563.177 5.331.496l.349.063c5.588 1.02 10.731 3.447 15.201 6.933l.347.267c1.579 1.26 3.083 2.684 4.386 4.227l.161.186c4.018 4.719 6.924 10.686 7.767 16.852l.033.239c.262 1.848.351 3.688.352 5.551l.001.307c-.001 1.623-.092 3.188-.386 4.788l-.05.292c-1.003 5.874-3.476 11.447-7.286 16.034l-.29.358c-1.422 1.721-3.011 3.382-4.797 4.729l-.444.354c-3.046 2.405-6.436 4.147-10.084 5.442l-.497.186c-1.766.627-3.615.974-5.465 1.266l-.364.058c-1.591.238-3.185.286-4.791.281h-.499l-1.339-.002h-1.408c-.886-.001-1.772-.001-2.656-.003-3.085-.001-6.163-.005-9.247-.008v-12.78l3.606-.006 2.282-.007c1.205-.006 2.407-.01 3.611-.011.878 0 1.754-.003 2.63-.007.462-.002.928-.005 1.389-.002 6.302.01 12.003-1.72 16.65-6.117l.291-.272c3.951-3.807 6.097-9.302 6.241-14.736.04-3.078-.476-6.048-1.68-8.892l-.104-.244c-2.382-5.559-6.98-9.439-12.504-11.675-2.204-.816-4.522-1.16-6.863-1.134-.301.004-.606.004-.909.001-1.677.003-3.304.127-4.924.591l-.331.098c-2.625.758-5.093 1.928-7.254 3.607l-.323.249c-1.046.817-2.031 1.682-2.873 2.71l-.154.179c-2.354 2.782-3.9 5.988-4.727 9.529l-.054.229c-.391 1.8-.423 3.633-.42 5.471l-.001.74c.002 1.4.001 2.801-.004 4.201l-.004 3.779c0 1.824-.001 3.649-.003 5.473l-.006 2.813-.005 6.076-.001.267-.008 8.614c-.001 2.023-.004 4.045-.006 6.066v.259l-.004 2.55-.006 5.46c0 1.331-.001 2.658-.003 3.985-.002.72-.002 1.438-.002 2.156 0 .658 0 1.317-.002 1.978v.708c.004 2.098-.07 4.214-.545 6.263l-.077.359-.161.716c-.071.302-.136.605-.199.909-.55 2.538-1.6 5.067-2.832 7.348l-.214.42c-.965 1.921-2.22 3.714-3.574 5.38l-.152.183c-.78.973-1.639 1.866-2.519 2.751l-.308.313a25.81 25.81 0 0 1-2.226 1.955l-.22.179c-3.698 2.953-7.975 5.044-12.538 6.262-.212.056-.422.115-.631.176-8.366 2.287-17.8.738-25.278-3.512-3.639-2.122-7.171-4.945-9.721-8.307-.121-.15-.241-.298-.356-.448a36.247 36.247 0 0 1-2.605-3.814l-.144-.234c-.889-1.466-1.562-3.034-2.222-4.617l-.11-.265c-3.253-7.791-2.911-17.367.184-25.133.865-2.069 1.869-4.089 3.123-5.951l.172-.261a31.91 31.91 0 0 1 2.547-3.29l.229-.263c.13-.152.266-.304.396-.452l.425-.493c1.216-1.389 2.593-2.615 4.068-3.723.208-.157.414-.315.62-.476 1.184-.901 2.437-1.665 3.729-2.4l.272-.159c1.586-.893 3.235-1.573 4.934-2.21l.253-.097c4.232-1.567 8.615-1.836 13.079-1.82 1.359 0 2.732.002 4.093.007 1.209.005 2.417.006 3.621.006 1.899.002 3.794.007 5.688.012v12.78l-3.612.013-2.282.011c-1.201.005-2.401.012-3.61.016-.876.003-1.751.005-2.629.013-.463.002-.924.005-1.387.006-4.322.005-8.193.592-12.033 2.662l-.217.114c-1.458.773-2.697 1.731-3.908 2.849-.206.185-.413.362-.63.535-.435.376-.794.788-1.16 1.232l-.153.18a21.18 21.18 0 0 0-4.474 8.651l-.094.342c-.42 1.682-.54 3.395-.536 5.127v.272c.019 5.672 2.392 10.671 6.206 14.769l.215.237c3.296 3.453 8.483 5.917 13.247 6.046.448.008.896.01 1.345.01.276 0 .553.003.835.005 4.917.019 9.864-1.73 13.577-4.995l.303-.267c.299-.263.592-.527.881-.799l.262-.247c.787-.756 1.451-1.599 2.104-2.474l.151-.201c1.929-2.606 3.117-5.723 3.635-8.909l.059-.362c.194-1.296.21-2.573.207-3.883l.003-.74v-2.02c0-.73 0-1.459.005-2.188v-3.782c0-1.827.004-3.654.007-5.481 0-.939 0-1.877.004-2.816l.003-6.082v-.268c.005-2.876.008-5.75.008-8.627l.013-8.883c.004-1.823.004-3.646.004-5.469 0-1.33.004-2.659.004-3.989.003-.721.003-1.442.003-2.159v-1.98c.005-.237.005-.473 0-.711-.011-8.523 3.072-17.023 8.941-23.268.244-.263.483-.531.717-.805.416-.48.861-.918 1.324-1.352l.252-.232a26.259 26.259 0 0 1 2.184-1.812l.46-.349c1.39-1.058 2.837-1.951 4.375-2.775l.318-.172c5.005-2.666 10.5-3.877 16.15-3.889Z" style="transform-box:fill-box;transform-origin:50% 50%" transform="rotate(-31 0 0)"/></svg>
@@ -0,0 +1,153 @@
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
+ }
131
+
132
+ ::backdrop {
133
+ backdrop-filter: blur(4px);
134
+ }
135
+
136
+ [popover] {
137
+ transition:
138
+ opacity 150ms ease-in-out,
139
+ display 150ms ease-out allow-discrete,
140
+ overlay 150ms ease-out allow-discrete;
141
+ opacity: 0;
142
+ }
143
+
144
+ [popover]:popover-open {
145
+ opacity: 1;
146
+ display: block;
147
+ }
148
+
149
+ @starting-style {
150
+ [popover]:popover-open {
151
+ opacity: 0;
152
+ }
153
+ }
@@ -0,0 +1,5 @@
1
+ export default new Map([
2
+ ['en_US', 'English'],
3
+ ['de_DE', 'Deutsch (German)'],
4
+ ['ja_JP', '日本語 (Japanese)'],
5
+ ]);
@@ -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,5 @@
1
+ import defineEnv from '@nkzw/define-env';
2
+
3
+ export default defineEnv(['SERVER_URL'], {
4
+ SERVER_URL: import.meta.env.VITE_SERVER_URL,
5
+ });
@@ -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
+ 'inline-flex items-center squircle border px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7
+ {
8
+ defaultVariants: {
9
+ variant: 'default',
10
+ },
11
+ variants: {
12
+ variant: {
13
+ default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
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,108 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { type ButtonHTMLAttributes, type ReactNode, useOptimistic, useTransition } from 'react';
4
+ import { useFormStatus } from 'react-dom';
5
+ import cx from '../lib/cx.tsx';
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap squircle text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
+ {
10
+ defaultVariants: {
11
+ size: 'default',
12
+ variant: 'default',
13
+ },
14
+ variants: {
15
+ size: {
16
+ default: 'h-10 px-3 py-2 active:pt-[11px] active:pb-[9px]',
17
+ icon: 'h-10 w-10',
18
+ lg: 'h-11 squircle px-6 active:pt-[11px] active:pb-[9px]',
19
+ sm: 'h-9 squircle px-2 active:pt-[11px] active:pb-[9px]',
20
+ },
21
+ variant: {
22
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
23
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
24
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
25
+ link: 'text-primary underline-offset-4 hover:underline',
26
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
27
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
28
+ },
29
+ },
30
+ },
31
+ );
32
+
33
+ const Button = ({
34
+ action,
35
+ asChild = false,
36
+ className,
37
+ disabled,
38
+ onClick: initialOnClick,
39
+ size,
40
+ variant,
41
+ ...props
42
+ }: ButtonHTMLAttributes<HTMLButtonElement> &
43
+ VariantProps<typeof buttonVariants> & {
44
+ action?: () => void;
45
+ asChild?: boolean;
46
+ }) => {
47
+ const Component = asChild ? Slot : 'button';
48
+
49
+ const [isPending, startTransition] = useTransition();
50
+
51
+ const onClick = initialOnClick || (action ? () => startTransition(action) : undefined);
52
+
53
+ return (
54
+ <Component
55
+ className={cx(buttonVariants({ className, size, variant }))}
56
+ disabled={disabled !== undefined ? disabled : isPending}
57
+ onClick={onClick}
58
+ {...props}
59
+ />
60
+ );
61
+ };
62
+
63
+ const AsyncButton = ({
64
+ action,
65
+ asChild = false,
66
+ children,
67
+ className,
68
+ disabled,
69
+ pendingPlaceholder = '...',
70
+ size,
71
+ variant,
72
+ ...props
73
+ }: ButtonHTMLAttributes<HTMLButtonElement> &
74
+ VariantProps<typeof buttonVariants> & {
75
+ action?: () => Promise<unknown> | void;
76
+ asChild?: boolean;
77
+ pendingPlaceholder?: ReactNode;
78
+ }) => {
79
+ const Component = asChild ? Slot : 'button';
80
+
81
+ const [optimisticIsPending, setOptimisticIsPending] = useOptimistic(false);
82
+ const [transitionIsPending, startTransition] = useTransition();
83
+ const { pending: formIsPending } = useFormStatus();
84
+
85
+ const isPending = transitionIsPending || optimisticIsPending || formIsPending;
86
+
87
+ const onClick = action
88
+ ? () => {
89
+ startTransition(async () => {
90
+ setOptimisticIsPending(true);
91
+ await action();
92
+ });
93
+ }
94
+ : undefined;
95
+
96
+ return (
97
+ <Component
98
+ className={cx(buttonVariants({ className, size, variant }))}
99
+ disabled={disabled || isPending || undefined}
100
+ onClick={onClick}
101
+ {...props}
102
+ >
103
+ {isPending ? pendingPlaceholder : children}
104
+ </Component>
105
+ );
106
+ };
107
+
108
+ export { AsyncButton, Button, buttonVariants };
@@ -0,0 +1,32 @@
1
+ import { VStack } from '@nkzw/stack';
2
+ import { HTMLAttributes } from 'react';
3
+ import cx from '../lib/cx.tsx';
4
+
5
+ export default function Card({
6
+ children,
7
+ className,
8
+ ...props
9
+ }: {
10
+ children: React.ReactNode;
11
+ className?: string;
12
+ } & HTMLAttributes<HTMLDivElement>) {
13
+ return (
14
+ <div
15
+ className={cx(
16
+ 'relative overflow-hidden squircle border border-gray-200/70 bg-white/85 shadow-[0_16px_70px_-32px_rgba(15,23,42,0.45)] dark:border-neutral-800 dark:bg-neutral-900/80 transition duration-200 ease-out hover:shadow-[0_24px_80px_-38px_rgba(15,23,42,0.55)]',
17
+ className,
18
+ )}
19
+ {...props}
20
+ >
21
+ <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" />
22
+ <VStack
23
+ className="relative z-10 flex h-full w-full"
24
+ gap={16}
25
+ horizontalPadding={20}
26
+ verticalPadding={16}
27
+ >
28
+ {children}
29
+ </VStack>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,75 @@
1
+ import type { Comment } from '@app/server/src/trpc/views.ts';
2
+ import Stack from '@nkzw/stack';
3
+ import { X } from 'lucide-react';
4
+ import { useView, view, ViewRef } from 'react-fate';
5
+ import { useFateClient } from 'react-fate';
6
+ import { Button } from './Button.tsx';
7
+ import Link from './Link.tsx';
8
+
9
+ export const CommentView = view<Comment>()({
10
+ author: {
11
+ id: true,
12
+ name: true,
13
+ username: true,
14
+ },
15
+ content: true,
16
+ id: true,
17
+ });
18
+
19
+ export const CommentViewWithPostCount = view<Comment>()({
20
+ ...CommentView,
21
+ post: { commentCount: true },
22
+ });
23
+
24
+ export default function CommentCard({
25
+ comment: commentRef,
26
+ link,
27
+ post,
28
+ }: {
29
+ comment: ViewRef<'Comment'>;
30
+ link?: boolean;
31
+ post: { commentCount: number; id: string; title: string };
32
+ }) {
33
+ const fate = useFateClient();
34
+ const comment = useView(CommentView, commentRef);
35
+ const { author } = comment;
36
+
37
+ return (
38
+ <div
39
+ 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"
40
+ key={comment.id}
41
+ >
42
+ <Stack between gap={16}>
43
+ <p className="font-medium text-gray-900 dark:text-gray-200">
44
+ {author?.name ?? 'Anonymous'}
45
+ </p>
46
+ <Button
47
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
48
+ onClick={async () => {
49
+ await fate.mutations.comment.delete({
50
+ delete: true,
51
+ input: { id: comment.id },
52
+ optimistic: {
53
+ post: { commentCount: post.commentCount - 1, id: post.id },
54
+ },
55
+ view: view<Comment>()({
56
+ id: true,
57
+ post: { commentCount: true },
58
+ }),
59
+ });
60
+ }}
61
+ size="sm"
62
+ variant="ghost"
63
+ >
64
+ <X size={14} />
65
+ </Button>
66
+ </Stack>
67
+ <p className="text-foreground/80">{comment.content}</p>
68
+ {link && (
69
+ <Link className="text-blue-500 underline hover:no-underline" to={`/post/${post.id}`}>
70
+ {post.title}
71
+ </Link>
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -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
+ <fbt desc="Try again button">Try again</fbt>
15
+ </a>
16
+ </div>
17
+ </VStack>
18
+ );
19
+ }