├── .gitignore ├── .prettierrc ├── postcss.config.js ├── README.md ├── app ├── db │ └── index.ts ├── locales │ ├── en │ │ └── translation.json │ └── fr │ │ └── translation.json ├── api.ts ├── client.tsx ├── ssr.tsx ├── services │ ├── auth │ │ ├── redis-session.ts │ │ ├── schemas.ts │ │ └── index.ts │ └── posts.ts ├── middleware.tsx ├── components │ ├── NotFound.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── form.tsx │ │ └── dropdown-menu.tsx │ ├── LocaleToggle.tsx │ ├── DefaultCatchBoundary.tsx │ └── theme │ │ ├── ThemeScript.ts │ │ └── ThemeProvider.tsx ├── lib │ ├── utils.ts │ └── i18n.ts ├── routes │ ├── _auth.tsx │ ├── index.tsx │ ├── __root.tsx │ └── auth │ │ ├── login.tsx │ │ └── register.tsx ├── router.tsx ├── styles │ └── globals.css └── routeTree.gen.ts ├── env.d.ts ├── .env.example ├── components.json ├── tsconfig.json ├── .eslintrc.cjs ├── app.config.ts ├── prisma └── schema.prisma ├── tailwind.config.js ├── package.json └── i18next-parser.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .vinxi 4 | .vercel 5 | .nitro 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": true, 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TanStack starter 2 | 3 | This is a small starter for those wanting to jump-start their project with the new TanStack Start. 4 | -------------------------------------------------------------------------------- /app/db/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | export default prisma; 6 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | import type { Auth } from '@/lib/auth/types'; 2 | 3 | declare module 'vinxi/http' { 4 | interface H3EventContext { 5 | auth: Auth; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Home": { 3 | "title": "Welcome to TanStack starter!" 4 | }, 5 | "language.en": "English", 6 | "language.fr": "French" 7 | } 8 | -------------------------------------------------------------------------------- /app/locales/fr/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Home": { 3 | "title": "Bienvenue dans le starter de TanStack !" 4 | }, 5 | "language.en": "Anglais", 6 | "language.fr": "Français" 7 | } 8 | -------------------------------------------------------------------------------- /app/api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createStartAPIHandler, 3 | defaultAPIFileRouteHandler, 4 | } from '@tanstack/start/api'; 5 | 6 | export default createStartAPIHandler(defaultAPIFileRouteHandler); 7 | -------------------------------------------------------------------------------- /app/client.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { hydrateRoot } from 'react-dom/client'; 3 | import { StartClient } from '@tanstack/start'; 4 | import { createRouter } from './router'; 5 | 6 | const router = createRouter(); 7 | 8 | hydrateRoot(document.getElementById('root')!, ); 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # This is an example environment file. With Tanstack Start, any environment variables prefixed 2 | # by VITE_ will be exposed to your client-side code. Everything else will be server-side. 3 | DATABASE_URL="postgresql://user:pass@localhost:5432/foo" 4 | 5 | BETTER_AUTH_SECRET=my_super_secret_here 6 | BETTER_AUTH_URL=http://localhost:3000 7 | -------------------------------------------------------------------------------- /app/ssr.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { 3 | createStartHandler, 4 | defaultStreamHandler, 5 | } from '@tanstack/start/server'; 6 | import { getRouterManifest } from '@tanstack/start/router-manifest'; 7 | 8 | import { createRouter } from './router'; 9 | 10 | export default createStartHandler({ 11 | createRouter, 12 | getRouterManifest, 13 | })(defaultStreamHandler); 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/styles/app.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "hooks": "@/hooks", 18 | "lib": "@/lib" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/services/auth/redis-session.ts: -------------------------------------------------------------------------------- 1 | // import { createClient } from 'redis'; 2 | // import type { SecondaryStorage } from 'better-auth'; 3 | 4 | // const redis = createClient(); 5 | 6 | // const redisSecondaryStorage: SecondaryStorage = { 7 | // get: async (key) => await redis.get(key), 8 | // set: async (key, value, ttl) => { 9 | // if (ttl) await redis.set(key, value, { EX: ttl }); 10 | // else await redis.set(key, value); 11 | // }, 12 | // delete: async (key) => await redis.del(key), 13 | // }; 14 | 15 | // export default redisSecondaryStorage; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "jsx": "react-jsx", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "target": "ES2022", 13 | "allowJs": true, 14 | "baseUrl": ".", 15 | "strictNullChecks": true, 16 | "paths": { 17 | "@/*": ["./app/*"] 18 | } 19 | }, 20 | "include": ["**/*.ts", "**/*.tsx"] 21 | } 22 | -------------------------------------------------------------------------------- /app/middleware.tsx: -------------------------------------------------------------------------------- 1 | import { defineMiddleware } from 'vinxi/http'; 2 | import { auth } from '@/services/auth'; 3 | 4 | export default defineMiddleware({ 5 | onRequest: async (event) => { 6 | const session = await auth.api.getSession({ 7 | headers: event.headers, 8 | }); 9 | 10 | const authResult = !session 11 | ? { isAuthenticated: false, user: null, session: null } 12 | : { 13 | isAuthenticated: true, 14 | user: session.user, 15 | session: session.session, 16 | }; 17 | 18 | event.context.auth = authResult; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@tanstack/react-router'; 2 | import { Button } from './ui/button'; 3 | 4 | export function NotFound() { 5 | return ( 6 |
7 |

The page you are looking for does not exist.

8 |

9 | 12 | 15 |

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/services/auth/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const signUpEmailSchema = z.object({ 4 | name: z.string(), 5 | email: z.string().email(), 6 | password: z.string().min(8), 7 | }); 8 | 9 | type SignUpEmailInput = z.infer; 10 | 11 | const signInEmailSchema = z.object({ 12 | email: z.string().email(), 13 | password: z.string().min(8), 14 | }); 15 | 16 | type SignInEmailInput = z.infer; 17 | 18 | const authSchemas = { 19 | signUp: signUpEmailSchema, 20 | signIn: signInEmailSchema, 21 | }; 22 | 23 | export type { SignUpEmailInput, SignInEmailInput }; 24 | export { authSchemas }; 25 | -------------------------------------------------------------------------------- /app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | /** 9 | * Helper function to convert a string, Date, or object to a Date object. 10 | * This is useful in TanStack Start because currently all dates get serialized to strings. 11 | * @returns Date 12 | */ 13 | export function actualDate(dateOrString: object | Date | string): Date { 14 | if (typeof dateOrString === 'string') return new Date(dateOrString); 15 | if (typeof dateOrString === 'object') return dateOrString as Date; 16 | return dateOrString; 17 | } 18 | -------------------------------------------------------------------------------- /app/routes/_auth.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'; 2 | import { zodSearchValidator } from '@tanstack/router-zod-adapter'; 3 | import { z } from 'zod'; 4 | 5 | function AuthLayout() { 6 | return ( 7 | <> 8 | 9 | 10 | ); 11 | } 12 | 13 | export const Route = createFileRoute('/_auth')({ 14 | validateSearch: zodSearchValidator( 15 | z.object({ 16 | callbackUrl: z.string().default('/'), 17 | }), 18 | ), 19 | beforeLoad: async ({ context, search }) => { 20 | if (context.auth.isAuthenticated) { 21 | throw redirect({ 22 | code: 302, 23 | to: search.callbackUrl, 24 | }); 25 | } 26 | }, 27 | component: AuthLayout, 28 | }); 29 | -------------------------------------------------------------------------------- /app/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const labelVariants = cva( 10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = 'Input'; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /app/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'next-themes'; 2 | import { Toaster as Sonner } from 'sonner'; 3 | 4 | type ToasterProps = React.ComponentProps; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = 'system' } = useTheme(); 8 | 9 | return ( 10 | 26 | ); 27 | }; 28 | 29 | export { Toaster }; 30 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a custom ESLint configuration for use with 3 | * internal (bundled by their consumer) libraries 4 | * that utilize React. 5 | */ 6 | 7 | /** @type {import("eslint").Linter.Config} */ 8 | module.exports = { 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:react/recommended', 13 | 'plugin:react-hooks/recommended', 14 | 'prettier', 15 | ], 16 | // plugins: ['only-warn'], 17 | globals: { 18 | React: true, 19 | JSX: true, 20 | }, 21 | env: { 22 | browser: true, 23 | }, 24 | ignorePatterns: [ 25 | // Ignore dotfiles 26 | '.*.js', 27 | 'node_modules/', 28 | 'dist/', 29 | ], 30 | overrides: [ 31 | // Force ESLint to detect .tsx files 32 | { files: ['*.js?(x)', '*.ts?(x)'] }, 33 | ], 34 | rules: { 35 | // Disabled due to buggy-ness with Shadcn 36 | 'react/prop-types': 'off', 37 | 'no-console': 'warn', 38 | '@typescript-eslint/consistent-type-imports': 'error', 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /app/components/LocaleToggle.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { Button } from '@/components/ui/button'; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuTrigger, 8 | } from '@/components/ui/dropdown-menu'; 9 | import { supportedLocales } from '@/lib/i18n'; 10 | import { useTranslation } from 'react-i18next'; 11 | import { LucideLanguages } from 'lucide-react'; 12 | 13 | const LocaleToggle: FC = () => { 14 | const { t, i18n } = useTranslation(); 15 | 16 | return ( 17 | 18 | 19 | 22 | 23 | 24 | {supportedLocales.map((locale) => ( 25 | i18n.changeLanguage(locale)} 28 | > 29 | {t(`language.${locale}`, { defaultValue: locale })} 30 | 31 | ))} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default LocaleToggle; 38 | -------------------------------------------------------------------------------- /app/router.tsx: -------------------------------------------------------------------------------- 1 | import { createRouter as createTanStackRouter } from '@tanstack/react-router'; 2 | import { routerWithQueryClient } from '@tanstack/react-router-with-query'; 3 | import { routeTree } from './routeTree.gen'; 4 | import { notifyManager, QueryClient } from '@tanstack/react-query'; 5 | import { DefaultCatchBoundary } from '@/components/DefaultCatchBoundary'; 6 | import { NotFound } from '@/components/NotFound'; 7 | 8 | export function createRouter() { 9 | if (typeof document !== 'undefined') { 10 | notifyManager.setScheduler(window.requestAnimationFrame); 11 | } 12 | 13 | const queryClient: QueryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | refetchOnReconnect: () => !queryClient.isMutating(), 17 | }, 18 | }, 19 | }); 20 | 21 | return routerWithQueryClient( 22 | createTanStackRouter({ 23 | routeTree, 24 | context: { queryClient }, 25 | defaultPreload: 'intent', 26 | defaultErrorComponent: DefaultCatchBoundary, 27 | defaultNotFoundComponent: NotFound, 28 | }), 29 | queryClient, 30 | ); 31 | } 32 | 33 | declare module '@tanstack/react-router' { 34 | interface Register { 35 | router: ReturnType; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import LocaleToggle from '@/components/LocaleToggle'; 4 | import { Button } from '@/components/ui/button'; 5 | import { useAuthQuery } from '@/services/auth'; 6 | import { createFileRoute, Link } from '@tanstack/react-router'; 7 | 8 | const HeroHeader: FC = () => { 9 | const { data: auth } = useAuthQuery(); 10 | const { t } = useTranslation(); 11 | 12 | return ( 13 |
14 |
15 |
16 | 19 | 20 |
21 |
{t('Home.title', `Welcome to TanStack starter!`)}
22 |
23 |
{JSON.stringify(auth, null, 2)}
24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | function Home() { 31 | return ; 32 | } 33 | 34 | export const Route = createFileRoute('/')({ 35 | component: Home, 36 | }); 37 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import path from 'path'; 3 | import { defineConfig } from '@tanstack/start/config'; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | import { type App } from 'vinxi'; 6 | 7 | const require = createRequire(import.meta.url); 8 | const prismaClientDirectory = path.normalize( 9 | path.relative( 10 | process.cwd(), 11 | require 12 | .resolve('@prisma/client') 13 | .replace(/@prisma(\/|\\)client(\/|\\).*/, '.prisma/client'), 14 | ), 15 | ); 16 | const prismaIndexBrowserPath = path.join( 17 | prismaClientDirectory, 18 | 'index-browser.js', 19 | ); 20 | 21 | const startConfig = defineConfig({ 22 | server: { 23 | preset: 'vercel', 24 | }, 25 | vite: { 26 | plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })], 27 | resolve: { 28 | alias: { 29 | '.prisma/client/index-browser': prismaIndexBrowserPath, 30 | }, 31 | }, 32 | }, 33 | }); 34 | 35 | const routers = startConfig.config.routers.map((r) => { 36 | return { 37 | ...r, 38 | middleware: r.target === 'server' ? './app/middleware.tsx' : undefined, 39 | }; 40 | }); 41 | 42 | const app: App = { 43 | ...startConfig, 44 | config: { 45 | ...startConfig.config, 46 | routers: routers, 47 | }, 48 | }; 49 | 50 | export default app; 51 | -------------------------------------------------------------------------------- /app/components/DefaultCatchBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorComponent, 3 | type ErrorComponentProps, 4 | Link, 5 | rootRouteId, 6 | useMatch, 7 | useRouter, 8 | } from '@tanstack/react-router'; 9 | import { Button } from './ui/button'; 10 | 11 | export function DefaultCatchBoundary({ error }: ErrorComponentProps) { 12 | const router = useRouter(); 13 | const isRoot = useMatch({ 14 | strict: false, 15 | select: (state) => state.id === rootRouteId, 16 | }); 17 | 18 | console.error(error); 19 | 20 | return ( 21 |
22 | 23 |
24 | 32 | {isRoot ? ( 33 | 36 | ) : ( 37 | 48 | )} 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/lib/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createServerFn } from '@tanstack/start'; 2 | import i18n from 'i18next'; 3 | import { initReactI18next } from 'react-i18next'; 4 | import { getHeader } from 'vinxi/http'; 5 | import { z } from 'zod'; 6 | import { zodI18nMap } from 'zod-i18n-map'; 7 | 8 | import en from '@/locales/en/translation.json'; 9 | import fr from '@/locales/fr/translation.json'; 10 | 11 | const resources = { 12 | en: { translation: en }, 13 | fr: { translation: fr }, 14 | } as const; 15 | 16 | type SupportedLocale = keyof typeof resources; 17 | 18 | const supportedLocales = Object.keys(resources) as SupportedLocale[]; 19 | 20 | type Locale = (typeof supportedLocales)[number]; 21 | 22 | const defaultLocale: Locale = 'en'; 23 | 24 | const getClientLocale = (): string => { 25 | const userLanguage = 26 | 'userLanguage' in navigator ? navigator.userLanguage : null; 27 | 28 | return navigator.language || (userLanguage as string); 29 | }; 30 | 31 | const getLocale = createServerFn('GET', async () => { 32 | const header = getHeader('Accept-Language'); 33 | const languages = header?.split(',') ?? []; 34 | 35 | return ( 36 | supportedLocales.find((lang) => languages.includes(lang)) ?? defaultLocale 37 | ); 38 | }); 39 | 40 | i18n.use(initReactI18next).init({ 41 | fallbackLng: defaultLocale, 42 | supportedLngs: supportedLocales, 43 | debug: import.meta.env.DEV, 44 | lng: getClientLocale(), 45 | 46 | saveMissing: true, 47 | saveMissingTo: 'current', 48 | resources, 49 | }); 50 | 51 | z.setErrorMap(zodI18nMap); 52 | 53 | export { getLocale, supportedLocales }; 54 | export default i18n; 55 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | id String @id 18 | name String 19 | email String 20 | emailVerified Boolean 21 | image String? 22 | createdAt DateTime 23 | updatedAt DateTime 24 | Session Session[] 25 | Account Account[] 26 | 27 | @@unique([email]) 28 | @@map("user") 29 | } 30 | 31 | model Session { 32 | id String @id 33 | expiresAt DateTime 34 | ipAddress String? 35 | userAgent String? 36 | userId String 37 | users User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 | 39 | @@map("session") 40 | } 41 | 42 | model Account { 43 | id String @id 44 | accountId String 45 | providerId String 46 | userId String 47 | users User @relation(fields: [userId], references: [id], onDelete: Cascade) 48 | accessToken String? 49 | refreshToken String? 50 | idToken String? 51 | expiresAt DateTime? 52 | password String? 53 | 54 | @@map("account") 55 | } 56 | 57 | model Verification { 58 | id String @id 59 | identifier String 60 | value String 61 | expiresAt DateTime 62 | 63 | @@map("verification") 64 | } 65 | -------------------------------------------------------------------------------- /app/components/theme/ThemeScript.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is adapted from next-themes to work with tanstack start. 3 | next-themes can be found at https://github.com/pacocoursey/next-themes under the MIT license. 4 | */ 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | const ThemeScript: (...args: any[]) => void = ( 8 | attribute, 9 | storageKey, 10 | defaultTheme, 11 | forcedTheme, 12 | themes, 13 | value, 14 | enableSystem, 15 | enableColorScheme, 16 | ) => { 17 | const el = document.documentElement; 18 | const systemThemes = ['light', 'dark']; 19 | const isClass = attribute === 'class'; 20 | const classes = 21 | isClass && value 22 | ? themes.map((t: string | number) => value[t] || t) 23 | : themes; 24 | 25 | function updateDOM(theme: string) { 26 | if (isClass) { 27 | el.classList.remove(...classes); 28 | el.classList.add(theme); 29 | } else { 30 | el.setAttribute(attribute, theme); 31 | } 32 | 33 | setColorScheme(theme); 34 | } 35 | 36 | function setColorScheme(theme: string) { 37 | if (enableColorScheme && systemThemes.includes(theme)) { 38 | el.style.colorScheme = theme; 39 | } 40 | } 41 | 42 | function getSystemTheme() { 43 | return window.matchMedia('(prefers-color-scheme: dark)').matches 44 | ? 'dark' 45 | : 'light'; 46 | } 47 | 48 | if (forcedTheme) { 49 | updateDOM(forcedTheme); 50 | } else { 51 | try { 52 | const themeName = localStorage.getItem(storageKey) || defaultTheme; 53 | const isSystem = enableSystem && themeName === 'system'; 54 | const theme = isSystem ? getSystemTheme() : themeName; 55 | updateDOM(theme); 56 | } catch (e) { 57 | // 58 | } 59 | } 60 | }; 61 | 62 | export default ThemeScript; 63 | -------------------------------------------------------------------------------- /app/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | }, 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = 'Alert'; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = 'AlertTitle'; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = 'AlertDescription'; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; 60 | -------------------------------------------------------------------------------- /app/services/posts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains an example minimal service for Posts. 3 | */ 4 | 5 | // import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; 6 | // import { createServerFn } from "@tanstack/start"; 7 | // import { z } from "zod"; 8 | 9 | // import prisma from '@/db'; 10 | // import { 11 | // queryOptions, 12 | // } from '@tanstack/react-query'; 13 | // import { createServerFn } from '@tanstack/start'; 14 | 15 | // const getPosts = createServerFn('GET', async () => { 16 | // const all = await prisma.post.findMany(); 17 | 18 | // return all; 19 | // }); 20 | 21 | // const createPost = createServerFn("POST", async (data: z.infer) => { 22 | // return prisma.post.create({ 23 | // data: { 24 | // content: data.content, 25 | // author: { 26 | // connect: { 27 | // id: data.author 28 | // } 29 | // } 30 | // } 31 | // }) 32 | // }); 33 | 34 | // const postQueries = { 35 | // getAll: () => 36 | // queryOptions({ 37 | // queryKey: ['posts', 'all'], 38 | // queryFn: () => getPosts(), 39 | // }), 40 | // } as const; 41 | 42 | // const createPostSchema = z.object({ 43 | // content: z.string(), 44 | // author: z.number(), 45 | // }); 46 | 47 | // const useCreatePostMutation = () => { 48 | // const queryClient = useQueryClient(); 49 | // return useMutation>({ 50 | // mutationFn: createPost, 51 | // onMutate: async () => { 52 | // await queryClient.cancelQueries({ queryKey: ['posts'] }); 53 | // await queryClient.invalidateQueries({ queryKey: ['posts', 'all'] }); 54 | // } 55 | // }) 56 | // } 57 | 58 | // const postSchemas = { 59 | // createPost: createPostSchema, 60 | // } as const; 61 | 62 | // export { postQueries, postSchemas, useCreatePostMutation }; 63 | -------------------------------------------------------------------------------- /app/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 0 0% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 3.9%; 13 | --primary: 0 0% 9%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 0 0% 96.1%; 16 | --secondary-foreground: 0 0% 9%; 17 | --muted: 0 0% 96.1%; 18 | --muted-foreground: 0 0% 45.1%; 19 | --accent: 0 0% 96.1%; 20 | --accent-foreground: 0 0% 9%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 0 0% 89.8%; 24 | --input: 0 0% 89.8%; 25 | --ring: 0 0% 3.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 0 0% 3.9%; 36 | --foreground: 0 0% 98%; 37 | --card: 0 0% 3.9%; 38 | --card-foreground: 0 0% 98%; 39 | --popover: 0 0% 3.9%; 40 | --popover-foreground: 0 0% 98%; 41 | --primary: 0 0% 98%; 42 | --primary-foreground: 0 0% 9%; 43 | --secondary: 0 0% 14.9%; 44 | --secondary-foreground: 0 0% 98%; 45 | --muted: 0 0% 14.9%; 46 | --muted-foreground: 0 0% 63.9%; 47 | --accent: 0 0% 14.9%; 48 | --accent-foreground: 0 0% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 0 0% 98%; 51 | --border: 0 0% 14.9%; 52 | --input: 0 0% 14.9%; 53 | --ring: 0 0% 83.1%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center 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', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | outline: 16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 17 | secondary: 18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 19 | ghost: 'hover:bg-accent hover:text-accent-foreground', 20 | link: 'text-primary underline-offset-4 hover:underline', 21 | }, 22 | size: { 23 | default: 'h-10 px-4 py-2', 24 | sm: 'h-9 rounded-md px-3', 25 | lg: 'h-11 rounded-md px-8', 26 | icon: 'h-10 w-10', 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: 'default', 31 | size: 'default', 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : 'button'; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = 'Button'; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /app/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = 'Card'; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = 'CardHeader'; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = 'CardTitle'; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = 'CardDescription'; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = 'CardContent'; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = 'CardFooter'; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ['class'], 6 | content: [ 7 | './pages/**/*.{ts,tsx}', 8 | './components/**/*.{ts,tsx}', 9 | './app/**/*.{ts,tsx}', 10 | './src/**/*.{ts,tsx}', 11 | ], 12 | theme: { 13 | container: { 14 | center: 'true', 15 | padding: '2rem', 16 | screens: { 17 | '2xl': '1400px' 18 | } 19 | }, 20 | extend: { 21 | colors: { 22 | border: 'hsl(var(--border))', 23 | input: 'hsl(var(--input))', 24 | ring: 'hsl(var(--ring))', 25 | background: 'hsl(var(--background))', 26 | foreground: 'hsl(var(--foreground))', 27 | primary: { 28 | DEFAULT: 'hsl(var(--primary))', 29 | foreground: 'hsl(var(--primary-foreground))' 30 | }, 31 | secondary: { 32 | DEFAULT: 'hsl(var(--secondary))', 33 | foreground: 'hsl(var(--secondary-foreground))' 34 | }, 35 | destructive: { 36 | DEFAULT: 'hsl(var(--destructive))', 37 | foreground: 'hsl(var(--destructive-foreground))' 38 | }, 39 | muted: { 40 | DEFAULT: 'hsl(var(--muted))', 41 | foreground: 'hsl(var(--muted-foreground))' 42 | }, 43 | accent: { 44 | DEFAULT: 'hsl(var(--accent))', 45 | foreground: 'hsl(var(--accent-foreground))' 46 | }, 47 | popover: { 48 | DEFAULT: 'hsl(var(--popover))', 49 | foreground: 'hsl(var(--popover-foreground))' 50 | }, 51 | card: { 52 | DEFAULT: 'hsl(var(--card))', 53 | foreground: 'hsl(var(--card-foreground))' 54 | } 55 | }, 56 | borderRadius: { 57 | lg: 'var(--radius)', 58 | md: 'calc(var(--radius) - 2px)', 59 | sm: 'calc(var(--radius) - 4px)' 60 | }, 61 | fontFamily: { 62 | sans: ['Geist Variable', ...fontFamily.sans], 63 | mono: ['Geist Mono Variable', ...fontFamily.mono] 64 | }, 65 | keyframes: { 66 | 'accordion-down': { 67 | from: { 68 | height: '0' 69 | }, 70 | to: { 71 | height: 'var(--radix-accordion-content-height)' 72 | } 73 | }, 74 | 'accordion-up': { 75 | from: { 76 | height: 'var(--radix-accordion-content-height)' 77 | }, 78 | to: { 79 | height: '0' 80 | } 81 | } 82 | }, 83 | animation: { 84 | 'accordion-down': 'accordion-down 0.2s ease-out', 85 | 'accordion-up': 'accordion-up 0.2s ease-out' 86 | } 87 | } 88 | }, 89 | plugins: [require('tailwindcss-animate')], 90 | }; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tanstack-starter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vinxi dev", 9 | "build": "vinxi build", 10 | "start": "vinxi start", 11 | "lint": "eslint . --ext .ts,.tsx", 12 | "ui": "pnpm dlx shadcn@latest", 13 | "db:push": "prisma migrate dev", 14 | "db:generate": "prisma generate", 15 | "postinstall": "prisma generate", 16 | "messages:extract": "i18next --config i18next-parser.config.ts" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "@hookform/resolvers": "^3.9.1", 23 | "@prisma/client": "5.20.0", 24 | "@radix-ui/react-dropdown-menu": "^2.1.2", 25 | "@radix-ui/react-label": "^2.1.0", 26 | "@radix-ui/react-slot": "^1.1.0", 27 | "@tanstack/react-query": "^5.59.15", 28 | "@tanstack/react-query-devtools": "^5.59.15", 29 | "@tanstack/react-router": "^1.74.6", 30 | "@tanstack/react-router-with-query": "^1.74.6", 31 | "@tanstack/router-zod-adapter": "^1.76.1", 32 | "@tanstack/start": "^1.74.6", 33 | "@vitejs/plugin-react": "^4.3.2", 34 | "better-auth": "0.5.3-beta.7", 35 | "class-variance-authority": "^0.7.0", 36 | "clsx": "^2.1.1", 37 | "i18next": "^23.16.4", 38 | "lucide-react": "^0.447.0", 39 | "next-themes": "^0.3.0", 40 | "non.geist": "^1.0.3", 41 | "prisma": "^5.20.0", 42 | "react": "^18.3.1", 43 | "react-dom": "^18.3.1", 44 | "react-hook-form": "^7.53.1", 45 | "react-i18next": "^15.1.0", 46 | "sonner": "^1.5.0", 47 | "tailwind-merge": "^2.5.3", 48 | "tailwindcss-animate": "^1.0.7", 49 | "vinxi": "^0.4.3", 50 | "zod": "^3.23.8", 51 | "zod-i18n-map": "^2.27.0" 52 | }, 53 | "devDependencies": { 54 | "@types/react": "^18.3.11", 55 | "@types/react-dom": "^18.3.0", 56 | "@typescript-eslint/eslint-plugin": "^7.1.0", 57 | "@typescript-eslint/parser": "^7.1.0", 58 | "autoprefixer": "^10.4.20", 59 | "eslint": "^8.57.1", 60 | "eslint-config-airbnb": "19.0.4", 61 | "eslint-config-prettier": "^9.1.0", 62 | "eslint-plugin-import": "^2.25.3", 63 | "eslint-plugin-jsx-a11y": "^6.5.1", 64 | "eslint-plugin-only-warn": "^1.1.0", 65 | "eslint-plugin-react": "^7.37.1", 66 | "eslint-plugin-react-hooks": "^4.6.2", 67 | "globals": "^15.10.0", 68 | "i18next-parser": "^9.0.2", 69 | "postcss": "^8.4.47", 70 | "tailwindcss": "^3.4.13", 71 | "typescript": "^5.6.2", 72 | "vite-tsconfig-paths": "^5.0.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { 3 | createRootRouteWithContext, 4 | useRouteContext, 5 | } from '@tanstack/react-router'; 6 | import { Outlet, ScrollRestoration } from '@tanstack/react-router'; 7 | import { Body, Head, Html, Meta, Scripts } from '@tanstack/start'; 8 | import { ThemeProvider } from '@/components/theme/ThemeProvider'; 9 | import type { QueryClient } from '@tanstack/react-query'; 10 | import { getAuthQueryOptions } from '@/services/auth'; 11 | import { I18nextProvider } from 'react-i18next'; 12 | import i18n, { getLocale } from '@/lib/i18n'; 13 | 14 | import '@/lib/i18n'; 15 | import globalCss from '@/styles/globals.css?url'; 16 | import geist from 'non.geist?url'; 17 | import geistMono from 'non.geist/mono?url'; 18 | 19 | export const Route = createRootRouteWithContext<{ 20 | queryClient: QueryClient; 21 | }>()({ 22 | meta: () => [ 23 | { 24 | charSet: 'utf-8', 25 | }, 26 | { 27 | name: 'viewport', 28 | content: 'width=device-width, initial-scale=1', 29 | }, 30 | { 31 | title: 'TanStack Start Starter', 32 | }, 33 | ], 34 | component: RootComponent, 35 | links: () => [ 36 | { rel: 'stylesheet', href: globalCss }, 37 | { rel: 'stylesheet', href: geist }, 38 | { rel: 'stylesheet', href: geistMono }, 39 | ], 40 | scripts: () => 41 | import.meta.env.DEV 42 | ? [ 43 | { 44 | type: 'module', 45 | children: `import RefreshRuntime from "/_build/@react-refresh"; 46 | RefreshRuntime.injectIntoGlobalHook(window) 47 | window.$RefreshReg$ = () => {} 48 | window.$RefreshSig$ = () => (type) => type`, 49 | }, 50 | ] 51 | : [], 52 | beforeLoad: async ({ context }) => { 53 | const auth = await context.queryClient.ensureQueryData( 54 | getAuthQueryOptions(), 55 | ); 56 | 57 | // You could also get geolocation for the locale 58 | const locale = await getLocale(); 59 | 60 | return { auth, locale }; 61 | }, 62 | }); 63 | 64 | function RootComponent() { 65 | return ( 66 | 67 | 68 | 69 | ); 70 | } 71 | 72 | function RootDocument({ children }: { children: ReactNode }) { 73 | const { locale } = useRouteContext({ from: '__root__' }); 74 | 75 | return ( 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | {children} 84 | 85 | 86 | 87 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /app/services/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { createServerFn } from '@tanstack/start'; 2 | import type { Session, User } from 'better-auth/types'; 3 | import { 4 | queryOptions, 5 | useMutation, 6 | useQueryClient, 7 | useSuspenseQuery, 8 | } from '@tanstack/react-query'; 9 | import { useRouter } from '@tanstack/react-router'; 10 | import prisma from '@/db'; 11 | import type { 12 | SignInEmailInput, 13 | SignUpEmailInput, 14 | } from '@/services/auth/schemas'; 15 | import { authSchemas } from '@/services/auth/schemas'; 16 | import { betterAuth } from 'better-auth'; 17 | import { prismaAdapter } from 'better-auth/adapters/prisma'; 18 | import { getEvent } from 'vinxi/http'; 19 | 20 | const auth = betterAuth({ 21 | database: prismaAdapter(prisma, { provider: 'postgresql' }), 22 | emailAndPassword: { 23 | enabled: true, 24 | }, 25 | // Uncomment below line to enable Redis for caching sessions and ratelimiting 26 | // secondaryStorage: redisSecondaryStorage 27 | }); 28 | 29 | type InferAuthApi = 30 | API extends keyof typeof auth.api 31 | ? Parameters<(typeof auth.api)[API]>[0] 32 | : never; 33 | 34 | type Auth = 35 | | { isAuthenticated: false; user: null; session: null } 36 | | { isAuthenticated: true; user: User; session: Session }; 37 | 38 | const authKeys = { 39 | auth: () => ['auth'], 40 | }; 41 | 42 | const getAuthQueryOptions = () => { 43 | return queryOptions({ 44 | queryKey: authKeys.auth(), 45 | queryFn: () => getAuth(), 46 | }); 47 | }; 48 | 49 | const useAuthQuery = () => useSuspenseQuery(getAuthQueryOptions()); 50 | 51 | const useInvalidateAuth = () => { 52 | const router = useRouter(); 53 | const queryClient = useQueryClient(); 54 | 55 | return async () => { 56 | await queryClient.invalidateQueries(getAuthQueryOptions()); 57 | await router.invalidate(); 58 | }; 59 | }; 60 | 61 | const getAuth = createServerFn('GET', async (): Promise => { 62 | const event = getEvent(); 63 | return event.context.auth; 64 | }); 65 | 66 | const signUpEmail = createServerFn( 67 | 'POST', 68 | async (input: SignUpEmailInput, ctx) => { 69 | return await auth.api.signUpEmail({ 70 | headers: ctx.request.headers, 71 | body: input, 72 | asResponse: true, 73 | }); 74 | }, 75 | ); 76 | 77 | const signInEmail = createServerFn( 78 | 'POST', 79 | async (input: SignInEmailInput, ctx) => { 80 | return await auth.api.signInEmail({ 81 | headers: ctx.request.headers, 82 | body: input, 83 | asResponse: true, 84 | }); 85 | }, 86 | ); 87 | 88 | const signOut = createServerFn('POST', async (_, ctx) => { 89 | return await auth.api.signOut({ 90 | headers: ctx.request.headers, 91 | asResponse: true, 92 | }); 93 | }); 94 | 95 | const useSignUpMutation = () => { 96 | const invalidateAuth = useInvalidateAuth(); 97 | 98 | return useMutation({ 99 | mutationFn: signUpEmail, 100 | onSuccess: invalidateAuth, 101 | }); 102 | }; 103 | 104 | const useSignInMutation = () => { 105 | const invalidateAuth = useInvalidateAuth(); 106 | 107 | return useMutation({ 108 | mutationFn: signInEmail, 109 | onSuccess: invalidateAuth, 110 | }); 111 | }; 112 | 113 | const useSignOutMutation = () => { 114 | const invalidateAuth = useInvalidateAuth(); 115 | 116 | return useMutation({ 117 | mutationFn: signOut, 118 | onSuccess: invalidateAuth, 119 | }); 120 | }; 121 | 122 | export { 123 | auth, 124 | authSchemas, 125 | getAuth, 126 | getAuthQueryOptions, 127 | useAuthQuery, 128 | useInvalidateAuth, 129 | useSignInMutation, 130 | useSignOutMutation, 131 | useSignUpMutation, 132 | }; 133 | export type { Auth, InferAuthApi }; 134 | -------------------------------------------------------------------------------- /app/routeTree.gen.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | 5 | // @ts-nocheck 6 | 7 | // noinspection JSUnusedGlobalSymbols 8 | 9 | // This file is auto-generated by TanStack Router 10 | 11 | // Import Routes 12 | 13 | import { Route as rootRoute } from './routes/__root' 14 | import { Route as AuthImport } from './routes/_auth' 15 | import { Route as IndexImport } from './routes/index' 16 | import { Route as AuthRegisterImport } from './routes/auth/register' 17 | import { Route as AuthLoginImport } from './routes/auth/login' 18 | 19 | // Create/Update Routes 20 | 21 | const AuthRoute = AuthImport.update({ 22 | id: '/_auth', 23 | getParentRoute: () => rootRoute, 24 | } as any) 25 | 26 | const IndexRoute = IndexImport.update({ 27 | id: '/', 28 | path: '/', 29 | getParentRoute: () => rootRoute, 30 | } as any) 31 | 32 | const AuthRegisterRoute = AuthRegisterImport.update({ 33 | id: '/auth/register', 34 | path: '/auth/register', 35 | getParentRoute: () => rootRoute, 36 | } as any) 37 | 38 | const AuthLoginRoute = AuthLoginImport.update({ 39 | id: '/auth/login', 40 | path: '/auth/login', 41 | getParentRoute: () => rootRoute, 42 | } as any) 43 | 44 | // Populate the FileRoutesByPath interface 45 | 46 | declare module '@tanstack/react-router' { 47 | interface FileRoutesByPath { 48 | '/': { 49 | id: '/' 50 | path: '/' 51 | fullPath: '/' 52 | preLoaderRoute: typeof IndexImport 53 | parentRoute: typeof rootRoute 54 | } 55 | '/_auth': { 56 | id: '/_auth' 57 | path: '' 58 | fullPath: '' 59 | preLoaderRoute: typeof AuthImport 60 | parentRoute: typeof rootRoute 61 | } 62 | '/auth/login': { 63 | id: '/auth/login' 64 | path: '/auth/login' 65 | fullPath: '/auth/login' 66 | preLoaderRoute: typeof AuthLoginImport 67 | parentRoute: typeof rootRoute 68 | } 69 | '/auth/register': { 70 | id: '/auth/register' 71 | path: '/auth/register' 72 | fullPath: '/auth/register' 73 | preLoaderRoute: typeof AuthRegisterImport 74 | parentRoute: typeof rootRoute 75 | } 76 | } 77 | } 78 | 79 | // Create and export the route tree 80 | 81 | export interface FileRoutesByFullPath { 82 | '/': typeof IndexRoute 83 | '': typeof AuthRoute 84 | '/auth/login': typeof AuthLoginRoute 85 | '/auth/register': typeof AuthRegisterRoute 86 | } 87 | 88 | export interface FileRoutesByTo { 89 | '/': typeof IndexRoute 90 | '': typeof AuthRoute 91 | '/auth/login': typeof AuthLoginRoute 92 | '/auth/register': typeof AuthRegisterRoute 93 | } 94 | 95 | export interface FileRoutesById { 96 | __root__: typeof rootRoute 97 | '/': typeof IndexRoute 98 | '/_auth': typeof AuthRoute 99 | '/auth/login': typeof AuthLoginRoute 100 | '/auth/register': typeof AuthRegisterRoute 101 | } 102 | 103 | export interface FileRouteTypes { 104 | fileRoutesByFullPath: FileRoutesByFullPath 105 | fullPaths: '/' | '' | '/auth/login' | '/auth/register' 106 | fileRoutesByTo: FileRoutesByTo 107 | to: '/' | '' | '/auth/login' | '/auth/register' 108 | id: '__root__' | '/' | '/_auth' | '/auth/login' | '/auth/register' 109 | fileRoutesById: FileRoutesById 110 | } 111 | 112 | export interface RootRouteChildren { 113 | IndexRoute: typeof IndexRoute 114 | AuthRoute: typeof AuthRoute 115 | AuthLoginRoute: typeof AuthLoginRoute 116 | AuthRegisterRoute: typeof AuthRegisterRoute 117 | } 118 | 119 | const rootRouteChildren: RootRouteChildren = { 120 | IndexRoute: IndexRoute, 121 | AuthRoute: AuthRoute, 122 | AuthLoginRoute: AuthLoginRoute, 123 | AuthRegisterRoute: AuthRegisterRoute, 124 | } 125 | 126 | export const routeTree = rootRoute 127 | ._addFileChildren(rootRouteChildren) 128 | ._addFileTypes() 129 | 130 | /* prettier-ignore-end */ 131 | 132 | /* ROUTE_MANIFEST_START 133 | { 134 | "routes": { 135 | "__root__": { 136 | "filePath": "__root.tsx", 137 | "children": [ 138 | "/", 139 | "/_auth", 140 | "/auth/login", 141 | "/auth/register" 142 | ] 143 | }, 144 | "/": { 145 | "filePath": "index.tsx" 146 | }, 147 | "/_auth": { 148 | "filePath": "_auth.tsx" 149 | }, 150 | "/auth/login": { 151 | "filePath": "auth/login.tsx" 152 | }, 153 | "/auth/register": { 154 | "filePath": "auth/register.tsx" 155 | } 156 | } 157 | } 158 | ROUTE_MANIFEST_END */ 159 | -------------------------------------------------------------------------------- /app/routes/auth/login.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { Card, CardContent, CardFooter } from '@/components/ui/card'; 3 | import { 4 | Form, 5 | FormControl, 6 | FormField, 7 | FormItem, 8 | FormLabel, 9 | FormMessage, 10 | } from '@/components/ui/form'; 11 | import { Input } from '@/components/ui/input'; 12 | import { authSchemas, useSignInMutation } from '@/services/auth'; 13 | import { zodResolver } from '@hookform/resolvers/zod'; 14 | import { createFileRoute, useRouter } from '@tanstack/react-router'; 15 | import { Alert } from '@/components/ui/alert'; 16 | import { ArrowRight } from 'lucide-react'; 17 | import { useState } from 'react'; 18 | import { useForm } from 'react-hook-form'; 19 | import { toast } from 'sonner'; 20 | import type { z } from 'zod'; 21 | import { Link } from '@tanstack/react-router'; 22 | 23 | function Login() { 24 | const [error, setError] = useState(null); 25 | const router = useRouter(); 26 | const { mutateAsync } = useSignInMutation(); 27 | const form = useForm>({ 28 | resolver: zodResolver(authSchemas.signIn), 29 | defaultValues: { 30 | email: '', 31 | password: '', 32 | }, 33 | }); 34 | 35 | const onSubmit = form.handleSubmit(async (data) => { 36 | const signUpPromise = mutateAsync(data, { 37 | onSuccess: () => { 38 | router.navigate({ to: '/' }); 39 | }, 40 | onError: () => {}, 41 | }); 42 | 43 | toast.promise(signUpPromise, { 44 | loading: 'Signing in...', 45 | error: 'Failed to sign in', 46 | }); 47 | 48 | try { 49 | await signUpPromise; 50 | } catch (error) { 51 | try { 52 | if (!(error instanceof Error)) throw new Error('Unknown error'); 53 | const parsedError = JSON.parse(error.message); 54 | setError(parsedError.body.body.message); 55 | } catch (e) { 56 | setError('Failed to sign in'); 57 | } 58 | } 59 | }); 60 | 61 | return ( 62 |
63 |
64 |
65 |

