Use next-themes in theme provider for React frontends, and fix neon setup (#407)

Co-authored-by: Aman Varshney <amanvarshney.work@gmail.com>
This commit is contained in:
Anmol
2025-07-20 08:36:09 +04:00
committed by GitHub
parent b72a83f13a
commit 2b71ef246c
11 changed files with 88 additions and 173 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
Use next-themes in theme provider for React frontends, and fix neon setup

View File

@@ -38,6 +38,8 @@ export const dependencyVersionMap = {
"@neondatabase/serverless": "^1.0.1", "@neondatabase/serverless": "^1.0.1",
pg: "^8.14.1", pg: "^8.14.1",
"@types/pg": "^8.11.11", "@types/pg": "^8.11.11",
"@types/ws": "^8.18.1",
ws: "^8.18.3",
mysql2: "^3.14.0", mysql2: "^3.14.0",

View File

@@ -57,7 +57,6 @@ export async function copyBaseTemplate(
): Promise<void> { ): Promise<void> {
const templateDir = path.join(PKG_ROOT, "templates/base"); const templateDir = path.join(PKG_ROOT, "templates/base");
await processAndCopyFiles(["**/*"], templateDir, projectDir, context); await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
await fs.ensureDir(path.join(projectDir, "packages"));
} }
export async function setupFrontendTemplates( export async function setupFrontendTemplates(

View File

@@ -51,8 +51,8 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
} else if (database === "postgres") { } else if (database === "postgres") {
if (dbSetup === "neon") { if (dbSetup === "neon") {
await addPackageDependency({ await addPackageDependency({
dependencies: ["drizzle-orm", "@neondatabase/serverless"], dependencies: ["drizzle-orm", "@neondatabase/serverless", "ws"],
devDependencies: ["drizzle-kit"], devDependencies: ["drizzle-kit", "@types/ws"],
projectDir: serverDir, projectDir: serverDir,
}); });
} else { } else {

View File

@@ -1,7 +1,13 @@
{{#if (or (eq runtime "bun") (eq runtime "node"))}} {{#if (or (eq runtime "bun") (eq runtime "node"))}}
{{#if (eq dbSetup "neon")}} {{#if (eq dbSetup "neon")}}
import { neon } from '@neondatabase/serverless'; import { neon, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http'; import { drizzle } from 'drizzle-orm/neon-http';
import ws from "ws";
neonConfig.webSocketConstructor = ws;
// To work in edge environments (Cloudflare Workers, Vercel Edge, etc.), enable querying over fetch
// neonConfig.poolQueryViaFetch = true
const sql = neon(process.env.DATABASE_URL || ""); const sql = neon(process.env.DATABASE_URL || "");
export const db = drizzle(sql); export const db = drizzle(sql);
@@ -14,9 +20,13 @@ export const db = drizzle(process.env.DATABASE_URL || "");
{{#if (eq runtime "workers")}} {{#if (eq runtime "workers")}}
{{#if (eq dbSetup "neon")}} {{#if (eq dbSetup "neon")}}
import { neon } from '@neondatabase/serverless'; import { neon, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http'; import { drizzle } from 'drizzle-orm/neon-http';
import { env } from "cloudflare:workers"; import { env } from "cloudflare:workers";
import ws from "ws";
neonConfig.webSocketConstructor = ws;
neonConfig.poolQueryViaFetch = true;
const sql = neon(env.DATABASE_URL || ""); const sql = neon(env.DATABASE_URL || "");
export const db = drizzle(sql); export const db = drizzle(sql);

View File

@@ -1,73 +1,11 @@
import { createContext, useContext, useEffect, useState } from "react"; import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({ export function ThemeProvider({
children, children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props ...props
}: ThemeProviderProps) { }: React.ComponentProps<typeof NextThemesProvider>) {
const [theme, setTheme] = useState<Theme>( return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
} }
root.classList.add(theme); export { useTheme } from "next-themes";
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -28,18 +28,12 @@ import { queryClient } from "./utils/trpc";
{{/if}} {{/if}}
export const links: Route.LinksFunction = () => [ export const links: Route.LinksFunction = () => [
{ { rel: "preconnect", href: "https://fonts.googleapis.com" },
rel: "preconnect", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
href: "https://fonts.googleapis.com",
},
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{ {
rel: "stylesheet", rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", href:
"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
}, },
]; ];
@@ -68,7 +62,12 @@ export default function App() {
); );
return ( return (
<ConvexProvider client={convex}> <ConvexProvider client={convex}>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh"> <div className="grid grid-rows-[auto_1fr] h-svh">
<Header /> <Header />
<Outlet /> <Outlet />
@@ -82,7 +81,12 @@ export default function App() {
export default function App() { export default function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh"> <div className="grid grid-rows-[auto_1fr] h-svh">
<Header /> <Header />
<Outlet /> <Outlet />
@@ -97,7 +101,12 @@ export default function App() {
export default function App() { export default function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh"> <div className="grid grid-rows-[auto_1fr] h-svh">
<Header /> <Header />
<Outlet /> <Outlet />
@@ -111,7 +120,12 @@ export default function App() {
{{else}} {{else}}
export default function App() { export default function App() {
return ( return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh"> <div className="grid grid-rows-[auto_1fr] h-svh">
<Header /> <Header />
<Outlet /> <Outlet />
@@ -126,7 +140,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!"; let message = "Oops!";
let details = "An unexpected error occurred."; let details = "An unexpected error occurred.";
let stack: string | undefined; let stack: string | undefined;
if (isRouteErrorResponse(error)) { if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error"; message = error.status === 404 ? "404" : "Error";
details = details =
@@ -137,7 +150,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
details = error.message; details = error.message;
stack = error.stack; stack = error.stack;
} }
return ( return (
<main className="pt-16 p-4 container mx-auto"> <main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1> <h1>{message}</h1>

View File

@@ -29,7 +29,7 @@ const TITLE_TEXT = `
`; `;
export function meta({}: Route.MetaArgs) { export function meta({}: Route.MetaArgs) {
return [{ title: "My App" }, { name: "description", content: "My App" }]; return [{ title: "{{projectName}}" }, { name: "description", content: "{{projectName}} is a web application" }];
} }
export default function Home() { export default function Home() {

View File

@@ -1,8 +1,9 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{projectName}}</title>
</head> </head>
<body> <body>

View File

@@ -1,73 +1,11 @@
import { createContext, useContext, useEffect, useState } from "react"; import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({ export function ThemeProvider({
children, children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props ...props
}: ThemeProviderProps) { }: React.ComponentProps<typeof NextThemesProvider>) {
const [theme, setTheme] = useState<Theme>( return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
} }
root.classList.add(theme); export { useTheme } from "next-themes";
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -45,11 +45,11 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
head: () => ({ head: () => ({
meta: [ meta: [
{ {
title: "My App", title: "{{projectName}}",
}, },
{ {
name: "description", name: "description",
content: "My App is a web application", content: "{{projectName}} is a web application",
}, },
], ],
links: [ links: [
@@ -75,7 +75,12 @@ function RootComponent() {
<> <>
<HeadContent /> <HeadContent />
{{#if (eq api "orpc")}} {{#if (eq api "orpc")}}
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh"> <div className="grid grid-rows-[auto_1fr] h-svh">
<Header /> <Header />
{isFetching ? <Loader /> : <Outlet />} {isFetching ? <Loader /> : <Outlet />}
@@ -83,7 +88,12 @@ function RootComponent() {
<Toaster richColors /> <Toaster richColors />
</ThemeProvider> </ThemeProvider>
{{else}} {{else}}
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh"> <div className="grid grid-rows-[auto_1fr] h-svh">
<Header /> <Header />
{isFetching ? <Loader /> : <Outlet />} {isFetching ? <Loader /> : <Outlet />}