From a1fef656bbf5f0c37f105b8ddbf21ff3e084a50b Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Sat, 2 Apr 2022 00:45:26 -0700 Subject: [PATCH] client: refactor header component for improved SSR --- client/components/app/index.tsx | 8 +- client/components/auth/index.tsx | 2 +- client/components/header/header.module.css | 59 ++++-- client/components/header/header.tsx | 197 -------------------- client/components/header/index.tsx | 207 ++++++++++++++++++++- client/components/post-page/index.tsx | 4 - client/lib/hooks/use-signed-in.ts | 1 + client/pages/_app.tsx | 5 +- client/pages/_middleware.tsx | 23 +-- client/pages/admin.tsx | 3 - client/pages/expired.tsx | 1 - client/pages/index.tsx | 46 +++-- client/pages/mine.tsx | 3 - client/pages/new/from/[id].tsx | 4 - client/pages/new/index.tsx | 4 - client/pages/signin.tsx | 7 +- client/pages/signup.tsx | 5 - 17 files changed, 289 insertions(+), 290 deletions(-) delete mode 100644 client/components/header/header.tsx diff --git a/client/components/app/index.tsx b/client/components/app/index.tsx index baf28c09..cb6686a1 100644 --- a/client/components/app/index.tsx +++ b/client/components/app/index.tsx @@ -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 { SkeletonTheme } from "react-loading-skeleton" -import { useTheme } from 'next-themes' + const App = ({ Component, pageProps, @@ -50,9 +51,10 @@ const App = ({ } } ) - return ( + return ( +
) diff --git a/client/components/auth/index.tsx b/client/components/auth/index.tsx index 1bd4fb35..32017904 100644 --- a/client/components/auth/index.tsx +++ b/client/components/auth/index.tsx @@ -29,7 +29,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => { const res = await resp.json() setRequiresServerPassword(res.requiresPasscode) } else { - setErrorMsg("Something went wrong.") + setErrorMsg("Something went wrong. Is the server running?") } } } diff --git a/client/components/header/header.module.css b/client/components/header/header.module.css index e096d825..7be42e5a 100644 --- a/client/components/header/header.module.css +++ b/client/components/header/header.module.css @@ -1,43 +1,62 @@ .tabs { - flex: 1 1; - padding: 0 var(--gap); + justify-content: center; + 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 { - position: relative; - z-index: 2; + position: relative; + z-index: 1; } .controls { - display: none !important; + margin-top: var(--gap); + display: none !important; } @media only screen and (max-width: 650px) { - .tabs { - display: none; - } + .tabs { + display: none; + } - .controls { - display: block !important; - } + .controls { + display: block !important; + } } .controls button:active, .controls button:focus, .controls button:hover { - outline: 1px solid rgba(0, 0, 0, 0.2); + outline: 1px solid rgba(0, 0, 0, 0.2); } .wrapper { - display: flex; - align-items: center; - width: min-content; + display: flex; + align-items: center; + width: min-content; } .selectContent { - width: auto; - height: 18px; - display: flex; - justify-content: space-between; - align-items: center; + width: auto; + height: 18px; + display: flex; + justify-content: space-between; + align-items: center; } diff --git a/client/components/header/header.tsx b/client/components/header/header.tsx deleted file mode 100644 index b7f121a1..00000000 --- a/client/components/header/header.tsx +++ /dev/null @@ -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(router.pathname === '/' ? 'home' : router.pathname.split('/')[1]); - const [expanded, setExpanded] = useState(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([]) - 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: , - value: "github" - }, - { - name: isMobile ? "Change theme" : "", - onClick: function () { - if (typeof window !== 'undefined') - setTheme(resolvedTheme === 'light' ? 'dark' : 'light'); - }, - icon: resolvedTheme === 'light' ? : , - value: "theme", - } - ] - - if (isSignedIn) - setPages([ - { - name: 'new', - icon: , - value: 'new', - href: '/new' - }, - { - name: 'yours', - icon: , - value: 'yours', - href: '/mine' - }, - // { - // name: 'settings', - // icon: , - // value: 'settings', - // href: '/settings' - // }, - { - name: 'sign out', - icon: , - value: 'signout', - onClick: signout - }, - ...defaultPages - ]) - else - setPages([ - { - name: 'home', - icon: , - value: 'home', - href: '/' - }, - { - name: 'Sign in', - icon: , - value: 'signin', - href: '/signin' - }, - { - name: 'Sign up', - icon: , - value: 'signup', - href: '/signup' - }, - ...defaultPages - ]) - if (userData?.role === "admin") { - setPages((pages) => [ - ...pages, - { - name: 'admin', - icon: , - 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 ( - -
- - {pages.map((tab) => { - return {tab.icon} {tab.name}} - value={tab.value} - key={`${tab.value}`} - /> - })} - -
-
- -
- {isMobile && expanded && (
- - {pages.map((tab, index) => { - return - })} - -
)} -
- ) -} - -export default Header diff --git a/client/components/header/index.tsx b/client/components/header/index.tsx index 7f452668..dfb99714 100644 --- a/client/components/header/index.tsx +++ b/client/components/header/index.tsx @@ -1,10 +1,207 @@ -import dynamic from 'next/dynamic' -const Header = dynamic(import('./header'), { - // ssr: false, - // loading: () => , -}) +import { ButtonGroup, Button, Page, Spacer, useBodyScroll, useMediaQuery, } from "@geist-ui/core"; + +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(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([]) + 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: , + value: "github" + }, + { + name: isMobile ? "Change theme" : "", + onClick: function () { + if (typeof window !== 'undefined') + setTheme(resolvedTheme === 'light' ? 'dark' : 'light'); + }, + icon: resolvedTheme === 'light' ? : , + value: "theme", + } + ] + + if (isSignedIn) + setPages([ + { + name: 'new', + icon: , + value: 'new', + href: '/new' + }, + { + name: 'yours', + icon: , + value: 'yours', + href: '/mine' + }, + // { + // name: 'settings', + // icon: , + // value: 'settings', + // href: '/settings' + // }, + { + name: 'sign out', + icon: , + value: 'signout', + // onClick: signout, + href: '/signout' + }, + ...defaultPages + ]) + else + setPages([ + { + name: 'home', + icon: , + value: 'home', + href: '/' + }, + { + name: 'Sign in', + icon: , + value: 'signin', + href: '/signin' + }, + { + name: 'Sign up', + icon: , + value: 'signup', + href: '/signup' + }, + ...defaultPages + ]) + if (userData?.role === "admin") { + setPages((pages) => [ + ...pages, + { + name: 'admin', + icon: , + 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 + } else if (tab.href) { + return + + + + + } + }, [isMobile, onTabChange, router.pathname]) + + const buttons = useMemo(() => pages.map(getButton), [pages, getButton]) + + return ( + +
+
+ {buttons} +
+
+
+ +
+ {isMobile && expanded && (
+ + {buttons} + +
)} +
+ ) +} export default Header diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx index 19d933a7..d0ef2171 100644 --- a/client/components/post-page/index.tsx +++ b/client/components/post-page/index.tsx @@ -1,4 +1,3 @@ -import Header from "@components/header/header" import PageSeo from "@components/page-seo" import VisibilityBadge from "@components/badges/visibility-badge" import DocumentComponent from '@components/view-document' @@ -88,9 +87,6 @@ const PostPage = ({ post }: Props) => { isPrivate={false} /> - -
-
diff --git a/client/lib/hooks/use-signed-in.ts b/client/lib/hooks/use-signed-in.ts index 343609ba..75129d12 100644 --- a/client/lib/hooks/use-signed-in.ts +++ b/client/lib/hooks/use-signed-in.ts @@ -12,6 +12,7 @@ const useSignedIn = () => { const router = useRouter() const signin = (token: string) => { setSignedIn(true) + // TODO: investigate SameSite / CORS cookie security Cookies.set("drift-token", token) } diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index 5d46b912..5b1da879 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -2,11 +2,8 @@ import '@styles/globals.css' import type { AppProps as NextAppProps } from "next/app"; import 'react-loading-skeleton/dist/skeleton.css' -import { SkeletonTheme } from 'react-loading-skeleton'; import Head from 'next/head'; -import { CssBaseline, GeistProvider, Themes } from '@geist-ui/core'; -import { useTheme, ThemeProvider } from 'next-themes' -import { useEffect } from 'react'; +import { ThemeProvider } from 'next-themes' import App from '@components/app'; type AppProps

= { diff --git a/client/pages/_middleware.tsx b/client/pages/_middleware.tsx index 0de2bd35..dc514ca5 100644 --- a/client/pages/_middleware.tsx +++ b/client/pages/_middleware.tsx @@ -11,18 +11,19 @@ export function middleware(req: NextRequest) { !req.nextUrl.pathname.startsWith('/api') && // header added when next/link pre-fetches a route !req.headers.get('x-middleware-preflight') - if (isPageRequest) { - if (pathname === '/signout') { - // 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 (signedIn) { - const resp = NextResponse.redirect(getURL('')); - resp.clearCookie('drift-token'); - resp.clearCookie('drift-userid'); - return resp - } - } else if (pathname === '/') { + if (pathname === '/signout') { + // 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 (signedIn) { + const resp = NextResponse.redirect(getURL('')); + resp.clearCookie('drift-token'); + resp.clearCookie('drift-userid'); + + return resp + } + } else if (isPageRequest) { + if (pathname === '/') { if (signedIn) { return NextResponse.redirect(getURL('new')) } diff --git a/client/pages/admin.tsx b/client/pages/admin.tsx index 80184ea2..ff446919 100644 --- a/client/pages/admin.tsx +++ b/client/pages/admin.tsx @@ -20,9 +20,6 @@ const AdminPage = () => { }, [router, signedIn]) return ( - -

- diff --git a/client/pages/expired.tsx b/client/pages/expired.tsx index aecb1fff..af849faa 100644 --- a/client/pages/expired.tsx +++ b/client/pages/expired.tsx @@ -5,7 +5,6 @@ import styles from '@styles/Home.module.css' const Expired = () => { return ( -
Error: The Drift you're trying to view has expired. diff --git a/client/pages/index.tsx b/client/pages/index.tsx index d3a84bc4..8e8bdd4f 100644 --- a/client/pages/index.tsx +++ b/client/pages/index.tsx @@ -5,21 +5,30 @@ import HomeComponent from '@components/home' import { Page, Text, Spacer, Tabs, Textarea, Card } from '@geist-ui/core' export async function getStaticProps() { - const resp = await fetch(process.env.API_URL + `/welcome`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - "x-secret-key": process.env.SECRET_KEY || '' - } - }) + try { - const { title, content, rendered } = await resp.json() - return { - props: { - introContent: content || null, - rendered: rendered || null, - introTitle: title || null, + const resp = await fetch(process.env.API_URL + `/welcome`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-secret-key": process.env.SECRET_KEY || '' + } + }) + + const { title, content, rendered } = await resp.json() + return { + props: { + introContent: content || null, + rendered: rendered || null, + introTitle: title || null, + } + } + } catch (error) { + return { + props: { + error: true + } } } } @@ -28,17 +37,16 @@ type Props = { introContent: string introTitle: string rendered: string + error?: boolean } -const Home = ({ rendered, introContent, introTitle }: Props) => { +const Home = ({ rendered, introContent, introTitle, error }: Props) => { return ( - -
- - + {error && Something went wrong. Is the server running?} + {!error && } ) diff --git a/client/pages/mine.tsx b/client/pages/mine.tsx index 4c795fc0..a4abe3dc 100644 --- a/client/pages/mine.tsx +++ b/client/pages/mine.tsx @@ -10,9 +10,6 @@ import { Page } from '@geist-ui/core'; const Home = ({ morePosts, posts, error }: { morePosts: boolean, posts: Post[]; error: boolean; }) => { return ( - -
- diff --git a/client/pages/new/from/[id].tsx b/client/pages/new/from/[id].tsx index fd7e6436..314ab3fa 100644 --- a/client/pages/new/from/[id].tsx +++ b/client/pages/new/from/[id].tsx @@ -24,10 +24,6 @@ const NewFromExisting = ({ {/* eslint-disable-next-line @next/next/no-css-tags */} - -
- - diff --git a/client/pages/new/index.tsx b/client/pages/new/index.tsx index 4e092545..198aa8a8 100644 --- a/client/pages/new/index.tsx +++ b/client/pages/new/index.tsx @@ -14,10 +14,6 @@ const New = () => { {/* eslint-disable-next-line @next/next/no-css-tags */} - -
- - diff --git a/client/pages/signin.tsx b/client/pages/signin.tsx index 4e1ed7cb..08fdfaa2 100644 --- a/client/pages/signin.tsx +++ b/client/pages/signin.tsx @@ -1,19 +1,14 @@ import { Page } from '@geist-ui/core'; import PageSeo from "@components/page-seo"; import Auth from "@components/auth"; -import Header from "@components/header/header"; import styles from '@styles/Home.module.css' const SignIn = () => ( - - -
- ) -export default SignIn \ No newline at end of file +export default SignIn diff --git a/client/pages/signup.tsx b/client/pages/signup.tsx index 383f4619..ff673213 100644 --- a/client/pages/signup.tsx +++ b/client/pages/signup.tsx @@ -1,16 +1,11 @@ import { Page } from '@geist-ui/core'; import Auth from "@components/auth"; -import Header from "@components/header/header"; import PageSeo from '@components/page-seo'; import styles from '@styles/Home.module.css' const SignUp = () => ( - - -
-