feat: add TanStack Start integration

This commit is contained in:
LovelessCodes
2024-10-29 21:36:06 +01:00
committed by GitHub
parent f9e605d441
commit a744354d88
43 changed files with 2905 additions and 217 deletions

View File

@@ -22,11 +22,11 @@ export default function Features({ stars }: { stars: string | null }) {
return ( return (
<div className="md:w-10/12 overflow-hidden mt-10 mx-auto font-geist relative md:border-l-0 md:border-[1.2px] rounded-none -pr-2"> <div className="md:w-10/12 overflow-hidden mt-10 mx-auto font-geist relative md:border-l-0 md:border-[1.2px] rounded-none -pr-2">
<Plus className="absolute top-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" /> <Plus className="absolute top-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" />
<div className="grid grid-cols-1 md:grid-cols-3 md:mx-0 grid-rows-4 md:grid-rows-4 w-full"> <div className="grid w-full grid-cols-1 grid-rows-4 md:grid-cols-3 md:mx-0 md:grid-rows-4">
<div className="relative items-start justify-start border-l-[1.2px] border-t-[1.2px] md:border-t-0 transform-gpu flex flex-col p-10 overflow-clip"> <div className="relative items-start justify-start border-l-[1.2px] border-t-[1.2px] md:border-t-0 transform-gpu flex flex-col p-10 overflow-clip">
<Plus className="absolute bottom-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" /> <Plus className="absolute bottom-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" />
<div className="flex gap-2 items-center my-1"> <div className="flex items-center gap-2 my-1">
<PlugZap2Icon className="w-4 h-4" /> <PlugZap2Icon className="w-4 h-4" />
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
Framework Agnostic{" "} Framework Agnostic{" "}
@@ -35,12 +35,12 @@ export default function Features({ stars }: { stars: string | null }) {
<div className="mt-2"> <div className="mt-2">
<div className="max-w-full"> <div className="max-w-full">
<div className="flex gap-3 "> <div className="flex gap-3 ">
<p className="text-xl md:text-2xl tracking-tighter font-normal max-w-lg"> <p className="max-w-lg text-xl font-normal tracking-tighter md:text-2xl">
Supports popular <strong>frameworks</strong> Supports popular <strong>frameworks</strong>
</p> </p>
</div> </div>
</div> </div>
<p className="text-left text-sm mt-2 text-muted-foreground"> <p className="mt-2 text-sm text-left text-muted-foreground">
Supports your favorite frontend, backend and meta frameworks, Supports your favorite frontend, backend and meta frameworks,
including React, Vue, Svelte, Astro, Solid, Next.js, Nuxt, Hono, including React, Vue, Svelte, Astro, Solid, Next.js, Nuxt, Hono,
and more{" "} and more{" "}
@@ -53,19 +53,19 @@ export default function Features({ stars }: { stars: string | null }) {
<div className="relative items-start justify-start border-l-[1.2px] border-t-[1.2px] md:border-t-0 transform-gpu flex flex-col p-10"> <div className="relative items-start justify-start border-l-[1.2px] border-t-[1.2px] md:border-t-0 transform-gpu flex flex-col p-10">
<Plus className="absolute bottom-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" /> <Plus className="absolute bottom-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" />
<div className="flex gap-2 items-center my-1"> <div className="flex items-center gap-2 my-1">
<LockClosedIcon className="w-4 h-4" /> <LockClosedIcon className="w-4 h-4" />
<p className="text-gray-600 dark:text-gray-400">Authentication</p> <p className="text-gray-600 dark:text-gray-400">Authentication</p>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<div className="max-w-full"> <div className="max-w-full">
<div className="flex gap-3 "> <div className="flex gap-3 ">
<p className="text-2xl tracking-tighter font-normal max-w-lg"> <p className="max-w-lg text-2xl font-normal tracking-tighter">
Email & Password <strong>Authentication</strong> Email & Password <strong>Authentication</strong>
</p> </p>
</div> </div>
</div> </div>
<p className="text-left text-sm mt-2 text-muted-foreground"> <p className="mt-2 text-sm text-left text-muted-foreground">
Builtin support for email and password authentication, with secure Builtin support for email and password authentication, with secure
password hashing and account management features{" "} password hashing and account management features{" "}
<a className="text-gray-50" href="/docs" target="_blank"> <a className="text-gray-50" href="/docs" target="_blank">
@@ -77,19 +77,19 @@ export default function Features({ stars }: { stars: string | null }) {
<div className="relative items-start justify-start md:border-l-[0.2px] border-t-[1.2px] md:border-t-0 flex flex-col p-10"> <div className="relative items-start justify-start md:border-l-[0.2px] border-t-[1.2px] md:border-t-0 flex flex-col p-10">
<Plus className="absolute bottom-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" /> <Plus className="absolute bottom-[-17px] left-[-17px] text-black/20 dark:text-white/30 w-8 h-8" />
<div className="flex gap-2 items-center my-1"> <div className="flex items-center gap-2 my-1">
<Webhook className="w-4 h-4" /> <Webhook className="w-4 h-4" />
<p className="text-gray-600 dark:text-gray-400">Social Sign-on</p> <p className="text-gray-600 dark:text-gray-400">Social Sign-on</p>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<div className="max-w-full"> <div className="max-w-full">
<div className="flex gap-3 "> <div className="flex gap-3 ">
<p className="text-2xl tracking-tighter font-normal max-w-lg"> <p className="max-w-lg text-2xl font-normal tracking-tighter">
Support multiple <strong>OAuth providers.</strong> Support multiple <strong>OAuth providers.</strong>
</p> </p>
</div> </div>
</div> </div>
<p className="text-left text-sm mt-2 text-muted-foreground"> <p className="mt-2 text-sm text-left text-muted-foreground">
Allow users to sign in with their accounts, including GitHub, Allow users to sign in with their accounts, including GitHub,
Google, Discord, Twitter, and more.{" "} Google, Discord, Twitter, and more.{" "}
<a className="text-gray-50" href="#" target="_blank"> <a className="text-gray-50" href="#" target="_blank">
@@ -99,19 +99,19 @@ export default function Features({ stars }: { stars: string | null }) {
</div> </div>
</div> </div>
<div className="items-start justify-start border-l-[1.2px] border-t-[1.2px] flex flex-col p-10 "> <div className="items-start justify-start border-l-[1.2px] border-t-[1.2px] flex flex-col p-10 ">
<div className="flex gap-2 items-center my-1"> <div className="flex items-center gap-2 my-1">
<ShieldCheckIcon className="w-4 h-4" /> <ShieldCheckIcon className="w-4 h-4" />
<p className="text-gray-600 dark:text-gray-400">Two Factor</p> <p className="text-gray-600 dark:text-gray-400">Two Factor</p>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<div className="max-w-full"> <div className="max-w-full">
<div className="flex gap-3 "> <div className="flex gap-3 ">
<p className="text-2xl tracking-tighter font-normal max-w-lg"> <p className="max-w-lg text-2xl font-normal tracking-tighter">
Two Factor <strong>Authentication</strong> Two Factor <strong>Authentication</strong>
</p> </p>
</div> </div>
</div> </div>
<p className="text-left text-sm mt-2 text-muted-foreground"> <p className="mt-2 text-sm text-left text-muted-foreground">
With our built-in two factor authentication plugin, you can add an With our built-in two factor authentication plugin, you can add an
extra layer of security to your account.{" "} extra layer of security to your account.{" "}
<Link className="text-gray-50" href="/docs" target="_blank"> <Link className="text-gray-50" href="/docs" target="_blank">
@@ -121,7 +121,7 @@ export default function Features({ stars }: { stars: string | null }) {
</div> </div>
</div> </div>
<div className="items-start justify-staart border-l-[1.2px] border-t-[1.2px] flex flex-col p-10 "> <div className="items-start justify-staart border-l-[1.2px] border-t-[1.2px] flex flex-col p-10 ">
<div className="flex gap-2 items-center my-1"> <div className="flex items-center gap-2 my-1">
<RabbitIcon className="w-4 h-4" /> <RabbitIcon className="w-4 h-4" />
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
Organization & Access Control{" "} Organization & Access Control{" "}
@@ -130,12 +130,12 @@ export default function Features({ stars }: { stars: string | null }) {
<div className="mt-2"> <div className="mt-2">
<div className="max-w-full"> <div className="max-w-full">
<div className="flex gap-3 "> <div className="flex gap-3 ">
<p className="text-2xl tracking-tighter font-normal max-w-lg"> <p className="max-w-lg text-2xl font-normal tracking-tighter">
Gain and manage <strong>access.</strong> Gain and manage <strong>access.</strong>
</p> </p>
</div> </div>
</div> </div>
<p className="text-left text-sm mt-2 text-muted-foreground"> <p className="mt-2 text-sm text-left text-muted-foreground">
Manage users and their access to resources within your Manage users and their access to resources within your
application.{" "} application.{" "}
<a className="text-gray-50" href="/docs" target="_blank"> <a className="text-gray-50" href="/docs" target="_blank">
@@ -145,7 +145,7 @@ export default function Features({ stars }: { stars: string | null }) {
</div> </div>
</div> </div>
<div className="items-start justify-start border-l-[1.2px] border-t-[1.2px] transform-gpu relative flex flex-col p-10 "> <div className="items-start justify-start border-l-[1.2px] border-t-[1.2px] transform-gpu relative flex flex-col p-10 ">
<div className="flex gap-2 items-center my-1"> <div className="flex items-center gap-2 my-1">
<PlugIcon className="w-4 h-4" /> <PlugIcon className="w-4 h-4" />
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
Plugin Ecosystem{" "} Plugin Ecosystem{" "}
@@ -153,13 +153,13 @@ export default function Features({ stars }: { stars: string | null }) {
</div> </div>
<div className="max-w-full"> <div className="max-w-full">
<div className="flex gap-3 "> <div className="flex gap-3 ">
<p className="text-2xl tracking-tighter font-normal max-w-lg"> <p className="max-w-lg text-2xl font-normal tracking-tighter">
Extend your application with plugins Extend your application with plugins
</p> </p>
</div> </div>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<p className="text-left text-sm mt-2 text-muted-foreground"> <p className="mt-2 text-sm text-left text-muted-foreground">
Enhance your application with our official plugins and those Enhance your application with our official plugins and those
created by the community.{" "} created by the community.{" "}
<a className="text-gray-50" href="/docs" target="_blank"> <a className="text-gray-50" href="/docs" target="_blank">
@@ -169,15 +169,15 @@ export default function Features({ stars }: { stars: string | null }) {
</div> </div>
</div> </div>
<div className="relative md:grid md:col-span-3 grid-cols-2 row-span-2 border-t-[1.2px] border-l-[1.2px] md:border-b-[1.2px] dark:border-b-0 h-full py-20 "> <div className="relative md:grid md:col-span-3 grid-cols-2 row-span-2 border-t-[1.2px] border-l-[1.2px] md:border-b-[1.2px] dark:border-b-0 h-full py-20 ">
<div className="p-16 pt-10 md:px-10 h-full md:absolute top-0 left-0 w-full"> <div className="top-0 left-0 w-full h-full p-16 pt-10 md:px-10 md:absolute">
<div className="flex flex-col gap-3 justify-center h-full items-center w-full"> <div className="flex flex-col items-center justify-center w-full h-full gap-3">
<div className="flex gap-2 items-center"> <div className="flex items-center gap-2">
<Globe2Icon className="w-4 h-4" /> <Globe2Icon className="w-4 h-4" />
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
Own your auth Own your auth
</p> </p>
</div> </div>
<p className="text-4xl md:text-4xl mt-4 tracking-tighter font-normal max-w-md mx-auto text-center"> <p className="max-w-md mx-auto mt-4 text-4xl font-normal tracking-tighter text-center md:text-4xl">
<strong>Roll your own auth with confidence in minutes!</strong> <strong>Roll your own auth with confidence in minutes!</strong>
</p> </p>
<div className="flex mt-[10px] z-20 justify-center items-start"> <div className="flex mt-[10px] z-20 justify-center items-start">
@@ -190,6 +190,7 @@ export default function Features({ stars }: { stars: string | null }) {
"solidStart", "solidStart",
"react", "react",
"hono", "hono",
"tanstack",
]} ]}
/> />
</div> </div>

View File

@@ -259,4 +259,40 @@ export const Icons = {
/> />
</svg> </svg>
), ),
tanstack: (props?: SVGProps<any>) => (
<svg
className={cn(props?.className)}
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 100 100"
>
<mask id="a" style={{maskType: "alpha"}} maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<circle cx="50" cy="50" r="50" className="fill-foreground"/>
</mask>
<g mask="url(#a)">
<circle cx="11" cy="119" r="52" className="fill-muted-foreground stroke-foreground" strokeWidth="4"/>
<circle cx="10" cy="125" r="52" className="fill-muted-foreground stroke-foreground" strokeWidth="4"/>
<circle cx="9" cy="131" r="52" className="fill-muted-foreground stroke-muted-foreground" strokeWidth="4"/>
<circle cx="88" cy="119" r="52" className="fill-muted-foreground stroke-foreground" strokeWidth="4"/>
<path className="fill-foreground" d="M89 35h2v5h-2zM83 34l2 1-1 4h-2zM77 31l2 1-3 4-2-1zM73 27l1 1-3 4-1-2zM70 23l1 1-4 3-1-2zM68 18v2l-4 1-1-2zM68 11l1 2-5 1-1-2zM69 6v2h-5V6z"/>
<circle cx="89" cy="125" r="52" className="fill-muted-foreground stroke-foreground" strokeWidth="4"/>
<circle cx="90" cy="131" r="52" className="fill-muted-foreground stroke-muted-foreground" strokeWidth="4"/>
<ellipse cx="49.5" cy="119" rx="41.5" ry="51" className="fill-muted-foreground"/>
<path d="M34 38v-9c1 1 2 4 5 6l7 30-8 2c-1-23-2-23-4-29Z" className="fill-foreground stroke-muted-foreground"/>
<path fillRule="evenodd" clipRule="evenodd" d="M95 123c0 31-20 57-45 57S5 154 5 123c0-27 14-50 33-56l12-2c25 0 45 26 45 58Zm-45 47c22 0 39-22 39-50S72 70 50 70s-39 22-39 50 17 50 39 50Z" className="fill-foreground"/>
<path d="M34 29c-4-8-11-5-14-4 2 3 5 4 9 4h5Z" className="fill-foreground stroke-muted-foreground"/>
<path d="M25 38c-1 6 0 14 2 18 5-7 7-13 7-18v-9c-5 1-7 5-9 9Z" className="fill-muted-foreground"/>
<path d="M34 29c-1 3-5 11-5 16m5-16c-5 1-7 5-9 9-1 6 0 14 2 18 5-7 7-13 7-18v-9Z" className="stroke-muted-foreground"/>
<path d="M44 18c-10 1-11 7-10 11l4-3c5-4 6-7 6-8Z" className="fill-foreground stroke-muted-foreground"/>
<path d="M34 29h7l18 4c-3-6-9-14-21-7l-4 3Z" className="fill-foreground"/>
<path d="M34 29c4-2 12-5 18-1m-18 1h7l18 4c-3-6-9-14-21-7l-4 3Z" className="stroke-muted-foreground"/>
<path d="M32 29a1189 1189 0 0 1-16 19c0-17 7-18 13-19h5a14 14 0 0 1-2 0Z" className="fill-foreground"/>
<path d="M34 29c-5 1-7 5-9 9l-9 10c0-17 7-18 13-19h5Zm0 0c-5 2-11 3-14 10" className="stroke-muted-foreground"/>
<path d="M41 29c9 2 13 10 15 14a25 25 0 0 1-22-14h7Z" className="fill-foreground"/>
<path d="M34 29c3 1 11 5 15 9m-15-9h7c9 2 13 10 15 14a25 25 0 0 1-22-14Z" className="stroke-muted-foreground"/>
<circle cx="91.5" cy="12.5" r="18.5" className="fill-foreground stroke-muted-foreground" strokeWidth="2"/>
</g>
</svg>
)
}; };

View File

@@ -601,12 +601,16 @@ export const contents: Content[] = [
icon: Icons.svelteKit, icon: Icons.svelteKit,
href: "/docs/integrations/svelte-kit", href: "/docs/integrations/svelte-kit",
}, },
{ {
title: "Solid Start", title: "Solid Start",
icon: Icons.solidStart, icon: Icons.solidStart,
href: "/docs/integrations/solid-start", href: "/docs/integrations/solid-start",
}, },
{
title: "TanStack Start",
icon: Icons.tanstack,
href: "/docs/integrations/tanstack",
},
{ {
group: true, group: true,
title: "Backend", title: "Backend",

View File

@@ -34,4 +34,8 @@ export const techStackIcons: TechStackIconType = {
name: "Astro", name: "Astro",
icon: <Icons.astro className="w-10 h-10" />, icon: <Icons.astro className="w-10 h-10" />,
}, },
tanstack: {
name: "TanStack Start",
icon: <Icons.tanstack className="w-10 h-10" />,
}
}; };

View File

@@ -292,7 +292,7 @@ The server provides a `session` object that you can use to access the session da
**Example: Using some popular frameworks** **Example: Using some popular frameworks**
<Tabs items={["NextJs", "Nuxt", "Svelte", "Astro", "Hono"]}> <Tabs items={["NextJs", "Nuxt", "Svelte", "Astro", "Hono", "TanStack"]}>
<Tab value="NextJs"> <Tab value="NextJs">
```ts title="server.ts" ```ts title="server.ts"
import { auth } from "./auth"; // path to your Better Auth server instance import { auth } from "./auth"; // path to your Better Auth server instance
@@ -369,6 +369,20 @@ The server provides a `session` object that you can use to access the session da
}); });
``` ```
</Tab> </Tab>
<Tab value="TanStack">
```ts title="app/routes/api/index.ts"
import { auth } from "./auth";
import { createAPIFileRoute } from "@tanstack/start/api";
export const Route = createAPIFileRoute("/api/$")({
GET: async ({ request }) => {
const session = await auth.api.getSession({
headers: request.headers
})
},
});
```
</Tab>
</Tabs> </Tabs>
## Using Plugins ## Using Plugins

