Skip to content

Commit

Permalink
[docs] Add SignInPage Vite + React Router example (#4335)
Browse files Browse the repository at this point in the history
  • Loading branch information
bharatkashyap authored Nov 5, 2024
1 parent 3bb2a53 commit 90eaa01
Show file tree
Hide file tree
Showing 20 changed files with 540 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ If you're using the default [Next.js example](https://github.com/mui/toolpad/tre
If you're not on the Next Auth v5 version yet, see the [example with Next Auth v4](https://github.com/mui/toolpad/tree/master/examples/core/auth-nextjs-pages-nextauth-4/) to get started.
:::

:::info
If you're using Vite with React Router, check out the [example with Vite and React Router](https://github.com/mui/toolpad/tree/master/examples/core/auth-vite/) that showcases how to use `SignInPage` along with any external authentication library of your choice.
:::

## Customization

### Branding
Expand Down
224 changes: 224 additions & 0 deletions docs/data/toolpad/core/introduction/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -836,3 +836,227 @@ That's it! You now have Toolpad Core integrated into your single-page app with R
:::info
For a full working example, see the [Toolpad Core Vite app with React Router example](https://github.com/mui/toolpad/tree/master/examples/core/vite/)
:::

### 4. (Optional) Set up authentication

You can use the `SignInPage` component to add authentication along with an external authentication provider of your choice. The following code demonstrates the code required to set up authentication with a mock provider.

#### a. Define a `SessionContext` to act as the mock authentication provider

```tsx title="src/SessionContext.ts"
import * as React from 'react';
import type { Session } from '@toolpad/core';

export interface SessionContextValue {
session: Session | null;
setSession: (session: Session | null) => void;
}

export const SessionContext = React.createContext<SessionContextValue>({
session: {},
setSession: () => {},
});

export function useSession() {
return React.useContext(SessionContext);
}
```

### b. Add the mock authentication and session data to the `AppProvider`

```tsx title="src/App.tsx"
import * as React from 'react';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { AppProvider } from '@toolpad/core/react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';
import type { Navigation, Session } from '@toolpad/core';
import { SessionContext } from './SessionContext';

const NAVIGATION: Navigation = [
{
kind: 'header',
title: 'Main items',
},
{
title: 'Dashboard',
icon: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
},
];

const BRANDING = {
title: 'My Toolpad Core App',
};

export default function App() {
const [session, setSession] = React.useState<Session | null>(null);
const navigate = useNavigate();

const signIn = React.useCallback(() => {
navigate('/sign-in');
}, [navigate]);

const signOut = React.useCallback(() => {
setSession(null);
navigate('/sign-in');
}, [navigate]);

const sessionContextValue = React.useMemo(
() => ({ session, setSession }),
[session, setSession],
);

return (
<SessionContext.Provider value={sessionContextValue}>
<AppProvider
navigation={NAVIGATION}
branding={BRANDING}
session={session}
authentication={{ signIn, signOut }}
>
<Outlet />
</AppProvider>
</SessionContext.Provider>
);
}
```

#### c. Protect routes inside the dashboard layout

```tsx title="src/layouts/dashboard.tsx"
import * as React from 'react';
import { Outlet, Navigate, useLocation } from 'react-router-dom';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
import { PageContainer } from '@toolpad/core/PageContainer';
import { useSession } from '../SessionContext';

export default function Layout() {
const { session } = useSession();
const location = useLocation();

if (!session) {
// Add the `callbackUrl` search parameter
const redirectTo = `/sign-in?callbackUrl=${encodeURIComponent(location.pathname)}`;

return <Navigate to={redirectTo} replace />;
}

return (
<DashboardLayout>
<PageContainer>
<Outlet />
</PageContainer>
</DashboardLayout>
);
}
```

You can protect any page or groups of pages through this mechanism.

#### d. Use the `SignInPage` component to create a sign-in page

```tsx title="src/pages/signIn.tsx"
'use client';
import * as React from 'react';
import { SignInPage } from '@toolpad/core/SignInPage';
import type { Session } from '@toolpad/core/AppProvider';
import { useNavigate } from 'react-router-dom';
import { useSession } from '../SessionContext';

const fakeAsyncGetSession = async (formData: any): Promise<Session> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (formData.get('password') === 'password') {
resolve({
user: {
name: 'Bharat Kashyap',
email: formData.get('email') || '',
image: 'https://avatars.githubusercontent.com/u/19550456',
},
});
}
reject(new Error('Incorrect credentials.'));
}, 1000);
});
};

export default function SignIn() {
const { setSession } = useSession();
const navigate = useNavigate();
return (
<SignInPage
providers={[{ id: 'credentials', name: 'Credentials' }]}
signIn={async (provider, formData, callbackUrl) => {
// Demo session
try {
const session = await fakeAsyncGetSession(formData);
if (session) {
setSession(session);
navigate(callbackUrl || '/', { replace: true });
return {};
}
} catch (error) {
return {
error: error instanceof Error ? error.message : 'An error occurred',
};
}
return {};
}}
/>
);
}
```

#### e. Add the sign in page to the router

```tsx title="src/main.tsx"
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import App from './App';
import Layout from './layouts/dashboard';
import DashboardPage from './pages';
import OrdersPage from './pages/orders';
import SignInPage from './pages/signIn';

const router = createBrowserRouter([
{
Component: App,
children: [
{
path: '/',
Component: Layout,
children: [
{
path: '/',
Component: DashboardPage,
},
{
path: '/orders',
Component: OrdersPage,
},
],
},
{
path: '/sign-in',
Component: SignInPage,
},
],
},
]);

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);
```

:::info
For a full working example, see the [Toolpad Core Vite app with React Router and authentication example](https://github.com/mui/toolpad/tree/master/examples/core/auth-vite/)
:::
7 changes: 7 additions & 0 deletions docs/src/modules/components/ExamplesGrid/core-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,12 @@ export default function examples() {
stackblitz:
'https://stackblitz.com/github/mui/toolpad/tree/master/examples/core/auth-nextjs-passkey',
},
{
title: 'Vite with React Router and authentication',
description:
'This app shows you to how to get started using Toolpad Core with Vite, React Router and any external authentication provider',
src: '/static/toolpad/docs/core/vite-react-router.png',
source: 'https://github.com/mui/toolpad/tree/master/examples/core/auth-vite',
},
];
}
24 changes: 24 additions & 0 deletions examples/core/auth-vite/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
8 changes: 8 additions & 0 deletions examples/core/auth-vite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
20 changes: 20 additions & 0 deletions examples/core/auth-vite/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<!-- Fonts to support Material Design -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
/>
<title>Toolpad Core Vite with Auth</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions examples/core/auth-vite/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "core-vite-auth",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@mui/icons-material": "^6",
"@mui/material": "^6",
"@toolpad/core": "workspace:*",
"react": "^18",
"react-dom": "^18",
"react-router-dom": "^6"
},
"devDependencies": {
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitejs/plugin-react": "^4.3.2",
"typescript": "^5",
"vite": "^5.4.8"
}
}
1 change: 1 addition & 0 deletions examples/core/auth-vite/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions examples/core/auth-vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { AppProvider } from '@toolpad/core/react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';
import type { Navigation, Session } from '@toolpad/core';
import { SessionContext } from './SessionContext';

const NAVIGATION: Navigation = [
{
kind: 'header',
title: 'Main items',
},
{
title: 'Dashboard',
icon: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
},
];

const BRANDING = {
title: 'My Toolpad Core App',
};

export default function App() {
const [session, setSession] = React.useState<Session | null>(null);
const navigate = useNavigate();

const signIn = React.useCallback(() => {
navigate('/sign-in');
}, [navigate]);

const signOut = React.useCallback(() => {
setSession(null);
navigate('/sign-in');
}, [navigate]);

const sessionContextValue = React.useMemo(() => ({ session, setSession }), [session, setSession]);

return (
<SessionContext.Provider value={sessionContextValue}>
<AppProvider
navigation={NAVIGATION}
branding={BRANDING}
session={session}
authentication={{ signIn, signOut }}
>
<Outlet />
</AppProvider>
</SessionContext.Provider>
);
}
Loading

0 comments on commit 90eaa01

Please sign in to comment.