Настройка отзывчивого проекта B2B с помощью Grommet, PropelAuth и Next.js

Grommet — это фреймворк на основе React, который предоставляет инструменты для быстрого создания отзывчивых и доступных приложений. Он имеет одни из самых интуитивных компонентов для настройки структуры вашего продукта (например, заголовки, нижние колонтитулы, отзывчивые боковые панели и т.д.).

В этом посте мы создадим новый отзывчивый фронтенд с помощью Grommet, PropelAuth и Next.js. Мы начнем с создания скелета, который выглядит следующим образом:

Затем мы сделаем боковую панель отзывчивой, чтобы она по-разному отображалась на небольших устройствах:

И, наконец, мы добавим поддержку B2B, чтобы наши пользователи могли создавать/соединяться с организациями и отображать информацию в боковой панели:

Создание AppBar с помощью Grommet и Next.js

Давайте начнем с создания нового приложения Next.js.

$ npx create-next-app
Вход в полноэкранный режим Выйдите из полноэкранного режима

Добавьте контейнер верхнего уровня Grommet в ваш _app.js.

function MyApp({Component, pageProps}) {
    return <Grommet full>
        <Component {...pageProps} />
    </Grommet>
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Затем мы можем сделать новый файл components/AppBar.jsx, который содержит наш заголовок. Этот пример взят, с небольшими изменениями, из руководства Grommet по началу работы с React.

import {Box, Button, Heading} from "grommet";

export default function AppBar() {
    return <Box
        tag='header'
        direction='row'
        align='center'
        justify='between'
        background='brand'
        pad={{left: 'medium', right: 'small', vertical: 'small'}}
        elevation='xsmall'
        style={{zIndex: '1'}}
    >
        <Heading level='3' margin='none'>My App</Heading>
        <Button primary label="Login"/>
    </Box>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если мы перейдем к нашему файлу index.js и обновим его для использования AppBar:

import Head from 'next/head'
import {Box} from "grommet";
import AppBar from "../components/AppBar";

export default function Home() {
    return <Box fill>
        <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app"/>
            <link rel="icon" href="/favicon.ico"/>
        </Head>
        <AppBar/>
    </Box>
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы получим:

Создание боковой панели с помощью Gromment и Next.js

Далее создадим боковую панель с помощью компонента Sidebar от Grommet:

import {Button, Nav, Sidebar} from "grommet";
import {Secure, Projects} from "grommet-icons";
import Link from "next/link";

const MySidebar = () => {
    return <Box background="brand" fill gap="medium" pad="medium">
               <Button icon={<Projects/>} plain label="Home"
                       href="/" as={(props) => <Link {...props} />}/>
               <Button icon={<Secure/>} plain label="Secret"
                       href="/secret" as={(props) => <Link {...props} />}/>
        </Box>
    }
}

export default MySidebar
Вход в полноэкранный режим Выход из полноэкранного режима

Вы можете видеть, что мы создали две страницы / и /secret и отображаем их как ссылки, чтобы получить плавные переходы между различными страницами.

Затем нам просто нужно подключить их в pages/index.js.

export default function Home() {
    return <Box fill>
        <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app"/>
            <link rel="icon" href="/favicon.ico"/>
        </Head>
        <AppBar/>
        <Box direction='row' flex overflow={{horizontal: 'hidden'}}>
            <MySidebar />
            <Box flex margin="medium">
                Hello from /
            </Box>
        </Box>
    </Box>
}
Войти в полноэкранный режим Выход из полноэкранного режима

Если вы нажмете Secret, то получите 404, потому что мы не создали pages/secret.js. Прежде чем мы это сделаем, поскольку большая часть логики будет повторяться, давайте потратим секунду на создание нового полезного компонента components/PageWrapper.js

import {Box} from "grommet";
import Head from "next/head";
import AppBar from "./AppBar";
import MySidebar from "./MySidebar";

export default function PageWrapper({title, description, children}) {
    return <Box fill>
        <Head>
            <title>{title}</title>
            <meta name="description" content={description} />
            <link rel="icon" href="/favicon.ico"/>
        </Head>
        <AppBar/>
        <Box direction='row' flex overflow={{horizontal: 'hidden'}}>
            <MySidebar />
            <Box flex margin="medium">
                {children}
            </Box>
        </Box>
    </Box>
}
Вход в полноэкранный режим Выйти из полноэкранного режима

И тогда наш pages/secret.js будет выглядеть следующим образом:

import PageWrapper from "../components/PageWrapper";

export default function Secret() {
    return <PageWrapper title="Secret" description="shhhh">
        Hello from /secret
    </PageWrapper>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Тематика вашего приложения

Фиолетовый не ваш цвет? Мы можем оформить все наше приложение, передав тему компоненту Grommet в src/_app.js

const theme = {
    global: {
        colors: {
            brand: '#3c5ccf',
        },
    },
};

function MyApp({Component, pageProps}) {
    return <Grommet full theme={theme}>
        <Component {...pageProps} />
    </Grommet>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление аутентификации пользователя в AppBar

Наша кнопка входа в систему сейчас просто для показа, а наша секретная страница… не очень секретная. PropelAuth — это сервис, который обеспечивает аутентификацию пользователей на хостинге. Он предоставляет полностью хостируемые пользовательские интерфейсы, включая регистрацию, вход и управление учетными записями. Для B2B/многопользовательских приложений он также обеспечивает управление организацией/RBAC.

Вы можете следовать руководству по началу работы, чтобы настроить свою страницу, наша выглядит следующим образом:

Поскольку мы включили Google login и magic link/passwordless login.

Вернемся в наш файл Next.js, давайте подключим все это. Начнем с установки библиотеки @propelauth/react:

$ yarn add @propelauth/react
Вход в полноэкранный режим Выход из полноэкранного режима

и добавим AuthProvider в наш _app.js.

import {AuthProvider} from "@propelauth/react";

function MyApp({Component, pageProps}) {
    return <Grommet full theme={theme}>
        <AuthProvider authUrl={process.env.NEXT_PUBLIC_AUTH_URL}>
            <Component {...pageProps} />
        </AuthProvider>
    </Grommet>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вы можете получить свой authUrl из вашей приборной панели. Если вы используете переменные env, как мы сделали выше, убедитесь, что перезапустили сервер после настройки файла .env.

AuthProvider отвечает за получение информации о пользователе и организации из PropelAuth для текущего пользователя. Провайдер следит за тем, чтобы выборка происходила только один раз, независимо от того, скольким компонентам/страницам нужна эта информация.

Далее мы обновим наш компонент/AppBar.jsx. Мы хотим, чтобы кнопка Login перенаправляла на страницу входа, а после входа отображала некоторую информацию о пользователе.

import {Avatar, Box, Button, Heading, Spinner} from "grommet";
import {useAuthInfo, useRedirectFunctions} from "@propelauth/react";

export default function AppBar(props) {
    return <Box {/*... truncated for space */}>
        <Heading level='3' margin='none'>My App</Heading>
        <AuthButtons />
    </Box>
}

const AuthButtons = () => {
    const authInfo = useAuthInfo();
    const {redirectToLoginPage, redirectToAccountPage} = useRedirectFunctions();

    if (authInfo.loading) {
        return <Spinner />
    } else if (authInfo.isLoggedIn) {
        return <Avatar src={authInfo.user.pictureUrl} onClick={redirectToAccountPage}/>
    } else {
        return <Button primary label="Login" onClick={redirectToLoginPage} />
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наша новая кнопка AuthButton имеет три состояния:

  1. Загрузка информации о текущем пользователе. Это происходит только один раз при первоначальной загрузке страницы.
  2. Если пользователь вошел в систему, мы показываем его фотографию профиля. Щелчок по ней перенаправляет на страницу аккаунта, размещенную PropelAuth для нас.
  3. Если они не вошли в систему, они могут нажать кнопку входа, чтобы перенаправить их на нашу страницу входа.

Если вы войдете в систему, вы увидите:

Или… любое изображение профиля, которое вы используете, но не собаку.

Защита нашей «секретной» страницы

Те же принципы, которые мы только что изучили на AppBar, применимы к нашим страницам. Мы можем использовать функцию useAuthInfo, чтобы определить, вошел ли пользователь в систему или нет.

Существует также функция withRequiredAuthInfo, которую можно использовать для обработки большого количества кода загрузки:

import PageWrapper from "../components/PageWrapper";
import {withRequiredAuthInfo} from "@propelauth/react";
import {Spinner} from "grommet";

// All of the same properties of useAuthInfo are automatically injected
//   but user is always set and isLoggedIn is always true
const SecretBodyInner = ({user}) => {
    return <div>Welcome to the secret page, {user.firstName}</div>
}

const SecretBody = withRequiredAuthInfo(SecretBodyInner, {
    displayWhileLoading: <Spinner/>,
    displayIfLoggedOut: <div>Please login to view</div>,
})

export default function Secret() {
    return <PageWrapper title="Secret" description="shhhh">
        <SecretBody/>
    </PageWrapper>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Компонент SecretBodyInner должен работать только в том случае, если пользователь вошел в систему.

Делаем наш сайдбар складным

Чтобы позволить нашим пользователям открывать/закрывать боковую панель, нам сначала нужно что-то, на что они могут нажать. Давайте добавим иконку гамбургера в наш AppBar с помощью [hamburger-react](https://hamburger-react.netlify.app/).

import Hamburger from 'hamburger-react'

// Take in two new properties: showSidebar and setShowSidebar
export default function AppBar({showSidebar, setShowSidebar}) {
    return <Box /*truncated for space*/>
        <Box direction='row' align='center' justify='between'>
            <OpenAndCloseSidebar showSidebar={showSidebar} setShowSidebar={setShowSidebar} />
            <Heading level='3' margin='none'>My App</Heading>
        </Box>
        <AuthButtons/>
    </Box>
}

const OpenAndCloseSidebar = ({showSidebar, setShowSidebar}) => {
    return <Box pad={{right: 'small'}}>
        <Hamburger toggle={setShowSidebar} toggled={showSidebar} />
    </Box>
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Если вам нужно иметь возможность управлять боковой панелью из произвольных компонентов, вы захотите использовать контекст React для управления ею, но поскольку нам нужен доступ к ней только в AppBar и Sidebar, мы можем управлять всем этим из нашего PageWrapper:

export default function PageWrapper({title, description, children}) {
    const [showSidebar, setShowSidebar] = useState(true);

    return <Box fill>
        <Head>
            <title>{title}</title>
            <meta name="description" content={description} />
            <link rel="icon" href="/favicon.ico"/>
        </Head>
        <AppBar showSidebar={showSidebar} setShowSidebar={setShowSidebar} />
        <Box direction='row' flex overflow={{horizontal: 'hidden'}}>
            <MySidebar showSidebar={showSidebar} setShowSidebar={setShowSidebar} />
            <Box flex margin="medium">
                {children}
            </Box>
        </Box>
    </Box>
} 
Вход в полноэкранный режим Выйти из полноэкранного режима

Отлично! Теперь нам просто нужно, чтобы боковая панель появлялась и исчезала при нажатии на гамбургер. Я взял существующий компонент MySidebar и переименовал его в MySidebarInner:

import {Collapsible} from "grommet";

const MySidebar = ({showSidebar}) => {
    return <Collapsible direction="horizontal" open={showSidebar}>
        <MySidebarInner />
    </Collapsible>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Складная конструкция обеспечивает плавный переход, когда мы открываем/закрываем сайдбар, как здесь:

Сделать боковую панель отзывчивой

На мобильных устройствах боковая панель может занимать много места. Вместо того чтобы боковая панель появлялась сбоку экрана, мы можем сделать так, чтобы она занимала весь экран. Когда мы закончим, это будет выглядеть следующим образом:

Grommet предоставляет нам ResponsiveContext, который позволяет нам изменять отображение в зависимости от размера экрана. Мы будем использовать его вместе с компонентом Layer overlay для отображения боковой панели поверх всего остального.

const MySidebar = ({showSidebar, setShowSidebar}) => {
    const renderSidebar = (size) => {
        if (size === "small") {
            return <Layer>
                <Box background="brand">
                    <Button icon={<FormClose/>} onClick={() => setShowSidebar(false)}/>
                </Box>
                <MySidebarInner />
            </Layer>
        } else {
            return <Collapsible direction="horizontal" open={showSidebar}>
                <MySidebarInner/>
            </Collapsible>
        }
    }

    return <ResponsiveContext.Consumer>
        {size => renderSidebar(size)}
    </ResponsiveContext.Consumer>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что нам также необходимо добавить новую кнопку, чтобы наши пользователи могли закрыть боковую панель в полноэкранном режиме.

Добавление организаций в боковую панель

Организации — это группы ваших пользователей, которые будут использовать ваш продукт вместе. Это может быть стартап из двух человек, крупное предприятие или чей-то побочный бизнес, который когда-нибудь станет компанией.

PropelAuth позволяет нашим пользователям управлять своими организациями (приглашать коллег, устанавливать роли и т.д.), поэтому нам остается только отобразить их в сайдбаре:

const MySidebarInner = () => {
    return <Box background="brand" fill gap="medium" pad="medium">
        <Button icon={<Projects/>} plain label="Home"
                href="/" as={(props) => <Link {...props} />}/>
        <Button icon={<Secure/>} plain label="Secret"
                href="/secret" as={(props) => <Link {...props} />}/>

        <Box background='brand' justify='end' align='center' fill>
            <OrgSelector/>
        </Box>
    </Box>
}

// If you aren't logged in, display nothing
const OrgSelector = withRequiredAuthInfo(OrgSelectorInner, {
    displayWhileLoading: <Spinner/>,
    displayIfLoggedOut: null
})

const OrgSelectorInner = ({orgHelper}) => {
    const {redirectToCreateOrgPage} = useRedirectFunctions();
    const orgs = orgHelper.getOrgs();

    // If the user isn't a member of any orgs, allow them to create one
    if (orgs.length === 0) {
        return <Button plain label="Create Organization" onClick={redirectToCreateOrgPage}/>
    }

    // getSelectedOrg will infer a default for the user's first load 
    const selectedOrg = orgHelper.getSelectedOrg()
    return <Menu
        label={selectedOrg.orgName}
        items={orgs.map(org => {
            return {
                label: org.orgName,
                onClick: () => orgHelper.selectOrg(org.orgId)
            }
        })}
    />
}
Войти в полноэкранный режим Выход из полноэкранного режима

getSelectedOrg и selectOrg предоставляются orgHelper, чтобы облегчить обращение к одной организации во всем нашем продукте.

И это все! Теперь у нас есть действительно мощный скелет для нашего проекта, и мы можем приступить к созданию остальной части нашего B2B или многоарендного приложения.

Оцените статью
Procodings.ru
Добавить комментарий