better-ts-stack 1.0.8 → 1.1.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.
Files changed (57) hide show
  1. package/dist/builder/configGenerator.d.ts.map +1 -1
  2. package/dist/builder/configGenerator.js +4 -1
  3. package/dist/builder/configGenerator.js.map +1 -1
  4. package/dist/builder/moduleSelector.d.ts.map +1 -1
  5. package/dist/builder/moduleSelector.js +1 -1
  6. package/dist/builder/moduleSelector.js.map +1 -1
  7. package/dist/builder/templateContext.d.ts +1 -1
  8. package/dist/builder/templateContext.d.ts.map +1 -1
  9. package/dist/builder/templateContext.js +1 -1
  10. package/dist/builder/templateContext.js.map +1 -1
  11. package/dist/prompts/stack.d.ts.map +1 -1
  12. package/dist/prompts/stack.js +16 -11
  13. package/dist/prompts/stack.js.map +1 -1
  14. package/dist/types/index.d.ts +8 -3
  15. package/dist/types/index.d.ts.map +1 -1
  16. package/dist/types/index.js +6 -1
  17. package/dist/types/index.js.map +1 -1
  18. package/package.json +1 -1
  19. package/templates/frontend/tanstack/components.json +18 -0
  20. package/templates/frontend/tanstack/config.json +49 -0
  21. package/templates/frontend/tanstack/eslint.config.mjs +23 -0
  22. package/templates/frontend/tanstack/gitignore +38 -0
  23. package/templates/frontend/tanstack/public/favicon.svg +7 -0
  24. package/templates/frontend/tanstack/src/components/ui/button.tsx +51 -0
  25. package/templates/frontend/tanstack/src/components/ui/card.tsx +60 -0
  26. package/templates/frontend/tanstack/src/components/ui/field.tsx +67 -0
  27. package/templates/frontend/tanstack/src/components/ui/input.tsx +18 -0
  28. package/templates/frontend/tanstack/src/lib/utils.ts +6 -0
  29. package/templates/frontend/tanstack/src/routeTree.gen.ts +68 -0
  30. package/templates/frontend/tanstack/src/router.tsx +32 -0
  31. package/templates/frontend/tanstack/src/routes/__root.tsx +70 -0
  32. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +102 -0
  33. package/templates/frontend/tanstack/src/styles/app.css +85 -0
  34. package/templates/frontend/tanstack/tsconfig.json +22 -0
  35. package/templates/frontend/tanstack/vite.config.ts +14 -0
  36. package/templates/modules/auth/tanstack/config.json +33 -0
  37. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +135 -0
  38. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +49 -0
  39. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +153 -0
  40. package/templates/modules/auth/tanstack/src/lib/auth-client.ts +3 -0
  41. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +12 -0
  42. package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +39 -0
  43. package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +38 -0
  44. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +140 -0
  45. package/templates/modules/auth/tanstack/src/routes/api/auth/$.ts +16 -0
  46. package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +111 -0
  47. package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +37 -0
  48. package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +37 -0
  49. package/templates/modules/docker/Dockerfile.hbs +125 -80
  50. package/templates/modules/drizzle/tanstack/config.json +26 -0
  51. package/templates/modules/drizzle/tanstack/drizzle.config.ts +13 -0
  52. package/templates/modules/drizzle/tanstack/src/lib/db.ts +13 -0
  53. package/templates/modules/drizzle/tanstack/src/lib/schema.ts.hbs +84 -0
  54. package/templates/modules/prisma/tanstack/config.json +28 -0
  55. package/templates/modules/prisma/tanstack/prisma/schema.prisma.hbs +91 -0
  56. package/templates/modules/prisma/tanstack/prisma.config.ts +12 -0
  57. package/templates/modules/prisma/tanstack/src/lib/prisma.ts +18 -0
