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",
|
||||
pg: "^8.14.1",
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/ws": "^8.18.1",
|
||||
ws: "^8.18.3",
|
||||
|
||||
mysql2: "^3.14.0",
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
Reference in New Issue
Block a user