From 34b1ab979f9dcef6a529623c4de4e7d8dc3583f8 Mon Sep 17 00:00:00 2001
From: Max Leiter <maxwell.leiter@gmail.com>
Date: Tue, 22 Mar 2022 20:06:15 -0700
Subject: [PATCH] client: overhaul markdown rendering (now server-side),
 refactor theming

---
 client/components/Link.tsx                    |   2 +-
 client/components/auth/index.tsx              |   6 +-
 .../button-dropdown/dropdown.module.css       |  26 +++
 client/components/button-dropdown/index.tsx   | 116 ++++++++++
 client/components/button/button.module.css    |  62 +++++
 client/components/button/index.tsx            |  28 +++
 .../document/formatting-icons/index.tsx       |   3 +-
 client/components/document/index.tsx          |  29 +--
 client/components/head/index.tsx              |  27 +++
 client/components/header/controls.tsx         |   7 +-
 client/components/header/header.tsx           | 179 +++++++++++++++
 client/components/header/index.tsx            | 212 +-----------------
 client/components/input/index.tsx             |  24 ++
 client/components/input/input.module.css      |  57 +++++
 client/components/new-post/index.tsx          |   5 +-
 client/components/new-post/password/index.tsx |   6 +-
 client/components/new-post/title/index.tsx    |   4 +-
 client/components/post-list/index.tsx         |   2 +-
 .../post-list/list-item-skeleton.tsx          |   6 +-
 client/components/post-list/list-item.tsx     |   9 +-
 client/components/post-page/index.tsx         |  17 +-
 client/components/preview/index.tsx           |  59 +++--
 client/components/visibility-badge/index.tsx  |   2 +-
 client/lib/hooks/use-signed-in.ts             |  11 +-
 client/lib/hooks/use-theme.ts                 |  27 +++
 client/lib/render-markdown.tsx                |   1 -
 client/package.json                           |   3 +
 client/pages/_app.tsx                         |  41 +---
 client/pages/_document.tsx                    |   2 +-
 .../pages/api/markdown/{[id].tsx => [id].ts}  |   5 +-
 client/pages/api/render-markdown.ts           |  30 +++
 client/pages/index.tsx                        |  15 +-
 client/pages/mine.tsx                         |  12 +-
 client/pages/new.tsx                          |   9 +-
 client/pages/post/[id].tsx                    |   8 +-
 client/pages/post/private/[id].tsx            |  24 +-
 client/pages/post/protected/[id].tsx          |   9 +-
 client/pages/signin.tsx                       |  13 +-
 client/pages/signup.tsx                       |  12 +-
 client/styles/globals.css                     | 128 +----------
 client/yarn.lock                              |  15 ++
 41 files changed, 735 insertions(+), 518 deletions(-)
 create mode 100644 client/components/button-dropdown/dropdown.module.css
 create mode 100644 client/components/button-dropdown/index.tsx
 create mode 100644 client/components/button/button.module.css
 create mode 100644 client/components/button/index.tsx
 create mode 100644 client/components/head/index.tsx
 create mode 100644 client/components/header/header.tsx
 create mode 100644 client/components/input/index.tsx
 create mode 100644 client/components/input/input.module.css
 create mode 100644 client/lib/hooks/use-theme.ts
 rename client/pages/api/markdown/{[id].tsx => [id].ts} (87%)
 create mode 100644 client/pages/api/render-markdown.ts

diff --git a/client/components/Link.tsx b/client/components/Link.tsx
index 9f2e376..e96f89b 100644
--- a/client/components/Link.tsx
+++ b/client/components/Link.tsx
@@ -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) => {
diff --git a/client/components/auth/index.tsx b/client/components/auth/index.tsx
index 43b1d28..5f48f3c 100644
--- a/client/components/auth/index.tsx
+++ b/client/components/auth/index.tsx
@@ -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")
         }
     }
diff --git a/client/components/button-dropdown/dropdown.module.css b/client/components/button-dropdown/dropdown.module.css
new file mode 100644
index 0000000..dd03da0
--- /dev/null
+++ b/client/components/button-dropdown/dropdown.module.css
@@ -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;
+}
diff --git a/client/components/button-dropdown/index.tsx b/client/components/button-dropdown/index.tsx
new file mode 100644
index 0000000..7000059
--- /dev/null
+++ b/client/components/button-dropdown/index.tsx
@@ -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
\ No newline at end of file
diff --git a/client/components/button/button.module.css b/client/components/button/button.module.css
new file mode 100644
index 0000000..381f396
--- /dev/null
+++ b/client/components/button/button.module.css
@@ -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);
+}
diff --git a/client/components/button/index.tsx b/client/components/button/index.tsx
new file mode 100644
index 0000000..0e85a79
--- /dev/null
+++ b/client/components/button/index.tsx
@@ -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
diff --git a/client/components/document/formatting-icons/index.tsx b/client/components/document/formatting-icons/index.tsx
index 279162d..1ef79c8 100644
--- a/client/components/document/formatting-icons/index.tsx
+++ b/client/components/document/formatting-icons/index.tsx
@@ -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
 
diff --git a/client/components/document/index.tsx b/client/components/document/index.tsx
index b34f1b1..d8200c8 100644
--- a/client/components/document/index.tsx
+++ b/client/components/document/index.tsx
@@ -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>
 
diff --git a/client/components/head/index.tsx b/client/components/head/index.tsx
new file mode 100644
index 0000000..1bc53d0
--- /dev/null
+++ b/client/components/head/index.tsx
@@ -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;
diff --git a/client/components/header/controls.tsx b/client/components/header/controls.tsx
index 6d4ee88..09b4afa 100644
--- a/client/components/header/controls.tsx
+++ b/client/components/header/controls.tsx
@@ -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
diff --git a/client/components/header/header.tsx b/client/components/header/header.tsx
new file mode 100644
index 0000000..4f07b54
--- /dev/null
+++ b/client/components/header/header.tsx
@@ -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
diff --git a/client/components/header/index.tsx b/client/components/header/index.tsx
index 7dec087..b8fe948 100644
--- a/client/components/header/index.tsx
+++ b/client/components/header/index.tsx
@@ -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 > * /}
\ No newline at end of file
diff --git a/client/components/input/index.tsx b/client/components/input/index.tsx
new file mode 100644
index 0000000..29c7ae9
--- /dev/null
+++ b/client/components/input/index.tsx
@@ -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
diff --git a/client/components/input/input.module.css b/client/components/input/input.module.css
new file mode 100644
index 0000000..2a5cf7f
--- /dev/null
+++ b/client/components/input/input.module.css
@@ -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);
+  }
+}
diff --git a/client/components/new-post/index.tsx b/client/components/new-post/index.tsx
index 95bda11..c28ce16 100644
--- a/client/components/new-post/index.tsx
+++ b/client/components/new-post/index.tsx
@@ -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';
diff --git a/client/components/new-post/password/index.tsx b/client/components/new-post/password/index.tsx
index 8615a30..9ab7a8f 100644
--- a/client/components/new-post/password/index.tsx
+++ b/client/components/new-post/password/index.tsx
@@ -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 = {
diff --git a/client/components/new-post/title/index.tsx b/client/components/new-post/title/index.tsx
index 89d7968..e5b5f82 100644
--- a/client/components/new-post/title/index.tsx
+++ b/client/components/new-post/title/index.tsx
@@ -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...",
diff --git a/client/components/post-list/index.tsx b/client/components/post-list/index.tsx
index 39af6d3..915d772 100644
--- a/client/components/post-list/index.tsx
+++ b/client/components/post-list/index.tsx
@@ -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'
 
diff --git a/client/components/post-list/list-item-skeleton.tsx b/client/components/post-list/list-item-skeleton.tsx
index 150c4ee..60610e3 100644
--- a/client/components/post-list/list-item-skeleton.tsx
+++ b/client/components/post-list/list-item-skeleton.tsx
@@ -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} />
diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx
index 36b2616..72082b7 100644
--- a/client/components/post-list/list-item.tsx
+++ b/client/components/post-list/list-item.tsx
@@ -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}
diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx
index 9e66ce0..4182277 100644
--- a/client/components/post-page/index.tsx
+++ b/client/components/post-page/index.tsx
@@ -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}>
diff --git a/client/components/preview/index.tsx b/client/components/preview/index.tsx
index 02170b6..3ad4be6 100644
--- a/client/components/preview/index.tsx
+++ b/client/components/preview/index.tsx
@@ -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)
+                }
+            } 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)
+                }
+            }
         }
-    }, [type, content])
-    return (<ReactMarkdownPreview height={height} content={contentToRender} />)
+        fetchPost()
+    }, [content, fileId, title])
+    return (<>
+        {isLoading ? <div>Loading...</div> : <div data-theme={theme} dangerouslySetInnerHTML={{ __html: preview }} style={{
+            height
+        }} />}
+    </>)
+
 }
 
 export default memo(MarkdownPreview)
diff --git a/client/components/visibility-badge/index.tsx b/client/components/visibility-badge/index.tsx
index e534450..d907fda 100644
--- a/client/components/visibility-badge/index.tsx
+++ b/client/components/visibility-badge/index.tsx
@@ -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 = {
diff --git a/client/lib/hooks/use-signed-in.ts b/client/lib/hooks/use-signed-in.ts
index 3288424..e6da5bd 100644
--- a/client/lib/hooks/use-signed-in.ts
+++ b/client/lib/hooks/use-signed-in.ts
@@ -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;
diff --git a/client/lib/hooks/use-theme.ts b/client/lib/hooks/use-theme.ts
new file mode 100644
index 0000000..0a33332
--- /dev/null
+++ b/client/lib/hooks/use-theme.ts
@@ -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
\ No newline at end of file
diff --git a/client/lib/render-markdown.tsx b/client/lib/render-markdown.tsx
index 5f6f467..b92569e 100644
--- a/client/lib/render-markdown.tsx
+++ b/client/lib/render-markdown.tsx
@@ -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'
diff --git a/client/package.json b/client/package.json
index b17c82c..332b25a 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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",
diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx
index f29abeb..2a11ca6 100644
--- a/client/pages/_app.tsx
+++ b/client/pages/_app.tsx
@@ -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>
     </>
diff --git a/client/pages/_document.tsx b/client/pages/_document.tsx
index c732c7a..4f43e76 100644
--- a/client/pages/_document.tsx
+++ b/client/pages/_document.tsx
@@ -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) {
diff --git a/client/pages/api/markdown/[id].tsx b/client/pages/api/markdown/[id].ts
similarity index 87%
rename from client/pages/api/markdown/[id].tsx
rename to client/pages/api/markdown/[id].ts
index 359c33e..14d34d9 100644
--- a/client/pages/api/markdown/[id].tsx
+++ b/client/pages/api/markdown/[id].ts
@@ -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}
diff --git a/client/pages/api/render-markdown.ts b/client/pages/api/render-markdown.ts
new file mode 100644
index 0000000..a9e4162
--- /dev/null
+++ b/client/pages/api/render-markdown.ts
@@ -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
diff --git a/client/pages/index.tsx b/client/pages/index.tsx
index 0c24fe5..5d96533 100644
--- a/client/pages/index.tsx
+++ b/client/pages/index.tsx
@@ -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 />
diff --git a/client/pages/mine.tsx b/client/pages/mine.tsx
index 6cb50e8..6e5eeed 100644
--- a/client/pages/mine.tsx
+++ b/client/pages/mine.tsx
@@ -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 >
diff --git a/client/pages/new.tsx b/client/pages/new.tsx
index 3eb521e..14e4bce 100644
--- a/client/pages/new.tsx
+++ b/client/pages/new.tsx
@@ -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 >
diff --git a/client/pages/post/[id].tsx b/client/pages/post/[id].tsx
index d2eb31c..3327ce2 100644
--- a/client/pages/post/[id].tsx
+++ b/client/pages/post/[id].tsx
@@ -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 () => {
diff --git a/client/pages/post/private/[id].tsx b/client/pages/post/private/[id].tsx
index c93f717..f3c7d1e 100644
--- a/client/pages/post/private/[id].tsx
+++ b/client/pages/post/private/[id].tsx
@@ -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) => {
diff --git a/client/pages/post/protected/[id].tsx b/client/pages/post/protected/[id].tsx
index 787042b..d34e970 100644
--- a/client/pages/post/protected/[id].tsx
+++ b/client/pages/post/protected/[id].tsx
@@ -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
diff --git a/client/pages/signin.tsx b/client/pages/signin.tsx
index e36776d..4e1ed7c 100644
--- a/client/pages/signin.tsx
+++ b/client/pages/signin.tsx
@@ -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>
diff --git a/client/pages/signup.tsx b/client/pages/signup.tsx
index d2ac510..383f461 100644
--- a/client/pages/signup.tsx
+++ b/client/pages/signup.tsx
@@ -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>
diff --git a/client/styles/globals.css b/client/styles/globals.css
index 2e93b83..63af768 100644
--- a/client/styles/globals.css
+++ b/client/styles/globals.css
@@ -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;
diff --git a/client/yarn.lock b/client/yarn.lock
index 112e3aa..69809e3 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -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"