client: overhaul markdown rendering (now server-side), refactor theming

This commit is contained in:
Max Leiter 2022-03-22 20:06:15 -07:00
parent d1ee9d857f
commit 34b1ab979f
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
41 changed files with 735 additions and 518 deletions

View file

@ -1,5 +1,5 @@
import type { LinkProps } from "@geist-ui/core"
import GeistLink from "@geist-ui/core/dist/link"
import { Link as GeistLink } from "@geist-ui/core"
import { useRouter } from "next/router";
const Link = (props: LinkProps) => {

View file

@ -4,6 +4,7 @@ import styles from './auth.module.css'
import { useRouter } from 'next/router'
import Link from '../Link'
import Cookies from "js-cookie";
import useSignedIn from '@lib/hooks/use-signed-in'
const NO_EMPTY_SPACE_REGEX = /^\S*$/;
const ERROR_MESSAGE = "Provide a non empty username and a password with at least 6 characters";
@ -17,7 +18,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
const [errorMsg, setErrorMsg] = useState('');
const [requiresServerPassword, setRequiresServerPassword] = useState(false);
const signingIn = page === 'signin'
const { signin } = useSignedIn();
useEffect(() => {
async function fetchRequiresPass() {
if (!signingIn) {
@ -37,7 +38,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
const handleJson = (json: any) => {
Cookies.set('drift-token', json.token);
signin(json.token)
Cookies.set('drift-userid', json.userId);
router.push('/')
@ -65,7 +66,6 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
handleJson(json)
} catch (err: any) {
console.log(err)
setErrorMsg(err.message ?? "Something went wrong")
}
}

View file

@ -0,0 +1,26 @@
.main {
margin-bottom: 2rem;
}
.dropdown {
position: relative;
display: inline-block;
vertical-align: middle;
cursor: pointer;
padding: 0;
border: 0;
background: transparent;
}
.dropdownContent {
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.25rem;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
}
.icon {
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -0,0 +1,116 @@
import Button from "@components/button"
import React, { useCallback, useEffect } from "react"
import { useState } from "react"
import styles from './dropdown.module.css'
import DownIcon from '@geist-ui/icons/arrowDown'
type Props = {
type?: "primary" | "secondary"
loading?: boolean
disabled?: boolean
className?: string
iconHeight?: number
}
type Attrs = Omit<React.HTMLAttributes<any>, keyof Props>
type ButtonDropdownProps = Props & Attrs
const ButtonDropdown: React.FC<React.PropsWithChildren<ButtonDropdownProps>> = ({
type,
className,
disabled,
loading,
iconHeight = 24,
...props
}) => {
const [visible, setVisible] = useState(false)
const [dropdown, setDropdown] = useState<HTMLDivElement | null>(null)
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
setVisible(!visible)
}
const onBlur = () => {
setVisible(false)
}
const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}
const onMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}
const onMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
setVisible(false)
}
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Escape") {
setVisible(false)
}
}
const onClickOutside = useCallback(() => (e: React.MouseEvent<HTMLDivElement>) => {
if (dropdown && !dropdown.contains(e.target as Node)) {
setVisible(false)
}
}, [dropdown])
useEffect(() => {
if (visible) {
document.addEventListener("mousedown", onClickOutside)
} else {
document.removeEventListener("mousedown", onClickOutside)
}
return () => {
document.removeEventListener("mousedown", onClickOutside)
}
}, [visible, onClickOutside])
if (!Array.isArray(props.children)) {
return null
}
return (
<div
className={`${styles.main} ${className}`}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
onKeyDown={onKeyDown}
onBlur={onBlur}
>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-end' }}>
{props.children[0]}
<Button style={{ height: iconHeight, width: iconHeight }} className={styles.icon} onClick={() => setVisible(!visible)}><DownIcon /></Button>
</div>
{
visible && (
<div
className={`${styles.dropdown}`}
>
<div
className={`${styles.dropdownContent}`}
>
{props.children.slice(1)}
</div>
</div>
)
}
</div >
)
}
export default ButtonDropdown

View file

@ -0,0 +1,62 @@
.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);
}
/*
--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);
}
.button[disabled] {
cursor: not-allowed;
background: var(--lighter-gray);
color: var(--gray);
}
.secondary {
background: var(--bg);
color: var(--fg);
}
/*
--bg: #131415;
--fg: #fafbfc;
--gray: #666;
--light-gray: #444;
--lighter-gray: #222;
--lightest-gray: #1a1a1a;
--article-color: #eaeaea;
--header-bg: rgba(19, 20, 21, 0.45);
--gray-alpha: rgba(255, 255, 255, 0.5);
--selection: rgba(255, 255, 255, 0.99);
*/
.primary {
background: var(--fg);
color: var(--bg);
}

View file

@ -0,0 +1,28 @@
import styles from './button.module.css'
import { forwardRef, Ref } from 'react'
type Props = React.HTMLProps<HTMLButtonElement> & {
children: React.ReactNode
buttonType?: 'primary' | 'secondary'
className?: string
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
}
// eslint-disable-next-line react/display-name
const Button = forwardRef<HTMLButtonElement, Props>(
({ children, onClick, className, buttonType = 'primary', type = 'button', disabled = false, ...props }, ref) => {
return (
<button
ref={ref}
className={`${styles.button} ${styles[type]} ${className}`}
disabled={disabled}
onClick={onClick}
{...props}
>
{children}
</button>
)
}
)
export default Button

