website
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { startTransition } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { HydratedRouter } from "react-router/dom";
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(document, <HydratedRouter />);
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import { PassThrough } from "node:stream";
|
||||
import { createReadableStreamFromReadable } from "@react-router/node";
|
||||
import { isbot } from "isbot";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import type {
|
||||
ActionFunctionArgs,
|
||||
AppLoadContext,
|
||||
EntryContext,
|
||||
LoaderFunctionArgs,
|
||||
} from "react-router";
|
||||
import { isRouteErrorResponse, ServerRouter } from "react-router";
|
||||
import { notifyError } from "./lib/errors";
|
||||
|
||||
export const streamTimeout = 60 * 20 * 1_000;
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
reactRouterContext: 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
|
||||
) {
|
||||
return isbot(request.headers.get("user-agent") || "")
|
||||
? handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
reactRouterContext
|
||||
)
|
||||
: handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
reactRouterContext
|
||||
);
|
||||
}
|
||||
|
||||
function handleBotRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
reactRouterContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<ServerRouter
|
||||
context={reactRouterContext}
|
||||
url={request.url}
|
||||
// abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
{
|
||||
onAllReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
timeoutId = setTimeout(abort, streamTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
function handleBrowserRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
reactRouterContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<ServerRouter
|
||||
context={reactRouterContext}
|
||||
url={request.url}
|
||||
// abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
{
|
||||
onShellReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
timeoutId = setTimeout(abort, streamTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
export function handleError(
|
||||
error: any,
|
||||
{ request, params, context }: LoaderFunctionArgs | ActionFunctionArgs
|
||||
) {
|
||||
// https://github.com/remix-run/remix/discussions/8933
|
||||
if (request.signal.aborted || isRouteErrorResponse(error)) {
|
||||
return;
|
||||
}
|
||||
if (error?.["status"] === 404) {
|
||||
return;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
notifyError(error, "unhandled remix error");
|
||||
} else {
|
||||
console.error("error", error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { captureException, flush, init } from "sentries";
|
||||
|
||||
init({
|
||||
dsn: "https://3e3f1075fec9ee2de1e0f79026b5f734@o4508014272446464.ingest.de.sentry.io/4508014292697168",
|
||||
integrations: [],
|
||||
tracesSampleRate: 0.01,
|
||||
profilesSampleRate: 0.01,
|
||||
beforeSend(event) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return null;
|
||||
}
|
||||
if (process.env.BYTECODE_RUN) {
|
||||
return null;
|
||||
}
|
||||
if (event?.["name"] === "AbortError") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
});
|
||||
|
||||
export async function notifyError(error: any, msg?: string) {
|
||||
console.error(msg, error);
|
||||
captureException(error, { extra: { msg } });
|
||||
await flush(1000);
|
||||
}
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "AppError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const sleep = (ms: number): Promise<void> => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import "website/src/styles/globals.css";
|
||||
import { Route } from "./+types/root";
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "react-router";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html className="" lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
if (isRouteErrorResponse(error)) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-8 text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">
|
||||
{error.status} {error.statusText}
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600">{error.data}</p>
|
||||
</div>
|
||||
);
|
||||
} else if (error instanceof Error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-8 text-center max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl font-bold mb-4">Error</h1>
|
||||
<p className="text-lg text-gray-600 mb-6">{error.message}</p>
|
||||
<p className="text-sm text-gray-500 mb-2">The stack trace is:</p>
|
||||
<pre className="bg-gray-100 p-4 rounded-lg text-xs text-left overflow-auto max-w-full border">
|
||||
{error.stack}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<h1 className="text-4xl font-bold text-center">Unknown Error</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes() satisfies RouteConfig;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { redirect } from 'react-router';
|
||||
|
||||
export const loader = () => {
|
||||
return redirect('https://github.com/remorses/playwriter');
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { sleep } from "../lib/utils";
|
||||
import { Route } from "./+types/defer-example";
|
||||
|
||||
async function getProjectLocation() {
|
||||
return Promise.resolve().then(() => sleep(1000).then(() => "hi"));
|
||||
}
|
||||
|
||||
export async function loader({}: Route.LoaderArgs) {
|
||||
return {
|
||||
project: getProjectLocation(),
|
||||
};
|
||||
}
|
||||
|
||||
export default function ProjectRoute({ loaderData }: Route.ComponentProps) {
|
||||
const location = React.use(loaderData.project);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Let's locate your project</h1>
|
||||
<p>Your project is at {location}.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
@import "tailwindcss";
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.637 0.237 25.331);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.871 0.006 286.286);
|
||||
--ring: oklch(0.871 0.006 286.286);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.871 0.006 286.286);
|
||||
--sidebar-primary: oklch(0.37 0.013 285.805);
|
||||
--sidebar-primary-foreground: oklch(0.871 0.006 286.286);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.871 0.006 286.286);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.871 0.006 286.286);
|
||||
}
|
||||
.dark {
|
||||
--background: oklch(0.21 0.006 285.885);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.637 0.237 25.331);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.274 0.006 286.033);
|
||||
--input: oklch(0.274 0.006 286.033);
|
||||
--ring: oklch(0.442 0.017 285.786);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: #000;
|
||||
--sidebar-foreground: oklch(0.871 0.006 286.286);
|
||||
--sidebar-primary: oklch(0.37 0.013 285.805);
|
||||
--sidebar-primary-foreground: oklch(0.871 0.006 286.286);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.871 0.006 286.286);
|
||||
--sidebar-border: oklch(0.274 0.006 286.033);
|
||||
--sidebar-ring: oklch(0.442 0.017 285.786);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar styles: shadcn-inspired */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-muted);
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-muted-foreground);
|
||||
}
|
||||
Reference in New Issue
Block a user