diff --git a/client/lib/hooks/use-trace-route.ts b/client/lib/hooks/use-trace-route.ts
new file mode 100644
index 00000000..b0268e32
--- /dev/null
+++ b/client/lib/hooks/use-trace-route.ts
@@ -0,0 +1,19 @@
+import { useRef, useEffect } from "react";
+
+function useTraceUpdate(props: { [key: string]: any }) {
+ const prev = useRef(props)
+ useEffect(() => {
+ const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
+ if (prev.current[k] !== v) {
+ ps[k] = [prev.current[k], v]
+ }
+ return ps
+ }, {} as { [key: string]: any })
+ if (Object.keys(changedProps).length > 0) {
+ console.log('Changed props:', changedProps)
+ }
+ prev.current = props
+ });
+}
+
+export default useTraceUpdate
\ No newline at end of file
diff --git a/client/lib/render-markdown.tsx b/client/lib/render-markdown.tsx
new file mode 100644
index 00000000..5f6f467d
--- /dev/null
+++ b/client/lib/render-markdown.tsx
@@ -0,0 +1,140 @@
+import Link from '@components/Link'
+import { marked } from 'marked'
+import Highlight, { defaultProps, Language } from 'prism-react-renderer'
+import { renderToStaticMarkup } from 'react-dom/server'
+//@ts-ignore
+delete defaultProps.theme
+// import linkStyles from '../components/link/link.module.css'
+
+const renderer = new marked.Renderer()
+
+renderer.heading = (text, level, _, slugger) => {
+ const id = slugger.slug(text)
+ const Component = `h${level}`
+
+ return renderToStaticMarkup(
+ //@ts-ignore
+
+
+ {text}
+
+
+ )
+}
+
+renderer.link = (href, _, text) => {
+ const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
+ if (isHrefLocal) {
+ return renderToStaticMarkup(
+
+ {text}
+
+ )
+ }
+ return `${text}`
+}
+
+renderer.image = function (href, _, text) {
+ return ``
+}
+
+renderer.checkbox = () => ''
+renderer.listitem = (text, task, checked) => {
+ if (task) {
+ return `
${text}`
+ }
+
+ return `${text}`
+}
+
+renderer.code = (code: string, language: string) => {
+ return renderToStaticMarkup(
+
+ {/* {title && {title}
} */}
+ {/* {language && title && {language}
} */}
+
+
+ )
+}
+
+marked.setOptions({
+ gfm: true,
+ breaks: true,
+ headerIds: true,
+ renderer,
+})
+
+const markdown = (markdown: string) => marked(markdown)
+
+export default markdown
+
+const Code = ({ code, language, highlight, title, ...props }: {
+ code: string,
+ language: string,
+ highlight?: string,
+ title?: string,
+}) => {
+ if (!language)
+ return (
+ <>
+
+ >
+ )
+
+ const highlightedLines = highlight
+ //@ts-ignore
+ ? highlight.split(',').reduce((lines, h) => {
+ if (h.includes('-')) {
+ // Expand ranges like 3-5 into [3,4,5]
+ const [start, end] = h.split('-').map(Number)
+ const x = Array(end - start + 1)
+ .fill(undefined)
+ .map((_, i) => i + start)
+ return [...lines, ...x]
+ }
+
+ return [...lines, Number(h)]
+ }, [])
+ : ''
+
+ // https://mdxjs.com/guides/syntax-harkedighlighting#all-together
+ return (
+ <>
+
+ {({ className, style, tokens, getLineProps, getTokenProps }) => (
+
+ {
+ tokens.map((line, i) => (
+
+ {
+ line.map((token, key) => (
+
+ ))
+ }
+
+ ))}
+
+ )}
+
+ >
+ )
+}
diff --git a/client/pages/api/markdown/[id].tsx b/client/pages/api/markdown/[id].tsx
new file mode 100644
index 00000000..359c33ec
--- /dev/null
+++ b/client/pages/api/markdown/[id].tsx
@@ -0,0 +1,44 @@
+import type { NextApiHandler } from "next";
+
+import markdown from "@lib/render-markdown";
+
+const renderMarkdown: NextApiHandler = async (req, res) => {
+ const { id } = req.query
+ const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, {
+ headers: {
+ 'Accept': 'text/plain',
+ 'x-secret-key': process.env.SECRET_KEY || '',
+ 'Authorization': `Bearer ${req.cookies['drift-token']}`,
+ }
+ })
+
+
+ const json = await file.json()
+ const { content, title } = json
+ const renderAsMarkdown = ['m', '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;
+
+ if (!renderAsMarkdown.includes(type)) {
+ contentToRender = `~~~${type}
+${content}
+~~~`
+ }
+
+ if (typeof contentToRender !== 'string') {
+ res.status(400).send('content must be a string')
+ return
+ }
+
+ res.setHeader('Content-Type', 'text/plain')
+ res.setHeader('Cache-Control', 'public, max-age=4800')
+ res.status(200).write(markdown(contentToRender))
+ res.end()
+}
+
+export default renderMarkdown
diff --git a/client/styles/Home.module.css b/client/styles/Home.module.css
index f770e0b2..3c3959ed 100644
--- a/client/styles/Home.module.css
+++ b/client/styles/Home.module.css
@@ -1,28 +1,11 @@
-.main {
- min-height: 100vh;
- flex: 1;
- display: flex;
- flex-direction: column;
- margin: 0 auto;
- width: var(--main-content-width);
-}
-
-.container {
+.wrapper {
+ height: 100% !important;
+ padding-bottom: var(--small-gap) !important;
width: 100% !important;
}
-@media screen and (max-width: 768px) {
- .container {
- width: 100%;
- margin: 0 auto !important;
- padding: 0;
- }
-
- .container h1 {
- font-size: 2rem;
- }
-
- .main {
- width: 100%;
- }
+.main {
+ max-width: var(--main-content) !important;
+ margin: 0 auto !important;
+ padding: 0 var(--gap) !important;
}
diff --git a/client/styles/globals.css b/client/styles/globals.css
index 4bcf1f4a..2e93b838 100644
--- a/client/styles/globals.css
+++ b/client/styles/globals.css
@@ -1,36 +1,251 @@
+@import "./syntax.css";
+@import "./markdown.css";
+@import "./nprogress.css";
+@import "./inter.css";
+
:root {
- --main-content-width: 800px;
- --page-nav-height: 60px;
- --gap: 8px;
- --gap-half: calc(var(--gap) / 2);
- --gap-double: calc(var(--gap) * 2);
- --border-radius: 4px;
- --font-size: 16px;
+ /* Spacing */
+ --gap-quarter: 0.25rem;
+ --gap-half: 0.5rem;
+ --gap: 1rem;
+ --gap-double: 2rem;
+ --small-gap: 4rem;
+ --big-gap: 4rem;
+ --main-content: 55rem;
+ --radius: 8px;
+ --inline-radius: 5px;
+
+ /* Typography */
+ --font-sans: "Inter", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
+ Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ --font-mono: ui-monospace, "SFMono-Regular", "Consolas", "Liberation Mono",
+ "Menlo", monospace;
+
+ /* Transitions */
+ --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 */
+ --token: #999;
+ --comment: #999;
+ --keyword: #fff;
+ --name: #fff;
+ --highlight: #2e2e2e;
}
-@media screen and (max-width: 768px) {
- :root {
- --main-content-width: 100%;
- }
-}
+[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);
-html,
-body {
- padding: 0;
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
- Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
-}
-
-a {
- color: inherit;
- text-decoration: none;
+ --token: #666;
+ --comment: #999;
+ --keyword: #000;
+ --name: #333;
+ --highlight: #eaeaea;
}
* {
box-sizing: border-box;
}
-li:before {
- content: "" !important;
+::selection {
+ text-shadow: none;
+ background: var(--selection);
+ color: var(--bg);
+}
+
+html {
+ line-height: 1.5;
+}
+
+html,
+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;
+}
+
+body {
+ min-height: 100vh;
+ font-family: var(--font-sans);
+ display: flex;
+ flex-direction: column;
+}
+
+p,
+li {
+ letter-spacing: -0.33px;
+ 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;
+ padding-left: 1rem;
+ 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;
+ padding: 2px 7px;
+ font-weight: 600;
+ background: var(--lighter-gray);
+ 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;
+ --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;
+ --name: #333;
+ --highlight: #eaeaea;
+ }
+
+ * {
+ text-shadow: none !important;
+ }
}
diff --git a/client/styles/inter.css b/client/styles/inter.css
new file mode 100644
index 00000000..61f0df75
--- /dev/null
+++ b/client/styles/inter.css
@@ -0,0 +1,100 @@
+/* latin */
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 100;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 200;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 300;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 400;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 500;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 600;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 700;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 800;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 900;
+ font-display: block;
+ src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
+ format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
+ U+FEFF, U+FFFD;
+}
diff --git a/client/styles/markdown.css b/client/styles/markdown.css
new file mode 100644
index 00000000..fb3fc107
--- /dev/null
+++ b/client/styles/markdown.css
@@ -0,0 +1,140 @@
+article {
+ max-width: var(--main-content);
+ margin: 0 auto;
+ line-height: 1.9;
+}
+
+article > * + * {
+ margin-top: 2em;
+}
+
+article p {
+ color: var(--article-color);
+}
+
+article img {
+ max-width: 100%;
+ /* width: var(--main-content); */
+ width: auto;
+ margin: auto;
+ display: block;
+ border-radius: var(--radius);
+}
+
+article [id]::before {
+ content: "";
+ display: block;
+ height: 70px;
+ margin-top: -70px;
+ visibility: hidden;
+}
+
+/* Lists */
+
+article ul {
+ padding: 0;
+ list-style-position: inside;
+ list-style-type: circle;
+}
+
+article ol {
+ padding: 0;
+ list-style-position: inside;
+}
+
+article ul li.reset {
+ display: flex;
+ align-items: flex-start;
+
+ list-style-type: none;
+ margin-left: -0.5rem;
+}
+
+article ul li.reset .check {
+ display: flex;
+ align-items: center;
+ margin-right: 0.51rem;
+}
+
+/* Checkbox */
+
+input[type="checkbox"] {
+ vertical-align: middle;
+ appearance: none;
+ display: inline-block;
+ background-origin: border-box;
+ user-select: none;
+ flex-shrink: 0;
+ height: 1rem;
+ width: 1rem;
+ background-color: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--fg);
+ border-radius: 3px;
+}
+
+input[type="checkbox"]:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+html[data-theme="light"] input[type="checkbox"]:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
+}
+
+input[type="checkbox"]:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px var(--gray);
+ border-color: var(--fg);
+}
+
+/* Code Snippets */
+
+.token-line:not(:last-child) {
+ min-height: 1.4rem;
+}
+
+article *:not(pre) > code {
+ font-weight: 600;
+ font-family: var(--font-sans);
+ font-size: 1rem;
+ padding: 0 3px;
+}
+
+article *:not(pre) > code::before,
+article *:not(pre) > code::after {
+ content: "\`";
+ color: var(--gray);
+ user-select: none;
+}
+
+article pre {
+ overflow-x: auto;
+ background: var(--lightest-gray);
+ border-radius: var(--inline-radius);
+ line-height: 1.8;
+ padding: 1rem;
+ font-size: 0.875rem;
+}
+
+/* Linkable Headers */
+
+.header-link {
+ color: inherit;
+ text-decoration: none;
+}
+
+.header-link::after {
+ opacity: 0;
+ content: "#";
+ margin-left: var(--gap-half);
+ color: var(--gray);
+}
+
+.header-link:hover::after {
+ opacity: 1;
+}
diff --git a/client/styles/nprogress.css b/client/styles/nprogress.css
new file mode 100644
index 00000000..0db6e676
--- /dev/null
+++ b/client/styles/nprogress.css
@@ -0,0 +1,23 @@
+#nprogress {
+ pointer-events: none;
+}
+
+#nprogress .bar {
+ position: fixed;
+ z-index: 2000;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 5px;
+ background: var(--fg);
+}
+
+#nprogress::after {
+ content: "";
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 5px;
+ background: transparent;
+}
diff --git a/client/styles/syntax.css b/client/styles/syntax.css
new file mode 100644
index 00000000..42a27295
--- /dev/null
+++ b/client/styles/syntax.css
@@ -0,0 +1,24 @@
+.keyword {
+ font-weight: bold;
+ color: var(--keyword);
+}
+
+.token.operator,
+.token.punctuation,
+.token.string,
+.token.number,
+.token.builtin,
+.token.variable {
+ color: var(--token);
+}
+
+.token.comment {
+ color: var(--comment);
+}
+
+.token.class-name,
+.token.function,
+.token.tag,
+.token.attr-name {
+ color: var(--name);
+}