create-better-t-stack 2.28.5 → 2.29.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/dist/index.js +16 -8
- package/package.json +1 -1
- 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/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);
|
|
@@ -3010,6 +3012,8 @@ async function setupApi(config) {
|
|
|
3010
3012
|
} else if (hasNuxtWeb) {
|
|
3011
3013
|
if (api === "orpc") await addPackageDependency({
|
|
3012
3014
|
dependencies: [
|
|
3015
|
+
"@tanstack/vue-query",
|
|
3016
|
+
"@tanstack/vue-query-devtools",
|
|
3013
3017
|
"@orpc/tanstack-query",
|
|
3014
3018
|
"@orpc/client",
|
|
3015
3019
|
"@orpc/server"
|
|
@@ -3113,6 +3117,10 @@ async function setupApi(config) {
|
|
|
3113
3117
|
const webDepsToAdd = ["convex"];
|
|
3114
3118
|
if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query");
|
|
3115
3119
|
if (hasSvelteWeb) webDepsToAdd.push("convex-svelte");
|
|
3120
|
+
if (hasNuxtWeb) {
|
|
3121
|
+
webDepsToAdd.push("convex-nuxt");
|
|
3122
|
+
webDepsToAdd.push("convex-vue");
|
|
3123
|
+
}
|
|
3116
3124
|
await addPackageDependency({
|
|
3117
3125
|
dependencies: webDepsToAdd,
|
|
3118
3126
|
projectDir: webDir
|
|
@@ -4903,7 +4911,7 @@ function getBunWebNativeWarning() {
|
|
|
4903
4911
|
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo. Use 'pnpm' if problems arise.`;
|
|
4904
4912
|
}
|
|
4905
4913
|
function getWorkersDeployInstructions(runCmd) {
|
|
4906
|
-
return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd
|
|
4914
|
+
return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
|
|
4907
4915
|
}
|
|
4908
4916
|
|
|
4909
4917
|
//#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.0",
|
|
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",
|
|
@@ -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
|