client: add next-themes support for CSS variables
This commit is contained in:
parent
186d536175
commit
6045200ac4
17 changed files with 146 additions and 254 deletions
25
client/components/app/index.tsx
Normal file
25
client/components/app/index.tsx
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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 }} />
|
||||
</>
|
||||
)
|
||||
|
||||
|
|
5
client/lib/types.d.ts
vendored
5
client/lib/types.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ const renderMarkdown: NextApiHandler = async (req, res) => {
|
|||
}
|
||||
const type = fileType()
|
||||
let contentToRender: string = content || ""
|
||||
|
||||
if (!renderAsMarkdown.includes(type)) {
|
||||
contentToRender = `~~~${type}
|
||||
${content}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue