client: refactor header component for improved SSR

This commit is contained in:
Max Leiter 2022-04-02 00:45:26 -07:00
parent e7cec9b827
commit a1fef656bb
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
17 changed files with 289 additions and 290 deletions

View file

@ -1,7 +1,8 @@
import { GeistProvider, CssBaseline, Themes } from "@geist-ui/core" import Header from "@components/header"
import { GeistProvider, CssBaseline, Themes, Page } from "@geist-ui/core"
import type { NextComponentType, NextPageContext } from "next" import type { NextComponentType, NextPageContext } from "next"
import { SkeletonTheme } from "react-loading-skeleton" import { SkeletonTheme } from "react-loading-skeleton"
import { useTheme } from 'next-themes'
const App = ({ const App = ({
Component, Component,
pageProps, pageProps,
@ -50,9 +51,10 @@ const App = ({
} }
} }
) )
return (<GeistProvider themes={[customTheme]} themeType={"custom"} > return (<GeistProvider themes={[customTheme]} themeType={"custom"}>
<SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}> <SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}>
<CssBaseline /> <CssBaseline />
<Header />
<Component {...pageProps} /> <Component {...pageProps} />
</SkeletonTheme> </SkeletonTheme>
</GeistProvider >) </GeistProvider >)

View file

@ -29,7 +29,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
const res = await resp.json() const res = await resp.json()
setRequiresServerPassword(res.requiresPasscode) setRequiresServerPassword(res.requiresPasscode)
} else { } else {
setErrorMsg("Something went wrong.") setErrorMsg("Something went wrong. Is the server running?")
} }
} }
} }

View file

@ -1,14 +1,33 @@
.tabs { .tabs {
flex: 1 1; justify-content: center;
padding: 0 var(--gap); display: flex;
margin-top: var(--gap);
}
.tabs .buttons {
display: flex;
justify-content: center;
align-items: center;
}
.tabs .buttons > button,
.tabs .buttons > a > button {
border: none;
border-radius: 0;
cursor: pointer;
}
.tabs .active {
border-bottom: 1px solid var(--darker-gray) !important;
} }
.mobile { .mobile {
position: relative; position: relative;
z-index: 2; z-index: 1;
} }
.controls { .controls {
margin-top: var(--gap);
display: none !important; display: none !important;
} }

View file

@ -1,197 +0,0 @@
import { ButtonGroup, Page, Spacer, Tabs, useBodyScroll, useMediaQuery, } from "@geist-ui/core";
import { useCallback, useEffect, useState } from "react";
import styles from './header.module.css';
import { useRouter } from "next/router";
import useSignedIn from "../../lib/hooks/use-signed-in";
import HomeIcon from '@geist-ui/icons/home';
import MenuIcon from '@geist-ui/icons/menu';
import GitHubIcon from '@geist-ui/icons/github';
import SignOutIcon from '@geist-ui/icons/userX';
import SignInIcon from '@geist-ui/icons/user';
import SignUpIcon from '@geist-ui/icons/userPlus';
import NewIcon from '@geist-ui/icons/plusCircle';
import YourIcon from '@geist-ui/icons/list'
import MoonIcon from '@geist-ui/icons/moon';
import SettingsIcon from '@geist-ui/icons/settings';
import SunIcon from '@geist-ui/icons/sun';
import { useTheme } from "next-themes"
import { Button } from "@geist-ui/core";
import useUserData from "@lib/hooks/use-user-data";
type Tab = {
name: string
icon: JSX.Element
value: string
onClick?: () => void
href?: string
}
const Header = () => {
const router = useRouter();
const [selectedTab, setSelectedTab] = useState<string>(router.pathname === '/' ? 'home' : router.pathname.split('/')[1]);
const [expanded, setExpanded] = useState<boolean>(false)
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
const isMobile = useMediaQuery('xs', { match: 'down' })
const { signedIn: isSignedIn, signout } = useSignedIn()
const userData = useUserData();
const [pages, setPages] = useState<Tab[]>([])
const { setTheme, resolvedTheme } = useTheme()
useEffect(() => {
setBodyHidden(expanded)
}, [expanded, setBodyHidden])
useEffect(() => {
if (!isMobile) {
setExpanded(false)
}
}, [isMobile])
useEffect(() => {
const defaultPages: Tab[] = [
{
name: isMobile ? "GitHub" : "",
href: "https://github.com/maxleiter/drift",
icon: <GitHubIcon />,
value: "github"
},
{
name: isMobile ? "Change theme" : "",
onClick: function () {
if (typeof window !== 'undefined')
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
},
icon: resolvedTheme === 'light' ? <MoonIcon /> : <SunIcon />,
value: "theme",
}
]
if (isSignedIn)
setPages([
{
name: 'new',
icon: <NewIcon />,
value: 'new',
href: '/new'
},
{
name: 'yours',
icon: <YourIcon />,
value: 'yours',
href: '/mine'
},
// {
// name: 'settings',
// icon: <SettingsIcon />,
// value: 'settings',
// href: '/settings'
// },
{
name: 'sign out',
icon: <SignOutIcon />,
value: 'signout',
onClick: signout
},
...defaultPages
])
else
setPages([
{
name: 'home',
icon: <HomeIcon />,
value: 'home',
href: '/'
},
{
name: 'Sign in',
icon: <SignInIcon />,
value: 'signin',
href: '/signin'
},
{
name: 'Sign up',
icon: <SignUpIcon />,
value: 'signup',
href: '/signup'
},
...defaultPages
])
if (userData?.role === "admin") {
setPages((pages) => [
...pages,
{
name: 'admin',
icon: <SettingsIcon />,
value: 'admin',
href: '/admin'
}
])
}
// TODO: investigate deps causing infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile, isSignedIn, resolvedTheme, userData])
const onTabChange = useCallback((tab: string) => {
if (typeof window === 'undefined') return
const match = pages.find(page => page.value === tab)
if (match?.onClick) {
match.onClick()
} else {
router.push(match?.href || '/')
}
}, [pages, router])
return (
<Page.Header height={'var(--page-nav-height)'} marginBottom={2}>
<div className={styles.tabs}>
<Tabs
value={selectedTab}
leftSpace={0}
align="center"
hideDivider
hideBorder
onChange={onTabChange}>
{pages.map((tab) => {
return <Tabs.Item
font="14px"
label={<>{tab.icon} {tab.name}</>}
value={tab.value}
key={`${tab.value}`}
/>
})}
</Tabs>
</div>
<div className={styles.controls}>
<Button
auto
type="abort"
onClick={() => setExpanded(!expanded)}
aria-label="Menu"
>
<Spacer height={5 / 6} width={0} />
<MenuIcon />
</Button>
</div>
{isMobile && expanded && (<div className={styles.mobile}>
<ButtonGroup vertical>
{pages.map((tab, index) => {
return <Button
key={`${tab.name}-${index}`}
onClick={() => onTabChange(tab.value)}
icon={tab.icon}
>
{tab.name}
</Button>
})}
</ButtonGroup>
</div>)}
</Page.Header >
)
}
export default Header

View file

@ -1,10 +1,207 @@
import dynamic from 'next/dynamic'
const Header = dynamic(import('./header'), { import { ButtonGroup, Button, Page, Spacer, useBodyScroll, useMediaQuery, } from "@geist-ui/core";
// ssr: false,
// loading: () => <MenuSkeleton />, import { useCallback, useEffect, useMemo, useState } from "react";
}) import styles from './header.module.css';
import useSignedIn from "../../lib/hooks/use-signed-in";
import HomeIcon from '@geist-ui/icons/home';
import MenuIcon from '@geist-ui/icons/menu';
import GitHubIcon from '@geist-ui/icons/github';
import SignOutIcon from '@geist-ui/icons/userX';
import SignInIcon from '@geist-ui/icons/user';
import SignUpIcon from '@geist-ui/icons/userPlus';
import NewIcon from '@geist-ui/icons/plusCircle';
import YourIcon from '@geist-ui/icons/list'
import MoonIcon from '@geist-ui/icons/moon';
import SettingsIcon from '@geist-ui/icons/settings';
import SunIcon from '@geist-ui/icons/sun';
import { useTheme } from "next-themes"
import useUserData from "@lib/hooks/use-user-data";
import Link from "next/link";
import { useRouter } from "next/router";
type Tab = {
name: string
icon: JSX.Element
value: string
onClick?: () => void
href?: string
}
const Header = () => {
const router = useRouter()
const [expanded, setExpanded] = useState<boolean>(false)
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
const isMobile = useMediaQuery('xs', { match: 'down' })
const { signedIn: isSignedIn, signout } = useSignedIn()
const userData = useUserData();
const [pages, setPages] = useState<Tab[]>([])
const { setTheme, resolvedTheme } = useTheme()
useEffect(() => {
setBodyHidden(expanded)
}, [expanded, setBodyHidden])
useEffect(() => {
if (!isMobile) {
setExpanded(false)
}
}, [isMobile])
useEffect(() => {
const defaultPages: Tab[] = [
{
name: isMobile ? "GitHub" : "",
href: "https://github.com/maxleiter/drift",
icon: <GitHubIcon />,
value: "github"
},
{
name: isMobile ? "Change theme" : "",
onClick: function () {
if (typeof window !== 'undefined')
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
},
icon: resolvedTheme === 'light' ? <MoonIcon /> : <SunIcon />,
value: "theme",
}
]
if (isSignedIn)
setPages([
{
name: 'new',
icon: <NewIcon />,
value: 'new',
href: '/new'
},
{
name: 'yours',
icon: <YourIcon />,
value: 'yours',
href: '/mine'
},
// {
// name: 'settings',
// icon: <SettingsIcon />,
// value: 'settings',
// href: '/settings'
// },
{
name: 'sign out',
icon: <SignOutIcon />,
value: 'signout',
// onClick: signout,
href: '/signout'
},
...defaultPages
])
else
setPages([
{
name: 'home',
icon: <HomeIcon />,
value: 'home',
href: '/'
},
{
name: 'Sign in',
icon: <SignInIcon />,
value: 'signin',
href: '/signin'
},
{
name: 'Sign up',
icon: <SignUpIcon />,
value: 'signup',
href: '/signup'
},
...defaultPages
])
if (userData?.role === "admin") {
setPages((pages) => [
...pages,
{
name: 'admin',
icon: <SettingsIcon />,
value: 'admin',
href: '/admin'
}
])
}
// TODO: investigate deps causing infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile, isSignedIn, resolvedTheme, userData])
const onTabChange = useCallback((tab: string) => {
if (typeof window === 'undefined') return
const match = pages.find(page => page.value === tab)
if (match?.onClick) {
match.onClick()
}
}, [pages])
const getButton = useCallback((tab: Tab) => {
const activeStyle = router.pathname === tab.href ? styles.active : ""
if (tab.onClick) {
return <Button
auto={isMobile ? false : true}
key={tab.value}
icon={tab.icon}
onClick={() => onTabChange(tab.value)}
className={`${styles.tab} ${activeStyle}`}
shadow={false}
>
{tab.name ? tab.name : undefined}
</Button>
} else if (tab.href) {
return <Link key={tab.value} href={tab.href}>
<a className={styles.tab}>
<Button
className={activeStyle}
auto={isMobile ? false : true}
icon={tab.icon}
shadow={false}
>
{tab.name ? tab.name : undefined}
</Button>
</a>
</Link>
}
}, [isMobile, onTabChange, router.pathname])
const buttons = useMemo(() => pages.map(getButton), [pages, getButton])
return (
<Page.Header height={'var(--page-nav-height)'} marginBottom={2}>
<div className={styles.tabs}>
<div className={styles.buttons}>
{buttons}
</div>
</div>
<div className={styles.controls}>
<Button
effect={false}
auto
type="abort"
onClick={() => setExpanded(!expanded)}
aria-label="Menu"
>
<Spacer height={5 / 6} width={0} />
<MenuIcon />
</Button>
</div>
{isMobile && expanded && (<div className={styles.mobile}>
<ButtonGroup vertical style={{
background: "var(--bg)",
}}>
{buttons}
</ButtonGroup>
</div>)}
</Page.Header >
)
}
export default Header export default Header

View file

@ -1,4 +1,3 @@
import Header from "@components/header/header"
import PageSeo from "@components/page-seo" import PageSeo from "@components/page-seo"
import VisibilityBadge from "@components/badges/visibility-badge" import VisibilityBadge from "@components/badges/visibility-badge"
import DocumentComponent from '@components/view-document' import DocumentComponent from '@components/view-document'
@ -88,9 +87,6 @@ const PostPage = ({ post }: Props) => {
isPrivate={false} isPrivate={false}
/> />
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={homeStyles.main}> <Page.Content className={homeStyles.main}>
<div className={styles.header}> <div className={styles.header}>
<span className={styles.title}> <span className={styles.title}>

View file

@ -12,6 +12,7 @@ const useSignedIn = () => {
const router = useRouter() const router = useRouter()
const signin = (token: string) => { const signin = (token: string) => {
setSignedIn(true) setSignedIn(true)
// TODO: investigate SameSite / CORS cookie security
Cookies.set("drift-token", token) Cookies.set("drift-token", token)
} }

View file

@ -2,11 +2,8 @@ import '@styles/globals.css'
import type { AppProps as NextAppProps } from "next/app"; import type { AppProps as NextAppProps } from "next/app";
import 'react-loading-skeleton/dist/skeleton.css' import 'react-loading-skeleton/dist/skeleton.css'
import { SkeletonTheme } from 'react-loading-skeleton';
import Head from 'next/head'; import Head from 'next/head';
import { CssBaseline, GeistProvider, Themes } from '@geist-ui/core'; import { ThemeProvider } from 'next-themes'
import { useTheme, ThemeProvider } from 'next-themes'
import { useEffect } from 'react';
import App from '@components/app'; import App from '@components/app';
type AppProps<P = any> = { type AppProps<P = any> = {

View file

@ -11,7 +11,7 @@ export function middleware(req: NextRequest) {
!req.nextUrl.pathname.startsWith('/api') && !req.nextUrl.pathname.startsWith('/api') &&
// header added when next/link pre-fetches a route // header added when next/link pre-fetches a route
!req.headers.get('x-middleware-preflight') !req.headers.get('x-middleware-preflight')
if (isPageRequest) {
if (pathname === '/signout') { if (pathname === '/signout') {
// If you're signed in we remove the cookie and redirect to the home page // If you're signed in we remove the cookie and redirect to the home page
// If you're not signed in we redirect to the home page // If you're not signed in we redirect to the home page
@ -22,7 +22,8 @@ export function middleware(req: NextRequest) {
return resp return resp
} }
} else if (pathname === '/') { } else if (isPageRequest) {
if (pathname === '/') {
if (signedIn) { if (signedIn) {
return NextResponse.redirect(getURL('new')) return NextResponse.redirect(getURL('new'))
} }

View file

@ -20,9 +20,6 @@ const AdminPage = () => {
}, [router, signedIn]) }, [router, signedIn])
return ( return (
<Page className={styles.wrapper}> <Page className={styles.wrapper}>
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<Admin /> <Admin />
</Page.Content> </Page.Content>

View file

@ -5,7 +5,6 @@ import styles from '@styles/Home.module.css'
const Expired = () => { const Expired = () => {
return ( return (
<Page> <Page>
<Header />
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<Note type="error" label={false}> <Note type="error" label={false}>
<Text h4>Error: The Drift you&apos;re trying to view has expired.</Text> <Text h4>Error: The Drift you&apos;re trying to view has expired.</Text>

View file

@ -5,6 +5,8 @@ import HomeComponent from '@components/home'
import { Page, Text, Spacer, Tabs, Textarea, Card } from '@geist-ui/core' import { Page, Text, Spacer, Tabs, Textarea, Card } from '@geist-ui/core'
export async function getStaticProps() { export async function getStaticProps() {
try {
const resp = await fetch(process.env.API_URL + `/welcome`, const resp = await fetch(process.env.API_URL + `/welcome`,
{ {
method: "GET", method: "GET",
@ -22,23 +24,29 @@ export async function getStaticProps() {
introTitle: title || null, introTitle: title || null,
} }
} }
} catch (error) {
return {
props: {
error: true
}
}
}
} }
type Props = { type Props = {
introContent: string introContent: string
introTitle: string introTitle: string
rendered: string rendered: string
error?: boolean
} }
const Home = ({ rendered, introContent, introTitle }: Props) => { const Home = ({ rendered, introContent, introTitle, error }: Props) => {
return ( return (
<Page className={styles.wrapper}> <Page className={styles.wrapper}>
<PageSeo /> <PageSeo />
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<HomeComponent rendered={rendered} introContent={introContent} introTitle={introTitle} /> {error && <Text>Something went wrong. Is the server running?</Text>}
{!error && <HomeComponent rendered={rendered} introContent={introContent} introTitle={introTitle} />}
</Page.Content> </Page.Content>
</Page> </Page>
) )

View file

@ -10,9 +10,6 @@ import { Page } from '@geist-ui/core';
const Home = ({ morePosts, posts, error }: { morePosts: boolean, posts: Post[]; error: boolean; }) => { const Home = ({ morePosts, posts, error }: { morePosts: boolean, posts: Post[]; error: boolean; }) => {
return ( return (
<Page className={styles.wrapper}> <Page className={styles.wrapper}>
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<MyPosts morePosts={morePosts} error={error} posts={posts} /> <MyPosts morePosts={morePosts} error={error} posts={posts} />
</Page.Content> </Page.Content>

View file

@ -24,10 +24,6 @@ const NewFromExisting = ({
{/* eslint-disable-next-line @next/next/no-css-tags */} {/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/css/react-datepicker.css" /> <link rel="stylesheet" href="/css/react-datepicker.css" />
</Head> </Head>
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<NewPost initialPost={post} newPostParent={parentId} /> <NewPost initialPost={post} newPostParent={parentId} />
</Page.Content> </Page.Content>

View file

@ -14,10 +14,6 @@ const New = () => {
{/* eslint-disable-next-line @next/next/no-css-tags */} {/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/css/react-datepicker.css" /> <link rel="stylesheet" href="/css/react-datepicker.css" />
</Head> </Head>
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<NewPost /> <NewPost />
</Page.Content> </Page.Content>

View file

@ -1,15 +1,10 @@
import { Page } from '@geist-ui/core'; import { Page } from '@geist-ui/core';
import PageSeo from "@components/page-seo"; import PageSeo from "@components/page-seo";
import Auth from "@components/auth"; import Auth from "@components/auth";
import Header from "@components/header/header";
import styles from '@styles/Home.module.css' import styles from '@styles/Home.module.css'
const SignIn = () => ( const SignIn = () => (
<Page width={"100%"}> <Page width={"100%"}>
<PageSeo title="Drift - Sign In" /> <PageSeo title="Drift - Sign In" />
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<Auth page="signin" /> <Auth page="signin" />
</Page.Content> </Page.Content>

View file

@ -1,16 +1,11 @@
import { Page } from '@geist-ui/core'; import { Page } from '@geist-ui/core';
import Auth from "@components/auth"; import Auth from "@components/auth";
import Header from "@components/header/header";
import PageSeo from '@components/page-seo'; import PageSeo from '@components/page-seo';
import styles from '@styles/Home.module.css' import styles from '@styles/Home.module.css'
const SignUp = () => ( const SignUp = () => (
<Page width="100%"> <Page width="100%">
<PageSeo title="Drift - Sign Up" /> <PageSeo title="Drift - Sign Up" />
<Page.Header>
<Header />
</Page.Header>
<Page.Content className={styles.main}> <Page.Content className={styles.main}>
<Auth page="signup" /> <Auth page="signup" />
</Page.Content> </Page.Content>