create-better-t-stack 2.1.4 → 2.1.5

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.
Files changed (28) hide show
  1. package/README.md +40 -19
  2. package/dist/index.js +97 -97
  3. package/package.json +1 -1
  4. package/templates/api/orpc/web/svelte/src/lib/orpc.ts.hbs +31 -0
  5. package/templates/auth/web/svelte/src/components/SignInForm.svelte +108 -0
  6. package/templates/auth/web/svelte/src/components/SignUpForm.svelte +142 -0
  7. package/templates/auth/web/svelte/src/components/UserMenu.svelte +54 -0
  8. package/templates/auth/web/svelte/src/lib/auth-client.ts +6 -0
  9. package/templates/auth/web/svelte/src/routes/dashboard/+page.svelte +31 -0
  10. package/templates/auth/web/svelte/src/routes/login/+page.svelte +12 -0
  11. package/templates/examples/ai/web/nuxt/app/pages/ai.vue +1 -2
  12. package/templates/examples/ai/web/svelte/src/routes/ai/+page.svelte +98 -0
  13. package/templates/examples/todo/web/svelte/src/routes/todos/+page.svelte +150 -0
  14. package/templates/frontend/react/tanstack-router/vite.config.ts.hbs +0 -2
  15. package/templates/frontend/svelte/_gitignore +23 -0
  16. package/templates/frontend/svelte/_npmrc +1 -0
  17. package/templates/frontend/svelte/package.json +31 -0
  18. package/templates/frontend/svelte/src/app.css +5 -0
  19. package/templates/frontend/svelte/src/app.d.ts +13 -0
  20. package/templates/frontend/svelte/src/app.html +12 -0
  21. package/templates/frontend/svelte/src/components/Header.svelte.hbs +40 -0
  22. package/templates/frontend/svelte/src/lib/index.ts +1 -0
  23. package/templates/frontend/svelte/src/routes/+layout.svelte.hbs +19 -0
  24. package/templates/frontend/svelte/src/routes/+page.svelte.hbs +44 -0
  25. package/templates/frontend/svelte/static/favicon.png +0 -0
  26. package/templates/frontend/svelte/svelte.config.js +18 -0
  27. package/templates/frontend/svelte/tsconfig.json +19 -0
  28. package/templates/frontend/svelte/vite.config.ts +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
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,31 @@
1
+ import { PUBLIC_SERVER_URL } from "$env/static/public";
2
+ import { createORPCClient } from "@orpc/client";
3
+ import { RPCLink } from "@orpc/client/fetch";
4
+ import type { RouterClient } from "@orpc/server";
5
+ import { createORPCSvelteQueryUtils } from "@orpc/svelte-query";
6
+ import { QueryCache, QueryClient } from "@tanstack/svelte-query";
7
+ import type { appRouter } from "../../../server/src/routers/index";
8
+
9
+ export const queryClient = new QueryClient({
10
+ queryCache: new QueryCache({
11
+ onError: (error) => {
12
+ console.error(`Error: ${error.message}`);
13
+ },
14
+ }),
15
+ });
16
+
17
+ export const link = new RPCLink({
18
+ url: `${PUBLIC_SERVER_URL}/rpc`,
19
+ {{#if auth}}
20
+ fetch(url, options) {
21
+ return fetch(url, {
22
+ ...options,
23
+ credentials: "include",
24
+ });
25
+ },
26
+ {{/if}}
27
+ });
28
+
29
+ export const client: RouterClient<typeof appRouter> = createORPCClient(link);
30
+
31
+ export const orpc = createORPCSvelteQueryUtils(client);
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import { createForm } from '@tanstack/svelte-form';
3
+ import { z } from 'zod';
4
+ import { authClient } from '$lib/auth-client';
5
+ import { goto } from '$app/navigation';
6
+
7
+ let { switchToSignUp } = $props<{ switchToSignUp: () => void }>();
8
+
9
+ const validationSchema = z.object({
10
+ email: z.string().email('Invalid email address'),
11
+ password: z.string().min(1, 'Password is required'),
12
+ });
13
+
14
+ const form = createForm(() => ({
15
+ defaultValues: { email: '', password: '' },
16
+ onSubmit: async ({ value }) => {
17
+ await authClient.signIn.email(
18
+ { email: value.email, password: value.password },
19
+ {
20
+ onSuccess: () => goto('/dashboard'),
21
+ onError: (error) => {
22
+ console.log(error.error.message || 'Sign in failed. Please try again.');
23
+ },
24
+ }
25
+ );
26
+
27
+ },
28
+ validators: {
29
+ onSubmit: validationSchema,
30
+ },
31
+ }));
32
+ </script>
33
+
34
+ <div class="mx-auto mt-10 w-full max-w-md p-6">
35
+ <h1 class="mb-6 text-center font-bold text-3xl">Welcome Back</h1>
36
+
37
+ <form
38
+ class="space-y-4"
39
+ onsubmit={(e) => {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ form.handleSubmit();
43
+ }}
44
+ >
45
+ <form.Field name="email">
46
+ {#snippet children(field)}
47
+ <div class="space-y-1">
48
+ <label for={field.name}>Email</label>
49
+ <input
50
+ id={field.name}
51
+ name={field.name}
52
+ type="email"
53
+ class="w-full border"
54
+ onblur={field.handleBlur}
55
+ value={field.state.value}
56
+ oninput={(e: Event) => {
57
+ const target = e.target as HTMLInputElement
58
+ field.handleChange(target.value)
59
+ }} />
60
+ {#if field.state.meta.isTouched}
61
+ {#each field.state.meta.errors as error}
62
+ <p class="text-sm text-red-500" role="alert">{error}</p>
63
+ {/each}
64
+ {/if}
65
+ </div>
66
+ {/snippet}
67
+ </form.Field>
68
+
69
+ <form.Field name="password">
70
+ {#snippet children(field)}
71
+ <div class="space-y-1">
72
+ <label for={field.name}>Password</label>
73
+ <input
74
+ id={field.name}
75
+ name={field.name}
76
+ type="password"
77
+ class="w-full border"
78
+ onblur={field.handleBlur}
79
+ value={field.state.value}
80
+ oninput={(e: Event) => {
81
+ const target = e.target as HTMLInputElement
82
+ field.handleChange(target.value)
83
+ }}
84
+ />
85
+ {#if field.state.meta.isTouched}
86
+ {#each field.state.meta.errors as error}
87
+ <p class="text-sm text-red-500" role="alert">{error}</p>
88
+ {/each}
89
+ {/if}
90
+ </div>
91
+ {/snippet}
92
+ </form.Field>
93
+
94
+ <form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}>
95
+ {#snippet children(state)}
96
+ <button type="submit" class="w-full" disabled={!state.canSubmit || state.isSubmitting}>
97
+ {state.isSubmitting ? 'Submitting...' : 'Sign In'}
98
+ </button>
99
+ {/snippet}
100
+ </form.Subscribe>
101
+ </form>
102
+
103
+ <div class="mt-4 text-center">
104
+ <button type="button" class="text-indigo-600 hover:text-indigo-800" onclick={switchToSignUp}>
105
+ Need an account? Sign Up
106
+ </button>
107
+ </div>
108
+ </div>
@@ -0,0 +1,142 @@
1
+ <script lang="ts">
2
+ import { createForm } from '@tanstack/svelte-form';
3
+ import { z } from 'zod';
4
+ import { authClient } from '$lib/auth-client';
5
+ import { goto } from '$app/navigation';
6
+
7
+ let { switchToSignIn } = $props<{ switchToSignIn: () => void }>();
8
+
9
+ const validationSchema = z.object({
10
+ name: z.string().min(2, 'Name must be at least 2 characters'),
11
+ email: z.string().email('Invalid email address'),
12
+ password: z.string().min(6, 'Password must be at least 6 characters'),
13
+ });
14
+
15
+
16
+ const form = createForm(() => ({
17
+ defaultValues: { name: '', email: '', password: '' },
18
+ onSubmit: async ({ value }) => {
19
+ await authClient.signUp.email(
20
+ {
21
+ email: value.email,
22
+ password: value.password,
23
+ name: value.name,
24
+ },
25
+ {
26
+ onSuccess: () => {
27
+ goto('/dashboard');
28
+ },
29
+ onError: (error) => {
30
+ console.log(error.error.message || 'Sign up failed. Please try again.');
31
+ },
32
+ }
33
+ );
34
+
35
+ },
36
+ validators: {
37
+ onSubmit: validationSchema,
38
+ },
39
+ }));
40
+ </script>
41
+
42
+ <div class="mx-auto mt-10 w-full max-w-md p-6">
43
+ <h1 class="mb-6 text-center font-bold text-3xl">Create Account</h1>
44
+
45
+ <form
46
+ id="form"
47
+ class="space-y-4"
48
+ onsubmit={(e) => {
49
+ e.preventDefault();
50
+ e.stopPropagation();
51
+ form.handleSubmit();
52
+ }}
53
+ >
54
+ <form.Field name="name">
55
+ {#snippet children(field)}
56
+ <div class="space-y-1">
57
+ <label for={field.name}>Name</label>
58
+ <input
59
+ id={field.name}
60
+ name={field.name}
61
+ class="w-full border"
62
+ onblur={field.handleBlur}
63
+ value={field.state.value}
64
+ oninput={(e: Event) => {
65
+ const target = e.target as HTMLInputElement
66
+ field.handleChange(target.value)
67
+ }}
68
+ />
69
+ {#if field.state.meta.isTouched}
70
+ {#each field.state.meta.errors as error}
71
+ <p class="text-sm text-red-500" role="alert">{error}</p>
72
+ {/each}
73
+ {/if}
74
+ </div>
75
+ {/snippet}
76
+ </form.Field>
77
+
78
+ <form.Field name="email">
79
+ {#snippet children(field)}
80
+ <div class="space-y-1">
81
+ <label for={field.name}>Email</label>
82
+ <input
83
+ id={field.name}
84
+ name={field.name}
85
+ type="email"
86
+ class="w-full border"
87
+ onblur={field.handleBlur}
88
+ value={field.state.value}
89
+ oninput={(e: Event) => {
90
+ const target = e.target as HTMLInputElement
91
+ field.handleChange(target.value)
92
+ }}
93
+ />
94
+ {#if field.state.meta.isTouched}
95
+ {#each field.state.meta.errors as error}
96
+ <p class="text-sm text-red-500" role="alert">{error}</p>
97
+ {/each}
98
+ {/if}
99
+ </div>
100
+ {/snippet}
101
+ </form.Field>
102
+
103
+ <form.Field name="password">
104
+ {#snippet children(field)}
105
+ <div class="space-y-1">
106
+ <label for={field.name}>Password</label>
107
+ <input
108
+ id={field.name}
109
+ name={field.name}
110
+ type="password"
111
+ class="w-full border"
112
+ onblur={field.handleBlur}
113
+ value={field.state.value}
114
+ oninput={(e: Event) => {
115
+ const target = e.target as HTMLInputElement
116
+ field.handleChange(target.value)
117
+ }}
118
+ />
119
+ {#if field.state.meta.errors}
120
+ {#each field.state.meta.errors as error}
121
+ <p class="text-sm text-red-500" role="alert">{error}</p>
122
+ {/each}
123
+ {/if}
124
+ </div>
125
+ {/snippet}
126
+ </form.Field>
127
+
128
+ <form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}>
129
+ {#snippet children(state)}
130
+ <button type="submit" class="w-full" disabled={!state.canSubmit || state.isSubmitting}>
131
+ {state.isSubmitting ? 'Submitting...' : 'Sign Up'}
132
+ </button>
133
+ {/snippet}
134
+ </form.Subscribe>
135
+ </form>
136
+
137
+ <div class="mt-4 text-center">
138
+ <button type="button" class="text-indigo-600 hover:text-indigo-800" onclick={switchToSignIn}>
139
+ Already have an account? Sign In
140
+ </button>
141
+ </div>
142
+ </div>
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import { authClient } from '$lib/auth-client';
3
+ import { goto } from '$app/navigation';
4
+ import { queryClient } from '$lib/orpc';
5
+
6
+ const sessionQuery = authClient.useSession();
7
+
8
+ async function handleSignOut() {
9
+ await authClient.signOut({
10
+ fetchOptions: {
11
+ onSuccess: () => {
12
+ queryClient.invalidateQueries();
13
+ goto('/');
14
+ },
15
+ onError: (error) => {
16
+ console.error('Sign out failed:', error);
17
+ }
18
+ }
19
+ });
20
+ }
21
+
22
+ function goToLogin() {
23
+ goto('/login');
24
+ }
25
+
26
+ </script>
27
+
28
+ <div class="relative">
29
+ {#if $sessionQuery.isPending}
30
+ <div class="h-8 w-24 animate-pulse rounded bg-neutral-700"></div>
31
+ {:else if $sessionQuery.data?.user}
32
+ {@const user = $sessionQuery.data.user}
33
+ <div class="flex items-center gap-3">
34
+ <span class="text-sm text-neutral-300 hidden sm:inline" title={user.email}>
35
+ {user.name || user.email?.split('@')[0] || 'User'}
36
+ </span>
37
+ <button
38
+ onclick={handleSignOut}
39
+ class="rounded px-3 py-1 text-sm bg-red-600 hover:bg-red-700 text-white transition-colors"
40
+ >
41
+ Sign Out
42
+ </button>
43
+ </div>
44
+ {:else}
45
+ <div class="flex items-center gap-2">
46
+ <button
47
+ onclick={goToLogin}
48
+ class="rounded px-3 py-1 text-sm bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
49
+ >
50
+ Sign In
51
+ </button>
52
+ </div>
53
+ {/if}
54
+ </div>
@@ -0,0 +1,6 @@
1
+ import { PUBLIC_SERVER_URL } from "$env/static/public";
2
+ import { createAuthClient } from "better-auth/svelte";
3
+
4
+ export const authClient = createAuthClient({
5
+ baseURL: PUBLIC_SERVER_URL,
6
+ });
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { goto } from '$app/navigation';
4
+ import { authClient } from '$lib/auth-client';
5
+ import { orpc } from '$lib/orpc';
6
+ import { createQuery } from '@tanstack/svelte-query';
7
+ import { get } from 'svelte/store';
8
+
9
+ const sessionQuery = authClient.useSession();
10
+
11
+ const privateDataQuery = createQuery(orpc.privateData.queryOptions());
12
+
13
+ onMount(() => {
14
+ const { data: session, isPending } = get(sessionQuery);
15
+ if (!session && !isPending) {
16
+ goto('/login');
17
+ }
18
+ });
19
+ </script>
20
+
21
+ {#if $sessionQuery.isPending}
22
+ <div>Loading...</div>
23
+ {:else if !$sessionQuery.data}
24
+ <!-- Redirecting... -->
25
+ {:else}
26
+ <div>
27
+ <h1>Dashboard</h1>
28
+ <p>Welcome {$sessionQuery.data.user.name}</p>
29
+ <p>privateData: {$privateDataQuery.data?.message}</p>
30
+ </div>
31
+ {/if}
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ import SignInForm from '../../components/SignInForm.svelte';
3
+ import SignUpForm from '../../components/SignUpForm.svelte';
4
+
5
+ let showSignIn = $state(true);
6
+ </script>
7
+
8
+ {#if showSignIn}
9
+ <SignInForm switchToSignUp={() => showSignIn = false} />
10
+ {:else}
11
+ <SignUpForm switchToSignIn={() => showSignIn = true} />
12
+ {/if}
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch, nextTick } from 'vue'
3
2
  import { useChat } from '@ai-sdk/vue'
3
+ import { nextTick, ref, watch } from 'vue'
4
4
 
5
5
  const config = useRuntimeConfig()
6
6
  const serverUrl = config.public.serverURL
@@ -16,7 +16,6 @@ watch(messages, async () => {
16
16
  messagesEndRef.value?.scrollIntoView({ behavior: 'smooth' })
17
17
  })
18
18
 
19
- // Helper: Concatenate all text parts for a message
20
19
  function getMessageText(message: any) {
21
20
  return message.parts
22
21
  .filter((part: any) => part.type === 'text')
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import { PUBLIC_SERVER_URL } from '$env/static/public';
3
+ import { Chat } from '@ai-sdk/svelte';
4
+
5
+ const chat = new Chat({
6
+ api: `${PUBLIC_SERVER_URL}/ai`,
7
+ });
8
+
9
+ let messagesEndElement: HTMLDivElement | null = null;
10
+
11
+ $effect(() => {
12
+ const messageCount = chat.messages.length;
13
+ if (messageCount > 0) {
14
+ setTimeout(() => {
15
+ messagesEndElement?.scrollIntoView({ behavior: 'smooth' });
16
+ }, 0);
17
+ }
18
+ });
19
+
20
+ </script>
21
+
22
+ <div class="mx-auto grid h-full w-full max-w-2xl grid-rows-[1fr_auto] overflow-hidden p-4">
23
+ <div class="mb-4 space-y-4 overflow-y-auto pb-4">
24
+ {#if chat.messages.length === 0}
25
+ <div class="mt-8 text-center text-neutral-500">Ask the AI anything to get started!</div>
26
+ {/if}
27
+
28
+ {#each chat.messages as message (message.id)}
29
+ <div
30
+ class="w-fit max-w-[85%] rounded-lg p-3 text-sm md:text-base"
31
+ class:ml-auto={message.role === 'user'}
32
+ class:bg-indigo-600={message.role === 'user'}
33
+ class:text-white={message.role === 'user'}
34
+ class:bg-neutral-700={message.role === 'assistant'}
35
+ class:text-neutral-100={message.role === 'assistant'}
36
+ >
37
+ <p
38
+ class="mb-1 text-xs font-semibold uppercase tracking-wide"
39
+ class:text-indigo-200={message.role === 'user'}
40
+ class:text-neutral-400={message.role === 'assistant'}
41
+ >
42
+ {message.role === 'user' ? 'You' : 'AI Assistant'}
43
+ </p>
44
+ <div class="whitespace-pre-wrap break-words">
45
+ {#each message.parts as part, partIndex (partIndex)}
46
+ {#if part.type === 'text'}
47
+ {part.text}
48
+ {:else if part.type === 'tool-invocation'}
49
+ <pre class="mt-2 rounded bg-neutral-800 p-2 text-xs text-neutral-300"
50
+ >{JSON.stringify(part.toolInvocation, null, 2)}</pre
51
+ >
52
+ {/if}
53
+ {/each}
54
+ </div>
55
+ </div>
56
+ {/each}
57
+ <div bind:this={messagesEndElement}></div>
58
+ </div>
59
+
60
+ <form
61
+ onsubmit={chat.handleSubmit}
62
+ class="flex w-full items-center space-x-2 border-t border-neutral-700 pt-4"
63
+ >
64
+ <input
65
+ name="prompt"
66
+ bind:value={chat.input}
67
+ placeholder="Type your message..."
68
+ class="flex-1 rounded border border-neutral-600 bg-neutral-800 px-3 py-2 text-neutral-100 placeholder-neutral-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:opacity-50"
69
+ autocomplete="off"
70
+ onkeydown={(e) => {
71
+ if (e.key === 'Enter' && !e.shiftKey) {
72
+ e.preventDefault();
73
+ chat.handleSubmit(e);
74
+ }
75
+ }}
76
+ />
77
+ <button
78
+ type="submit"
79
+ disabled={!chat.input.trim()}
80
+ class="inline-flex h-10 w-10 items-center justify-center rounded bg-indigo-600 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-neutral-900 disabled:cursor-not-allowed disabled:opacity-50"
81
+ aria-label="Send message"
82
+ >
83
+ <svg
84
+ xmlns="http://www.w3.org/2000/svg"
85
+ width="18"
86
+ height="18"
87
+ viewBox="0 0 24 24"
88
+ fill="none"
89
+ stroke="currentColor"
90
+ stroke-width="2"
91
+ stroke-linecap="round"
92
+ stroke-linejoin="round"
93
+ >
94
+ <path d="m22 2-7 20-4-9-9-4Z" /><path d="M22 2 11 13" />
95
+ </svg>
96
+ </button>
97
+ </form>
98
+ </div>
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import { orpc } from '$lib/orpc';
3
+ import { createQuery, createMutation } from '@tanstack/svelte-query';
4
+
5
+ let newTodoText = $state('');
6
+
7
+ const todosQuery = createQuery(orpc.todo.getAll.queryOptions());
8
+
9
+ const addMutation = createMutation(
10
+ orpc.todo.create.mutationOptions({
11
+ onSuccess: () => {
12
+ $todosQuery.refetch();
13
+ newTodoText = '';
14
+ },
15
+ onError: (error) => {
16
+ console.error('Failed to create todo:', error?.message ?? error);
17
+ },
18
+ })
19
+ );
20
+
21
+ const toggleMutation = createMutation(
22
+ orpc.todo.toggle.mutationOptions({
23
+ onSuccess: () => {
24
+ $todosQuery.refetch();
25
+ },
26
+ onError: (error) => {
27
+ console.error('Failed to toggle todo:', error?.message ?? error);
28
+ },
29
+ })
30
+ );
31
+
32
+ const deleteMutation = createMutation(
33
+ orpc.todo.delete.mutationOptions({
34
+ onSuccess: () => {
35
+ $todosQuery.refetch();
36
+ },
37
+ onError: (error) => {
38
+ console.error('Failed to delete todo:', error?.message ?? error);
39
+ },
40
+ })
41
+ );
42
+
43
+ function handleAddTodo(event: SubmitEvent) {
44
+ event.preventDefault();
45
+ const text = newTodoText.trim();
46
+ if (text) {
47
+ $addMutation.mutate({ text });
48
+ }
49
+ }
50
+
51
+ function handleToggleTodo(id: number, completed: boolean) {
52
+ $toggleMutation.mutate({ id, completed: !completed });
53
+ }
54
+
55
+ function handleDeleteTodo(id: number) {
56
+ $deleteMutation.mutate({ id });
57
+ }
58
+
59
+ const isAdding = $derived($addMutation.isPending);
60
+ const canAdd = $derived(!isAdding && newTodoText.trim().length > 0);
61
+ const isLoadingTodos = $derived($todosQuery.isLoading);
62
+ const todos = $derived($todosQuery.data ?? []);
63
+ const hasTodos = $derived(todos.length > 0);
64
+
65
+ </script>
66
+
67
+ <div class="p-4">
68
+ <h1 class="text-xl mb-4">Todos</h1>
69
+
70
+ <form onsubmit={handleAddTodo} class="flex gap-2 mb-4">
71
+ <input
72
+ type="text"
73
+ bind:value={newTodoText}
74
+ placeholder="New task..."
75
+ disabled={isAdding}
76
+ class=" p-1 flex-grow"
77
+ />
78
+ <button
79
+ type="submit"
80
+ disabled={!canAdd}
81
+ class="bg-blue-500 text-white px-3 py-1 rounded disabled:opacity-50"
82
+ >
83
+ {#if isAdding}Adding...{:else}Add{/if}
84
+ </button>
85
+ </form>
86
+
87
+ {#if isLoadingTodos}
88
+ <p>Loading...</p>
89
+ {:else if !hasTodos}
90
+ <p>No todos yet.</p>
91
+ {:else}
92
+ <ul class="space-y-1">
93
+ {#each todos as todo (todo.id)}
94
+ {@const isToggling = $toggleMutation.isPending && $toggleMutation.variables?.id === todo.id}
95
+ {@const isDeleting = $deleteMutation.isPending && $deleteMutation.variables?.id === todo.id}
96
+ {@const isDisabled = isToggling || isDeleting}
97
+ <li
98
+ class="flex items-center justify-between p-2 "
99
+ class:opacity-50={isDisabled}
100
+ >
101
+ <div class="flex items-center gap-2">
102
+ <input
103
+ type="checkbox"
104
+ id={`todo-${todo.id}`}
105
+ checked={todo.completed}
106
+ onchange={() => handleToggleTodo(todo.id, todo.completed)}
107
+ disabled={isDisabled}
108
+ />
109
+ <label
110
+ for={`todo-${todo.id}`}
111
+ class:line-through={todo.completed}
112
+ >
113
+ {todo.text}
114
+ </label>
115
+ </div>
116
+ <button
117
+ type="button"
118
+ onclick={() => handleDeleteTodo(todo.id)}
119
+ disabled={isDisabled}
120
+ aria-label="Delete todo"
121
+ class="text-red-500 px-1 disabled:opacity-50"
122
+ >
123
+ {#if isDeleting}Deleting...{:else}X{/if}
124
+ </button>
125
+ </li>
126
+ {/each}
127
+ </ul>
128
+ {/if}
129
+
130
+ {#if $todosQuery.isError}
131
+ <p class="mt-4 text-red-500">
132
+ Error loading: {$todosQuery.error?.message ?? 'Unknown error'}
133
+ </p>
134
+ {/if}
135
+ {#if $addMutation.isError}
136
+ <p class="mt-4 text-red-500">
137
+ Error adding: {$addMutation.error?.message ?? 'Unknown error'}
138
+ </p>
139
+ {/if}
140
+ {#if $toggleMutation.isError}
141
+ <p class="mt-4 text-red-500">
142
+ Error updating: {$toggleMutation.error?.message ?? 'Unknown error'}
143
+ </p>
144
+ {/if}
145
+ {#if $deleteMutation.isError}
146
+ <p class="mt-4 text-red-500">
147
+ Error deleting: {$deleteMutation.error?.message ?? 'Unknown error'}
148
+ </p>
149
+ {/if}
150
+ </div>
@@ -1,4 +1,3 @@
1
- {{! Import VitePWA only if 'pwa' addon is selected }}
2
1
  {{#if (includes addons "pwa")}}
3
2
  import { VitePWA } from "vite-plugin-pwa";
4
3
  {{/if}}
@@ -13,7 +12,6 @@ export default defineConfig({
13
12
  tailwindcss(),
14
13
  TanStackRouterVite({}),
15
14
  react(),
16
- {{! Add VitePWA plugin config only if 'pwa' addon is selected }}
17
15
  {{#if (includes addons "pwa")}}
18
16
  VitePWA({
19
17
  registerType: "autoUpdate",