mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add solid
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { Link } from "@tanstack/solid-router";
|
||||
{{#if auth}}
|
||||
import UserMenu from "./user-menu";
|
||||
{{/if}}
|
||||
import { For } from "solid-js";
|
||||
|
||||
export default function Header() {
|
||||
const links = [
|
||||
{ to: "/", label: "Home" },
|
||||
{{#if auth}}
|
||||
{ to: "/dashboard", label: "Dashboard" },
|
||||
{{/if}}
|
||||
{{#if (includes examples "todo")}}
|
||||
{ to: "/todos", label: "Todos" },
|
||||
{{/if}}
|
||||
{{#if (includes examples "ai")}}
|
||||
{ to: "/ai", label: "AI Chat" },
|
||||
{{/if}}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="flex flex-row items-center justify-between px-2 py-1">
|
||||
<nav class="flex gap-4 text-lg">
|
||||
<For each={links}>
|
||||
{(link) => <Link to={link.to}>{link.label}</Link>}
|
||||
</For>
|
||||
</nav>
|
||||
<div class="flex items-center gap-2">
|
||||
{{#if auth}}
|
||||
<UserMenu />
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Loader2 } from "lucide-solid";
|
||||
|
||||
export default function Loader() {
|
||||
return (
|
||||
<div class="flex h-full items-center justify-center pt-8">
|
||||
<Loader2 class="animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
apps/cli/templates/frontend/solid/src/main.tsx.hbs
Normal file
32
apps/cli/templates/frontend/solid/src/main.tsx.hbs
Normal file
@@ -0,0 +1,32 @@
|
||||
import { RouterProvider, createRouter } from "@tanstack/solid-router";
|
||||
import { render } from "solid-js/web";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import "./styles.css";
|
||||
import { QueryClientProvider } from "@tanstack/solid-query";
|
||||
import { queryClient } from "./utils/orpc";
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
defaultPreload: "intent",
|
||||
scrollRestoration: true,
|
||||
defaultPreloadStaleTime: 0,
|
||||
});
|
||||
|
||||
declare module "@tanstack/solid-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("app");
|
||||
if (rootElement) {
|
||||
render(() => <App />, rootElement);
|
||||
}
|
||||
21
apps/cli/templates/frontend/solid/src/routes/__root.tsx.hbs
Normal file
21
apps/cli/templates/frontend/solid/src/routes/__root.tsx.hbs
Normal file
@@ -0,0 +1,21 @@
|
||||
import Header from "@/components/header";
|
||||
import { Outlet, createRootRouteWithContext } from "@tanstack/solid-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/solid-router-devtools";
|
||||
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
|
||||
|
||||
export const Route = createRootRouteWithContext()({
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<>
|
||||
<div class="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
<Outlet />
|
||||
</div>
|
||||
<SolidQueryDevtools />
|
||||
<TanStackRouterDevtools />
|
||||
</>
|
||||
);
|
||||
}
|
||||
65
apps/cli/templates/frontend/solid/src/routes/index.tsx.hbs
Normal file
65
apps/cli/templates/frontend/solid/src/routes/index.tsx.hbs
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createFileRoute } from "@tanstack/solid-router";
|
||||
import { useQuery } from "@tanstack/solid-query";
|
||||
import { orpc } from "../utils/orpc";
|
||||
import { Match, Switch } from "solid-js";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: App,
|
||||
});
|
||||
|
||||
const TITLE_TEXT = `
|
||||
██████╗ ███████╗████████╗████████╗███████╗██████╗
|
||||
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
|
||||
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
|
||||
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
|
||||
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
|
||||
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
|
||||
|
||||
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
|
||||
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
|
||||
██║ ███████╗ ██║ ███████║██║ █████╔╝
|
||||
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
|
||||
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
|
||||
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
||||
`;
|
||||
|
||||
function App() {
|
||||
const healthCheck = useQuery(() => orpc.healthCheck.queryOptions());
|
||||
|
||||
return (
|
||||
<div class="container mx-auto max-w-3xl px-4 py-2">
|
||||
<pre class="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
|
||||
<div class="grid gap-6">
|
||||
<section class="rounded-lg border p-4">
|
||||
<h2 class="mb-2 font-medium">API Status</h2>
|
||||
<Switch>
|
||||
<Match when={healthCheck.isPending}>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-gray-500 animate-pulse" />{" "}
|
||||
<span class="text-sm text-muted-foreground">Checking...</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={healthCheck.isError}>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-red-500" />
|
||||
<span class="text-sm text-muted-foreground">Disconnected</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={healthCheck.isSuccess}>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{healthCheck.data
|
||||
? "Connected"
|
||||
: "Disconnected (Success but no data)"}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
132
apps/cli/templates/frontend/solid/src/routes/todos.tsx
Normal file
132
apps/cli/templates/frontend/solid/src/routes/todos.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { createFileRoute } from "@tanstack/solid-router";
|
||||
import { Loader2, Trash2 } from "lucide-solid";
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { orpc } from "@/utils/orpc";
|
||||
import { useQuery, useMutation } from "@tanstack/solid-query";
|
||||
|
||||
export const Route = createFileRoute("/todos")({
|
||||
component: TodosRoute,
|
||||
});
|
||||
|
||||
function TodosRoute() {
|
||||
const [newTodoText, setNewTodoText] = createSignal("");
|
||||
|
||||
const todos = useQuery(() => orpc.todo.getAll.queryOptions());
|
||||
|
||||
const createMutation = useMutation(() =>
|
||||
orpc.todo.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
todos.refetch();
|
||||
setNewTodoText("");
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const toggleMutation = useMutation(() =>
|
||||
orpc.todo.toggle.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
|
||||
const deleteMutation = useMutation(() =>
|
||||
orpc.todo.delete.mutationOptions({
|
||||
onSuccess: () => todos.refetch(),
|
||||
}),
|
||||
);
|
||||
|
||||
const handleAddTodo = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (newTodoText().trim()) {
|
||||
createMutation.mutate({ text: newTodoText() });
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTodo = (id: number, completed: boolean) => {
|
||||
toggleMutation.mutate({ id, completed: !completed });
|
||||
};
|
||||
|
||||
const handleDeleteTodo = (id: number) => {
|
||||
deleteMutation.mutate({ id });
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="mx-auto w-full max-w-md py-10">
|
||||
<div class="rounded-lg border p-6 shadow-sm">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-semibold">Todo List</h2>
|
||||
<p class="text-sm">Manage your tasks efficiently</p>
|
||||
</div>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={handleAddTodo}
|
||||
class="mb-6 flex items-center space-x-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={newTodoText()}
|
||||
onInput={(e) => setNewTodoText(e.currentTarget.value)}
|
||||
placeholder="Add a new task..."
|
||||
disabled={createMutation.isPending}
|
||||
class="w-full rounded-md border p-2 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || !newTodoText().trim()}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
|
||||
>
|
||||
<Show when={createMutation.isPending} fallback="Add">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
</Show>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Show when={todos.isLoading}>
|
||||
<div class="flex justify-center py-4">
|
||||
<Loader2 class="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!todos.isLoading && todos.data?.length === 0}>
|
||||
<p class="py-4 text-center">No todos yet. Add one above!</p>
|
||||
</Show>
|
||||
|
||||
<Show when={!todos.isLoading}>
|
||||
<ul class="space-y-2">
|
||||
<For each={todos.data}>
|
||||
{(todo) => (
|
||||
<li class="flex items-center justify-between rounded-md border p-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todo.completed}
|
||||
onChange={() =>
|
||||
handleToggleTodo(todo.id, todo.completed)
|
||||
}
|
||||
id={`todo-${todo.id}`}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<label
|
||||
for={`todo-${todo.id}`}
|
||||
class={todo.completed ? "line-through" : ""}
|
||||
>
|
||||
{todo.text}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteTodo(todo.id)}
|
||||
aria-label="Delete todo"
|
||||
class="ml-2 rounded-md p-1"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
apps/cli/templates/frontend/solid/src/styles.css
Normal file
5
apps/cli/templates/frontend/solid/src/styles.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
@apply bg-neutral-950 text-neutral-100;
|
||||
}
|
||||
Reference in New Issue
Block a user