migrate header info back to client-side

This commit is contained in:
Max Leiter 2023-05-20 15:41:53 -07:00
parent dc11f8eb0c
commit c416f5d5e8
11 changed files with 79 additions and 108 deletions

View file

@ -4,15 +4,14 @@ import bundleAnalyzer from "@next/bundle-analyzer"
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
experimental: { experimental: {
// esmExternals: true, appDir: true
appDir: true,
}, },
rewrites() { rewrites() {
return [ return [
{ {
source: "/file/raw/:id", source: "/file/raw/:id",
destination: `/api/raw/:id` destination: `/api/raw/:id`
}, }
] ]
}, },
images: { images: {

View file

@ -1,6 +1,6 @@
"use client" "use client"
import { startTransition, Suspense, useState } from "react" import { useState } from "react"
import styles from "./auth.module.css" import styles from "./auth.module.css"
import Link from "../../../components/link" import Link from "../../../components/link"
import { signIn } from "next-auth/react" import { signIn } from "next-auth/react"
@ -52,10 +52,7 @@ function Auth({
}) })
setSubmitting(false) setSubmitting(false)
} else { } else {
startTransition(() => { router.push("/new")
router.push("/new")
router.refresh()
})
} }
} }
@ -75,10 +72,7 @@ function Auth({
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* Suspense boundary because useSearchParams causes static bailout */} <ErrorQueryParamsHandler />
<Suspense fallback={null}>
<ErrorQueryParamsHandler />
</Suspense>
<div className={styles.form}> <div className={styles.form}>
<div className={styles.formContentSpace}> <div className={styles.formContentSpace}>
<h1>Sign {signText}</h1> <h1>Sign {signText}</h1>

View file

@ -2,9 +2,9 @@
import { useToasts } from "@components/toasts" import { useToasts } from "@components/toasts"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useEffect } from "react" import { Suspense, useEffect } from "react"
export function ErrorQueryParamsHandler() { function _ErrorQueryParamsHandler() {
const queryParams = useSearchParams() const queryParams = useSearchParams()
const { setToast } = useToasts() const { setToast } = useToasts()
@ -19,3 +19,12 @@ export function ErrorQueryParamsHandler() {
return null return null
} }
export function ErrorQueryParamsHandler() {
/* Suspense boundary because useSearchParams causes static bailout */
return (
<Suspense fallback={null}>
<_ErrorQueryParamsHandler />
</Suspense>
)
}

View file

@ -6,10 +6,7 @@ import Header from "@components/header"
import { Inter } from "next/font/google" import { Inter } from "next/font/google"
import { getMetadata } from "src/app/lib/metadata" import { getMetadata } from "src/app/lib/metadata"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import { cookies } from "next/headers"
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" }) const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
import { THEME_COOKIE, DEFAULT_THEME, SIGNED_IN_COOKIE } from "@lib/constants"
import { Suspense } from "react"
const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false }) const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false })
@ -18,10 +15,6 @@ export default async function RootLayout({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const cookiesList = cookies()
const theme = cookiesList.get(THEME_COOKIE)?.value || DEFAULT_THEME
const isAuthenticated = Boolean(cookiesList.get(SIGNED_IN_COOKIE)?.value)
return ( return (
// suppressHydrationWarning is required because of next-themes // suppressHydrationWarning is required because of next-themes
<html lang="en" className={inter.variable} suppressHydrationWarning> <html lang="en" className={inter.variable} suppressHydrationWarning>
@ -30,9 +23,7 @@ export default async function RootLayout({
<Providers> <Providers>
<Layout> <Layout>
<CmdK /> <CmdK />
<Suspense fallback={<>Loading...</>}> <Header />
<Header theme={theme} isAuthenticated={isAuthenticated} />
</Suspense>
{children} {children}
</Layout> </Layout>
</Providers> </Providers>

View file

@ -2,7 +2,6 @@ import { Command } from "cmdk"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { FilePlus, Moon, Search, Settings, Sun } from "react-feather" import { FilePlus, Moon, Search, Settings, Sun } from "react-feather"
import { setDriftTheme } from "src/app/lib/set-theme"
import { CmdKPage } from ".." import { CmdKPage } from ".."
import Item from "../item" import Item from "../item"
@ -42,7 +41,7 @@ export default function HomePage({
<Item <Item
shortcut="T" shortcut="T"
onSelect={() => { onSelect={() => {
setDriftTheme(resolvedTheme === "dark" ? "light" : "dark", setTheme) setTheme(resolvedTheme === "dark" ? "light" : "dark")
}} }}
icon={resolvedTheme === "dark" ? <Sun /> : <Moon />} icon={resolvedTheme === "dark" ? <Sun /> : <Moon />}
> >

View file

@ -2,8 +2,8 @@
import { useSelectedLayoutSegments } from "next/navigation" import { useSelectedLayoutSegments } from "next/navigation"
import FadeIn from "@components/fade-in" import FadeIn from "@components/fade-in"
import { setDriftTheme } from "src/app/lib/set-theme"
import { import {
Circle,
Home, Home,
Moon, Moon,
PlusCircle, PlusCircle,
@ -18,6 +18,7 @@ import Link from "@components/link"
import { useSessionSWR } from "@lib/use-session-swr" import { useSessionSWR } from "@lib/use-session-swr"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import styles from "./buttons.module.css" import styles from "./buttons.module.css"
import { useEffect, useState } from "react"
// constant width for sign in / sign out buttons to avoid CLS // constant width for sign in / sign out buttons to avoid CLS
const SIGN_IN_WIDTH = 110 const SIGN_IN_WIDTH = 110
@ -38,27 +39,6 @@ type Tab = {
} }
) )
export function HeaderButtons({
isAuthenticated,
theme: initialTheme
}: {
isAuthenticated: boolean
theme: string
}) {
const { isAdmin, userId } = useSessionSWR()
const { resolvedTheme } = useTheme()
return (
<>
{getButtons({
isAuthenticated,
theme: resolvedTheme ? resolvedTheme : initialTheme,
isAdmin,
userId
})}
</>
)
}
function NavButton(tab: Tab) { function NavButton(tab: Tab) {
const segment = useSelectedLayoutSegments().slice(-1)[0] const segment = useSelectedLayoutSegments().slice(-1)[0]
const isActive = segment === tab.value.toLowerCase() const isActive = segment === tab.value.toLowerCase()
@ -89,32 +69,51 @@ function NavButton(tab: Tab) {
} }
} }
function ThemeButton({ theme }: { theme: string }) { function ThemeButton() {
const { setTheme } = useTheme() const { setTheme, resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return ( return (
<NavButton <>
name="Theme" {!mounted && (
icon={theme === "dark" ? <Sun /> : <Moon />} <NavButton
value="dark" name="Theme"
onClick={() => { icon={<Circle opacity={0.3} />}
setDriftTheme(theme === "dark" ? "light" : "dark", setTheme) value="dark"
}} href=""
key="theme" key="theme"
/> />
)}
{mounted && (
<NavButton
name="Theme"
icon={
<FadeIn>{resolvedTheme === "dark" ? <Sun /> : <Moon />}</FadeIn>
}
value="dark"
onClick={() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}}
key="theme"
/>
)}
</>
) )
} }
export function getButtons({ export function HeaderButtons(): JSX.Element {
isAuthenticated, const { isAdmin, isAuthenticated, userId } = useSessionSWR()
theme,
isAdmin, useEffect(() => {
userId if (isAuthenticated && !userId) {
}: { signOut()
isAuthenticated: boolean }
theme: string }, [isAuthenticated, userId])
isAdmin?: boolean
userId?: string
}) {
return ( return (
<> <>
<NavButton <NavButton
@ -145,7 +144,7 @@ export function getButtons({
href="/settings" href="/settings"
key="settings" key="settings"
/> />
<ThemeButton key="theme-button" theme={theme} /> <ThemeButton key="theme-button" />
{isAdmin && ( {isAdmin && (
<FadeIn> <FadeIn>
<NavButton <NavButton
@ -181,6 +180,16 @@ export function getButtons({
width={SIGN_IN_WIDTH} width={SIGN_IN_WIDTH}
/> />
)} )}
{isAuthenticated === undefined && (
<NavButton
name="Sign"
key="signin"
icon={<User />}
value="signin"
href="/signin"
width={SIGN_IN_WIDTH}
/>
)}
</> </>
) )
} }

View file

@ -2,21 +2,15 @@ import styles from "./header.module.css"
import { HeaderButtons } from "./buttons" import { HeaderButtons } from "./buttons"
import MobileHeader from "./mobile" import MobileHeader from "./mobile"
export default function Header({ export default function Header() {
theme,
isAuthenticated
}: {
theme: string
isAuthenticated: boolean
}) {
return ( return (
<header className={styles.header}> <header className={styles.header}>
<div className={styles.tabs}> <div className={styles.tabs}>
<div className={styles.buttons}> <div className={styles.buttons}>
<HeaderButtons isAuthenticated={isAuthenticated} theme={theme} /> <HeaderButtons />
</div> </div>
</div> </div>
<MobileHeader isAuthenticated={isAuthenticated} theme={theme} /> <MobileHeader />
</header> </header>
) )
} }

View file

@ -6,23 +6,9 @@ import Button from "@components/button"
import { Menu } from "react-feather" import { Menu } from "react-feather"
import clsx from "clsx" import clsx from "clsx"
import styles from "./mobile.module.css" import styles from "./mobile.module.css"
import { getButtons } from "./buttons" import { HeaderButtons } from "./buttons"
import { useSessionSWR } from "@lib/use-session-swr"
export default function MobileHeader({
isAuthenticated,
theme
}: {
isAuthenticated: boolean
theme: string
}) {
const { isAdmin } = useSessionSWR()
const buttons = getButtons({
isAuthenticated,
theme,
isAdmin
})
export default function MobileHeader() {
// TODO: this is a hack to close the radix ui menu when a next link is clicked // TODO: this is a hack to close the radix ui menu when a next link is clicked
const onClick = () => { const onClick = () => {
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })) document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }))
@ -40,7 +26,7 @@ export default function MobileHeader({
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content> <DropdownMenu.Content>
{buttons.props.children.map((button: JSX.Element) => ( {HeaderButtons().props.children.map((button: JSX.Element) => (
<DropdownMenu.Item <DropdownMenu.Item
key={`mobile-${button?.key}`} key={`mobile-${button?.key}`}
className={styles.dropdownItem} className={styles.dropdownItem}

View file

@ -1,9 +0,0 @@
import { THEME_COOKIE } from "@lib/constants"
import { Cookies } from "react-cookie"
const cookies = new Cookies()
export function setDriftTheme(theme: string, setter: (theme: string) => void) {
setter(theme)
cookies.set(THEME_COOKIE, theme, { path: "/" })
}

View file

@ -10,7 +10,6 @@ export function isAllowedVisibilityForWebpage(
export const DEFAULT_THEME = "dark" export const DEFAULT_THEME = "dark"
export const SIGNED_IN_COOKIE = "next-auth.session-token" export const SIGNED_IN_COOKIE = "next-auth.session-token"
export const THEME_COOKIE = "drift-theme"
// Code files for uploading with drag and drop and syntax highlighting // Code files for uploading with drag and drop and syntax highlighting
export const allowedFileTypes = [ export const allowedFileTypes = [

View file

@ -10,7 +10,7 @@ export function useSessionSWR(swrOpts: SWRConfiguration = {}) {
mutate mutate
} = useSWR<Session>("/api/auth/session", { } = useSWR<Session>("/api/auth/session", {
fetcher: (url) => fetch(url).then((res) => res.json()) as Promise<Session>, fetcher: (url) => fetch(url).then((res) => res.json()) as Promise<Session>,
revalidateOnFocus: false, revalidateOnFocus: true,
...swrOpts ...swrOpts
}) })