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

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

View File

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

View File

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

View File

@@ -1,7 +1,13 @@
{{#if (or (eq runtime "bun") (eq runtime "node"))}}
{{#if (eq dbSetup "neon")}}
import { neon } from '@neondatabase/serverless';
import { neon, neonConfig } from '@neondatabase/serverless';
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 || "");
export const db = drizzle(sql);
@@ -14,9 +20,13 @@ export const db = drizzle(process.env.DATABASE_URL || "");
{{#if (eq runtime "workers")}}
{{#if (eq dbSetup "neon")}}
import { neon } from '@neondatabase/serverless';
import { neon, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import { env } from "cloudflare:workers";
import ws from "ws";
neonConfig.webSocketConstructor = ws;
neonConfig.poolQueryViaFetch = true;
const sql = neon(env.DATABASE_URL || "");
export const db = drizzle(sql);

View File

@@ -1,73 +1,11 @@
import { createContext, useContext, useEffect, useState } from "react";
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);
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (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);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
export { useTheme } from "next-themes";

View File

@@ -15,31 +15,25 @@ import { Toaster } from "./components/ui/sonner";
{{#if (eq backend "convex")}}
import { ConvexProvider, ConvexReactClient } from "convex/react";
{{else}}
{{#unless (eq api "none")}}
{{#unless (eq api "none")}}
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
{{#if (eq api "orpc")}}
{{#if (eq api "orpc")}}
import { queryClient } from "./utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
{{/if}}
{{#if (eq api "trpc")}}
import { queryClient } from "./utils/trpc";
{{/if}}
{{/unless}}
{{/if}}
{{/unless}}
{{/if}}
export const links: Route.LinksFunction = () => [
{
rel: "preconnect",
href: "https://fonts.googleapis.com",
},
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{
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 (
<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">
<Header />
<Outlet />
@@ -82,13 +81,18 @@ export default function App() {
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<div className="grid grid-rows-[auto_1fr] h-svh">
<Header />
<Outlet />
</div>
<Toaster richColors />
</ThemeProvider>
<ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh">
<Header />
<Outlet />
</div>
<Toaster richColors />
</ThemeProvider>
<ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
</QueryClientProvider>
);
@@ -97,7 +101,12 @@ export default function App() {
export default function App() {
return (
<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">
<Header />
<Outlet />
@@ -111,7 +120,12 @@ export default function App() {
{{else}}
export default function App() {
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">
<Header />
<Outlet />
@@ -126,7 +140,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
@@ -137,7 +150,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
details = error.message;
stack = error.stack;
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>

View File

@@ -29,7 +29,7 @@ const TITLE_TEXT = `
`;
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() {

View File

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

View File

@@ -1,73 +1,11 @@
import { createContext, useContext, useEffect, useState } from "react";
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);
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (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);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
export { useTheme } from "next-themes";

View File

@@ -45,11 +45,11 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
head: () => ({
meta: [
{
title: "My App",
title: "{{projectName}}",
},
{
name: "description",
content: "My App is a web application",
content: "{{projectName}} is a web application",
},
],
links: [
@@ -75,7 +75,12 @@ function RootComponent() {
<>
<HeadContent />
{{#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">
<Header />
{isFetching ? <Loader /> : <Outlet />}
@@ -83,7 +88,12 @@ function RootComponent() {
<Toaster richColors />
</ThemeProvider>
{{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">
<Header />
{isFetching ? <Loader /> : <Outlet />}