View File

@@ -236,7 +236,7 @@ Create a new file or route in your framework's designated catch-all route handle
Better Auth supports any backend framework with standard Request and Response objects and offers helper functions for popular frameworks. Better Auth supports any backend framework with standard Request and Response objects and offers helper functions for popular frameworks.
</Callout> </Callout>
<Tabs items={["next-js", "nuxt", "svelte-kit", "remix", "solid-start", "hono", "express", "elysia"]} defaultValue="react"> <Tabs items={["next-js", "nuxt", "svelte-kit", "remix", "solid-start", "hono", "express", "elysia", "tanstack-start"]} defaultValue="react">
<Tab value="next-js"> <Tab value="next-js">
```ts title="/app/api/auth/[...all]/route.ts" ```ts title="/app/api/auth/[...all]/route.ts"
import { auth } from "@/lib/auth"; // path to your auth file import { auth } from "@/lib/auth"; // path to your auth file
@@ -354,6 +354,22 @@ Better Auth supports any backend framework with standard Request and Response ob
); );
``` ```
</Tab> </Tab>
<Tab value="tanstack-start">
```ts
// app/routes/api/auth/$.ts
import { auth } from '~/lib/server/auth'
import { createAPIFileRoute } from '@tanstack/start/api'
export const Route = createAPIFileRoute('/api/auth/$')({
GET: ({ request }) => {
return auth.handler(request)
},
POST: ({ request }) => {
return auth.handler(request)
},
});
```
</Tab>
</Tabs> </Tabs>
</Step> </Step>

