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 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.0",
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 === "nuxt" || f === "solid");
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 !== "nuxt" && option.value !== "solid";
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 === "nuxt" || f === "solid");
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
- const _hasTanstackRouter = frontend.includes("tanstack-router");
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
- const _hasSolid = frontend.includes("solid");
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 = characters.length;
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 || "bun run"} deploy`}`;
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.28.5",
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.5.0",
66
- "trpc-cli": "^0.10.0",
65
+ "posthog-node": "^5.6.0",
66
+ "trpc-cli": "^0.10.2",
67
67
  "ts-morph": "^26.0.0",
68
- "zod": "^4.0.5"
68
+ "zod": "^4.0.14"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/fs-extra": "^11.0.4",
72
- "@types/node": "^24.0.13",
73
- "tsdown": "^0.12.9",
74
- "typescript": "^5.8.3"
72
+ "@types/node": "^24.2.0",
73
+ "tsdown": "^0.13.3",
74
+ "typescript": "^5.9.2"
75
75
  }
76
76
  }
@@ -9,9 +9,9 @@
9
9
  "license": "ISC",
10
10
  "description": "",
11
11
  "devDependencies": {
12
- "typescript": "^5.8.3"
12
+ "typescript": "^5.9.2"
13
13
  },
14
14
  "dependencies": {
15
- "convex": "^1.25.0"
15
+ "convex": "^1.25.4"
16
16
  }
17
17
  }
@@ -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,4 +1,4 @@
1
- {{#if (includes frontend "nuxt")}}
1
+ {{#if (or (includes frontend "nuxt") (includes frontend "native-nativewind"))}}
2
2
  # [install]
3
3
  # linker = "isolated"
4
4
  {{else}}
@@ -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,5 +1,4 @@
1
1
  <script setup lang="ts">
2
- import { USeparator } from '#components';
3
2
  import ModeToggle from './ModeToggle.vue'
4
3
  {{#if auth}}
5
4
  import UserMenu from './UserMenu.vue'
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
2
4
  const colorMode = useColorMode()
3
5
 
4
6
  const isDark = computed({
@@ -1,8 +1,13 @@
1
1
  <script setup lang="ts">
2
- {{#unless (eq api "none")}}
3
- const { $orpc } = useNuxtApp()
4
- import { useQuery } from '@tanstack/vue-query'
5
- {{/unless}}
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
- {{#unless (eq api "none")}}
24
- const healthCheck = useQuery($orpc.healthCheck.queryOptions())
25
- {{/unless}}
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
- </div>
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: ['@nuxt/ui'],
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>