mirror of https://github.com/MaxLeiter/Drift
				
				
				
			Merge with main
						commit
						da8e7415dc
					
				@ -1,3 +1,4 @@
 | 
				
			|||||||
API_URL=http://localhost:3000
 | 
					API_URL=http://localhost:3000
 | 
				
			||||||
WELCOME_TITLE="Welcome to Drift"
 | 
					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."
 | 
					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 dynamic from 'next/dynamic'
 | 
				
			||||||
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'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Tab = {
 | 
					const Header = dynamic(import('./header'), {
 | 
				
			||||||
    name: string
 | 
					    ssr: false,
 | 
				
			||||||
    icon: JSX.Element
 | 
					    // loading: () => <MenuSkeleton />,
 | 
				
			||||||
    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 >
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Header
 | 
					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 PostList from "../post-list"
 | 
				
			||||||
import Cookies from "js-cookie"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fetcher = (url: string) => fetch(url, {
 | 
					const MyPosts = ({ posts, error }: { posts: any, error: any }) => {
 | 
				
			||||||
    headers: {
 | 
					    return <PostList posts={posts} error={error} />
 | 
				
			||||||
        '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} />
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default MyPosts
 | 
					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 { memo, useEffect, useState } from "react"
 | 
				
			||||||
import ReactMarkdownPreview from "./react-markdown-preview"
 | 
					import styles from './preview.module.css'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Props = {
 | 
					type Props = {
 | 
				
			||||||
    content?: string
 | 
					 | 
				
			||||||
    height?: number | string
 | 
					    height?: number | string
 | 
				
			||||||
 | 
					    fileId?: string
 | 
				
			||||||
 | 
					    content?: string
 | 
				
			||||||
 | 
					    title?: string
 | 
				
			||||||
    //  file extensions we can highlight 
 | 
					    //  file extensions we can highlight 
 | 
				
			||||||
    type?: string
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MarkdownPreview = ({ content = '', height = 500, type = 'markdown' }: Props) => {
 | 
					const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
 | 
				
			||||||
    const [contentToRender, setContent] = useState(content)
 | 
					    const [preview, setPreview] = useState<string>(content || "")
 | 
				
			||||||
 | 
					    const [isLoading, setIsLoading] = useState<boolean>(true)
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        // 'm' so it doesn't flash code when you change the type to md
 | 
					        async function fetchPost() {
 | 
				
			||||||
        const renderAsMarkdown = ['m', 'markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
 | 
					            if (fileId) {
 | 
				
			||||||
        if (!renderAsMarkdown.includes(type)) {
 | 
					                const resp = await fetch(`/server-api/files/html/${fileId}`, {
 | 
				
			||||||
            setContent(`~~~${type}
 | 
					                    method: "GET",
 | 
				
			||||||
${content}
 | 
					                })
 | 
				
			||||||
~~~
 | 
					                if (resp.ok) {
 | 
				
			||||||
`)
 | 
					                    const res = await resp.text()
 | 
				
			||||||
        } else {
 | 
					                    setPreview(res)
 | 
				
			||||||
            setContent(content)
 | 
					                    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)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            setIsLoading(false)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }, [type, content])
 | 
					        fetchPost()
 | 
				
			||||||
    return (<ReactMarkdownPreview height={height} content={contentToRender} />)
 | 
					    }, [content, fileId, title])
 | 
				
			||||||
 | 
					    return (<>
 | 
				
			||||||
 | 
					        {isLoading ? <div>Loading...</div> : <article className={styles.markdownPreview} dangerouslySetInnerHTML={{ __html: preview }} style={{
 | 
				
			||||||
 | 
					            height
 | 
				
			||||||
 | 
					        }} />}
 | 
				
			||||||
 | 
					    </>)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default memo(MarkdownPreview)
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,30 +1,39 @@
 | 
				
			|||||||
export default function generateUUID() {
 | 
					export default function generateUUID() {
 | 
				
			||||||
    if (typeof crypto === 'object') {
 | 
						if (typeof crypto === "object") {
 | 
				
			||||||
        if (typeof crypto.randomUUID === 'function') {
 | 
							if (typeof crypto.randomUUID === "function") {
 | 
				
			||||||
            // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
 | 
								// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
 | 
				
			||||||
            return crypto.randomUUID();
 | 
								return crypto.randomUUID()
 | 
				
			||||||
        }
 | 
							}
 | 
				
			||||||
        if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
 | 
							if (
 | 
				
			||||||
            // https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
 | 
								typeof crypto.getRandomValues === "function" &&
 | 
				
			||||||
            const callback = (c: string) => {
 | 
								typeof Uint8Array === "function"
 | 
				
			||||||
                const num = Number(c);
 | 
							) {
 | 
				
			||||||
                return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16);
 | 
								// https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
 | 
				
			||||||
            };
 | 
								const callback = (c: string) => {
 | 
				
			||||||
            return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback);
 | 
									const num = Number(c)
 | 
				
			||||||
        }
 | 
									return (
 | 
				
			||||||
    }
 | 
										num ^
 | 
				
			||||||
    let timestamp = new Date().getTime();
 | 
										(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
 | 
				
			||||||
    let perforNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0;
 | 
									).toString(16)
 | 
				
			||||||
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
 | 
								}
 | 
				
			||||||
        let random = Math.random() * 16;
 | 
								return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback)
 | 
				
			||||||
        if (timestamp > 0) {
 | 
							}
 | 
				
			||||||
            random = (timestamp + random) % 16 | 0;
 | 
						}
 | 
				
			||||||
            timestamp = Math.floor(timestamp / 16);
 | 
						let timestamp = new Date().getTime()
 | 
				
			||||||
        } else {
 | 
						let perforNow =
 | 
				
			||||||
            random = (perforNow + random) % 16 | 0;
 | 
							(typeof performance !== "undefined" &&
 | 
				
			||||||
            perforNow = Math.floor(perforNow / 16);
 | 
								performance.now &&
 | 
				
			||||||
        }
 | 
								performance.now() * 1000) ||
 | 
				
			||||||
        return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16);
 | 
							0
 | 
				
			||||||
    });
 | 
						return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
 | 
				
			||||||
};
 | 
							let random = Math.random() * 16
 | 
				
			||||||
 | 
							if (timestamp > 0) {
 | 
				
			||||||
 | 
								random = (timestamp + random) % 16 | 0
 | 
				
			||||||
 | 
								timestamp = Math.floor(timestamp / 16)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								random = (perforNow + random) % 16 | 0
 | 
				
			||||||
 | 
								perforNow = Math.floor(perforNow / 16)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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 Cookies from "js-cookie"
 | 
				
			||||||
import { useCallback, useEffect } from "react"
 | 
					import { useRouter } from "next/router"
 | 
				
			||||||
import useSharedState from "./use-shared-state";
 | 
					import { useEffect, useState } from "react"
 | 
				
			||||||
import Cookies from 'js-cookie'
 | 
					import useSharedState from "./use-shared-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const useSignedIn = ({ redirectIfNotAuthed = false }: { redirectIfNotAuthed?: boolean }) => {
 | 
					const useSignedIn = () => {
 | 
				
			||||||
    const [isSignedIn, setSignedIn] = useSharedState('isSignedIn', false)
 | 
						const [signedIn, setSignedIn] = useSharedState(
 | 
				
			||||||
    const [isLoading, setLoading] = useSharedState('isLoading', true)
 | 
							"signedIn",
 | 
				
			||||||
    const signout = useCallback(() => setSignedIn(false), [setSignedIn])
 | 
							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();
 | 
						const signout = () => {
 | 
				
			||||||
    if (redirectIfNotAuthed && !isLoading && isSignedIn === false) {
 | 
							setSignedIn(false)
 | 
				
			||||||
        router.push('/signin')
 | 
							Cookies.remove("drift-token")
 | 
				
			||||||
    }
 | 
							router.push("/")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
						useEffect(() => {
 | 
				
			||||||
        async function checkToken() {
 | 
							if (token) {
 | 
				
			||||||
            const token = Cookies.get('drift-token')
 | 
								setSignedIn(true)
 | 
				
			||||||
            if (token) {
 | 
							} else {
 | 
				
			||||||
                const response = await fetch('/server-api/auth/verify-token', {
 | 
								setSignedIn(false)
 | 
				
			||||||
                    method: 'GET',
 | 
							}
 | 
				
			||||||
                    headers: {
 | 
						}, [setSignedIn, token])
 | 
				
			||||||
                        'Authorization': `Bearer ${token}`
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                if (response.ok) {
 | 
					 | 
				
			||||||
                    setSignedIn(true)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            setLoading(false)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        setLoading(true)
 | 
					 | 
				
			||||||
        checkToken()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const interval = setInterval(() => {
 | 
						return { signedIn, signin, token, signout }
 | 
				
			||||||
            checkToken()
 | 
					 | 
				
			||||||
        }, 10000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return () => clearInterval(interval);
 | 
					 | 
				
			||||||
    }, [setLoading, setSignedIn])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { isSignedIn, isLoading, signout }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default useSignedIn
 | 
					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
 | 
				
			||||||
@ -1,24 +1,34 @@
 | 
				
			|||||||
import { NextApiRequest, NextApiResponse } from "next"
 | 
					import { NextApiRequest, NextApiResponse } from "next"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
 | 
					const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
 | 
				
			||||||
    const { id, download } = req.query
 | 
						const { id, download } = req.query
 | 
				
			||||||
    const file = await fetch(`${process.env.API_URL}/files/raw/${id}`)
 | 
						const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, {
 | 
				
			||||||
    if (file.ok) {
 | 
							headers: {
 | 
				
			||||||
        const data = await file.json()
 | 
								Accept: "text/plain",
 | 
				
			||||||
        const { title, content } = data
 | 
								"x-secret-key": process.env.SECRET_KEY || "",
 | 
				
			||||||
        // serve the file raw as plain text
 | 
								Authorization: `Bearer ${req.cookies["drift-token"]}`
 | 
				
			||||||
        res.setHeader("Content-Type", "text/plain")
 | 
							}
 | 
				
			||||||
        res.setHeader('Cache-Control', 's-maxage=86400');
 | 
						})
 | 
				
			||||||
        if (download) {
 | 
					 | 
				
			||||||
            res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            res.setHeader("Content-Disposition", `inline; filename="${title}"`)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        res.status(200).send(content)
 | 
						res.setHeader("Content-Type", "text/plain; charset=utf-8")
 | 
				
			||||||
    } else {
 | 
						res.setHeader("Cache-Control", "s-maxage=86400")
 | 
				
			||||||
        res.status(404).send("File not found")
 | 
						if (file.ok) {
 | 
				
			||||||
    }
 | 
							const json = await file.json()
 | 
				
			||||||
 | 
							const data = json
 | 
				
			||||||
 | 
							const { title, content } = data
 | 
				
			||||||
 | 
							// serve the file raw as plain text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (download) {
 | 
				
			||||||
 | 
								res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								res.setHeader("Content-Disposition", `inline; filename="${title}"`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							res.status(200).write(content, "utf-8")
 | 
				
			||||||
 | 
							res.end()
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							res.status(404).send("File not found")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default getRawFile
 | 
					export default getRawFile
 | 
				
			||||||
 | 
				
			|||||||
@ -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 styles from '@styles/Home.module.css'
 | 
				
			||||||
import { Page } from '@geist-ui/core'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Header from '@components/header'
 | 
					import Header from '@components/header'
 | 
				
			||||||
import MyPosts from '@components/my-posts'
 | 
					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 (
 | 
					  return (
 | 
				
			||||||
    <Page className={styles.container} width="100%">
 | 
					    <Page className={styles.container}>
 | 
				
			||||||
      <Page.Header>
 | 
					      <Page.Header>
 | 
				
			||||||
        <Header theme={theme} changeTheme={changeTheme} />
 | 
					        <Header />
 | 
				
			||||||
      </Page.Header>
 | 
					      </Page.Header>
 | 
				
			||||||
      <Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
 | 
					      <Page.Content className={styles.main}>
 | 
				
			||||||
        <MyPosts />
 | 
					        <MyPosts error={error} posts={posts} />
 | 
				
			||||||
      </Page.Content>
 | 
					      </Page.Content>
 | 
				
			||||||
    </Page >
 | 
					    </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
 | 
					export default Home
 | 
				
			||||||
 | 
				
			|||||||
@ -1,31 +1,23 @@
 | 
				
			|||||||
import styles from '@styles/Home.module.css'
 | 
					import styles from '@styles/Home.module.css'
 | 
				
			||||||
import NewPost from '@components/new-post'
 | 
					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 Header from '@components/header'
 | 
				
			||||||
import { ThemeProps } from './_app'
 | 
					 | 
				
			||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import PageSeo from '@components/page-seo'
 | 
					import PageSeo from '@components/page-seo'
 | 
				
			||||||
 | 
					import { Page } from '@geist-ui/core'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Home = ({ theme, changeTheme }: ThemeProps) => {
 | 
					const New = () => {
 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
  const { isSignedIn, isLoading } = useSignedIn({ redirectIfNotAuthed: true })
 | 
					 | 
				
			||||||
  if (!isSignedIn && !isLoading) {
 | 
					 | 
				
			||||||
    router.push("/signin")
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Page className={styles.container} width="100%">
 | 
					    <Page className={styles.container} width="100%">
 | 
				
			||||||
      <PageSeo title="Drift - New" />
 | 
					      <PageSeo title="Drift - New" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Page.Header>
 | 
					      <Page.Header>
 | 
				
			||||||
        <Header theme={theme} changeTheme={changeTheme} />
 | 
					        <Header />
 | 
				
			||||||
      </Page.Header>
 | 
					      </Page.Header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
 | 
					      <Page.Content className={styles.main}>
 | 
				
			||||||
        {isSignedIn && <NewPost />}
 | 
					        <NewPost />
 | 
				
			||||||
      </Page.Content>
 | 
					      </Page.Content>
 | 
				
			||||||
    </Page >
 | 
					    </Page >
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Home
 | 
					export default New
 | 
				
			||||||
 | 
				
			|||||||
@ -1,111 +1,49 @@
 | 
				
			|||||||
import { Button, Page, Text } from "@geist-ui/core";
 | 
					import type { GetStaticPaths, GetStaticProps } from "next";
 | 
				
			||||||
import Skeleton from 'react-loading-skeleton';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useRouter } from "next/router";
 | 
					import type { Post } from "@lib/types";
 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					import PostPage from "@components/post-page";
 | 
				
			||||||
import Document from '../../components/document'
 | 
					 | 
				
			||||||
import Header from "../../components/header";
 | 
					 | 
				
			||||||
import VisibilityBadge from "../../components/visibility-badge";
 | 
					 | 
				
			||||||
import { ThemeProps } from "../_app";
 | 
					 | 
				
			||||||
import PageSeo from "components/page-seo";
 | 
					 | 
				
			||||||
import Head from "next/head";
 | 
					 | 
				
			||||||
import styles from './styles.module.css';
 | 
					 | 
				
			||||||
import Cookies from "js-cookie";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Post = ({ theme, changeTheme }: ThemeProps) => {
 | 
					export type PostProps = {
 | 
				
			||||||
    const [post, setPost] = useState<any>()
 | 
					    post: Post
 | 
				
			||||||
    const [isLoading, setIsLoading] = useState(true)
 | 
					}
 | 
				
			||||||
    const [error, setError] = useState<string>()
 | 
					 | 
				
			||||||
    const router = useRouter();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					const PostView = ({ post }: PostProps) => {
 | 
				
			||||||
        async function fetchPost() {
 | 
					    return <PostPage post={post} />
 | 
				
			||||||
            setIsLoading(true);
 | 
					}
 | 
				
			||||||
            if (router.query.id) {
 | 
					 | 
				
			||||||
                const post = await fetch(`/server-api/posts/${router.query.id}`, {
 | 
					 | 
				
			||||||
                    method: "GET",
 | 
					 | 
				
			||||||
                    headers: {
 | 
					 | 
				
			||||||
                        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
                        "Authorization": `Bearer ${Cookies.get("drift-token")}`
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (post.ok) {
 | 
					export const getStaticPaths: GetStaticPaths = async () => {
 | 
				
			||||||
                    const res = await post.json()
 | 
					    const posts = await fetch(process.env.API_URL + `/posts/`, {
 | 
				
			||||||
                    if (res)
 | 
					        method: "GET",
 | 
				
			||||||
                        setPost(res)
 | 
					        headers: {
 | 
				
			||||||
                    else
 | 
					            "Content-Type": "application/json",
 | 
				
			||||||
                        setError("Post not found")
 | 
					            "x-secret-key": process.env.SECRET_KEY || "",
 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    if (post.status.toString().startsWith("4")) {
 | 
					 | 
				
			||||||
                        router.push("/signin")
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        setError(post.statusText)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                setIsLoading(false)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        fetchPost()
 | 
					    })
 | 
				
			||||||
    }, [router, router.query.id])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const download = async () => {
 | 
					    const json = await posts.json()
 | 
				
			||||||
        const clientZip = require("client-zip")
 | 
					    const filtered = json.filter((post: Post) => post.visibility === "public" || post.visibility === "unlisted")
 | 
				
			||||||
 | 
					    const paths = filtered.map((post: Post) => ({
 | 
				
			||||||
 | 
					        params: { id: post.id }
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const blob = await clientZip.downloadZip(post.files.map((file: any) => {
 | 
					    return { paths, fallback: 'blocking' }
 | 
				
			||||||
            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 (
 | 
					export const getStaticProps: GetStaticProps = async ({ params }) => {
 | 
				
			||||||
        <Page width={"100%"}>
 | 
					    const post = await fetch(process.env.API_URL + `/posts/${params?.id}`, {
 | 
				
			||||||
            {!isLoading && (
 | 
					        method: "GET",
 | 
				
			||||||
                <PageSeo
 | 
					        headers: {
 | 
				
			||||||
                    title={`${post.title} - Drift`}
 | 
					            "Content-Type": "application/json",
 | 
				
			||||||
                    description={post.description}
 | 
					            "x-secret-key": process.env.SECRET_KEY || "",
 | 
				
			||||||
                    isPrivate={post.visibility === 'private'}
 | 
					        }
 | 
				
			||||||
                />
 | 
					    })
 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <Page.Header>
 | 
					    return {
 | 
				
			||||||
                <Header theme={theme} changeTheme={changeTheme} />
 | 
					        props: {
 | 
				
			||||||
            </Page.Header>
 | 
					            post: await post.json()
 | 
				
			||||||
            <Page.Content width={"var(--main-content-width)"} margin="auto">
 | 
					        },
 | 
				
			||||||
                {error && <Text type="error">{error}</Text>}
 | 
					    }
 | 
				
			||||||
                {/* {!error && (isLoading || !post?.files) && <Loading />} */}
 | 
					 | 
				
			||||||
                {!error && isLoading && <><Text h2><Skeleton width={400} /></Text>
 | 
					 | 
				
			||||||
                    <Document skeleton={true} />
 | 
					 | 
				
			||||||
                </>}
 | 
					 | 
				
			||||||
                {!isLoading && post && <>
 | 
					 | 
				
			||||||
                    <div className={styles.header}>
 | 
					 | 
				
			||||||
                        <Text h2>{post.title} <VisibilityBadge visibility={post.visibility} /></Text>
 | 
					 | 
				
			||||||
                        <Button auto onClick={download}>
 | 
					 | 
				
			||||||
                            Download as ZIP archive
 | 
					 | 
				
			||||||
                        </Button>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    {post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
 | 
					 | 
				
			||||||
                        <Document
 | 
					 | 
				
			||||||
                            key={id}
 | 
					 | 
				
			||||||
                            id={id}
 | 
					 | 
				
			||||||
                            content={content}
 | 
					 | 
				
			||||||
                            title={title}
 | 
					 | 
				
			||||||
                            editable={false}
 | 
					 | 
				
			||||||
                            initialTab={'preview'}
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                    ))}
 | 
					 | 
				
			||||||
                </>}
 | 
					 | 
				
			||||||
            </Page.Content>
 | 
					 | 
				
			||||||
        </Page >
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Post
 | 
					export default PostView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {
 | 
					.wrapper {
 | 
				
			||||||
  min-height: 100vh;
 | 
					  height: 100% !important;
 | 
				
			||||||
  flex: 1;
 | 
					  padding-bottom: var(--small-gap) !important;
 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  margin: 0 auto;
 | 
					 | 
				
			||||||
  width: var(--main-content-width);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.container {
 | 
					 | 
				
			||||||
  width: 100% !important;
 | 
					  width: 100% !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen and (max-width: 768px) {
 | 
					.main {
 | 
				
			||||||
  .container {
 | 
					  max-width: var(--main-content) !important;
 | 
				
			||||||
    width: 100%;
 | 
					  margin: 0 auto !important;
 | 
				
			||||||
    margin: 0 auto !important;
 | 
					  padding: 0 0 !important;
 | 
				
			||||||
    padding: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .container h1 {
 | 
					 | 
				
			||||||
    font-size: 2rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .main {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,32 +1,154 @@
 | 
				
			|||||||
 | 
					@import "./syntax.css";
 | 
				
			||||||
 | 
					@import "./markdown.css";
 | 
				
			||||||
 | 
					@import "./inter.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:root {
 | 
					:root {
 | 
				
			||||||
  --main-content-width: 800px;
 | 
						/* Spacing */
 | 
				
			||||||
  --page-nav-height: 60px;
 | 
						--gap-quarter: 0.25rem;
 | 
				
			||||||
  --gap: 8px;
 | 
						--gap-half: 0.5rem;
 | 
				
			||||||
  --gap-half: calc(var(--gap) / 2);
 | 
						--gap: 1rem;
 | 
				
			||||||
  --gap-double: calc(var(--gap) * 2);
 | 
						--gap-double: 2rem;
 | 
				
			||||||
  --border-radius: 4px;
 | 
						--small-gap: 4rem;
 | 
				
			||||||
  --font-size: 16px;
 | 
						--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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[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);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen and (max-width: 768px) {
 | 
					* {
 | 
				
			||||||
  :root {
 | 
						box-sizing: border-box;
 | 
				
			||||||
    --main-content-width: 100%;
 | 
					}
 | 
				
			||||||
  }
 | 
					
 | 
				
			||||||
 | 
					::selection {
 | 
				
			||||||
 | 
						text-shadow: none;
 | 
				
			||||||
 | 
						background: var(--selection);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html,
 | 
					html,
 | 
				
			||||||
body {
 | 
					body {
 | 
				
			||||||
  padding: 0;
 | 
						padding: 0;
 | 
				
			||||||
  margin: 0;
 | 
						margin: 0;
 | 
				
			||||||
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
 | 
						font-size: 15px;
 | 
				
			||||||
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
 | 
						text-rendering: optimizeLegibility;
 | 
				
			||||||
 | 
						-webkit-font-smoothing: antialiased;
 | 
				
			||||||
 | 
						-moz-osx-font-smoothing: grayscale;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a {
 | 
					body {
 | 
				
			||||||
  color: inherit;
 | 
						min-height: 100vh;
 | 
				
			||||||
  text-decoration: none;
 | 
						font-family: var(--font-sans);
 | 
				
			||||||
 | 
						display: flex;
 | 
				
			||||||
 | 
						flex-direction: column;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* {
 | 
					p {
 | 
				
			||||||
  box-sizing: border-box;
 | 
						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;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						* {
 | 
				
			||||||
 | 
							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
 | 
					.env
 | 
				
			||||||
node_modules/
 | 
					node_modules/
 | 
				
			||||||
dist/
 | 
					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 { celebrate, Joi } from "celebrate";
 | 
				
			||||||
import { Router } from "express";
 | 
					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();
 | 
					export const files = Router();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
files.get(
 | 
					files.get("/raw/:id",
 | 
				
			||||||
  "/raw/:id",
 | 
					 | 
				
			||||||
  celebrate({
 | 
					  celebrate({
 | 
				
			||||||
    params: {
 | 
					    params: {
 | 
				
			||||||
      id: Joi.string().required(),
 | 
					      id: Joi.string().required(),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					  secretKey,
 | 
				
			||||||
  async (req, res, next) => {
 | 
					  async (req, res, next) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const file = await File.findOne({
 | 
					      const file = await File.findOne({
 | 
				
			||||||
        where: {
 | 
					        where: {
 | 
				
			||||||
          id: req.params.id,
 | 
					          id: req.params.id
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        attributes: ["title", "content"],
 | 
					        attributes: ["title", "content"],
 | 
				
			||||||
      });
 | 
					      })
 | 
				
			||||||
      // TODO: fix post inclusion
 | 
					
 | 
				
			||||||
      // if (file?.post.visibility === 'public' || file?.post.visibility === 'unlisted') {
 | 
					      if (!file) {
 | 
				
			||||||
      res.setHeader("Cache-Control", "public, max-age=86400");
 | 
					        return res.status(404).json({ error: "File not found" })
 | 
				
			||||||
      res.json(file);
 | 
					      }
 | 
				
			||||||
      // } else {
 | 
					
 | 
				
			||||||
      // TODO: should this be `private, `?
 | 
					      // TODO: JWT-checkraw files
 | 
				
			||||||
      // res.setHeader("Cache-Control", "max-age=86400");
 | 
					      if (file?.post?.visibility === "private") {
 | 
				
			||||||
      // res.json(file);
 | 
					        // jwt(req as UserJwtRequest, res, () => {
 | 
				
			||||||
      // }
 | 
					        //     res.json(file);
 | 
				
			||||||
    } catch (e) {
 | 
					        // })
 | 
				
			||||||
 | 
					        res.json(file);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        res.json(file);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    catch (e) {
 | 
				
			||||||
      next(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 { Router } from "express";
 | 
				
			||||||
import { User } from "../../lib/models/User";
 | 
					// import jwt from "@lib/middleware/jwt";
 | 
				
			||||||
import { File } from "../../lib/models/File";
 | 
					// import { User } from "@lib/models/User";
 | 
				
			||||||
import jwt, { UserJwtRequest } from "../../lib/middleware/jwt";
 | 
					 | 
				
			||||||
import { Post } from "../../lib/models/Post";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const users = Router();
 | 
					export const users = Router();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
users.get("/", jwt, async (req, res, next) => {
 | 
					// users.get("/", jwt, async (req, res, next) => {
 | 
				
			||||||
  try {
 | 
					//   try {
 | 
				
			||||||
    const allUsers = await User.findAll();
 | 
					//     const allUsers = await User.findAll();
 | 
				
			||||||
    res.json(allUsers);
 | 
					//     res.json(allUsers);
 | 
				
			||||||
  } catch (error) {
 | 
					//   } catch (error) {
 | 
				
			||||||
    next(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);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
				
			|||||||
					Loading…
					
					
				
		Reference in New Issue