View file

@ -1,11 +1,10 @@
import ButtonGroup from "@geist-ui/core/dist/button-group"
import Button from "@geist-ui/core/dist/button"
import Bold from '@geist-ui/icons/bold'
import Italic from '@geist-ui/icons/italic'
import Link from '@geist-ui/icons/link'
import ImageIcon from '@geist-ui/icons/image'
import { RefObject, useCallback, useMemo } from "react"
import styles from '../document.module.css'
import { Button, ButtonGroup } from "@geist-ui/core"
// TODO: clean up

View file

@ -1,11 +1,4 @@
import Button from "@geist-ui/core/dist/button"
import Card from "@geist-ui/core/dist/card"
import ButtonGroup from "@geist-ui/core/dist/button-group"
import Input from "@geist-ui/core/dist/input"
import Spacer from "@geist-ui/core/dist/spacer"
import Tabs from "@geist-ui/core/dist/tabs"
import Textarea from "@geist-ui/core/dist/textarea"
import Tooltip from "@geist-ui/core/dist/tooltip"
import { ChangeEvent, memo, useCallback, useMemo, useRef, useState } from "react"
import styles from './document.module.css'
@ -15,9 +8,8 @@ import ExternalLink from '@geist-ui/icons/externalLink'
import FormattingIcons from "./formatting-icons"
import Skeleton from "react-loading-skeleton"
import dynamic from "next/dynamic";
const MarkdownPreview = dynamic(() => import("../preview"))
import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core"
import Preview from "@components/preview"
// import Link from "next/link"
type Props = {
@ -74,13 +66,6 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
setTab(newTab as 'edit' | 'preview')
}
const getType = useCallback(() => {
if (!title) return
const pathParts = title.split(".")
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
return language
}, [title])
const onTitleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setTitle ? setTitle(event.target.value) : null, [setTitle])
const removeFile = useCallback(() => (remove?: () => void) => {
@ -140,14 +125,14 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
</div>
<div className={styles.descriptionContainer}>
{tab === 'edit' && editable && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
{rawLink && <DownloadButton rawLink={rawLink()} />}
{rawLink && id && <DownloadButton rawLink={rawLink()} />}
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
<Tabs.Item label={editable ? "Edit" : "Raw"} value="edit">
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ marginTop: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
<Textarea
ref={codeEditorRef}
placeholder="Type some contents..."
placeholder=""
value={content}
onChange={handleOnContentChange}
width="100%"
@ -160,7 +145,7 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
</div>
</Tabs.Item>
<Tabs.Item label="Preview" value="preview">
<MarkdownPreview height={height} content={content} type={getType()} />
<Preview height={height} fileId={id} title={title} content={content} />
</Tabs.Item>
</Tabs>

View file

@ -0,0 +1,27 @@
import Head from "next/head";
import React from "react";
type PageSeoProps = {
title?: string;
description?: string;
isLoading?: boolean;
isPrivate?: boolean
};
const PageSeo = ({
title = 'Drift',
description = "A self-hostable clone of GitHub Gist",
isPrivate = false
}: PageSeoProps) => {
return (
<>
<Head>
<title>{title}</title>
{!isPrivate && <meta name="description" content={description} />}
</Head>
</>
);
};
export default PageSeo;

View file

@ -1,19 +1,16 @@
import React from 'react'
import MoonIcon from '@geist-ui/icons/moon'
import SunIcon from '@geist-ui/icons/sun'
import Select from '@geist-ui/core/dist/select'
// import { useAllThemes, useTheme } from '@geist-ui/core'
import styles from './header.module.css'
import { ThemeProps } from '@lib/types'
import Cookies from 'js-cookie'
import { Select } from '@geist-ui/core'
const Controls = ({ changeTheme, theme }: ThemeProps) => {
const switchThemes = (type: string | string[]) => {
const switchThemes = () => {
changeTheme()
Cookies.set('drift-theme', Array.isArray(type) ? type[0] : type)
}
return (
<div className={styles.wrapper}>
<Select

View file

@ -0,0 +1,179 @@
import { ButtonGroup, Page, Spacer, Tabs, useBodyScroll, useMediaQuery, } from "@geist-ui/core";
import { useCallback, useEffect, useState } from "react";
import styles from './header.module.css';
import { useRouter } from "next/router";
import useSignedIn from "../../lib/hooks/use-signed-in";
import HomeIcon from '@geist-ui/icons/home';
import MenuIcon from '@geist-ui/icons/menu';
import GitHubIcon from '@geist-ui/icons/github';
import SignOutIcon from '@geist-ui/icons/userX';
import SignInIcon from '@geist-ui/icons/user';
import SignUpIcon from '@geist-ui/icons/userPlus';
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 type { ThemeProps } from "@lib/types";
import useTheme from "@lib/hooks/use-theme";
import { Button } from "@geist-ui/core";
type Tab = {
name: string
icon: JSX.Element
condition?: boolean
value: string
onClick?: () => void
href?: string
}
const Header = () => {
const router = useRouter();
const [selectedTab, setSelectedTab] = useState<string>(router.pathname === '/' ? 'home' : router.pathname.split('/')[1]);
const [expanded, setExpanded] = useState<boolean>(false)
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
const isMobile = useMediaQuery('xs', { match: 'down' })
const { signedIn: isSignedIn } = useSignedIn()
const [pages, setPages] = useState<Tab[]>([])
const { changeTheme, theme } = useTheme()
useEffect(() => {
setBodyHidden(expanded)
}, [expanded, setBodyHidden])
useEffect(() => {
if (!isMobile) {
setExpanded(false)
}
}, [isMobile])
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"
},
{
name: isMobile ? "GitHub" : "",
href: "https://github.com/maxleiter/drift",
icon: <GitHubIcon />,
condition: true,
value: "github"
},
{
name: isMobile ? "Change theme" : "",
onClick: function () {
if (typeof window !== 'undefined') {
changeTheme();
}
},
icon: theme === 'light' ? <MoonIcon /> : <SunIcon />,
condition: true,
value: "theme",
}
]
setPages(pageList.filter(page => page.condition))
}, [changeTheme, isMobile, isSignedIn, theme])
const onTabChange = useCallback((tab: string) => {
if (typeof window === 'undefined') return
const match = pages.find(page => page.value === tab)
if (match?.onClick) {
match.onClick()
} else {
router.push(match?.href || '/')
}
}, [pages, router])
return (
<Page.Header height={'var(--page-nav-height)'} margin={0} paddingBottom={0} paddingTop={"var(--gap)"}>
<div className={styles.tabs}>
<Tabs
value={selectedTab}
leftSpace={0}
align="center"
hideDivider
hideBorder
onChange={onTabChange}>
{pages.map((tab) => {
return <Tabs.Item
font="14px"
label={<>{tab.icon} {tab.name}</>}
value={tab.value}
key={`${tab.value}`}
/>
})}
</Tabs>
</div>
<div className={styles.controls}>
<Button
auto
type="abort"
onClick={() => setExpanded(!expanded)}
aria-label="Menu"
>
<Spacer height={5 / 6} width={0} />
<MenuIcon />
</Button>
</div>
{isMobile && expanded && (<div className={styles.mobile}>
<ButtonGroup vertical>
{pages.map((tab, index) => {
return <Button
key={`${tab.name}-${index}`}
onClick={() => onTabChange(tab.value)}
icon={tab.icon}
>
{tab.name}
</Button>
})}
</ButtonGroup>
</div>)}
</Page.Header >
)
}
export default Header

View file

@ -1,210 +1,8 @@
import Page from "@geist-ui/core/dist/page";
import ButtonGroup from "@geist-ui/core/dist/button-group";
import Button from "@geist-ui/core/dist/button";
import useBodyScroll from "@geist-ui/core/dist/use-body-scroll";
import useMediaQuery from "@geist-ui/core/dist/use-media-query";
import Tabs from "@geist-ui/core/dist/tabs";
import Spacer from "@geist-ui/core/dist/spacer";
import dynamic from 'next/dynamic'
import { useEffect, useState } from "react";
import styles from './header.module.css';
import { useRouter } from "next/router";
import useSignedIn from "../../lib/hooks/use-signed-in";
import HomeIcon from '@geist-ui/icons/home';
import MenuIcon from '@geist-ui/icons/menu';
import GitHubIcon from '@geist-ui/icons/github';
import SignOutIcon from '@geist-ui/icons/userX';
import SignInIcon from '@geist-ui/icons/user';
import SignUpIcon from '@geist-ui/icons/userPlus';
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 type { ThemeProps } from "@lib/types";
type Tab = {
name: string
icon: JSX.Element
condition?: boolean
value: string
onClick?: () => void
href?: string
}
const Header = ({ changeTheme, theme }: ThemeProps) => {
const router = useRouter();
const [selectedTab, setSelectedTab] = useState<string>(router.pathname === '/' ? 'home' : router.pathname.split('/')[1]);
const [expanded, setExpanded] = useState<boolean>(false)
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
const isMobile = useMediaQuery('xs', { match: 'down' })
const { signedIn: isSignedIn } = useSignedIn()
const [pages, setPages] = useState<Tab[]>([])
useEffect(() => {
setBodyHidden(expanded)
}, [expanded, setBodyHidden])
useEffect(() => {
if (!isMobile) {
setExpanded(false)
}
}, [isMobile])
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"
},
{
name: isMobile ? "GitHub" : "",
href: "https://github.com/maxleiter/drift",
icon: <GitHubIcon />,
condition: true,
value: "github"
},
{
name: isMobile ? "Change theme" : "",
onClick: function () {
if (typeof window !== 'undefined') {
changeTheme();
setSelectedTab('');
}
},
icon: theme === 'light' ? <MoonIcon /> : <SunIcon />,
condition: true,
value: "theme",
}
]
setPages(pageList.filter(page => page.condition))
}, [changeTheme, isMobile, isSignedIn, theme])
// useEffect(() => {
// setSelectedTab(pages.find((page) => {
// console.log(page.href, router.asPath)
// if (page.href && page.href === router.asPath) {
// return true
// }
// })?.href)
// }, [pages, router, router.pathname])
const onTabChange = (tab: string) => {
const match = pages.find(page => page.value === tab)
if (match?.onClick) {
match.onClick()
} else if (match?.href) {
router.push(`${match.href}`)
}
}
return (
<Page.Header height={'var(--page-nav-height)'} margin={0} paddingBottom={0} paddingTop={"var(--gap)"}>
<div className={styles.tabs}>
<Tabs
value={selectedTab}
leftSpace={0}
align="center"
hideDivider
hideBorder
onChange={onTabChange}>
{pages.map((tab) => {
return <Tabs.Item
font="14px"
label={<>{tab.icon} {tab.name}</>}
value={tab.value}
key={`${tab.value}`}
/>
})}
</Tabs>
</div>
<div className={styles.controls}>
<Button
auto
type="abort"
onClick={() => setExpanded(!expanded)}
aria-label="Menu"
>
<Spacer height={5 / 6} width={0} />
<MenuIcon />
</Button>
</div>
{isMobile && expanded && (<div className={styles.mobile}>
<ButtonGroup vertical>
{pages.map((tab, index) => {
return <Button
key={`${tab.name}-${index}`}
onClick={() => onTabChange(tab.value)}
icon={tab.icon}
>
{tab.name}
</Button>
})}
</ButtonGroup>
</div>)}
</Page.Header >
)
}
const Header = dynamic(import('./header'), {
ssr: false,
// loading: () => <MenuSkeleton />,
})
export default Header
// {/* {/* <ButtonGroup>
// <Button onClick={() => {
// }}><Link href="/signin">Sign out</Link></Button>
// <Button>
// <Link href="/mine">
// Yours
// </Link>
// </Button>
// <Button>
// {/* TODO: Link outside Button, but seems to break ButtonGroup */}
// <Link href="/new">
// New
// </Link>
// </Button >
// <Button onClick={() => changeTheme()}>
// <ShiftBy y={6}>{theme.type === 'light' ? <Moon /> : <Sun />}</ShiftBy>
// </Button>
// </ButtonGroup > * /}

