diff --git a/.changeset/spicy-eggs-rule.md b/.changeset/spicy-eggs-rule.md new file mode 100644 index 0000000..83629e5 --- /dev/null +++ b/.changeset/spicy-eggs-rule.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +Use next-themes in theme provider for React frontends, and fix neon setup diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index fc20859..719d60e 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -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", diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index c9f8c88..bc7ad7b 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -57,7 +57,6 @@ export async function copyBaseTemplate( ): Promise { const templateDir = path.join(PKG_ROOT, "templates/base"); await processAndCopyFiles(["**/*"], templateDir, projectDir, context); - await fs.ensureDir(path.join(projectDir, "packages")); } export async function setupFrontendTemplates( diff --git a/apps/cli/src/helpers/setup/db-setup.ts b/apps/cli/src/helpers/setup/db-setup.ts index bd1401b..6ca567f 100644 --- a/apps/cli/src/helpers/setup/db-setup.ts +++ b/apps/cli/src/helpers/setup/db-setup.ts @@ -51,8 +51,8 @@ export async function setupDatabase(config: ProjectConfig): Promise { } 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 { diff --git a/apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs b/apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs index 732bdd7..3f381d1 100644 --- a/apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs +++ b/apps/cli/templates/db/drizzle/postgres/src/db/index.ts.hbs @@ -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); diff --git a/apps/cli/templates/frontend/react/react-router/src/components/theme-provider.tsx b/apps/cli/templates/frontend/react/react-router/src/components/theme-provider.tsx index 7b9eeb2..d962534 100644 --- a/apps/cli/templates/frontend/react/react-router/src/components/theme-provider.tsx +++ b/apps/cli/templates/frontend/react/react-router/src/components/theme-provider.tsx @@ -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(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( - () => (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 ( - - {children} - - ); +}: React.ComponentProps) { + return {children}; } -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"; diff --git a/apps/cli/templates/frontend/react/react-router/src/root.tsx.hbs b/apps/cli/templates/frontend/react/react-router/src/root.tsx.hbs index 76ad772..1660470 100644 --- a/apps/cli/templates/frontend/react/react-router/src/root.tsx.hbs +++ b/apps/cli/templates/frontend/react/react-router/src/root.tsx.hbs @@ -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 ( - +
@@ -82,13 +81,18 @@ export default function App() { export default function App() { return ( - -
-
- -
- -
+ +
+
+ +
+ +
); @@ -97,7 +101,12 @@ export default function App() { export default function App() { return ( - +
@@ -111,7 +120,12 @@ export default function App() { {{else}} export default function App() { return ( - +
@@ -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 (

{message}

diff --git a/apps/cli/templates/frontend/react/react-router/src/routes/_index.tsx.hbs b/apps/cli/templates/frontend/react/react-router/src/routes/_index.tsx.hbs index 0e98ba7..22979ef 100644 --- a/apps/cli/templates/frontend/react/react-router/src/routes/_index.tsx.hbs +++ b/apps/cli/templates/frontend/react/react-router/src/routes/_index.tsx.hbs @@ -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() { diff --git a/apps/cli/templates/frontend/react/tanstack-router/index.html b/apps/cli/templates/frontend/react/tanstack-router/index.html.hbs similarity index 83% rename from apps/cli/templates/frontend/react/tanstack-router/index.html rename to apps/cli/templates/frontend/react/tanstack-router/index.html.hbs index d7d639d..1f807b1 100644 --- a/apps/cli/templates/frontend/react/tanstack-router/index.html +++ b/apps/cli/templates/frontend/react/tanstack-router/index.html.hbs @@ -1,8 +1,9 @@ - + + {{projectName}} diff --git a/apps/cli/templates/frontend/react/tanstack-router/src/components/theme-provider.tsx b/apps/cli/templates/frontend/react/tanstack-router/src/components/theme-provider.tsx index 7b9eeb2..d962534 100644 --- a/apps/cli/templates/frontend/react/tanstack-router/src/components/theme-provider.tsx +++ b/apps/cli/templates/frontend/react/tanstack-router/src/components/theme-provider.tsx @@ -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(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( - () => (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 ( - - {children} - - ); +}: React.ComponentProps) { + return {children}; } -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"; diff --git a/apps/cli/templates/frontend/react/tanstack-router/src/routes/__root.tsx.hbs b/apps/cli/templates/frontend/react/tanstack-router/src/routes/__root.tsx.hbs index 258a240..03c3d2f 100644 --- a/apps/cli/templates/frontend/react/tanstack-router/src/routes/__root.tsx.hbs +++ b/apps/cli/templates/frontend/react/tanstack-router/src/routes/__root.tsx.hbs @@ -45,11 +45,11 @@ export const Route = createRootRouteWithContext()({ 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() { <> {{#if (eq api "orpc")}} - +
{isFetching ? : } @@ -83,7 +88,12 @@ function RootComponent() { {{else}} - +
{isFetching ? : }