migrate header info back to client-side
This commit is contained in:
parent
dc11f8eb0c
commit
c416f5d5e8
11 changed files with 79 additions and 108 deletions
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 />}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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: "/" })
|
|
||||||
}
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue