create-fate 1.5.2 → 1.6.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.
- package/README.md +1 -2
- package/bin/create-fate.mjs +14 -50
- package/package.json +1 -1
- package/templates/fate/README.md +1 -2
- package/templates/fate/_shared/vue-app/package.json +4 -3
- package/templates/fate/_shared/vue-app/vite.config.ts +18 -11
- package/templates/fate/drizzle/client/package.json +6 -6
- package/templates/fate/graphql/client/package.json +6 -6
- package/templates/fate/graphql/server/.gitignore +5 -0
- package/templates/fate/graphql/server/node_modules/.bin/esbuild +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/eslint +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/fate +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/intent +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/jiti +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/nft +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/nodemon +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/oxfmt +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/oxlint +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/prisma +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/prisma-json-types-generator +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/prisma-pothos-types +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/tsc +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/tsgolint +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/tsx +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/vitest +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/vp +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/vpr +22 -0
- package/templates/fate/graphql/server/node_modules/.bin/yaml +22 -0
- package/templates/fate/graphql-client/README.md +11 -1
- package/templates/fate/graphql-client/env.ts +6 -0
- package/templates/fate/graphql-client/package.json +6 -6
- package/templates/fate/http/client/package.json +6 -6
- package/templates/fate/prisma/client/package.json +6 -6
- package/templates/fate/void/README.md +12 -0
- package/templates/fate/void/db/migrations/20260918045815_legal_dragon_lord.sql +15 -0
- package/templates/fate/{cloudflare/server/db/migrations/meta/20260508073357_snapshot.json → void/db/migrations/meta/20260918045815_snapshot.json} +174 -124
- package/templates/fate/void/db/migrations/meta/_journal.json +7 -0
- package/templates/fate/void/package.json +6 -7
- package/templates/fate/cloudflare/.github/workflows/push.yml +0 -27
- package/templates/fate/cloudflare/.vite-hooks/pre-commit +0 -1
- package/templates/fate/cloudflare/.vscode/extensions.json +0 -12
- package/templates/fate/cloudflare/.vscode/settings.json +0 -25
- package/templates/fate/cloudflare/AGENTS.md +0 -55
- package/templates/fate/cloudflare/README.md +0 -137
- package/templates/fate/cloudflare/_gitignore +0 -20
- package/templates/fate/cloudflare/client/node_modules/.bin/browserslist +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/cf-wrangler +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/drizzle-kit +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/esbuild +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/fate +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/fbtee +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/intent +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/jiti +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/oxfmt +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/oxlint +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/rolldown +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/tsc +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/tsx +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/vitest +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/void +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/vp +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/vpr +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/workerd +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/wrangler +0 -22
- package/templates/fate/cloudflare/client/node_modules/.bin/wrangler2 +0 -22
- package/templates/fate/cloudflare/client/package.json +0 -62
- package/templates/fate/cloudflare/client/pages/index.tsx +0 -96
- package/templates/fate/cloudflare/client/pages/layout.tsx +0 -68
- package/templates/fate/cloudflare/client/pages/login.tsx +0 -107
- package/templates/fate/cloudflare/client/pages/post/[id].tsx +0 -22
- package/templates/fate/cloudflare/client/pages/search.tsx +0 -99
- package/templates/fate/cloudflare/client/public/icon.svg +0 -1
- package/templates/fate/cloudflare/client/src/App.css +0 -153
- package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +0 -5
- package/templates/fate/cloudflare/client/src/lib/LocaleContext.tsx +0 -61
- package/templates/fate/cloudflare/client/src/lib/cx.tsx +0 -6
- package/templates/fate/cloudflare/client/src/lib/env.tsx +0 -5
- package/templates/fate/cloudflare/client/src/ui/Badge.tsx +0 -31
- package/templates/fate/cloudflare/client/src/ui/Button.tsx +0 -108
- package/templates/fate/cloudflare/client/src/ui/Card.tsx +0 -32
- package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +0 -75
- package/templates/fate/cloudflare/client/src/ui/Error.tsx +0 -19
- package/templates/fate/cloudflare/client/src/ui/H2.tsx +0 -15
- package/templates/fate/cloudflare/client/src/ui/H3.tsx +0 -20
- package/templates/fate/cloudflare/client/src/ui/Header.tsx +0 -98
- package/templates/fate/cloudflare/client/src/ui/Input.tsx +0 -17
- package/templates/fate/cloudflare/client/src/ui/Link.tsx +0 -11
- package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +0 -34
- package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +0 -303
- package/templates/fate/cloudflare/client/src/ui/Section.tsx +0 -23
- package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +0 -131
- package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +0 -6
- package/templates/fate/cloudflare/client/start.mjs +0 -11
- package/templates/fate/cloudflare/client/translations/de-DE.json +0 -632
- package/templates/fate/cloudflare/client/translations/ja-JP.json +0 -632
- package/templates/fate/cloudflare/client/vite.config.ts +0 -41
- package/templates/fate/cloudflare/client/void.json +0 -29
- package/templates/fate/cloudflare/package.json +0 -42
- package/templates/fate/cloudflare/pnpm-workspace.yaml +0 -41
- package/templates/fate/cloudflare/server/.env.example +0 -1
- package/templates/fate/cloudflare/server/db/db.ts +0 -30
- package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +0 -134
- package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +0 -170
- package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +0 -20
- package/templates/fate/cloudflare/server/db/queries.ts +0 -211
- package/templates/fate/cloudflare/server/db/schema.ts +0 -331
- package/templates/fate/cloudflare/server/node_modules/.bin/cf-wrangler +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/drizzle-kit +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/esbuild +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/fate +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/intent +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/jiti +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/oxfmt +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/oxlint +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/tsc +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/tsx +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/vitest +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/vp +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/vpr +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/workerd +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/wrangler +0 -22
- package/templates/fate/cloudflare/server/node_modules/.bin/wrangler2 +0 -22
- package/templates/fate/cloudflare/server/package.json +0 -34
- package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +0 -17
- package/templates/fate/cloudflare/server/src/fate/context.ts +0 -15
- package/templates/fate/cloudflare/server/src/fate/live.ts +0 -7
- package/templates/fate/cloudflare/server/src/fate/server.ts +0 -414
- package/templates/fate/cloudflare/server/src/fate/views.ts +0 -164
- package/templates/fate/cloudflare/server/src/index.ts +0 -80
- package/templates/fate/cloudflare/server/src/lib/auth.ts +0 -64
- package/templates/fate/cloudflare/server/src/lib/origins.ts +0 -17
- package/templates/fate/cloudflare/server/src/router.ts +0 -2
- package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +0 -17
- package/templates/fate/cloudflare/server/tsconfig.json +0 -10
- package/templates/fate/cloudflare/server/wrangler.jsonc +0 -29
- package/templates/fate/cloudflare/tsconfig.json +0 -24
- package/templates/fate/cloudflare/vite.config.ts +0 -75
- package/templates/fate/graphql-client/.env.example +0 -3
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import type { Comment as InlineComment, Post } from '@app/server/src/router.ts';
|
|
2
|
-
import Stack, { VStack } from '@nkzw/stack';
|
|
3
|
-
import { cx } from 'class-variance-authority';
|
|
4
|
-
import { fbs } from 'fbtee';
|
|
5
|
-
import {
|
|
6
|
-
KeyboardEvent,
|
|
7
|
-
Suspense,
|
|
8
|
-
startTransition,
|
|
9
|
-
useActionState,
|
|
10
|
-
useCallback,
|
|
11
|
-
useEffect,
|
|
12
|
-
useState,
|
|
13
|
-
} from 'react';
|
|
14
|
-
import { ErrorBoundary } from 'react-error-boundary';
|
|
15
|
-
import {
|
|
16
|
-
defer,
|
|
17
|
-
type Deferred,
|
|
18
|
-
useListView,
|
|
19
|
-
useFateClient,
|
|
20
|
-
useView,
|
|
21
|
-
view,
|
|
22
|
-
ViewRef,
|
|
23
|
-
} from 'react-fate';
|
|
24
|
-
import { Button } from '../ui/Button.tsx';
|
|
25
|
-
import Card from '../ui/Card.tsx';
|
|
26
|
-
import AuthClient from '../user/AuthClient.tsx';
|
|
27
|
-
import CommentCard, { CommentView } from './CommentCard.tsx';
|
|
28
|
-
import H3 from './H3.tsx';
|
|
29
|
-
import Link from './Link.tsx';
|
|
30
|
-
import { UserView } from './UserCard.tsx';
|
|
31
|
-
|
|
32
|
-
const CommentConnectionView = {
|
|
33
|
-
args: { first: 3 },
|
|
34
|
-
items: {
|
|
35
|
-
node: CommentView,
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const PostView = view<Post>()({
|
|
40
|
-
author: UserView,
|
|
41
|
-
commentCount: true,
|
|
42
|
-
comments: defer(CommentConnectionView),
|
|
43
|
-
content: true,
|
|
44
|
-
id: true,
|
|
45
|
-
likes: true,
|
|
46
|
-
title: true,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const PostComments = ({
|
|
50
|
-
comments,
|
|
51
|
-
post,
|
|
52
|
-
}: {
|
|
53
|
-
comments: Deferred<{ items: ReadonlyArray<{ node: ViewRef<'Comment'> }> }>;
|
|
54
|
-
post: { commentCount: number; id: string; title: string };
|
|
55
|
-
}) => {
|
|
56
|
-
const [items, loadNext] = useListView(CommentConnectionView, comments);
|
|
57
|
-
|
|
58
|
-
return items.length > 0 ? (
|
|
59
|
-
<VStack gap={12}>
|
|
60
|
-
{items.map(({ node }) => (
|
|
61
|
-
<CommentCard comment={node} key={node.id} post={post} />
|
|
62
|
-
))}
|
|
63
|
-
{loadNext ? (
|
|
64
|
-
<Button onClick={loadNext} variant="ghost">
|
|
65
|
-
<fbt desc="Load more comments button">Load more comments</fbt>
|
|
66
|
-
</Button>
|
|
67
|
-
) : null}
|
|
68
|
-
</VStack>
|
|
69
|
-
) : null;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const CommentInput = ({
|
|
73
|
-
error,
|
|
74
|
-
post,
|
|
75
|
-
}: {
|
|
76
|
-
error?: unknown;
|
|
77
|
-
post: { commentCount: number; id: string };
|
|
78
|
-
}) => {
|
|
79
|
-
const fate = useFateClient();
|
|
80
|
-
const { data: session } = AuthClient.useSession();
|
|
81
|
-
const user = session?.user;
|
|
82
|
-
const [commentText, setCommentText] = useState('');
|
|
83
|
-
|
|
84
|
-
const [addCommentResult, handleAddComment, addCommentIsPending] = useActionState(async () => {
|
|
85
|
-
const content = commentText.trim();
|
|
86
|
-
|
|
87
|
-
if (!content) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const result = await fate.mutations.comment.add({
|
|
92
|
-
input: { content, postId: post.id },
|
|
93
|
-
optimistic: {
|
|
94
|
-
author: user
|
|
95
|
-
? {
|
|
96
|
-
id: user.id,
|
|
97
|
-
name: user.name ?? 'Anonymous',
|
|
98
|
-
}
|
|
99
|
-
: null,
|
|
100
|
-
content,
|
|
101
|
-
id: `optimistic:${Date.now().toString(36)}`,
|
|
102
|
-
post: { commentCount: post.commentCount + 1, id: post.id },
|
|
103
|
-
},
|
|
104
|
-
view: view<InlineComment>()({
|
|
105
|
-
...CommentView,
|
|
106
|
-
post: { commentCount: true },
|
|
107
|
-
}),
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
setCommentText('');
|
|
111
|
-
|
|
112
|
-
return result;
|
|
113
|
-
}, null);
|
|
114
|
-
|
|
115
|
-
const maybeSubmitComment = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
116
|
-
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
117
|
-
startTransition(() => handleAddComment());
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const commentingIsDisabled = addCommentIsPending || commentText.trim().length === 0;
|
|
122
|
-
|
|
123
|
-
const anyServerError = error || addCommentResult?.error;
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<VStack action={handleAddComment} as="form" gap>
|
|
127
|
-
<span className="text-sm font-medium text-foreground">
|
|
128
|
-
<fbt desc="Comment add label">Add a comment</fbt>
|
|
129
|
-
</span>
|
|
130
|
-
<textarea
|
|
131
|
-
className="squircle min-h-20 w-full border border-gray-200/80 bg-gray-100/50 p-3 text-sm placeholder-gray-500 transition outline-none focus:border-gray-500 focus:ring-2 focus:ring-gray-200 disabled:opacity-50 dark:border-neutral-800 dark:bg-neutral-900/40 dark:placeholder-gray-400"
|
|
132
|
-
disabled={addCommentIsPending}
|
|
133
|
-
onChange={(event) => setCommentText(event.target.value)}
|
|
134
|
-
onKeyDown={maybeSubmitComment}
|
|
135
|
-
placeholder={fbs('Share your thoughts...', 'Placeholder text for comment input')}
|
|
136
|
-
value={commentText}
|
|
137
|
-
/>
|
|
138
|
-
{anyServerError ? (
|
|
139
|
-
<p className="text-sm text-destructive">
|
|
140
|
-
{anyServerError instanceof Error ? (
|
|
141
|
-
anyServerError.message
|
|
142
|
-
) : (
|
|
143
|
-
<fbt desc="Error text">Something went wrong. Please try again.</fbt>
|
|
144
|
-
)}
|
|
145
|
-
</p>
|
|
146
|
-
) : null}
|
|
147
|
-
<Stack end gap>
|
|
148
|
-
<Button disabled={commentingIsDisabled} size="sm" type="submit" variant="secondary">
|
|
149
|
-
<fbt desc="Post comment button">Post comment</fbt>
|
|
150
|
-
</Button>
|
|
151
|
-
</Stack>
|
|
152
|
-
</VStack>
|
|
153
|
-
);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
export function PostCard({ detail, post: postRef }: { detail?: boolean; post: ViewRef<'Post'> }) {
|
|
157
|
-
const fate = useFateClient();
|
|
158
|
-
const post = useView(PostView, postRef);
|
|
159
|
-
const author = useView(UserView, post.author);
|
|
160
|
-
|
|
161
|
-
const [likeResult, likeAction, likeIsPending] = useActionState(fate.actions.post.like, null);
|
|
162
|
-
|
|
163
|
-
const [, unlikeAction, unlikeIsPending] = useActionState(fate.actions.post.unlike, null);
|
|
164
|
-
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (likeResult?.error) {
|
|
167
|
-
const timeout = setTimeout(() => startTransition(() => likeAction('reset')), 3000);
|
|
168
|
-
return () => clearTimeout(timeout);
|
|
169
|
-
}
|
|
170
|
-
}, [likeAction, likeResult]);
|
|
171
|
-
|
|
172
|
-
const handleLike = useCallback(
|
|
173
|
-
async (options?: { error?: 'boundary' | 'callSite'; slow?: boolean }) => {
|
|
174
|
-
likeAction({
|
|
175
|
-
input: { id: post.id, ...options },
|
|
176
|
-
optimistic: { likes: post.likes + 1 },
|
|
177
|
-
view: PostView,
|
|
178
|
-
});
|
|
179
|
-
},
|
|
180
|
-
[likeAction, post.id, post.likes],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const handleUnlike = useCallback(async () => {
|
|
184
|
-
unlikeAction({
|
|
185
|
-
input: { id: post.id },
|
|
186
|
-
optimistic: {
|
|
187
|
-
likes: Math.max(post.likes - 1, 0),
|
|
188
|
-
},
|
|
189
|
-
view: PostView,
|
|
190
|
-
});
|
|
191
|
-
}, [post.id, post.likes, unlikeAction]);
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<Card>
|
|
195
|
-
<VStack gap={16}>
|
|
196
|
-
<Stack alignStart between gap={16} wrap>
|
|
197
|
-
<VStack gap>
|
|
198
|
-
<Link to={`/post/${post.id}`}>
|
|
199
|
-
<H3>{post.title}</H3>
|
|
200
|
-
</Link>
|
|
201
|
-
</VStack>
|
|
202
|
-
<Stack alignCenter gap={12} wrap>
|
|
203
|
-
<Stack
|
|
204
|
-
alignCenter
|
|
205
|
-
className="squircle bg-slate-100 px-3 py-2 text-sm font-semibold text-slate-800 dark:bg-neutral-800 dark:text-white"
|
|
206
|
-
gap
|
|
207
|
-
>
|
|
208
|
-
<span>👍</span>
|
|
209
|
-
<span>
|
|
210
|
-
<fbt desc="Like count">
|
|
211
|
-
<fbt:plural count={post.likes} many="Likes" showCount="yes">
|
|
212
|
-
Like
|
|
213
|
-
</fbt:plural>
|
|
214
|
-
</fbt>
|
|
215
|
-
</span>
|
|
216
|
-
</Stack>
|
|
217
|
-
<Button action={handleLike} disabled={likeIsPending} size="sm" variant="outline">
|
|
218
|
-
<fbt desc="Like button">Like</fbt>
|
|
219
|
-
</Button>
|
|
220
|
-
{detail && (
|
|
221
|
-
<Button
|
|
222
|
-
action={() => handleLike({ slow: true })}
|
|
223
|
-
disabled={likeIsPending}
|
|
224
|
-
size="sm"
|
|
225
|
-
variant="outline"
|
|
226
|
-
>
|
|
227
|
-
<fbt desc="Like button with a slow request">Like (Slow)</fbt>
|
|
228
|
-
</Button>
|
|
229
|
-
)}
|
|
230
|
-
{detail && (
|
|
231
|
-
<Button
|
|
232
|
-
action={() => handleLike({ error: 'callSite' })}
|
|
233
|
-
className={cx(
|
|
234
|
-
'w-32 transition-colors duration-150',
|
|
235
|
-
likeResult?.error ? 'border-red-500 text-red-500 hover:text-red-500' : '',
|
|
236
|
-
)}
|
|
237
|
-
disabled={likeIsPending}
|
|
238
|
-
size="sm"
|
|
239
|
-
variant="outline"
|
|
240
|
-
>
|
|
241
|
-
{likeResult?.error ? (
|
|
242
|
-
<fbt desc="Very short error messages">Oops, try again!</fbt>
|
|
243
|
-
) : (
|
|
244
|
-
<fbt desc="Like button that triggers an error">Like (Error)</fbt>
|
|
245
|
-
)}
|
|
246
|
-
</Button>
|
|
247
|
-
)}
|
|
248
|
-
{detail && (
|
|
249
|
-
<Button
|
|
250
|
-
action={() => handleLike({ error: 'boundary' })}
|
|
251
|
-
disabled={likeIsPending}
|
|
252
|
-
size="sm"
|
|
253
|
-
variant="outline"
|
|
254
|
-
>
|
|
255
|
-
<fbt desc="Like button that triggers a network error">Like (Network Error)</fbt>
|
|
256
|
-
</Button>
|
|
257
|
-
)}
|
|
258
|
-
{detail && (
|
|
259
|
-
<Button
|
|
260
|
-
onClick={() =>
|
|
261
|
-
fate.mutations.post.like({
|
|
262
|
-
input: { id: post.id },
|
|
263
|
-
optimistic: { likes: post.likes + 1 },
|
|
264
|
-
view: PostView,
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
size="sm"
|
|
268
|
-
variant="outline"
|
|
269
|
-
>
|
|
270
|
-
<fbt desc="Like button that can be pressed many times">Like (Many)</fbt>
|
|
271
|
-
</Button>
|
|
272
|
-
)}
|
|
273
|
-
<Button
|
|
274
|
-
action={handleUnlike}
|
|
275
|
-
disabled={unlikeIsPending || post.likes === 0}
|
|
276
|
-
size="sm"
|
|
277
|
-
variant="outline"
|
|
278
|
-
>
|
|
279
|
-
<fbt desc="Unlike button">Unlike</fbt>
|
|
280
|
-
</Button>
|
|
281
|
-
</Stack>
|
|
282
|
-
</Stack>
|
|
283
|
-
<p className="text-sm leading-relaxed text-foreground/90 lg:text-base">{post.content}</p>
|
|
284
|
-
<p className="text-sm text-muted-foreground">- {author?.name ?? 'Unknown author'}</p>
|
|
285
|
-
<VStack gap={16}>
|
|
286
|
-
<h4 className="text-base font-semibold text-foreground">
|
|
287
|
-
<fbt desc="Comment headline">
|
|
288
|
-
<fbt:plural count={post.commentCount} many="Comments" showCount="ifMany">
|
|
289
|
-
One Comment
|
|
290
|
-
</fbt:plural>
|
|
291
|
-
</fbt>
|
|
292
|
-
</h4>
|
|
293
|
-
<Suspense fallback={null}>
|
|
294
|
-
<PostComments comments={post.comments} post={post} />
|
|
295
|
-
</Suspense>
|
|
296
|
-
<ErrorBoundary fallbackRender={({ error }) => <CommentInput error={error} post={post} />}>
|
|
297
|
-
<CommentInput post={post} />
|
|
298
|
-
</ErrorBoundary>
|
|
299
|
-
</VStack>
|
|
300
|
-
</VStack>
|
|
301
|
-
</Card>
|
|
302
|
-
);
|
|
303
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Gap, VStack } from '@nkzw/stack';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
3
|
-
import cx from '../lib/cx.tsx';
|
|
4
|
-
|
|
5
|
-
export default function Section({
|
|
6
|
-
children,
|
|
7
|
-
className,
|
|
8
|
-
gap,
|
|
9
|
-
}: {
|
|
10
|
-
children: ReactNode;
|
|
11
|
-
className?: string;
|
|
12
|
-
gap?: Gap;
|
|
13
|
-
}) {
|
|
14
|
-
return (
|
|
15
|
-
<VStack
|
|
16
|
-
as="section"
|
|
17
|
-
className={cx('max-w-8xl container mx-auto px-4 py-6 lg:px-8 lg:py-10', className)}
|
|
18
|
-
gap={gap}
|
|
19
|
-
>
|
|
20
|
-
{children}
|
|
21
|
-
</VStack>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { User } from '@app/server/src/router.ts';
|
|
2
|
-
import safeParse from '@nkzw/core/safeParse.js';
|
|
3
|
-
import Stack, { VStack } from '@nkzw/stack';
|
|
4
|
-
import { fbs } from 'fbtee';
|
|
5
|
-
import { ChangeEvent, useActionState, useState } from 'react';
|
|
6
|
-
import { view } from 'react-fate';
|
|
7
|
-
import { useFateClient } from 'react-fate';
|
|
8
|
-
import { ViewRef } from 'react-fate';
|
|
9
|
-
import { useView } from 'react-fate';
|
|
10
|
-
import { Button } from '../ui/Button.tsx';
|
|
11
|
-
import Card from '../ui/Card.tsx';
|
|
12
|
-
import AuthClient from '../user/AuthClient.tsx';
|
|
13
|
-
import H2 from './H2.tsx';
|
|
14
|
-
import Input from './Input.tsx';
|
|
15
|
-
|
|
16
|
-
export type SessionUser = {
|
|
17
|
-
id?: string | null;
|
|
18
|
-
name?: string | null;
|
|
19
|
-
username?: string | null;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const UserView = view<User>()({
|
|
23
|
-
id: true,
|
|
24
|
-
name: true,
|
|
25
|
-
username: true,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const UserNameForm = ({ user }: { user: SessionUser }) => {
|
|
29
|
-
const fate = useFateClient();
|
|
30
|
-
const [name, setName] = useState(user.name ?? '');
|
|
31
|
-
const [error, setError] = useState<string | null>(null);
|
|
32
|
-
|
|
33
|
-
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
34
|
-
setError(null);
|
|
35
|
-
setName(event.target.value);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const [, submitAction, isPending] = useActionState(async () => {
|
|
39
|
-
const id = user?.id;
|
|
40
|
-
if (!id) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const newName = name.trim();
|
|
45
|
-
setName(newName);
|
|
46
|
-
|
|
47
|
-
if (newName === user.name) {
|
|
48
|
-
setError(null);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
setError(null);
|
|
54
|
-
await fate.mutations.user.update({
|
|
55
|
-
input: { name: newName },
|
|
56
|
-
optimistic: {
|
|
57
|
-
id,
|
|
58
|
-
username: newName,
|
|
59
|
-
},
|
|
60
|
-
view: UserView,
|
|
61
|
-
});
|
|
62
|
-
await AuthClient.updateUser({ name });
|
|
63
|
-
} catch (error) {
|
|
64
|
-
setError(
|
|
65
|
-
(error instanceof Error &&
|
|
66
|
-
error.message &&
|
|
67
|
-
safeParse<Array<{ message: string }>>(error.message)?.[0]?.message) ||
|
|
68
|
-
'Failed to update user name.',
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
}, null);
|
|
72
|
-
|
|
73
|
-
const trimmedName = name.trim();
|
|
74
|
-
const originalName = user.name ?? '';
|
|
75
|
-
const isSaveDisabled = !user.id || !trimmedName || trimmedName === originalName || isPending;
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div>
|
|
79
|
-
<VStack action={submitAction} as="form" gap={12}>
|
|
80
|
-
<h3 className="font-semibold">
|
|
81
|
-
<fbt desc="Headline for update name section">Update Name</fbt>
|
|
82
|
-
</h3>
|
|
83
|
-
<label className="sr-only" htmlFor="header-username">
|
|
84
|
-
<fbt desc="Username label">Username</fbt>
|
|
85
|
-
</label>
|
|
86
|
-
<Input
|
|
87
|
-
aria-describedby={error ? 'header-username-error' : undefined}
|
|
88
|
-
aria-invalid={error ? 'true' : undefined}
|
|
89
|
-
className="w-48"
|
|
90
|
-
disabled={isPending}
|
|
91
|
-
id="header-username"
|
|
92
|
-
name="name"
|
|
93
|
-
onChange={handleChange}
|
|
94
|
-
placeholder={fbs('name', 'name input field placeholder')}
|
|
95
|
-
title={error ?? undefined}
|
|
96
|
-
value={name}
|
|
97
|
-
/>
|
|
98
|
-
<div>
|
|
99
|
-
<Button disabled={isSaveDisabled} size="sm" type="submit" variant="secondary">
|
|
100
|
-
<fbt desc="Button for saving">Save</fbt>
|
|
101
|
-
</Button>
|
|
102
|
-
</div>
|
|
103
|
-
</VStack>
|
|
104
|
-
{error ? <span id="header-username-error">{error}</span> : null}
|
|
105
|
-
</div>
|
|
106
|
-
);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export default function UserCard({ user: userRef }: { user: ViewRef<'User'> }) {
|
|
110
|
-
const user = useView(UserView, userRef);
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<Card>
|
|
114
|
-
<VStack between className="h-full" gap={16}>
|
|
115
|
-
<VStack gap={16}>
|
|
116
|
-
<H2>
|
|
117
|
-
<fbt desc="Your account headline">Your account</fbt>
|
|
118
|
-
</H2>
|
|
119
|
-
<Stack alignCenter between gap={16}>
|
|
120
|
-
<p className="text-sm text-muted-foreground">
|
|
121
|
-
<fbt desc="Signed in text">
|
|
122
|
-
Signed in as <fbt:param name="username">{user.name}</fbt:param>.
|
|
123
|
-
</fbt>
|
|
124
|
-
</p>
|
|
125
|
-
</Stack>
|
|
126
|
-
</VStack>
|
|
127
|
-
<UserNameForm user={user} />
|
|
128
|
-
</VStack>
|
|
129
|
-
</Card>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
import { fileURLToPath, URL } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const dist = new URL('./dist/', import.meta.url);
|
|
6
|
-
// Void's Node renderer reads this manifest to emit client scripts and styles.
|
|
7
|
-
globalThis.__VITE_MANIFEST__ = JSON.parse(
|
|
8
|
-
readFileSync(new URL('client/.vite/manifest.json', dist), 'utf8'),
|
|
9
|
-
);
|
|
10
|
-
process.chdir(fileURLToPath(dist));
|
|
11
|
-
await import(new URL('ssr/index.js', dist).href);
|