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

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.DS_Store
/node_modules/
# React Router
/.react-router/
/build/

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/homepage.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,46 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
<list>
<option value="http://localhost" />
<option value="http://127.0.0.1" />
<option value="http://0.0.0.0" />
<option value="http://www.w3.org/" />
<option value="http://json-schema.org/draft" />
<option value="http://java.sun.com/" />
<option value="http://xmlns.jcp.org/" />
<option value="http://javafx.com/javafx/" />
<option value="http://javafx.com/fxml" />
<option value="http://maven.apache.org/xsd/" />
<option value="http://maven.apache.org/POM/" />
<option value="http://www.springframework.org/schema/" />
<option value="http://www.springframework.org/tags" />
<option value="http://www.springframework.org/security/tags" />
<option value="http://www.thymeleaf.org" />
<option value="http://www.jboss.org/j2ee/schema/" />
<option value="http://www.jboss.com/xml/ns/" />
<option value="http://www.ibm.com/webservices/xsd" />
<option value="http://activemq.apache.org/schema/" />
<option value="http://schema.cloudfoundry.org/spring/" />
<option value="http://schemas.xmlsoap.org/" />
<option value="http://cxf.apache.org/schemas/" />
<option value="http://primefaces.org/ui" />
<option value="http://tiles.apache.org/" />
<option value="http://{hostname}" />
<option value="http://www.example.com" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredNames">
<list>
<option value="id" />
</list>
</option>
</inspection_tool>
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/homepage.iml" filepath="$PROJECT_DIR$/.idea/homepage.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

41
README.md Normal file
View File

@@ -0,0 +1,41 @@
# Material UI - React Router example in TypeScript
## How to use
Download the example [or clone the repo](https://github.com/mui/material-ui):
<!-- #target-branch-reference -->
```bash
curl https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/material-ui-react-router-ts
cd material-ui-react-router-ts
```
Install it and run:
```bash
npm install
npm run dev
```
or:
<!-- #target-branch-reference -->
[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/github/mui/material-ui/tree/master/examples/material-ui-react-router-ts)
[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mui/material-ui/tree/master/examples/material-ui-react-router-ts)
## The idea behind the example
<!-- #host-reference -->
This example demonstrates how you can use Material UI with [React Router](https://reactrouter.com/) in [TypeScript](https://github.com/Microsoft/TypeScript).
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI.
## What's next?
<!-- #host-reference -->
You now have a working example project.
You can head back to the documentation and continue by browsing the [templates](https://mui.com/material-ui/getting-started/templates/) section.

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>
);
}

4660
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "material-ui-react-router-ts",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@emotion/cache": "latest",
"@emotion/react": "latest",
"@emotion/server": "latest",
"@emotion/styled": "latest",
"@mui/material": "latest",
"@react-router/node": "latest",
"@react-router/serve": "latest",
"isbot": "latest",
"react": "latest",
"react-dom": "latest",
"react-router": "latest"
},
"devDependencies": {
"@react-router/dev": "latest",
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"typescript": "latest",
"vite": "latest",
"vite-tsconfig-paths": "latest"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

7
react-router.config.ts Normal file
View File

@@ -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;

22
tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"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
}
}

16
vite.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { reactRouter } from '@react-router/dev/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [reactRouter(), tsconfigPaths()],
ssr: {
// Workaround for resolving dependencies in the server bundle
// Without this, the React context will be different between direct import and transitive imports in development environment
// For more information, see https://github.com/mui/material-ui/issues/45878#issuecomment-2987441663
optimizeDeps: {
include: ['@emotion/*', '@mui/*'],
},
noExternal: ['@emotion/*', '@mui/*'],
},
});