diff --git a/.github/workflows/quality-gateway-pull-request.yml b/.github/workflows/quality-gateway-pull-request.yml
index b9759c8e..d153c2cf 100644
--- a/.github/workflows/quality-gateway-pull-request.yml
+++ b/.github/workflows/quality-gateway-pull-request.yml
@@ -17,7 +17,7 @@ jobs:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun lint
- - run: bun build:static
+ - run: bunx react-router build
biome:
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index d81b981b..5476a5ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,7 @@ node_modules
# playwright
test-results
playwright-report
+
+# React Router
+/.react-router/
+/build/
diff --git a/README.md b/README.md
index e3cce4ca..7b9ac013 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,16 @@ This WebApp is the main project of PodCodar, a learning community about programm
- 📖 [Remix docs](https://remix.run/docs)
+## Features
+
+- 🚀 Server-side rendering
+- ⚡️ Hot Module Replacement (HMR)
+- 📦 Asset bundling and optimization
+- 🔄 Data loading and mutations
+- 🔒 TypeScript by default
+- 🎉 TailwindCSS for styling
+- 📖 [React Router docs](https://reactrouter.com/)
+
## Development
### System Dependencies
diff --git a/app/cookies.server.ts b/app/cookies.server.ts
index 6d92ac22..d23cd861 100644
--- a/app/cookies.server.ts
+++ b/app/cookies.server.ts
@@ -1,5 +1,5 @@
import { MAX_COOKIE_AGE } from "@packages/contants";
-import { createCookie } from "@remix-run/cloudflare"; // or cloudflare/deno
+import { createCookie } from "react-router";
export const selectedTheme = createCookie("selected-theme", {
path: "/",
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
deleted file mode 100644
index c56bc718..00000000
--- a/app/entry.client.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * By default, Remix will handle hydrating your app on the client for you.
- * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
- * For more information, see https://remix.run/file-conventions/entry.client
- */
-
-import { RemixBrowser } from "@remix-run/react";
-import { StrictMode, startTransition } from "react";
-import { hydrateRoot } from "react-dom/client";
-
-startTransition(() => {
- hydrateRoot(
- document,
-
-
- ,
- );
-});
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index 36edf1f4..1701765b 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -1,35 +1,27 @@
-/**
- * By default, Remix will handle generating the HTTP Response for you.
- * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
- * For more information, see https://remix.run/file-conventions/entry.server
- */
-
-import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
-import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";
+import {
+ type AppLoadContext,
+ type EntryContext,
+ ServerRouter,
+} from "react-router";
const ABORT_DELAY = 5000;
export default async function handleRequest(
request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
+ status: number,
+ headers: Headers,
+ routerContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- loadContext: AppLoadContext,
+ _loadContext: AppLoadContext,
) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY);
const body = await renderToReadableStream(
- ,
+ ,
{
signal: controller.signal,
onError(error: unknown) {
@@ -37,7 +29,7 @@ export default async function handleRequest(
// Log streaming rendering errors from inside the shell
console.error(error);
}
- responseStatusCode = 500;
+ status = 500;
},
},
);
@@ -48,9 +40,6 @@ export default async function handleRequest(
await body.allReady;
}
- responseHeaders.set("Content-Type", "text/html");
- return new Response(body, {
- headers: responseHeaders,
- status: responseStatusCode,
- });
+ headers.set("Content-Type", "text/html");
+ return new Response(body, { headers, status });
}
diff --git a/app/root.tsx b/app/root.tsx
index eba83df3..c51edcaf 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,11 +1,9 @@
import type { Theme } from "@packages/utils/theme";
-import type {
- ActionFunctionArgs,
- LinksFunction,
- LoaderFunctionArgs,
-} from "@remix-run/cloudflare";
import {
+ type ActionFunctionArgs,
Links,
+ type LinksFunction,
+ type LoaderFunctionArgs,
Meta,
Outlet,
Scripts,
@@ -14,7 +12,7 @@ import {
redirect,
useLoaderData,
useRouteError,
-} from "@remix-run/react";
+} from "react-router";
import "./tailwind.css";
import Metadata from "@packages/components/Metadata";
diff --git a/app/routes.ts b/app/routes.ts
new file mode 100644
index 00000000..a4c86c3b
--- /dev/null
+++ b/app/routes.ts
@@ -0,0 +1,6 @@
+import { type RouteConfig, route } from "@react-router/dev/routes";
+import { flatRoutes } from "@react-router/fs-routes";
+
+const routes = await flatRoutes();
+
+export default [...routes, route("/*", "./catchall.tsx")] satisfies RouteConfig;
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 664c5e30..a2b0f1ae 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -5,11 +5,14 @@ import RoadmapSection from "@packages/components/RoadmapSection";
import TechSection from "@packages/components/TechSection";
import TestimonialSection from "@packages/components/TestimonialSection";
import WhyItWorksSection from "@packages/components/WhyItWorksSection";
-import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/cloudflare";
-import { useLoaderData } from "@remix-run/react";
import { description, title } from "@packages/config/site";
import { Database } from "@packages/repositories/db";
+import {
+ type LoaderFunctionArgs,
+ type MetaFunction,
+ useLoaderData,
+} from "react-router";
export const meta: MetaFunction = () => {
return [{ title }, { name: "description", content: description }];
diff --git a/app/routes/team.tsx b/app/routes/team.tsx
index 382d0f25..7d155f60 100644
--- a/app/routes/team.tsx
+++ b/app/routes/team.tsx
@@ -1,10 +1,8 @@
import TeamPage from "@packages/components/TeamPage";
import { Database } from "@packages/repositories/db";
-import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
-import { useLoaderData } from "@remix-run/react";
+import { type LoaderFunctionArgs, useLoaderData } from "react-router";
export async function loader({ context }: LoaderFunctionArgs) {
- console.log(context);
const db = new Database(
context.cloudflare.env.TURSO_CONNECTION_URL,
context.cloudflare.env.TURSO_AUTH_TOKEN,
diff --git a/bun.lockb b/bun.lockb
index 3d5f4ddf..f1d480a3 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/functions/[[path]].ts b/functions/[[path]].ts
index 41877c61..324175ce 100644
--- a/functions/[[path]].ts
+++ b/functions/[[path]].ts
@@ -1,10 +1,9 @@
-import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
+import { createRequestHandler } from "@react-router/cloudflare";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore - the server build file is generated by `remix vite:build`
-// eslint-disable-next-line import/no-unresolved
+// @ts-ignore - the server build file is generated by `react-router build`
import * as build from "../build/server";
-export const onRequest = createPagesFunctionHandler({
+export const onRequest = createRequestHandler({
build,
});
diff --git a/load-context.ts b/load-context.ts
index 6a0eb6bf..96c09202 100644
--- a/load-context.ts
+++ b/load-context.ts
@@ -1,9 +1,9 @@
-import type { AppLoadContext } from "@remix-run/cloudflare";
+import type { AppLoadContext } from "react-router";
import type { PlatformProxy } from "wrangler";
type Cloudflare = Omit, "dispose">;
-declare module "@remix-run/cloudflare" {
+declare module "react-router" {
interface AppLoadContext {
cloudflare: Cloudflare;
extra: string; // augmented
diff --git a/package.json b/package.json
index 9b51534a..d8223ecd 100644
--- a/package.json
+++ b/package.json
@@ -6,62 +6,64 @@
"sideEffects": false,
"type": "module",
"scripts": {
- "dev": "bun run typegen && remix vite:dev",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
+ "dev": "bun run typegen && react-router dev",
"prebuild": "bun run typegen && bun run db:migrate",
- "build": "remix vite:build",
+ "build": "react-router build",
"deploy": "bun run build && wrangler pages deploy",
"lint": "biome lint",
"fmt": "biome check --write ",
"encrypt": "gpg -c .env",
- "decrypt": "gpg -d .env.gpg > .env && cp .env .dev.vars",
+ "decrypt": "gpg -d .env.gpg > .env && cp .env .dev.vars && wrangler types",
"test": "bun test packages app",
"e2e": "playwright test",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
- "typegen": "wrangler types && drizzle-kit generate",
"preview": "bun run build && wrangler pages dev",
"start": "wrangler pages dev ./build/client",
+ "typegen": "drizzle-kit generate && react-router typegen",
"typecheck": "tsc"
},
"dependencies": {
"@libsql/client": "^0.14.0",
- "@remix-run/cloudflare": "^2.15.2",
- "@remix-run/cloudflare-pages": "^2.15.2",
- "@remix-run/react": "^2.15.2",
- "@remix-run/serve": "^2.15.2",
- "daisyui": "^4.12.22",
+ "@react-router/cloudflare": "^7.1.1",
+ "@react-router/fs-routes": "^7.1.1",
+ "@react-router/node": "^7.1.1",
+ "@react-router/serve": "^7.1.1",
+ "daisyui": "^4.12.23",
"dotenv": "^16.4.7",
- "drizzle-orm": "^0.38.2",
- "i18next": "^24.2.0",
- "isbot": "^4.1.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-i18next": "^15.2.0"
+ "drizzle-orm": "^0.38.3",
+ "i18next": "^24.2.1",
+ "isbot": "^5.1.21",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-i18next": "^15.4.0",
+ "react-router": "^7.1.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@playwright/test": "^1.49.1",
- "@remix-run/dev": "^2.15.2",
- "@types/react": "^18.2.20",
- "@types/react-dom": "^18.2.7",
- "autoprefixer": "^10.4.19",
+ "@react-router/dev": "^7.1.1",
+ "@types/node": "^20.17.13",
+ "@types/react": "^19.0.7",
+ "@types/react-dom": "^19.0.3",
+ "autoprefixer": "^10.4.20",
"drizzle-kit": "^0.30.1",
- "lefthook": "^1.10.0",
- "lint-staged": "^15.2.11",
+ "lefthook": "^1.10.7",
+ "lint-staged": "^15.3.0",
"mermaid": "^11.4.1",
- "postcss": "^8.4.38",
- "tailwindcss": "^3.4.4",
- "typescript": "^5.1.6",
- "vite": "^5.1.0",
- "vite-tsconfig-paths": "^4.2.1",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.3",
+ "vite": "^5.4.11",
+ "vite-tsconfig-paths": "^4.3.2",
"vitepress": "^1.5.0",
"vitepress-carbon": "^1.5.0",
"vitepress-plugin-mermaid": "^2.0.17",
- "wrangler": "^3.99.0"
+ "wrangler": "^3.102.0"
},
"lint-staged": {
"*.{md,mdx}": "bunx prettier --write",
diff --git a/packages/components/Link.tsx b/packages/components/Link.tsx
index 5449e828..bfb18d7c 100644
--- a/packages/components/Link.tsx
+++ b/packages/components/Link.tsx
@@ -1,4 +1,4 @@
-import { Link as DefaulLink, type LinkProps } from "@remix-run/react";
+import { Link as DefaulLink, type LinkProps } from "react-router";
type Props = Omit & {
children: React.ReactNode;
diff --git a/packages/components/NavBar.tsx b/packages/components/NavBar.tsx
index ac21c4e0..cdf1689f 100644
--- a/packages/components/NavBar.tsx
+++ b/packages/components/NavBar.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { useState } from "react";
import { Logo } from "@packages/components/icons";
diff --git a/packages/components/ToggleThemeButton.tsx b/packages/components/ToggleThemeButton.tsx
index abedd4b9..7073d056 100644
--- a/packages/components/ToggleThemeButton.tsx
+++ b/packages/components/ToggleThemeButton.tsx
@@ -1,8 +1,6 @@
-"use client";
-
import { type Theme, strToTheme, toggleTheme } from "@packages/utils/theme";
-import { Form } from "@remix-run/react";
import { useState } from "react";
+import { Form } from "react-router";
export default function ToggleThemeButton() {
const [colorMode, setColorMode] = useState(
diff --git a/react-router.config.ts b/react-router.config.ts
new file mode 100644
index 00000000..504439d9
--- /dev/null
+++ b/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/tsconfig.json b/tsconfig.json
index c615d4ac..c2ef94d4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,38 +1,28 @@
{
"include": [
- "**/*.ts",
- "**/*.tsx",
- "**/.server/**/*.ts",
- "**/.server/**/*.tsx",
- "**/.client/**/*.ts",
- "**/.client/**/*.tsx"
+ "**/*",
+ "**/.server/**/*",
+ "**/.client/**/*",
+ ".react-router/types/**/*"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
- "types": [
- "vite/client",
- "@remix-run/cloudflare",
- "@cloudflare/workers-types/2023-07-01"
- ],
- "isolatedModules": true,
- "esModuleInterop": true,
- "jsx": "react-jsx",
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "resolveJsonModule": true,
+ "types": ["node", "vite/client"],
"target": "ES2022",
- "strict": true,
- "allowJs": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"@packages/*": ["./packages/*"],
- "f/*": ["./functions/*"],
"~/*": ["./app/*"]
},
-
- // Vite takes care of building everything, not tsc.
- "noEmit": true
+ "esModuleInterop": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true
}
}
diff --git a/vite.config.ts b/vite.config.ts
index 0022448f..7f32a878 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,6 @@
import viteYaml from "@modyfi/vite-plugin-yaml";
-import {
- vitePlugin as remix,
- cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
-} from "@remix-run/dev";
+import { reactRouter } from "@react-router/dev/vite";
+import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
@@ -10,24 +8,9 @@ import { getLoadContext } from "./load-context";
export default defineConfig({
plugins: [
- remixCloudflareDevProxy({
- getLoadContext,
- }),
+ cloudflareDevProxy({ getLoadContext }),
viteYaml(),
- remix({
- future: {
- v3_fetcherPersist: true,
- v3_relativeSplatPath: true,
- v3_throwAbortReason: true,
- v3_singleFetch: true,
- v3_lazyRouteDiscovery: true,
- },
- routes(defineRoutes) {
- return defineRoutes((route) => {
- route("/*", "catchall.tsx");
- });
- },
- }),
+ reactRouter(),
tsconfigPaths(),
],
});
diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts
index 2ae67591..6a207551 100644
--- a/worker-configuration.d.ts
+++ b/worker-configuration.d.ts
@@ -3,4 +3,6 @@
interface Env {
TURSO_CONNECTION_URL: string;
TURSO_AUTH_TOKEN: string;
+ FLY_API_TOKEN: string;
+ CR_PAT: string;
}