create-fate 1.0.3 → 1.3.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 (208) hide show
  1. package/README.md +3 -0
  2. package/bin/create-fate.mjs +594 -9
  3. package/package.json +2 -2
  4. package/templates/fate/README.md +3 -0
  5. package/templates/fate/_shared/vue-app/package.json +54 -0
  6. package/templates/fate/_shared/vue-app/pages/index.vue +57 -0
  7. package/templates/fate/_shared/vue-app/pages/layout.vue +32 -0
  8. package/templates/fate/_shared/vue-app/pages/post/[id].vue +24 -0
  9. package/templates/fate/_shared/vue-app/src/App.css +63 -0
  10. package/templates/fate/_shared/vue-app/src/fateViews.ts +14 -0
  11. package/templates/fate/_shared/vue-app/src/lib/cx.ts +6 -0
  12. package/templates/fate/_shared/vue-app/src/lib/env.ts +3 -0
  13. package/templates/fate/_shared/vue-app/src/ui/Button.vue +67 -0
  14. package/templates/fate/_shared/vue-app/src/ui/Card.vue +16 -0
  15. package/templates/fate/_shared/vue-app/src/ui/Error.vue +9 -0
  16. package/templates/fate/_shared/vue-app/src/ui/ErrorBoundary.vue +22 -0
  17. package/templates/fate/_shared/vue-app/src/ui/Header.vue +35 -0
  18. package/templates/fate/_shared/vue-app/src/ui/Link.vue +15 -0
  19. package/templates/fate/_shared/vue-app/src/ui/PostCard.vue +29 -0
  20. package/templates/fate/_shared/vue-app/src/ui/Section.vue +19 -0
  21. package/templates/fate/_shared/vue-app/src/ui/Thinking.vue +11 -0
  22. package/templates/fate/_shared/vue-app/src/ui/UserCard.vue +23 -0
  23. package/templates/fate/_shared/vue-app/src/vue-shims.d.ts +6 -0
  24. package/templates/fate/_shared/vue-app/tsconfig.json +22 -0
  25. package/templates/fate/_shared/vue-app/vite.config.ts +58 -0
  26. package/templates/fate/_shared/vue-app/void.json +5 -0
  27. package/templates/fate/cloudflare/.github/workflows/push.yml +27 -0
  28. package/templates/fate/cloudflare/.vite-hooks/pre-commit +1 -0
  29. package/templates/fate/cloudflare/.vscode/extensions.json +12 -0
  30. package/templates/fate/cloudflare/.vscode/settings.json +25 -0
  31. package/templates/fate/cloudflare/AGENTS.md +55 -0
  32. package/templates/fate/cloudflare/README.md +126 -0
  33. package/templates/fate/cloudflare/_gitignore +20 -0
  34. package/templates/fate/cloudflare/client/package.json +56 -0
  35. package/templates/fate/cloudflare/client/pages/index.tsx +96 -0
  36. package/templates/fate/cloudflare/client/pages/layout.tsx +82 -0
  37. package/templates/fate/cloudflare/client/pages/login.tsx +107 -0
  38. package/templates/fate/cloudflare/client/pages/post/[id].tsx +22 -0
  39. package/templates/fate/cloudflare/client/pages/search.tsx +99 -0
  40. package/templates/fate/cloudflare/client/public/icon.svg +1 -0
  41. package/templates/fate/cloudflare/client/src/App.css +153 -0
  42. package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +5 -0
  43. package/templates/fate/cloudflare/client/src/lib/cx.tsx +6 -0
  44. package/templates/fate/cloudflare/client/src/lib/env.tsx +5 -0
  45. package/templates/fate/cloudflare/client/src/ui/Badge.tsx +31 -0
  46. package/templates/fate/cloudflare/client/src/ui/Button.tsx +108 -0
  47. package/templates/fate/cloudflare/client/src/ui/Card.tsx +32 -0
  48. package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +75 -0
  49. package/templates/fate/cloudflare/client/src/ui/Error.tsx +19 -0
  50. package/templates/fate/cloudflare/client/src/ui/H2.tsx +15 -0
  51. package/templates/fate/cloudflare/client/src/ui/H3.tsx +20 -0
  52. package/templates/fate/cloudflare/client/src/ui/Header.tsx +98 -0
  53. package/templates/fate/cloudflare/client/src/ui/Input.tsx +17 -0
  54. package/templates/fate/cloudflare/client/src/ui/Link.tsx +11 -0
  55. package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +52 -0
  56. package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +303 -0
  57. package/templates/fate/cloudflare/client/src/ui/Section.tsx +23 -0
  58. package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +131 -0
  59. package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +6 -0
  60. package/templates/fate/cloudflare/client/translations/de_DE.json +689 -0
  61. package/templates/fate/cloudflare/client/translations/ja_JP.json +689 -0
  62. package/templates/fate/cloudflare/client/vite.config.ts +45 -0
  63. package/templates/fate/cloudflare/client/void.json +29 -0
  64. package/templates/fate/cloudflare/package.json +46 -0
  65. package/templates/fate/cloudflare/pnpm-workspace.yaml +39 -0
  66. package/templates/fate/cloudflare/server/.env.example +1 -0
  67. package/templates/fate/cloudflare/server/db/db.ts +30 -0
  68. package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +133 -0
  69. package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +170 -0
  70. package/templates/fate/cloudflare/server/db/migrations/meta/20260508073357_snapshot.json +868 -0
  71. package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +20 -0
  72. package/templates/fate/cloudflare/server/db/queries.ts +211 -0
  73. package/templates/fate/cloudflare/server/db/schema.ts +332 -0
  74. package/templates/fate/cloudflare/server/package.json +34 -0
  75. package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +17 -0
  76. package/templates/fate/cloudflare/server/src/fate/context.ts +15 -0
  77. package/templates/fate/cloudflare/server/src/fate/live.ts +7 -0
  78. package/templates/fate/cloudflare/server/src/fate/server.ts +411 -0
  79. package/templates/fate/cloudflare/server/src/fate/views.ts +164 -0
  80. package/templates/fate/cloudflare/server/src/index.ts +80 -0
  81. package/templates/fate/cloudflare/server/src/lib/auth.ts +66 -0
  82. package/templates/fate/cloudflare/server/src/lib/origins.ts +17 -0
  83. package/templates/fate/cloudflare/server/src/router.ts +2 -0
  84. package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +17 -0
  85. package/templates/fate/cloudflare/server/tsconfig.json +10 -0
  86. package/templates/fate/cloudflare/server/wrangler.jsonc +29 -0
  87. package/templates/fate/cloudflare/tsconfig.json +33 -0
  88. package/templates/fate/cloudflare/vite.config.ts +72 -0
  89. package/templates/fate/drizzle/README.md +2 -8
  90. package/templates/fate/drizzle/client/pages/layout.tsx +2 -2
  91. package/templates/fate/drizzle/client/src/ui/PostCard.tsx +38 -15
  92. package/templates/fate/drizzle/pnpm-workspace.yaml +9 -3
  93. package/templates/fate/drizzle/server/vite.config.ts +1 -1
  94. package/templates/fate/graphql/.github/workflows/push.yml +27 -0
  95. package/templates/fate/graphql/.vite-hooks/pre-commit +1 -0
  96. package/templates/fate/graphql/.vscode/extensions.json +12 -0
  97. package/templates/fate/graphql/.vscode/settings.json +25 -0
  98. package/templates/fate/graphql/AGENTS.md +55 -0
  99. package/templates/fate/graphql/README.md +121 -0
  100. package/templates/fate/graphql/_gitignore +23 -0
  101. package/templates/fate/graphql/client/package.json +57 -0
  102. package/templates/fate/graphql/client/pages/index.tsx +96 -0
  103. package/templates/fate/graphql/client/pages/layout.tsx +83 -0
  104. package/templates/fate/graphql/client/pages/login.tsx +107 -0
  105. package/templates/fate/graphql/client/pages/post/[id].tsx +22 -0
  106. package/templates/fate/graphql/client/pages/search.tsx +99 -0
  107. package/templates/fate/graphql/client/public/icon.svg +1 -0
  108. package/templates/fate/graphql/client/src/App.css +153 -0
  109. package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +5 -0
  110. package/templates/fate/graphql/client/src/lib/cx.tsx +6 -0
  111. package/templates/fate/graphql/client/src/lib/env.tsx +5 -0
  112. package/templates/fate/graphql/client/src/ui/Badge.tsx +31 -0
  113. package/templates/fate/graphql/client/src/ui/Button.tsx +108 -0
  114. package/templates/fate/graphql/client/src/ui/Card.tsx +32 -0
  115. package/templates/fate/graphql/client/src/ui/CommentCard.tsx +75 -0
  116. package/templates/fate/graphql/client/src/ui/Error.tsx +19 -0
  117. package/templates/fate/graphql/client/src/ui/H2.tsx +15 -0
  118. package/templates/fate/graphql/client/src/ui/H3.tsx +20 -0
  119. package/templates/fate/graphql/client/src/ui/Header.tsx +98 -0
  120. package/templates/fate/graphql/client/src/ui/Input.tsx +17 -0
  121. package/templates/fate/graphql/client/src/ui/Link.tsx +11 -0
  122. package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +52 -0
  123. package/templates/fate/graphql/client/src/ui/PostCard.tsx +303 -0
  124. package/templates/fate/graphql/client/src/ui/Section.tsx +23 -0
  125. package/templates/fate/graphql/client/src/ui/UserCard.tsx +131 -0
  126. package/templates/fate/graphql/client/src/user/AuthClient.tsx +6 -0
  127. package/templates/fate/graphql/client/translations/de_DE.json +689 -0
  128. package/templates/fate/graphql/client/translations/ja_JP.json +689 -0
  129. package/templates/fate/graphql/client/vite.config.ts +40 -0
  130. package/templates/fate/graphql/client/void.json +29 -0
  131. package/templates/fate/graphql/docker-compose.yml +11 -0
  132. package/templates/fate/graphql/package.json +47 -0
  133. package/templates/fate/graphql/pnpm-workspace.yaml +45 -0
  134. package/templates/fate/graphql/server/.env.example +5 -0
  135. package/templates/fate/graphql/server/README.md +27 -0
  136. package/templates/fate/graphql/server/package.json +64 -0
  137. package/templates/fate/graphql/server/prisma.config.ts +17 -0
  138. package/templates/fate/graphql/server/scripts/generate-graphql.tsx +40 -0
  139. package/templates/fate/graphql/server/src/__tests__/index.test.tsx +5 -0
  140. package/templates/fate/graphql/server/src/app.tsx +47 -0
  141. package/templates/fate/graphql/server/src/graphql/builder.tsx +121 -0
  142. package/templates/fate/graphql/server/src/graphql/context.tsx +22 -0
  143. package/templates/fate/graphql/server/src/graphql/fate.ts +85 -0
  144. package/templates/fate/graphql/server/src/graphql/lib/authDirectives.tsx +93 -0
  145. package/templates/fate/graphql/server/src/graphql/lib/decodeGlobalID.tsx +12 -0
  146. package/templates/fate/graphql/server/src/graphql/lib/decodeIDOrThrow.tsx +10 -0
  147. package/templates/fate/graphql/server/src/graphql/lib/encodeGlobalID.tsx +5 -0
  148. package/templates/fate/graphql/server/src/graphql/live.tsx +241 -0
  149. package/templates/fate/graphql/server/src/graphql/mutations.tsx +265 -0
  150. package/templates/fate/graphql/server/src/graphql/nodes/Category.tsx +35 -0
  151. package/templates/fate/graphql/server/src/graphql/nodes/Comment.tsx +46 -0
  152. package/templates/fate/graphql/server/src/graphql/nodes/Event.tsx +71 -0
  153. package/templates/fate/graphql/server/src/graphql/nodes/Post.tsx +45 -0
  154. package/templates/fate/graphql/server/src/graphql/nodes/Tag.tsx +11 -0
  155. package/templates/fate/graphql/server/src/graphql/nodes/User.tsx +28 -0
  156. package/templates/fate/graphql/server/src/graphql/schema.tsx +16 -0
  157. package/templates/fate/graphql/server/src/graphql/views.ts +193 -0
  158. package/templates/fate/graphql/server/src/index.tsx +45 -0
  159. package/templates/fate/graphql/server/src/lib/auth.tsx +31 -0
  160. package/templates/fate/graphql/server/src/lib/env.tsx +3 -0
  161. package/templates/fate/graphql/server/src/lib/origins.ts +18 -0
  162. package/templates/fate/graphql/server/src/prisma/prisma.tsx +9 -0
  163. package/templates/fate/graphql/server/src/prisma/schema.prisma +181 -0
  164. package/templates/fate/graphql/server/src/prisma/seed.tsx +161 -0
  165. package/templates/fate/graphql/server/src/user/SessionUser.tsx +17 -0
  166. package/templates/fate/graphql/server/src/user/isAdmin.tsx +3 -0
  167. package/templates/fate/graphql/server/tsconfig.json +33 -0
  168. package/templates/fate/graphql/server/vite.config.ts +55 -0
  169. package/templates/fate/graphql/tsconfig.json +33 -0
  170. package/templates/fate/graphql/vite.config.ts +80 -0
  171. package/templates/fate/graphql-client/.env.example +3 -0
  172. package/templates/fate/graphql-client/AGENTS.md +3 -0
  173. package/templates/fate/graphql-client/README.md +69 -0
  174. package/templates/fate/graphql-client/_gitignore +14 -0
  175. package/templates/fate/graphql-client/package.json +62 -0
  176. package/templates/fate/graphql-client/pages/index.tsx +69 -0
  177. package/templates/fate/graphql-client/pages/layout.tsx +41 -0
  178. package/templates/fate/graphql-client/pnpm-workspace.yaml +38 -0
  179. package/templates/fate/graphql-client/public/icon.svg +8 -0
  180. package/templates/fate/graphql-client/src/App.css +28 -0
  181. package/templates/fate/graphql-client/src/fate/graphql.ts +52 -0
  182. package/templates/fate/graphql-client/tsconfig.json +30 -0
  183. package/templates/fate/graphql-client/vite.config.ts +87 -0
  184. package/templates/fate/graphql-client/void.json +29 -0
  185. package/templates/fate/http/README.md +2 -8
  186. package/templates/fate/http/client/pages/layout.tsx +2 -2
  187. package/templates/fate/http/client/src/ui/PostCard.tsx +37 -15
  188. package/templates/fate/http/pnpm-workspace.yaml +9 -3
  189. package/templates/fate/http/server/vite.config.ts +1 -1
  190. package/templates/fate/prisma/README.md +2 -8
  191. package/templates/fate/prisma/client/pages/layout.tsx +2 -2
  192. package/templates/fate/prisma/client/src/ui/PostCard.tsx +37 -15
  193. package/templates/fate/prisma/pnpm-workspace.yaml +9 -3
  194. package/templates/fate/prisma/server/vite.config.ts +1 -1
  195. package/templates/fate/prisma/vite.config.ts +1 -1
  196. package/templates/fate/void/package.json +10 -2
  197. package/templates/fate/void/pages/login.tsx +3 -3
  198. package/templates/fate/void/pnpm-workspace.yaml +12 -4
  199. package/templates/fate/void/src/ui/Button.tsx +42 -26
  200. package/templates/fate/void/src/ui/CategoryCard.tsx +3 -7
  201. package/templates/fate/void/src/ui/PostCard.tsx +38 -15
  202. package/templates/fate/void/vite.config.ts +15 -14
  203. package/templates/fate/drizzle/public/fate-logo-dark.svg +0 -4
  204. package/templates/fate/drizzle/public/fate-logo.svg +0 -4
  205. package/templates/fate/http/public/fate-logo-dark.svg +0 -4
  206. package/templates/fate/http/public/fate-logo.svg +0 -4
  207. package/templates/fate/prisma/public/fate-logo-dark.svg +0 -4
  208. package/templates/fate/prisma/public/fate-logo.svg +0 -4
