client: add next-themes support for CSS variables

This commit is contained in:
Max Leiter 2022-03-23 18:21:46 -07:00
parent 186d536175
commit 6045200ac4
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
17 changed files with 146 additions and 254 deletions

View file

@ -0,0 +1,25 @@
import { GeistProvider, CssBaseline } 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,
}: {
Component: NextComponentType<NextPageContext, any, any>
pageProps: any
}) => {
const skeletonBaseColor = 'var(--light-gray)'
const skeletonHighlightColor = 'var(--lighter-gray)'
const { theme } = useTheme();
return (<GeistProvider themeType={theme}>
<SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}>
<CssBaseline />
<Component {...pageProps} />
</SkeletonTheme>
</GeistProvider>)
}
export default App

View file

@ -16,15 +16,6 @@
height: var(--input-height); height: var(--input-height);
} }
/*
--input-height: 2.5rem;
--input-border: 1px solid var(--light-gray);
--input-border-focus: 1px solid var(--gray);
--input-border-error: 1px solid var(--red);
--input-bg: var(--bg);
--input-fg: var(--fg);
--input-placeholder-fg: var(--light-gray); */
.button:hover, .button:hover,
.button:focus { .button:focus {
outline: none; outline: none;

View file

@ -1,14 +1,22 @@
import React 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 { useAllThemes, useTheme } from '@geist-ui/core'
import styles from './header.module.css' import styles from './header.module.css'
import { ThemeProps } from '@lib/types'
import { Select } from '@geist-ui/core' import { Select } from '@geist-ui/core'
import { useTheme } from 'next-themes'
const Controls = ({ changeTheme, theme }: ThemeProps) => { const Controls = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => setMounted(true), [])
if (!mounted) return null
const switchThemes = () => { const switchThemes = () => {
changeTheme() if (theme === 'dark') {
setTheme('light')
} else {
setTheme('dark')
}
} }
return ( return (

View file

@ -16,7 +16,7 @@ import NewIcon from '@geist-ui/icons/plusCircle';
import YourIcon from '@geist-ui/icons/list' import YourIcon from '@geist-ui/icons/list'
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 useTheme from "@lib/hooks/use-theme"; import { useTheme } from "next-themes"
import { Button } from "@geist-ui/core"; import { Button } from "@geist-ui/core";
type Tab = { type Tab = {
@ -37,7 +37,7 @@ const Header = () => {
const isMobile = useMediaQuery('xs', { match: 'down' }) const isMobile = useMediaQuery('xs', { match: 'down' })
const { signedIn: isSignedIn, signout } = useSignedIn() const { signedIn: isSignedIn, signout } = useSignedIn()
const [pages, setPages] = useState<Tab[]>([]) const [pages, setPages] = useState<Tab[]>([])
const { changeTheme, theme } = useTheme() const { setTheme, theme } = useTheme()
useEffect(() => { useEffect(() => {
setBodyHidden(expanded) setBodyHidden(expanded)
}, [expanded, setBodyHidden]) }, [expanded, setBodyHidden])
@ -60,9 +60,8 @@ const Header = () => {
{ {
name: isMobile ? "Change theme" : "", name: isMobile ? "Change theme" : "",
onClick: function () { onClick: function () {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined')
changeTheme(); setTheme(theme === 'light' ? 'dark' : 'light');
}
}, },
icon: theme === 'light' ? <MoonIcon /> : <SunIcon />, icon: theme === 'light' ? <MoonIcon /> : <SunIcon />,
condition: true, condition: true,
@ -73,9 +72,9 @@ const Header = () => {
if (isSignedIn) if (isSignedIn)
setPages([ setPages([
{ {
name: 'home', name: 'new',
icon: <HomeIcon />, icon: <NewIcon />,
value: 'home', value: 'new',
href: '/' href: '/'
}, },
{ {
@ -116,58 +115,7 @@ const Header = () => {
]) ])
// TODO: investigate deps causing infinite loop // TODO: investigate deps causing infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [changeTheme, isMobile, isSignedIn, theme]) }, [isMobile, isSignedIn, theme])
// useEffect(() => {
// const pageList: Tab[] = [
// {
// name: "Home",
// href: "/",
// icon: <HomeIcon />,
// condition: !isSignedIn,
// value: "home"
// },
// {
// name: "New",
// href: "/new",
// icon: <NewIcon />,
// condition: isSignedIn,
// value: "new"
// },
// {
// name: "Yours",
// href: "/mine",
// icon: <YourIcon />,
// condition: isSignedIn,
// value: "mine"
// },
// {
// name: "Sign out",
// href: "/signout",
// icon: <SignOutIcon />,
// condition: isSignedIn,
// value: "signout"
// },
// {
// name: "Sign in",
// href: "/signin",
// icon: <SignInIcon />,
// condition: !isSignedIn,
// value: "signin"
// },
// {
// name: "Sign up",
// href: "/signup",
// icon: <SignUpIcon />,
// condition: !isSignedIn,
// value: "signup"
// },
// ]
// setPages(pageList.filter(page => page.condition))
// }, [changeTheme, isMobile, isSignedIn, theme])
const onTabChange = useCallback((tab: string) => { const onTabChange = useCallback((tab: string) => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return

View file

@ -1,4 +1,3 @@
import useTheme from "@lib/hooks/use-theme"
import { memo, useEffect, useState } from "react" import { memo, useEffect, useState } from "react"
import styles from './preview.module.css' import styles from './preview.module.css'
@ -13,17 +12,14 @@ type Props = {
const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => { const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
const [preview, setPreview] = useState<string>(content || "") const [preview, setPreview] = useState<string>(content || "")
const [isLoading, setIsLoading] = useState<boolean>(true) const [isLoading, setIsLoading] = useState<boolean>(true)
const { theme } = useTheme()
useEffect(() => { useEffect(() => {
async function fetchPost() { async function fetchPost() {
if (fileId) { if (fileId) {
const resp = await fetch(`/server-api/files/html/${fileId}`, { const resp = await fetch(`/server-api/files/html/${fileId}`, {
method: "GET", method: "GET",
}) })
console.log(resp)
if (resp.ok) { if (resp.ok) {
const res = await resp.text() const res = await resp.text()
console.log(res)
setPreview(res) setPreview(res)
setIsLoading(false) setIsLoading(false)
} }

View file

@ -75,11 +75,11 @@
} }
.markdownPreview h5 { .markdownPreview h5 {
font-size: 0.875rem; font-size: 1rem;
} }
.markdownPreview h6 { .markdownPreview h6 {
font-size: 0.75rem; font-size: 0.875rem;
} }
.markdownPreview ul { .markdownPreview ul {

View file

@ -1,62 +0,0 @@
import remarkGfm from "remark-gfm"
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/prism-async-light';
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeRaw from 'rehype-raw'
// @ts-ignore because of no types in remark-a11y-emoji
// import a11yEmoji from '@fec/remark-a11y-emoji';
import styles from './preview.module.css'
import dark from 'react-syntax-highlighter/dist/cjs/styles/prism/vsc-dark-plus'
import light from 'react-syntax-highlighter/dist/cjs/styles/prism/vs'
import useSharedState from "@lib/hooks/use-shared-state";
import ReactMarkdown from "react-markdown";
type Props = {
content: string | undefined
height: number | string
}
const ReactMarkdownPreview = ({ content, height }: Props) => {
const [themeType] = useSharedState<string>('theme')
return (<div style={{ height }}>
<ReactMarkdown className={styles.markdownPreview}
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }], rehypeRaw]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<SyntaxHighlighter
lineNumberStyle={{
minWidth: "2.25rem"
}}
customStyle={{
padding: 0,
margin: 0,
background: 'transparent'
}}
codeTagProps={{
style: { background: 'transparent', color: 'inherit' }
}}
style={themeType === 'dark' ? dark : light}
showLineNumbers={true}
language={match[1]}
PreTag="div"
{...props}
>{String(children).replace(/\n$/, '')}</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}>
{content || ""}
</ReactMarkdown></div>)
}
export default ReactMarkdownPreview

View file

@ -1,18 +1,18 @@
// useDebounce.js // useDebounce.js
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react"
export default function useDebounce(value: any, delay: number) { export default function useDebounce(value: any, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value); const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
setDebouncedValue(value); setDebouncedValue(value)
}, delay); }, delay)
return () => { return () => {
clearTimeout(handler); clearTimeout(handler)
}; }
}, [value, delay]); }, [value, delay])
return debouncedValue; return debouncedValue
} }

View file

@ -1,27 +0,0 @@
import { useCallback, useEffect } from "react"
import useSharedState from "./use-shared-state"
const useTheme = () => {
const isClient = typeof window === "object"
const [themeType, setThemeType] = useSharedState<string>("theme", "light")
useEffect(() => {
if (!isClient) return
const storedTheme = localStorage.getItem("drift-theme")
if (storedTheme) {
setThemeType(storedTheme)
}
}, [isClient, setThemeType])
const changeTheme = useCallback(() => {
setThemeType((last) => {
const newTheme = last === "dark" ? "light" : "dark"
localStorage.setItem("drift-theme", newTheme)
return newTheme
})
}, [setThemeType])
return { theme: themeType, changeTheme }
}
export default useTheme

View file

@ -60,20 +60,20 @@ renderer.listitem = (text, task, checked) => {
return `<li>${text}</li>` return `<li>${text}</li>`
} }
renderer.code = (code: string, language: string) => { // renderer.code = (code: string, language: string) => {
return renderToStaticMarkup( // return renderToStaticMarkup(
<pre> // <pre>
{/* {title && <code>{title} </code>} */} // {/* {title && <code>{title} </code>} */}
{/* {language && title && <code style={{}}> {language} </code>} */} // {/* {language && title && <code style={{}}> {language} </code>} */}
<Code // <Code
language={language} // language={language}
// title={title} // // title={title}
code={code} // code={code}
// highlight={highlight} // // highlight={highlight}
/> // />
</pre> // </pre>
) // )
} // }
marked.setOptions({ marked.setOptions({
gfm: true, gfm: true,
@ -95,8 +95,7 @@ const Code = ({ code, language, highlight, title, ...props }: {
if (!language) if (!language)
return ( return (
<> <>
<code {...props} dangerouslySetInnerHTML={{ __html: code } <code {...props} dangerouslySetInnerHTML={{ __html: code }} />
} />
</> </>
) )

View file

@ -1,10 +1,5 @@
export type PostVisibility = "unlisted" | "private" | "public" | "protected" export type PostVisibility = "unlisted" | "private" | "public" | "protected"
export type ThemeProps = {
theme: "light" | "dark" | string
changeTheme: () => void
}
export type Document = { export type Document = {
title: string title: string
content: string content: string

View file

@ -22,6 +22,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"marked": "^4.0.12", "marked": "^4.0.12",
"next": "^12.1.1-canary.15", "next": "^12.1.1-canary.15",
"next-themes": "^0.1.1",
"postcss": "^8.4.12", "postcss": "^8.4.12",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-hover-media-feature": "^1.0.2", "postcss-hover-media-feature": "^1.0.2",

View file

@ -4,8 +4,10 @@ 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 { SkeletonTheme } from 'react-loading-skeleton';
import Head from 'next/head'; import Head from 'next/head';
import useTheme from '@lib/hooks/use-theme'; import { CssBaseline, GeistProvider, Themes } from '@geist-ui/core';
import { CssBaseline, GeistProvider } from '@geist-ui/core'; import { useTheme, ThemeProvider } from 'next-themes'
import { useEffect } from 'react';
import App from '@components/app';
type AppProps<P = any> = { type AppProps<P = any> = {
pageProps: P; pageProps: P;
@ -13,12 +15,8 @@ type AppProps<P = any> = {
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
const { theme } = useTheme()
const skeletonBaseColor = 'var(--light-gray)'
const skeletonHighlightColor = 'var(--lighter-gray)'
return ( return (
<div data-theme={theme}> <div>
<Head> <Head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
@ -33,12 +31,9 @@ function MyApp({ Component, pageProps }: AppProps) {
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<title>Drift</title> <title>Drift</title>
</Head> </Head>
<GeistProvider themeType={theme}> <ThemeProvider defaultTheme="system">
<SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}> <App Component={Component} pageProps={pageProps} />
<CssBaseline /> </ThemeProvider>
<Component {...pageProps} />
</SkeletonTheme>
</GeistProvider>
</div> </div>
) )
} }

View file

@ -23,7 +23,6 @@ const renderMarkdown: NextApiHandler = async (req, res) => {
} }
const type = fileType() const type = fileType()
let contentToRender: string = content || "" let contentToRender: string = content || ""
if (!renderAsMarkdown.includes(type)) { if (!renderAsMarkdown.includes(type)) {
contentToRender = `~~~${type} contentToRender = `~~~${type}
${content} ${content}

View file

@ -32,7 +32,7 @@
--highlight: #2e2e2e; --highlight: #2e2e2e;
/* Dark Mode Colors */ /* Dark Mode Colors */
--bg: #131415; --bg: #0e0e0e;
--fg: #fafbfc; --fg: #fafbfc;
--gray: #666; --gray: #666;
--light-gray: #444; --light-gray: #444;
@ -94,10 +94,34 @@ body {
flex-direction: column; flex-direction: column;
} }
p, p {
li { overflow-wrap: break-word;
letter-spacing: -0.33px; hyphens: auto;
font-size: 1.125rem; }
input,
button,
textarea,
select {
font-size: 1rem;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
} }
blockquote { blockquote {
@ -117,15 +141,6 @@ code {
font-family: var(--font-mono) !important; font-family: var(--font-mono) !important;
} }
kbd {
font-family: var(--font-sans);
font-size: 1rem;
padding: 2px 7px;
font-weight: 600;
background: var(--lighter-gray);
border-radius: 5px;
}
@media print { @media print {
:root { :root {
--bg: #fff; --bg: #fff;
@ -150,3 +165,8 @@ kbd {
text-shadow: none !important; text-shadow: none !important;
} }
} }
#root,
#__next {
isolation: isolate;
}

View file

@ -2893,6 +2893,11 @@ needle@^2.5.2:
iconv-lite "^0.4.4" iconv-lite "^0.4.4"
sax "^1.2.4" sax "^1.2.4"
next-themes@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a"
integrity sha512-Iqxt6rhS/KfK/iHJ0tfFjTcdLEAI0AgwFuAFrMwLOPK5e+MI3I+fzyvBoS+VaOS+NldUiazurhgwYhrfV0VXsQ==
next-unused@^0.0.6: next-unused@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/next-unused/-/next-unused-0.0.6.tgz#dbefa300bf5586e33d5bfde909130fb19ab04a64" resolved "https://registry.yarnpkg.com/next-unused/-/next-unused-0.0.6.tgz#dbefa300bf5586e33d5bfde909130fb19ab04a64"

View file

@ -42,7 +42,6 @@ files.get("/html/:id", async (req, res, next) => {
return res.status(404).json({ error: "File not found" }) return res.status(404).json({ error: "File not found" })
} }
console.log(file.html)
res.setHeader('Content-Type', 'text/plain') res.setHeader('Content-Type', 'text/plain')
res.setHeader('Cache-Control', 'public, max-age=4800') res.setHeader('Cache-Control', 'public, max-age=4800')
res.status(200).write(file.html) res.status(200).write(file.html)