Welcome back

66 | 67 | Sign back into your account 68 | 69 |
70 |
71 | 72 | 73 | 74 | {error && {error}} 75 |
76 | ( 80 | 81 | Email 82 | 83 | 88 | 89 | 90 | 91 | )} 92 | /> 93 |
94 |
95 | ( 99 | 100 | Password 101 | 102 | 103 | 104 | 105 | 106 | )} 107 | /> 108 |
109 |
110 | 111 | 112 | 113 |
114 |
115 | 116 | 122 |
123 |
124 | ); 125 | } 126 | 127 | export const Route = createFileRoute('/auth/login')({ 128 | component: Login, 129 | }); 130 | -------------------------------------------------------------------------------- /app/routes/auth/register.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { Card, CardContent, CardFooter } from '@/components/ui/card'; 3 | import { 4 | Form, 5 | FormControl, 6 | FormField, 7 | FormItem, 8 | FormLabel, 9 | FormMessage, 10 | } from '@/components/ui/form'; 11 | import { Input } from '@/components/ui/input'; 12 | import { authSchemas, useSignUpMutation } from '@/services/auth'; 13 | import { zodResolver } from '@hookform/resolvers/zod'; 14 | import { Link } from '@tanstack/react-router'; 15 | import { createFileRoute, useRouter } from '@tanstack/react-router'; 16 | import { ArrowRight } from 'lucide-react'; 17 | import { useForm } from 'react-hook-form'; 18 | import { toast } from 'sonner'; 19 | import type { z } from 'zod'; 20 | 21 | function Register() { 22 | const router = useRouter(); 23 | const { mutateAsync } = useSignUpMutation(); 24 | const form = useForm>({ 25 | resolver: zodResolver(authSchemas.signUp), 26 | defaultValues: { 27 | email: '', 28 | name: '', 29 | password: '', 30 | }, 31 | }); 32 | 33 | const onSubmit = form.handleSubmit(async (data) => { 34 | const signUpPromise = mutateAsync(data, { 35 | onSuccess: () => { 36 | router.navigate({ to: '/' }); 37 | }, 38 | }); 39 | 40 | toast.promise(signUpPromise, { 41 | loading: 'Creating account...', 42 | success: 'Account created successfully', 43 | error: 'Failed to create account', 44 | }); 45 | 46 | await signUpPromise; 47 | }); 48 | 49 | return ( 50 |
51 |
52 |
53 |

Get started on My app

54 | 55 | The fastest way to kickstart your TanStack application. 56 | 57 |
58 |
59 | 60 | 61 | 62 |
63 | ( 67 | 68 | Your name 69 | 70 | 71 | 72 | 73 | 74 | )} 75 | /> 76 |
77 |
78 | ( 82 | 83 | Email 84 | 85 | 90 | 91 | 92 | 93 | )} 94 | /> 95 |
96 |
97 | ( 101 | 102 | Password 103 | 104 | 105 | 106 | 107 | 108 | )} 109 | /> 110 |
111 |
112 | 113 | 114 | 115 |
116 |
117 | 118 | 119 | 125 |
126 |
127 | ); 128 | } 129 | 130 | export const Route = createFileRoute('/auth/register')({ 131 | component: Register, 132 | }); 133 | -------------------------------------------------------------------------------- /app/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type * as LabelPrimitive from '@radix-ui/react-label'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; 5 | import { Controller, FormProvider, useFormContext } from 'react-hook-form'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | import { Label } from '@/components/ui/label'; 9 | 10 | const Form = FormProvider; 11 | 12 | type FormFieldContextValue< 13 | TFieldValues extends FieldValues = FieldValues, 14 | TName extends FieldPath = FieldPath, 15 | > = { 16 | name: TName; 17 | }; 18 | 19 | const FormFieldContext = React.createContext( 20 | {} as FormFieldContextValue, 21 | ); 22 | 23 | const FormField = < 24 | TFieldValues extends FieldValues = FieldValues, 25 | TName extends FieldPath = FieldPath, 26 | >({ 27 | ...props 28 | }: ControllerProps) => { 29 | return ( 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | const useFormField = () => { 37 | const fieldContext = React.useContext(FormFieldContext); 38 | const itemContext = React.useContext(FormItemContext); 39 | const { getFieldState, formState } = useFormContext(); 40 | 41 | const fieldState = getFieldState(fieldContext.name, formState); 42 | 43 | if (!fieldContext) { 44 | throw new Error('useFormField should be used within '); 45 | } 46 | 47 | const { id } = itemContext; 48 | 49 | return { 50 | id, 51 | name: fieldContext.name, 52 | formItemId: `${id}-form-item`, 53 | formDescriptionId: `${id}-form-item-description`, 54 | formMessageId: `${id}-form-item-message`, 55 | ...fieldState, 56 | }; 57 | }; 58 | 59 | type FormItemContextValue = { 60 | id: string; 61 | }; 62 | 63 | const FormItemContext = React.createContext( 64 | {} as FormItemContextValue, 65 | ); 66 | 67 | const FormItem = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => { 71 | const id = React.useId(); 72 | 73 | return ( 74 | 75 |
76 | 77 | ); 78 | }); 79 | FormItem.displayName = 'FormItem'; 80 | 81 | const FormLabel = React.forwardRef< 82 | React.ElementRef, 83 | React.ComponentPropsWithoutRef 84 | >(({ className, ...props }, ref) => { 85 | const { error, formItemId } = useFormField(); 86 | 87 | return ( 88 |