@@ -0,0 +1,68 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as IndexRouteImport } from './routes/index'
13
+
14
+ const IndexRoute = IndexRouteImport.update({
15
+ id: '/',
16
+ path: '/',
17
+ getParentRoute: () => rootRouteImport,
18
+ } as any)
19
+
20
+ export interface FileRoutesByFullPath {
21
+ '/': typeof IndexRoute
22
+ }
23
+ export interface FileRoutesByTo {
24
+ '/': typeof IndexRoute
25
+ }
26
+ export interface FileRoutesById {
27
+ __root__: typeof rootRouteImport
28
+ '/': typeof IndexRoute
29
+ }
30
+ export interface FileRouteTypes {
31
+ fileRoutesByFullPath: FileRoutesByFullPath
32
+ fullPaths: '/'
33
+ fileRoutesByTo: FileRoutesByTo
34
+ to: '/'
35
+ id: '__root__' | '/'
36
+ fileRoutesById: FileRoutesById
37
+ }
38
+ export interface RootRouteChildren {
39
+ IndexRoute: typeof IndexRoute
40
+ }
41
+
42
+ declare module '@tanstack/react-router' {
43
+ interface FileRoutesByPath {
44
+ '/': {
45
+ id: '/'
46
+ path: '/'
47
+ fullPath: '/'
48
+ preLoaderRoute: typeof IndexRouteImport
49
+ parentRoute: typeof rootRouteImport
50
+ }
51
+ }
52
+ }
53
+
54
+ const rootRouteChildren: RootRouteChildren = {
55
+ IndexRoute: IndexRoute,
56
+ }
57
+ export const routeTree = rootRouteImport
58
+ ._addFileChildren(rootRouteChildren)
59
+ ._addFileTypes<FileRouteTypes>()
60
+
61
+ import type { getRouter } from './router.tsx'
62
+ import type { createStart } from '@tanstack/react-start'
63
+ declare module '@tanstack/react-start' {
64
+ interface Register {
65
+ ssr: true
66
+ router: Awaited<ReturnType<typeof getRouter>>
67
+ }
68
+ }
@@ -0,0 +1,32 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ import { createRouter } from "@tanstack/react-router";
3
+ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
4
+
5
+ import { routeTree } from "~/routeTree.gen";
6
+
7
+ export function getRouter() {
8
+ const queryClient = new QueryClient({
9
+ defaultOptions: {
10
+ queries: {
11
+ staleTime: 30_000,
12
+ },
13
+ },
14
+ });
15
+
16
+ const router = createRouter({
17
+ routeTree,
18
+ context: {
19
+ queryClient,
20
+ },
21
+ defaultPreload: "intent",
22
+ defaultPreloadStaleTime: 0,
23
+ scrollRestoration: true,
24
+ });
25
+
26
+ setupRouterSsrQueryIntegration({
27
+ router,
28
+ queryClient,
29
+ });
30
+
31
+ return router;
32
+ }
@@ -0,0 +1,70 @@
1
+ /// <reference types="vite/client" />
2
+ import {
3
+ HeadContent,
4
+ Outlet,
5
+ Scripts,
6
+ createRootRouteWithContext,
7
+ } from "@tanstack/react-router";
8
+ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
9
+ import type { QueryClient } from "@tanstack/react-query";
10
+ import * as React from "react";
11
+
12
+ import appCss from "~/styles/app.css?url";
13
+
14
+ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
15
+ head: () => ({
16
+ meta: [
17
+ {
18
+ charSet: "utf-8",
19
+ },
20
+ {
21
+ name: "viewport",
22
+ content: "width=device-width, initial-scale=1",
23
+ },
24
+ ],
25
+ links: [
26
+ {
27
+ rel: "icon",
28
+ type: "image/svg+xml",
29
+ href: "/favicon.svg",
30
+ },
31
+ ],
32
+ }),
33
+ component: RootComponent,
34
+ notFoundComponent: () => <NotFound />,
35
+ })
36
+
37
+ function RootComponent() {
38
+ return (
39
+ <RootDocument>
40
+ <Outlet />
41
+ </RootDocument>
42
+ )
43
+ }
44
+
45
+ function RootDocument({ children }: { children: React.ReactNode }) {
46
+ return (
47
+ <html lang="en">
48
+ <head>
49
+ <HeadContent />
50
+ <link rel="stylesheet" href={appCss} />
51
+ </head>
52
+ <body>
53
+ {children}
54
+ <TanStackRouterDevtools />
55
+ <Scripts />
56
+ </body>
57
+ </html>
58
+ )
59
+ }
60
+
61
+ function NotFound() {
62
+ return (
63
+ <div className="flex min-h-[50vh] items-center justify-center">
64
+ <div className="text-center">
65
+ <h1 className="text-4xl font-bold">404</h1>
66
+ <p className="mt-2 text-muted-foreground">Page not found</p>
67
+ </div>
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,102 @@
1
+ import { createFileRoute{{#if useAuth}}, Link{{/if}} } from "@tanstack/react-router";
2
+ import { ArrowRight } from "lucide-react";
3
+
4
+ import { buttonVariants } from "~/components/ui/button";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
6
+ import { cn } from "~/lib/utils";
7
+
8
+ export const Route = createFileRoute("/")({
9
+ component: Home,
10
+ });
11
+
12
+ const featureList = [
13
+ "TanStack Start SSR",
14
+ "TanStack Router and Query",
15
+ "React 19 and TypeScript",
16
+ "Tailwind CSS v4",
17
+ {{#if useAuth}}"Better Auth-ready session flow",{{/if}}
18
+ {{#if (eq database "prisma")}}"Prisma database tooling",{{/if}}
19
+ {{#if (eq database "drizzle")}}"Drizzle database tooling",{{/if}}
20
+ ];
21
+
22
+ function Home() {
23
+ return (
24
+ <main className="min-h-screen bg-background text-foreground">
25
+ <div className="mx-auto flex min-h-screen max-w-6xl flex-col justify-center gap-12 px-6 py-16 sm:px-10 lg:px-12">
26
+ <section className="grid gap-10 lg:grid-cols-[1.2fr_0.8fr] lg:items-center">
27
+ <div className="space-y-6">
28
+ <span className="inline-flex w-fit items-center rounded-full border border-border bg-card px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-muted-foreground shadow-sm">
29
+ Created with better-ts-stack
30
+ </span>
31
+ <div className="space-y-4">
32
+ <h1 className="max-w-3xl text-4xl font-semibold tracking-tight sm:text-5xl">
33
+ Ship your stack faster with a polished TypeScript foundation.
34
+ </h1>
35
+ <p className="max-w-2xl text-base leading-7 text-muted-foreground sm:text-lg">
36
+ Your project already includes the core app shell, database wiring,
37
+ and a shadcn-compatible UI foundation so you can keep building
38
+ instead of setting up boilerplate.
39
+ </p>
40
+ </div>
41
+ <div className="flex flex-col gap-3 sm:flex-row">
42
+ {{#if useAuth}}
43
+ <Link
44
+ to="/sign-up"
45
+ className={cn(buttonVariants({ size: "lg" }), "gap-2")}
46
+ >
47
+ Create an account
48
+ <ArrowRight className="size-4" />
49
+ </Link>
50
+ <Link
51
+ to="/sign-in"
52
+ className={buttonVariants({ variant: "outline", size: "lg" })}
53
+ >
54
+ Login
55
+ </Link>
56
+ {{else}}
57
+ <a
58
+ href="https://github.com/Abdullah-dev0/better-ts-stack"
59
+ target="_blank"
60
+ rel="noreferrer"
61
+ className={cn(buttonVariants({ size: "lg" }), "gap-2")}
62
+ >
63
+ Explore the docs
64
+ <ArrowRight className="size-4" />
65
+ </a>
66
+ <a
67
+ href="https://tanstack.com/start/latest"
68
+ target="_blank"
69
+ rel="noreferrer"
70
+ className={buttonVariants({ variant: "outline", size: "lg" })}
71
+ >
72
+ TanStack guide
73
+ </a>
74
+ {{/if}}
75
+ </div>
76
+ </div>
77
+
78
+ <Card className="border-border bg-card shadow-sm">
79
+ <CardHeader>
80
+ <CardTitle>Included in this starter</CardTitle>
81
+ </CardHeader>
82
+ <CardContent className="space-y-4">
83
+ <ul className="space-y-3 text-sm text-muted-foreground">
84
+ {featureList.map((feature) => (
85
+ <li key={feature} className="flex items-center gap-3">
86
+ <span className="size-2 rounded-full bg-primary" />
87
+ <span>{feature}</span>
88
+ </li>
89
+ ))}
90
+ </ul>
91
+ <div className="rounded-2xl border border-border bg-muted px-4 py-3 text-sm text-muted-foreground">
92
+ Start editing{" "}
93
+ <code className="font-mono">src/routes/index.tsx</code> and
94
+ shape the project around your product instead of setup chores.
95
+ </div>
96
+ </CardContent>
97
+ </Card>
98
+ </section>
99
+ </div>
100
+ </main>
101
+ );
102
+ }
@@ -0,0 +1,85 @@
1
+ @import "tailwindcss" source("../");
2
+
3
+ @theme inline {
4
+ --color-background: var(--background);
5
+ --color-foreground: var(--foreground);
6
+ --color-card: var(--card);
7
+ --color-card-foreground: var(--card-foreground);
8
+ --color-primary: var(--primary);
9
+ --color-primary-foreground: var(--primary-foreground);
10
+ --color-secondary: var(--secondary);
11
+ --color-secondary-foreground: var(--secondary-foreground);
12
+ --color-muted: var(--muted);
13
+ --color-muted-foreground: var(--muted-foreground);
14
+ --color-accent: var(--accent);
15
+ --color-accent-foreground: var(--accent-foreground);
16
+ --color-destructive: var(--destructive);
17
+ --color-border: var(--border);
18
+ --color-input: var(--input);
19
+ --color-ring: var(--ring);
20
+ }
21
+
22
+ :root {
23
+ --background: oklch(0.985 0 0);
24
+ --foreground: oklch(0.145 0 0);
25
+ --card: oklch(1 0 0);
26
+ --card-foreground: oklch(0.145 0 0);
27
+ --primary: oklch(0.205 0 0);
28
+ --primary-foreground: oklch(0.985 0 0);
29
+ --secondary: oklch(0.97 0 0);
30
+ --secondary-foreground: oklch(0.205 0 0);
31
+ --muted: oklch(0.97 0 0);
32
+ --muted-foreground: oklch(0.556 0 0);
33
+ --accent: oklch(0.97 0 0);
34
+ --accent-foreground: oklch(0.205 0 0);
35
+ --destructive: oklch(0.577 0.245 27.325);
36
+ --border: oklch(0.922 0 0);
37
+ --input: oklch(0.922 0 0);
38
+ --ring: oklch(0.708 0 0);
39
+ }
40
+
41
+ @media (prefers-color-scheme: dark) {
42
+ :root {
43
+ --background: oklch(0.145 0 0);
44
+ --foreground: oklch(0.985 0 0);
45
+ --card: oklch(0.205 0 0);
46
+ --card-foreground: oklch(0.985 0 0);
47
+ --primary: oklch(0.922 0 0);
48
+ --primary-foreground: oklch(0.205 0 0);
49
+ --secondary: oklch(0.269 0 0);
50
+ --secondary-foreground: oklch(0.985 0 0);
51
+ --muted: oklch(0.269 0 0);
52
+ --muted-foreground: oklch(0.708 0 0);
53
+ --accent: oklch(0.269 0 0);
54
+ --accent-foreground: oklch(0.985 0 0);
55
+ --destructive: oklch(0.704 0.191 22.216);
56
+ --border: oklch(1 0 0 / 10%);
57
+ --input: oklch(1 0 0 / 15%);
58
+ --ring: oklch(0.556 0 0);
59
+ }
60
+ }
61
+
62
+ @layer base {
63
+ *,
64
+ ::after,
65
+ ::before,
66
+ ::backdrop,
67
+ ::file-selector-button {
68
+ border-color: var(--color-gray-200, currentcolor);
69
+ }
70
+ }
71
+
72
+ @layer base {
73
+ html {
74
+ color-scheme: light dark;
75
+ }
76
+
77
+ * {
78
+ @apply border-gray-200 dark:border-gray-800;
79
+ }
80
+
81
+ html,
82
+ body {
83
+ @apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200;
84
+ }
85
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "include": ["**/*.ts", "**/*.tsx"],
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "esModuleInterop": true,
6
+ "jsx": "react-jsx",
7
+ "module": "ESNext",
8
+ "moduleResolution": "Bundler",
9
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
10
+ "isolatedModules": true,
11
+ "resolveJsonModule": true,
12
+ "skipLibCheck": true,
13
+ "target": "ES2022",
14
+ "allowJs": true,
15
+ "noEmit": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "paths": {
18
+ "~/*": ["./src/*"]
19
+ }
20
+ },
21
+ "exclude": ["node_modules"]
22
+ }
@@ -0,0 +1,14 @@
1
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import viteReact from "@vitejs/plugin-react";
4
+ import { defineConfig } from "vite";
5
+
6
+ export default defineConfig({
7
+ server: {
8
+ port: 3000,
9
+ },
10
+ resolve: {
11
+ tsconfigPaths: true,
12
+ },
13
+ plugins: [tanstackStart({ srcDirectory: "src" }), tailwindcss(), viteReact()],
14
+ });
@@ -0,0 +1,33 @@
1
+ {
2
+ "id": "auth",
3
+ "name": "Better Auth",
4
+ "description": "Better Auth for TanStack Start with optional Prisma, Drizzle, or MongoDB adapter",
5
+ "type": "feature",
6
+ "dependencies": {
7
+ "@hookform/resolvers": "^5.9.1",
8
+ "better-auth": "^1.7.3",
9
+ "dotenv": "^17.4.2",
10
+ "mongodb": "^7.6.0",
11
+ "react-hook-form": "^7.87.0",
12
+ "zod": "^4.5.4"
13
+ },
14
+ "devDependencies": {},
15
+ "scripts": {},
16
+ "envVars": {
17
+ "BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
18
+ "BETTER_AUTH_URL": "http://localhost:3000"
19
+ },
20
+ "templateFiles": [
21
+ "src/lib/auth.ts.hbs",
22
+ "src/lib/auth-client.ts",
23
+ "src/lib/auth-functions.ts",
24
+ "src/routes/api/auth/$.ts",
25
+ "src/routes/dashboard.tsx",
26
+ "src/routes/sign-in.tsx",
27
+ "src/routes/sign-up.tsx",
28
+ "src/components/auth/sign-in-form.tsx",
29
+ "src/components/auth/sign-up-form.tsx",
30
+ "src/components/auth/sign-out-button.tsx",
31
+ "src/lib/auth-schema.ts"
32
+ ]
33
+ }
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import { Link, useNavigate } from "@tanstack/react-router";
4
+ import { useState, useTransition } from "react";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { LoaderCircle } from "lucide-react";
7
+ import { useForm } from "react-hook-form";
8
+
9
+ import { authClient } from "~/lib/auth-client";
10
+ import {
11
+ getAuthErrorMessage,
12
+ signInSchema,
13
+ type SignInValues,
14
+ } from "~/lib/auth-schema";
15
+ import { Button } from "~/components/ui/button";
16
+ import {
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardFooter,
21
+ CardHeader,
22
+ CardTitle,
23
+ } from "~/components/ui/card";
24
+ import {
25
+ Field,
26
+ FieldDescription,
27
+ FieldError,
28
+ FieldGroup,
29
+ FieldLabel,
30
+ } from "~/components/ui/field";
31
+ import { Input } from "~/components/ui/input";
32
+
33
+ export function SignInForm() {
34
+ const navigate = useNavigate();
35
+ const [isPending, startTransition] = useTransition();
36
+ const [authError, setAuthError] = useState<string | null>(null);
37
+ const form = useForm<SignInValues>({
38
+ resolver: zodResolver(signInSchema),
39
+ defaultValues: {
40
+ email: "",
41
+ password: "",
42
+ },
43
+ });
44
+
45
+ const onSubmit = form.handleSubmit((values) => {
46
+ setAuthError(null);
47
+
48
+ startTransition(async () => {
49
+ const result = await authClient.signIn.email({
50
+ email: values.email,
51
+ password: values.password,
52
+ });
53
+
54
+ if (result.error) {
55
+ setAuthError(getAuthErrorMessage(result.error));
56
+ return;
57
+ }
58
+
59
+ navigate({ to: "/dashboard" });
60
+ });
61
+ });
62
+
63
+ return (
64
+ <Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
65
+ <CardHeader className="space-y-2">
66
+ <CardTitle>Login to your account</CardTitle>
67
+ <CardDescription>
68
+ Sign in with your email and password to continue to your dashboard.
69
+ </CardDescription>
70
+ </CardHeader>
71
+ <CardContent>
72
+ <form className="space-y-6" onSubmit={onSubmit} noValidate>
73
+ <FieldGroup>
74
+ <Field data-invalid={Boolean(form.formState.errors.email)}>
75
+ <FieldLabel htmlFor="email">Email</FieldLabel>
76
+ <Input
77
+ id="email"
78
+ type="email"
79
+ autoComplete="email"
80
+ aria-invalid={Boolean(form.formState.errors.email)}
81
+ placeholder="you@example.com"
82
+ {...form.register("email")}
83
+ />
84
+ <FieldDescription>
85
+ Use the email you registered with.
86
+ </FieldDescription>
87
+ <FieldError errors={[form.formState.errors.email]} />
88
+ </Field>
89
+
90
+ <Field data-invalid={Boolean(form.formState.errors.password)}>
91
+ <FieldLabel htmlFor="password">Password</FieldLabel>
92
+ <Input
93
+ id="password"
94
+ type="password"
95
+ autoComplete="current-password"
96
+ aria-invalid={Boolean(form.formState.errors.password)}
97
+ placeholder="Enter your password"
98
+ {...form.register("password")}
99
+ />
100
+ <FieldError errors={[form.formState.errors.password]} />
101
+ </Field>
102
+ </FieldGroup>
103
+
104
+ {authError ? (
105
+ <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
106
+ {authError}
107
+ </div>
108
+ ) : null}
109
+
110
+ <Button className="w-full" type="submit" disabled={isPending}>
111
+ {isPending ? (
112
+ <>
113
+ <LoaderCircle className="mr-2 size-4 animate-spin" />
114
+ Signing in
115
+ </>
116
+ ) : (
117
+ "Login"
118
+ )}
119
+ </Button>
120
+ </form>
121
+ </CardContent>
122
+ <CardFooter className="justify-center">
123
+ <p className="text-sm text-slate-600">
124
+ Need an account?{" "}
125
+ <Link
126
+ className="font-medium text-sky-700 hover:text-sky-800"
127
+ to="/sign-up"
128
+ >
129
+ Signup
130
+ </Link>
131
+ </p>
132
+ </CardFooter>
133
+ </Card>
134
+ );
135
+ }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { useNavigate } from "@tanstack/react-router";
4
+ import { useState, useTransition } from "react";
5
+ import { LoaderCircle, LogOut } from "lucide-react";
6
+
7
+ import { authClient } from "~/lib/auth-client";
8
+ import { Button } from "~/components/ui/button";
9
+
10
+ export function SignOutButton() {
11
+ const navigate = useNavigate();
12
+ const [isPending, startTransition] = useTransition();
13
+ const [error, setError] = useState<string | null>(null);
14
+
15
+ return (
16
+ <div className="space-y-2">
17
+ <Button
18
+ variant="outline"
19
+ onClick={() => {
20
+ setError(null);
21
+ startTransition(async () => {
22
+ const result = await authClient.signOut();
23
+
24
+ if (result.error) {
25
+ setError("Unable to sign out right now. Please try again.");
26
+ return;
27
+ }
28
+
29
+ navigate({ to: "/sign-in" });
30
+ });
31
+ }}
32
+ disabled={isPending}
33
+ >
34
+ {isPending ? (
35
+ <>
36
+ <LoaderCircle className="mr-2 size-4 animate-spin" />
37
+ Signing out
38
+ </>
39
+ ) : (
40
+ <>
41
+ <LogOut className="mr-2 size-4" />
42
+ Sign out
43
+ </>
44
+ )}
45
+ </Button>
46
+ {error ? <p className="text-sm text-rose-600">{error}</p> : null}
47
+ </div>
48
+ );
49
+ }