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

@ -1,46 +1,37 @@
.button {
user-select: none;
cursor: pointer;
border-radius: var(--radius);
color: var(--input-fg);
font-weight: 400;
font-size: 1.1rem;
background: var(--input-bg);
border: var(--input-border);
height: 2rem;
display: flex;
align-items: center;
padding: var(--gap-quarter) var(--gap-half);
transition: background-color var(--transition), color var(--transition);
width: 100%;
height: var(--input-height);
user-select: none;
cursor: pointer;
border-radius: var(--radius);
color: var(--input-fg);
font-weight: 400;
font-size: 1.1rem;
background: var(--input-bg);
border: var(--input-border);
height: 2rem;
display: flex;
align-items: center;
padding: var(--gap-quarter) var(--gap-half);
transition: background-color var(--transition), color var(--transition);
width: 100%;
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:focus {
outline: none;
background: var(--input-bg-hover);
border: var(--input-border-focus);
outline: none;
background: var(--input-bg-hover);
border: var(--input-border-focus);
}
.button[disabled] {
cursor: not-allowed;
background: var(--lighter-gray);
color: var(--gray);
cursor: not-allowed;
background: var(--lighter-gray);
color: var(--gray);
}
.secondary {
background: var(--bg);
color: var(--fg);
background: var(--bg);
color: var(--fg);
}
/*
@ -57,6 +48,6 @@
*/
.primary {
background: var(--fg);
color: var(--bg);
background: var(--fg);
color: var(--bg);
}

View file

@ -1,14 +1,22 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import MoonIcon from '@geist-ui/icons/moon'
import SunIcon from '@geist-ui/icons/sun'
// import { useAllThemes, useTheme } from '@geist-ui/core'
import styles from './header.module.css'
import { ThemeProps } from '@lib/types'
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 = () => {
changeTheme()
if (theme === 'dark') {
setTheme('light')
} else {
setTheme('dark')
}
}
return (

View file

@ -16,7 +16,7 @@ import NewIcon from '@geist-ui/icons/plusCircle';
import YourIcon from '@geist-ui/icons/list'
import MoonIcon from '@geist-ui/icons/moon';
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";
type Tab = {
@ -37,7 +37,7 @@ const Header = () => {
const isMobile = useMediaQuery('xs', { match: 'down' })
const { signedIn: isSignedIn, signout } = useSignedIn()
const [pages, setPages] = useState<Tab[]>([])
const { changeTheme, theme } = useTheme()
const { setTheme, theme } = useTheme()
useEffect(() => {
setBodyHidden(expanded)
}, [expanded, setBodyHidden])
@ -60,9 +60,8 @@ const Header = () => {
{
name: isMobile ? "Change theme" : "",
onClick: function () {
if (typeof window !== 'undefined') {
changeTheme();
}
if (typeof window !== 'undefined')
setTheme(theme === 'light' ? 'dark' : 'light');
},
icon: theme === 'light' ? <MoonIcon /> : <SunIcon />,
condition: true,
@ -73,9 +72,9 @@ const Header = () => {
if (isSignedIn)
setPages([
{
name: 'home',
icon: <HomeIcon />,
value: 'home',
name: 'new',
icon: <NewIcon />,
value: 'new',
href: '/'
},
{
@ -116,58 +115,7 @@ const Header = () => {
])
// TODO: investigate deps causing infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [changeTheme, 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])
}, [isMobile, isSignedIn, theme])
const onTabChange = useCallback((tab: string) => {
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 styles from './preview.module.css'
@ -13,17 +12,14 @@ type Props = {
const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
const [preview, setPreview] = useState<string>(content || "")
const [isLoading, setIsLoading] = useState<boolean>(true)
const { theme } = useTheme()
useEffect(() => {
async function fetchPost() {
if (fileId) {
const resp = await fetch(`/server-api/files/html/${fileId}`, {
method: "GET",
})
console.log(resp)
if (resp.ok) {
const res = await resp.text()
console.log(res)
setPreview(res)
setIsLoading(false)
}

View file

@ -75,11 +75,11 @@
}
.markdownPreview h5 {
font-size: 0.875rem;
font-size: 1rem;
}
.markdownPreview h6 {
font-size: 0.75rem;
font-size: 0.875rem;
}
.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
import { useState, useEffect } from 'react';
import { useState, useEffect } from "react"
export default function useDebounce(value: any, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return () => {
clearTimeout(handler)
}
}, [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>`
}
renderer.code = (code: string, language: string) => {
return renderToStaticMarkup(
<pre>
{/* {title && <code>{title} </code>} */}
{/* {language && title && <code style={{}}> {language} </code>} */}
<Code
language={language}
// title={title}
code={code}
// highlight={highlight}
/>
</pre>
)
}
// renderer.code = (code: string, language: string) => {
// return renderToStaticMarkup(
// <pre>
// {/* {title && <code>{title} </code>} */}
// {/* {language && title && <code style={{}}> {language} </code>} */}
// <Code
// language={language}
// // title={title}
// code={code}
// // highlight={highlight}
// />
// </pre>
// )
// }
marked.setOptions({
gfm: true,
@ -95,8 +95,7 @@ const Code = ({ code, language, highlight, title, ...props }: {
if (!language)
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 ThemeProps = {
theme: "light" | "dark" | string
changeTheme: () => void
}
export type Document = {
title: string
content: string

View file

@ -22,6 +22,7 @@
"lodash.debounce": "^4.0.8",
"marked": "^4.0.12",
"next": "^12.1.1-canary.15",
"next-themes": "^0.1.1",
"postcss": "^8.4.12",
"postcss-flexbugs-fixes": "^5.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 { SkeletonTheme } from 'react-loading-skeleton';
import Head from 'next/head';
import useTheme from '@lib/hooks/use-theme';
import { CssBaseline, GeistProvider } from '@geist-ui/core';
import { CssBaseline, GeistProvider, Themes } from '@geist-ui/core';
import { useTheme, ThemeProvider } from 'next-themes'
import { useEffect } from 'react';
import App from '@components/app';
type AppProps<P = any> = {
pageProps: P;
@ -13,12 +15,8 @@ type AppProps<P = any> = {
function MyApp({ Component, pageProps }: AppProps) {
const { theme } = useTheme()
const skeletonBaseColor = 'var(--light-gray)'
const skeletonHighlightColor = 'var(--lighter-gray)'
return (
<div data-theme={theme}>
<div>
<Head>
<meta charSet="utf-8" />
<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" />
<title>Drift</title>
</Head>
<GeistProvider themeType={theme}>
<SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}>
<CssBaseline />
<Component {...pageProps} />
</SkeletonTheme>
</GeistProvider>
<ThemeProvider defaultTheme="system">
<App Component={Component} pageProps={pageProps} />
</ThemeProvider>
</div>
)
}

View file

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

View file

@ -32,7 +32,7 @@
--highlight: #2e2e2e;
/* Dark Mode Colors */
--bg: #131415;
--bg: #0e0e0e;
--fg: #fafbfc;
--gray: #666;
--light-gray: #444;
@ -94,10 +94,34 @@ body {
flex-direction: column;
}
p,
li {
letter-spacing: -0.33px;
font-size: 1.125rem;
p {
overflow-wrap: break-word;
hyphens: auto;
}
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 {
@ -117,15 +141,6 @@ code {
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 {
:root {
--bg: #fff;
@ -150,3 +165,8 @@ kbd {
text-shadow: none !important;
}
}
#root,
#__next {
isolation: isolate;
}

View file

@ -2893,6 +2893,11 @@ needle@^2.5.2:
iconv-lite "^0.4.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:
version "0.0.6"
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" })
}
console.log(file.html)
res.setHeader('Content-Type', 'text/plain')
res.setHeader('Cache-Control', 'public, max-age=4800')
res.status(200).write(file.html)