@@ -0,0 +1,99 @@
1
+ import type { Comment, Post } from '@app/server/src/router.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 isDevelopment = import.meta.env.DEV;
27
+
28
+ const CommentResult = ({ comment: commentRef }: { comment: ViewRef<'Comment'> }) => {
29
+ const comment = useView(CommentSearchView, commentRef);
30
+ const post = useView(CommentPostView, comment.post);
31
+
32
+ return <CommentCard comment={comment} link post={post} />;
33
+ };
34
+
35
+ const SearchResults = ({ isStale, query }: { isStale: boolean; query: string }) => {
36
+ const { commentSearch } = useRequest({
37
+ commentSearch: { args: { query }, list: CommentSearchView },
38
+ });
39
+
40
+ if (commentSearch.length === 0) {
41
+ return (
42
+ <p>
43
+ <fbt desc="No matches text">
44
+ No matches for{' '}
45
+ <i>
46
+ &quot;<fbt:param name="query">{query}</fbt:param>&quot;
47
+ </i>
48
+ </fbt>
49
+ </p>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <VStack className={cx(isStale && 'opacity-50')} gap={12}>
55
+ {commentSearch.map((comment) => (
56
+ <CommentResult comment={comment} key={comment.id} />
57
+ ))}
58
+ </VStack>
59
+ );
60
+ };
61
+
62
+ export default function SearchPage() {
63
+ const [query, setQuery] = useState('');
64
+ const deferredQuery = useDeferredValue(query);
65
+ const isStale = query !== deferredQuery;
66
+
67
+ return (
68
+ <Section>
69
+ <Card>
70
+ <Stack alignCenter between gap={16}>
71
+ <Input
72
+ className="w-64"
73
+ onChange={(e) => setQuery(e.target.value)}
74
+ placeholder={fbs('Search comments...', 'Search input placeholder')}
75
+ ref={(ref) => ref?.focus()}
76
+ value={query}
77
+ />
78
+ {isDevelopment ? (
79
+ <div className="text-xs text-muted-foreground">
80
+ <fbt desc="Slowdown label explanation">500ms artificial slowdown</fbt>
81
+ </div>
82
+ ) : null}
83
+ </Stack>
84
+
85
+ <ErrorBoundary FallbackComponent={Error}>
86
+ <Suspense
87
+ fallback={
88
+ <h2>
89
+ <fbt desc="Text for thinking/loading screen">Thinking…</fbt>
90
+ </h2>
91
+ }
92
+ >
93
+ {query.trim().length > 0 ? <SearchResults isStale={isStale} query={query} /> : null}
94
+ </Suspense>
95
+ </ErrorBoundary>
96
+ </Card>
97
+ </Section>
98
+ );
99
+ }
@@ -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/router.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
+ }
@@ -0,0 +1,15 @@
1
+ import cx from '../lib/cx.tsx';
2
+
3
+ export default function H2({
4
+ children,
5
+ className,
6
+ }: {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ }) {
10
+ return (
11
+ <h2 className={cx('text-2xl font-semibold text-gray-900 dark:text-gray-50', className)}>
12
+ {children}
13
+ </h2>
14
+ );
15
+ }
@@ -0,0 +1,20 @@
1
+ import cx from '../lib/cx.tsx';
2
+
3
+ export default function H2({
4
+ children,
5
+ className,
6
+ }: {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ }) {
10
+ return (
11
+ <h3
12
+ className={cx(
13
+ 'bg-linear-to-r from-slate-900 to-slate-600 bg-clip-text text-lg font-semibold text-transparent transition dark:from-white dark:to-slate-200 uppercase tracking-widest duration-150 opacity-100 hover:opacity-70 active:translate-y-[1.5px]',
14
+ className,
15
+ )}
16
+ >
17
+ {children}
18
+ </h3>
19
+ );
20
+ }