Skip to content

Commit 2b71ef2

Browse files
Use next-themes in theme provider for React frontends, and fix neon setup (#407)
Co-authored-by: Aman Varshney <[email protected]>
1 parent b72a83f commit 2b71ef2

File tree

11 files changed

+88
-173
lines changed

11 files changed

+88
-173
lines changed

.changeset/spicy-eggs-rule.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-better-t-stack": patch
3+
---
4+
5+
Use next-themes in theme provider for React frontends, and fix neon setup

apps/cli/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export const dependencyVersionMap = {
3838
"@neondatabase/serverless": "^1.0.1",
3939
pg: "^8.14.1",
4040
"@types/pg": "^8.11.11",
41+
"@types/ws": "^8.18.1",
42+
ws: "^8.18.3",
4143

4244
mysql2: "^3.14.0",
4345

apps/cli/src/helpers/project-generation/template-manager.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export async function copyBaseTemplate(
5757
): Promise<void> {
5858
const templateDir = path.join(PKG_ROOT, "templates/base");
5959
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
60-
await fs.ensureDir(path.join(projectDir, "packages"));
6160
}
6261

6362
export async function setupFrontendTemplates(

apps/cli/src/helpers/setup/db-setup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
5151
} else if (database === "postgres") {
5252
if (dbSetup === "neon") {
5353
await addPackageDependency({
54-
dependencies: ["drizzle-orm", "@neondatabase/serverless"],
55-
devDependencies: ["drizzle-kit"],
54+
dependencies: ["drizzle-orm", "@neondatabase/serverless", "ws"],
55+
devDependencies: ["drizzle-kit", "@types/ws"],
5656
projectDir: serverDir,
5757
});
5858
} else {

apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{{#if (or (eq runtime "bun") (eq runtime "node"))}}
22
{{#if (eq dbSetup "neon")}}
3-
import { neon } from '@neondatabase/serverless';
3+
import { neon, neonConfig } from '@neondatabase/serverless';
44
import { drizzle } from 'drizzle-orm/neon-http';
5+
import ws from "ws";
6+
7+
neonConfig.webSocketConstructor = ws;
8+
9+
// To work in edge environments (Cloudflare Workers, Vercel Edge, etc.), enable querying over fetch
10+
// neonConfig.poolQueryViaFetch = true
511

612
const sql = neon(process.env.DATABASE_URL || "");
713
export const db = drizzle(sql);
@@ -14,9 +20,13 @@ export const db = drizzle(process.env.DATABASE_URL || "");
1420

1521
{{#if (eq runtime "workers")}}
1622
{{#if (eq dbSetup "neon")}}
17-
import { neon } from '@neondatabase/serverless';
23+
import { neon, neonConfig } from '@neondatabase/serverless';
1824
import { drizzle } from 'drizzle-orm/neon-http';
1925
import { env } from "cloudflare:workers";
26+
import ws from "ws";
27+
28+
neonConfig.webSocketConstructor = ws;
29+
neonConfig.poolQueryViaFetch = true;
2030

2131
const sql = neon(env.DATABASE_URL || "");
2232
export const db = drizzle(sql);
Lines changed: 5 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,11 @@
1-
import { createContext, useContext, useEffect, useState } from "react";
2-
3-
type Theme = "dark" | "light" | "system";
4-
5-
type ThemeProviderProps = {
6-
children: React.ReactNode;
7-
defaultTheme?: Theme;
8-
storageKey?: string;
9-
};
10-
11-
type ThemeProviderState = {
12-
theme: Theme;
13-
setTheme: (theme: Theme) => void;
14-
};
15-
16-
const initialState: ThemeProviderState = {
17-
theme: "system",
18-
setTheme: () => null,
19-
};
20-
21-
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
1+
import * as React from "react";
2+
import { ThemeProvider as NextThemesProvider } from "next-themes";
223

234
export function ThemeProvider({
245
children,
25-
defaultTheme = "system",
26-
storageKey = "vite-ui-theme",
276
...props
28-
}: ThemeProviderProps) {
29-
const [theme, setTheme] = useState<Theme>(
30-
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31-
);
32-
33-
useEffect(() => {
34-
const root = window.document.documentElement;
35-
36-
root.classList.remove("light", "dark");
37-
38-
if (theme === "system") {
39-
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40-
.matches
41-
? "dark"
42-
: "light";
43-
44-
root.classList.add(systemTheme);
45-
return;
46-
}
47-
48-
root.classList.add(theme);
49-
}, [theme]);
50-
51-
const value = {
52-
theme,
53-
setTheme: (theme: Theme) => {
54-
localStorage.setItem(storageKey, theme);
55-
setTheme(theme);
56-
},
57-
};
58-
59-
return (
60-
<ThemeProviderContext.Provider {...props} value={value}>
61-
{children}
62-
</ThemeProviderContext.Provider>
63-
);
7+
}: React.ComponentProps<typeof NextThemesProvider>) {
8+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
649
}
6510

66-
export const useTheme = () => {
67-
const context = useContext(ThemeProviderContext);
68-
69-
if (context === undefined)
70-
throw new Error("useTheme must be used within a ThemeProvider");
71-
72-
return context;
73-
};
11+
export { useTheme } from "next-themes";

apps/cli/templates/frontend/react/react-router/src/root.tsx.hbs

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,25 @@ import { Toaster } from "./components/ui/sonner";
1515
{{#if (eq backend "convex")}}
1616
import { ConvexProvider, ConvexReactClient } from "convex/react";
1717
{{else}}
18-
{{#unless (eq api "none")}}
18+
{{#unless (eq api "none")}}
1919
import { QueryClientProvider } from "@tanstack/react-query";
2020
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
21-
{{#if (eq api "orpc")}}
21+
{{#if (eq api "orpc")}}
2222
import { queryClient } from "./utils/orpc";
23-
{{/if}}
24-
{{#if (eq api "trpc")}}
23+
{{/if}}
24+
{{#if (eq api "trpc")}}
2525
import { queryClient } from "./utils/trpc";
26-
{{/if}}
27-
{{/unless}}
26+
{{/if}}
27+
{{/unless}}
2828
{{/if}}
2929

3030
export const links: Route.LinksFunction = () => [
31-
{
32-
rel: "preconnect",
33-
href: "https://fonts.googleapis.com",
34-
},
35-
{
36-
rel: "preconnect",
37-
href: "https://fonts.gstatic.com",
38-
crossOrigin: "anonymous",
39-
},
31+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
32+
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
4033
{
4134
rel: "stylesheet",
42-
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
35+
href:
36+
"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
4337
},
4438
];
4539

@@ -68,7 +62,12 @@ export default function App() {
6862
);
6963
return (
7064
<ConvexProvider client={convex}>
71-
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
65+
<ThemeProvider
66+
attribute="class"
67+
defaultTheme="dark"
68+
disableTransitionOnChange
69+
storageKey="vite-ui-theme"
70+
>
7271
<div className="grid grid-rows-[auto_1fr] h-svh">
7372
<Header />
7473
<Outlet />
@@ -82,13 +81,18 @@ export default function App() {
8281
export default function App() {
8382
return (
8483
<QueryClientProvider client={queryClient}>
85-
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
86-
<div className="grid grid-rows-[auto_1fr] h-svh">
87-
<Header />
88-
<Outlet />
89-
</div>
90-
<Toaster richColors />
91-
</ThemeProvider>
84+
<ThemeProvider
85+
attribute="class"
86+
defaultTheme="dark"
87+
disableTransitionOnChange
88+
storageKey="vite-ui-theme"
89+
>
90+
<div className="grid grid-rows-[auto_1fr] h-svh">
91+
<Header />
92+
<Outlet />
93+
</div>
94+
<Toaster richColors />
95+
</ThemeProvider>
9296
<ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
9397
</QueryClientProvider>
9498
);
@@ -97,7 +101,12 @@ export default function App() {
97101
export default function App() {
98102
return (
99103
<QueryClientProvider client={queryClient}>
100-
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
104+
<ThemeProvider
105+
attribute="class"
106+
defaultTheme="dark"
107+
disableTransitionOnChange
108+
storageKey="vite-ui-theme"
109+
>
101110
<div className="grid grid-rows-[auto_1fr] h-svh">
102111
<Header />
103112
<Outlet />
@@ -111,7 +120,12 @@ export default function App() {
111120
{{else}}
112121
export default function App() {
113122
return (
114-
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
123+
<ThemeProvider
124+
attribute="class"
125+
defaultTheme="dark"
126+
disableTransitionOnChange
127+
storageKey="vite-ui-theme"
128+
>
115129
<div className="grid grid-rows-[auto_1fr] h-svh">
116130
<Header />
117131
<Outlet />
@@ -126,7 +140,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
126140
let message = "Oops!";
127141
let details = "An unexpected error occurred.";
128142
let stack: string | undefined;
129-
130143
if (isRouteErrorResponse(error)) {
131144
message = error.status === 404 ? "404" : "Error";
132145
details =
@@ -137,7 +150,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
137150
details = error.message;
138151
stack = error.stack;
139152
}
140-
141153
return (
142154
<main className="pt-16 p-4 container mx-auto">
143155
<h1>{message}</h1>

apps/cli/templates/frontend/react/react-router/src/routes/_index.tsx.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const TITLE_TEXT = `
2929
`;
3030

3131
export function meta({}: Route.MetaArgs) {
32-
return [{ title: "My App" }, { name: "description", content: "My App" }];
32+
return [{ title: "{{projectName}}" }, { name: "description", content: "{{projectName}} is a web application" }];
3333
}
3434

3535
export default function Home() {

apps/cli/templates/frontend/react/tanstack-router/index.html renamed to apps/cli/templates/frontend/react/tanstack-router/index.html.hbs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>{{projectName}}</title>
67
</head>
78

89
<body>
Lines changed: 5 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,11 @@
1-
import { createContext, useContext, useEffect, useState } from "react";
2-
3-
type Theme = "dark" | "light" | "system";
4-
5-
type ThemeProviderProps = {
6-
children: React.ReactNode;
7-
defaultTheme?: Theme;
8-
storageKey?: string;
9-
};
10-
11-
type ThemeProviderState = {
12-
theme: Theme;
13-
setTheme: (theme: Theme) => void;
14-
};
15-
16-
const initialState: ThemeProviderState = {
17-
theme: "system",
18-
setTheme: () => null,
19-
};
20-
21-
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
1+
import * as React from "react";
2+
import { ThemeProvider as NextThemesProvider } from "next-themes";
223

234
export function ThemeProvider({
245
children,
25-
defaultTheme = "system",
26-
storageKey = "vite-ui-theme",
276
...props
28-
}: ThemeProviderProps) {
29-
const [theme, setTheme] = useState<Theme>(
30-
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31-
);
32-
33-
useEffect(() => {
34-
const root = window.document.documentElement;
35-
36-
root.classList.remove("light", "dark");
37-
38-
if (theme === "system") {
39-
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40-
.matches
41-
? "dark"
42-
: "light";
43-
44-
root.classList.add(systemTheme);
45-
return;
46-
}
47-
48-
root.classList.add(theme);
49-
}, [theme]);
50-
51-
const value = {
52-
theme,
53-
setTheme: (theme: Theme) => {
54-
localStorage.setItem(storageKey, theme);
55-
setTheme(theme);
56-
},
57-
};
58-
59-
return (
60-
<ThemeProviderContext.Provider {...props} value={value}>
61-
{children}
62-
</ThemeProviderContext.Provider>
63-
);
7+
}: React.ComponentProps<typeof NextThemesProvider>) {
8+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
649
}
6510

66-
export const useTheme = () => {
67-
const context = useContext(ThemeProviderContext);
68-
69-
if (context === undefined)
70-
throw new Error("useTheme must be used within a ThemeProvider");
71-
72-
return context;
73-
};
11+
export { useTheme } from "next-themes";

0 commit comments

Comments
 (0)