mirror of https://github.com/MaxLeiter/Drift
				
				
				
			Merge with main
						commit
						da8e7415dc
					
				@ -1,3 +1,4 @@
 | 
			
		||||
API_URL=http://localhost:3000
 | 
			
		||||
WELCOME_TITLE="Welcome to Drift"
 | 
			
		||||
WELCOME_CONTENT="### Drift is a self-hostable clone of GitHub Gist. \nIt is a simple way to share code and text snippets with your friends, with support for the following:\n  \n  - Render GitHub Extended Markdown (including images)\n  - User authentication\n  - Private, public, and secret posts\n  \n  If you want to signup, you can join at [/signup](/signup) as long as you have a passcode provided by the administrator (which you don\'t need for this demo).\n  **This demo is on a memory-only database, so accounts and pastes can be deleted at any time.**\n  You can find the source code on [GitHub](https://github.com/MaxLeiter/drift).\n  \n  Drift was inspired by [this tweet](https://twitter.com/emilyst/status/1499858264346935297):\n  > What is the absolute closest thing to GitHub Gist that can be self-hosted?\n    In terms of design and functionality. Hosts images and markdown, rendered. Creates links that can be private or public. Uses/requires registration. I have looked at dozens of pastebin-like things."
 | 
			
		||||
SECRET_KEY=secret
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "semi": false,
 | 
			
		||||
  "trailingComma": "none",
 | 
			
		||||
  "singleQuote": false,
 | 
			
		||||
  "printWidth": 80,
 | 
			
		||||
  "useTabs": true
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
import { GeistProvider, CssBaseline, Themes } from "@geist-ui/core"
 | 
			
		||||
import type { NextComponentType, NextPageContext } from "next"
 | 
			
		||||
