create-fate 1.15.0 → 1.16.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 +2 -1
- package/bin/create-fate.mjs +64 -24
- package/package.json +1 -1
- package/templates/fate/_shared/client-only-vue/pages/about.vue +6 -0
- package/templates/fate/_shared/client-only-vue/pages/index.vue +72 -0
- package/templates/fate/_shared/client-only-vue/pages/layout.vue +30 -0
- package/templates/fate/_shared/client-only-vue/vite.config.ts +12 -0
- package/templates/fate/_shared/vue-app/package.json +2 -2
- package/templates/fate/_shared/vue-app/pages/post/[id].vue +1 -1
- package/templates/fate/_shared/vue-app/void.json +3 -0
- package/templates/fate/client-only/AGENTS.md +5 -0
- package/templates/fate/client-only/README.md +25 -0
- package/templates/fate/client-only/_gitignore +6 -0
- package/templates/fate/client-only/package.json +47 -0
- package/templates/fate/client-only/pages/about.server.ts +1 -0
- package/templates/fate/client-only/pages/about.tsx +11 -0
- package/templates/fate/client-only/pages/index.server.ts +1 -0
- package/templates/fate/client-only/pages/index.tsx +82 -0
- package/templates/fate/client-only/pages/layout.tsx +24 -0
- package/templates/fate/client-only/pnpm-workspace.yaml +24 -0
- package/templates/fate/client-only/src/state.ts +79 -0
- package/templates/fate/client-only/src/style.css +42 -0
- package/templates/fate/client-only/tsconfig.json +33 -0
- package/templates/fate/client-only/vite.config.ts +12 -0
- package/templates/fate/client-only/void.config.ts +11 -0
- package/templates/fate/drizzle/client/package.json +4 -3
- package/templates/fate/drizzle/client/pages/post/[id].tsx +1 -1
- package/templates/fate/drizzle/client/src/ui/Link.tsx +5 -3
- package/templates/fate/drizzle/client/tsconfig.json +16 -0
- package/templates/fate/drizzle/client/void.json +3 -0
- package/templates/fate/graphql/client/package.json +4 -3
- package/templates/fate/graphql/client/pages/post/[id].tsx +1 -1
- package/templates/fate/graphql/client/src/ui/Link.tsx +5 -3
- package/templates/fate/graphql/client/tsconfig.json +16 -0
- package/templates/fate/graphql/client/void.json +3 -0
- package/templates/fate/graphql/server/vite.config.ts +0 -13
- package/templates/fate/graphql-client/package.json +2 -2
- package/templates/fate/graphql-client/void.json +3 -0
- package/templates/fate/http/client/package.json +4 -3
- package/templates/fate/http/client/pages/post/[id].tsx +1 -1
- package/templates/fate/http/client/src/ui/Link.tsx +5 -3
- package/templates/fate/http/client/tsconfig.json +16 -0
- package/templates/fate/http/client/void.json +3 -0
- package/templates/fate/prisma/client/package.json +4 -3
- package/templates/fate/prisma/client/pages/post/[id].tsx +1 -1
- package/templates/fate/prisma/client/src/ui/Link.tsx +5 -3
- package/templates/fate/prisma/client/tsconfig.json +16 -0
- package/templates/fate/prisma/client/void.json +3 -0
- package/templates/fate/void/package.json +2 -2
- package/templates/fate/void/pages/category/[id].tsx +1 -1
- package/templates/fate/void/pages/post/[id].tsx +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ Create a new fate app:
|
|
|
6
6
|
vp create fate
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Dependencies are installed during creation. Templates that use a server also generate the fate client.
|
|
10
10
|
|
|
11
11
|
Choose between these templates:
|
|
12
12
|
|
|
13
|
+
- `client-only` uses Void Pages Routing with local state persisted in IndexedDB.
|
|
13
14
|
- `void`: Void pages router with Drizzle, live updates, and native Cloudflare deployment.
|
|
14
15
|
- `drizzle`: tRPC with Drizzle.
|
|
15
16
|
- `graphql`: GraphQL with Prisma.
|
package/bin/create-fate.mjs
CHANGED
|
@@ -7,6 +7,11 @@ import { URL } from 'node:url';
|
|
|
7
7
|
import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts';
|
|
8
8
|
|
|
9
9
|
const variants = {
|
|
10
|
+
'client-only': {
|
|
11
|
+
description: 'Void pages with local state saved in IndexedDB',
|
|
12
|
+
label: 'Client Only',
|
|
13
|
+
template: 'client-only',
|
|
14
|
+
},
|
|
10
15
|
drizzle: {
|
|
11
16
|
description: 'tRPC with Drizzle',
|
|
12
17
|
label: 'Drizzle',
|
|
@@ -50,11 +55,17 @@ const frontendFrameworks = {
|
|
|
50
55
|
},
|
|
51
56
|
};
|
|
52
57
|
|
|
53
|
-
const fateDependencyNames = [
|
|
58
|
+
const fateDependencyNames = [
|
|
59
|
+
'@nkzw/fate',
|
|
60
|
+
'@nkzw/fate-indexeddb',
|
|
61
|
+
'react-fate',
|
|
62
|
+
'void-fate',
|
|
63
|
+
'vue-fate',
|
|
64
|
+
];
|
|
54
65
|
|
|
55
66
|
const usage = () => {
|
|
56
67
|
process.stdout
|
|
57
|
-
.write(`Usage: create-fate [directory] [--template
|
|
68
|
+
.write(`Usage: create-fate [directory] [--template ${Object.keys(variants).join('|')}] [--framework react|vue]
|
|
58
69
|
|
|
59
70
|
Create a new fate app.
|
|
60
71
|
|
|
@@ -329,6 +340,13 @@ const updatePackageJsonFiles = (rootDir, dir, packageName, fateDependencyVersion
|
|
|
329
340
|
const readPackageJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
330
341
|
|
|
331
342
|
const writePackageJson = (filePath, json) => {
|
|
343
|
+
for (const section of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
344
|
+
if (json[section]) {
|
|
345
|
+
json[section] = Object.fromEntries(
|
|
346
|
+
Object.entries(json[section]).sort(([left], [right]) => left.localeCompare(right)),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
332
350
|
fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
|
|
333
351
|
};
|
|
334
352
|
|
|
@@ -548,7 +566,7 @@ const vueTemplatePackage = () =>
|
|
|
548
566
|
path.resolve(packageRoot, 'templates', 'fate', '_shared', 'vue-app');
|
|
549
567
|
|
|
550
568
|
const frontendRootForVariant = (targetPath, selectedVariant) =>
|
|
551
|
-
|
|
569
|
+
['client-only', 'graphql-client', 'void'].includes(selectedVariant)
|
|
552
570
|
? targetPath
|
|
553
571
|
: path.join(targetPath, 'client');
|
|
554
572
|
|
|
@@ -558,6 +576,35 @@ const removeIfExists = (entryPath) => {
|
|
|
558
576
|
}
|
|
559
577
|
};
|
|
560
578
|
|
|
579
|
+
const configureClientOnlyVue = (targetPath) => {
|
|
580
|
+
const pages = path.join(targetPath, 'pages');
|
|
581
|
+
for (const file of fs.readdirSync(pages)) {
|
|
582
|
+
if (file.endsWith('.tsx')) {
|
|
583
|
+
removeIfExists(path.join(pages, file));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
fs.cpSync(path.resolve(packageRoot, 'templates/fate/_shared/client-only-vue'), targetPath, {
|
|
587
|
+
recursive: true,
|
|
588
|
+
});
|
|
589
|
+
const packagePath = path.join(targetPath, 'package.json');
|
|
590
|
+
const packageJson = readPackageJson(packagePath);
|
|
591
|
+
const vuePackageJson = readPackageJson(path.join(vueTemplatePackage(), 'package.json'));
|
|
592
|
+
removePackageEntries(packageJson.dependencies, reactPackageEntries);
|
|
593
|
+
removePackageEntries(packageJson.devDependencies, reactPackageEntries);
|
|
594
|
+
for (const name of ['@void/vue', 'vue', 'vue-fate']) {
|
|
595
|
+
packageJson.dependencies[name] = vuePackageJson.dependencies[name];
|
|
596
|
+
}
|
|
597
|
+
writePackageJson(packagePath, packageJson);
|
|
598
|
+
const agentsPath = path.join(targetPath, 'AGENTS.md');
|
|
599
|
+
fs.writeFileSync(
|
|
600
|
+
agentsPath,
|
|
601
|
+
fs
|
|
602
|
+
.readFileSync(agentsPath, 'utf8')
|
|
603
|
+
.replaceAll('react-fate', 'vue-fate')
|
|
604
|
+
.replaceAll('React', 'Vue'),
|
|
605
|
+
);
|
|
606
|
+
};
|
|
607
|
+
|
|
561
608
|
const removeReactFrontend = (frontendRoot, selectedVariant) => {
|
|
562
609
|
if (selectedVariant === 'void' || selectedVariant === 'graphql-client') {
|
|
563
610
|
for (const entry of [
|
|
@@ -834,10 +881,10 @@ const configureVueTemplate = (targetPath, selectedVariant) => {
|
|
|
834
881
|
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
835
882
|
? "['**/.cloudflare/**', '.fate', '.void', '.void-wrangler.jsonc', 'dist', 'node_modules', 'vite.config.ts.timestamp-*']"
|
|
836
883
|
: "['**/.cloudflare/**', '.fate', '.void', 'dist', 'node_modules', '../dist', 'vite.config.ts.timestamp-*']",
|
|
837
|
-
__TSCONFIG_EXTENDS__:
|
|
884
|
+
'"__TSCONFIG_EXTENDS__"':
|
|
838
885
|
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
839
|
-
? './.void/tsconfig.json'
|
|
840
|
-
: '../tsconfig.json',
|
|
886
|
+
? '"./.void/tsconfig.json"'
|
|
887
|
+
: '["../tsconfig.json", "./.void/tsconfig.json"]',
|
|
841
888
|
'"__TSCONFIG_INCLUDE__"':
|
|
842
889
|
selectedVariant === 'void'
|
|
843
890
|
? `[
|
|
@@ -918,23 +965,12 @@ const setupProject = (targetPath, selectedVariant) => {
|
|
|
918
965
|
runCommand('vp', ['run', 'fbtee:all'], frontendRoot);
|
|
919
966
|
}
|
|
920
967
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
case 'graphql-client':
|
|
928
|
-
runCommand('vp', ['run', 'prepare:void'], targetPath);
|
|
929
|
-
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
930
|
-
break;
|
|
931
|
-
case 'void':
|
|
932
|
-
runCommand('vp', ['run', 'prepare:void'], targetPath);
|
|
933
|
-
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
934
|
-
break;
|
|
935
|
-
default:
|
|
936
|
-
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
937
|
-
break;
|
|
968
|
+
if (selectedVariant === 'graphql' || selectedVariant === 'prisma') {
|
|
969
|
+
runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
|
|
970
|
+
}
|
|
971
|
+
runCommand('vp', ['run', 'prepare:void'], frontendRoot);
|
|
972
|
+
if (selectedVariant !== 'client-only') {
|
|
973
|
+
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
938
974
|
}
|
|
939
975
|
};
|
|
940
976
|
|
|
@@ -1004,7 +1040,11 @@ const main = async () => {
|
|
|
1004
1040
|
);
|
|
1005
1041
|
}
|
|
1006
1042
|
if (selectedFramework === 'vue') {
|
|
1007
|
-
|
|
1043
|
+
if (selectedVariant === 'client-only') {
|
|
1044
|
+
configureClientOnlyVue(targetPath);
|
|
1045
|
+
} else {
|
|
1046
|
+
configureVueTemplate(targetPath, selectedVariant);
|
|
1047
|
+
}
|
|
1008
1048
|
}
|
|
1009
1049
|
restoreTemplateFileNames(targetPath);
|
|
1010
1050
|
if (selectedFramework === 'vue') {
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import { useFateClient, useRequest, useView } from 'vue-fate';
|
|
4
|
+
import { WorkspaceView } from '../src/state.ts';
|
|
5
|
+
|
|
6
|
+
const fate = useFateClient();
|
|
7
|
+
const request = useRequest({ workspace: { view: WorkspaceView } });
|
|
8
|
+
const state = useView(WorkspaceView, () => request.data.value?.workspace);
|
|
9
|
+
const title = ref('');
|
|
10
|
+
const pending = ref(false);
|
|
11
|
+
const error = ref('');
|
|
12
|
+
await request.ready();
|
|
13
|
+
await state.ready();
|
|
14
|
+
|
|
15
|
+
const run = async (mutate: () => Promise<{ error?: Error }>) => {
|
|
16
|
+
pending.value = true;
|
|
17
|
+
error.value = '';
|
|
18
|
+
try {
|
|
19
|
+
const result = await mutate();
|
|
20
|
+
if (result.error) {
|
|
21
|
+
error.value = result.error.message;
|
|
22
|
+
}
|
|
23
|
+
} catch (cause) {
|
|
24
|
+
error.value = cause instanceof Error ? cause.message : String(cause);
|
|
25
|
+
} finally {
|
|
26
|
+
pending.value = false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const add = () => {
|
|
31
|
+
if (title.value.trim()) {
|
|
32
|
+
void run(async () => {
|
|
33
|
+
const result = await fate.mutations.task.add({ input: { title: title.value } });
|
|
34
|
+
if (!result.error) {
|
|
35
|
+
title.value = '';
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<h1>Tasks</h1>
|
|
45
|
+
<p>{{ state?.progress }}</p>
|
|
46
|
+
<form @submit.prevent="add">
|
|
47
|
+
<input v-model="title" aria-label="Task title" required />
|
|
48
|
+
<button :disabled="pending" type="submit">Add task</button>
|
|
49
|
+
</form>
|
|
50
|
+
<p v-if="error" role="alert">{{ error }}</p>
|
|
51
|
+
<ul>
|
|
52
|
+
<li v-for="task in state?.tasks" :key="task.id">
|
|
53
|
+
<label>
|
|
54
|
+
<input
|
|
55
|
+
:checked="task.done"
|
|
56
|
+
:disabled="pending"
|
|
57
|
+
type="checkbox"
|
|
58
|
+
@change="run(() => fate.mutations.task.toggle({ input: { id: task.id } }))"
|
|
59
|
+
/>
|
|
60
|
+
{{ task.title }}
|
|
61
|
+
</label>
|
|
62
|
+
<button
|
|
63
|
+
:aria-label="`Remove ${task.title}`"
|
|
64
|
+
:disabled="pending"
|
|
65
|
+
type="button"
|
|
66
|
+
@click="run(() => fate.mutations.task.remove({ input: { id: task.id } }))"
|
|
67
|
+
>
|
|
68
|
+
Remove
|
|
69
|
+
</button>
|
|
70
|
+
</li>
|
|
71
|
+
</ul>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import '../src/style.css';
|
|
3
|
+
import { Link } from '@void/vue';
|
|
4
|
+
import { onErrorCaptured, ref } from 'vue';
|
|
5
|
+
import { FateClient } from 'vue-fate';
|
|
6
|
+
import { getTaskClient } from '../src/state.ts';
|
|
7
|
+
|
|
8
|
+
const client = getTaskClient();
|
|
9
|
+
const error = ref('');
|
|
10
|
+
onErrorCaptured((cause) => {
|
|
11
|
+
error.value = String(cause);
|
|
12
|
+
return false;
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<nav aria-label="Main navigation">
|
|
18
|
+
<Link href="/">Tasks</Link>
|
|
19
|
+
<Link href="/about">About</Link>
|
|
20
|
+
</nav>
|
|
21
|
+
<main>
|
|
22
|
+
<p v-if="error" role="alert">{{ error }}</p>
|
|
23
|
+
<FateClient v-else :client="client">
|
|
24
|
+
<Suspense>
|
|
25
|
+
<slot />
|
|
26
|
+
<template #fallback><p>Restoring tasks…</p></template>
|
|
27
|
+
</Suspense>
|
|
28
|
+
</FateClient>
|
|
29
|
+
</main>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { voidVue } from '@void/vue/plugin';
|
|
2
|
+
import { defineConfig } from 'vite-plus';
|
|
3
|
+
import { voidPlugin } from 'void';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
fmt: { ignorePatterns: ['.void/', 'dist/', 'pnpm-lock.yaml'], singleQuote: true },
|
|
7
|
+
lint: {
|
|
8
|
+
ignorePatterns: ['.void/', 'dist/'],
|
|
9
|
+
options: { typeAware: true, typeCheck: true },
|
|
10
|
+
},
|
|
11
|
+
plugins: [voidPlugin(), voidVue()],
|
|
12
|
+
});
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"@nkzw/core": "^1.3.1",
|
|
23
23
|
"@nkzw/define-env": "^1.1.0",
|
|
24
24
|
"@trpc/client": "^11.19.0",
|
|
25
|
-
"@void/vue": "^1.
|
|
25
|
+
"@void/vue": "^1.7.0",
|
|
26
26
|
"class-variance-authority": "^0.7.1",
|
|
27
27
|
"clsx": "^2.1.1",
|
|
28
28
|
"dotenv": "^18.0.9",
|
|
29
29
|
"graphql-sse": "^2.6.3",
|
|
30
30
|
"tailwind-merge": "^3.7.0",
|
|
31
|
-
"void": "^1.
|
|
31
|
+
"void": "^1.7.0",
|
|
32
32
|
"vue": "^3.5.44",
|
|
33
33
|
"vue-fate": "latest"
|
|
34
34
|
},
|
|
@@ -5,7 +5,7 @@ import { PostView } from '../../src/fateViews.ts';
|
|
|
5
5
|
import PostCard from '../../src/ui/PostCard.vue';
|
|
6
6
|
import Section from '../../src/ui/Section.vue';
|
|
7
7
|
|
|
8
|
-
const params = useParams<
|
|
8
|
+
const params = useParams<'/post/[id]'>();
|
|
9
9
|
const id = params.id;
|
|
10
10
|
|
|
11
11
|
if (!id) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Client-only fate
|
|
2
|
+
|
|
3
|
+
Use `node_modules/react-fate/docs/` for bundled guidance on building React applications with fate. Start with `index.md`.
|
|
4
|
+
|
|
5
|
+
Client definitions and mutations live in `src/state.ts`. Run `vp run dev:setup` after changing Void routes. Run `vp run test:all` and `vp run build` to verify changes.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Client-only fate
|
|
2
|
+
|
|
3
|
+
A task list with Void Pages Routing, `react-fate`, and IndexedDB. Try adding, completing, or removing a task, then reload to see the saved state. The About page demonstrates navigation between static routes.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
vp install
|
|
9
|
+
vp run dev:setup
|
|
10
|
+
vp run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The app's schema, views, and mutations live in `src/state.ts`. The project uses Void route types from `void prepare`. fate registers the client schema at runtime.
|
|
14
|
+
|
|
15
|
+
## Deployment
|
|
16
|
+
|
|
17
|
+
Build static files into `dist/client/` with `vp run build`. Serve that directory with a static host that resolves clean URLs to the corresponding HTML files.
|
|
18
|
+
|
|
19
|
+
To deploy directly to Cloudflare, run:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
vp exec void deploy --platform cloudflare
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Void deploys both pages as static assets. IndexedDB holds each browser's task data.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fate-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "fate is a modern data client for the web.",
|
|
6
|
+
"author": "Christoph Nakazawa <christoph.pojer@gmail.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git://github.com/nkzw-tech/fate.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "vp build",
|
|
14
|
+
"dev": "vp dev",
|
|
15
|
+
"dev:setup": "vp run prepare:void",
|
|
16
|
+
"prepare:void": "void prepare",
|
|
17
|
+
"test:all": "vp check"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@nkzw/fate": "latest",
|
|
21
|
+
"@nkzw/fate-indexeddb": "latest",
|
|
22
|
+
"@void/react": "^1.7.0",
|
|
23
|
+
"react": "^19.3.1",
|
|
24
|
+
"react-dom": "^19.3.1",
|
|
25
|
+
"react-error-boundary": "^6.1.6",
|
|
26
|
+
"react-fate": "latest",
|
|
27
|
+
"void": "^1.7.0",
|
|
28
|
+
"zod": "^4.6.7"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^19.3.0",
|
|
32
|
+
"@types/react-dom": "^19.3.0",
|
|
33
|
+
"typescript": "^7.0.2",
|
|
34
|
+
"vite": "catalog:",
|
|
35
|
+
"vite-plus": "catalog:"
|
|
36
|
+
},
|
|
37
|
+
"browserslist": [
|
|
38
|
+
">0.2%",
|
|
39
|
+
"not dead",
|
|
40
|
+
"not op_mini all"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": "^24.22.0 || >=26.12.0",
|
|
44
|
+
"pnpm": ">=11.0.0"
|
|
45
|
+
},
|
|
46
|
+
"packageManager": "pnpm@12.6.0"
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ssr = false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ssr = false;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { useFateClient, useRequest, useView } from 'react-fate';
|
|
3
|
+
import { WorkspaceView } from '../src/state.ts';
|
|
4
|
+
|
|
5
|
+
export default function Tasks() {
|
|
6
|
+
const fate = useFateClient();
|
|
7
|
+
const { workspace } = useRequest({ workspace: { view: WorkspaceView } });
|
|
8
|
+
const { progress, tasks } = useView(WorkspaceView, workspace);
|
|
9
|
+
const [title, setTitle] = useState('');
|
|
10
|
+
const [pending, setPending] = useState(false);
|
|
11
|
+
const [error, setError] = useState('');
|
|
12
|
+
|
|
13
|
+
const run = async (mutate: () => Promise<{ error?: Error }>) => {
|
|
14
|
+
setPending(true);
|
|
15
|
+
setError('');
|
|
16
|
+
try {
|
|
17
|
+
const result = await mutate();
|
|
18
|
+
if (result.error) {
|
|
19
|
+
setError(result.error.message);
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
23
|
+
} finally {
|
|
24
|
+
setPending(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const add = (event: FormEvent<HTMLFormElement>) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
if (title.trim()) {
|
|
31
|
+
void run(async () => {
|
|
32
|
+
const result = await fate.mutations.task.add({ input: { title } });
|
|
33
|
+
if (!result.error) {
|
|
34
|
+
setTitle('');
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<h1>Tasks</h1>
|
|
44
|
+
<p>{progress}</p>
|
|
45
|
+
<form onSubmit={add}>
|
|
46
|
+
<input
|
|
47
|
+
aria-label="Task title"
|
|
48
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
49
|
+
required
|
|
50
|
+
value={title}
|
|
51
|
+
/>
|
|
52
|
+
<button disabled={pending} type="submit">
|
|
53
|
+
Add task
|
|
54
|
+
</button>
|
|
55
|
+
</form>
|
|
56
|
+
{error && <p role="alert">{error}</p>}
|
|
57
|
+
<ul>
|
|
58
|
+
{tasks.map(({ done, id, title }) => (
|
|
59
|
+
<li key={id}>
|
|
60
|
+
<label>
|
|
61
|
+
<input
|
|
62
|
+
checked={done}
|
|
63
|
+
disabled={pending}
|
|
64
|
+
onChange={() => void run(() => fate.mutations.task.toggle({ input: { id } }))}
|
|
65
|
+
type="checkbox"
|
|
66
|
+
/>
|
|
67
|
+
{title}
|
|
68
|
+
</label>
|
|
69
|
+
<button
|
|
70
|
+
aria-label={`Remove ${title}`}
|
|
71
|
+
disabled={pending}
|
|
72
|
+
onClick={() => void run(() => fate.mutations.task.remove({ input: { id } }))}
|
|
73
|
+
type="button"
|
|
74
|
+
>
|
|
75
|
+
Remove
|
|
76
|
+
</button>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import '../src/style.css';
|
|
2
|
+
import { Link } from '@void/react';
|
|
3
|
+
import { Suspense, type PropsWithChildren } from 'react';
|
|
4
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
5
|
+
import { FateClient } from 'react-fate';
|
|
6
|
+
import { getTaskClient } from '../src/state.ts';
|
|
7
|
+
|
|
8
|
+
export default function Layout({ children }: PropsWithChildren) {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<nav aria-label="Main navigation">
|
|
12
|
+
<Link href="/">Tasks</Link>
|
|
13
|
+
<Link href="/about">About</Link>
|
|
14
|
+
</nav>
|
|
15
|
+
<main>
|
|
16
|
+
<ErrorBoundary fallbackRender={({ error }) => <p role="alert">{String(error)}</p>}>
|
|
17
|
+
<FateClient client={getTaskClient()}>
|
|
18
|
+
<Suspense fallback={<p>Restoring tasks…</p>}>{children}</Suspense>
|
|
19
|
+
</FateClient>
|
|
20
|
+
</ErrorBoundary>
|
|
21
|
+
</main>
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
packages:
|
|
2
|
+
- .
|
|
3
|
+
|
|
4
|
+
allowBuilds:
|
|
5
|
+
better-sqlite3: false
|
|
6
|
+
esbuild: true
|
|
7
|
+
workerd: true
|
|
8
|
+
|
|
9
|
+
catalog:
|
|
10
|
+
vite: npm:@voidzero-dev/vite-plus-core@1.4.0
|
|
11
|
+
vite-plus: 1.4.0
|
|
12
|
+
|
|
13
|
+
minimumReleaseAgeExclude:
|
|
14
|
+
- '@voidzero-dev/vite-plus-core'
|
|
15
|
+
- vite-plus
|
|
16
|
+
|
|
17
|
+
overrides:
|
|
18
|
+
vite: 'catalog:'
|
|
19
|
+
|
|
20
|
+
peerDependencyRules:
|
|
21
|
+
allowAny:
|
|
22
|
+
- vite
|
|
23
|
+
allowedVersions:
|
|
24
|
+
vite: '*'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clientSchema,
|
|
3
|
+
computed,
|
|
4
|
+
createClient,
|
|
5
|
+
dataView,
|
|
6
|
+
field,
|
|
7
|
+
list,
|
|
8
|
+
view,
|
|
9
|
+
type EntityOf,
|
|
10
|
+
} from '@nkzw/fate';
|
|
11
|
+
import { createIndexedDBStorage } from '@nkzw/fate-indexeddb';
|
|
12
|
+
import { createPersistence } from '@nkzw/fate/persistence';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
type TaskData = { done: boolean; id: string; title: string };
|
|
16
|
+
type WorkspaceData = { id: string; tasks: Array<TaskData> };
|
|
17
|
+
const task = dataView<TaskData>('Task')({ done: true, id: true, title: true });
|
|
18
|
+
const workspace = dataView<WorkspaceData>('Workspace')({
|
|
19
|
+
id: true,
|
|
20
|
+
progress: computed({
|
|
21
|
+
resolve: (_item: WorkspaceData, { done = [] }) =>
|
|
22
|
+
`${done.filter(Boolean).length} of ${done.length} complete`,
|
|
23
|
+
select: { done: field('tasks.done') },
|
|
24
|
+
}),
|
|
25
|
+
tasks: list(task),
|
|
26
|
+
});
|
|
27
|
+
const schema = clientSchema({
|
|
28
|
+
roots: { workspace: { id: 'main', type: 'Workspace' } },
|
|
29
|
+
types: {
|
|
30
|
+
Task: { defaults: (id) => ({ done: false, id, title: '' }), view: task },
|
|
31
|
+
Workspace: {
|
|
32
|
+
defaults: (id) => ({
|
|
33
|
+
id,
|
|
34
|
+
tasks: [{ done: false, id: 'welcome', title: 'Add your first task' }],
|
|
35
|
+
}),
|
|
36
|
+
view: workspace,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const createTaskClient = () =>
|
|
42
|
+
createClient({
|
|
43
|
+
persistence: createPersistence({ key: 'tasks', storage: createIndexedDBStorage() }),
|
|
44
|
+
}).registerClientSchema(schema, (builder) => ({
|
|
45
|
+
'task.add': builder.mutation('Task', {
|
|
46
|
+
input: z.object({ title: z.string().trim().min(1) }),
|
|
47
|
+
resolve: ({ input, store }) => {
|
|
48
|
+
const task = store.create('Task', { id: crypto.randomUUID(), title: input.title });
|
|
49
|
+
store.update('Workspace', 'main', {
|
|
50
|
+
tasks: [...(store.get('Workspace', 'main')?.tasks ?? []), task],
|
|
51
|
+
});
|
|
52
|
+
return task;
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
'task.remove': builder.mutation('Task', {
|
|
56
|
+
input: z.object({ id: z.string() }),
|
|
57
|
+
resolve: ({ input, store }) => store.delete('Task', input.id),
|
|
58
|
+
}),
|
|
59
|
+
'task.toggle': builder.mutation('Task', {
|
|
60
|
+
input: z.object({ id: z.string() }),
|
|
61
|
+
resolve: ({ input, store }) =>
|
|
62
|
+
store.update('Task', input.id, { done: !store.get('Task', input.id)?.done }),
|
|
63
|
+
}),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
type TaskClient = ReturnType<typeof createTaskClient>;
|
|
67
|
+
let client: TaskClient | undefined;
|
|
68
|
+
export const getTaskClient = () => (client ??= createTaskClient());
|
|
69
|
+
|
|
70
|
+
export const WorkspaceView = view<EntityOf<TaskClient, 'Workspace'>>()({
|
|
71
|
+
progress: true,
|
|
72
|
+
tasks: { done: true, id: true, title: true },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
declare module '@nkzw/fate' {
|
|
76
|
+
interface ClientRegistration {
|
|
77
|
+
client: TaskClient;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
font-family: system-ui, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0 auto;
|
|
8
|
+
max-width: 38rem;
|
|
9
|
+
padding: 2rem 1rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
nav,
|
|
13
|
+
form,
|
|
14
|
+
li {
|
|
15
|
+
align-items: center;
|
|
16
|
+
display: flex;
|
|
17
|
+
gap: 1rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
form input {
|
|
21
|
+
flex: 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
input,
|
|
25
|
+
button {
|
|
26
|
+
font: inherit;
|
|
27
|
+
padding: 0.4rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
li {
|
|
31
|
+
border-bottom: 1px solid GrayText;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
padding: 0.75rem 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ul {
|
|
37
|
+
padding: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[aria-current='page'] {
|
|
41
|
+
font-weight: bold;
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.void/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowImportingTsExtensions": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"forceConsistentCasingInFileNames": true,
|
|
7
|
+
"incremental": true,
|
|
8
|
+
"isolatedModules": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
11
|
+
"module": "ESNext",
|
|
12
|
+
"moduleResolution": "Bundler",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"noImplicitOverride": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"strict": true,
|
|
19
|
+
"target": "ES2022",
|
|
20
|
+
"types": ["vite/client", "void/env"]
|
|
21
|
+
},
|
|
22
|
+
"include": [
|
|
23
|
+
".void/**/*.d.ts",
|
|
24
|
+
"pages/**/*.ts",
|
|
25
|
+
"pages/**/*.tsx",
|
|
26
|
+
"pages/**/*.vue",
|
|
27
|
+
"src/**/*.ts",
|
|
28
|
+
"src/**/*.tsx",
|
|
29
|
+
"src/**/*.vue",
|
|
30
|
+
"vite.config.ts",
|
|
31
|
+
"void.config.ts"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { voidReact } from '@void/react/plugin';
|
|
2
|
+
import { defineConfig } from 'vite-plus';
|
|
3
|
+
import { voidPlugin } from 'void';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
fmt: { ignorePatterns: ['.void/', 'dist/', 'pnpm-lock.yaml'], singleQuote: true },
|
|
7
|
+
lint: {
|
|
8
|
+
ignorePatterns: ['.void/', 'dist/'],
|
|
9
|
+
options: { typeAware: true, typeCheck: true },
|
|
10
|
+
},
|
|
11
|
+
plugins: [voidPlugin(), voidReact()],
|
|
12
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'void/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
head: {
|
|
5
|
+
htmlAttrs: { lang: 'en' },
|
|
6
|
+
meta: [{ content: 'width=device-width, initial-scale=1', name: 'viewport' }],
|
|
7
|
+
title: 'Tasks',
|
|
8
|
+
},
|
|
9
|
+
output: 'static',
|
|
10
|
+
routing: { typedRoutes: true },
|
|
11
|
+
});
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "vp run fbtee:all && vp build",
|
|
13
13
|
"dev": "vp dev",
|
|
14
|
-
"dev:setup": "vp run fbtee:all",
|
|
14
|
+
"dev:setup": "vp run fbtee:all && vp run prepare:void",
|
|
15
15
|
"fate:generate": "vp exec fate generate",
|
|
16
16
|
"fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
|
|
17
17
|
"fbtee:collect": "vp exec fbtee collect",
|
|
18
18
|
"fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
|
|
19
19
|
"fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
|
|
20
|
+
"prepare:void": "void prepare",
|
|
20
21
|
"start": "node start.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"@nkzw/stack": "^2.3.2",
|
|
28
29
|
"@radix-ui/react-slot": "^1.5.0",
|
|
29
30
|
"@trpc/client": "^11.19.0",
|
|
30
|
-
"@void/react": "^1.
|
|
31
|
+
"@void/react": "^1.7.0",
|
|
31
32
|
"better-auth": "^1.7.14",
|
|
32
33
|
"class-variance-authority": "^0.7.1",
|
|
33
34
|
"clsx": "^2.1.1",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"react-error-boundary": "^6.1.6",
|
|
39
40
|
"react-fate": "latest",
|
|
40
41
|
"tailwind-merge": "^3.7.0",
|
|
41
|
-
"void": "^1.
|
|
42
|
+
"void": "^1.7.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@nkzw/fbtee-compiler": "^5.0.0",
|
|
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
|
|
|
4
4
|
import Section from '../../src/ui/Section.tsx';
|
|
5
5
|
|
|
6
6
|
export default function PostPage() {
|
|
7
|
-
const { id } = useParams<
|
|
7
|
+
const { id } = useParams<'/post/[id]'>();
|
|
8
8
|
|
|
9
9
|
if (!id) {
|
|
10
10
|
throw new Error('fate: Post ID is required.');
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Link as VoidLink } from '@void/react';
|
|
2
2
|
import type { ComponentProps } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
type VoidLinkProps = ComponentProps<typeof VoidLink>;
|
|
5
|
+
|
|
6
|
+
export type LinkProps = Omit<VoidLinkProps, 'href'> & {
|
|
7
|
+
href?: VoidLinkProps['href'];
|
|
8
|
+
to?: VoidLinkProps['href'];
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
export default function Link({ href, to, ...props }: LinkProps) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../tsconfig.json", "./.void/tsconfig.json"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": ["vite/client", "void/env"]
|
|
5
|
+
},
|
|
6
|
+
"include": [
|
|
7
|
+
".fate/**/*.ts",
|
|
8
|
+
".void/**/*.d.ts",
|
|
9
|
+
"pages/**/*.ts",
|
|
10
|
+
"pages/**/*.tsx",
|
|
11
|
+
"src/**/*.ts",
|
|
12
|
+
"src/**/*.tsx",
|
|
13
|
+
"env.ts",
|
|
14
|
+
"vite.config.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "vp run fbtee:all && vp build",
|
|
13
13
|
"dev": "vp dev",
|
|
14
|
-
"dev:setup": "vp run fbtee:all",
|
|
14
|
+
"dev:setup": "vp run fbtee:all && vp run prepare:void",
|
|
15
15
|
"fate:generate": "vp exec fate generate",
|
|
16
16
|
"fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
|
|
17
17
|
"fbtee:collect": "vp exec fbtee collect",
|
|
18
18
|
"fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
|
|
19
19
|
"fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
|
|
20
|
+
"prepare:void": "void prepare",
|
|
20
21
|
"start": "node start.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"@nkzw/define-env": "^1.1.0",
|
|
27
28
|
"@nkzw/stack": "^2.3.2",
|
|
28
29
|
"@radix-ui/react-slot": "^1.5.0",
|
|
29
|
-
"@void/react": "^1.
|
|
30
|
+
"@void/react": "^1.7.0",
|
|
30
31
|
"better-auth": "^1.7.14",
|
|
31
32
|
"class-variance-authority": "^0.7.1",
|
|
32
33
|
"clsx": "^2.1.1",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"react-error-boundary": "^6.1.6",
|
|
39
40
|
"react-fate": "latest",
|
|
40
41
|
"tailwind-merge": "^3.7.0",
|
|
41
|
-
"void": "^1.
|
|
42
|
+
"void": "^1.7.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@nkzw/fbtee-compiler": "^5.0.0",
|
|
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
|
|
|
4
4
|
import Section from '../../src/ui/Section.tsx';
|
|
5
5
|
|
|
6
6
|
export default function PostPage() {
|
|
7
|
-
const { id } = useParams<
|
|
7
|
+
const { id } = useParams<'/post/[id]'>();
|
|
8
8
|
|
|
9
9
|
if (!id) {
|
|
10
10
|
throw new Error('fate: Post ID is required.');
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Link as VoidLink } from '@void/react';
|
|
2
2
|
import type { ComponentProps } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
type VoidLinkProps = ComponentProps<typeof VoidLink>;
|
|
5
|
+
|
|
6
|
+
export type LinkProps = Omit<VoidLinkProps, 'href'> & {
|
|
7
|
+
href?: VoidLinkProps['href'];
|
|
8
|
+
to?: VoidLinkProps['href'];
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
export default function Link({ href, to, ...props }: LinkProps) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../tsconfig.json", "./.void/tsconfig.json"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": ["vite/client", "void/env"]
|
|
5
|
+
},
|
|
6
|
+
"include": [
|
|
7
|
+
".fate/**/*.ts",
|
|
8
|
+
".void/**/*.d.ts",
|
|
9
|
+
"pages/**/*.ts",
|
|
10
|
+
"pages/**/*.tsx",
|
|
11
|
+
"src/**/*.ts",
|
|
12
|
+
"src/**/*.tsx",
|
|
13
|
+
"env.ts",
|
|
14
|
+
"vite.config.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -24,19 +24,6 @@ export default defineConfig({
|
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
],
|
|
27
|
-
rules: {
|
|
28
|
-
'import-x-js/no-extraneous-dependencies': [
|
|
29
|
-
'error',
|
|
30
|
-
{
|
|
31
|
-
devDependencies: [
|
|
32
|
-
'./prisma.config.ts',
|
|
33
|
-
'./scripts/**/*.tsx',
|
|
34
|
-
'./src/__tests__/**/*.tsx',
|
|
35
|
-
'./vite.config.ts',
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
27
|
},
|
|
41
28
|
pack: {
|
|
42
29
|
deps: { resolveDepSubpath: true },
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@nkzw/fate": "latest",
|
|
26
|
-
"@void/react": "^1.
|
|
26
|
+
"@void/react": "^1.7.0",
|
|
27
27
|
"fbtee": "^5.0.0",
|
|
28
28
|
"graphql-sse": "^2.6.3",
|
|
29
29
|
"lucide-react": "^1.61.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"react-dom": "^19.3.1",
|
|
32
32
|
"react-error-boundary": "^6.1.6",
|
|
33
33
|
"react-fate": "latest",
|
|
34
|
-
"void": "^1.
|
|
34
|
+
"void": "^1.7.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@nkzw/eslint-plugin": "^2.0.0",
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "vp run fbtee:all && vp build",
|
|
13
13
|
"dev": "vp dev",
|
|
14
|
-
"dev:setup": "vp run fbtee:all",
|
|
14
|
+
"dev:setup": "vp run fbtee:all && vp run prepare:void",
|
|
15
15
|
"fate:generate": "vp exec fate generate",
|
|
16
16
|
"fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
|
|
17
17
|
"fbtee:collect": "vp exec fbtee collect",
|
|
18
18
|
"fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
|
|
19
19
|
"fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
|
|
20
|
+
"prepare:void": "void prepare",
|
|
20
21
|
"start": "node start.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"@nkzw/define-env": "^1.1.0",
|
|
27
28
|
"@nkzw/stack": "^2.3.2",
|
|
28
29
|
"@radix-ui/react-slot": "^1.5.0",
|
|
29
|
-
"@void/react": "^1.
|
|
30
|
+
"@void/react": "^1.7.0",
|
|
30
31
|
"better-auth": "^1.7.14",
|
|
31
32
|
"class-variance-authority": "^0.7.1",
|
|
32
33
|
"clsx": "^2.1.1",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"react-error-boundary": "^6.1.6",
|
|
38
39
|
"react-fate": "latest",
|
|
39
40
|
"tailwind-merge": "^3.7.0",
|
|
40
|
-
"void": "^1.
|
|
41
|
+
"void": "^1.7.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@nkzw/fbtee-compiler": "^5.0.0",
|
|
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
|
|
|
4
4
|
import Section from '../../src/ui/Section.tsx';
|
|
5
5
|
|
|
6
6
|
export default function PostPage() {
|
|
7
|
-
const { id } = useParams<
|
|
7
|
+
const { id } = useParams<'/post/[id]'>();
|
|
8
8
|
|
|
9
9
|
if (!id) {
|
|
10
10
|
throw new Error('fate: Post ID is required.');
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Link as VoidLink } from '@void/react';
|
|
2
2
|
import type { ComponentProps } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
type VoidLinkProps = ComponentProps<typeof VoidLink>;
|
|
5
|
+
|
|
6
|
+
export type LinkProps = Omit<VoidLinkProps, 'href'> & {
|
|
7
|
+
href?: VoidLinkProps['href'];
|
|
8
|
+
to?: VoidLinkProps['href'];
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
export default function Link({ href, to, ...props }: LinkProps) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../tsconfig.json", "./.void/tsconfig.json"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": ["vite/client", "void/env"]
|
|
5
|
+
},
|
|
6
|
+
"include": [
|
|
7
|
+
".fate/**/*.ts",
|
|
8
|
+
".void/**/*.d.ts",
|
|
9
|
+
"pages/**/*.ts",
|
|
10
|
+
"pages/**/*.tsx",
|
|
11
|
+
"src/**/*.ts",
|
|
12
|
+
"src/**/*.tsx",
|
|
13
|
+
"env.ts",
|
|
14
|
+
"vite.config.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "vp run fbtee:all && vp build",
|
|
13
13
|
"dev": "vp dev",
|
|
14
|
-
"dev:setup": "vp run fbtee:all",
|
|
14
|
+
"dev:setup": "vp run fbtee:all && vp run prepare:void",
|
|
15
15
|
"fate:generate": "vp exec fate generate",
|
|
16
16
|
"fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
|
|
17
17
|
"fbtee:collect": "vp exec fbtee collect",
|
|
18
18
|
"fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
|
|
19
19
|
"fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
|
|
20
|
+
"prepare:void": "void prepare",
|
|
20
21
|
"start": "node start.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"@nkzw/stack": "^2.3.2",
|
|
28
29
|
"@radix-ui/react-slot": "^1.5.0",
|
|
29
30
|
"@trpc/client": "^11.19.0",
|
|
30
|
-
"@void/react": "^1.
|
|
31
|
+
"@void/react": "^1.7.0",
|
|
31
32
|
"better-auth": "^1.7.14",
|
|
32
33
|
"class-variance-authority": "^0.7.1",
|
|
33
34
|
"clsx": "^2.1.1",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"react-error-boundary": "^6.1.6",
|
|
39
40
|
"react-fate": "latest",
|
|
40
41
|
"tailwind-merge": "^3.7.0",
|
|
41
|
-
"void": "^1.
|
|
42
|
+
"void": "^1.7.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@nkzw/fbtee-compiler": "^5.0.0",
|
|
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
|
|
|
4
4
|
import Section from '../../src/ui/Section.tsx';
|
|
5
5
|
|
|
6
6
|
export default function PostPage() {
|
|
7
|
-
const { id } = useParams<
|
|
7
|
+
const { id } = useParams<'/post/[id]'>();
|
|
8
8
|
|
|
9
9
|
if (!id) {
|
|
10
10
|
throw new Error('fate: Post ID is required.');
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Link as VoidLink } from '@void/react';
|
|
2
2
|
import type { ComponentProps } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
type VoidLinkProps = ComponentProps<typeof VoidLink>;
|
|
5
|
+
|
|
6
|
+
export type LinkProps = Omit<VoidLinkProps, 'href'> & {
|
|
7
|
+
href?: VoidLinkProps['href'];
|
|
8
|
+
to?: VoidLinkProps['href'];
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
export default function Link({ href, to, ...props }: LinkProps) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../tsconfig.json", "./.void/tsconfig.json"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": ["vite/client", "void/env"]
|
|
5
|
+
},
|
|
6
|
+
"include": [
|
|
7
|
+
".fate/**/*.ts",
|
|
8
|
+
".void/**/*.d.ts",
|
|
9
|
+
"pages/**/*.ts",
|
|
10
|
+
"pages/**/*.tsx",
|
|
11
|
+
"src/**/*.ts",
|
|
12
|
+
"src/**/*.tsx",
|
|
13
|
+
"env.ts",
|
|
14
|
+
"vite.config.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@nkzw/fate": "latest",
|
|
30
30
|
"@nkzw/stack": "^2.3.2",
|
|
31
31
|
"@radix-ui/react-slot": "^1.5.0",
|
|
32
|
-
"@void/react": "^1.
|
|
32
|
+
"@void/react": "^1.7.0",
|
|
33
33
|
"better-auth": "^1.7.15",
|
|
34
34
|
"class-variance-authority": "^0.7.1",
|
|
35
35
|
"clsx": "^2.1.1",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"react-error-boundary": "^6.1.6",
|
|
42
42
|
"react-fate": "latest",
|
|
43
43
|
"tailwind-merge": "^3.7.0",
|
|
44
|
-
"void": "^1.
|
|
44
|
+
"void": "^1.7.0",
|
|
45
45
|
"void-fate": "latest",
|
|
46
46
|
"zod": "^4.6.7"
|
|
47
47
|
},
|
|
@@ -4,7 +4,7 @@ import CategoryCard, { CategoryView } from '../../src/ui/CategoryCard.tsx';
|
|
|
4
4
|
import Section from '../../src/ui/Section.tsx';
|
|
5
5
|
|
|
6
6
|
export default function CategoryPage() {
|
|
7
|
-
const { id } = useParams<
|
|
7
|
+
const { id } = useParams<'/category/[id]'>();
|
|
8
8
|
|
|
9
9
|
if (!id) {
|
|
10
10
|
throw new Error('fate: Category ID is required.');
|
|
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
|
|
|
4
4
|
import Section from '../../src/ui/Section.tsx';
|
|
5
5
|
|
|
6
6
|
export default function PostPage() {
|
|
7
|
-
const { id } = useParams<
|
|
7
|
+
const { id } = useParams<'/post/[id]'>();
|
|
8
8
|
|
|
9
9
|
if (!id) {
|
|
10
10
|
throw new Error('fate: Post ID is required.');
|