Nastavení responzivního B2B projektu s Grommet, PropelAuth a Next.js

Grommet je framework založený na Reactu, který vám poskytuje nástroje pro rychlé vytváření citlivých a přístupných aplikací. Obsahuje některé z nejintuitivnějších komponent pro nastavení struktury vašeho produktu (např. záhlaví, zápatí, responzivní postranní panely atd.).

V tomto příspěvku nastavíme nový responzivní frontend pomocí Grommet, PropelAuth a Next.js. Začneme tím, že postavíme kostru, která vypadá takto:

Poté postranní panel změníme na responzivní, aby se na menších zařízeních zobrazoval jinak:

A nakonec přidáme podporu B2B, aby naši uživatelé mohli vytvářet organizace nebo se k nim připojovat a zobrazovat informace na postranním panelu:

Vytvoření AppBar pomocí Grommet a Next.js

Začněme vytvořením nové aplikace Next.js.

$ npx create-next-app

Přidejte do svého _app.js kontejner nejvyšší úrovně Grommet

function MyApp({Component, pageProps}) {
    return <Grommet full>
        <Component {...pageProps} />
    </Grommet>
}

Poté můžeme vytvořit nový soubor components/AppBar.jsx který obsahuje naši hlavičku. Tento příklad je s malými úpravami převzat z Grommetova úvodního průvodce pro 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>
}

Pokud přejdeme na naše index.js soubor a aktualizujte jej, aby používal 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>
}

Dostáváme:

Vytvoření postranního panelu pomocí Gromment a Next.js

Dále vytvoříme postranní panel pomocí komponenty Grommet's Sidebar:

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

Můžete vidět, že jsme vytvořili dvě stránky / a /secret a vykreslujeme je jako Odkaz s získat plynulé přechody mezi různými stránkami.

Pak to stačí zapojit do 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>
}

Pokud kliknete na Tajné , dostanete 404, protože jsme nevytvořili pages/secret.js . Než to uděláme, protože se spousta logiky bude opakovat, věnujte chvíli vytvoření nové užitečné komponenty 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>
}

A pak naše stránky/secret.js může vypadat jen takto:

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

export default function Secret() {
    return <PageWrapper title="Secret" description="shhhh">
        Hello from /secret
    </PageWrapper>
}

Nastavení vaší aplikace

Fialová není vaše barva? Předáním tématu můžeme motivovat celou naši aplikaci do naší Grommet komponent v src/_app.js

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

function MyApp({Component, pageProps}) {
    return <Grommet full theme={theme}>
        <Component {...pageProps} />
    </Grommet>
}

Přidání ověření uživatele do našeho AppBar

Naše přihlašovací tlačítko je právě teď jen pro ukázku a naše tajná stránka... není příliš tajná. PropelAuth je služba, která poskytuje hostované ověření uživatele. Poskytuje plně hostovaná uživatelská rozhraní včetně registrace, přihlášení a správy účtu. Pro B2B/multi-tenant aplikace poskytuje také správu organizace/RBAC.

Při konfiguraci stránky můžete postupovat podle průvodce Začínáme, naše stránka vypadá takto:

Od té doby, co jsme zapnuli přihlašování Google a magický odkaz/přihlašování bez hesla.

Zpět v našem souboru Next.js, pojďme to všechno propojit. Začneme instalací @propelauth/react knihovna:

$ yarn add @propelauth/react

a přidání AuthProvider do našeho _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>
}

Svou authUrl můžete získat ze svého řídicího panelu. Pokud používáte proměnné env jako my výše, nezapomeňte po nastavení .env restartovat server soubor.

AuthProvider je zodpovědný za načítání informací o uživateli a organizaci z PropelAuth pro aktuálního uživatele. Poskytovatel zajišťuje, že načítání proběhne pouze jednou, bez ohledu na to, kolik komponent/stránek potřebuje informace.

Dále aktualizujeme naše komponenty/AppBar.jsx . Chceme Přihlášení tlačítko pro přesměrování na přihlašovací stránku a po přihlášení k zobrazení některých informací o uživateli.

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} />
    }
}

Naše nová AuthButtons má tři stavy:

  1. Načítání informací o aktuálním uživateli. To se stane pouze jednou při prvním načtení stránky.
  2. Pokud jsou přihlášeni, zobrazujeme jejich profilový obrázek. Kliknutím dojde k přesměrování na stránku účtu, kterou pro nás hostuje PropelAuth.
  3. Pokud nejsou přihlášeni, mohou kliknout na tlačítko přihlášení a budou přesměrováni na naši hostovanou přihlašovací stránku.

Pokud se přihlásíte, uvidíte:

Nebo… jakýkoli profilový obrázek, který používáte, není pes.

Ochrana naší stránky „Tajné“

Pro naše stránky platí stejné principy, které jsme se právě naučili z našeho AppBaru. Můžeme použít useAuthInfo zjistit, zda je uživatel přihlášen nebo ne.

K dispozici je také funkce withRequiredAuthInfo které můžete použít ke zpracování velké části načítání standardního kódu:

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

Komponenta SecretBodyInner vždy potřebuje řešit pouze případ, kdy je uživatel přihlášen.

Skládací boční panel

Abychom umožnili našim uživatelům otevřít/zavřít postranní panel, potřebujeme nejprve něco, na co mohou kliknout. Pojďme přidat ikonu hamburgeru do našeho AppBar pomocí [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>
}

Pokud potřebujete mít možnost ovládat postranní panel z libovolných komponent, budete k jeho správě chtít použít kontext React, ale protože k němu potřebujeme přístup pouze v AppBar a Sidebar, můžeme to vše spravovat z našeho 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>
} 

Skvělý! Nyní potřebujeme, aby se postranní panel zobrazil a zmizel po kliknutí na Hamburger. Vzal jsem stávající komponentu MySidebar a přejmenovali jej na MySidebarInner:

import {Collapsible} from "grommet";

const MySidebar = ({showSidebar}) => {
    return <Collapsible direction="horizontal" open={showSidebar}>
        <MySidebarInner />
    </Collapsible>
}

Sbalitelný umožňuje hladký přechod, když otevřeme/zavřeme postranní panel, jako je tento:

Aby byl náš postranní panel responzivní

Na mobilu může náš postranní panel zabírat spoustu místa. Namísto toho, aby postranní panel vyskočil ze strany obrazovky, můžeme jej nechat zabírat celou obrazovku. Až skončíme, bude to vypadat takto:

Grommet nám poskytuje ResponsiveContext , což nám umožňuje měnit to, co vykreslujeme na základě velikosti obrazovky. Použijeme ji společně s vrstvou překryvná komponenta pro zobrazení postranního panelu nad vším.

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

Všimněte si, že jsme také potřebovali přidat nové tlačítko, aby naši uživatelé mohli zavřít celoobrazovkový postranní panel.

Přidávání organizací na náš postranní panel

Organizace jsou skupiny vašich uživatelů, kteří budou váš produkt používat společně. Může to být startup pro dvě osoby, velký podnik nebo něčí vedlejší podnik, ze kterého se jednoho dne chtějí stát společností.

PropelAuth umožňuje našim uživatelům spravovat své vlastní organizace (zvaní spolupracovníků, nastavení rolí atd.), takže vše, co musíme udělat, je zobrazit je na našem postranním panelu:

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 a selectOrg jsou poskytovány orgHelper, aby bylo snazší odkazovat na jednu organizaci v rámci našeho produktu.

A to je vše! Nyní máme opravdu výkonnou kostru pro náš projekt a můžeme začít budovat zbytek naší B2B nebo multi tenantové aplikace.