View file

@ -0,0 +1,24 @@
import React from 'react'
import styles from './input.module.css'
type Props = React.HTMLProps<HTMLInputElement> & {
label?: string
fontSize?: number | string
}
// eslint-disable-next-line react/display-name
const Input = React.forwardRef<HTMLInputElement, Props>(({ label, className, ...props }, ref) => {
return (<div className={styles.wrapper}>
{label && <label className={styles.label}>{label}</label>}
<input
ref={ref}
className={className ? `${styles.input} ${className}` : styles.input}
{...props}
/>
</div>
)
})
export default Input

View file

@ -0,0 +1,57 @@
.wrapper {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
height: 100%;
font-size: 1rem;
}
.input {
height: 2.5rem;
border-radius: var(--inline-radius);
background: var(--bg);
color: var(--fg);
border: 1px solid var(--light-gray);
padding: 0 var(--gap-half);
outline: none;
transition: border-color var(--transition);
display: flex;
justify-content: center;
margin: 0;
width: 100%;
}
.input::placeholder {
font-size: 1.5rem;
}
.input:focus {
border-color: var(--input-border-focus);
}
.label {
display: inline-flex;
width: initial;
height: 100%;
align-items: center;
pointer-events: none;
margin: 0;
padding: 0 var(--gap-half);
color: var(--fg);
background-color: var(--light-gray);
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
border-top: 1px solid var(--input-border);
border-left: 1px solid var(--input-border);
border-bottom: 1px solid var(--input-border);
font-size: inherit;
line-height: 1;
white-space: nowrap;
}
@media screen and (max-width: 768px) {
.wrapper {
margin-bottom: var(--gap);
}
}

View file

@ -1,7 +1,4 @@
import Button from '@geist-ui/core/dist/button'
import useToasts from '@geist-ui/core/dist/use-toasts'
import ButtonDropdown from '@geist-ui/core/dist/button-dropdown'
import { Button, useToasts, ButtonDropdown } from '@geist-ui/core'
import { useRouter } from 'next/router';
import { useCallback, useState } from 'react'
import generateUUID from '@lib/generate-uuid';

View file

@ -1,7 +1,5 @@
import Input from "@geist-ui/core/dist/input"
import Modal from "@geist-ui/core/dist/modal"
import Note from "@geist-ui/core/dist/note"
import Spacer from "@geist-ui/core/dist/spacer"
import { Modal, Note, Spacer, Input } from "@geist-ui/core"
import { useState } from "react"
type Props = {

View file

@ -1,9 +1,9 @@
import { ChangeEvent, memo, useCallback } from 'react'
import Text from '@geist-ui/core/dist/text'
import Input from '@geist-ui/core/dist/input'
import { Text } from '@geist-ui/core'
import ShiftBy from '@components/shift-by'
import styles from '../post.module.css'
import { Input } from '@geist-ui/core'
const titlePlaceholders = [
"How to...",

View file

@ -1,4 +1,4 @@
import Text from "@geist-ui/core/dist/text"
import { Text } from "@geist-ui/core"
import NextLink from "next/link"
import Link from '../Link'

View file

@ -1,9 +1,7 @@
import Card from "@geist-ui/core/dist/card";
import Spacer from "@geist-ui/core/dist/spacer";
import Grid from "@geist-ui/core/dist/grid";
import Divider from "@geist-ui/core/dist/divider";
import Skeleton from "react-loading-skeleton";
import { Card, Divider, Grid, Spacer } from "@geist-ui/core";
const ListItemSkeleton = () => (<Card>
<Spacer height={1 / 2} />

View file

@ -1,11 +1,3 @@
import Card from "@geist-ui/core/dist/card"
import Spacer from "@geist-ui/core/dist/spacer"
import Grid from "@geist-ui/core/dist/grid"
import Divider from "@geist-ui/core/dist/divider"
import Link from "@geist-ui/core/dist/link"
import Text from "@geist-ui/core/dist/text"
import Input from "@geist-ui/core/dist/input"
import Tooltip from "@geist-ui/core/dist/tooltip"
import NextLink from "next/link"
import { useEffect, useMemo, useState } from "react"
@ -13,6 +5,7 @@ import timeAgo from "@lib/time-ago"
import ShiftBy from "../shift-by"
import VisibilityBadge from "../visibility-badge"
import getPostPath from "@lib/get-post-path"
import { Input, Link, Text, Card, Spacer, Grid, Tooltip, Divider } from "@geist-ui/core"
const FilenameInput = ({ title }: { title: string }) => <Input
value={title}

View file

@ -1,19 +1,18 @@
import Header from "@components/header"
import Header from "@components/header/header"
import PageSeo from "@components/page-seo"
import VisibilityBadge from "@components/visibility-badge"
import Page from "@geist-ui/core/dist/page"
import Button from "@geist-ui/core/dist/button"
import Text from "@geist-ui/core/dist/text"
import DocumentComponent from '@components/document'
import styles from './post-page.module.css'
import homeStyles from '@styles/Home.module.css'
import type { Post, ThemeProps } from "@lib/types"
import type { Post } from "@lib/types"
import { Page, Button, Text } from "@geist-ui/core"
type Props = ThemeProps & {
type Props = {
post: Post
}
const PostPage = ({ post, changeTheme, theme }: Props) => {
const PostPage = ({ post }: Props) => {
const download = async () => {
const downloadZip = (await import("client-zip")).downloadZip
const blob = await downloadZip(post.files.map((file: any) => {
@ -39,9 +38,9 @@ const PostPage = ({ post, changeTheme, theme }: Props) => {
/>
<Page.Header>
<Header theme={theme} changeTheme={changeTheme} />
<Header />
</Page.Header>
<Page.Content width={"var(--main-content-width)"} margin="auto">
<Page.Content className={homeStyles.main}>
{/* {!isLoading && <PostFileExplorer files={post.files} />} */}
<div className={styles.header}>
<div className={styles.titleAndBadge}>

View file

@ -1,28 +1,55 @@
import useTheme from "@lib/hooks/use-theme"
import { memo, useEffect, useState } from "react"
import ReactMarkdownPreview from "./react-markdown-preview"
type Props = {
content?: string
height?: number | string
fileId?: string
content?: string
title?: string
// file extensions we can highlight
type?: string
}
const MarkdownPreview = ({ content = '', height = 500, type = 'markdown' }: Props) => {
const [contentToRender, setContent] = useState(content)
const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
const [preview, setPreview] = useState<string>(content || "")
const [isLoading, setIsLoading] = useState<boolean>(true)
const { theme } = useTheme()
useEffect(() => {
// 'm' so it doesn't flash code when you change the type to md
const renderAsMarkdown = ['m', 'markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
if (!renderAsMarkdown.includes(type)) {
setContent(`~~~${type}
${content}
~~~
`)
} else {
setContent(content)
async function fetchPost() {
if (fileId) {
const resp = await fetch(`/api/markdown/${fileId}`, {
method: "GET",
})
if (resp.ok) {
const res = await resp.text()
setPreview(res)
setIsLoading(false)
}
}, [type, content])
return (<ReactMarkdownPreview height={height} content={contentToRender} />)
} else {
const resp = await fetch(`/api/render-markdown`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
content,
}),
})
if (resp.ok) {
const res = await resp.text()
setPreview(res)
setIsLoading(false)
}
}
}
fetchPost()
}, [content, fileId, title])
return (<>
{isLoading ? <div>Loading...</div> : <div data-theme={theme} dangerouslySetInnerHTML={{ __html: preview }} style={{
height
}} />}
</>)
}
export default memo(MarkdownPreview)

View file

@ -1,4 +1,4 @@
import Badge from "@geist-ui/core/dist/badge";
import { Badge } from "@geist-ui/core"
import type { PostVisibility } from "@lib/types"
type Props = {

View file

@ -1,9 +1,14 @@
import Cookies from "js-cookie";
import { useEffect, useState } from "react";
import useSharedState from "./use-shared-state";
const useSignedIn = () => {
const [signedIn, setSignedIn] = useState(typeof window === 'undefined' ? false : !!Cookies.get("drift-token"));
const [signedIn, setSignedIn] = useSharedState('signedIn', typeof window === 'undefined' ? false : !!Cookies.get("drift-token"));
const token = Cookies.get("drift-token")
const signin = (token: string) => {
setSignedIn(true);
Cookies.set("drift-token", token);
}
useEffect(() => {
if (token) {
@ -11,9 +16,9 @@ const useSignedIn = () => {
} else {
setSignedIn(false);
}
}, [token]);
}, [setSignedIn, token]);
return { signedIn, token };
return { signedIn, signin, token };
}
export default useSignedIn;

View file

@ -0,0 +1,27 @@
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

@ -1,4 +1,3 @@
import Link from '@components/Link'
import { marked } from 'marked'
import Highlight, { defaultProps, Language } from 'prism-react-renderer'
import { renderToStaticMarkup } from 'react-dom/server'

View file

@ -18,7 +18,9 @@
"cookie": "^0.4.2",
"dotenv": "^16.0.0",
"js-cookie": "^3.0.1",
"marked": "^4.0.12",
"next": "^12.1.1-canary.15",
"prism-react-renderer": "^1.3.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-dropzone": "^12.0.4",
@ -33,6 +35,7 @@
},
"devDependencies": {
"@next/bundle-analyzer": "^12.1.0",
"@types/marked": "^4.0.3",
"@types/node": "17.0.21",
"@types/react": "17.0.39",
"@types/react-dom": "^17.0.14",

View file

@ -1,46 +1,21 @@
import '@styles/globals.css'
import GeistProvider from '@geist-ui/core/dist/geist-provider'
import CssBaseline from '@geist-ui/core/dist/css-baseline'
import useTheme from '@geist-ui/core/dist/use-theme'
import { useEffect, useMemo, useState } from 'react'
import type { AppProps as NextAppProps } from "next/app";
import useSharedState from '@lib/hooks/use-shared-state';
import 'react-loading-skeleton/dist/skeleton.css'
import { SkeletonTheme } from 'react-loading-skeleton';
import Head from 'next/head';
import type { ThemeProps } from '@lib/types';
import Cookies from 'js-cookie';
import useTheme from '@lib/hooks/use-theme';
import { CssBaseline, GeistProvider } from '@geist-ui/core';
type AppProps<P = any> = {
pageProps: P;
} & Omit<NextAppProps<P>, "pageProps">;
function MyApp({ Component, pageProps }: AppProps<ThemeProps>) {
const [themeType, setThemeType] = useSharedState<string>('theme', Cookies.get('drift-theme') || 'light')
useEffect(() => {
const storedTheme = Cookies.get('drift-theme')
if (storedTheme) setThemeType(storedTheme)
// TODO: useReducer?
}, [setThemeType, themeType])
const changeTheme = () => {
const newTheme = themeType === 'dark' ? 'light' : 'dark'
localStorage.setItem('drift-theme', newTheme)
setThemeType(last => (last === 'dark' ? 'light' : 'dark'))
}
const skeletonBaseColor = useMemo(() => {
if (themeType === 'dark') return '#333'
return '#eee'
}, [themeType])
const skeletonHighlightColor = useMemo(() => {
if (themeType === 'dark') return '#555'
return '#ddd'
}, [themeType])
function MyApp({ Component, pageProps }: AppProps) {
const { theme } = useTheme()
const skeletonBaseColor = 'var(--light-gray)'
const skeletonHighlightColor = 'var(--lighter-gray)'
return (
<>
@ -58,10 +33,10 @@ function MyApp({ Component, pageProps }: AppProps<ThemeProps>) {
<meta name="theme-color" content="#ffffff" />
<title>Drift</title>
</Head>
<GeistProvider themeType={themeType} >
<GeistProvider themeType={theme} >
<SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}>
<CssBaseline />
<Component {...pageProps} theme={themeType || 'light'} changeTheme={changeTheme} />
<Component {...pageProps} />
</SkeletonTheme>
</GeistProvider>
</>

View file

@ -1,5 +1,5 @@
import { CssBaseline } from '@geist-ui/core'
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import CssBaseline from '@geist-ui/core/dist/css-baseline'
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {

View file

@ -12,17 +12,16 @@ const renderMarkdown: NextApiHandler = async (req, res) => {
}
})
const json = await file.json()
const { content, title } = json
const renderAsMarkdown = ['m', 'markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
const fileType = () => {
const pathParts = title.split(".")
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
return language
}
const type = fileType()
let contentToRender: string = content;
let contentToRender: string = '\n' + content;
if (!renderAsMarkdown.includes(type)) {
contentToRender = `~~~${type}

View file

@ -0,0 +1,30 @@
import type { NextApiHandler } from "next";
import markdown from "@lib/render-markdown";
const renderMarkdown: NextApiHandler = async (req, res) => {
const { content, title } = req.body
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
const fileType = () => {
const pathParts = title.split(".")
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
return language
}
const type = fileType()
let contentToRender: string = '\n' + (content || '');
if (!renderAsMarkdown.includes(type)) {
contentToRender = `~~~${type}
${content}
~~~`
}
if (typeof contentToRender !== 'string') {
res.status(400).send('content must be a string')
return
}
res.status(200).write(markdown(contentToRender))
res.end()
}
export default renderMarkdown

View file

@ -1,13 +1,10 @@
import styles from '@styles/Home.module.css'
import Page from '@geist-ui/core/dist/page'
import Spacer from '@geist-ui/core/dist/spacer'
import Text from '@geist-ui/core/dist/text'
import Header from '@components/header'
import Document from '@components/document'
import Image from 'next/image'
import ShiftBy from '@components/shift-by'
import PageSeo from '@components/page-seo'
import { ThemeProps } from '@lib/types'
import { Page, Text, Spacer } from '@geist-ui/core'
export function getStaticProps() {
const introDoc = process.env.WELCOME_CONTENT
@ -19,19 +16,19 @@ export function getStaticProps() {
}
}
type Props = ThemeProps & {
type Props = {
introContent: string
}
const Home = ({ theme, changeTheme, introContent }: Props) => {
const Home = ({ introContent }: Props) => {
return (
<Page className={styles.container} width="100%">
<Page className={styles.container}>
<PageSeo />
<Page.Header>
<Header theme={theme} changeTheme={changeTheme} />
<Header />
</Page.Header>
<Page.Content width={"var(--main-content-width)"} margin="auto" paddingTop={"var(--gap)"}>
<Page.Content className={styles.main}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<ShiftBy y={-2}><Image src={'/assets/logo-optimized.svg'} width={'48px'} height={'48px'} alt="" /></ShiftBy>
<Spacer />

View file

@ -1,19 +1,19 @@
import styles from '@styles/Home.module.css'
import Page from '@geist-ui/core/dist/page'
import Header from '@components/header'
import MyPosts from '@components/my-posts'
import cookie from "cookie";
import type { GetServerSideProps } from 'next';
import type { ThemeProps } from '@lib/types';
import { Post } from '@lib/types';
import { Page } from '@geist-ui/core';
const Home = ({ posts, error, theme, changeTheme }: ThemeProps & { posts: any; error: any; }) => {
const Home = ({ posts, error }: { posts: Post[]; error: any; }) => {
return (
<Page className={styles.container} width="100%">
<Page className={styles.container}>
<Page.Header>
<Header theme={theme} changeTheme={changeTheme} />
<Header />
</Page.Header>
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
<Page.Content className={styles.main}>
<MyPosts error={error} posts={posts} />
</Page.Content>
</Page >

View file

@ -1,20 +1,19 @@
import styles from '@styles/Home.module.css'
import NewPost from '@components/new-post'
import Page from '@geist-ui/core/dist/page'
import Header from '@components/header'
import PageSeo from '@components/page-seo'
import type { ThemeProps } from '@lib/types'
import { Page } from '@geist-ui/core'
const New = ({ theme, changeTheme }: ThemeProps) => {
const New = () => {
return (
<Page className={styles.container} width="100%">
<PageSeo title="Drift - New" />
<Page.Header>
<Header theme={theme} changeTheme={changeTheme} />
<Header />
</Page.Header>
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
<Page.Content className={styles.main}>
<NewPost />
</Page.Content>
</Page >

View file

@ -1,14 +1,14 @@
import type { GetStaticPaths, GetStaticProps } from "next";
import type { Post, ThemeProps } from "@lib/types";
import type { Post } from "@lib/types";
import PostPage from "@components/post-page";
export type PostProps = ThemeProps & {
export type PostProps = {
post: Post
}
const PostView = ({ post, theme, changeTheme }: PostProps) => {
return <PostPage post={post} theme={theme} changeTheme={changeTheme} />
const PostView = ({ post }: PostProps) => {
return <PostPage post={post} />
}
export const getStaticPaths: GetStaticPaths = async () => {

View file

@ -1,28 +1,14 @@
import cookie from "cookie";
import type { GetServerSideProps } from "next";
import { PostVisibility, ThemeProps } from "@lib/types";
import { Post } from "@lib/types";
import PostPage from "@components/post-page";
type File = {
id: string
title: string
content: string
export type PostProps = {
post: Post
}
type Files = File[]
export type PostProps = ThemeProps & {
post: {
id: string
title: string
description: string
visibility: PostVisibility
files: Files
}
}
const Post = ({ post, theme, changeTheme }: PostProps) => {
return (<PostPage post={post} changeTheme={changeTheme} theme={theme} />)
const Post = ({ post, }: PostProps) => {
return (<PostPage post={post} />)
}
export const getServerSideProps: GetServerSideProps = async (context) => {

View file

@ -1,14 +1,13 @@
import useToasts from "@geist-ui/core/dist/use-toasts";
import Page from "@geist-ui/core/dist/page";
import { Page, useToasts } from '@geist-ui/core';
import type { Post, ThemeProps } from "@lib/types";
import type { Post } from "@lib/types";
import PasswordModal from "@components/new-post/password";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Cookies from "js-cookie";
import PostPage from "@components/post-page";
const Post = ({ theme, changeTheme }: ThemeProps) => {
const Post = () => {
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true);
const [post, setPost] = useState<Post>()
const router = useRouter()
@ -74,7 +73,7 @@ const Post = ({ theme, changeTheme }: ThemeProps) => {
return <Page><PasswordModal creating={false} onClose={onClose} onSubmit={onSubmit} isOpen={isPasswordModalOpen} /></Page>
}
return (<PostPage post={post} changeTheme={changeTheme} theme={theme} />)
return (<PostPage post={post} />)
}
export default Post

View file

@ -1,17 +1,16 @@
import Page from "@geist-ui/core/dist/page";
import { Page } from '@geist-ui/core';
import PageSeo from "@components/page-seo";
import Auth from "@components/auth";
import Header from "@components/header";
import type { ThemeProps } from "@lib/types";
const SignIn = ({ theme, changeTheme }: ThemeProps) => (
import Header from "@components/header/header";
import styles from '@styles/Home.module.css'
const SignIn = () => (
<Page width={"100%"}>
<PageSeo title="Drift - Sign In" />
<Page.Header>
<Header theme={theme} changeTheme={changeTheme} />
<Header />
</Page.Header>
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="auto">
<Page.Content className={styles.main}>
<Auth page="signin" />
</Page.Content>
</Page>

View file

@ -1,17 +1,17 @@
import Page from "@geist-ui/core/dist/page";
import { Page } from '@geist-ui/core';
import Auth from "@components/auth";
import Header from "@components/header";
import Header from "@components/header/header";
import PageSeo from '@components/page-seo';
import type { ThemeProps } from "@lib/types";
import styles from '@styles/Home.module.css'
const SignUp = ({ theme, changeTheme }: ThemeProps) => (
const SignUp = () => (
<Page width="100%">
<PageSeo title="Drift - Sign Up" />
<Page.Header>
<Header theme={theme} changeTheme={changeTheme} />
<Header />
</Page.Header>
<Page.Content width={"var(--main-content-width)"} paddingTop={"var(--gap)"} margin="auto">
<Page.Content className={styles.main}>
<Auth page="signup" />
</Page.Content>
</Page>

View file

@ -25,29 +25,7 @@
--transition: 0.1s ease-in-out;
--transition-slow: 0.3s ease-in-out;
/* Dark Mode Colors */
--bg: #131415;
--fg: #fafbfc;
--gray: #666;
--light-gray: #444;
--lighter-gray: #222;
--lightest-gray: #1a1a1a;
--article-color: #eaeaea;
--header-bg: rgba(19, 20, 21, 0.45);
--gray-alpha: rgba(255, 255, 255, 0.5);
--selection: rgba(255, 255, 255, 0.99);
/* Forms */
--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);
--input-bg-hover: var(--lightest-gray);
/* Syntax Highlighting */
--page-nav-height: 64px;
--token: #999;
--comment: #999;
--keyword: #fff;
@ -56,17 +34,6 @@
}
[data-theme="light"] {
--bg: #fff;
--fg: #000;
--gray: #888;
--light-gray: #dedede;
--lighter-gray: #f5f5f5;
--lightest-gray: #fafafa;
--article-color: #212121;
--header-bg: rgba(255, 255, 255, 0.8);
--gray-alpha: rgba(19, 20, 21, 0.5);
--selection: rgba(0, 0, 0, 0.99);
--token: #666;
--comment: #999;
--keyword: #000;
@ -81,11 +48,6 @@
::selection {
text-shadow: none;
background: var(--selection);
color: var(--bg);
}
html {
line-height: 1.5;
}
html,
@ -93,8 +55,6 @@ body {
padding: 0;
margin: 0;
font-size: 15px;
background: var(--bg);
color: var(--fg);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@ -113,44 +73,6 @@ li {
font-size: 1.125rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-sans);
font-weight: 600;
line-height: 1.75;
}
h1 {
font-size: 2.5rem;
font-weight: 600;
line-height: 1.25;
letter-spacing: -0.89px;
}
h2 {
font-size: 2rem;
letter-spacing: -0.69px;
}
h3 {
font-size: 1.5rem;
letter-spacing: -0.47px;
}
h4 {
font-size: 1.25rem;
letter-spacing: -0.33px;
}
hr {
border: none;
border-bottom: 1px solid var(--light-gray);
}
blockquote {
font-style: italic;
margin: 0;
@ -158,52 +80,16 @@ blockquote {
border-left: 3px solid var(--light-gray);
}
button {
border: none;
padding: 0;
margin: 0;
line-height: inherit;
font-size: inherit;
}
p a,
a.reset {
outline: none;
color: var(--fg);
text-decoration: none;
}
p a:hover,
p a:focus,
p a:active,
a.reset:hover,
a.reset:focus {
color: var(--gray);
}
pre,
code {
font-family: var(--font-mono);
}
.clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.flex {
display: flex;
}
kbd {
font-family: var(--font-sans);
font-size: 1rem;
@ -213,18 +99,6 @@ kbd {
border-radius: 5px;
}
summary {
cursor: pointer;
outline: none;
}
details {
border-radius: var(--radius);
background: var(--lightest-gray);
padding: 1rem;
border-radius: var(--radius);
}
@media print {
:root {
--bg: #fff;

View file

@ -197,6 +197,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/marked@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.3.tgz#2098f4a77adaba9ce881c9e0b6baf29116e5acc4"
integrity sha512-HnMWQkLJEf/PnxZIfbm0yGJRRZYYMhb++O9M36UCTA9z53uPvVoSlAwJr3XOpDEryb7Hwl1qAx/MV6YIW1RXxg==
"@types/mdast@^3.0.0":
version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@ -1804,6 +1809,11 @@ markdown-table@^3.0.0:
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c"
integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==
marked@^4.0.12:
version "4.0.12"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d"
integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==
mdast-util-definitions@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817"
@ -2559,6 +2569,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prism-react-renderer@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ==
prismjs@^1.25.0, prismjs@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"