diff --git a/with-react-router/.dockerignore b/with-react-router/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/with-react-router/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/with-react-router/.env.example b/with-react-router/.env.example new file mode 100644 index 0000000..4238161 --- /dev/null +++ b/with-react-router/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://neondb_owner:...@ep-...us-east-1.aws.neon.tech/neondb?sslmode=require" diff --git a/with-react-router/.gitignore b/with-react-router/.gitignore new file mode 100644 index 0000000..9b7c041 --- /dev/null +++ b/with-react-router/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/with-react-router/Dockerfile b/with-react-router/Dockerfile new file mode 100644 index 0000000..207bf93 --- /dev/null +++ b/with-react-router/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/with-react-router/README.md b/with-react-router/README.md new file mode 100644 index 0000000..376a5e6 --- /dev/null +++ b/with-react-router/README.md @@ -0,0 +1,114 @@ + + +# Getting started with Neon and React Router + +## Clone the repository + +```bash +npx degit neondatabase/examples/with-react-router ./with-react-router +``` + +## Create a .env file + +Run the command below to copy the `.env.example` file: + +```bash +cp .env.example .env +``` + +## Store your Neon credentials + +Store your Neon credentials in the `.env` file. You can obtain the connection string for your database from the Connection Details widget on the Neon Dashboard. + +``` +DATABASE_URL="postgresql://neondb_owner:...@ep-...us-east-1.aws.neon.tech/neondb?sslmode=require" +``` + +- `user` is the database user. +- `password` is the database user’s password. +- `endpoint_hostname` is the host with neon.tech as the [TLD](https://www.cloudflare.com/en-gb/learning/dns/top-level-domain/). +- `dbname` is the name of the database. “neondb” is the default database created with each Neon project. +- `?sslmode=require` an optional query parameter that enforces the [SSL](https://www.cloudflare.com/en-gb/learning/ssl/what-is-ssl/) mode while connecting to the Postgres instance for better security. + +**Important**: To ensure the security of your data, never expose your Neon credentials to the browser. + +## Install dependencies + +Run the command below to install project dependencies: + +```bash +npm install +``` + +## Development + +Run the application using the following command: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +React Router can be deployed to many different deployment targets including Vercel, Netlify, Cloudflare Workers, and more. Check out [React Router's official templates on GitHub](https://github.com/remix-run/react-router-templates) for more information. This template is based on React Router's default template running a standard Node server. It also ships with example Docker files for dockerized deployments. + +### Docker Deployment + +This template includes three Dockerfiles optimized for different package managers: + +- `Dockerfile` - for npm +- `Dockerfile.pnpm` - for pnpm +- `Dockerfile.bun` - for bun + +To build and run using Docker: + +```bash +# For npm +docker build -t my-app . + +# For pnpm +docker build -f Dockerfile.pnpm -t my-app . + +# For bun +docker build -f Dockerfile.bun -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. diff --git a/with-react-router/app/app.css b/with-react-router/app/app.css new file mode 100644 index 0000000..c3758b7 --- /dev/null +++ b/with-react-router/app/app.css @@ -0,0 +1,11 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +html, +body { + @apply bg-gray-950; +} diff --git a/with-react-router/app/root.tsx b/with-react-router/app/root.tsx new file mode 100644 index 0000000..9fc6636 --- /dev/null +++ b/with-react-router/app/root.tsx @@ -0,0 +1,75 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +import type { Route } from "./+types/root"; +import "./app.css"; + +export const links: Route.LinksFunction = () => [ + { 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", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +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 = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/with-react-router/app/routes.ts b/with-react-router/app/routes.ts new file mode 100644 index 0000000..102b402 --- /dev/null +++ b/with-react-router/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from "@react-router/dev/routes"; + +export default [index("routes/home.tsx")] satisfies RouteConfig; diff --git a/with-react-router/app/routes/home.tsx b/with-react-router/app/routes/home.tsx new file mode 100644 index 0000000..69abdf1 --- /dev/null +++ b/with-react-router/app/routes/home.tsx @@ -0,0 +1,21 @@ +import { neon } from "@neondatabase/serverless"; +import type { Route } from "./+types/home"; +import { Welcome } from "../welcome/welcome"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "Neon with React Router" }, + { name: "description", content: "Welcome to React Router + Neon!" }, + ]; +} + +export async function loader({}: Route.ClientLoaderArgs) { + const sql = neon(`${process.env.DATABASE_URL}`); + const response = await sql`SELECT version()`; + const { version } = response[0]; + return { version }; +} + +export default function Home({ loaderData }: Route.ComponentProps) { + return ; +} diff --git a/with-react-router/app/welcome/welcome.tsx b/with-react-router/app/welcome/welcome.tsx new file mode 100644 index 0000000..3129f3a --- /dev/null +++ b/with-react-router/app/welcome/welcome.tsx @@ -0,0 +1,49 @@ +export function Welcome({ databaseVersion }: { databaseVersion: string }) { + return ( +
+
+
+
+ React Router +
+

+ Database version: {databaseVersion} +

+
+
+ +
+
+
+ ); +} + +const resources = [ + { + href: "https://reactrouter.com/docs", + text: "React Router Docs", + }, + { + href: "https://neon.tech/docs/introduction", + text: "Neon Docs", + }, +]; diff --git a/with-react-router/package.json b/with-react-router/package.json new file mode 100644 index 0000000..978675a --- /dev/null +++ b/with-react-router/package.json @@ -0,0 +1,31 @@ +{ + "type": "module", + "name": "with-react-router", + "scripts": { + "dev": "react-router dev", + "build": "react-router build", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@neondatabase/serverless": "^0.10.4", + "@react-router/node": "^7.1.4", + "@react-router/serve": "^7.1.4", + "isbot": "^5.1.17", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.1.4" + }, + "devDependencies": { + "@react-router/dev": "^7.1.4", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^20", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", + "react-router-devtools": "^1.1.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.2", + "vite": "^5.4.11", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/with-react-router/public/favicon.ico b/with-react-router/public/favicon.ico new file mode 100644 index 0000000..4dd289d Binary files /dev/null and b/with-react-router/public/favicon.ico differ diff --git a/with-react-router/react-router.config.ts b/with-react-router/react-router.config.ts new file mode 100644 index 0000000..6ff16f9 --- /dev/null +++ b/with-react-router/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/with-react-router/tsconfig.json b/with-react-router/tsconfig.json new file mode 100644 index 0000000..dc391a4 --- /dev/null +++ b/with-react-router/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": [ + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/with-react-router/vite.config.ts b/with-react-router/vite.config.ts new file mode 100644 index 0000000..efe1f79 --- /dev/null +++ b/with-react-router/vite.config.ts @@ -0,0 +1,12 @@ +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], + build: { + cssMinify: true, + ssr: false, + }, +});