import { SkeletonTheme } from "react-loading-skeleton"
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
const App = ({
 | 
			
		||||
    Component,
 | 
			
		||||
    pageProps,
 | 
			
		||||
}: {
 | 
			
		||||
    Component: NextComponentType<NextPageContext, any, any>
 | 
			
		||||
    pageProps: any
 | 
			
		||||
}) => {
 | 
			
		||||
    const skeletonBaseColor = 'var(--light-gray)'
 | 
			
		||||
    const skeletonHighlightColor = 'var(--lighter-gray)'
 | 
			
		||||
 | 
			
		||||
    const customTheme = Themes.createFromLight(
 | 
			
		||||
        {
 | 
			
		||||
            type: "custom",
 | 
			
		||||
            palette: {
 | 
			
		||||
                background: 'var(--bg)',
 | 
			
		||||
                foreground: 'var(--fg)',
 | 
			
		||||
                accents_1: 'var(--lightest-gray)',
 | 
			
		||||
                accents_2: 'var(--lighter-gray)',
 | 
			
		||||
                accents_3: 'var(--light-gray)',
 | 
			
		||||
                accents_4: 'var(--gray)',
 | 
			
		||||
                accents_5: 'var(--darker-gray)',
 | 
			
		||||
                accents_6: 'var(--darker-gray)',
 | 
			
		||||
                accents_7: 'var(--darkest-gray)',
 | 
			
		||||
                accents_8: 'var(--darkest-gray)',
 | 
			
		||||
                border: 'var(--light-gray)',
 | 
			
		||||
            },
 | 
			
		||||
            font: {
 | 
			
		||||
                mono: 'var(--font-mono)',
 | 
			
		||||
                sans: 'var(--font-sans)',
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    return (<GeistProvider themes={[customTheme]} themeType={"custom"} >
 | 
			
		||||
        <SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}>
 | 
			
		||||
            <CssBaseline />
 | 
			
		||||
            <Component {...pageProps} />
 | 
			
		||||
        </SkeletonTheme>
 | 
			
		||||
    </GeistProvider >)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
@ -0,0 +1,53 @@
 | 
			
		||||
.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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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);
 | 
			
		||||
}
 | 
			
		||||
@ -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,41 +0,0 @@
 | 
			
		||||
.input {
 | 
			
		||||
  background: #efefef;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.descriptionContainer {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  min-height: 400px;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer > div {
 | 
			
		||||
  /* Override geist-ui styling */
 | 
			
		||||
  margin: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.textarea {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.actionWrapper {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.actionWrapper .actions {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: 0;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,35 @@
 | 
			
		||||
import type { Document } from "@lib/types"
 | 
			
		||||
import DocumentComponent from "@components/edit-document"
 | 
			
		||||
import { ChangeEvent, memo, useCallback } from "react"
 | 
			
		||||
 | 
			
		||||
const DocumentList = ({ docs, removeDoc, updateDocContent, updateDocTitle, onPaste }: {
 | 
			
		||||
    docs: Document[],
 | 
			
		||||
    updateDocTitle: (i: number) => (title: string) => void
 | 
			
		||||
    updateDocContent: (i: number) => (content: string) => void
 | 
			
		||||
    removeDoc: (i: number) => () => void
 | 
			
		||||
    onPaste: (e: any) => void
 | 
			
		||||
}) => {
 | 
			
		||||
    const handleOnChange = useCallback((i) => (e: ChangeEvent<HTMLTextAreaElement>) => {
 | 
			
		||||
        updateDocContent(i)(e.target.value)
 | 
			
		||||
    }, [updateDocContent])
 | 
			
		||||
 | 
			
		||||
    return (<>{
 | 
			
		||||
        docs.map(({ content, id, title }, i) => {
 | 
			
		||||
            return (
 | 
			
		||||
                <DocumentComponent
 | 
			
		||||
                    onPaste={onPaste}
 | 
			
		||||
                    key={id}
 | 
			
		||||
                    remove={removeDoc(i)}
 | 
			
		||||
                    setContent={updateDocContent(i)}
 | 
			
		||||
                    setTitle={updateDocTitle(i)}
 | 
			
		||||
                    handleOnContentChange={handleOnChange(i)}
 | 
			
		||||
                    content={content}
 | 
			
		||||
                    title={title}
 | 
			
		||||
                />
 | 
			
		||||
            )
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    </>)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default memo(DocumentList)
 | 
			
		||||
@ -0,0 +1,48 @@
 | 
			
		||||
.card {
 | 
			
		||||
	margin: var(--gap) auto;
 | 
			
		||||
	padding: var(--gap);
 | 
			
		||||
	border: 1px solid var(--light-gray);
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.input {
 | 
			
		||||
	background: #efefef;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.descriptionContainer {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	min-height: 400px;
 | 
			
		||||
	overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	justify-content: space-between;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	height: 36px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer > div {
 | 
			
		||||
	/* Override geist-ui styling */
 | 
			
		||||
	margin: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.textarea {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.actionWrapper {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.actionWrapper .actions {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	right: 0;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,123 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import { ChangeEvent, memo, useCallback, useMemo, useRef, useState } from "react"
 | 
			
		||||
import styles from './document.module.css'
 | 
			
		||||
import Trash from '@geist-ui/icons/trash'
 | 
			
		||||
import Download from '@geist-ui/icons/download'
 | 
			
		||||
import ExternalLink from '@geist-ui/icons/externalLink'
 | 
			
		||||
import FormattingIcons from "./formatting-icons"
 | 
			
		||||
import Skeleton from "react-loading-skeleton"
 | 
			
		||||
 | 
			
		||||
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 = {
 | 
			
		||||
    title?: string
 | 
			
		||||
    content?: string
 | 
			
		||||
    setTitle?: (title: string) => void
 | 
			
		||||
    setContent?: (content: string) => void
 | 
			
		||||
    handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
 | 
			
		||||
    initialTab?: "edit" | "preview"
 | 
			
		||||
    skeleton?: boolean
 | 
			
		||||
    remove?: () => void
 | 
			
		||||
    onPaste?: (e: any) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Document = ({ onPaste, remove, title, content, setTitle, setContent, initialTab = 'edit', skeleton, handleOnContentChange }: Props) => {
 | 
			
		||||
    const codeEditorRef = useRef<HTMLTextAreaElement>(null)
 | 
			
		||||
    const [tab, setTab] = useState(initialTab)
 | 
			
		||||
    // const height = editable ? "500px" : '100%'
 | 
			
		||||
    const height = "100%";
 | 
			
		||||
 | 
			
		||||
    const handleTabChange = (newTab: string) => {
 | 
			
		||||
        if (newTab === 'edit') {
 | 
			
		||||
            codeEditorRef.current?.focus()
 | 
			
		||||
        }
 | 
			
		||||
        setTab(newTab as 'edit' | 'preview')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onTitleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setTitle ? setTitle(event.target.value) : null, [setTitle])
 | 
			
		||||
 | 
			
		||||
    const removeFile = useCallback((remove?: () => void) => {
 | 
			
		||||
        if (remove) {
 | 
			
		||||
            if (content && content.trim().length > 0) {
 | 
			
		||||
                const confirmed = window.confirm("Are you sure you want to remove this file?")
 | 
			
		||||
                if (confirmed) {
 | 
			
		||||
                    remove()
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                remove()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }, [content])
 | 
			
		||||
 | 
			
		||||
    if (skeleton) {
 | 
			
		||||
        return <>
 | 
			
		||||
            <Spacer height={1} />
 | 
			
		||||
            <div className={styles.card}>
 | 
			
		||||
                <div className={styles.fileNameContainer}>
 | 
			
		||||
                    <Skeleton width={275} height={36} />
 | 
			
		||||
                    {remove && <Skeleton width={36} height={36} />}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className={styles.descriptionContainer}>
 | 
			
		||||
                    <div style={{ flexDirection: 'row', display: 'flex' }}><Skeleton width={125} height={36} /></div>
 | 
			
		||||
                    <Skeleton width={'100%'} height={350} />
 | 
			
		||||
                </div >
 | 
			
		||||
            </div>
 | 
			
		||||
        </>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <Spacer height={1} />
 | 
			
		||||
            <div className={styles.card}>
 | 
			
		||||
                <div className={styles.fileNameContainer}>
 | 
			
		||||
                    <Input
 | 
			
		||||
                        placeholder="MyFile.md"
 | 
			
		||||
                        value={title}
 | 
			
		||||
                        onChange={onTitleChange}
 | 
			
		||||
                        marginTop="var(--gap-double)"
 | 
			
		||||
                        size={1.2}
 | 
			
		||||
                        font={1.2}
 | 
			
		||||
                        label="Filename"
 | 
			
		||||
                        width={"100%"}
 | 
			
		||||
                        id={title}
 | 
			
		||||
                    />
 | 
			
		||||
                    {remove && <Button type="abort" ghost icon={<Trash />} auto height={'36px'} width={'36px'} onClick={() => removeFile(remove)} />}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className={styles.descriptionContainer}>
 | 
			
		||||
                    {tab === 'edit' && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
 | 
			
		||||
                    <Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
 | 
			
		||||
                        <Tabs.Item label={"Edit"} value="edit">
 | 
			
		||||
                            {/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
 | 
			
		||||
                            <div style={{ marginTop: 'var(--gap-half)', display: 'flex', flexDirection: 'column' }}>
 | 
			
		||||
                                <Textarea
 | 
			
		||||
                                    onPaste={onPaste ? onPaste : undefined}
 | 
			
		||||
                                    ref={codeEditorRef}
 | 
			
		||||
                                    placeholder=""
 | 
			
		||||
                                    value={content}
 | 
			
		||||
                                    onChange={handleOnContentChange}
 | 
			
		||||
                                    width="100%"
 | 
			
		||||
                                    // TODO: Textarea should grow to fill parent if height == 100%
 | 
			
		||||
                                    style={{ flex: 1, minHeight: 350 }}
 | 
			
		||||
                                    resize="vertical"
 | 
			
		||||
                                    className={styles.textarea}
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </Tabs.Item>
 | 
			
		||||
                        <Tabs.Item label="Preview" value="preview">
 | 
			
		||||
                            <div style={{ marginTop: 'var(--gap-half)', }}>
 | 
			
		||||
                                <Preview height={height} title={title} content={content} />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </Tabs.Item>
 | 
			
		||||
                    </Tabs>
 | 
			
		||||
 | 
			
		||||
                </div >
 | 
			
		||||
            </div >
 | 
			
		||||
        </>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default memo(Document)
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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 { useTheme } from "next-themes"
 | 
			
		||||
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, signout } = useSignedIn()
 | 
			
		||||
    const [pages, setPages] = useState<Tab[]>([])
 | 
			
		||||
    const { setTheme, theme } = useTheme()
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setBodyHidden(expanded)
 | 
			
		||||
    }, [expanded, setBodyHidden])
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!isMobile) {
 | 
			
		||||
            setExpanded(false)
 | 
			
		||||
        }
 | 
			
		||||
    }, [isMobile])
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const defaultPages: Tab[] = [
 | 
			
		||||
            {
 | 
			
		||||
                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')
 | 
			
		||||
                        setTheme(theme === 'light' ? 'dark' : 'light');
 | 
			
		||||
                },
 | 
			
		||||
                icon: theme === 'light' ? <MoonIcon /> : <SunIcon />,
 | 
			
		||||
                condition: true,
 | 
			
		||||
                value: "theme",
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        if (isSignedIn)
 | 
			
		||||
            setPages([
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'new',
 | 
			
		||||
                    icon: <NewIcon />,
 | 
			
		||||
                    value: 'new',
 | 
			
		||||
                    href: '/'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'yours',
 | 
			
		||||
                    icon: <YourIcon />,
 | 
			
		||||
                    value: 'yours',
 | 
			
		||||
                    href: '/mine'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'sign out',
 | 
			
		||||
                    icon: <SignOutIcon />,
 | 
			
		||||
                    value: 'signout',
 | 
			
		||||
                    onClick: signout
 | 
			
		||||
                },
 | 
			
		||||
                ...defaultPages
 | 
			
		||||
            ])
 | 
			
		||||
        else
 | 
			
		||||
            setPages([
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'home',
 | 
			
		||||
                    icon: <HomeIcon />,
 | 
			
		||||
                    value: 'home',
 | 
			
		||||
                    href: '/'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'Sign in',
 | 
			
		||||
                    icon: <SignInIcon />,
 | 
			
		||||
                    value: 'signin',
 | 
			
		||||
                    href: '/signin'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'Sign up',
 | 
			
		||||
                    icon: <SignUpIcon />,
 | 
			
		||||
                    value: 'signup',
 | 
			
		||||
                    href: '/signup'
 | 
			
		||||
                },
 | 
			
		||||
                ...defaultPages
 | 
			
		||||
            ])
 | 
			
		||||
        // TODO: investigate deps causing infinite loop 
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    }, [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,223 +1,8 @@
 | 
			
		||||
import { Page, ButtonGroup, Button, useBodyScroll, useMediaQuery, Tabs, Spacer } from "@geist-ui/core";
 | 
			
		||||
import { Github as GitHubIcon, UserPlus as SignUpIcon, User as SignInIcon, Home as HomeIcon, Menu as MenuIcon, Tool as SettingsIcon, UserX as SignoutIcon, PlusCircle as NewIcon, List as YourIcon, Moon, Sun } from "@geist-ui/icons";
 | 
			
		||||
import { DriftProps } from "../../pages/_app";
 | 
			
		||||
import { useEffect, useMemo, useState } from "react";
 | 
			
		||||
import styles from './header.module.css';
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import useSignedIn from "../../lib/hooks/use-signed-in";
 | 
			
		||||
import Cookies from 'js-cookie'
 | 
			
		||||
import dynamic from 'next/dynamic'
 | 
			
		||||
 | 
			
		||||
type Tab = {
 | 
			
		||||
    name: string
 | 
			
		||||
    icon: JSX.Element
 | 
			
		||||
    condition?: boolean
 | 
			
		||||
    value: string
 | 
			
		||||
    onClick?: () => void
 | 
			
		||||
    href?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const Header = ({ changeTheme, theme }: DriftProps) => {
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
    const [selectedTab, setSelectedTab] = useState<string>();
 | 
			
		||||
    const [expanded, setExpanded] = useState<boolean>(false)
 | 
			
		||||
    const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
 | 
			
		||||
    const isMobile = useMediaQuery('xs', { match: 'down' })
 | 
			
		||||
    const { isLoading, isSignedIn, signout } = useSignedIn({ redirectIfNotAuthed: false })
 | 
			
		||||
    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: true,
 | 
			
		||||
                value: "home"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: "New",
 | 
			
		||||
                href: "/new",
 | 
			
		||||
                icon: <NewIcon />,
 | 
			
		||||
                condition: isSignedIn,
 | 
			
		||||
                value: "new"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: "Yours",
 | 
			
		||||
                href: "/mine",
 | 
			
		||||
                icon: <YourIcon />,
 | 
			
		||||
                condition: isSignedIn,
 | 
			
		||||
                value: "mine"
 | 
			
		||||
            },
 | 
			
		||||
            // {
 | 
			
		||||
            //     name: "Settings",
 | 
			
		||||
            //     href: "/settings",
 | 
			
		||||
            //     icon: <SettingsIcon />,
 | 
			
		||||
            //     condition: isSignedIn
 | 
			
		||||
            // },
 | 
			
		||||
            {
 | 
			
		||||
                name: "Sign out",
 | 
			
		||||
                onClick: () => {
 | 
			
		||||
                    if (typeof window !== 'undefined') {
 | 
			
		||||
                        localStorage.clear();
 | 
			
		||||
 | 
			
		||||
                        // // send token to API blacklist
 | 
			
		||||
                        // fetch('/api/auth/signout', {
 | 
			
		||||
                        //     method: 'POST',
 | 
			
		||||
                        //     headers: {
 | 
			
		||||
                        //         'Content-Type': 'application/json'
 | 
			
		||||
                        //     },
 | 
			
		||||
                        //     body: JSON.stringify({
 | 
			
		||||
                        //         token: Cookies.get("drift-token")
 | 
			
		||||
                        //     })
 | 
			
		||||
                        // })
 | 
			
		||||
 | 
			
		||||
                        signout();
 | 
			
		||||
                        router.push("/signin");
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                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(undefined);
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                icon: theme === 'light' ? <Moon /> : <Sun />,
 | 
			
		||||
                condition: true,
 | 
			
		||||
                value: "theme",
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        if (isLoading) {
 | 
			
		||||
            return setPages([])
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setPages(pageList.filter(page => page.condition))
 | 
			
		||||
    }, [changeTheme, isLoading, isMobile, isSignedIn, router, signout, 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}>
 | 
			
		||||
                    {!isLoading && 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 > * /}
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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,17 +1,7 @@
 | 
			
		||||
import useSWR from "swr"
 | 
			
		||||
import PostList from "../post-list"
 | 
			
		||||
import Cookies from "js-cookie"
 | 
			
		||||
 | 
			
		||||
const fetcher = (url: string) => fetch(url, {
 | 
			
		||||
    headers: {
 | 
			
		||||
        'Content-Type': 'application/json',
 | 
			
		||||
        'Authorization': `Bearer ${Cookies.get("drift-token")}`
 | 
			
		||||
    },
 | 
			
		||||
}).then(r => r.json())
 | 
			
		||||
 | 
			
		||||
const MyPosts = () => {
 | 
			
		||||
    const { data, error } = useSWR('/server-api/users/mine', fetcher)
 | 
			
		||||
    return <PostList posts={data} error={error} />
 | 
			
		||||
const MyPosts = ({ posts, error }: { posts: any, error: any }) => {
 | 
			
		||||
    return <PostList posts={posts} error={error} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MyPosts
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,52 @@
 | 
			
		||||
 | 
			
		||||
import { Modal, Note, Spacer, Input } from "@geist-ui/core"
 | 
			
		||||
import { useState } from "react"
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    creating?: boolean
 | 
			
		||||
    isOpen: boolean
 | 
			
		||||
    onClose: () => void
 | 
			
		||||
    onSubmit: (password: string) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PasswordModal = ({ isOpen, onClose, onSubmit: onSubmitAfterVerify, creating }: Props) => {
 | 
			
		||||
    const [password, setPassword] = useState<string>()
 | 
			
		||||
    const [confirmPassword, setConfirmPassword] = useState<string>()
 | 
			
		||||
    const [error, setError] = useState<string>()
 | 
			
		||||
 | 
			
		||||
    const onSubmit = () => {
 | 
			
		||||
        if (!password || (creating && !confirmPassword)) {
 | 
			
		||||
            setError('Please enter a password')
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (password !== confirmPassword && creating) {
 | 
			
		||||
            setError("Passwords do not match")
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        onSubmitAfterVerify(password)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (<>
 | 
			
		||||
        {<Modal visible={isOpen} >
 | 
			
		||||
            <Modal.Title>Enter a password</Modal.Title>
 | 
			
		||||
            <Modal.Content>
 | 
			
		||||
                {!error && creating && <Note type="warning" label='Warning'>
 | 
			
		||||
                    This doesn't protect your post from the server administrator.
 | 
			
		||||
                </Note>}
 | 
			
		||||
                {error && <Note type="error" label='Error'>
 | 
			
		||||
                    {error}
 | 
			
		||||
                </Note>}
 | 
			
		||||
                <Spacer />
 | 
			
		||||
                <Input width={"100%"} label="Password" marginBottom={1} htmlType="password" placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
 | 
			
		||||
                {creating && <Input width={"100%"} label="Confirm" htmlType="password" placeholder="Confirm Password" onChange={(e) => setConfirmPassword(e.target.value)} />}
 | 
			
		||||
            </Modal.Content>
 | 
			
		||||
            <Modal.Action passive onClick={onClose}>Cancel</Modal.Action>
 | 
			
		||||
            <Modal.Action onClick={onSubmit}>Submit</Modal.Action>
 | 
			
		||||
        </Modal>}
 | 
			
		||||
    </>)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default PasswordModal
 | 
			
		||||
@ -0,0 +1,71 @@
 | 
			
		||||
import Header from "@components/header/header"
 | 
			
		||||
import PageSeo from "@components/page-seo"
 | 
			
		||||
import VisibilityBadge from "@components/visibility-badge"
 | 
			
		||||
import DocumentComponent from '@components/view-document'
 | 
			
		||||
import styles from './post-page.module.css'
 | 
			
		||||
import homeStyles from '@styles/Home.module.css'
 | 
			
		||||
 | 
			
		||||
import type { File, Post } from "@lib/types"
 | 
			
		||||
import { Page, Button, Text } from "@geist-ui/core"
 | 
			
		||||
import ShiftBy from "@components/shift-by"
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    post: Post
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PostPage = ({ post }: Props) => {
 | 
			
		||||
    const download = async () => {
 | 
			
		||||
        const downloadZip = (await import("client-zip")).downloadZip
 | 
			
		||||
        const blob = await downloadZip(post.files.map((file: any) => {
 | 
			
		||||
            return {
 | 
			
		||||
                name: file.title,
 | 
			
		||||
                input: file.content,
 | 
			
		||||
                lastModified: new Date(file.updatedAt)
 | 
			
		||||
            }
 | 
			
		||||
        })).blob()
 | 
			
		||||
        const link = document.createElement("a")
 | 
			
		||||
        link.href = URL.createObjectURL(blob)
 | 
			
		||||
        link.download = `${post.title}.zip`
 | 
			
		||||
        link.click()
 | 
			
		||||
        link.remove()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Page width={"100%"}>
 | 
			
		||||
            <PageSeo
 | 
			
		||||
                title={`${post.title} - Drift`}
 | 
			
		||||
                description={post.description}
 | 
			
		||||
                isPrivate={false}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <Page.Header>
 | 
			
		||||
                <Header />
 | 
			
		||||
            </Page.Header>
 | 
			
		||||
            <Page.Content className={homeStyles.main}>
 | 
			
		||||
                {/* {!isLoading && <PostFileExplorer files={post.files} />} */}
 | 
			
		||||
                <div className={styles.header}>
 | 
			
		||||
                    <div className={styles.titleAndBadge}>
 | 
			
		||||
                        <Text h2>{post.title}</Text>
 | 
			
		||||
                        <ShiftBy y={-5}>
 | 
			
		||||
                            <VisibilityBadge visibility={post.visibility} />
 | 
			
		||||
                        </ShiftBy>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <Button auto onClick={download}>
 | 
			
		||||
                        Download as ZIP archive
 | 
			
		||||
                    </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
                {post.files.map(({ id, content, title }: File) => (
 | 
			
		||||
                    <DocumentComponent
 | 
			
		||||
                        key={id}
 | 
			
		||||
                        title={title}
 | 
			
		||||
                        initialTab={'preview'}
 | 
			
		||||
                        id={id}
 | 
			
		||||
                        content={content}
 | 
			
		||||
                    />
 | 
			
		||||
                ))}
 | 
			
		||||
            </Page.Content>
 | 
			
		||||
        </Page >
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PostPage
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
.header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header .titleAndBadge {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 650px) {
 | 
			
		||||
  .header {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .header .titleAndBadge {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    padding-bottom: var(--gap-double);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,28 +1,55 @@
 | 
			
		||||
import { memo, useEffect, useState } from "react"
 | 
			
		||||
import ReactMarkdownPreview from "./react-markdown-preview"
 | 
			
		||||
import styles from './preview.module.css'
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    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(`/server-api/files/html/${fileId}`, {
 | 
			
		||||
                    method: "GET",
 | 
			
		||||
                })
 | 
			
		||||
                if (resp.ok) {
 | 
			
		||||
                    const res = await resp.text()
 | 
			
		||||
                    setPreview(res)
 | 
			
		||||
                    setIsLoading(false)
 | 
			
		||||
                }
 | 
			
		||||
            } else if (content) {
 | 
			
		||||
                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} />)
 | 
			
		||||
            }
 | 
			
		||||
            setIsLoading(false)
 | 
			
		||||
        }
 | 
			
		||||
        fetchPost()
 | 
			
		||||
    }, [content, fileId, title])
 | 
			
		||||
    return (<>
 | 
			
		||||
        {isLoading ? <div>Loading...</div> : <article className={styles.markdownPreview} dangerouslySetInnerHTML={{ __html: preview }} style={{
 | 
			
		||||
            height
 | 
			
		||||
        }} />}
 | 
			
		||||
    </>)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default memo(MarkdownPreview)
 | 
			
		||||
 | 
			
		||||
@ -1,59 +0,0 @@
 | 
			
		||||
import ReactMarkdown from "react-markdown"
 | 
			
		||||
import remarkGfm from "remark-gfm"
 | 
			
		||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
 | 
			
		||||
import rehypeSlug from 'rehype-slug'
 | 
			
		||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
 | 
			
		||||
 | 
			
		||||
// @ts-ignore because of no types in remark-a11y-emoji
 | 
			
		||||
import a11yEmoji from '@fec/remark-a11y-emoji';
 | 
			
		||||
 | 
			
		||||
import styles from './preview.module.css'
 | 
			
		||||
import { vscDarkPlus as dark, vs as light } from 'react-syntax-highlighter/dist/cjs/styles/prism'
 | 
			
		||||
import useSharedState from "@lib/hooks/use-shared-state";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
    content: string | undefined
 | 
			
		||||
    height: number | string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ReactMarkdownPreview = ({ content, height }: Props) => {
 | 
			
		||||
    const [themeType] = useSharedState<string>('theme')
 | 
			
		||||
    return (<div style={{ height }}>
 | 
			
		||||
        <ReactMarkdown className={styles.markdownPreview}
 | 
			
		||||
            remarkPlugins={[remarkGfm, a11yEmoji]}
 | 
			
		||||
            rehypePlugins={[rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]]}
 | 
			
		||||
            components={{
 | 
			
		||||
                code({ node, inline, className, children, ...props }) {
 | 
			
		||||
                    const match = /language-(\w+)/.exec(className || '')
 | 
			
		||||
                    return !inline && match ? (
 | 
			
		||||
                        <SyntaxHighlighter
 | 
			
		||||
                            lineNumberStyle={{
 | 
			
		||||
                                minWidth: "2.25rem"
 | 
			
		||||
                            }}
 | 
			
		||||
                            customStyle={{
 | 
			
		||||
                                padding: 0,
 | 
			
		||||
                                margin: 0,
 | 
			
		||||
                                background: 'transparent'
 | 
			
		||||
                            }}
 | 
			
		||||
                            codeTagProps={{
 | 
			
		||||
                                style: { background: 'transparent' }
 | 
			
		||||
                            }}
 | 
			
		||||
                            style={themeType === 'dark' ? dark : light}
 | 
			
		||||
                            showLineNumbers={true}
 | 
			
		||||
                            language={match[1]}
 | 
			
		||||
                            PreTag="div"
 | 
			
		||||
                            {...props}
 | 
			
		||||
                        >{String(children).replace(/\n$/, '')}</SyntaxHighlighter>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <code className={className} {...props}>
 | 
			
		||||
                            {children}
 | 
			
		||||
                        </code>
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }}>
 | 
			
		||||
            {content || ""}
 | 
			
		||||
        </ReactMarkdown></div>)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default ReactMarkdownPreview
 | 
			
		||||
@ -0,0 +1,22 @@
 | 
			
		||||
import type { Document } from "@lib/types"
 | 
			
		||||
import DocumentComponent from "@components/edit-document"
 | 
			
		||||
import { memo, } from "react"
 | 
			
		||||
 | 
			
		||||
const DocumentList = ({ docs }: {
 | 
			
		||||
    docs: Document[],
 | 
			
		||||
}) => {
 | 
			
		||||
    return (<>{
 | 
			
		||||
        docs.map(({ content, id, title }) => {
 | 
			
		||||
            return (
 | 
			
		||||
                <DocumentComponent
 | 
			
		||||
                    key={id}
 | 
			
		||||
                    content={content}
 | 
			
		||||
                    title={title}
 | 
			
		||||
                />
 | 
			
		||||
            )
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    </>)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default memo(DocumentList)
 | 
			
		||||
@ -0,0 +1,40 @@
 | 
			
		||||
.card {
 | 
			
		||||
	margin: var(--gap) auto;
 | 
			
		||||
	padding: var(--gap);
 | 
			
		||||
	border: 1px solid var(--light-gray);
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.descriptionContainer {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	min-height: 400px;
 | 
			
		||||
	overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	justify-content: space-between;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	height: 36px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileNameContainer > div {
 | 
			
		||||
	/* Override geist-ui styling */
 | 
			
		||||
	margin: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.actionWrapper {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.actionWrapper .actions {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	right: 0;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,13 @@
 | 
			
		||||
import type { PostVisibility } from "./types"
 | 
			
		||||
 | 
			
		||||
export default function getPostPath(visibility: PostVisibility, id: string) {
 | 
			
		||||
	switch (visibility) {
 | 
			
		||||
		case "private":
 | 
			
		||||
			return `/post/private/${id}`
 | 
			
		||||
		case "protected":
 | 
			
		||||
			return `/post/protected/${id}`
 | 
			
		||||
		case "unlisted":
 | 
			
		||||
		case "public":
 | 
			
		||||
			return `/post/${id}`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
// useDebounce.js
 | 
			
		||||
import { useState, useEffect } from "react"
 | 
			
		||||
 | 
			
		||||
export default function useDebounce(value: any, delay: number) {
 | 
			
		||||
	const [debouncedValue, setDebouncedValue] = useState(value)
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		const handler = setTimeout(() => {
 | 
			
		||||
			setDebouncedValue(value)
 | 
			
		||||
		}, delay)
 | 
			
		||||
 | 
			
		||||
		return () => {
 | 
			
		||||
			clearTimeout(handler)
 | 
			
		||||
		}
 | 
			
		||||
	}, [value, delay])
 | 
			
		||||
 | 
			
		||||
	return debouncedValue
 | 
			
		||||
}
 | 
			
		||||
@ -1,45 +1,35 @@
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { useCallback, useEffect } from "react"
 | 
			
		||||
import useSharedState from "./use-shared-state";
 | 
			
		||||
import Cookies from 'js-cookie'
 | 
			
		||||
import Cookies from "js-cookie"
 | 
			
		||||
import { useRouter } from "next/router"
 | 
			
		||||
import { useEffect, useState } from "react"
 | 
			
		||||
import useSharedState from "./use-shared-state"
 | 
			
		||||
 | 
			
		||||
const useSignedIn = ({ redirectIfNotAuthed = false }: { redirectIfNotAuthed?: boolean }) => {
 | 
			
		||||
    const [isSignedIn, setSignedIn] = useSharedState('isSignedIn', false)
 | 
			
		||||
    const [isLoading, setLoading] = useSharedState('isLoading', true)
 | 
			
		||||
    const signout = useCallback(() => setSignedIn(false), [setSignedIn])
 | 
			
		||||
const useSignedIn = () => {
 | 
			
		||||
	const [signedIn, setSignedIn] = useSharedState(
 | 
			
		||||
		"signedIn",
 | 
			
		||||
		typeof window === "undefined" ? false : !!Cookies.get("drift-token")
 | 
			
		||||
	)
 | 
			
		||||
	const token = Cookies.get("drift-token")
 | 
			
		||||
	const router = useRouter()
 | 
			
		||||
	const signin = (token: string) => {
 | 
			
		||||
		setSignedIn(true)
 | 
			
		||||
		Cookies.set("drift-token", token)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
    if (redirectIfNotAuthed && !isLoading && isSignedIn === false) {
 | 
			
		||||
        router.push('/signin')
 | 
			
		||||
	const signout = () => {
 | 
			
		||||
		setSignedIn(false)
 | 
			
		||||
		Cookies.remove("drift-token")
 | 
			
		||||
		router.push("/")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
        async function checkToken() {
 | 
			
		||||
            const token = Cookies.get('drift-token')
 | 
			
		||||
		if (token) {
 | 
			
		||||
                const response = await fetch('/server-api/auth/verify-token', {
 | 
			
		||||
                    method: 'GET',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Authorization': `Bearer ${token}`
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
			setSignedIn(true)
 | 
			
		||||
		} else {
 | 
			
		||||
			setSignedIn(false)
 | 
			
		||||
		}
 | 
			
		||||
            }
 | 
			
		||||
            setLoading(false)
 | 
			
		||||
        }
 | 
			
		||||
        setLoading(true)
 | 
			
		||||
        checkToken()
 | 
			
		||||
 | 
			
		||||
        const interval = setInterval(() => {
 | 
			
		||||
            checkToken()
 | 
			
		||||
        }, 10000);
 | 
			
		||||
 | 
			
		||||
        return () => clearInterval(interval);
 | 
			
		||||
    }, [setLoading, setSignedIn])
 | 
			
		||||
	}, [setSignedIn, token])
 | 
			
		||||
 | 
			
		||||
    return { isSignedIn, isLoading, signout }
 | 
			
		||||
	return { signedIn, signin, token, signout }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useSignedIn
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
@ -0,0 +1,152 @@
 | 
			
		||||
import { marked } from 'marked'
 | 
			
		||||
import Highlight, { defaultProps, Language, } from 'prism-react-renderer'
 | 
			
		||||
import { renderToStaticMarkup } from 'react-dom/server'
 | 
			
		||||
 | 
			
		||||
// // image sizes. DDoS Safe?
 | 
			
		||||
// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
 | 
			
		||||
// //@ts-ignore
 | 
			
		||||
// Lexer.rules.inline.normal.link = imageSizeLink;
 | 
			
		||||
// //@ts-ignore
 | 
			
		||||
// Lexer.rules.inline.gfm.link = imageSizeLink;
 | 
			
		||||
// //@ts-ignore
 | 
			
		||||
// Lexer.rules.inline.breaks.link = imageSizeLink;
 | 
			
		||||
 | 
			
		||||
//@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
 | 
			
		||||
        <Component>
 | 
			
		||||
            <a href={`#${id}`} id={id} style={{ color: "inherit" }} dangerouslySetInnerHTML={{ __html: (text) }} >
 | 
			
		||||
            </a>
 | 
			
		||||
        </Component>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// renderer.link = (href, _, text) => {
 | 
			
		||||
//     const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
 | 
			
		||||
//     if (isHrefLocal) {
 | 
			
		||||
//         return renderToStaticMarkup(
 | 
			
		||||
//             <a href={href || ''}>
 | 
			
		||||
//                 {text}
 | 
			
		||||
//             </a>
 | 
			
		||||
//         )
 | 
			
		||||
//     }
 | 
			
		||||
 | 
			
		||||
//     // dirty hack
 | 
			
		||||
//     // if text contains elements, render as html
 | 
			
		||||
//     return <a href={href || ""} target="_blank" rel="noopener noreferrer" dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }} ></a>
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderer.image = function (href, _, text) {
 | 
			
		||||
    return `<Image loading="lazy" src="${href}" alt="${text}" layout="fill" />`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
renderer.checkbox = () => ''
 | 
			
		||||
renderer.listitem = (text, task, checked) => {
 | 
			
		||||
    if (task) {
 | 
			
		||||
        return `<li class="reset"><span class="check">​<input type="checkbox" disabled ${checked ? 'checked' : ''
 | 
			
		||||
            } /></span><span>${text}</span></li>`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return `<li>${text}</li>`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
renderer.code = (code: string, language: string) => {
 | 
			
		||||
    return renderToStaticMarkup(
 | 
			
		||||
        <pre>
 | 
			
		||||
            {/* {title && <code>{title} </code>} */}
 | 
			
		||||
            {/* {language && title && <code style={{}}> {language} </code>} */}
 | 
			
		||||
            <Code
 | 
			
		||||
                language={language}
 | 
			
		||||
                // title={title}
 | 
			
		||||
                code={code}
 | 
			
		||||
            // highlight={highlight}
 | 
			
		||||
            />
 | 
			
		||||
        </pre>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
            <>
 | 
			
		||||
                <code {...props} dangerouslySetInnerHTML={{ __html: code }} />
 | 
			
		||||
            </>
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    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 (
 | 
			
		||||
        <>
 | 
			
		||||
            <Highlight {...defaultProps} code={code.trim()} language={language as Language} >
 | 
			
		||||
                {({ className, style, tokens, getLineProps, getTokenProps }) => (
 | 
			
		||||
                    <code className={className} style={{ ...style }}>
 | 
			
		||||
                        {
 | 
			
		||||
                            tokens.map((line, i) => (
 | 
			
		||||
                                <div
 | 
			
		||||
                                    key={i}
 | 
			
		||||
                                    {...getLineProps({ line, key: i })}
 | 
			
		||||
                                    style={
 | 
			
		||||
                                        //@ts-ignore
 | 
			
		||||
                                        highlightedLines.includes((i + 1).toString())
 | 
			
		||||
                                            ? {
 | 
			
		||||
                                                background: 'var(--highlight)',
 | 
			
		||||
                                                margin: '0 -1rem',
 | 
			
		||||
                                                padding: '0 1rem',
 | 
			
		||||
                                            }
 | 
			
		||||
                                            : undefined
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    {
 | 
			
		||||
                                        line.map((token, key) => (
 | 
			
		||||
                                            <span key={key} {...getTokenProps({ token, key })} />
 | 
			
		||||
                                        ))
 | 
			
		||||
                                    }
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ))}
 | 
			
		||||
                    </code>
 | 
			
		||||
                )}
 | 
			
		||||
            </Highlight>
 | 
			
		||||
        </>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
export type PostVisibility = "unlisted" | "private" | "public" | "protected"
 | 
			
		||||
 | 
			
		||||
export type Document = {
 | 
			
		||||
	title: string
 | 
			
		||||
	content: string
 | 
			
		||||
	id: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type File = {
 | 
			
		||||
	id: string
 | 
			
		||||
	title: string
 | 
			
		||||
	content: string
 | 
			
		||||
	html: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Files = File[]
 | 
			
		||||
 | 
			
		||||
export type Post = {
 | 
			
		||||
	id: string
 | 
			
		||||
	title: string
 | 
			
		||||
	description: string
 | 
			
		||||
	visibility: PostVisibility
 | 
			
		||||
	files: Files
 | 
			
		||||
}
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
const dotenv = require("dotenv");
 | 
			
		||||
dotenv.config();
 | 
			
		||||
 | 
			
		||||
/** @type {import('next').NextConfig} */
 | 
			
		||||
const nextConfig = {
 | 
			
		||||
  reactStrictMode: true,
 | 
			
		||||
  experimental: {
 | 
			
		||||
    outputStandalone: true,
 | 
			
		||||
  },
 | 
			
		||||
  async rewrites() {
 | 
			
		||||
    return [
 | 
			
		||||
      {
 | 
			
		||||
        source: "/server-api/:path*",
 | 
			
		||||
        destination: `${process.env.API_URL}/:path*`,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: "/file/raw/:id",
 | 
			
		||||
        destination: `/api/raw/:id`,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = nextConfig;
 | 
			
		||||
@ -0,0 +1,39 @@
 | 
			
		||||
import dotenv from "dotenv"
 | 
			
		||||
import bundleAnalyzer from "@next/bundle-analyzer"
 | 
			
		||||
 | 
			
		||||
dotenv.config()
 | 
			
		||||
 | 
			
		||||
/** @type {import('next').NextConfig} */
 | 
			
		||||
const nextConfig = {
 | 
			
		||||
  reactStrictMode: true,
 | 
			
		||||
  experimental: {
 | 
			
		||||
    outputStandalone: true,
 | 
			
		||||
    esmExternals: true
 | 
			
		||||
  },
 | 
			
		||||
  webpack: (config, { dev, isServer }) => {
 | 
			
		||||
    if (!dev && !isServer) {
 | 
			
		||||
      Object.assign(config.resolve.alias, {
 | 
			
		||||
        react: "preact/compat",
 | 
			
		||||
        "react-dom/test-utils": "preact/test-utils",
 | 
			
		||||
        "react-dom": "preact/compat"
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return config
 | 
			
		||||
  },
 | 
			
		||||
  async rewrites() {
 | 
			
		||||
    return [
 | 
			
		||||
      {
 | 
			
		||||
        source: "/server-api/:path*",
 | 
			
		||||
        destination: `${process.env.API_URL}/:path*`
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: "/file/raw/:id",
 | 
			
		||||
        destination: `/api/raw/:id`
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
 | 
			
		||||
  nextConfig
 | 
			
		||||
)
 | 
			
		||||
@ -0,0 +1,46 @@
 | 
			
		||||
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
 | 
			
		||||
 | 
			
		||||
const PUBLIC_FILE = /.(.*)$/
 | 
			
		||||
 | 
			
		||||
export function middleware(req: NextRequest) {
 | 
			
		||||
    const pathname = req.nextUrl.pathname
 | 
			
		||||
    const signedIn = req.cookies['drift-token']
 | 
			
		||||
    const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href
 | 
			
		||||
    // const isPageRequest =
 | 
			
		||||
    //     !PUBLIC_FILE.test(req.nextUrl.pathname) &&
 | 
			
		||||
    //     !req.nextUrl.pathname.startsWith('/api') &&
 | 
			
		||||
    //     // header added when next/link pre-fetches a route
 | 
			
		||||
    //     !req.headers.get('x-middleware-preflight')
 | 
			
		||||
 | 
			
		||||
    if (pathname === '/signout') {
 | 
			
		||||
        // If you're signed in we remove the cookie and redirect to the home page
 | 
			
		||||
        // If you're not signed in we redirect to the home page
 | 
			
		||||
        if (signedIn) {
 | 
			
		||||
            const resp = NextResponse.redirect(getURL(''));
 | 
			
		||||
            resp.clearCookie('drift-token');
 | 
			
		||||
            resp.clearCookie('drift-userid');
 | 
			
		||||
 | 
			
		||||
            return resp
 | 
			
		||||
        }
 | 
			
		||||
    } else if (pathname === '/') {
 | 
			
		||||
        if (signedIn) {
 | 
			
		||||
            return NextResponse.rewrite(getURL('new'))
 | 
			
		||||
        }
 | 
			
		||||
        // If you're not signed in we redirect the new post page to the home page
 | 
			
		||||
    } else if (pathname === '/new') {
 | 
			
		||||
        if (!signedIn) {
 | 
			
		||||
            return NextResponse.redirect(getURL(''))
 | 
			
		||||
        }
 | 
			
		||||
        // If you're signed in we redirect the sign in page to the home page (which is the new page)
 | 
			
		||||
    } else if (pathname === '/signin' || pathname === '/signup') {
 | 
			
		||||
        if (signedIn) {
 | 
			
		||||
            return NextResponse.redirect(getURL(''))
 | 
			
		||||
        }
 | 
			
		||||
    } else if (pathname === '/new') {
 | 
			
		||||
        if (!signedIn) {
 | 
			
		||||
            return NextResponse.redirect(getURL('/signin'))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return NextResponse.next()
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,58 @@
 | 
			
		||||
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"]}`
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	if (file.status
 | 
			
		||||
		!== 200) {
 | 
			
		||||
		return res.status(404).json({ error: "File not found" })
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const json = await file.json()
 | 
			
		||||
	const { content, title } = json
 | 
			
		||||
	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.setHeader("Content-Type", "text/plain")
 | 
			
		||||
	res.setHeader("Cache-Control", "public, max-age=4800")
 | 
			
		||||
	res.status(200).write(markdown(contentToRender))
 | 
			
		||||
	res.end()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default renderMarkdown
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
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 = content || ""
 | 
			
		||||
	if (!renderAsMarkdown.includes(type)) {
 | 
			
		||||
		contentToRender = `~~~${type}
 | 
			
		||||
${content}
 | 
			
		||||
~~~`
 | 
			
		||||
	} else {
 | 
			
		||||
		contentToRender = "\n" + 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,20 +1,60 @@
 | 
			
		||||
import styles from '@styles/Home.module.css'
 | 
			
		||||
import { Page } from '@geist-ui/core'
 | 
			
		||||
 | 
			
		||||
import Header from '@components/header'
 | 
			
		||||
import MyPosts from '@components/my-posts'
 | 
			
		||||
import cookie from "cookie";
 | 
			
		||||
import type { GetServerSideProps } from 'next';
 | 
			
		||||
import { Post } from '@lib/types';
 | 
			
		||||
import { Page } from '@geist-ui/core';
 | 
			
		||||
 | 
			
		||||
const Home = ({ theme, changeTheme }: { theme: "light" | "dark", changeTheme: () => void }) => {
 | 
			
		||||
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}>
 | 
			
		||||
        <MyPosts />
 | 
			
		||||
      <Page.Content className={styles.main}>
 | 
			
		||||
        <MyPosts error={error} posts={posts} />
 | 
			
		||||
      </Page.Content>
 | 
			
		||||
    </Page >
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
// get server side props 
 | 
			
		||||
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
 | 
			
		||||
  const driftToken = cookie.parse(req.headers.cookie || '')[`drift-token`]
 | 
			
		||||
  if (!driftToken) {
 | 
			
		||||
    return {
 | 
			
		||||
      redirect: {
 | 
			
		||||
        destination: '/',
 | 
			
		||||
        permanent: false,
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const posts = await fetch(process.env.API_URL + `/posts/mine`, {
 | 
			
		||||
    method: "GET",
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
      "Authorization": `Bearer ${driftToken}`,
 | 
			
		||||
      "x-secret-key": process.env.SECRET_KEY || ''
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (!posts.ok || posts.status !== 200) {
 | 
			
		||||
    return {
 | 
			
		||||
      redirect: {
 | 
			
		||||
        destination: '/',
 | 
			
		||||
        permanent: false,
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    props: {
 | 
			
		||||
      posts: await posts.json(),
 | 
			
		||||
      error: posts.status !== 200,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Home
 | 
			
		||||
 | 
			
		||||
@ -1,31 +1,23 @@
 | 
			
		||||
import styles from '@styles/Home.module.css'
 | 
			
		||||
import NewPost from '@components/new-post'
 | 
			
		||||
import { Page } from '@geist-ui/core'
 | 
			
		||||
import useSignedIn from '@lib/hooks/use-signed-in'
 | 
			
		||||
import Header from '@components/header'
 | 
			
		||||
import { ThemeProps } from './_app'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import PageSeo from '@components/page-seo'
 | 
			
		||||
import { Page } from '@geist-ui/core'
 | 
			
		||||
 | 
			
		||||
const Home = ({ theme, changeTheme }: ThemeProps) => {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { isSignedIn, isLoading } = useSignedIn({ redirectIfNotAuthed: true })
 | 
			
		||||
  if (!isSignedIn && !isLoading) {
 | 
			
		||||
    router.push("/signin")
 | 
			
		||||
  }
 | 
			
		||||
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}>
 | 
			
		||||
        {isSignedIn && <NewPost />}
 | 
			
		||||
      <Page.Content className={styles.main}>
 | 
			
		||||
        <NewPost />
 | 
			
		||||
      </Page.Content>
 | 
			
		||||
    </Page >
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Home
 | 
			
		||||
export default New
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,58 @@
 | 
			
		||||
import cookie from "cookie";
 | 
			
		||||
import type { GetServerSideProps } from "next";
 | 
			
		||||
import { Post } from "@lib/types";
 | 
			
		||||
import PostPage from "@components/post-page";
 | 
			
		||||
 | 
			
		||||
export type PostProps = {
 | 
			
		||||
    post: Post
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Post = ({ post, }: PostProps) => {
 | 
			
		||||
    return (<PostPage post={post} />)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps: GetServerSideProps = async (context) => {
 | 
			
		||||
    const headers = context.req.headers
 | 
			
		||||
    const host = headers.host
 | 
			
		||||
    const driftToken = cookie.parse(headers.cookie || '')[`drift-token`]
 | 
			
		||||
 | 
			
		||||
    if (context.query.id) {
 | 
			
		||||
        const post = await fetch('http://' + host + `/server-api/posts/${context.query.id}`, {
 | 
			
		||||
            method: "GET",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Content-Type": "application/json",
 | 
			
		||||
                "Authorization": `Bearer ${driftToken}`,
 | 
			
		||||
                "x-secret-key": process.env.SECRET_KEY || "",
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (!post.ok || post.status !== 200) {
 | 
			
		||||
            return {
 | 
			
		||||
                redirect: {
 | 
			
		||||
                    destination: '/',
 | 
			
		||||
                    permanent: false,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            const json = await post.json();
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                props: {
 | 
			
		||||
                    post: json
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.log(e)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        props: {
 | 
			
		||||
            post: null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Post
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,80 @@
 | 
			
		||||
import { Page, useToasts } from '@geist-ui/core';
 | 
			
		||||
 | 
			
		||||
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 = () => {
 | 
			
		||||
    const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true);
 | 
			
		||||
    const [post, setPost] = useState<Post>()
 | 
			
		||||
    const router = useRouter()
 | 
			
		||||
    const { setToast } = useToasts()
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (router.isReady) {
 | 
			
		||||
            const fetchPostWithAuth = async () => {
 | 
			
		||||
                const resp = await fetch(`/server-api/posts/${router.query.id}`, {
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        Authorization: `Bearer ${Cookies.get('drift-token')}`
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                if (!resp.ok) return
 | 
			
		||||
                const post = await resp.json()
 | 
			
		||||
 | 
			
		||||
                if (!post) return
 | 
			
		||||
                setPost(post)
 | 
			
		||||
            }
 | 
			
		||||
            fetchPostWithAuth()
 | 
			
		||||
        }
 | 
			
		||||
    }, [router.isReady, router.query.id])
 | 
			
		||||
 | 
			
		||||
    const onSubmit = async (password: string) => {
 | 
			
		||||
        const res = await fetch(`/server-api/posts/${router.query.id}?password=${password}`, {
 | 
			
		||||
            method: "GET",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Content-Type": "application/json",
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (!res.ok) {
 | 
			
		||||
            setToast({
 | 
			
		||||
                type: "error",
 | 
			
		||||
                text: "Wrong password"
 | 
			
		||||
            })
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const data = await res.json()
 | 
			
		||||
        if (data) {
 | 
			
		||||
            if (data.error) {
 | 
			
		||||
                setToast({
 | 
			
		||||
                    text: data.error,
 | 
			
		||||
                    type: "error"
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
                setPost(data)
 | 
			
		||||
                setIsPasswordModalOpen(false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onClose = () => {
 | 
			
		||||
        setIsPasswordModalOpen(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!router.isReady) {
 | 
			
		||||
        return <></>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!post) {
 | 
			
		||||
        return <Page><PasswordModal creating={false} onClose={onClose} onSubmit={onSubmit} isOpen={isPasswordModalOpen} /></Page>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (<PostPage post={post} />)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Post
 | 
			
		||||
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
.header {
 | 
			
		||||
   display: flex;
 | 
			
		||||
   justify-content: space-between;
 | 
			
		||||
   align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 650px) {
 | 
			
		||||
    .header {
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
{
 | 
			
		||||
  "plugins": [
 | 
			
		||||
    "postcss-flexbugs-fixes",
 | 
			
		||||
    "postcss-hover-media-feature",
 | 
			
		||||
    [
 | 
			
		||||
      "postcss-preset-env",
 | 
			
		||||
      {
 | 
			
		||||
        "autoprefixer": {
 | 
			
		||||
          "flexbox": "no-2009"
 | 
			
		||||
        },
 | 
			
		||||
        "stage": 3,
 | 
			
		||||
        "features": {
 | 
			
		||||
          "custom-properties": false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@ -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%;
 | 
			
		||||
  }
 | 
			
		||||
  max-width: var(--main-content) !important;
 | 
			
		||||
  margin: 0 auto !important;
 | 
			
		||||
  padding: 0 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,32 +1,154 @@
 | 
			
		||||
@import "./syntax.css";
 | 
			
		||||
@import "./markdown.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;
 | 
			
		||||
 | 
			
		||||
	--page-nav-height: 64px;
 | 
			
		||||
	--token: #999;
 | 
			
		||||
	--comment: #999;
 | 
			
		||||
	--keyword: #fff;
 | 
			
		||||
	--name: #fff;
 | 
			
		||||
	--highlight: #2e2e2e;
 | 
			
		||||
 | 
			
		||||
	/* Dark Mode Colors */
 | 
			
		||||
	--bg: #000;
 | 
			
		||||
	--fg: #fafbfc;
 | 
			
		||||
	--gray: #666;
 | 
			
		||||
	--light-gray: #444;
 | 
			
		||||
	--lighter-gray: #222;
 | 
			
		||||
	--lightest-gray: #1a1a1a;
 | 
			
		||||
	--darker-gray: #b4b4b4;
 | 
			
		||||
	--darkest-gray: #efefef;
 | 
			
		||||
	--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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 768px) {
 | 
			
		||||
  :root {
 | 
			
		||||
    --main-content-width: 100%;
 | 
			
		||||
[data-theme="light"] {
 | 
			
		||||
	--token: #666;
 | 
			
		||||
	--comment: #999;
 | 
			
		||||
	--keyword: #000;
 | 
			
		||||
	--name: #333;
 | 
			
		||||
	--highlight: #eaeaea;
 | 
			
		||||
 | 
			
		||||
	--bg: #fff;
 | 
			
		||||
	--fg: #000;
 | 
			
		||||
	--gray: #888;
 | 
			
		||||
 | 
			
		||||
	--light-gray: #dedede;
 | 
			
		||||
	--lighter-gray: #f5f5f5;
 | 
			
		||||
	--lightest-gray: #fafafa;
 | 
			
		||||
	--darker-gray: #555;
 | 
			
		||||
	--darkest-gray: #222;
 | 
			
		||||
	--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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
* {
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::selection {
 | 
			
		||||
	text-shadow: none;
 | 
			
		||||
	background: var(--selection);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
	font-size: 15px;
 | 
			
		||||
	text-rendering: optimizeLegibility;
 | 
			
		||||
	-webkit-font-smoothing: antialiased;
 | 
			
		||||
	-moz-osx-font-smoothing: grayscale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
body {
 | 
			
		||||
	min-height: 100vh;
 | 
			
		||||
	font-family: var(--font-sans);
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
	overflow-wrap: break-word;
 | 
			
		||||
	hyphens: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input,
 | 
			
		||||
button,
 | 
			
		||||
textarea,
 | 
			
		||||
select {
 | 
			
		||||
	font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
blockquote {
 | 
			
		||||
	font-style: italic;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding-left: 1rem;
 | 
			
		||||
	border-left: 3px solid var(--light-gray);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.reset {
 | 
			
		||||
	outline: none;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pre,
 | 
			
		||||
code {
 | 
			
		||||
	font-family: var(--font-mono) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	* {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
		text-shadow: none !important;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#root,
 | 
			
		||||
#__next {
 | 
			
		||||
	isolation: isolate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,130 @@
 | 
			
		||||
article {
 | 
			
		||||
	max-width: var(--main-content);
 | 
			
		||||
	margin: 0 auto;
 | 
			
		||||
	line-height: 1.9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
article > * + * {
 | 
			
		||||
	margin-top: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
article img {
 | 
			
		||||
	max-width: 100%;
 | 
			
		||||
	margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
article [id]::before {
 | 
			
		||||
	content: "";
 | 
			
		||||
	display: block;
 | 
			
		||||
	height: var(--gap-half);
 | 
			
		||||
	margin-top: calc(var(--gap-half) * -1);
 | 
			
		||||
	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;
 | 
			
		||||
	border-color: var(--fg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Code Snippets */
 | 
			
		||||
 | 
			
		||||
.token-line:not(:last-child) {
 | 
			
		||||
	min-height: 1.4rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
article *:not(pre) > code {
 | 
			
		||||
	font-weight: 500;
 | 
			
		||||
	font-family: var(--font-sans);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
article li > p {
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
article code > * {
 | 
			
		||||
	font-family: var(--font-mono);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
article pre {
 | 
			
		||||
	overflow-x: auto;
 | 
			
		||||
	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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header-link:hover::after {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
}
 | 
			
		||||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								@ -1,3 +1,4 @@
 | 
			
		||||
.env
 | 
			
		||||
node_modules/
 | 
			
		||||
dist/
 | 
			
		||||
drift.sqlite
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
{
 | 
			
		||||
  "production": {
 | 
			
		||||
    "database": "../drift.sqlite",
 | 
			
		||||
    "host": "127.0.0.1",
 | 
			
		||||
    "port": "3306",
 | 
			
		||||
    "user": "root",
 | 
			
		||||
    "password": "root",
 | 
			
		||||
    "dialect": "sqlite"
 | 
			
		||||
  },
 | 
			
		||||
  "development": {
 | 
			
		||||
    "database": "../drift.sqlite",
 | 
			
		||||
    "host": "127.0.0.1",
 | 
			
		||||
    "port": "3306",
 | 
			
		||||
    "user": "root",
 | 
			
		||||
    "password": "root",
 | 
			
		||||
    "dialect": "sqlite"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
import {Sequelize} from 'sequelize-typescript';
 | 
			
		||||
 | 
			
		||||
export const sequelize = new Sequelize({
 | 
			
		||||
  dialect: 'sqlite',
 | 
			
		||||
  database: 'movies',
 | 
			
		||||
  storage: ':memory:',
 | 
			
		||||
  models: [__dirname + '/models']
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
const { DataTypes } = require("sequelize");
 | 
			
		||||
 | 
			
		||||
async function up(qi) {
 | 
			
		||||
  try {
 | 
			
		||||
    await qi.addColumn("Posts", "html", {
 | 
			
		||||
      allowNull: true,
 | 
			
		||||
      type: DataTypes.STRING,
 | 
			
		||||
    });
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
    throw e;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function down(qi) {
 | 
			
		||||
  try {
 | 
			
		||||
    await qi.removeColumn("Posts", "html");
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
    throw e;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  up,
 | 
			
		||||
  down,
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
import { NextFunction, Request, Response } from 'express';
 | 
			
		||||
 | 
			
		||||
const key = process.env.SECRET_KEY;
 | 
			
		||||
if (!key) {
 | 
			
		||||
    throw new Error('SECRET_KEY is not set.');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function authenticateToken(req: Request, res: Response, next: NextFunction) {
 | 
			
		||||
    const requestKey = req.headers['x-secret-key']
 | 
			
		||||
    if (requestKey !== key) {
 | 
			
		||||
        return res.sendStatus(401)
 | 
			
		||||
    }
 | 
			
		||||
    next()
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,152 @@
 | 
			
		||||
import { marked } from 'marked'
 | 
			
		||||
import Highlight, { defaultProps, Language, } from 'prism-react-renderer'
 | 
			
		||||
import { renderToStaticMarkup } from 'react-dom/server'
 | 
			
		||||
 | 
			
		||||
// // image sizes. DDoS Safe?
 | 
			
		||||
// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
 | 
			
		||||
// //@ts-ignore
 | 
			
		||||
// Lexer.rules.inline.normal.link = imageSizeLink;
 | 
			
		||||
// //@ts-ignore
 | 
			
		||||
// Lexer.rules.inline.gfm.link = imageSizeLink;
 | 
			
		||||
// //@ts-ignore
 | 
			
		||||
// Lexer.rules.inline.breaks.link = imageSizeLink;
 | 
			
		||||
 | 
			
		||||
//@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
 | 
			
		||||
        <Component>
 | 
			
		||||
            <a href={`#${id}`} id={id} style={{ color: "inherit" }} dangerouslySetInnerHTML={{ __html: (text) }} >
 | 
			
		||||
            </a>
 | 
			
		||||
        </Component>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// renderer.link = (href, _, text) => {
 | 
			
		||||
//     const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
 | 
			
		||||
//     if (isHrefLocal) {
 | 
			
		||||
//         return renderToStaticMarkup(
 | 
			
		||||
//             <a href={href || ''}>
 | 
			
		||||
//                 {text}
 | 
			
		||||
//             </a>
 | 
			
		||||
//         )
 | 
			
		||||
//     }
 | 
			
		||||
 | 
			
		||||
//     // dirty hack
 | 
			
		||||
//     // if text contains elements, render as html
 | 
			
		||||
//     return <a href={href || ""} target="_blank" rel="noopener noreferrer" dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }} ></a>
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderer.image = function (href, _, text) {
 | 
			
		||||
    return `<Image loading="lazy" src="${href}" alt="${text}" layout="fill" />`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
renderer.checkbox = () => ''
 | 
			
		||||
renderer.listitem = (text, task, checked) => {
 | 
			
		||||
    if (task) {
 | 
			
		||||
        return `<li class="reset"><span class="check">​<input type="checkbox" disabled ${checked ? 'checked' : ''
 | 
			
		||||
            } /></span><span>${text}</span></li>`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return `<li>${text}</li>`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
renderer.code = (code: string, language: string) => {
 | 
			
		||||
    return renderToStaticMarkup(
 | 
			
		||||
        <pre>
 | 
			
		||||
            {/* {title && <code>{title} </code>} */}
 | 
			
		||||
            {/* {language && title && <code style={{}}> {language} </code>} */}
 | 
			
		||||
            <Code
 | 
			
		||||
                language={language}
 | 
			
		||||
                // title={title}
 | 
			
		||||
                code={code}
 | 
			
		||||
            // highlight={highlight}
 | 
			
		||||
            />
 | 
			
		||||
        </pre>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
            <>
 | 
			
		||||
                <code {...props} dangerouslySetInnerHTML={{ __html: code }} />
 | 
			
		||||
            </>
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    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 (
 | 
			
		||||
        <>
 | 
			
		||||
            <Highlight {...defaultProps} code={code.trim()} language={language as Language} >
 | 
			
		||||
                {({ className, style, tokens, getLineProps, getTokenProps }) => (
 | 
			
		||||
                    <code className={className} style={{ ...style }}>
 | 
			
		||||
                        {
 | 
			
		||||
                            tokens.map((line, i) => (
 | 
			
		||||
                                <div
 | 
			
		||||
                                    key={i}
 | 
			
		||||
                                    {...getLineProps({ line, key: i })}
 | 
			
		||||
                                    style={
 | 
			
		||||
                                        //@ts-ignore
 | 
			
		||||
                                        highlightedLines.includes((i + 1).toString())
 | 
			
		||||
                                            ? {
 | 
			
		||||
                                                background: 'var(--highlight)',
 | 
			
		||||
                                                margin: '0 -1rem',
 | 
			
		||||
                                                padding: '0 1rem',
 | 
			
		||||
                                            }
 | 
			
		||||
                                            : undefined
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    {
 | 
			
		||||
                                        line.map((token, key) => (
 | 
			
		||||
                                            <span key={key} {...getTokenProps({ token, key })} />
 | 
			
		||||
                                        ))
 | 
			
		||||
                                    }
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ))}
 | 
			
		||||
                    </code>
 | 
			
		||||
                )}
 | 
			
		||||
            </Highlight>
 | 
			
		||||
        </>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,9 @@
 | 
			
		||||
import { Sequelize } from 'sequelize-typescript';
 | 
			
		||||
 | 
			
		||||
export const sequelize = new Sequelize({
 | 
			
		||||
  dialect: 'sqlite',
 | 
			
		||||
  database: 'drift',
 | 
			
		||||
  storage: process.env.MEMORY_DB === "true" ? ":memory:" : __dirname + '/../../drift.sqlite',
 | 
			
		||||
  models: [__dirname + '/models'],
 | 
			
		||||
  host: 'localhost',
 | 
			
		||||
});
 | 
			
		||||
@ -1,36 +1,72 @@
 | 
			
		||||
import { celebrate, Joi } from "celebrate";
 | 
			
		||||
import { Router } from "express";
 | 
			
		||||
// import { Movie } from '../models/Post'
 | 
			
		||||
import { File } from "../../lib/models/File";
 | 
			
		||||
import { File } from "@lib/models/File";
 | 
			
		||||
import secretKey from "@lib/middleware/secret-key";
 | 
			
		||||
 | 
			
		||||
export const files = Router();
 | 
			
		||||
 | 
			
		||||
files.get(
 | 
			
		||||
  "/raw/:id",
 | 
			
		||||
files.get("/raw/:id",
 | 
			
		||||
  celebrate({
 | 
			
		||||
    params: {
 | 
			
		||||
      id: Joi.string().required(),
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
  secretKey,
 | 
			
		||||
  async (req, res, next) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const file = await File.findOne({
 | 
			
		||||
        where: {
 | 
			
		||||
          id: req.params.id,
 | 
			
		||||
          id: req.params.id
 | 
			
		||||
        },
 | 
			
		||||
        attributes: ["title", "content"],
 | 
			
		||||
      });
 | 
			
		||||
      // TODO: fix post inclusion
 | 
			
		||||
      // if (file?.post.visibility === 'public' || file?.post.visibility === 'unlisted') {
 | 
			
		||||
      res.setHeader("Cache-Control", "public, max-age=86400");
 | 
			
		||||
      res.json(file);
 | 
			
		||||
      // } else {
 | 
			
		||||
      // TODO: should this be `private, `?
 | 
			
		||||
      // res.setHeader("Cache-Control", "max-age=86400");
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!file) {
 | 
			
		||||
        return res.status(404).json({ error: "File not found" })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // TODO: JWT-checkraw files
 | 
			
		||||
      if (file?.post?.visibility === "private") {
 | 
			
		||||
        // jwt(req as UserJwtRequest, res, () => {
 | 
			
		||||
        //     res.json(file);
 | 
			
		||||
      // }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        // })
 | 
			
		||||
        res.json(file);
 | 
			
		||||
      } else {
 | 
			
		||||
        res.json(file);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    catch (e) {
 | 
			
		||||
      next(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
files.get("/html/:id",
 | 
			
		||||
  celebrate({
 | 
			
		||||
    params: {
 | 
			
		||||
      id: Joi.string().required(),
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
  async (req, res, next) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const file = await File.findOne({
 | 
			
		||||
        where: {
 | 
			
		||||
          id: req.params.id
 | 
			
		||||
        },
 | 
			
		||||
        attributes: ["html"],
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!file) {
 | 
			
		||||
        return res.status(404).json({ error: "File not found" })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      res.setHeader('Content-Type', 'text/plain')
 | 
			
		||||
      res.setHeader('Cache-Control', 'public, max-age=4800')
 | 
			
		||||
      res.status(200).write(file.html)
 | 
			
		||||
      res.end()
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      next(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -1,47 +1,14 @@
 | 
			
		||||
import { Router } from "express";
 | 
			
		||||
import { User } from "../../lib/models/User";
 | 
			
		||||
import { File } from "../../lib/models/File";
 | 
			
		||||
import jwt, { UserJwtRequest } from "../../lib/middleware/jwt";
 | 
			
		||||
import { Post } from "../../lib/models/Post";
 | 
			
		||||
// import jwt from "@lib/middleware/jwt";
 | 
			
		||||
// import { User } from "@lib/models/User";
 | 
			
		||||
 | 
			
		||||
export const users = Router();
 | 
			
		||||
 | 
			
		||||
users.get("/", jwt, async (req, res, next) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const allUsers = await User.findAll();
 | 
			
		||||
    res.json(allUsers);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    next(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
users.get("/mine", jwt, async (req: UserJwtRequest, res, next) => {
 | 
			
		||||
  if (!req.user) {
 | 
			
		||||
    return res.status(401).json({ error: "Unauthorized" });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const user = await User.findByPk(req.user.id, {
 | 
			
		||||
      include: [
 | 
			
		||||
        {
 | 
			
		||||
          model: Post,
 | 
			
		||||
          as: "posts",
 | 
			
		||||
          include: [
 | 
			
		||||
            {
 | 
			
		||||
              model: File,
 | 
			
		||||
              as: "files",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return res.status(404).json({ error: "User not found" });
 | 
			
		||||
    }
 | 
			
		||||
    return res.json(
 | 
			
		||||
      user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
 | 
			
		||||
    );
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    next(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
// users.get("/", jwt, async (req, res, next) => {
 | 
			
		||||
//   try {
 | 
			
		||||
//     const allUsers = await User.findAll();
 | 
			
		||||
//     res.json(allUsers);
 | 
			
		||||
//   } catch (error) {
 | 
			
		||||
//     next(error);
 | 
			
		||||
//   }
 | 
			
		||||
// });
 | 
			
		||||
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue