client: overhaul markdown rendering (now server-side), refactor theming
This commit is contained in:
parent
d1ee9d857f
commit
34b1ab979f
41 changed files with 735 additions and 518 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
26
client/components/button-dropdown/dropdown.module.css
Normal file
26
client/components/button-dropdown/dropdown.module.css
Normal 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;
|
||||
}
|
116
client/components/button-dropdown/index.tsx
Normal file
116
client/components/button-dropdown/index.tsx
Normal 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
|
62
client/components/button/button.module.css
Normal file
62
client/components/button/button.module.css
Normal 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);
|
||||
}
|
28
client/components/button/index.tsx
Normal file
28
client/components/button/index.tsx
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
27
client/components/head/index.tsx
Normal file
27
client/components/head/index.tsx
Normal 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;
|
|
@ -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
|
||||
|
|
179
client/components/header/header.tsx
Normal file
179
client/components/header/header.tsx
Normal 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
|
|
@ -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 > * /}
|
24
client/components/input/index.tsx
Normal file
24
client/components/input/index.tsx
Normal 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
|
57
client/components/input/input.module.css
Normal file
57
client/components/input/input.module.css
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
|
|
27
client/lib/hooks/use-theme.ts
Normal file
27
client/lib/hooks/use-theme.ts
Normal 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
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
30
client/pages/api/render-markdown.ts
Normal file
30
client/pages/api/render-markdown.ts
Normal 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
|
|
@ -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 />
|
||||
|
|
|
@ -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 >
|
||||
|
|
|
@ -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 >
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue