create-better-t-stack 2.28.5 → 2.29.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/dist/index.js +22 -17
- package/package.json +7 -7
- package/templates/backend/convex/packages/backend/package.json.hbs +2 -2
- package/templates/examples/todo/web/nuxt/app/pages/todos.vue.hbs +195 -0
- package/templates/extras/bunfig.toml.hbs +1 -1
- package/templates/frontend/nuxt/app/{app.vue → app.vue.hbs} +4 -0
- package/templates/frontend/nuxt/app/components/Header.vue.hbs +0 -1
- package/templates/frontend/nuxt/app/components/{ModeToggle.vue → ModeToggle.vue.hbs} +2 -0
- package/templates/frontend/nuxt/app/pages/index.vue.hbs +31 -10
- package/templates/frontend/nuxt/nuxt.config.ts.hbs +11 -1
- package/templates/frontend/nuxt/package.json.hbs +0 -2
- package/templates/examples/todo/web/nuxt/app/pages/todos.vue +0 -108
- /package/templates/{frontend → api/orpc/web}/nuxt/app/plugins/vue-query.ts.hbs +0 -0
- /package/templates/frontend/nuxt/app/components/{Loader.vue → Loader.vue.hbs} +0 -0
package/dist/index.js
CHANGED
|
@@ -100,10 +100,14 @@ const dependencyVersionMap = {
|
|
|
100
100
|
"@trpc/tanstack-react-query": "^11.4.2",
|
|
101
101
|
"@trpc/server": "^11.4.2",
|
|
102
102
|
"@trpc/client": "^11.4.2",
|
|
103
|
-
convex: "^1.25.
|
|
103
|
+
convex: "^1.25.4",
|
|
104
104
|
"@convex-dev/react-query": "^0.0.0-alpha.8",
|
|
105
105
|
"convex-svelte": "^0.0.11",
|
|
106
|
+
"convex-nuxt": "0.1.5",
|
|
107
|
+
"convex-vue": "^0.1.5",
|
|
106
108
|
"@tanstack/svelte-query": "^5.74.4",
|
|
109
|
+
"@tanstack/vue-query-devtools": "^5.83.0",
|
|
110
|
+
"@tanstack/vue-query": "^5.83.0",
|
|
107
111
|
"@tanstack/react-query-devtools": "^5.80.5",
|
|
108
112
|
"@tanstack/react-query": "^5.80.5",
|
|
109
113
|
"@tanstack/solid-query": "^5.75.0",
|
|
@@ -471,7 +475,7 @@ async function getAuthChoice(auth, hasDatabase, backend) {
|
|
|
471
475
|
//#region src/prompts/backend.ts
|
|
472
476
|
async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
473
477
|
if (backendFramework !== void 0) return backendFramework;
|
|
474
|
-
const hasIncompatibleFrontend = frontends?.some((f) => f === "
|
|
478
|
+
const hasIncompatibleFrontend = frontends?.some((f) => f === "solid");
|
|
475
479
|
const backendOptions = [
|
|
476
480
|
{
|
|
477
481
|
value: "hono",
|
|
@@ -509,12 +513,10 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
|
509
513
|
label: "None",
|
|
510
514
|
hint: "No backend server"
|
|
511
515
|
});
|
|
512
|
-
let initialValue = DEFAULT_CONFIG.backend;
|
|
513
|
-
if (hasIncompatibleFrontend && initialValue === "convex") initialValue = "hono";
|
|
514
516
|
const response = await select({
|
|
515
517
|
message: "Select backend",
|
|
516
518
|
options: backendOptions,
|
|
517
|
-
initialValue
|
|
519
|
+
initialValue: DEFAULT_CONFIG.backend
|
|
518
520
|
});
|
|
519
521
|
if (isCancel(response)) {
|
|
520
522
|
cancel(pc.red("Operation cancelled"));
|
|
@@ -754,7 +756,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
754
756
|
}
|
|
755
757
|
];
|
|
756
758
|
const webOptions = allWebOptions.filter((option) => {
|
|
757
|
-
if (backend === "convex") return option.value !== "
|
|
759
|
+
if (backend === "convex") return option.value !== "solid";
|
|
758
760
|
return true;
|
|
759
761
|
});
|
|
760
762
|
const webFramework = await select({
|
|
@@ -1466,7 +1468,7 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1466
1468
|
process.exit(1);
|
|
1467
1469
|
}
|
|
1468
1470
|
if (providedFlags.has("frontend") && options.frontend) {
|
|
1469
|
-
const incompatibleFrontends = options.frontend.filter((f) => f === "
|
|
1471
|
+
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1470
1472
|
if (incompatibleFrontends.length > 0) {
|
|
1471
1473
|
consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1472
1474
|
process.exit(1);
|
|
@@ -1618,7 +1620,7 @@ const BTS_CONFIG_FILE = "bts.jsonc";
|
|
|
1618
1620
|
async function writeBtsConfig(projectConfig) {
|
|
1619
1621
|
const btsConfig = {
|
|
1620
1622
|
version: getLatestCLIVersion(),
|
|
1621
|
-
createdAt: new Date().toISOString(),
|
|
1623
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1622
1624
|
database: projectConfig.database,
|
|
1623
1625
|
orm: projectConfig.orm,
|
|
1624
1626
|
backend: projectConfig.backend,
|
|
@@ -1633,7 +1635,7 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1633
1635
|
webDeploy: projectConfig.webDeploy
|
|
1634
1636
|
};
|
|
1635
1637
|
const baseContent = {
|
|
1636
|
-
$schema: "https://better-t-stack.dev/schema.json",
|
|
1638
|
+
$schema: "https://r2.better-t-stack.dev/schema.json",
|
|
1637
1639
|
version: btsConfig.version,
|
|
1638
1640
|
createdAt: btsConfig.createdAt,
|
|
1639
1641
|
database: btsConfig.database,
|
|
@@ -1859,11 +1861,11 @@ async function setupTauri(config) {
|
|
|
1859
1861
|
};
|
|
1860
1862
|
await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
|
|
1861
1863
|
}
|
|
1862
|
-
|
|
1864
|
+
frontend.includes("tanstack-router");
|
|
1863
1865
|
const hasReactRouter = frontend.includes("react-router");
|
|
1864
1866
|
const hasNuxt = frontend.includes("nuxt");
|
|
1865
1867
|
const hasSvelte = frontend.includes("svelte");
|
|
1866
|
-
|
|
1868
|
+
frontend.includes("solid");
|
|
1867
1869
|
const hasNext = frontend.includes("next");
|
|
1868
1870
|
const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
|
|
1869
1871
|
const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
|
|
@@ -2292,7 +2294,6 @@ async function setupFrontendTemplates(projectDir, context) {
|
|
|
2292
2294
|
const hasSolidWeb = context.frontend.includes("solid");
|
|
2293
2295
|
const hasNativeWind = context.frontend.includes("native-nativewind");
|
|
2294
2296
|
const hasUnistyles = context.frontend.includes("native-unistyles");
|
|
2295
|
-
const _hasNative = hasNativeWind || hasUnistyles;
|
|
2296
2297
|
const isConvex = context.backend === "convex";
|
|
2297
2298
|
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
|
|
2298
2299
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
@@ -2703,7 +2704,7 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
|
2703
2704
|
if (!defineCall) return;
|
|
2704
2705
|
const configObj = defineCall.getArguments()[0];
|
|
2705
2706
|
if (!configObj) return;
|
|
2706
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
2707
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2707
2708
|
const compatProp = configObj.getProperty("compatibilityDate");
|
|
2708
2709
|
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
|
|
2709
2710
|
else configObj.addPropertyAssignment({
|
|
@@ -3010,6 +3011,8 @@ async function setupApi(config) {
|
|
|
3010
3011
|
} else if (hasNuxtWeb) {
|
|
3011
3012
|
if (api === "orpc") await addPackageDependency({
|
|
3012
3013
|
dependencies: [
|
|
3014
|
+
"@tanstack/vue-query",
|
|
3015
|
+
"@tanstack/vue-query-devtools",
|
|
3013
3016
|
"@orpc/tanstack-query",
|
|
3014
3017
|
"@orpc/client",
|
|
3015
3018
|
"@orpc/server"
|
|
@@ -3113,6 +3116,10 @@ async function setupApi(config) {
|
|
|
3113
3116
|
const webDepsToAdd = ["convex"];
|
|
3114
3117
|
if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query");
|
|
3115
3118
|
if (hasSvelteWeb) webDepsToAdd.push("convex-svelte");
|
|
3119
|
+
if (hasNuxtWeb) {
|
|
3120
|
+
webDepsToAdd.push("convex-nuxt");
|
|
3121
|
+
webDepsToAdd.push("convex-vue");
|
|
3122
|
+
}
|
|
3116
3123
|
await addPackageDependency({
|
|
3117
3124
|
dependencies: webDepsToAdd,
|
|
3118
3125
|
projectDir: webDir
|
|
@@ -3198,7 +3205,7 @@ async function setupAuth(config) {
|
|
|
3198
3205
|
function generateAuthSecret(length = 32) {
|
|
3199
3206
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
3200
3207
|
let result = "";
|
|
3201
|
-
const charactersLength =
|
|
3208
|
+
const charactersLength = 62;
|
|
3202
3209
|
for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
3203
3210
|
return result;
|
|
3204
3211
|
}
|
|
@@ -4170,13 +4177,11 @@ DATABASE_AUTH_TOKEN=your_auth_token`);
|
|
|
4170
4177
|
}
|
|
4171
4178
|
async function setupTurso(config) {
|
|
4172
4179
|
const { orm, projectDir } = config;
|
|
4173
|
-
const _isDrizzle = orm === "drizzle";
|
|
4174
4180
|
const setupSpinner = spinner();
|
|
4175
4181
|
setupSpinner.start("Checking Turso CLI availability...");
|
|
4176
4182
|
try {
|
|
4177
4183
|
const platform = os.platform();
|
|
4178
4184
|
const isMac = platform === "darwin";
|
|
4179
|
-
const _isLinux = platform === "linux";
|
|
4180
4185
|
const isWindows = platform === "win32";
|
|
4181
4186
|
if (isWindows) {
|
|
4182
4187
|
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
|
|
@@ -4903,7 +4908,7 @@ function getBunWebNativeWarning() {
|
|
|
4903
4908
|
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.`;
|
|
4904
4909
|
}
|
|
4905
4910
|
function getWorkersDeployInstructions(runCmd) {
|
|
4906
|
-
return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd
|
|
4911
|
+
return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
|
|
4907
4912
|
}
|
|
4908
4913
|
|
|
4909
4914
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.1",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,15 +62,15 @@
|
|
|
62
62
|
"handlebars": "^4.7.8",
|
|
63
63
|
"jsonc-parser": "^3.3.1",
|
|
64
64
|
"picocolors": "^1.1.1",
|
|
65
|
-
"posthog-node": "^5.
|
|
66
|
-
"trpc-cli": "^0.10.
|
|
65
|
+
"posthog-node": "^5.6.0",
|
|
66
|
+
"trpc-cli": "^0.10.2",
|
|
67
67
|
"ts-morph": "^26.0.0",
|
|
68
|
-
"zod": "^4.0.
|
|
68
|
+
"zod": "^4.0.14"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/fs-extra": "^11.0.4",
|
|
72
|
-
"@types/node": "^24.0
|
|
73
|
-
"tsdown": "^0.
|
|
74
|
-
"typescript": "^5.
|
|
72
|
+
"@types/node": "^24.2.0",
|
|
73
|
+
"tsdown": "^0.13.3",
|
|
74
|
+
"typescript": "^5.9.2"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
{{#if (eq backend "convex")}}
|
|
4
|
+
import { api } from "@{{ projectName }}/backend/convex/_generated/api";
|
|
5
|
+
import type { Id } from "@{{ projectName }}/backend/convex/_generated/dataModel";
|
|
6
|
+
import { useConvexMutation, useConvexQuery } from "convex-vue";
|
|
7
|
+
|
|
8
|
+
const { data, error, isPending } = useConvexQuery(api.todos.getAll, {});
|
|
9
|
+
|
|
10
|
+
const newTodoText = ref("");
|
|
11
|
+
const { mutate: createTodo, isPending: isCreatePending } = useConvexMutation(api.todos.create);
|
|
12
|
+
|
|
13
|
+
const { mutate: toggleTodo } = useConvexMutation(api.todos.toggle);
|
|
14
|
+
const { mutate: deleteTodo, error: deleteError } = useConvexMutation(
|
|
15
|
+
api.todos.deleteTodo,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
function handleAddTodo() {
|
|
19
|
+
const text = newTodoText.value.trim();
|
|
20
|
+
if (!text) return;
|
|
21
|
+
|
|
22
|
+
createTodo({ text });
|
|
23
|
+
newTodoText.value = "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleToggleTodo(id: Id<"todos">, completed: boolean) {
|
|
27
|
+
toggleTodo({ id, completed: !completed });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleDeleteTodo(id: Id<"todos">) {
|
|
31
|
+
deleteTodo({ id });
|
|
32
|
+
}
|
|
33
|
+
{{else}}
|
|
34
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
|
35
|
+
|
|
36
|
+
const { $orpc } = useNuxtApp()
|
|
37
|
+
|
|
38
|
+
const newTodoText = ref('')
|
|
39
|
+
const queryClient = useQueryClient()
|
|
40
|
+
|
|
41
|
+
const todos = useQuery($orpc.todo.getAll.queryOptions())
|
|
42
|
+
|
|
43
|
+
const createMutation = useMutation($orpc.todo.create.mutationOptions({
|
|
44
|
+
onSuccess: () => {
|
|
45
|
+
queryClient.invalidateQueries()
|
|
46
|
+
newTodoText.value = ''
|
|
47
|
+
}
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
const toggleMutation = useMutation($orpc.todo.toggle.mutationOptions({
|
|
51
|
+
onSuccess: () => queryClient.invalidateQueries()
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
const deleteMutation = useMutation($orpc.todo.delete.mutationOptions({
|
|
55
|
+
onSuccess: () => queryClient.invalidateQueries()
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
function handleAddTodo() {
|
|
59
|
+
if (newTodoText.value.trim()) {
|
|
60
|
+
createMutation.mutate({ text: newTodoText.value })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleToggleTodo(id: number, completed: boolean) {
|
|
65
|
+
toggleMutation.mutate({ id, completed: !completed })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleDeleteTodo(id: number) {
|
|
69
|
+
deleteMutation.mutate({ id })
|
|
70
|
+
}
|
|
71
|
+
{{/if}}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<div class="mx-auto w-full max-w-md py-10">
|
|
76
|
+
<UCard>
|
|
77
|
+
<template #header>
|
|
78
|
+
<div>
|
|
79
|
+
<div class="text-xl font-bold">Todo List</div>
|
|
80
|
+
<div class="text-muted text-sm">Manage your tasks efficiently</div>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
<form @submit.prevent="handleAddTodo" class="mb-6 flex items-center gap-2">
|
|
84
|
+
<UInput
|
|
85
|
+
v-model="newTodoText"
|
|
86
|
+
placeholder="Add a new task..."
|
|
87
|
+
autocomplete="off"
|
|
88
|
+
class="w-full"
|
|
89
|
+
{{#if (eq backend "convex")}}
|
|
90
|
+
:disabled="isCreatePending"
|
|
91
|
+
{{/if}}
|
|
92
|
+
/>
|
|
93
|
+
<UButton
|
|
94
|
+
type="submit"
|
|
95
|
+
{{#if (eq backend "convex")}}
|
|
96
|
+
:disabled="isCreatePending || !newTodoText.trim()"
|
|
97
|
+
{{/if}}
|
|
98
|
+
>
|
|
99
|
+
{{#if (eq backend "convex")}}
|
|
100
|
+
<span v-if="isCreatePending">
|
|
101
|
+
<UIcon name="i-lucide-loader-2" class="animate-spin" />
|
|
102
|
+
</span>
|
|
103
|
+
<span v-else>Add</span>
|
|
104
|
+
{{else}}
|
|
105
|
+
Add
|
|
106
|
+
{{/if}}
|
|
107
|
+
</UButton>
|
|
108
|
+
</form>
|
|
109
|
+
|
|
110
|
+
{{#if (eq backend "convex")}}
|
|
111
|
+
<p v-if="error || deleteError" class="mb-4 text-red-500">
|
|
112
|
+
Error: \{{ error?.message || deleteError?.message }}
|
|
113
|
+
</p>
|
|
114
|
+
<div v-if="isPending" class="flex justify-center py-4">
|
|
115
|
+
<UIcon name="i-lucide-loader-2" class="animate-spin w-6 h-6" />
|
|
116
|
+
</div>
|
|
117
|
+
<p v-else-if="data?.length === 0" class="py-4 text-center">
|
|
118
|
+
No todos yet. Add one above!
|
|
119
|
+
</p>
|
|
120
|
+
<ul v-else-if="data" class="space-y-2">
|
|
121
|
+
<li
|
|
122
|
+
v-for="todo in data"
|
|
123
|
+
:key="todo._id"
|
|
124
|
+
class="flex items-center justify-between rounded-md border p-2"
|
|
125
|
+
>
|
|
126
|
+
<div class="flex items-center gap-2">
|
|
127
|
+
<UCheckbox
|
|
128
|
+
:model-value="todo.completed"
|
|
129
|
+
@update:model-value="() => handleToggleTodo(todo._id, todo.completed)"
|
|
130
|
+
:id="`todo-${todo._id}`"
|
|
131
|
+
/>
|
|
132
|
+
<label
|
|
133
|
+
:for="`todo-${todo._id}`"
|
|
134
|
+
:class="{ 'line-through text-muted': todo.completed }"
|
|
135
|
+
class="cursor-pointer"
|
|
136
|
+
>
|
|
137
|
+
\{{ todo.text }}
|
|
138
|
+
</label>
|
|
139
|
+
</div>
|
|
140
|
+
<UButton
|
|
141
|
+
color="neutral"
|
|
142
|
+
variant="ghost"
|
|
143
|
+
size="sm"
|
|
144
|
+
square
|
|
145
|
+
@click="handleDeleteTodo(todo._id)"
|
|
146
|
+
aria-label="Delete todo"
|
|
147
|
+
icon="i-lucide-trash-2"
|
|
148
|
+
/>
|
|
149
|
+
</li>
|
|
150
|
+
</ul>
|
|
151
|
+
{{else}}
|
|
152
|
+
<div v-if="todos.status.value === 'pending'" class="flex justify-center py-4">
|
|
153
|
+
<UIcon name="i-lucide-loader-2" class="animate-spin w-6 h-6" />
|
|
154
|
+
</div>
|
|
155
|
+
<p v-else-if="todos.status.value === 'error'" class="py-4 text-center text-red-500">
|
|
156
|
+
Error: \{{ todos.error.value?.message || 'Failed to load todos' }}
|
|
157
|
+
</p>
|
|
158
|
+
<p v-else-if="todos.data.value?.length === 0" class="py-4 text-center">
|
|
159
|
+
No todos yet. Add one above!
|
|
160
|
+
</p>
|
|
161
|
+
<ul v-else class="space-y-2">
|
|
162
|
+
<li
|
|
163
|
+
v-for="todo in todos.data.value"
|
|
164
|
+
:key="todo.id"
|
|
165
|
+
class="flex items-center justify-between rounded-md border p-2"
|
|
166
|
+
>
|
|
167
|
+
<div class="flex items-center gap-2">
|
|
168
|
+
<UCheckbox
|
|
169
|
+
:model-value="todo.completed"
|
|
170
|
+
@update:model-value="() => handleToggleTodo(todo.id, todo.completed)"
|
|
171
|
+
:id="`todo-${todo.id}`"
|
|
172
|
+
/>
|
|
173
|
+
<label
|
|
174
|
+
:for="`todo-${todo.id}`"
|
|
175
|
+
:class="{ 'line-through text-muted': todo.completed }"
|
|
176
|
+
class="cursor-pointer"
|
|
177
|
+
>
|
|
178
|
+
\{{ todo.text }}
|
|
179
|
+
</label>
|
|
180
|
+
</div>
|
|
181
|
+
<UButton
|
|
182
|
+
color="neutral"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="sm"
|
|
185
|
+
square
|
|
186
|
+
@click="handleDeleteTodo(todo.id)"
|
|
187
|
+
aria-label="Delete todo"
|
|
188
|
+
icon="i-lucide-trash-2"
|
|
189
|
+
/>
|
|
190
|
+
</li>
|
|
191
|
+
</ul>
|
|
192
|
+
{{/if}}
|
|
193
|
+
</UCard>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
{{#if (eq api "orpc")}}
|
|
2
3
|
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
|
4
|
+
{{/if}}
|
|
3
5
|
</script>
|
|
4
6
|
|
|
5
7
|
<template>
|
|
@@ -9,5 +11,7 @@ import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
|
|
9
11
|
<NuxtPage />
|
|
10
12
|
</NuxtLayout>
|
|
11
13
|
</UApp>
|
|
14
|
+
{{#if (eq api "orpc")}}
|
|
12
15
|
<VueQueryDevtools />
|
|
16
|
+
{{/if}}
|
|
13
17
|
</template>
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
{{#
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
{{
|
|
2
|
+
{{#if (eq backend "convex")}}
|
|
3
|
+
import { api } from "@{{ projectName }}/backend/convex/_generated/api";
|
|
4
|
+
import { useConvexQuery } from "convex-vue";
|
|
5
|
+
{{else}}
|
|
6
|
+
{{#unless (eq api "none")}}
|
|
7
|
+
const { $orpc } = useNuxtApp()
|
|
8
|
+
import { useQuery } from '@tanstack/vue-query'
|
|
9
|
+
{{/unless}}
|
|
10
|
+
{{/if}}
|
|
6
11
|
|
|
7
12
|
const TITLE_TEXT = `
|
|
8
13
|
██████╗ ███████╗████████╗████████╗███████╗██████╗
|
|
@@ -20,19 +25,34 @@ const TITLE_TEXT = `
|
|
|
20
25
|
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
|
21
26
|
`;
|
|
22
27
|
|
|
23
|
-
{{#
|
|
24
|
-
const healthCheck =
|
|
25
|
-
{{
|
|
28
|
+
{{#if (eq backend "convex")}}
|
|
29
|
+
const healthCheck = useConvexQuery(api.healthCheck.get, {});
|
|
30
|
+
{{else}}
|
|
31
|
+
{{#unless (eq api "none")}}
|
|
32
|
+
const healthCheck = useQuery($orpc.healthCheck.queryOptions())
|
|
33
|
+
{{/unless}}
|
|
34
|
+
{{/if}}
|
|
26
35
|
</script>
|
|
27
36
|
|
|
28
37
|
<template>
|
|
29
38
|
<div class="container mx-auto max-w-3xl px-4 py-2">
|
|
30
39
|
<pre class="overflow-x-auto font-mono text-sm whitespace-pre-wrap">\{{ TITLE_TEXT }}</pre>
|
|
31
40
|
<div class="grid gap-6 mt-4">
|
|
32
|
-
{{#unless (eq api "none")}}
|
|
33
41
|
<section class="rounded-lg border p-4">
|
|
34
42
|
<h2 class="mb-2 font-medium">API Status</h2>
|
|
35
43
|
<div class="flex items-center gap-2">
|
|
44
|
+
{{#if (eq backend "convex")}}
|
|
45
|
+
<span class="text-sm text-muted-foreground">
|
|
46
|
+
\{{
|
|
47
|
+
healthCheck === undefined
|
|
48
|
+
? "Checking..."
|
|
49
|
+
: healthCheck.data.value === "OK"
|
|
50
|
+
? "Connected"
|
|
51
|
+
: "Error"
|
|
52
|
+
}}
|
|
53
|
+
</span>
|
|
54
|
+
{{else}}
|
|
55
|
+
{{#unless (eq api "none")}}
|
|
36
56
|
<div class="flex items-center gap-2">
|
|
37
57
|
<div
|
|
38
58
|
class="w-2 h-2 rounded-full"
|
|
@@ -60,9 +80,10 @@ const healthCheck = useQuery($orpc.healthCheck.queryOptions())
|
|
|
60
80
|
</template>
|
|
61
81
|
</span>
|
|
62
82
|
</div>
|
|
63
|
-
|
|
83
|
+
{{/unless}}
|
|
84
|
+
{{/if}}
|
|
85
|
+
</div>
|
|
64
86
|
</section>
|
|
65
|
-
{{/unless}}
|
|
66
87
|
</div>
|
|
67
88
|
</div>
|
|
68
89
|
</template>
|
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
export default defineNuxtConfig({
|
|
3
3
|
compatibilityDate: 'latest',
|
|
4
4
|
devtools: { enabled: true },
|
|
5
|
-
modules: [
|
|
5
|
+
modules: [
|
|
6
|
+
'@nuxt/ui'
|
|
7
|
+
{{#if (eq backend "convex")}},
|
|
8
|
+
'convex-nuxt'
|
|
9
|
+
{{/if}}
|
|
10
|
+
],
|
|
6
11
|
css: ['~/assets/css/main.css'],
|
|
7
12
|
devServer: {
|
|
8
13
|
port: 3001
|
|
9
14
|
},
|
|
10
15
|
ssr: false,
|
|
16
|
+
{{#if (eq backend "convex")}}
|
|
17
|
+
convex: {
|
|
18
|
+
url: process.env.NUXT_PUBLIC_CONVEX_URL,
|
|
19
|
+
},
|
|
20
|
+
{{/if}}
|
|
11
21
|
runtimeConfig: {
|
|
12
22
|
public: {
|
|
13
23
|
serverURL: process.env.NUXT_PUBLIC_SERVER_URL,
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@nuxt/ui": "3.3.0",
|
|
14
|
-
"@tanstack/vue-query": "^5.83.0",
|
|
15
14
|
"nuxt": "^4.0.2",
|
|
16
15
|
"typescript": "^5.8.3",
|
|
17
16
|
"vue": "^3.5.18",
|
|
@@ -20,7 +19,6 @@
|
|
|
20
19
|
},
|
|
21
20
|
"devDependencies": {
|
|
22
21
|
"tailwindcss": "^4.1.11",
|
|
23
|
-
"@tanstack/vue-query-devtools": "^5.83.0",
|
|
24
22
|
"@iconify-json/lucide": "^1.2.57"
|
|
25
23
|
}
|
|
26
24
|
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
|
4
|
-
|
|
5
|
-
const { $orpc } = useNuxtApp()
|
|
6
|
-
|
|
7
|
-
const newTodoText = ref('')
|
|
8
|
-
const queryClient = useQueryClient()
|
|
9
|
-
|
|
10
|
-
const todos = useQuery($orpc.todo.getAll.queryOptions())
|
|
11
|
-
|
|
12
|
-
const createMutation = useMutation($orpc.todo.create.mutationOptions({
|
|
13
|
-
onSuccess: () => {
|
|
14
|
-
queryClient.invalidateQueries()
|
|
15
|
-
newTodoText.value = ''
|
|
16
|
-
}
|
|
17
|
-
}))
|
|
18
|
-
|
|
19
|
-
const toggleMutation = useMutation($orpc.todo.toggle.mutationOptions({
|
|
20
|
-
onSuccess: () => queryClient.invalidateQueries()
|
|
21
|
-
}))
|
|
22
|
-
|
|
23
|
-
const deleteMutation = useMutation($orpc.todo.delete.mutationOptions({
|
|
24
|
-
onSuccess: () => queryClient.invalidateQueries()
|
|
25
|
-
}))
|
|
26
|
-
|
|
27
|
-
function handleAddTodo() {
|
|
28
|
-
if (newTodoText.value.trim()) {
|
|
29
|
-
createMutation.mutate({ text: newTodoText.value })
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function handleToggleTodo(id: number, completed: boolean) {
|
|
34
|
-
toggleMutation.mutate({ id, completed: !completed })
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function handleDeleteTodo(id: number) {
|
|
38
|
-
deleteMutation.mutate({ id })
|
|
39
|
-
}
|
|
40
|
-
</script>
|
|
41
|
-
|
|
42
|
-
<template>
|
|
43
|
-
<div class="mx-auto w-full max-w-md py-10">
|
|
44
|
-
<UCard>
|
|
45
|
-
<template #header>
|
|
46
|
-
<div>
|
|
47
|
-
<div class="text-xl font-bold">Todo List</div>
|
|
48
|
-
<div class="text-muted text-sm">Manage your tasks efficiently</div>
|
|
49
|
-
</div>
|
|
50
|
-
</template>
|
|
51
|
-
<form @submit.prevent="handleAddTodo" class="mb-6 flex items-center gap-2">
|
|
52
|
-
<UInput
|
|
53
|
-
v-model="newTodoText"
|
|
54
|
-
placeholder="Add a new task..."
|
|
55
|
-
autocomplete="off"
|
|
56
|
-
class="w-full"
|
|
57
|
-
/>
|
|
58
|
-
<UButton
|
|
59
|
-
type="submit"
|
|
60
|
-
icon="i-lucide-plus"
|
|
61
|
-
>
|
|
62
|
-
Add
|
|
63
|
-
</UButton>
|
|
64
|
-
</form>
|
|
65
|
-
|
|
66
|
-
<div v-if="todos.status.value === 'pending'" class="flex justify-center py-4">
|
|
67
|
-
<UIcon name="i-lucide-loader-2" class="animate-spin w-6 h-6" />
|
|
68
|
-
</div>
|
|
69
|
-
<p v-else-if="todos.status.value === 'error'" class="py-4 text-center text-red-500">
|
|
70
|
-
Error: {{ todos.error.value?.message || 'Failed to load todos' }}
|
|
71
|
-
</p>
|
|
72
|
-
<p v-else-if="todos.data.value?.length === 0" class="py-4 text-center">
|
|
73
|
-
No todos yet. Add one above!
|
|
74
|
-
</p>
|
|
75
|
-
<ul v-else class="space-y-2">
|
|
76
|
-
<li
|
|
77
|
-
v-for="todo in todos.data.value"
|
|
78
|
-
:key="todo.id"
|
|
79
|
-
class="flex items-center justify-between rounded-md border p-2"
|
|
80
|
-
>
|
|
81
|
-
<div class="flex items-center gap-2">
|
|
82
|
-
<UCheckbox
|
|
83
|
-
:model-value="todo.completed"
|
|
84
|
-
@update:model-value="() => handleToggleTodo(todo.id, todo.completed)"
|
|
85
|
-
:id="`todo-${todo.id}`"
|
|
86
|
-
/>
|
|
87
|
-
<label
|
|
88
|
-
:for="`todo-${todo.id}`"
|
|
89
|
-
:class="{ 'line-through text-muted': todo.completed }"
|
|
90
|
-
class="cursor-pointer"
|
|
91
|
-
>
|
|
92
|
-
{{ todo.text }}
|
|
93
|
-
</label>
|
|
94
|
-
</div>
|
|
95
|
-
<UButton
|
|
96
|
-
color="neutral"
|
|
97
|
-
variant="ghost"
|
|
98
|
-
size="sm"
|
|
99
|
-
square
|
|
100
|
-
@click="handleDeleteTodo(todo.id)"
|
|
101
|
-
aria-label="Delete todo"
|
|
102
|
-
icon="i-lucide-trash-2"
|
|
103
|
-
/>
|
|
104
|
-
</li>
|
|
105
|
-
</ul>
|
|
106
|
-
</UCard>
|
|
107
|
-
</div>
|
|
108
|
-
</template>
|
|
File without changes
|
|
File without changes
|