mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
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:
5
.changeset/spicy-eggs-rule.md
Normal file
5
.changeset/spicy-eggs-rule.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Use next-themes in theme provider for React frontends, and fix neon setup
|
||||||
@@ -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",
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}, [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 = () => {
|
export { useTheme } from "next-themes";
|
||||||
const context = useContext(ThemeProviderContext);
|
|
||||||
|
|
||||||
if (context === undefined)
|
|
||||||
throw new Error("useTheme must be used within a ThemeProvider");
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -15,31 +15,25 @@ import { Toaster } from "./components/ui/sonner";
|
|||||||
{{#if (eq backend "convex")}}
|
{{#if (eq backend "convex")}}
|
||||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#unless (eq api "none")}}
|
{{#unless (eq api "none")}}
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
{{#if (eq api "orpc")}}
|
{{#if (eq api "orpc")}}
|
||||||
import { queryClient } from "./utils/orpc";
|
import { queryClient } from "./utils/orpc";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (eq api "trpc")}}
|
{{#if (eq api "trpc")}}
|
||||||
import { queryClient } from "./utils/trpc";
|
import { queryClient } from "./utils/trpc";
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
|
||||||
}, [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 = () => {
|
export { useTheme } from "next-themes";
|
||||||
const context = useContext(ThemeProviderContext);
|
|
||||||
|
|
||||||
if (context === undefined)
|
|
||||||
throw new Error("useTheme must be used within a ThemeProvider");
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 />}
|
||||||
|
|||||||
Reference in New Issue
Block a user