create-fate 1.0.4 → 1.3.1
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 +3 -0
- package/bin/create-fate.mjs +594 -9
- package/package.json +2 -2
- package/templates/fate/README.md +3 -0
- package/templates/fate/_shared/vue-app/package.json +54 -0
- package/templates/fate/_shared/vue-app/pages/index.vue +57 -0
- package/templates/fate/_shared/vue-app/pages/layout.vue +32 -0
- package/templates/fate/_shared/vue-app/pages/post/[id].vue +24 -0
- package/templates/fate/_shared/vue-app/src/App.css +63 -0
- package/templates/fate/_shared/vue-app/src/fateViews.ts +14 -0
- package/templates/fate/_shared/vue-app/src/lib/cx.ts +6 -0
- package/templates/fate/_shared/vue-app/src/lib/env.ts +3 -0
- package/templates/fate/_shared/vue-app/src/ui/Button.vue +67 -0
- package/templates/fate/_shared/vue-app/src/ui/Card.vue +16 -0
- package/templates/fate/_shared/vue-app/src/ui/Error.vue +9 -0
- package/templates/fate/_shared/vue-app/src/ui/ErrorBoundary.vue +22 -0
- package/templates/fate/_shared/vue-app/src/ui/Header.vue +35 -0
- package/templates/fate/_shared/vue-app/src/ui/Link.vue +15 -0
- package/templates/fate/_shared/vue-app/src/ui/PostCard.vue +29 -0
- package/templates/fate/_shared/vue-app/src/ui/Section.vue +19 -0
- package/templates/fate/_shared/vue-app/src/ui/Thinking.vue +11 -0
- package/templates/fate/_shared/vue-app/src/ui/UserCard.vue +23 -0
- package/templates/fate/_shared/vue-app/src/vue-shims.d.ts +6 -0
- package/templates/fate/_shared/vue-app/tsconfig.json +22 -0
- package/templates/fate/_shared/vue-app/vite.config.ts +58 -0
- package/templates/fate/_shared/vue-app/void.json +5 -0
- package/templates/fate/cloudflare/.github/workflows/push.yml +27 -0
- package/templates/fate/cloudflare/.vite-hooks/pre-commit +1 -0
- package/templates/fate/cloudflare/.vscode/extensions.json +12 -0
- package/templates/fate/cloudflare/.vscode/settings.json +25 -0
- package/templates/fate/cloudflare/AGENTS.md +55 -0
- package/templates/fate/cloudflare/README.md +126 -0
- package/templates/fate/cloudflare/_gitignore +20 -0
- package/templates/fate/cloudflare/client/package.json +56 -0
- package/templates/fate/cloudflare/client/pages/index.tsx +96 -0
- package/templates/fate/cloudflare/client/pages/layout.tsx +82 -0
- package/templates/fate/cloudflare/client/pages/login.tsx +107 -0
- package/templates/fate/cloudflare/client/pages/post/[id].tsx +22 -0
- package/templates/fate/cloudflare/client/pages/search.tsx +99 -0
- package/templates/fate/cloudflare/client/public/icon.svg +1 -0
- package/templates/fate/cloudflare/client/src/App.css +153 -0
- package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/cloudflare/client/src/lib/cx.tsx +6 -0
- package/templates/fate/cloudflare/client/src/lib/env.tsx +5 -0
- package/templates/fate/cloudflare/client/src/ui/Badge.tsx +31 -0
- package/templates/fate/cloudflare/client/src/ui/Button.tsx +108 -0
- package/templates/fate/cloudflare/client/src/ui/Card.tsx +32 -0
- package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +75 -0
- package/templates/fate/cloudflare/client/src/ui/Error.tsx +19 -0
- package/templates/fate/cloudflare/client/src/ui/H2.tsx +15 -0
- package/templates/fate/cloudflare/client/src/ui/H3.tsx +20 -0
- package/templates/fate/cloudflare/client/src/ui/Header.tsx +98 -0
- package/templates/fate/cloudflare/client/src/ui/Input.tsx +17 -0
- package/templates/fate/cloudflare/client/src/ui/Link.tsx +11 -0
- package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +52 -0
- package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +303 -0
- package/templates/fate/cloudflare/client/src/ui/Section.tsx +23 -0
- package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +131 -0
- package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +6 -0
- package/templates/fate/cloudflare/client/translations/de_DE.json +689 -0
- package/templates/fate/cloudflare/client/translations/ja_JP.json +689 -0
- package/templates/fate/cloudflare/client/vite.config.ts +45 -0
- package/templates/fate/cloudflare/client/void.json +29 -0
- package/templates/fate/cloudflare/package.json +46 -0
- package/templates/fate/cloudflare/pnpm-workspace.yaml +41 -0
- package/templates/fate/cloudflare/server/.env.example +1 -0
- package/templates/fate/cloudflare/server/db/db.ts +30 -0
- package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +133 -0
- package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +170 -0
- package/templates/fate/cloudflare/server/db/migrations/meta/20260508073357_snapshot.json +868 -0
- package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +20 -0
- package/templates/fate/cloudflare/server/db/queries.ts +211 -0
- package/templates/fate/cloudflare/server/db/schema.ts +332 -0
- package/templates/fate/cloudflare/server/package.json +34 -0
- package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +17 -0
- package/templates/fate/cloudflare/server/src/fate/context.ts +15 -0
- package/templates/fate/cloudflare/server/src/fate/live.ts +7 -0
- package/templates/fate/cloudflare/server/src/fate/server.ts +411 -0
- package/templates/fate/cloudflare/server/src/fate/views.ts +164 -0
- package/templates/fate/cloudflare/server/src/index.ts +80 -0
- package/templates/fate/cloudflare/server/src/lib/auth.ts +66 -0
- package/templates/fate/cloudflare/server/src/lib/origins.ts +17 -0
- package/templates/fate/cloudflare/server/src/router.ts +2 -0
- package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +17 -0
- package/templates/fate/cloudflare/server/tsconfig.json +10 -0
- package/templates/fate/cloudflare/server/wrangler.jsonc +29 -0
- package/templates/fate/cloudflare/tsconfig.json +33 -0
- package/templates/fate/cloudflare/vite.config.ts +72 -0
- package/templates/fate/drizzle/README.md +2 -8
- package/templates/fate/drizzle/client/pages/layout.tsx +2 -2
- package/templates/fate/drizzle/client/src/ui/PostCard.tsx +38 -15
- package/templates/fate/drizzle/pnpm-workspace.yaml +9 -3
- package/templates/fate/drizzle/server/vite.config.ts +1 -1
- package/templates/fate/graphql/.github/workflows/push.yml +27 -0
- package/templates/fate/graphql/.vite-hooks/pre-commit +1 -0
- package/templates/fate/graphql/.vscode/extensions.json +12 -0
- package/templates/fate/graphql/.vscode/settings.json +25 -0
- package/templates/fate/graphql/AGENTS.md +55 -0
- package/templates/fate/graphql/README.md +121 -0
- package/templates/fate/graphql/_gitignore +23 -0
- package/templates/fate/graphql/client/package.json +57 -0
- package/templates/fate/graphql/client/pages/index.tsx +96 -0
- package/templates/fate/graphql/client/pages/layout.tsx +83 -0
- package/templates/fate/graphql/client/pages/login.tsx +107 -0
- package/templates/fate/graphql/client/pages/post/[id].tsx +22 -0
- package/templates/fate/graphql/client/pages/search.tsx +99 -0
- package/templates/fate/graphql/client/public/icon.svg +1 -0
- package/templates/fate/graphql/client/src/App.css +153 -0
- package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/graphql/client/src/lib/cx.tsx +6 -0
- package/templates/fate/graphql/client/src/lib/env.tsx +5 -0
- package/templates/fate/graphql/client/src/ui/Badge.tsx +31 -0
- package/templates/fate/graphql/client/src/ui/Button.tsx +108 -0
- package/templates/fate/graphql/client/src/ui/Card.tsx +32 -0
- package/templates/fate/graphql/client/src/ui/CommentCard.tsx +75 -0
- package/templates/fate/graphql/client/src/ui/Error.tsx +19 -0
- package/templates/fate/graphql/client/src/ui/H2.tsx +15 -0
- package/templates/fate/graphql/client/src/ui/H3.tsx +20 -0
- package/templates/fate/graphql/client/src/ui/Header.tsx +98 -0
- package/templates/fate/graphql/client/src/ui/Input.tsx +17 -0
- package/templates/fate/graphql/client/src/ui/Link.tsx +11 -0
- package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +52 -0
- package/templates/fate/graphql/client/src/ui/PostCard.tsx +303 -0
- package/templates/fate/graphql/client/src/ui/Section.tsx +23 -0
- package/templates/fate/graphql/client/src/ui/UserCard.tsx +131 -0
- package/templates/fate/graphql/client/src/user/AuthClient.tsx +6 -0
- package/templates/fate/graphql/client/translations/de_DE.json +689 -0
- package/templates/fate/graphql/client/translations/ja_JP.json +689 -0
- package/templates/fate/graphql/client/vite.config.ts +40 -0
- package/templates/fate/graphql/client/void.json +29 -0
- package/templates/fate/graphql/docker-compose.yml +11 -0
- package/templates/fate/graphql/package.json +47 -0
- package/templates/fate/graphql/pnpm-workspace.yaml +45 -0
- package/templates/fate/graphql/server/.env.example +5 -0
- package/templates/fate/graphql/server/README.md +27 -0
- package/templates/fate/graphql/server/package.json +64 -0
- package/templates/fate/graphql/server/prisma.config.ts +17 -0
- package/templates/fate/graphql/server/scripts/generate-graphql.tsx +40 -0
- package/templates/fate/graphql/server/src/__tests__/index.test.tsx +5 -0
- package/templates/fate/graphql/server/src/app.tsx +47 -0
- package/templates/fate/graphql/server/src/graphql/builder.tsx +121 -0
- package/templates/fate/graphql/server/src/graphql/context.tsx +22 -0
- package/templates/fate/graphql/server/src/graphql/fate.ts +85 -0
- package/templates/fate/graphql/server/src/graphql/lib/authDirectives.tsx +93 -0
- package/templates/fate/graphql/server/src/graphql/lib/decodeGlobalID.tsx +12 -0
- package/templates/fate/graphql/server/src/graphql/lib/decodeIDOrThrow.tsx +10 -0
- package/templates/fate/graphql/server/src/graphql/lib/encodeGlobalID.tsx +5 -0
- package/templates/fate/graphql/server/src/graphql/live.tsx +241 -0
- package/templates/fate/graphql/server/src/graphql/mutations.tsx +265 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Category.tsx +35 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Comment.tsx +46 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Event.tsx +71 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Post.tsx +45 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Tag.tsx +11 -0
- package/templates/fate/graphql/server/src/graphql/nodes/User.tsx +28 -0
- package/templates/fate/graphql/server/src/graphql/schema.tsx +16 -0
- package/templates/fate/graphql/server/src/graphql/views.ts +193 -0
- package/templates/fate/graphql/server/src/index.tsx +45 -0
- package/templates/fate/graphql/server/src/lib/auth.tsx +31 -0
- package/templates/fate/graphql/server/src/lib/env.tsx +3 -0
- package/templates/fate/graphql/server/src/lib/origins.ts +18 -0
- package/templates/fate/graphql/server/src/prisma/prisma.tsx +9 -0
- package/templates/fate/graphql/server/src/prisma/schema.prisma +181 -0
- package/templates/fate/graphql/server/src/prisma/seed.tsx +161 -0
- package/templates/fate/graphql/server/src/user/SessionUser.tsx +17 -0
- package/templates/fate/graphql/server/src/user/isAdmin.tsx +3 -0
- package/templates/fate/graphql/server/tsconfig.json +33 -0
- package/templates/fate/graphql/server/vite.config.ts +55 -0
- package/templates/fate/graphql/tsconfig.json +33 -0
- package/templates/fate/graphql/vite.config.ts +80 -0
- package/templates/fate/graphql-client/.env.example +3 -0
- package/templates/fate/graphql-client/AGENTS.md +3 -0
- package/templates/fate/graphql-client/README.md +69 -0
- package/templates/fate/graphql-client/_gitignore +14 -0
- package/templates/fate/graphql-client/package.json +62 -0
- package/templates/fate/graphql-client/pages/index.tsx +69 -0
- package/templates/fate/graphql-client/pages/layout.tsx +41 -0
- package/templates/fate/graphql-client/pnpm-workspace.yaml +38 -0
- package/templates/fate/graphql-client/public/icon.svg +8 -0
- package/templates/fate/graphql-client/src/App.css +28 -0
- package/templates/fate/graphql-client/src/fate/graphql.ts +52 -0
- package/templates/fate/graphql-client/tsconfig.json +30 -0
- package/templates/fate/graphql-client/vite.config.ts +87 -0
- package/templates/fate/graphql-client/void.json +29 -0
- package/templates/fate/http/README.md +2 -8
- package/templates/fate/http/client/pages/layout.tsx +2 -2
- package/templates/fate/http/client/src/ui/PostCard.tsx +37 -15
- package/templates/fate/http/pnpm-workspace.yaml +9 -3
- package/templates/fate/http/server/vite.config.ts +1 -1
- package/templates/fate/prisma/README.md +2 -8
- package/templates/fate/prisma/client/pages/layout.tsx +2 -2
- package/templates/fate/prisma/client/src/ui/PostCard.tsx +37 -15
- package/templates/fate/prisma/pnpm-workspace.yaml +9 -3
- package/templates/fate/prisma/server/vite.config.ts +1 -1
- package/templates/fate/prisma/vite.config.ts +1 -1
- package/templates/fate/void/package.json +10 -2
- package/templates/fate/void/pnpm-workspace.yaml +12 -4
- package/templates/fate/void/src/ui/CategoryCard.tsx +3 -7
- package/templates/fate/void/src/ui/PostCard.tsx +38 -15
- package/templates/fate/void/vite.config.ts +5 -0
- package/templates/fate/drizzle/public/fate-logo-dark.svg +0 -4
- package/templates/fate/drizzle/public/fate-logo.svg +0 -4
- package/templates/fate/http/public/fate-logo-dark.svg +0 -4
- package/templates/fate/http/public/fate-logo.svg +0 -4
- package/templates/fate/prisma/public/fate-logo-dark.svg +0 -4
- package/templates/fate/prisma/public/fate-logo.svg +0 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vp staged
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"recommendations": [
|
|
3
|
+
"dbaeumer.vscode-eslint",
|
|
4
|
+
"oxc.oxc-vscode",
|
|
5
|
+
"styled-components.vscode-styled-components",
|
|
6
|
+
"sysoev.vscode-open-in-github",
|
|
7
|
+
"tailwindcss.intellisense",
|
|
8
|
+
"TypeScriptTeam.native-preview",
|
|
9
|
+
"usernamehw.errorlens",
|
|
10
|
+
"wix.vscode-import-cost"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"editor.formatOnSave": true,
|
|
3
|
+
"files.insertFinalNewline": true,
|
|
4
|
+
"files.trimFinalNewlines": true,
|
|
5
|
+
"editor.codeActionsOnSave": {
|
|
6
|
+
"source.fixAll.eslint": "explicit"
|
|
7
|
+
},
|
|
8
|
+
"[javascript]": {
|
|
9
|
+
"editor.defaultFormatter": "oxc.oxc-vscode"
|
|
10
|
+
},
|
|
11
|
+
"[typescriptreact]": {
|
|
12
|
+
"editor.defaultFormatter": "oxc.oxc-vscode"
|
|
13
|
+
},
|
|
14
|
+
"[typescript]": {
|
|
15
|
+
"editor.defaultFormatter": "oxc.oxc-vscode"
|
|
16
|
+
},
|
|
17
|
+
"[json]": {
|
|
18
|
+
"editor.defaultFormatter": "oxc.oxc-vscode"
|
|
19
|
+
},
|
|
20
|
+
"typescript.preferences.importModuleSpecifierEnding": "js",
|
|
21
|
+
"typescript.reportStyleChecksAsWarnings": false,
|
|
22
|
+
"typescript.updateImportsOnFileMove.enabled": "always",
|
|
23
|
+
"typescript.preferences.autoImportFileExcludePatterns": ["**/node_modules/vitest"],
|
|
24
|
+
"typescript.experimental.useTsgo": true
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Using _fate_ for React applications
|
|
2
|
+
|
|
3
|
+
This guidance is for agents working in projects bootstrapped from the fate template. It focuses on the client-side patterns for fate's view composition and action execution.
|
|
4
|
+
|
|
5
|
+
## Core Workflow
|
|
6
|
+
|
|
7
|
+
- **Define views with explicit selections:** Define reusable view objects with `view<T>()({...})`, compose them via spreads, and resolve them with `useView`.
|
|
8
|
+
- **Resolve data via references:** Avoid passing raw fetched objects; instead pass `ViewRef`s to downstream components to keep selections scoped and cache-aware. Then call `const result = useView(viewDefinition, ref)` to read only the selected fields while subscribing to cache updates. Name the prop after the result data, and rename it using destructuring in the function arguments: `const PostComponent = ({ post: postRef }: { post: ViewRef<'Post'>}) => { const post = useView(PostView, postRef); ... }`.
|
|
9
|
+
- **Request data at screen roots:** Use `useRequest` to compose all views required for a route or screen into a single typed request; wrap roots in `Suspense` and an error boundary since requests may suspend or throw.
|
|
10
|
+
- **Compose reusable views:** Extract nested selections into their own views (e.g., `UserView`) and compose them inside parent views (`author: UserView`) to avoid duplication and overfetching. This keeps selections DRY and ensures view updates propagate consistently when the cache changes.
|
|
11
|
+
|
|
12
|
+
## Actions & Mutations
|
|
13
|
+
|
|
14
|
+
- **Prefer `fate.actions` with `useActionState`:** Trigger mutations through React Actions to integrate with Suspense/`useTransition`, receiving `[result, action, isPending]`. Call actions via the relevant data inputs, for example `action({input: { id: post.id }})`.
|
|
15
|
+
- **Optimistic updates by default:** Pass `optimistic` payloads to immediately update the normalized cache; only views selecting the changed fields re-render and failures roll back automatically. Optimistic payloads should match the expected server data changes of the associated object.
|
|
16
|
+
- **Select extra fields when side effects matter:** When a mutation affects related entities (e.g., comment count on a post), pass a `view` to the action to fetch the needed fields in the same round-trip and keep the cache coherent for all dependent views.
|
|
17
|
+
- **Imperative escape hatch:** Use `fate.mutations` when you must fire mutations outside React components or without waiting on prior actions; handle loading/error states yourself.
|
|
18
|
+
- **Handle deletions and resets explicitly:** Use the `delete: true` flag to evict records from the cache, and send the `'reset'` token to clear stale action errors without re-running the mutation.
|
|
19
|
+
- **Error handling model:** Treat expected errors (e.g., 404) at the call site via the action result; unexpected errors bubble to the nearest React error boundary.
|
|
20
|
+
|
|
21
|
+
## Common Pitfalls
|
|
22
|
+
|
|
23
|
+
- **Use ViewRefs:** Pass references instead of raw data. Keep components focused on views and `ViewRef`s to let fate manage data masking and updates.
|
|
24
|
+
- **Co-locate data needs:** Keep view definitions near their components and compose them upward to the request root for predictable fetching.
|
|
25
|
+
- **Use Suspense:** Wrap pages in `Suspense`/`ErrorBoundary` instead of ad-hoc loading/error flags; fate aligns with Async React by design.
|
|
26
|
+
- **Do not duplicate types:** Keep generated fate types and views imported from the server package. Avoid redefining entity shapes on the client. This ensures view selections stay type-safe end-to-end.
|
|
27
|
+
- **`useRequest` only at the root:** When adding routes or layouts, create a dedicated `useRequest` call per screen root that pulls together all child views; do not scatter `useRequest` across leaf components unless it's necessary to issue requests due to user input.
|
|
28
|
+
- **React Actions:** Prefer React Action-friendly UI primitives (buttons/forms that accept an `action` prop). If unavailable, wrap action calls with `startTransition` in `onClick` handlers.
|
|
29
|
+
- **fate client support** fate's local client support files are maintained by the Vite/fate tooling. Do not manually edit `.fate` files; make the proper schema changes on the server and run `pnpm fate:generate` when working outside Vite dev.
|
|
30
|
+
- **Library Versions:** This repository uses the most recent releases of React, fate, Vite, and more. You might not know about the new releases yet, please don't get confused. The versions you see are real, and there are no feature or version mismatches. You can search the internet for more information about them.
|
|
31
|
+
|
|
32
|
+
Full documentation can be found on the filesystem at `./client/node_modules/react-fate/README.md` or online at [fate.technology](https://fate.technology/).
|
|
33
|
+
|
|
34
|
+
## Review Checklist for Agents
|
|
35
|
+
|
|
36
|
+
- [ ] Every component that reads server data does so through `useView` and receives a `ViewRef` prop.
|
|
37
|
+
- [ ] Root components gather data with a single `useRequest` call.
|
|
38
|
+
- [ ] Mutations are wired through `useActionState` with optimistic updates and necessary `view` selections; imperative `fate.mutations` are justified when used.
|
|
39
|
+
- [ ] Shared selections live in reusable views using regular JavaScript object spreads rather than duplicated field lists.
|
|
40
|
+
|
|
41
|
+
<!--VITE PLUS START-->
|
|
42
|
+
|
|
43
|
+
# Using Vite+, the Unified Toolchain for the Web
|
|
44
|
+
|
|
45
|
+
This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, and it invokes Vite through `vp dev` and `vp build`. Run `vp help` to print a list of commands and `vp <command> --help` for information about a specific command.
|
|
46
|
+
|
|
47
|
+
Docs are local at `node_modules/vite-plus/docs` or online at https://viteplus.dev/guide/.
|
|
48
|
+
|
|
49
|
+
## Review Checklist
|
|
50
|
+
|
|
51
|
+
- [ ] Run `vp install` after pulling remote changes and before getting started.
|
|
52
|
+
- [ ] Run `vp check` and `vp test` to format, lint, type check and test changes.
|
|
53
|
+
- [ ] Check if there are `vite.config.ts` tasks or `package.json` scripts necessary for validation, run via `vp run <script>`.
|
|
54
|
+
|
|
55
|
+
<!--VITE PLUS END-->
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# _fate_
|
|
2
|
+
|
|
3
|
+
**_fate_** is a modern data client for React inspired by [Relay](https://relay.dev/) and [GraphQL](https://graphql.org/). It combines view composition, normalized caching, data masking, Async React features, live updates, and type-safe data fetching.
|
|
4
|
+
|
|
5
|
+
Check out [fate.technology](https://fate.technology) for documentation, examples, and guides.
|
|
6
|
+
|
|
7
|
+
## Quick Start Template
|
|
8
|
+
|
|
9
|
+
Create a new fate app with Vite+:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vp create fate my-app --template cloudflare
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Technologies
|
|
16
|
+
|
|
17
|
+
This template is a client plus Cloudflare Worker server app. It uses the Cloudflare fate transport, D1 with Drizzle, Better Auth, and Cloudflare Durable Objects for SSE-backed live updates.
|
|
18
|
+
|
|
19
|
+
Next to [_fate_](https://fate.technology), it comes with the following technologies:
|
|
20
|
+
|
|
21
|
+
### Client Technologies
|
|
22
|
+
|
|
23
|
+
- [Vite 8](https://vitejs.dev/)
|
|
24
|
+
- [React](https://reactjs.org/) with [React Compiler](https://react.dev/learn/react-compiler) enabled
|
|
25
|
+
- [Tailwind](https://tailwindcss.com/)
|
|
26
|
+
- [fbtee](https://github.com/nkzw-tech/fbtee) for i18n
|
|
27
|
+
- [Better Auth](https://www.better-auth.com/) for authentication
|
|
28
|
+
- [Void Pages Router](https://void.cloud/docs)
|
|
29
|
+
- [TypeScript](https://www.typescriptlang.org)
|
|
30
|
+
- [pnpm](https://pnpm.io/)
|
|
31
|
+
|
|
32
|
+
### Server Technologies
|
|
33
|
+
|
|
34
|
+
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
|
35
|
+
- [Cloudflare D1](https://developers.cloudflare.com/d1/) with [Drizzle](https://orm.drizzle.team/)
|
|
36
|
+
- Cloudflare Durable Objects for fate live SSE fanout
|
|
37
|
+
- [Better Auth](https://better-auth.com/) for authentication
|
|
38
|
+
|
|
39
|
+
### Folder Structure
|
|
40
|
+
|
|
41
|
+
- `client/` - The React client application using _fate_.
|
|
42
|
+
- `server/` - The Cloudflare Worker fate server using D1 with Drizzle.
|
|
43
|
+
- `server/db/migrations/` - D1 migrations, including the seed data used by the demo UI.
|
|
44
|
+
|
|
45
|
+
## Initial Setup
|
|
46
|
+
|
|
47
|
+
You'll need Node.js 24+, [Vite+](https://viteplus.dev/guide/), and a Cloudflare account if you plan to deploy.
|
|
48
|
+
|
|
49
|
+
Install dependencies:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
vp install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Review `server/.env`, which is copied from `server/.env.example` when the app is created. The default local value points the client at Wrangler's local Worker:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
VITE_SERVER_URL=http://localhost:8787
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Set up the local D1 database, seed data, translations, and fate client support:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
vp run dev:setup
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Start the app:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
vp run dev
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The client runs at `http://localhost:6001` and the Worker runs at `http://localhost:8787`. Cloudflare fate requests go to `/fate`; live updates use the SSE endpoint at `/fate-live`.
|
|
74
|
+
|
|
75
|
+
The seeded demo login is:
|
|
76
|
+
|
|
77
|
+
- `alex@example.com`
|
|
78
|
+
- `password-alex`
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
Common commands from the project root:
|
|
83
|
+
|
|
84
|
+
- `vp run dev` starts the client and Worker together.
|
|
85
|
+
- `vp run dev:client` starts only the client.
|
|
86
|
+
- `vp run dev:server` starts only the Worker.
|
|
87
|
+
- `vp run dev:setup` applies local D1 migrations, runs fbtee setup, and prepares fate client support.
|
|
88
|
+
- `vp run fate:generate` refreshes fate client support after changing server views, roots, or mutations.
|
|
89
|
+
- `vp check --fix` formats, lints, and type-checks the workspace.
|
|
90
|
+
- `vp test` runs the test suite.
|
|
91
|
+
- `vp run build` type-checks the Worker and builds the client.
|
|
92
|
+
|
|
93
|
+
## Deploying to Cloudflare
|
|
94
|
+
|
|
95
|
+
Create a D1 database and copy the generated `database_id` into `server/wrangler.jsonc`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd server
|
|
99
|
+
pnpm exec wrangler d1 create fate-cloudflare
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Set a production Better Auth secret:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm exec wrangler secret put BETTER_AUTH_SECRET
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Apply the migrations to the remote D1 database:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm run db:migrate:remote
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Deploy the Worker:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm run deploy
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
To test the local client against the deployed Worker, run this from the project root:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
VITE_SERVER_URL="https://<your-worker>.<your-subdomain>.workers.dev" pnpm run dev:client
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If you deploy the client to a hosted origin, add that origin to `server/src/lib/origins.ts`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.fate/
|
|
2
|
+
**/.fate/
|
|
3
|
+
.void/
|
|
4
|
+
**/.void/
|
|
5
|
+
.eslintcache
|
|
6
|
+
.pnpm-debug.log
|
|
7
|
+
**/node_modules/
|
|
8
|
+
/node_modules/
|
|
9
|
+
client/.enum_manifest.json
|
|
10
|
+
client/dist/
|
|
11
|
+
client/source_strings.json
|
|
12
|
+
client/src/translations/
|
|
13
|
+
coverage/
|
|
14
|
+
dist/
|
|
15
|
+
server/.env
|
|
16
|
+
server/.prod.env
|
|
17
|
+
server/.wrangler/
|
|
18
|
+
server/dist/
|
|
19
|
+
tsconfig.tsbuildinfo
|
|
20
|
+
vite.config.ts.timestamp-*
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"author": "Christoph Nakazawa <christoph.pojer@gmail.com>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git://github.com/nkzw-tech/fate.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "vp build",
|
|
13
|
+
"dev": "vp dev",
|
|
14
|
+
"dev:setup": "vp run fbtee:all",
|
|
15
|
+
"fate:generate": "vp exec fate generate",
|
|
16
|
+
"fbtee:all": "vpx fbtee collect && vpx fbtee translate"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@app/server": "workspace:*",
|
|
20
|
+
"@nkzw/core": "^1.3.1",
|
|
21
|
+
"@nkzw/define-env": "^1.1.0",
|
|
22
|
+
"@nkzw/stack": "^2.3.2",
|
|
23
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
24
|
+
"@void/react": "^0.7.13",
|
|
25
|
+
"better-auth": "^1.6.9",
|
|
26
|
+
"cf-fate": "latest",
|
|
27
|
+
"class-variance-authority": "^0.7.1",
|
|
28
|
+
"clsx": "^2.1.1",
|
|
29
|
+
"fbtee": "^1.7.4",
|
|
30
|
+
"lucide-react": "^1.14.0",
|
|
31
|
+
"react": "^19.2.5",
|
|
32
|
+
"react-dom": "^19.2.5",
|
|
33
|
+
"react-error-boundary": "^6.1.1",
|
|
34
|
+
"react-fate": "latest",
|
|
35
|
+
"tailwind-merge": "^3.5.0",
|
|
36
|
+
"void": "^0.7.13"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@nkzw/babel-preset-fbtee": "^1.7.4",
|
|
40
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
41
|
+
"@tailwindcss/vite": "^4.2.4",
|
|
42
|
+
"@types/babel__core": "^7.20.5",
|
|
43
|
+
"@types/node": "^25.6.0",
|
|
44
|
+
"@types/react": "^19.2.14",
|
|
45
|
+
"@types/react-dom": "^19.2.3",
|
|
46
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
47
|
+
"tailwindcss": "^4.2.4",
|
|
48
|
+
"vite": "catalog:",
|
|
49
|
+
"vite-plus": "catalog:"
|
|
50
|
+
},
|
|
51
|
+
"browserslist": [
|
|
52
|
+
">0.2%",
|
|
53
|
+
"not dead",
|
|
54
|
+
"not op_mini all"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Stack, { VStack } from '@nkzw/stack';
|
|
2
|
+
import { ConnectionRef, useListView, useRequest } from 'react-fate';
|
|
3
|
+
import cx from '../src/lib/cx.tsx';
|
|
4
|
+
import { Button } from '../src/ui/Button.tsx';
|
|
5
|
+
import Card from '../src/ui/Card.tsx';
|
|
6
|
+
import H2 from '../src/ui/H2.tsx';
|
|
7
|
+
import Link from '../src/ui/Link.tsx';
|
|
8
|
+
import { PostCard, PostView } from '../src/ui/PostCard.tsx';
|
|
9
|
+
import Section from '../src/ui/Section.tsx';
|
|
10
|
+
import UserCard, { UserView } from '../src/ui/UserCard.tsx';
|
|
11
|
+
|
|
12
|
+
const PostConnectionView = {
|
|
13
|
+
args: { first: 3 },
|
|
14
|
+
items: {
|
|
15
|
+
node: PostView,
|
|
16
|
+
},
|
|
17
|
+
pagination: {
|
|
18
|
+
hasNext: true,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const PostFeed = ({ posts: postsRef }: { posts: ConnectionRef<'Post'> }) => {
|
|
23
|
+
const [posts, loadNext] = useListView(PostConnectionView, postsRef);
|
|
24
|
+
|
|
25
|
+
return posts.length ? (
|
|
26
|
+
<VStack gap={16}>
|
|
27
|
+
<H2 className="pl-5">
|
|
28
|
+
<fbt desc="Latest posts header">Latest posts</fbt>
|
|
29
|
+
</H2>
|
|
30
|
+
<VStack gap={32}>
|
|
31
|
+
{posts.map(({ node }) => (
|
|
32
|
+
<PostCard key={node.id} post={node} />
|
|
33
|
+
))}
|
|
34
|
+
{loadNext ? (
|
|
35
|
+
<Stack center>
|
|
36
|
+
<Button onClick={loadNext} variant="ghost">
|
|
37
|
+
<fbt desc="Load more posts button">Load more posts</fbt>
|
|
38
|
+
</Button>
|
|
39
|
+
</Stack>
|
|
40
|
+
) : null}
|
|
41
|
+
</VStack>
|
|
42
|
+
</VStack>
|
|
43
|
+
) : null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default function HomePage() {
|
|
47
|
+
const { posts, viewer } = useRequest({
|
|
48
|
+
posts: { list: PostConnectionView },
|
|
49
|
+
viewer: { view: UserView },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Section gap={32}>
|
|
54
|
+
<div
|
|
55
|
+
className={cx('grid gap-8 lg:items-stretch', viewer?.id ? 'lg:grid-cols-[1.6fr_1fr]' : '')}
|
|
56
|
+
>
|
|
57
|
+
<Card className="border border-white/20 bg-linear-to-r from-blue-500 to-sky-500 text-white dark:from-blue-600 dark:to-sky-600">
|
|
58
|
+
<Stack alignCenter gap={12} wrap>
|
|
59
|
+
<span className="squircle bg-white/20 px-2 py-1 text-xs font-semibold tracking-widest uppercase">
|
|
60
|
+
<span className="lowercase italic">fate</span> demo
|
|
61
|
+
</span>
|
|
62
|
+
</Stack>
|
|
63
|
+
<div className="space-y-3">
|
|
64
|
+
<h1 className="text-3xl leading-tight font-semibold text-balance lg:text-4xl">
|
|
65
|
+
<fbt desc="Description of fate">
|
|
66
|
+
fate is a modern data client for React inspired by Relay and GraphQL.
|
|
67
|
+
</fbt>
|
|
68
|
+
</h1>
|
|
69
|
+
<p className="text-sm text-white/80 lg:text-base">
|
|
70
|
+
<fbt desc="Tagline for fate">
|
|
71
|
+
fate combines view composition, normalized caching, data masking, Async React
|
|
72
|
+
features, and type-safe data fetching.
|
|
73
|
+
</fbt>
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
<Stack alignCenter gap={12} wrap>
|
|
77
|
+
{!viewer?.id && (
|
|
78
|
+
<>
|
|
79
|
+
<Button asChild size="sm" variant="secondary">
|
|
80
|
+
<Link className="squircle px-4 py-2 text-sm font-semibold" to="/login">
|
|
81
|
+
<fbt desc="Login button">Login</fbt>
|
|
82
|
+
</Link>
|
|
83
|
+
</Button>
|
|
84
|
+
<span className="text-sm text-white/80">
|
|
85
|
+
<fbt desc="Login button label">Sign in to post comments.</fbt>
|
|
86
|
+
</span>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
</Stack>
|
|
90
|
+
</Card>
|
|
91
|
+
{viewer?.id && <UserCard user={viewer} />}
|
|
92
|
+
</div>
|
|
93
|
+
<PostFeed posts={posts} />
|
|
94
|
+
</Section>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/// <reference types="fbtee/ReactTypes.d.ts" />
|
|
2
|
+
|
|
3
|
+
import '../src/App.css';
|
|
4
|
+
import Stack from '@nkzw/stack';
|
|
5
|
+
import { createLocaleContext } from 'fbtee';
|
|
6
|
+
import { ReactNode, Suspense, useMemo } from 'react';
|
|
7
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
8
|
+
import { FateClient } from 'react-fate';
|
|
9
|
+
import { createFateClient } from 'react-fate/client';
|
|
10
|
+
import AvailableLanguages from '../src/lib/AvailableLanguages.tsx';
|
|
11
|
+
import env from '../src/lib/env.tsx';
|
|
12
|
+
import Card from '../src/ui/Card.tsx';
|
|
13
|
+
import Error from '../src/ui/Error.tsx';
|
|
14
|
+
import Header from '../src/ui/Header.tsx';
|
|
15
|
+
import Section from '../src/ui/Section.tsx';
|
|
16
|
+
import AuthClient from '../src/user/AuthClient.tsx';
|
|
17
|
+
|
|
18
|
+
const Thinking = () => (
|
|
19
|
+
<Section>
|
|
20
|
+
<Stack center className="animate-pulse text-gray-500 italic" verticalPadding={48}>
|
|
21
|
+
<fbt desc="Text for thinking/loading screen">Thinking...</fbt>
|
|
22
|
+
</Stack>
|
|
23
|
+
</Section>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const LocaleContext = createLocaleContext({
|
|
27
|
+
availableLanguages: AvailableLanguages,
|
|
28
|
+
clientLocales: [navigator.language, ...navigator.languages],
|
|
29
|
+
loadLocale: async (locale: string) => {
|
|
30
|
+
if (locale !== 'en_US' && AvailableLanguages.has(locale)) {
|
|
31
|
+
return (await import(`../src/translations/${locale}.json`)).default[locale];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export default function Layout({ children }: { children: ReactNode }) {
|
|
39
|
+
const { data: session } = AuthClient.useSession();
|
|
40
|
+
const userId = session?.user.id;
|
|
41
|
+
|
|
42
|
+
const fate = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
createFateClient({
|
|
45
|
+
fetch: (input, init) =>
|
|
46
|
+
fetch(input, {
|
|
47
|
+
...init,
|
|
48
|
+
credentials: userId ? 'include' : undefined,
|
|
49
|
+
}),
|
|
50
|
+
liveUrl: `${env('SERVER_URL')}/fate-live`,
|
|
51
|
+
url: `${env('SERVER_URL')}/fate`,
|
|
52
|
+
}),
|
|
53
|
+
[userId],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<LocaleContext>
|
|
58
|
+
<div className="min-h-screen bg-background text-foreground">
|
|
59
|
+
<div className="min-h-screen bg-[radial-gradient(circle_at_20%_20%,rgba(59,130,246,0.08),transparent_35%),radial-gradient(circle_at_80%_0,rgba(99,102,241,0.08),transparent_28%)]">
|
|
60
|
+
<Header />
|
|
61
|
+
{session ? (
|
|
62
|
+
<Thinking />
|
|
63
|
+
) : (
|
|
64
|
+
<FateClient client={fate} key={userId}>
|
|
65
|
+
<ErrorBoundary
|
|
66
|
+
fallbackRender={({ error }) => (
|
|
67
|
+
<Section>
|
|
68
|
+
<Card>
|
|
69
|
+
<Error error={error} />
|
|
70
|
+
</Card>
|
|
71
|
+
</Section>
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<Suspense fallback={<Thinking />}>{children}</Suspense>
|
|
75
|
+
</ErrorBoundary>
|
|
76
|
+
</FateClient>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</LocaleContext>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -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/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql#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
|
+
}
|