remove next-themes, convert header to custom button
This commit is contained in:
parent
bff7c90e5f
commit
dfe0d39fa0
21 changed files with 281 additions and 160 deletions
|
@ -36,6 +36,11 @@
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* when data-theme on html is light, change font color */
|
||||||
|
[data-theme="light"] .warning {
|
||||||
|
color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ const ButtonDropdown: React.FC<
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.main} ${className}`}
|
className={`${styles.main} ${className || ""}`}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
|
|
|
@ -46,8 +46,13 @@
|
||||||
margin-left: var(--gap-half);
|
margin-left: var(--gap-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconLeft {
|
||||||
|
margin-right: var(--gap-half);
|
||||||
|
}
|
||||||
|
|
||||||
.icon svg {
|
.icon svg {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
transform: scale(1.2) translateY(-0.05em);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ type Props = React.HTMLProps<HTMLButtonElement> & {
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
iconRight?: React.ReactNode
|
iconRight?: React.ReactNode
|
||||||
|
iconLeft?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
|
@ -20,6 +21,7 @@ const Button = forwardRef<HTMLButtonElement, Props>(
|
||||||
type = "button",
|
type = "button",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
iconRight,
|
iconRight,
|
||||||
|
iconLeft,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
|
@ -27,11 +29,16 @@ const Button = forwardRef<HTMLButtonElement, Props>(
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`${styles.button} ${styles[type]} ${className}`}
|
className={`${styles.button} ${styles[type]} ${className || ""}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
{iconLeft && (
|
||||||
|
<span className={`${styles.icon} ${styles.iconLeft}`}>
|
||||||
|
{iconLeft}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
{iconRight && (
|
{iconRight && (
|
||||||
<span className={`${styles.icon} ${styles.iconRight}`}>
|
<span className={`${styles.icon} ${styles.iconRight}`}>
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default function Card({
|
||||||
className?: string
|
className?: string
|
||||||
} & React.ComponentProps<"div">) {
|
} & React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.card} ${className}`} {...props}>
|
<div className={`${styles.card} ${className || ""}`} {...props}>
|
||||||
<div className={styles.content}>{children}</div>
|
<div className={styles.content}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
import MoonIcon from "@geist-ui/icons/moon"
|
import MoonIcon from "@geist-ui/icons/moon"
|
||||||
import SunIcon from "@geist-ui/icons/sun"
|
import SunIcon from "@geist-ui/icons/sun"
|
||||||
// import { useAllThemes, useTheme } from '@geist-ui/core'
|
|
||||||
import styles from "./header.module.css"
|
import styles from "./header.module.css"
|
||||||
import { Select } from "@geist-ui/core/dist"
|
import { Select } from "@geist-ui/core/dist"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "@components/theme/ThemeClientContextProvider"
|
||||||
|
|
||||||
const Controls = () => {
|
const Controls = () => {
|
||||||
const [mounted, setMounted] = useState(false)
|
const { theme, setTheme } = useTheme()
|
||||||
const { resolvedTheme, setTheme } = useTheme()
|
|
||||||
useEffect(() => setMounted(true), [])
|
|
||||||
if (!mounted) return null
|
|
||||||
const switchThemes = () => {
|
const switchThemes = () => {
|
||||||
if (resolvedTheme === "dark") {
|
if (theme === "dark") {
|
||||||
setTheme("light")
|
setTheme("light")
|
||||||
} else {
|
} else {
|
||||||
setTheme("dark")
|
setTheme("dark")
|
||||||
|
@ -26,7 +22,7 @@ const Controls = () => {
|
||||||
h="28px"
|
h="28px"
|
||||||
pure
|
pure
|
||||||
onChange={switchThemes}
|
onChange={switchThemes}
|
||||||
value={resolvedTheme}
|
value={theme}
|
||||||
>
|
>
|
||||||
<Select.Option value="light">
|
<Select.Option value="light">
|
||||||
<span className={styles.selectContent}>
|
<span className={styles.selectContent}>
|
||||||
|
|
|
@ -11,20 +11,37 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs .buttons > button,
|
.tabs .buttons button {
|
||||||
.tabs .buttons > a > button {
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs .active {
|
.tabs .buttons > a:hover,
|
||||||
border-bottom: 1px solid var(--darker-gray) !important;
|
.tabs .buttons > button:hover {
|
||||||
|
color: var(--fg);
|
||||||
|
box-shadow: inset 0 -1px 0 var(--fg);
|
||||||
|
transition: box-shadow 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs a:active,
|
||||||
|
.tabs a:focus,
|
||||||
|
.tabs button:active,
|
||||||
|
.tabs button:focus {
|
||||||
|
color: var(--darker-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile button {
|
||||||
|
z-index: 1000;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ButtonGroup,
|
|
||||||
Button,
|
|
||||||
Page,
|
Page,
|
||||||
Spacer,
|
|
||||||
useBodyScroll,
|
useBodyScroll,
|
||||||
useMediaQuery
|
useMediaQuery
|
||||||
} from "@geist-ui/core/dist"
|
} from "@geist-ui/core/dist"
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import styles from "./header.module.css"
|
import styles from "./header.module.css"
|
||||||
|
|
||||||
import HomeIcon from "@geist-ui/icons/home"
|
import HomeIcon from "@geist-ui/icons/home"
|
||||||
|
@ -23,11 +20,12 @@ import YourIcon from "@geist-ui/icons/list"
|
||||||
import MoonIcon from "@geist-ui/icons/moon"
|
import MoonIcon from "@geist-ui/icons/moon"
|
||||||
import SettingsIcon from "@geist-ui/icons/settings"
|
import SettingsIcon from "@geist-ui/icons/settings"
|
||||||
import SunIcon from "@geist-ui/icons/sun"
|
import SunIcon from "@geist-ui/icons/sun"
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
// import useUserData from "@lib/hooks/use-user-data"
|
// import useUserData from "@lib/hooks/use-user-data"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { signOut } from "next-auth/react"
|
import { signOut } from "next-auth/react"
|
||||||
|
import { useTheme } from "@components/theme/ThemeClientContextProvider"
|
||||||
|
import Button from "@components/button"
|
||||||
|
|
||||||
type Tab = {
|
type Tab = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -44,8 +42,7 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
|
||||||
const isMobile = useMediaQuery("xs", { match: "down" })
|
const isMobile = useMediaQuery("xs", { match: "down" })
|
||||||
// const { status } = useSession()
|
// const { status } = useSession()
|
||||||
// const signedIn = status === "authenticated"
|
// const signedIn = status === "authenticated"
|
||||||
const { setTheme, resolvedTheme } = useTheme()
|
const { setTheme, theme } = useTheme()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBodyHidden(expanded)
|
setBodyHidden(expanded)
|
||||||
}, [expanded, setBodyHidden])
|
}, [expanded, setBodyHidden])
|
||||||
|
@ -68,16 +65,16 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
|
||||||
name: isMobile ? "Change theme" : "",
|
name: isMobile ? "Change theme" : "",
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
if (typeof window !== "undefined")
|
if (typeof window !== "undefined")
|
||||||
setTheme(resolvedTheme === "light" ? "dark" : "light")
|
setTheme(theme === "light" ? "dark" : "light")
|
||||||
},
|
},
|
||||||
icon: resolvedTheme === "light" ? <MoonIcon /> : <SunIcon />,
|
icon: theme === "light" ? <MoonIcon /> : <SunIcon />,
|
||||||
value: "theme"
|
value: "theme"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
defaultPages.push({
|
defaultPages.push({
|
||||||
name: "admin",
|
name: "Admin",
|
||||||
icon: <SettingsIcon />,
|
icon: <SettingsIcon />,
|
||||||
value: "admin",
|
value: "admin",
|
||||||
href: "/admin"
|
href: "/admin"
|
||||||
|
@ -87,25 +84,25 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
|
||||||
if (signedIn)
|
if (signedIn)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "new",
|
name: "New",
|
||||||
icon: <NewIcon />,
|
icon: <NewIcon />,
|
||||||
value: "new",
|
value: "new",
|
||||||
href: "/new"
|
href: "/new"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "yours",
|
name: "Yours",
|
||||||
icon: <YourIcon />,
|
icon: <YourIcon />,
|
||||||
value: "yours",
|
value: "yours",
|
||||||
href: "/mine"
|
href: "/mine"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "Settings",
|
||||||
icon: <SettingsIcon />,
|
icon: <SettingsIcon />,
|
||||||
value: "settings",
|
value: "settings",
|
||||||
href: "/settings"
|
href: "/settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sign out",
|
name: "Sign Out",
|
||||||
icon: <SignOutIcon />,
|
icon: <SignOutIcon />,
|
||||||
value: "signout",
|
value: "signout",
|
||||||
onClick: () => signOut()
|
onClick: () => signOut()
|
||||||
|
@ -115,7 +112,7 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
|
||||||
else
|
else
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "home",
|
name: "Home",
|
||||||
icon: <HomeIcon />,
|
icon: <HomeIcon />,
|
||||||
value: "home",
|
value: "home",
|
||||||
href: "/"
|
href: "/"
|
||||||
|
@ -147,26 +144,29 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getButton = (tab: Tab) => {
|
const getButton = (tab: Tab) => {
|
||||||
const activeStyle = pathname === tab.href ? styles.active : ""
|
const isActive = pathname === tab.href
|
||||||
|
const activeStyle = isActive ? styles.active : ""
|
||||||
if (tab.onClick) {
|
if (tab.onClick) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
auto={isMobile ? false : true}
|
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
icon={tab.icon}
|
iconLeft={tab.icon}
|
||||||
onClick={() => onTabChange(tab.value)}
|
onClick={() => onTabChange(tab.value)}
|
||||||
className={`${styles.tab} ${activeStyle}`}
|
className={`${styles.tab} ${activeStyle}`}
|
||||||
shadow={false}
|
aria-label={tab.name}
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
>
|
>
|
||||||
{tab.name ? tab.name : undefined}
|
{tab.name ? tab.name : undefined}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
} else if (tab.href) {
|
} else if (tab.href) {
|
||||||
return (
|
return (
|
||||||
<Link key={tab.value} href={tab.href} className={styles.tab}>
|
<Link
|
||||||
<Button auto={isMobile ? false : true} icon={tab.icon} shadow={false}>
|
key={tab.value}
|
||||||
{tab.name ? tab.name : undefined}
|
href={tab.href}
|
||||||
</Button>
|
className={`${styles.tab} ${activeStyle}`}
|
||||||
|
>
|
||||||
|
<Button iconLeft={tab.icon}>{tab.name ? tab.name : undefined}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -180,28 +180,14 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
|
||||||
<div className={styles.buttons}>{buttons}</div>
|
<div className={styles.buttons}>{buttons}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
<Button
|
<Button onClick={() => setExpanded(!expanded)} aria-label="Menu">
|
||||||
effect={false}
|
|
||||||
auto
|
|
||||||
type="abort"
|
|
||||||
onClick={() => setExpanded(!expanded)}
|
|
||||||
aria-label="Menu"
|
|
||||||
>
|
|
||||||
<Spacer height={5 / 6} width={0} />
|
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* setExpanded should occur elsewhere; we don't want to close if they change themes */}
|
{/* setExpanded should occur elsewhere; we don't want to close if they change themes */}
|
||||||
{isMobile && expanded && (
|
{isMobile && expanded && (
|
||||||
<div className={styles.mobile} onClick={() => setExpanded(!expanded)}>
|
<div className={styles.mobile} onClick={() => setExpanded(!expanded)}>
|
||||||
<ButtonGroup
|
{buttons}
|
||||||
vertical
|
|
||||||
style={{
|
|
||||||
background: "var(--bg)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{buttons}
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
|
|
5
client/app/components/page/index.tsx
Normal file
5
client/app/components/page/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
|
export default function Page({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className={styles.page}>{children}</div>
|
||||||
|
}
|
10
client/app/components/page/page.module.css
Normal file
10
client/app/components/page/page.module.css
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.page {
|
||||||
|
max-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0 calc(1.34 * 16px) 0 calc(1.34 * 16px);
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
'use client';
|
"use client"
|
||||||
import { Fieldset, Text, Divider } from "@geist-ui/core/dist"
|
import Card from "@components/card"
|
||||||
import styles from "./settings-group.module.css"
|
import styles from "./settings-group.module.css"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -9,13 +9,11 @@ type Props = {
|
||||||
|
|
||||||
const SettingsGroup = ({ title, children }: Props) => {
|
const SettingsGroup = ({ title, children }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Fieldset width={'100%'}>
|
<Card>
|
||||||
<Fieldset.Content>
|
<h4>{title}</h4>
|
||||||
<Text h4>{title}</Text>
|
<hr />
|
||||||
</Fieldset.Content>
|
<div className={styles.content}>{children}</div>
|
||||||
<Divider />
|
</Card>
|
||||||
<Fieldset.Content className={styles.content}>{children}</Fieldset.Content>
|
|
||||||
</Fieldset>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
62
client/app/components/theme/ThemeClientContextProvider.tsx
Normal file
62
client/app/components/theme/ThemeClientContextProvider.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
FunctionComponent,
|
||||||
|
PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
|
useMemo
|
||||||
|
} from "react"
|
||||||
|
import React, { useContext, useState, createContext } from "react"
|
||||||
|
import { DEFAULT_THEME, Theme, THEME_COOKIE_NAME } from "./theme"
|
||||||
|
import { setCookie } from "cookies-next"
|
||||||
|
|
||||||
|
interface UseThemeProps {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<UseThemeProps | null>(null)
|
||||||
|
|
||||||
|
export function useTheme(): {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
} {
|
||||||
|
return (
|
||||||
|
useContext(ThemeContext) || {
|
||||||
|
theme: DEFAULT_THEME,
|
||||||
|
setTheme: () => {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends PropsWithChildren<{}> {
|
||||||
|
defaultTheme: Theme
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeClientContextProvider: FunctionComponent<Props> = ({
|
||||||
|
defaultTheme,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const [theme, setThemeState] = useState<Theme>(defaultTheme)
|
||||||
|
const setCookieAndDocument = useCallback(
|
||||||
|
(theme: Theme) => {
|
||||||
|
setThemeState(theme)
|
||||||
|
setCookie(THEME_COOKIE_NAME, theme)
|
||||||
|
document.documentElement.setAttribute("data-theme", theme)
|
||||||
|
},
|
||||||
|
[setThemeState]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setTheme = useCallback(
|
||||||
|
(theme: Theme) => {
|
||||||
|
setCookieAndDocument(theme)
|
||||||
|
},
|
||||||
|
[setCookieAndDocument]
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme])
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeClientContextProvider
|
26
client/app/components/theme/ThemeProvider.tsx
Normal file
26
client/app/components/theme/ThemeProvider.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import type { FunctionComponent, PropsWithChildren } from "react";
|
||||||
|
import ThemeClientContextProvider from "./ThemeClientContextProvider";
|
||||||
|
import ThemeServerContextProvider, {
|
||||||
|
useServerTheme,
|
||||||
|
} from "./ThemeServerContextProvider";
|
||||||
|
|
||||||
|
const ThemeProviderWrapper: FunctionComponent<PropsWithChildren<{}>> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const theme = useServerTheme();
|
||||||
|
return (
|
||||||
|
<ThemeClientContextProvider defaultTheme={theme}>
|
||||||
|
{children}
|
||||||
|
</ThemeClientContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeProvider: FunctionComponent<PropsWithChildren<{}>> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<ThemeServerContextProvider>
|
||||||
|
<ThemeProviderWrapper>{children}</ThemeProviderWrapper>
|
||||||
|
</ThemeServerContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProvider;
|
24
client/app/components/theme/ThemeServerContextProvider.tsx
Normal file
24
client/app/components/theme/ThemeServerContextProvider.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import type { FunctionComponent, PropsWithChildren } from "react";
|
||||||
|
// @ts-ignore -- createServerContext is not in @types/react atm
|
||||||
|
import { useContext, createServerContext } from "react";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { Theme, THEME_COOKIE_NAME } from "./theme";
|
||||||
|
import { DEFAULT_THEME } from "./theme";
|
||||||
|
|
||||||
|
const ThemeContext = createServerContext<Theme | null>(null);
|
||||||
|
|
||||||
|
export function useServerTheme(): Theme {
|
||||||
|
return useContext(ThemeContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeServerContextProvider: FunctionComponent<PropsWithChildren<{}>> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const cookiesList = cookies();
|
||||||
|
const theme = cookiesList.get(THEME_COOKIE_NAME)?.value ?? DEFAULT_THEME;
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeServerContextProvider;
|
5
client/app/components/theme/theme.tsx
Normal file
5
client/app/components/theme/theme.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type Theme = "light" | "dark";
|
||||||
|
|
||||||
|
export const DEFAULT_THEME: Theme = "light";
|
||||||
|
|
||||||
|
export const THEME_COOKIE_NAME = "drift-theme";
|
|
@ -1,6 +1,4 @@
|
||||||
"use client"
|
import Card from "@components/card"
|
||||||
|
|
||||||
import { Card } from "@geist-ui/core/dist"
|
|
||||||
import * as RadixTooltip from "@radix-ui/react-tooltip"
|
import * as RadixTooltip from "@radix-ui/react-tooltip"
|
||||||
import "./tooltip.css"
|
import "./tooltip.css"
|
||||||
|
|
||||||
|
@ -16,14 +14,12 @@ const Tooltip = ({
|
||||||
} & RadixTooltip.TooltipProps) => {
|
} & RadixTooltip.TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<RadixTooltip.Root {...props}>
|
<RadixTooltip.Root {...props}>
|
||||||
<RadixTooltip.Trigger asChild className={className}>{children}</RadixTooltip.Trigger>
|
<RadixTooltip.Trigger asChild className={className}>
|
||||||
|
{children}
|
||||||
|
</RadixTooltip.Trigger>
|
||||||
|
|
||||||
<RadixTooltip.Content>
|
<RadixTooltip.Content>
|
||||||
<Card className="tooltip">
|
<Card className="tooltip">{content}</Card>
|
||||||
<Card.Body margin={0} padding={1 / 2}>
|
|
||||||
{content}
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
</RadixTooltip.Content>
|
</RadixTooltip.Content>
|
||||||
</RadixTooltip.Root>
|
</RadixTooltip.Root>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import "@styles/globals.css"
|
import "@styles/globals.css"
|
||||||
import { ServerThemeProvider } from "next-themes"
|
|
||||||
import { LayoutWrapper } from "./root-layout-wrapper"
|
import { LayoutWrapper } from "./root-layout-wrapper"
|
||||||
import styles from '@styles/Home.module.css';
|
import styles from "@styles/Home.module.css"
|
||||||
import { cookies } from "next/headers";
|
import { getSession } from "@lib/server/session"
|
||||||
import { getSession } from "@lib/server/session";
|
import ThemeProvider from "@components/theme/ThemeProvider"
|
||||||
|
import { THEME_COOKIE_NAME } from "@components/theme/theme"
|
||||||
|
import { useServerTheme } from "@components/theme/ThemeServerContextProvider"
|
||||||
|
|
||||||
interface RootLayoutProps {
|
interface RootLayoutProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -13,19 +14,33 @@ export default async function RootLayout({ children }: RootLayoutProps) {
|
||||||
// TODO: this opts out of SSG
|
// TODO: this opts out of SSG
|
||||||
const session = await getSession()
|
const session = await getSession()
|
||||||
return (
|
return (
|
||||||
<ServerThemeProvider
|
<html lang="en">
|
||||||
disableTransitionOnChange
|
<head>
|
||||||
attribute="data-theme"
|
<script
|
||||||
enableColorScheme
|
dangerouslySetInnerHTML={{
|
||||||
>
|
__html: `
|
||||||
<html lang="en">
|
(function() {
|
||||||
<head>
|
var theme = document.cookie
|
||||||
|
.split('; ')
|
||||||
</head>
|
.find(row => row.startsWith('${THEME_COOKIE_NAME}='))
|
||||||
|
.split('=')[1];
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
console.log("theme on load", theme)
|
||||||
|
})();
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<ThemeProvider>
|
||||||
<body className={styles.main}>
|
<body className={styles.main}>
|
||||||
<LayoutWrapper signedIn={Boolean(session?.user)} isAdmin={session?.user.role === "admin"}>{children}</LayoutWrapper>
|
<LayoutWrapper
|
||||||
|
signedIn={Boolean(session?.user)}
|
||||||
|
isAdmin={session?.user.role === "admin"}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LayoutWrapper>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</ThemeProvider>
|
||||||
</ServerThemeProvider>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +1,24 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Header from "@components/header"
|
import Header from "@components/header"
|
||||||
import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist"
|
import Page from "@components/page"
|
||||||
import { ThemeProvider } from "next-themes"
|
|
||||||
import * as RadixTooltip from "@radix-ui/react-tooltip"
|
import * as RadixTooltip from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
export function LayoutWrapper({
|
export function LayoutWrapper({
|
||||||
children,
|
children,
|
||||||
signedIn,
|
signedIn,
|
||||||
isAdmin,
|
isAdmin
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
signedIn?: boolean
|
signedIn?: boolean
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
}) {
|
}) {
|
||||||
const customTheme = Themes.createFromLight({
|
|
||||||
type: "custom",
|
|
||||||
palette: {
|
|
||||||
background: "var(--bg)",
|
|
||||||
foreground: "var(--fg)",
|
|
||||||
accents_1: "var(--lightest-gray)",
|
|
||||||
accents_2: "var(--lighter-gray)",
|
|
||||||
accents_3: "var(--light-gray)",
|
|
||||||
accents_4: "var(--gray)",
|
|
||||||
accents_5: "var(--darker-gray)",
|
|
||||||
accents_6: "var(--darker-gray)",
|
|
||||||
accents_7: "var(--darkest-gray)",
|
|
||||||
accents_8: "var(--darkest-gray)",
|
|
||||||
border: "var(--light-gray)",
|
|
||||||
warning: "var(--warning)"
|
|
||||||
},
|
|
||||||
expressiveness: {
|
|
||||||
dropdownBoxShadow: "0 0 0 1px var(--lighter-gray)",
|
|
||||||
shadowSmall: "0 0 0 1px var(--lighter-gray)",
|
|
||||||
shadowLarge: "0 0 0 1px var(--lighter-gray)",
|
|
||||||
shadowMedium: "0 0 0 1px var(--lighter-gray)"
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
gap: "var(--gap)",
|
|
||||||
gapHalf: "var(--gap-half)",
|
|
||||||
gapQuarter: "var(--gap-quarter)",
|
|
||||||
gapNegative: "var(--gap-negative)",
|
|
||||||
gapHalfNegative: "var(--gap-half-negative)",
|
|
||||||
gapQuarterNegative: "var(--gap-quarter-negative)",
|
|
||||||
radius: "var(--radius)"
|
|
||||||
},
|
|
||||||
font: {
|
|
||||||
mono: "var(--font-mono)",
|
|
||||||
sans: "var(--font-sans)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadixTooltip.Provider delayDuration={200}>
|
<RadixTooltip.Provider delayDuration={200}>
|
||||||
<GeistProvider themes={[customTheme]} themeType={"custom"}>
|
<Page>
|
||||||
<ThemeProvider
|
<Header isAdmin={isAdmin} signedIn={signedIn} />
|
||||||
disableTransitionOnChange
|
{children}
|
||||||
cookieName="drift-theme"
|
</Page>
|
||||||
attribute="data-theme"
|
|
||||||
>
|
|
||||||
<CssBaseline />
|
|
||||||
<Page width={"100%"}>
|
|
||||||
<Page.Header>
|
|
||||||
<Header isAdmin={isAdmin} signedIn={signedIn} />
|
|
||||||
</Page.Header>
|
|
||||||
{children}
|
|
||||||
</Page>
|
|
||||||
</ThemeProvider>
|
|
||||||
</GeistProvider>
|
|
||||||
</RadixTooltip.Provider>
|
</RadixTooltip.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
--border: var(--lighter-gray);
|
--border: var(--lighter-gray);
|
||||||
--warning: rgb(27, 134, 23);
|
--warning: rgb(27, 134, 23);
|
||||||
--link: #3291ff;
|
--link: #3291ff;
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
|
@ -69,8 +70,10 @@
|
||||||
--header-bg: rgba(255, 255, 255, 0.8);
|
--header-bg: rgba(255, 255, 255, 0.8);
|
||||||
--gray-alpha: rgba(19, 20, 21, 0.5);
|
--gray-alpha: rgba(19, 20, 21, 0.5);
|
||||||
--selection: var(0, 0, 0, .6);
|
--selection: var(0, 0, 0, .6);
|
||||||
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
@ -96,7 +99,6 @@ body {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/* TODO: this should be unnecessary. Overides the browser background for color-scheme while geist-ui catches up */
|
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +133,12 @@ code {
|
||||||
font-family: var(--font-mono) !important;
|
font-family: var(--font-mono) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--light-gray);
|
||||||
|
margin: var(--gap) 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
:root {
|
:root {
|
||||||
--bg: #fff;
|
--bg: #fff;
|
||||||
|
@ -166,3 +174,4 @@ main {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"next": "13.0.3",
|
"next": "13.0.3",
|
||||||
"next-auth": "^4.16.4",
|
"next-auth": "^4.16.4",
|
||||||
"next-themes": "npm:@wits/next-themes@0.2.7",
|
"next-themes": "^0.2.1",
|
||||||
"rc-table": "7.24.1",
|
"rc-table": "7.24.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-datepicker": "4.8.0",
|
"react-datepicker": "4.8.0",
|
||||||
|
@ -69,5 +69,9 @@
|
||||||
"components",
|
"components",
|
||||||
"lib"
|
"lib"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ specifiers:
|
||||||
katex: ^0.16.3
|
katex: ^0.16.3
|
||||||
next: 13.0.3
|
next: 13.0.3
|
||||||
next-auth: ^4.16.4
|
next-auth: ^4.16.4
|
||||||
next-themes: npm:@wits/next-themes@0.2.7
|
next-themes: ^0.2.1
|
||||||
next-unused: 0.0.6
|
next-unused: 0.0.6
|
||||||
prettier: 2.6.2
|
prettier: 2.6.2
|
||||||
prisma: ^4.6.1
|
prisma: ^4.6.1
|
||||||
|
@ -58,7 +58,7 @@ dependencies:
|
||||||
jest: 29.3.1_@types+node@17.0.23
|
jest: 29.3.1_@types+node@17.0.23
|
||||||
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
|
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
|
||||||
next-auth: 4.16.4_ogpkrxaz2lg6nectum6dl66tn4
|
next-auth: 4.16.4_ogpkrxaz2lg6nectum6dl66tn4
|
||||||
next-themes: /@wits/next-themes/0.2.7_ogpkrxaz2lg6nectum6dl66tn4
|
next-themes: 0.2.1_ogpkrxaz2lg6nectum6dl66tn4
|
||||||
rc-table: 7.24.1_biqbaboplfbrettd7655fr4n2y
|
rc-table: 7.24.1_biqbaboplfbrettd7655fr4n2y
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
|
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
|
||||||
|
@ -1594,18 +1594,6 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@wits/next-themes/0.2.7_ogpkrxaz2lg6nectum6dl66tn4:
|
|
||||||
resolution: {integrity: sha512-CpmNH3RRqf2w0i1Xbrz5GKNE/d5gMq1oBlGpofY9LWcjH225nUgrxP15wKRITRAbn68ERDbsBGEBiaRECTmQag==}
|
|
||||||
peerDependencies:
|
|
||||||
next: '*'
|
|
||||||
react: '*'
|
|
||||||
react-dom: '*'
|
|
||||||
dependencies:
|
|
||||||
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0_react@18.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/abbrev/1.1.1:
|
/abbrev/1.1.1:
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -5129,6 +5117,18 @@ packages:
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/next-themes/0.2.1_ogpkrxaz2lg6nectum6dl66tn4:
|
||||||
|
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
|
||||||
|
peerDependencies:
|
||||||
|
next: '*'
|
||||||
|
react: '*'
|
||||||
|
react-dom: '*'
|
||||||
|
dependencies:
|
||||||
|
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/next-unused/0.0.6:
|
/next-unused/0.0.6:
|
||||||
resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==}
|
resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
Loading…
Reference in a new issue