View File

@@ -0,0 +1,29 @@
---
title: TanStack Start Integration
description: Tanstack Start Integration Guide
---
This integration guide is assuming you are using TanStack Start.
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
### Mount the handler
We need to mount the handler to a TanStack API endpoint.
Create a new file: `/app/routes/api/auth/$.ts`
```ts
import { auth } from '@/lib/auth'
import { createAPIFileRoute } from '@tanstack/start/api'
export const Route = createAPIFileRoute('/api/auth/$')({
GET: ({ request }) => {
return auth.handler(request)
},
POST: ({ request }) => {
return auth.handler(request)
},
})
```
This will allow you to access use the `getSession` method in all of your routes.

View File

@@ -0,0 +1,27 @@
![Banner](./header.webp)
An example of using Better Auth with [TanStack Start](https://tanstack.com/start).
## Setup
To install dependencies:
```bash
pnpm install
```
To migrate Better-Auth:
```bash
pnpx @better-auth/cli migrate
```
To run:
```bash
pnpm dev
```
## Preview
![Sign In Preview](./preview.webp)

View File

@@ -0,0 +1,12 @@
import { defineConfig } from "@tanstack/start/config";
import viteTsConfigPaths from "vite-tsconfig-paths";
export default defineConfig({
vite: {
plugins: [
viteTsConfigPaths({
projects: ["./tsconfig.json"],
}),
],
},
});

View File

@@ -0,0 +1,6 @@
import {
createStartAPIHandler,
defaultAPIFileRouteHandler,
} from "@tanstack/start/api";
export default createStartAPIHandler(defaultAPIFileRouteHandler);

View File

@@ -0,0 +1,12 @@
import { StartClient } from "@tanstack/start";
import { hydrateRoot } from "react-dom/client";
import { createRouter } from "./router";
const router = createRouter();
const root = document.getElementById("root");
if (!root) {
throw new Error("Root element not found");
}
hydrateRoot(root, <StartClient router={router} />);

View File

@@ -0,0 +1,78 @@
"use client";
import { Link } from "@tanstack/react-router";
import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { signIn } from "~/lib/client/auth";
export function LoginForm() {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const form = e.target as HTMLFormElement;
const data = new FormData(form);
signIn.email(
{
email: data.get("email") as string,
password: data.get("password") as string,
},
{
onError: (error) => {
console.warn(error);
toast.error(error.error.message);
},
onSuccess: () => {
toast.success("You have been logged in!");
},
},
);
}
return (
<Card className="mx-auto max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Sign In</CardTitle>
<CardDescription>
Enter your email below to sign in to your account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input id="password" name="password" type="password" required />
</div>
<Button type="submit" className="w-full">
Sign In
</Button>
</form>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<Link to="/auth/signup" className="underline">
Sign up
</Link>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,90 @@
"use client";
import { Link } from "@tanstack/react-router";
import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { signUp } from "~/lib/client/auth";
export function RegisterForm() {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const form = e.target as HTMLFormElement;
const data = new FormData(form);
console.log(data);
signUp.email(
{
name: data.get("name") as string,
email: data.get("email") as string,
password: data.get("password") as string,
},
{
onError: (error) => {
console.warn(error);
toast.error(error.error.message);
},
onSuccess: () => {
toast.success("Account has been created!");
},
},
);
}
return (
<Card className="mx-auto max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Sign Up</CardTitle>
<CardDescription>
Enter your email below to sign up to an account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
name="name"
id="name"
type="name"
placeholder="John Doe"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input id="password" name="password" type="password" required />
</div>
<Button type="submit" className="w-full">
Sign Up
</Button>
</form>
<div className="mt-4 text-center text-sm">
Already have an account?{" "}
<Link to="/auth/signin" className="underline">
Sign in
</Link>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,58 @@
"use client";
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "~/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,88 @@
"use client";
import * as React from "react";
import { cn } from "~/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import { cn } from "~/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,129 @@
"use client";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "~/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@@ -0,0 +1,31 @@
"use client";
import { useTheme } from "next-themes";
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster };

View File

@@ -0,0 +1,5 @@
import { createAuthClient } from "better-auth/react";
export const { useSession, signIn, signOut, signUp } = createAuthClient({
baseURL: "http://localhost:3000",
});

View File

@@ -0,0 +1,9 @@
import { betterAuth } from "better-auth";
import Database from "better-sqlite3";
export const auth = betterAuth({
database: new Database("data.db"),
emailAndPassword: {
enabled: true,
},
});

View File

@@ -0,0 +1,81 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--popover: 224 71% 4%;
--popover-foreground: 215 20.2% 65.1%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,9 @@
import { LoginForm } from "~/components/login-form"
export default function Page() {
return (
<div className="flex h-screen w-full items-center justify-center px-4">
<LoginForm />
</div>
)
}

View File

@@ -0,0 +1,136 @@
/* prettier-ignore-start */
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file is auto-generated by TanStack Router
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index'
import { Route as AuthSignupImport } from './routes/auth/signup'
import { Route as AuthSigninImport } from './routes/auth/signin'
// Create/Update Routes
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
const AuthSignupRoute = AuthSignupImport.update({
id: '/auth/signup',
path: '/auth/signup',
getParentRoute: () => rootRoute,
} as any)
const AuthSigninRoute = AuthSigninImport.update({
id: '/auth/signin',
path: '/auth/signin',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/auth/signin': {
id: '/auth/signin'
path: '/auth/signin'
fullPath: '/auth/signin'
preLoaderRoute: typeof AuthSigninImport
parentRoute: typeof rootRoute
}
'/auth/signup': {
id: '/auth/signup'
path: '/auth/signup'
fullPath: '/auth/signup'
preLoaderRoute: typeof AuthSignupImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/auth/signin': typeof AuthSigninRoute
'/auth/signup': typeof AuthSignupRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/auth/signin': typeof AuthSigninRoute
'/auth/signup': typeof AuthSignupRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/auth/signin': typeof AuthSigninRoute
'/auth/signup': typeof AuthSignupRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/auth/signin' | '/auth/signup'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/auth/signin' | '/auth/signup'
id: '__root__' | '/' | '/auth/signin' | '/auth/signup'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AuthSigninRoute: typeof AuthSigninRoute
AuthSignupRoute: typeof AuthSignupRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuthSigninRoute: AuthSigninRoute,
AuthSignupRoute: AuthSignupRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* prettier-ignore-end */
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/auth/signin",
"/auth/signup"
]
},
"/": {
"filePath": "index.tsx"
},
"/auth/signin": {
"filePath": "auth/signin.tsx"
},
"/auth/signup": {
"filePath": "auth/signup.tsx"
}
}
}
ROUTE_MANIFEST_END */

View File

@@ -0,0 +1,16 @@
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function createRouter() {
const router = createTanStackRouter({
routeTree,
});
return router;
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}

View File

@@ -0,0 +1,327 @@
import { Link, createRootRoute, useRouter } from "@tanstack/react-router";
import { Outlet, ScrollRestoration } from "@tanstack/react-router";
import { Body, Head, Html, Meta, Scripts } from "@tanstack/start";
import type * as React from "react";
import { useEffect, useState } from "react";
import { signOut, useSession } from "~/lib/client/auth";
import globalStylesheet from "~/lib/style/global.css?url";
import "~/lib/style/global.css";
import { DoorOpen, LoaderCircle, Moon, Sun } from "lucide-react";
import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from "~/components/ui/navigation-menu";
import { Toaster } from "~/components/ui/sonner";
export const Route = createRootRoute({
meta: () => [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "Better Auth - TanStack Start Example",
},
],
links: () => [
{
rel: "stylesheet",
href: globalStylesheet,
},
],
component: RootComponent,
});
function RootComponent() {
const [theme, setTheme] = useState<"light" | "dark">("light");
const [loading, setLoading] = useState(true);
const { data, isPending } = useSession();
const { navigate } = useRouter();
useEffect(() => {
if (!data?.user) {
if (!location.pathname.includes("auth/")) {
navigate({ to: "/auth/signin" });
}
} else {
navigate({ to: "/" });
}
setTheme(
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
);
}, [data, navigate]);
useEffect(() => {
if (!isPending) {
setLoading(false);
}
}, [isPending]);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
}, [theme]);
return (
<RootDocument>
{loading ? (
<div className="flex h-screen w-screen items-center justify-center">
<LoaderCircle className="animate-spin h-20 w-20" />
</div>
) : (
<>
<nav className="grid grid-cols-3 items-center w-full p-4">
<div className="flex items-center justify-center gap-2">
<svg
width="60"
height="45"
viewBox="0 0 60 45"
fill="none"
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
>
<title>Better Auth</title>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z"
className="fill-black dark:fill-white"
/>
</svg>
<p>BETTER-AUTH</p>
<p>x</p>
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
width="45"
height="45"
viewBox="0 0 100 100"
fill="none"
>
<title>TanStack Start</title>
<mask
id="a"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="100"
height="100"
>
<circle cx="50" cy="50" r="50" className="fill-foreground" />
</mask>
<g mask="url(#a)">
<circle
cx="11"
cy="119"
r="52"
className="fill-muted-foreground stroke-foreground"
strokeWidth="4"
/>
<circle
cx="10"
cy="125"
r="52"
className="fill-muted-foreground stroke-foreground"
strokeWidth="4"
/>
<circle
cx="9"
cy="131"
r="52"
className="fill-muted-foreground stroke-muted-foreground"
strokeWidth="4"
/>
<circle
cx="88"
cy="119"
r="52"
className="fill-muted-foreground stroke-foreground"
strokeWidth="4"
/>
<path
className="fill-foreground"
d="M89 35h2v5h-2zM83 34l2 1-1 4h-2zM77 31l2 1-3 4-2-1zM73 27l1 1-3 4-1-2zM70 23l1 1-4 3-1-2zM68 18v2l-4 1-1-2zM68 11l1 2-5 1-1-2zM69 6v2h-5V6z"
/>
<circle
cx="89"
cy="125"
r="52"
className="fill-muted-foreground stroke-foreground"
strokeWidth="4"
/>
<circle
cx="90"
cy="131"
r="52"
className="fill-muted-foreground stroke-muted-foreground"
strokeWidth="4"
/>
<ellipse
cx="49.5"
cy="119"
rx="41.5"
ry="51"
className="fill-muted-foreground"
/>
<path
d="M34 38v-9c1 1 2 4 5 6l7 30-8 2c-1-23-2-23-4-29Z"
className="fill-foreground stroke-muted-foreground"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M95 123c0 31-20 57-45 57S5 154 5 123c0-27 14-50 33-56l12-2c25 0 45 26 45 58Zm-45 47c22 0 39-22 39-50S72 70 50 70s-39 22-39 50 17 50 39 50Z"
className="fill-foreground"
/>
<path
d="M34 29c-4-8-11-5-14-4 2 3 5 4 9 4h5Z"
className="fill-foreground stroke-muted-foreground"
/>
<path
d="M25 38c-1 6 0 14 2 18 5-7 7-13 7-18v-9c-5 1-7 5-9 9Z"
className="fill-muted-foreground"
/>
<path
d="M34 29c-1 3-5 11-5 16m5-16c-5 1-7 5-9 9-1 6 0 14 2 18 5-7 7-13 7-18v-9Z"
className="stroke-muted-foreground"
/>
<path
d="M44 18c-10 1-11 7-10 11l4-3c5-4 6-7 6-8Z"
className="fill-foreground stroke-muted-foreground"
/>
<path
d="M34 29h7l18 4c-3-6-9-14-21-7l-4 3Z"
className="fill-foreground"
/>
<path
d="M34 29c4-2 12-5 18-1m-18 1h7l18 4c-3-6-9-14-21-7l-4 3Z"
className="stroke-muted-foreground"
/>
<path
d="M32 29a1189 1189 0 0 1-16 19c0-17 7-18 13-19h5a14 14 0 0 1-2 0Z"
className="fill-foreground"
/>
<path
d="M34 29c-5 1-7 5-9 9l-9 10c0-17 7-18 13-19h5Zm0 0c-5 2-11 3-14 10"
className="stroke-muted-foreground"
/>
<path
d="M41 29c9 2 13 10 15 14a25 25 0 0 1-22-14h7Z"
className="fill-foreground"
/>
<path
d="M34 29c3 1 11 5 15 9m-15-9h7c9 2 13 10 15 14a25 25 0 0 1-22-14Z"
className="stroke-muted-foreground"
/>
<circle
cx="91.5"
cy="12.5"
r="18.5"
className="fill-foreground stroke-muted-foreground"
strokeWidth="2"
/>
</g>
</svg>
<p>TANSTACK START.</p>
</div>
<div className="flex items-center justify-center gap-4">
{data?.user ? (
<p>Hello {data.user.name}</p>
) : (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink asChild>
<Link
to="/auth/signin"
className={navigationMenuTriggerStyle()}
activeProps={{ className: "bg-accent/50" }}
>
Sign In
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild>
<Link
to="/auth/signup"
className={navigationMenuTriggerStyle()}
activeProps={{ className: "bg-accent/50" }}
>
Sign Up
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)}
</div>
<div className="flex items-center gap-4 justify-center">
{data?.user && (
<Button
onClick={() =>
signOut(
{},
{
onError: (error) => {
console.warn(error);
toast.error(error.error.message);
},
onSuccess: () => {
toast.success("You have been signed out!");
},
},
)
}
variant="destructive"
>
<DoorOpen className="w-5 h-5" />
</Button>
)}
<Button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
{theme === "light" ? (
<Moon onClick={() => setTheme("dark")} className="w-5 h-5" />
) : (
<Sun onClick={() => setTheme("light")} className="w-5 h-5" />
)}
</Button>
</div>
</nav>
<Outlet />
</>
)}
<Toaster richColors position="bottom-center" />
</RootDocument>
);
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<Html>
<Head>
<Meta />
</Head>
<Body>
{children}
<ScrollRestoration />
<Scripts />
</Body>
</Html>
);
}

