create-better-t-stack 2.7.1 → 2.8.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 +11 -11
- package/package.json +1 -1
- package/templates/api/orpc/web/solid/src/utils/orpc.ts.hbs +30 -0
- package/templates/auth/web/solid/src/components/sign-in-form.tsx +132 -0
- package/templates/auth/web/solid/src/components/sign-up-form.tsx +158 -0
- package/templates/auth/web/solid/src/components/user-menu.tsx +54 -0
- package/templates/auth/web/solid/src/lib/auth-client.ts +5 -0
- package/templates/auth/web/solid/src/routes/dashboard.tsx +38 -0
- package/templates/auth/web/solid/src/routes/login.tsx +23 -0
- package/templates/db/prisma/sqlite/prisma/schema/schema.prisma +1 -1
- package/templates/examples/todo/web/solid/src/routes/todos.tsx +132 -0
- package/templates/frontend/native/babel.config.js +1 -1
- package/templates/frontend/react/react-router/vite.config.ts.hbs +3 -7
- package/templates/frontend/react/tanstack-router/vite.config.ts.hbs +3 -5
- package/templates/frontend/solid/_gitignore +7 -0
- package/templates/frontend/solid/index.html +13 -0
- package/templates/frontend/solid/package.json +33 -0
- package/templates/frontend/solid/public/robots.txt +3 -0
- package/templates/frontend/solid/src/components/header.tsx.hbs +38 -0
- package/templates/frontend/solid/src/components/loader.tsx +9 -0
- package/templates/frontend/solid/src/main.tsx.hbs +32 -0
- package/templates/frontend/solid/src/routes/__root.tsx.hbs +21 -0
- package/templates/frontend/solid/src/routes/index.tsx.hbs +65 -0
- package/templates/frontend/solid/src/routes/todos.tsx +132 -0
- package/templates/frontend/solid/src/styles.css +5 -0
- package/templates/frontend/solid/tsconfig.json +29 -0
- package/templates/frontend/solid/vite.config.js.hbs +39 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
import { createForm } from "@tanstack/solid-form";
|
|
3
|
+
import { useNavigate } from "@tanstack/solid-router";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { For } from "solid-js";
|
|
6
|
+
|
|
7
|
+
export default function SignUpForm({
|
|
8
|
+
onSwitchToSignIn,
|
|
9
|
+
}: {
|
|
10
|
+
onSwitchToSignIn: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
const navigate = useNavigate({
|
|
13
|
+
from: "/",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const form = createForm(() => ({
|
|
17
|
+
defaultValues: {
|
|
18
|
+
email: "",
|
|
19
|
+
password: "",
|
|
20
|
+
name: "",
|
|
21
|
+
},
|
|
22
|
+
onSubmit: async ({ value }) => {
|
|
23
|
+
await authClient.signUp.email(
|
|
24
|
+
{
|
|
25
|
+
email: value.email,
|
|
26
|
+
password: value.password,
|
|
27
|
+
name: value.name,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
onSuccess: () => {
|
|
31
|
+
navigate({
|
|
32
|
+
to: "/dashboard",
|
|
33
|
+
});
|
|
34
|
+
console.log("Sign up successful");
|
|
35
|
+
},
|
|
36
|
+
onError: (error) => {
|
|
37
|
+
console.error(error.error.message);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
validators: {
|
|
43
|
+
onSubmit: z.object({
|
|
44
|
+
name: z.string().min(2, "Name must be at least 2 characters"),
|
|
45
|
+
email: z.string().email("Invalid email address"),
|
|
46
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div class="mx-auto w-full mt-10 max-w-md p-6">
|
|
53
|
+
<h1 class="mb-6 text-center text-3xl font-bold">Create Account</h1>
|
|
54
|
+
|
|
55
|
+
<form
|
|
56
|
+
onSubmit={(e) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
void form.handleSubmit();
|
|
60
|
+
}}
|
|
61
|
+
class="space-y-4"
|
|
62
|
+
>
|
|
63
|
+
<div>
|
|
64
|
+
<form.Field name="name">
|
|
65
|
+
{(field) => (
|
|
66
|
+
<div class="space-y-2">
|
|
67
|
+
<label for={field().name}>Name</label>
|
|
68
|
+
<input
|
|
69
|
+
id={field().name}
|
|
70
|
+
name={field().name}
|
|
71
|
+
value={field().state.value}
|
|
72
|
+
onBlur={field().handleBlur}
|
|
73
|
+
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
|
74
|
+
class="w-full rounded border p-2"
|
|
75
|
+
/>
|
|
76
|
+
<For each={field().state.meta.errors}>
|
|
77
|
+
{(error) => (
|
|
78
|
+
<p class="text-sm text-red-600">{error?.message}</p>
|
|
79
|
+
)}
|
|
80
|
+
</For>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</form.Field>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div>
|
|
87
|
+
<form.Field name="email">
|
|
88
|
+
{(field) => (
|
|
89
|
+
<div class="space-y-2">
|
|
90
|
+
<label for={field().name}>Email</label>
|
|
91
|
+
<input
|
|
92
|
+
id={field().name}
|
|
93
|
+
name={field().name}
|
|
94
|
+
type="email"
|
|
95
|
+
value={field().state.value}
|
|
96
|
+
onBlur={field().handleBlur}
|
|
97
|
+
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
|
98
|
+
class="w-full rounded border p-2"
|
|
99
|
+
/>
|
|
100
|
+
<For each={field().state.meta.errors}>
|
|
101
|
+
{(error) => (
|
|
102
|
+
<p class="text-sm text-red-600">{error?.message}</p>
|
|
103
|
+
)}
|
|
104
|
+
</For>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</form.Field>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div>
|
|
111
|
+
<form.Field name="password">
|
|
112
|
+
{(field) => (
|
|
113
|
+
<div class="space-y-2">
|
|
114
|
+
<label for={field().name}>Password</label>
|
|
115
|
+
<input
|
|
116
|
+
id={field().name}
|
|
117
|
+
name={field().name}
|
|
118
|
+
type="password"
|
|
119
|
+
value={field().state.value}
|
|
120
|
+
onBlur={field().handleBlur}
|
|
121
|
+
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
|
122
|
+
class="w-full rounded border p-2"
|
|
123
|
+
/>
|
|
124
|
+
<For each={field().state.meta.errors}>
|
|
125
|
+
{(error) => (
|
|
126
|
+
<p class="text-sm text-red-600">{error?.message}</p>
|
|
127
|
+
)}
|
|
128
|
+
</For>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</form.Field>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<form.Subscribe>
|
|
135
|
+
{(state) => (
|
|
136
|
+
<button
|
|
137
|
+
type="submit"
|
|
138
|
+
class="w-full rounded bg-indigo-600 p-2 text-white hover:bg-indigo-700 disabled:opacity-50"
|
|
139
|
+
disabled={!state().canSubmit || state().isSubmitting}
|
|
140
|
+
>
|
|
141
|
+
{state().isSubmitting ? "Submitting..." : "Sign Up"}
|
|
142
|
+
</button>
|
|
143
|
+
)}
|
|
144
|
+
</form.Subscribe>
|
|
145
|
+
</form>
|
|
146
|
+
|
|
147
|
+
<div class="mt-4 text-center">
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={onSwitchToSignIn}
|
|
151
|
+
class="text-sm text-indigo-600 hover:text-indigo-800 hover:underline"
|
|
152
|
+
>
|
|
153
|
+
Already have an account? Sign In
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
import { useNavigate, Link } from "@tanstack/solid-router";
|
|
3
|
+
import { createSignal, Show } from "solid-js";
|
|
4
|
+
|
|
5
|
+
export default function UserMenu() {
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
const session = authClient.useSession();
|
|
8
|
+
const [isMenuOpen, setIsMenuOpen] = createSignal(false);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div class="relative inline-block text-left">
|
|
12
|
+
<Show when={session().isPending}>
|
|
13
|
+
<div class="h-9 w-24 animate-pulse rounded" />
|
|
14
|
+
</Show>
|
|
15
|
+
|
|
16
|
+
<Show when={!session().isPending && !session().data}>
|
|
17
|
+
<Link to="/login" class="inline-block border rounded px-4 text-sm">
|
|
18
|
+
Sign In
|
|
19
|
+
</Link>
|
|
20
|
+
</Show>
|
|
21
|
+
|
|
22
|
+
<Show when={!session().isPending && session().data}>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="inline-block border rounded px-4 text-sm"
|
|
26
|
+
onClick={() => setIsMenuOpen(!isMenuOpen())}
|
|
27
|
+
>
|
|
28
|
+
{session().data?.user.name}
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<Show when={isMenuOpen()}>
|
|
32
|
+
<div class="absolute right-0 mt-2 w-56 rounded p-1 shadow-sm">
|
|
33
|
+
<div class="px-4 text-sm">{session().data?.user.email}</div>
|
|
34
|
+
<button
|
|
35
|
+
class="mt-1 w-full border rounded px-4 text-center text-sm"
|
|
36
|
+
onClick={() => {
|
|
37
|
+
setIsMenuOpen(false);
|
|
38
|
+
authClient.signOut({
|
|
39
|
+
fetchOptions: {
|
|
40
|
+
onSuccess: () => {
|
|
41
|
+
navigate({ to: "/" });
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
Sign Out
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</Show>
|
|
51
|
+
</Show>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
import { orpc } from "@/utils/orpc";
|
|
3
|
+
import { useQuery } from "@tanstack/solid-query";
|
|
4
|
+
import { createFileRoute } from "@tanstack/solid-router";
|
|
5
|
+
import { createEffect, Show } from "solid-js";
|
|
6
|
+
|
|
7
|
+
export const Route = createFileRoute("/dashboard")({
|
|
8
|
+
component: RouteComponent,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function RouteComponent() {
|
|
12
|
+
const session = authClient.useSession();
|
|
13
|
+
const navigate = Route.useNavigate();
|
|
14
|
+
|
|
15
|
+
const privateData = useQuery(() => orpc.privateData.queryOptions());
|
|
16
|
+
|
|
17
|
+
createEffect(() => {
|
|
18
|
+
if (!session().data && !session().isPending) {
|
|
19
|
+
navigate({
|
|
20
|
+
to: "/login",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<Show when={session().isPending}>
|
|
28
|
+
<div>Loading...</div>
|
|
29
|
+
</Show>
|
|
30
|
+
|
|
31
|
+
<Show when={!session().isPending && session().data}>
|
|
32
|
+
<h1>Dashboard</h1>
|
|
33
|
+
<p>Welcome {session().data?.user.name}</p>
|
|
34
|
+
<p>privateData: {privateData.data?.message}</p>
|
|
35
|
+
</Show>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import SignInForm from "@/components/sign-in-form";
|
|
2
|
+
import SignUpForm from "@/components/sign-up-form";
|
|
3
|
+
import { createFileRoute } from "@tanstack/solid-router";
|
|
4
|
+
import { createSignal, Match, Switch } from "solid-js";
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute("/login")({
|
|
7
|
+
component: RouteComponent,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function RouteComponent() {
|
|
11
|
+
const [showSignIn, setShowSignIn] = createSignal(false);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Switch>
|
|
15
|
+
<Match when={showSignIn()}>
|
|
16
|
+
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
|
|
17
|
+
</Match>
|
|
18
|
+
<Match when={!showSignIn()}>
|
|
19
|
+
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
|
|
20
|
+
</Match>
|
|
21
|
+
</Switch>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/solid-router";
|
|
2
|
+
import { Loader2, Trash2 } from "lucide-solid";
|
|
3
|
+
import { createSignal, For, Show } from "solid-js";
|
|
4
|
+
import { orpc } from "@/utils/orpc";
|
|
5
|
+
import { useQuery, useMutation } from "@tanstack/solid-query";
|
|
6
|
+
|
|
7
|
+
export const Route = createFileRoute("/todos")({
|
|
8
|
+
component: TodosRoute,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function TodosRoute() {
|
|
12
|
+
const [newTodoText, setNewTodoText] = createSignal("");
|
|
13
|
+
|
|
14
|
+
const todos = useQuery(() => orpc.todo.getAll.queryOptions());
|
|
15
|
+
|
|
16
|
+
const createMutation = useMutation(() =>
|
|
17
|
+
orpc.todo.create.mutationOptions({
|
|
18
|
+
onSuccess: () => {
|
|
19
|
+
todos.refetch();
|
|
20
|
+
setNewTodoText("");
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const toggleMutation = useMutation(() =>
|
|
26
|
+
orpc.todo.toggle.mutationOptions({
|
|
27
|
+
onSuccess: () => todos.refetch(),
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const deleteMutation = useMutation(() =>
|
|
32
|
+
orpc.todo.delete.mutationOptions({
|
|
33
|
+
onSuccess: () => todos.refetch(),
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const handleAddTodo = (e: Event) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
if (newTodoText().trim()) {
|
|
40
|
+
createMutation.mutate({ text: newTodoText() });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleToggleTodo = (id: number, completed: boolean) => {
|
|
45
|
+
toggleMutation.mutate({ id, completed: !completed });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleDeleteTodo = (id: number) => {
|
|
49
|
+
deleteMutation.mutate({ id });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div class="mx-auto w-full max-w-md py-10">
|
|
54
|
+
<div class="rounded-lg border p-6 shadow-sm">
|
|
55
|
+
<div class="mb-4">
|
|
56
|
+
<h2 class="text-xl font-semibold">Todo List</h2>
|
|
57
|
+
<p class="text-sm">Manage your tasks efficiently</p>
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<form
|
|
61
|
+
onSubmit={handleAddTodo}
|
|
62
|
+
class="mb-6 flex items-center space-x-2"
|
|
63
|
+
>
|
|
64
|
+
<input
|
|
65
|
+
type="text"
|
|
66
|
+
value={newTodoText()}
|
|
67
|
+
onInput={(e) => setNewTodoText(e.currentTarget.value)}
|
|
68
|
+
placeholder="Add a new task..."
|
|
69
|
+
disabled={createMutation.isPending}
|
|
70
|
+
class="w-full rounded-md border p-2 text-sm"
|
|
71
|
+
/>
|
|
72
|
+
<button
|
|
73
|
+
type="submit"
|
|
74
|
+
disabled={createMutation.isPending || !newTodoText().trim()}
|
|
75
|
+
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
|
|
76
|
+
>
|
|
77
|
+
<Show when={createMutation.isPending} fallback="Add">
|
|
78
|
+
<Loader2 class="h-4 w-4 animate-spin" />
|
|
79
|
+
</Show>
|
|
80
|
+
</button>
|
|
81
|
+
</form>
|
|
82
|
+
|
|
83
|
+
<Show when={todos.isLoading}>
|
|
84
|
+
<div class="flex justify-center py-4">
|
|
85
|
+
<Loader2 class="h-6 w-6 animate-spin" />
|
|
86
|
+
</div>
|
|
87
|
+
</Show>
|
|
88
|
+
|
|
89
|
+
<Show when={!todos.isLoading && todos.data?.length === 0}>
|
|
90
|
+
<p class="py-4 text-center">No todos yet. Add one above!</p>
|
|
91
|
+
</Show>
|
|
92
|
+
|
|
93
|
+
<Show when={!todos.isLoading}>
|
|
94
|
+
<ul class="space-y-2">
|
|
95
|
+
<For each={todos.data}>
|
|
96
|
+
{(todo) => (
|
|
97
|
+
<li class="flex items-center justify-between rounded-md border p-2">
|
|
98
|
+
<div class="flex items-center space-x-2">
|
|
99
|
+
<input
|
|
100
|
+
type="checkbox"
|
|
101
|
+
checked={todo.completed}
|
|
102
|
+
onChange={() =>
|
|
103
|
+
handleToggleTodo(todo.id, todo.completed)
|
|
104
|
+
}
|
|
105
|
+
id={`todo-${todo.id}`}
|
|
106
|
+
class="h-4 w-4"
|
|
107
|
+
/>
|
|
108
|
+
<label
|
|
109
|
+
for={`todo-${todo.id}`}
|
|
110
|
+
class={todo.completed ? "line-through" : ""}
|
|
111
|
+
>
|
|
112
|
+
{todo.text}
|
|
113
|
+
</label>
|
|
114
|
+
</div>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => handleDeleteTodo(todo.id)}
|
|
118
|
+
aria-label="Delete todo"
|
|
119
|
+
class="ml-2 rounded-md p-1"
|
|
120
|
+
>
|
|
121
|
+
<Trash2 class="h-4 w-4" />
|
|
122
|
+
</button>
|
|
123
|
+
</li>
|
|
124
|
+
)}
|
|
125
|
+
</For>
|
|
126
|
+
</ul>
|
|
127
|
+
</Show>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -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}}
|
|
@@ -12,24 +11,21 @@ export default defineConfig({
|
|
|
12
11
|
tailwindcss(),
|
|
13
12
|
reactRouter(),
|
|
14
13
|
tsconfigPaths(),
|
|
15
|
-
{{! Add VitePWA plugin config only if 'pwa' addon is selected }}
|
|
16
14
|
{{#if (includes addons "pwa")}}
|
|
17
15
|
VitePWA({
|
|
18
16
|
registerType: "autoUpdate",
|
|
19
17
|
manifest: {
|
|
20
|
-
// Use context variables for better naming
|
|
21
18
|
name: "{{projectName}}",
|
|
22
19
|
short_name: "{{projectName}}",
|
|
23
20
|
description: "{{projectName}} - PWA Application",
|
|
24
21
|
theme_color: "#0c0c0c",
|
|
25
|
-
// Add more manifest options as needed
|
|
26
22
|
},
|
|
27
23
|
pwaAssets: {
|
|
28
|
-
disabled: false,
|
|
29
|
-
config: true,
|
|
24
|
+
disabled: false,
|
|
25
|
+
config: true,
|
|
30
26
|
},
|
|
31
27
|
devOptions: {
|
|
32
|
-
enabled: true,
|
|
28
|
+
enabled: true,
|
|
33
29
|
},
|
|
34
30
|
}),
|
|
35
31
|
{{/if}}
|
|
@@ -16,19 +16,17 @@ export default defineConfig({
|
|
|
16
16
|
VitePWA({
|
|
17
17
|
registerType: "autoUpdate",
|
|
18
18
|
manifest: {
|
|
19
|
-
// Use context variables for better naming
|
|
20
19
|
name: "{{projectName}}",
|
|
21
20
|
short_name: "{{projectName}}",
|
|
22
21
|
description: "{{projectName}} - PWA Application",
|
|
23
22
|
theme_color: "#0c0c0c",
|
|
24
|
-
// Add more manifest options as needed
|
|
25
23
|
},
|
|
26
24
|
pwaAssets: {
|
|
27
|
-
disabled: false,
|
|
28
|
-
config: true,
|
|
25
|
+
disabled: false,
|
|
26
|
+
config: true,
|
|
29
27
|
},
|
|
30
28
|
devOptions: {
|
|
31
|
-
enabled: true,
|
|
29
|
+
enabled: true,
|
|
32
30
|
},
|
|
33
31
|
}),
|
|
34
32
|
{{/if}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="/favicon.ico" />
|
|
7
|
+
<meta name="theme-color" content="#000000" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite --port 3001",
|
|
7
|
+
"build": "vite build && tsc",
|
|
8
|
+
"serve": "vite preview",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@orpc/client": "^1.1.1",
|
|
13
|
+
"@orpc/server": "^1.1.1",
|
|
14
|
+
"@orpc/solid-query": "^1.1.1",
|
|
15
|
+
"@tailwindcss/vite": "^4.0.6",
|
|
16
|
+
"@tanstack/router-plugin": "^1.109.2",
|
|
17
|
+
"@tanstack/solid-form": "^1.9.0",
|
|
18
|
+
"@tanstack/solid-query": "^5.75.0",
|
|
19
|
+
"@tanstack/solid-query-devtools": "^5.75.0",
|
|
20
|
+
"@tanstack/solid-router": "^1.110.0",
|
|
21
|
+
"@tanstack/solid-router-devtools": "^1.109.2",
|
|
22
|
+
"better-auth": "^1.2.7",
|
|
23
|
+
"lucide-solid": "^0.507.0",
|
|
24
|
+
"solid-js": "^1.9.4",
|
|
25
|
+
"tailwindcss": "^4.0.6",
|
|
26
|
+
"zod": "^3.24.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.7.2",
|
|
30
|
+
"vite": "^6.0.11",
|
|
31
|
+
"vite-plugin-solid": "^2.11.2"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Link } from "@tanstack/solid-router";
|
|
2
|
+
{{#if auth}}
|
|
3
|
+
import UserMenu from "./user-menu";
|
|
4
|
+
{{/if}}
|
|
5
|
+
import { For } from "solid-js";
|
|
6
|
+
|
|
7
|
+
export default function Header() {
|
|
8
|
+
const links = [
|
|
9
|
+
{ to: "/", label: "Home" },
|
|
10
|
+
{{#if auth}}
|
|
11
|
+
{ to: "/dashboard", label: "Dashboard" },
|
|
12
|
+
{{/if}}
|
|
13
|
+
{{#if (includes examples "todo")}}
|
|
14
|
+
{ to: "/todos", label: "Todos" },
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{#if (includes examples "ai")}}
|
|
17
|
+
{ to: "/ai", label: "AI Chat" },
|
|
18
|
+
{{/if}}
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
<div class="flex flex-row items-center justify-between px-2 py-1">
|
|
24
|
+
<nav class="flex gap-4 text-lg">
|
|
25
|
+
<For each={links}>
|
|
26
|
+
{(link) => <Link to={link.to}>{link.label}</Link>}
|
|
27
|
+
</For>
|
|
28
|
+
</nav>
|
|
29
|
+
<div class="flex items-center gap-2">
|
|
30
|
+
{{#if auth}}
|
|
31
|
+
<UserMenu />
|
|
32
|
+
{{/if}}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<hr />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { RouterProvider, createRouter } from "@tanstack/solid-router";
|
|
2
|
+
import { render } from "solid-js/web";
|
|
3
|
+
import { routeTree } from "./routeTree.gen";
|
|
4
|
+
import "./styles.css";
|
|
5
|
+
import { QueryClientProvider } from "@tanstack/solid-query";
|
|
6
|
+
import { queryClient } from "./utils/orpc";
|
|
7
|
+
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
routeTree,
|
|
10
|
+
defaultPreload: "intent",
|
|
11
|
+
scrollRestoration: true,
|
|
12
|
+
defaultPreloadStaleTime: 0,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
declare module "@tanstack/solid-router" {
|
|
16
|
+
interface Register {
|
|
17
|
+
router: typeof router;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function App() {
|
|
22
|
+
return (
|
|
23
|
+
<QueryClientProvider client={queryClient}>
|
|
24
|
+
<RouterProvider router={router} />
|
|
25
|
+
</QueryClientProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rootElement = document.getElementById("app");
|
|
30
|
+
if (rootElement) {
|
|
31
|
+
render(() => <App />, rootElement);
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Header from "@/components/header";
|
|
2
|
+
import { Outlet, createRootRouteWithContext } from "@tanstack/solid-router";
|
|
3
|
+
import { TanStackRouterDevtools } from "@tanstack/solid-router-devtools";
|
|
4
|
+
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
|
|
5
|
+
|
|
6
|
+
export const Route = createRootRouteWithContext()({
|
|
7
|
+
component: RootComponent,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function RootComponent() {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<div class="grid grid-rows-[auto_1fr] h-svh">
|
|
14
|
+
<Header />
|
|
15
|
+
<Outlet />
|
|
16
|
+
</div>
|
|
17
|
+
<SolidQueryDevtools />
|
|
18
|
+
<TanStackRouterDevtools />
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|