This commit is contained in:
2025-09-22 18:24:52 +05:30
commit 26ea6d17b1
25 changed files with 5255 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
import Typography from '@mui/material/Typography';
import MuiLink from '@mui/material/Link';
export default function Copyright() {
return (
<Typography
variant="body2"
align="center"
sx={{
color: 'text.secondary',
}}
>
{'Copyright © '}
<MuiLink color="inherit" href="https://mui.com/">
Your Website
</MuiLink>{' '}
{new Date().getFullYear()}.
</Typography>
);
}

23
app/components/ProTip.tsx Normal file
View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import Link from '@mui/material/Link';
import SvgIcon, { type SvgIconProps } from '@mui/material/SvgIcon';
import Typography from '@mui/material/Typography';
function LightBulbIcon(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z" />
</SvgIcon>
);
}
export default function ProTip() {
return (
<Typography sx={{ mt: 6, mb: 3, color: 'text.secondary' }}>
<LightBulbIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
{'Pro tip: See more '}
<Link href="https://mui.com/material-ui/getting-started/templates/">templates</Link>
{' in the Material UI documentation.'}
</Typography>
);
}

15
app/createCache.ts Normal file
View File

@@ -0,0 +1,15 @@
import createCache from '@emotion/cache';
export default function createEmotionCache(options?: Parameters<typeof createCache>[0]) {
const emotionCache = createCache({ key: 'mui', ...options });
const prevInsert = emotionCache.insert;
emotionCache.insert = (...args) => {
// ignore styles that contain layer order (`@layer ...` without `{`)
if (!args[1].styles.match(/^@layer\s+[^{]*$/)) {
args[1].styles = `@layer mui {${args[1].styles}}`;
}
return prevInsert(...args);
};
return emotionCache;
}

12
app/entry.client.tsx Normal file
View File

@@ -0,0 +1,12 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { HydratedRouter } from 'react-router/dom';
React.startTransition(() => {
ReactDOM.hydrateRoot(
document,
<React.StrictMode>
<HydratedRouter />
</React.StrictMode>,
);
});

101
app/entry.server.tsx Normal file
View File

@@ -0,0 +1,101 @@
import { Transform } from 'node:stream';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import type { EntryContext } from 'react-router';
import { ServerRouter } from 'react-router';
import { createReadableStreamFromReadable } from '@react-router/node';
import { isbot } from 'isbot';
import createEmotionServer from '@emotion/server/create-instance';
import { CacheProvider } from '@emotion/react';
import createEmotionCache from './createCache';
export const streamTimeout = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
) {
const cache = createEmotionCache();
const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);
return new Promise((resolve, reject) => {
let shellRendered = false;
const userAgent = request.headers.get('user-agent');
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
const readyOption: keyof ReactDOMServer.RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady';
const { pipe, abort } = ReactDOMServer.renderToPipeableStream(
<CacheProvider value={cache}>
<ServerRouter context={routerContext} url={request.url} />
</CacheProvider>,
{
[readyOption]() {
shellRendered = true;
// Collect the HTML chunks
const chunks: Buffer[] = [];
// Create transform stream to collect HTML and inject styles
const transformStream = new Transform({
transform(chunk, _encoding, callback) {
// Collect chunks, don't pass them through yet
chunks.push(chunk);
callback();
},
flush(callback) {
// Combine all chunks into HTML string
const html = Buffer.concat(chunks).toString();
// Extract emotion styles from the collected HTML
const styles = constructStyleTagsFromChunks(extractCriticalToChunks(html));
if (styles) {
const injectedHtml = html.replace('</head>', `${styles}</head>`);
this.push(injectedHtml);
} else {
this.push(html);
}
callback();
},
});
const stream = createReadableStreamFromReadable(transformStream);
responseHeaders.set('Content-Type', 'text/html');
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(transformStream);
},
onShellError(error: unknown) {
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);
}
},
},
);
// Abort the rendering stream after the `streamTimeout` so it has time to
// flush down the rejected boundaries
setTimeout(abort, streamTimeout + 1000);
});
}

92
app/root.tsx Normal file
View File

@@ -0,0 +1,92 @@
import * as React from 'react';
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from 'react-router';
import { CacheProvider } from '@emotion/react';
import Box from '@mui/material/Box';
import AppTheme from './theme';
import createEmotionCache from './createCache';
import type { Route } from './+types/root';
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=Roboto:wght@300;400;500;700&display=swap',
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
const cache = createEmotionCache();
export default function App() {
if (typeof window !== 'undefined') {
return (
<CacheProvider value={cache}>
<AppTheme>
<Outlet />
</AppTheme>
</CacheProvider>
);
}
return (
<AppTheme>
<Outlet />
</AppTheme>
);
}
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 (
<Box component="main" sx={{ pt: 8, p: 2, maxWidth: 'lg', mx: 'auto' }}>
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<Box component="pre" sx={{ width: '100%', p: 2, overflowX: 'auto' }}>
<code>{stack}</code>
</Box>
)}
</Box>
);
}

6
app/routes.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('/about', 'routes/about.tsx'),
] satisfies RouteConfig;

45
app/routes/about.tsx Normal file
View File

@@ -0,0 +1,45 @@
import * as React from 'react';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import { Link as ReactRouterLink } from 'react-router';
import ProTip from '~/components/ProTip';
import Copyright from '~/components/Copyright';
export function meta() {
return [
{ title: 'About' },
{
name: 'description',
content: 'About the project',
},
];
}
export default function About() {
return (
<Container maxWidth="lg">
<Box
sx={{
my: 4,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography variant="h4" component="h1" sx={{ mb: 2 }}>
Material UI - Next.js example in TypeScript
</Typography>
<Box sx={{ maxWidth: 'sm' }}>
<Button variant="contained" component={ReactRouterLink} to="/">
Go to the home page
</Button>
</Box>
<ProTip />
<Copyright />
</Box>
</Container>
);
}

43
app/routes/home.tsx Normal file
View File

@@ -0,0 +1,43 @@
import * as React from 'react';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import { Link as ReactRouterLink } from 'react-router';
import ProTip from '~/components/ProTip';
import Copyright from '~/components/Copyright';
export function meta() {
return [
{ title: 'Material UI - React Router example in TypeScript' },
{
name: 'description',
content: 'Welcome to Material UI - React Router example in TypeScript!',
},
];
}
export default function Home() {
return (
<Container maxWidth="lg">
<Box
sx={{
my: 4,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography variant="h4" component="h1" sx={{ mb: 2 }}>
Material UI - Next.js App Router example in TypeScript
</Typography>
<Link to="/about" color="secondary" component={ReactRouterLink}>
Go to the about page
</Link>
<ProTip />
<Copyright />
</Box>
</Container>
);
}

24
app/theme.tsx Normal file
View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const theme = createTheme({
cssVariables: true,
colorSchemes: {
light: true,
dark: true,
},
});
interface AppThemeProps {
children: React.ReactNode;
}
export default function AppTheme({ children }: AppThemeProps) {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
);
}