View File

@@ -0,0 +1,11 @@
import { createAPIFileRoute } from "@tanstack/start/api";
import { auth } from "~/lib/server/auth";
export const Route = createAPIFileRoute("/api/auth/$")({
GET: ({ request }) => {
return auth.handler(request);
},
POST: ({ request }) => {
return auth.handler(request);
},
});

View File

@@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { LoginForm } from "~/components/login-form";
export const Route = createFileRoute("/auth/signin")({
component: SignIn,
});
function SignIn() {
return (
<div className="container">
<LoginForm />
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { RegisterForm } from "~/components/register-form";
export const Route = createFileRoute("/auth/signup")({
component: SignUp,
});
function SignUp() {
return (
<div className="container">
<RegisterForm />
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { createFileRoute } from "@tanstack/react-router";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { useSession } from "~/lib/client/auth";
export const Route = createFileRoute("/")({
component: Home,
});
function Home() {
const { data } = useSession();
return (
<div className="container flex justify-center">
<Card className="w-fit">
{data?.user && (
<>
<CardHeader>
<CardTitle>Welcome, {data.user.name}!</CardTitle>
<CardDescription>
You are signed in as {data.user.email}.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2 justify-start">
<div className="flex flex-col">
<p>Created At</p>
<Input
readOnly
disabled
value={data.user.createdAt.toLocaleString()}
/>
<p>Session ID</p>
<Input readOnly disabled value={data.session.id} />
</div>
</CardContent>
</>
)}
</Card>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { getRouterManifest } from "@tanstack/start/router-manifest";
import {
createStartHandler,
defaultStreamHandler,
} from "@tanstack/start/server";
import { createRouter } from "./router";
export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler);

View File

@@ -0,0 +1,12 @@
import { defineConfig } from '@tanstack/start/config'
import viteTsConfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
vite: {
plugins: [
viteTsConfigPaths({
projects: ['./tsconfig.json']
}),
]
},
})

View File

@@ -0,0 +1,7 @@
create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" boolean not null, "image" text, "createdAt" date not null, "updatedAt" date not null);
create table "session" ("id" text not null primary key, "expiresAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id"));
create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "expiresAt" date, "password" text);
create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null)

View File

@@ -0,0 +1,30 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/lib/style/global.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "~/components",
"utils": "~/lib/utils"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,44 @@
{
"name": "tanstack-example",
"type": "module",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/better-sqlite3": "^7.6.11",
"@types/bun": "latest",
"@types/node": "^22.8.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"vite-tsconfig-paths": "^5.0.1"
},
"peerDependencies": {
"typescript": "^5.6.3"
},
"dependencies": {
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-router": "^1.77.6",
"@tanstack/start": "^1.77.6",
"@vitejs/plugin-react": "^4.3.3",
"better-auth": "^0.6.2",
"better-sqlite3": "^11.5.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.454.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"vinxi": "^0.4.3"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,85 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: [
'"GeistMono"',
"ui-monospace",
"SFMono-Regular",
"Roboto Mono",
"Menlo",
"Monaco",
"Liberation Mono",
"DejaVu Sans Mono",
"Courier New",
"monospace",
],
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
};

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"baseUrl": ".",
"paths": {
"~/*": ["app/*"]
}
},
"exclude": ["node_modules", ".output"]
}

1463
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff