client: overhaul markdown rendering (now server-side), refactor theming
This commit is contained in:
parent
d1ee9d857f
commit
34b1ab979f
41 changed files with 735 additions and 518 deletions
|
@ -1,5 +1,5 @@
|
||||||
import type { LinkProps } from "@geist-ui/core"
|
import type { LinkProps } from "@geist-ui/core"
|
||||||
import GeistLink from "@geist-ui/core/dist/link"
|
import { Link as GeistLink } from "@geist-ui/core"
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const Link = (props: LinkProps) => {
|
const Link = (props: LinkProps) => {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import styles from './auth.module.css'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import Link from '../Link'
|
import Link from '../Link'
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import useSignedIn from '@lib/hooks/use-signed-in'
|
||||||
|
|
||||||
const NO_EMPTY_SPACE_REGEX = /^\S*$/;
|
const NO_EMPTY_SPACE_REGEX = /^\S*$/;
|
||||||
const ERROR_MESSAGE = "Provide a non empty username and a password with at least 6 characters";
|
const ERROR_MESSAGE = "Provide a non empty username and a password with at least 6 characters";
|
||||||
|
@ -17,7 +18,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
|
||||||
const [errorMsg, setErrorMsg] = useState('');
|
const [errorMsg, setErrorMsg] = useState('');
|
||||||
const [requiresServerPassword, setRequiresServerPassword] = useState(false);
|
const [requiresServerPassword, setRequiresServerPassword] = useState(false);
|
||||||
const signingIn = page === 'signin'
|
const signingIn = page === 'signin'
|
||||||
|
const { signin } = useSignedIn();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchRequiresPass() {
|
async function fetchRequiresPass() {
|
||||||
if (!signingIn) {
|
if (!signingIn) {
|
||||||
|
@ -37,7 +38,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
|
||||||
|
|
||||||
|
|
||||||
const handleJson = (json: any) => {
|
const handleJson = (json: any) => {
|
||||||
Cookies.set('drift-token', json.token);
|
signin(json.token)
|
||||||
Cookies.set('drift-userid', json.userId);
|
Cookies.set('drift-userid', json.userId);
|
||||||
|
|
||||||
router.push('/')
|
router.push('/')
|
||||||
|
@ -65,7 +66,6 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
|
||||||
|
|
||||||
handleJson(json)
|
handleJson(json)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log(err)
|
|
||||||
setErrorMsg(err.message ?? "Something went wrong")
|
setErrorMsg(err.message ?? "Something went wrong")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
26
client/components/button-dropdown/dropdown.module.css
Normal file
26
client/components/button-dropdown/dropdown.module.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
.main {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownContent {
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
116
client/components/button-dropdown/index.tsx
Normal file
116
client/components/button-dropdown/index.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import Button from "@components/button"
|
||||||
|
import React, { useCallback, useEffect } from "react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import styles from './dropdown.module.css'
|
||||||
|
import DownIcon from '@geist-ui/icons/arrowDown'
|
||||||
|
type Props = {
|
||||||
|
type?: "primary" | "secondary"
|
||||||
|
loading?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
iconHeight?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attrs = Omit<React.HTMLAttributes<any>, keyof Props>
|
||||||
|
type ButtonDropdownProps = Props & Attrs
|
||||||
|
|
||||||
|
const ButtonDropdown: React.FC<React.PropsWithChildren<ButtonDropdownProps>> = ({
|
||||||
|
type,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
iconHeight = 24,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const [dropdown, setDropdown] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopImmediatePropagation()
|
||||||
|
setVisible(!visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopImmediatePropagation()
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickOutside = useCallback(() => (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (dropdown && !dropdown.contains(e.target as Node)) {
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
}, [dropdown])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
document.addEventListener("mousedown", onClickOutside)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener("mousedown", onClickOutside)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", onClickOutside)
|
||||||
|
}
|
||||||
|
}, [visible, onClickOutside])
|
||||||
|
|
||||||
|
if (!Array.isArray(props.children)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.main} ${className}`}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={onBlur}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-end' }}>
|
||||||
|
{props.children[0]}
|
||||||
|
<Button style={{ height: iconHeight, width: iconHeight }} className={styles.icon} onClick={() => setVisible(!visible)}><DownIcon /></Button>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
visible && (
|
||||||
|
<div
|
||||||
|
className={`${styles.dropdown}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${styles.dropdownContent}`}
|
||||||
|
>
|
||||||
|
{props.children.slice(1)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ButtonDropdown
|
62
client/components/button/button.module.css
Normal file
62
client/components/button/button.module.css
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
.button {
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--input-fg);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: var(--input-border);
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--gap-quarter) var(--gap-half);
|
||||||
|
transition: background-color var(--transition), color var(--transition);
|
||||||
|
width: 100%;
|
||||||
|
height: var(--input-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
--input-height: 2.5rem;
|
||||||
|
--input-border: 1px solid var(--light-gray);
|
||||||
|
--input-border-focus: 1px solid var(--gray);
|
||||||
|
--input-border-error: 1px solid var(--red);
|
||||||
|
--input-bg: var(--bg);
|
||||||
|
--input-fg: var(--fg);
|
||||||
|
--input-placeholder-fg: var(--light-gray); */
|
||||||
|
|
||||||
|
.button:hover,
|
||||||
|
.button:focus {
|
||||||
|
outline: none;
|
||||||
|
background: var(--input-bg-hover);
|
||||||
|
border: var(--input-border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--lighter-gray);
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
--bg: #131415;
|
||||||
|
--fg: #fafbfc;
|
||||||
|
--gray: #666;
|
||||||
|
--light-gray: #444;
|
||||||
|
--lighter-gray: #222;
|
||||||
|
--lightest-gray: #1a1a1a;
|
||||||
|
--article-color: #eaeaea;
|
||||||
|
--header-bg: rgba(19, 20, 21, 0.45);
|
||||||
|
--gray-alpha: rgba(255, 255, 255, 0.5);
|
||||||
|
--selection: rgba(255, 255, 255, 0.99);
|
||||||
|
*/
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: var(--fg);
|
||||||
|
color: var(--bg);
|
||||||
|
}
|
28
client/components/button/index.tsx
Normal file
28
client/components/button/index.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import styles from './button.module.css'
|
||||||
|
import { forwardRef, Ref } from 'react'
|
||||||
|
|
||||||
|
type Props = React.HTMLProps<HTMLButtonElement> & {
|
||||||
|
children: React.ReactNode
|
||||||
|
buttonType?: 'primary' | 'secondary'
|
||||||
|
className?: string
|
||||||
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const Button = forwardRef<HTMLButtonElement, Props>(
|
||||||
|
({ children, onClick, className, buttonType = 'primary', type = 'button', disabled = false, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={`${styles.button} ${styles[type]} ${className}`}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Button
|
|
@ -1,11 +1,10 @@
|
||||||
import ButtonGroup from "@geist-ui/core/dist/button-group"
|
|
||||||
import Button from "@geist-ui/core/dist/button"
|
|
||||||
import Bold from '@geist-ui/icons/bold'
|
import Bold from '@geist-ui/icons/bold'
|
||||||
import Italic from '@geist-ui/icons/italic'
|
import Italic from '@geist-ui/icons/italic'
|
||||||
import Link from '@geist-ui/icons/link'
|
import Link from '@geist-ui/icons/link'
|
||||||
import ImageIcon from '@geist-ui/icons/image'
|
import ImageIcon from '@geist-ui/icons/image'
|
||||||
import { RefObject, useCallback, useMemo } from "react"
|
import { RefObject, useCallback, useMemo } from "react"
|
||||||
import styles from '../document.module.css'
|
import styles from '../document.module.css'
|
||||||
|
import { Button, ButtonGroup } from "@geist-ui/core"
|
||||||
|
|
||||||
// TODO: clean up
|
// TODO: clean up
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
import Button from "@geist-ui/core/dist/button"
|
|
||||||
import Card from "@geist-ui/core/dist/card"
|
|
||||||
import ButtonGroup from "@geist-ui/core/dist/button-group"
|
|
||||||
import Input from "@geist-ui/core/dist/input"
|
|
||||||
import Spacer from "@geist-ui/core/dist/spacer"
|
|
||||||
import Tabs from "@geist-ui/core/dist/tabs"
|
|
||||||
import Textarea from "@geist-ui/core/dist/textarea"
|
|
||||||
import Tooltip from "@geist-ui/core/dist/tooltip"
|
|
||||||
|
|
||||||
import { ChangeEvent, memo, useCallback, useMemo, useRef, useState } from "react"
|
import { ChangeEvent, memo, useCallback, useMemo, useRef, useState } from "react"
|
||||||
import styles from './document.module.css'
|
import styles from './document.module.css'
|
||||||
|
@ -15,9 +8,8 @@ import ExternalLink from '@geist-ui/icons/externalLink'
|
||||||
import FormattingIcons from "./formatting-icons"
|
import FormattingIcons from "./formatting-icons"
|
||||||
import Skeleton from "react-loading-skeleton"
|
import Skeleton from "react-loading-skeleton"
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core"
|
||||||
|
import Preview from "@components/preview"
|
||||||
const MarkdownPreview = dynamic(() => import("../preview"))
|
|
||||||
|
|
||||||
// import Link from "next/link"
|
// import Link from "next/link"
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -74,13 +66,6 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
setTab(newTab as 'edit' | 'preview')
|
setTab(newTab as 'edit' | 'preview')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getType = useCallback(() => {
|
|
||||||
if (!title) return
|
|
||||||
const pathParts = title.split(".")
|
|
||||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
|
||||||
return language
|
|
||||||
}, [title])
|
|
||||||
|
|
||||||
const onTitleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setTitle ? setTitle(event.target.value) : null, [setTitle])
|
const onTitleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setTitle ? setTitle(event.target.value) : null, [setTitle])
|
||||||
|
|
||||||
const removeFile = useCallback(() => (remove?: () => void) => {
|
const removeFile = useCallback(() => (remove?: () => void) => {
|
||||||
|
@ -140,14 +125,14 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionContainer}>
|
<div className={styles.descriptionContainer}>
|
||||||
{tab === 'edit' && editable && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
|
{tab === 'edit' && editable && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
|
||||||
{rawLink && <DownloadButton rawLink={rawLink()} />}
|
{rawLink && id && <DownloadButton rawLink={rawLink()} />}
|
||||||
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
|
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
|
||||||
<Tabs.Item label={editable ? "Edit" : "Raw"} value="edit">
|
<Tabs.Item label={editable ? "Edit" : "Raw"} value="edit">
|
||||||
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
|
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ marginTop: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={codeEditorRef}
|
ref={codeEditorRef}
|
||||||
placeholder="Type some contents..."
|
placeholder=""
|
||||||
value={content}
|
value={content}
|
||||||
onChange={handleOnContentChange}
|
onChange={handleOnContentChange}
|
||||||
width="100%"
|
width="100%"
|
||||||
|
@ -160,7 +145,7 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Item>
|
</Tabs.Item>
|
||||||
<Tabs.Item label="Preview" value="preview">
|
<Tabs.Item label="Preview" value="preview">
|
||||||
<MarkdownPreview height={height} content={content} type={getType()} />
|
<Preview height={height} fileId={id} title={title} content={content} />
|
||||||
</Tabs.Item>
|
</Tabs.Item>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|
27
client/components/head/index.tsx
Normal file
27
client/components/head/index.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import Head from "next/head";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type PageSeoProps = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
isPrivate?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageSeo = ({
|
||||||
|
title = 'Drift',
|
||||||
|
description = "A self-hostable clone of GitHub Gist",
|
||||||
|
isPrivate = false
|
||||||
|
}: PageSeoProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{title}</title>
|
||||||
|
{!isPrivate && <meta name="description" content={description} />}
|
||||||
|
</Head>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageSeo;
|
|
@ -1,19 +1,16 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import MoonIcon from '@geist-ui/icons/moon'
|
import MoonIcon from '@geist-ui/icons/moon'
|
||||||
import SunIcon from '@geist-ui/icons/sun'
|
import SunIcon from '@geist-ui/icons/sun'
|
||||||
import Select from '@geist-ui/core/dist/select'
|
|
||||||
// import { useAllThemes, useTheme } from '@geist-ui/core'
|
// import { useAllThemes, useTheme } from '@geist-ui/core'
|
||||||
import styles from './header.module.css'
|
import styles from './header.module.css'
|
||||||
import { ThemeProps } from '@lib/types'
|
import { ThemeProps } from '@lib/types'
|
||||||
import Cookies from 'js-cookie'
|
import { Select } from '@geist-ui/core'
|
||||||
|
|
||||||
const Controls = ({ changeTheme, theme }: ThemeProps) => {
|
const Controls = ({ changeTheme, theme }: ThemeProps) => {
|
||||||
const switchThemes = (type: string | string[]) => {
|
const switchThemes = () => {
|
||||||
changeTheme()
|
changeTheme()
|
||||||
Cookies.set('drift-theme', Array.isArray(type) ? type[0] : type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<Select
|
<Select
|
||||||
|
|
179
client/components/header/header.tsx
Normal file
179
client/components/header/header.tsx
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
|
||||||
|
import { ButtonGroup, Page, Spacer, Tabs, useBodyScroll, useMediaQuery, } from "@geist-ui/core";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import styles from './header.module.css';
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import useSignedIn from "../../lib/hooks/use-signed-in";
|
||||||
|
|
||||||
|
import HomeIcon from '@geist-ui/icons/home';
|
||||||
|
import MenuIcon from '@geist-ui/icons/menu';
|
||||||
|
import GitHubIcon from '@geist-ui/icons/github';
|
||||||
|
import SignOutIcon from '@geist-ui/icons/userX';
|
||||||
|
import SignInIcon from '@geist-ui/icons/user';
|
||||||
|
import SignUpIcon from '@geist-ui/icons/userPlus';
|
||||||
|
import NewIcon from '@geist-ui/icons/plusCircle';
|
||||||
|
import YourIcon from '@geist-ui/icons/list'
|
||||||
|
import MoonIcon from '@geist-ui/icons/moon';
|
||||||
|
import SunIcon from '@geist-ui/icons/sun';
|
||||||
|
import type { ThemeProps } from "@lib/types";
|
||||||
|
import useTheme from "@lib/hooks/use-theme";
|
||||||
|
import { Button } from "@geist-ui/core";
|
||||||
|
|
||||||
|
type Tab = {
|
||||||
|
name: string
|
||||||
|
icon: JSX.Element
|
||||||
|
condition?: boolean
|
||||||
|
value: string
|
||||||
|
onClick?: () => void
|
||||||
|
href?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [selectedTab, setSelectedTab] = useState<string>(router.pathname === '/' ? 'home' : router.pathname.split('/')[1]);
|
||||||
|
const [expanded, setExpanded] = useState<boolean>(false)
|
||||||
|
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
|
||||||
|
const isMobile = useMediaQuery('xs', { match: 'down' })
|
||||||
|
const { signedIn: isSignedIn } = useSignedIn()
|
||||||
|
const [pages, setPages] = useState<Tab[]>([])
|
||||||
|
const { changeTheme, theme } = useTheme()
|
||||||
|
useEffect(() => {
|
||||||
|
setBodyHidden(expanded)
|
||||||
|
}, [expanded, setBodyHidden])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile) {
|
||||||
|
setExpanded(false)
|
||||||
|
}
|
||||||
|
}, [isMobile])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pageList: Tab[] = [
|
||||||
|
{
|
||||||
|
name: "Home",
|
||||||
|
href: "/",
|
||||||
|
icon: <HomeIcon />,
|
||||||
|
condition: !isSignedIn,
|
||||||
|
value: "home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New",
|
||||||
|
href: "/new",
|
||||||
|
icon: <NewIcon />,
|
||||||
|
condition: isSignedIn,
|
||||||
|
value: "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Yours",
|
||||||
|
href: "/mine",
|
||||||
|
icon: <YourIcon />,
|
||||||
|
condition: isSignedIn,
|
||||||
|
value: "mine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sign out",
|
||||||
|
href: "/signout",
|
||||||
|
icon: <SignOutIcon />,
|
||||||
|
condition: isSignedIn,
|
||||||
|
value: "signout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sign in",
|
||||||
|
href: "/signin",
|
||||||
|
icon: <SignInIcon />,
|
||||||
|
condition: !isSignedIn,
|
||||||
|
value: "signin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sign up",
|
||||||
|
href: "/signup",
|
||||||
|
icon: <SignUpIcon />,
|
||||||
|
condition: !isSignedIn,
|
||||||
|
value: "signup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: isMobile ? "GitHub" : "",
|
||||||
|
href: "https://github.com/maxleiter/drift",
|
||||||
|
icon: <GitHubIcon />,
|
||||||
|
condition: true,
|
||||||
|
value: "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: isMobile ? "Change theme" : "",
|
||||||
|
onClick: function () {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
changeTheme();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: theme === 'light' ? <MoonIcon /> : <SunIcon />,
|
||||||
|
condition: true,
|
||||||
|
value: "theme",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
setPages(pageList.filter(page => page.condition))
|
||||||
|
}, [changeTheme, isMobile, isSignedIn, theme])
|
||||||
|
|
||||||
|
|
||||||
|
const onTabChange = useCallback((tab: string) => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
const match = pages.find(page => page.value === tab)
|
||||||
|
if (match?.onClick) {
|
||||||
|
match.onClick()
|
||||||
|
} else {
|
||||||
|
router.push(match?.href || '/')
|
||||||
|
}
|
||||||
|
}, [pages, router])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page.Header height={'var(--page-nav-height)'} margin={0} paddingBottom={0} paddingTop={"var(--gap)"}>
|
||||||
|
<div className={styles.tabs}>
|
||||||
|
<Tabs
|
||||||
|
value={selectedTab}
|
||||||
|
leftSpace={0}
|
||||||
|
align="center"
|
||||||
|
hideDivider
|
||||||
|
hideBorder
|
||||||
|
onChange={onTabChange}>
|
||||||
|
{pages.map((tab) => {
|
||||||
|
return <Tabs.Item
|
||||||
|
font="14px"
|
||||||
|
label={<>{tab.icon} {tab.name}</>}
|
||||||
|
value={tab.value}
|
||||||
|
key={`${tab.value}`}
|
||||||
|
/>
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<Button
|
||||||
|
auto
|
||||||
|
type="abort"
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
aria-label="Menu"
|
||||||
|
>
|
||||||
|
<Spacer height={5 / 6} width={0} />
|
||||||
|
<MenuIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isMobile && expanded && (<div className={styles.mobile}>
|
||||||
|
<ButtonGroup vertical>
|
||||||
|
{pages.map((tab, index) => {
|
||||||
|
return <Button
|
||||||
|
key={`${tab.name}-${index}`}
|
||||||
|
onClick={() => onTabChange(tab.value)}
|
||||||
|
icon={tab.icon}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</Button>
|
||||||
|
})}
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>)}
|
||||||
|
</Page.Header >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
|
@ -1,210 +1,8 @@
|
||||||
import Page from "@geist-ui/core/dist/page";
|
import dynamic from 'next/dynamic'
|
||||||
import ButtonGroup from "@geist-ui/core/dist/button-group";
|
|
||||||
import Button from "@geist-ui/core/dist/button";
|
|
||||||
import useBodyScroll from "@geist-ui/core/dist/use-body-scroll";
|
|
||||||
import useMediaQuery from "@geist-ui/core/dist/use-media-query";
|
|
||||||
import Tabs from "@geist-ui/core/dist/tabs";
|
|
||||||
import Spacer from "@geist-ui/core/dist/spacer";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
const Header = dynamic(import('./header'), {
|
||||||
import styles from './header.module.css';
|
ssr: false,
|
||||||
import { useRouter } from "next/router";
|
// loading: () => <MenuSkeleton />,
|
||||||
import useSignedIn from "../../lib/hooks/use-signed-in";
|
})
|
||||||
|
|
||||||
import HomeIcon from '@geist-ui/icons/home';
|
|
||||||
import MenuIcon from '@geist-ui/icons/menu';
|
|
||||||
import GitHubIcon from '@geist-ui/icons/github';
|
|
||||||
import SignOutIcon from '@geist-ui/icons/userX';
|
|
||||||
import SignInIcon from '@geist-ui/icons/user';
|
|
||||||
import SignUpIcon from '@geist-ui/icons/userPlus';
|
|
||||||
import NewIcon from '@geist-ui/icons/plusCircle';
|
|
||||||
import YourIcon from '@geist-ui/icons/list'
|
|
||||||
import MoonIcon from '@geist-ui/icons/moon';
|
|
||||||
import SunIcon from '@geist-ui/icons/sun';
|
|
||||||
import type { ThemeProps } from "@lib/types";
|
|
||||||
|
|
||||||
type Tab = {
|
|
||||||
name: string
|
|
||||||
icon: JSX.Element
|
|
||||||
condition?: boolean
|
|
||||||
value: string
|
|
||||||
onClick?: () => void
|
|
||||||
href?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const Header = ({ changeTheme, theme }: ThemeProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const [selectedTab, setSelectedTab] = useState<string>(router.pathname === '/' ? 'home' : router.pathname.split('/')[1]);
|
|
||||||
const [expanded, setExpanded] = useState<boolean>(false)
|
|
||||||
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
|
|
||||||
const isMobile = useMediaQuery('xs', { match: 'down' })
|
|
||||||
const { signedIn: isSignedIn } = useSignedIn()
|
|
||||||
const [pages, setPages] = useState<Tab[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setBodyHidden(expanded)
|
|
||||||
}, [expanded, setBodyHidden])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isMobile) {
|
|
||||||
setExpanded(false)
|
|
||||||
}
|
|
||||||
}, [isMobile])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const pageList: Tab[] = [
|
|
||||||
{
|
|
||||||
name: "Home",
|
|
||||||
href: "/",
|
|
||||||
icon: <HomeIcon />,
|
|
||||||
condition: !isSignedIn,
|
|
||||||
value: "home"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "New",
|
|
||||||
href: "/new",
|
|
||||||
icon: <NewIcon />,
|
|
||||||
condition: isSignedIn,
|
|
||||||
value: "new"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Yours",
|
|
||||||
href: "/mine",
|
|
||||||
icon: <YourIcon />,
|
|
||||||
condition: isSignedIn,
|
|
||||||
value: "mine"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sign out",
|
|
||||||
href: "/signout",
|
|
||||||
icon: <SignOutIcon />,
|
|
||||||
condition: isSignedIn,
|
|
||||||
value: "signout"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sign in",
|
|
||||||
href: "/signin",
|
|
||||||
icon: <SignInIcon />,
|
|
||||||
condition: !isSignedIn,
|
|
||||||
value: "signin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sign up",
|
|
||||||
href: "/signup",
|
|
||||||
icon: <SignUpIcon />,
|
|
||||||
condition: !isSignedIn,
|
|
||||||
value: "signup"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: isMobile ? "GitHub" : "",
|
|
||||||
href: "https://github.com/maxleiter/drift",
|
|
||||||
icon: <GitHubIcon />,
|
|
||||||
condition: true,
|
|
||||||
value: "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: isMobile ? "Change theme" : "",
|
|
||||||
onClick: function () {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
changeTheme();
|
|
||||||
setSelectedTab('');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: theme === 'light' ? <MoonIcon /> : <SunIcon />,
|
|
||||||
condition: true,
|
|
||||||
value: "theme",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
setPages(pageList.filter(page => page.condition))
|
|
||||||
}, [changeTheme, isMobile, isSignedIn, theme])
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// setSelectedTab(pages.find((page) => {
|
|
||||||
// console.log(page.href, router.asPath)
|
|
||||||
// if (page.href && page.href === router.asPath) {
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// })?.href)
|
|
||||||
// }, [pages, router, router.pathname])
|
|
||||||
|
|
||||||
const onTabChange = (tab: string) => {
|
|
||||||
const match = pages.find(page => page.value === tab)
|
|
||||||
if (match?.onClick) {
|
|
||||||
match.onClick()
|
|
||||||
} else if (match?.href) {
|
|
||||||
router.push(`${match.href}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page.Header height={'var(--page-nav-height)'} margin={0} paddingBottom={0} paddingTop={"var(--gap)"}>
|
|
||||||
<div className={styles.tabs}>
|
|
||||||
<Tabs
|
|
||||||
value={selectedTab}
|
|
||||||
leftSpace={0}
|
|
||||||
align="center"
|
|
||||||
hideDivider
|
|
||||||
hideBorder
|
|
||||||
onChange={onTabChange}>
|
|
||||||
{pages.map((tab) => {
|
|
||||||
return <Tabs.Item
|
|
||||||
font="14px"
|
|
||||||
label={<>{tab.icon} {tab.name}</>}
|
|
||||||
value={tab.value}
|
|
||||||
key={`${tab.value}`}
|
|
||||||
/>
|
|
||||||
})}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
<div className={styles.controls}>
|
|
||||||
<Button
|
|
||||||
auto
|
|
||||||
type="abort"
|
|
||||||
onClick={() => setExpanded(!expanded)}
|
|
||||||
aria-label="Menu"
|
|
||||||
>
|
|
||||||
<Spacer height={5 / 6} width={0} />
|
|
||||||
<MenuIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{isMobile && expanded && (<div className={styles.mobile}>
|
|
||||||
<ButtonGroup vertical>
|
|
||||||
{pages.map((tab, index) => {
|
|
||||||
return <Button
|
|
||||||
key={`${tab.name}-${index}`}
|
|
||||||
onClick={() => onTabChange(tab.value)}
|
|
||||||
icon={tab.icon}
|
|
||||||
>
|
|
||||||
{tab.name}
|
|
||||||
</Button>
|
|
||||||
})}
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>)}
|
|
||||||
</Page.Header >
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 > * /}
|
|
24
client/components/input/index.tsx
Normal file
24
client/components/input/index.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react'
|
||||||
|
import styles from './input.module.css'
|
||||||
|
|
||||||
|
type Props = React.HTMLProps<HTMLInputElement> & {
|
||||||
|
label?: string
|
||||||
|
fontSize?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, Props>(({ label, className, ...props }, ref) => {
|
||||||
|
return (<div className={styles.wrapper}>
|
||||||
|
{label && <label className={styles.label}>{label}</label>}
|
||||||
|
<input
|
||||||
|
|
||||||
|
ref={ref}
|
||||||
|
className={className ? `${styles.input} ${className}` : styles.input}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Input
|
57
client/components/input/input.module.css
Normal file
57
client/components/input/input.module.css
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: var(--inline-radius);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
border: 1px solid var(--light-gray);
|
||||||
|
padding: 0 var(--gap-half);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color var(--transition);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input::placeholder {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
border-color: var(--input-border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: inline-flex;
|
||||||
|
width: initial;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 var(--gap-half);
|
||||||
|
color: var(--fg);
|
||||||
|
background-color: var(--light-gray);
|
||||||
|
border-top-left-radius: var(--radius);
|
||||||
|
border-bottom-left-radius: var(--radius);
|
||||||
|
border-top: 1px solid var(--input-border);
|
||||||
|
border-left: 1px solid var(--input-border);
|
||||||
|
border-bottom: 1px solid var(--input-border);
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.wrapper {
|
||||||
|
margin-bottom: var(--gap);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,4 @@
|
||||||
import Button from '@geist-ui/core/dist/button'
|
import { Button, useToasts, ButtonDropdown } from '@geist-ui/core'
|
||||||
import useToasts from '@geist-ui/core/dist/use-toasts'
|
|
||||||
import ButtonDropdown from '@geist-ui/core/dist/button-dropdown'
|
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import generateUUID from '@lib/generate-uuid';
|
import generateUUID from '@lib/generate-uuid';
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import Input from "@geist-ui/core/dist/input"
|
|
||||||
import Modal from "@geist-ui/core/dist/modal"
|
import { Modal, Note, Spacer, Input } from "@geist-ui/core"
|
||||||
import Note from "@geist-ui/core/dist/note"
|
|
||||||
import Spacer from "@geist-ui/core/dist/spacer"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { ChangeEvent, memo, useCallback } from 'react'
|
import { ChangeEvent, memo, useCallback } from 'react'
|
||||||
import Text from '@geist-ui/core/dist/text'
|
import { Text } from '@geist-ui/core'
|
||||||
import Input from '@geist-ui/core/dist/input'
|
|
||||||
|
|
||||||
import ShiftBy from '@components/shift-by'
|
import ShiftBy from '@components/shift-by'
|
||||||
import styles from '../post.module.css'
|
import styles from '../post.module.css'
|
||||||
|
import { Input } from '@geist-ui/core'
|
||||||
|
|
||||||
const titlePlaceholders = [
|
const titlePlaceholders = [
|
||||||
"How to...",
|
"How to...",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Text from "@geist-ui/core/dist/text"
|
import { Text } from "@geist-ui/core"
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link"
|
||||||
import Link from '../Link'
|
import Link from '../Link'
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import Card from "@geist-ui/core/dist/card";
|
|
||||||
import Spacer from "@geist-ui/core/dist/spacer";
|
|
||||||
import Grid from "@geist-ui/core/dist/grid";
|
|
||||||
import Divider from "@geist-ui/core/dist/divider";
|
|
||||||
|
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import { Card, Divider, Grid, Spacer } from "@geist-ui/core";
|
||||||
|
|
||||||
const ListItemSkeleton = () => (<Card>
|
const ListItemSkeleton = () => (<Card>
|
||||||
<Spacer height={1 / 2} />
|
<Spacer height={1 / 2} />
|
||||||
|
|
|
@ -1,11 +1,3 @@
|
||||||
import Card from "@geist-ui/core/dist/card"
|
|
||||||
import Spacer from "@geist-ui/core/dist/spacer"
|
|
||||||
import Grid from "@geist-ui/core/dist/grid"
|
|
||||||
import Divider from "@geist-ui/core/dist/divider"
|
|
||||||
import Link from "@geist-ui/core/dist/link"
|
|
||||||
import Text from "@geist-ui/core/dist/text"
|
|
||||||
import Input from "@geist-ui/core/dist/input"
|
|
||||||
import Tooltip from "@geist-ui/core/dist/tooltip"
|
|
||||||
|
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
@ -13,6 +5,7 @@ import timeAgo from "@lib/time-ago"
|
||||||
import ShiftBy from "../shift-by"
|
import ShiftBy from "../shift-by"
|
||||||
import VisibilityBadge from "../visibility-badge"
|
import VisibilityBadge from "../visibility-badge"
|
||||||
import getPostPath from "@lib/get-post-path"
|
import getPostPath from "@lib/get-post-path"
|
||||||
|
import { Input, Link, Text, Card, Spacer, Grid, Tooltip, Divider } from "@geist-ui/core"
|
||||||
|
|
||||||
const FilenameInput = ({ title }: { title: string }) => <Input
|
const FilenameInput = ({ title }: { title: string }) => <Input
|
||||||
value={title}
|
value={title}
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import Header from "@components/header"
|
import Header from "@components/header/header"
|
||||||
import PageSeo from "@components/page-seo"
|
import PageSeo from "@components/page-seo"
|
||||||
import VisibilityBadge from "@components/visibility-badge"
|
import VisibilityBadge from "@components/visibility-badge"
|
||||||
import Page from "@geist-ui/core/dist/page"
|
|
||||||
import Button from "@geist-ui/core/dist/button"
|
|
||||||
import Text from "@geist-ui/core/dist/text"
|
|
||||||
import DocumentComponent from '@components/document'
|
import DocumentComponent from '@components/document'
|
||||||
import styles from './post-page.module.css'
|
import styles from './post-page.module.css'
|
||||||
|
import homeStyles from '@styles/Home.module.css'
|
||||||
|
|
||||||
import type { Post, ThemeProps } from "@lib/types"
|
import type { Post } from "@lib/types"
|
||||||
|
import { Page, Button, Text } from "@geist-ui/core"
|
||||||
|
|
||||||
type Props = ThemeProps & {
|
type Props = {
|
||||||
post: Post
|
post: Post
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostPage = ({ post, changeTheme, theme }: Props) => {
|
const PostPage = ({ post }: Props) => {
|
||||||
const download = async () => {
|
const download = async () => {
|
||||||
const downloadZip = (await import("client-zip")).downloadZip
|
const downloadZip = (await import("client-zip")).downloadZip
|
||||||
const blob = await downloadZip(post.files.map((file: any) => {
|
const blob = await downloadZip(post.files.map((file: any) => {
|
||||||
|
@ -39,9 +38,9 @@ const PostPage = ({ post, changeTheme, theme }: Props) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Page.Header>
|
<Page.Header>
|
||||||
<Header theme={theme} changeTheme={changeTheme} />
|
<Header />
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
<Page.Content width={"var(--main-content-width)"} margin="auto">
|
<Page.Content className={homeStyles.main}>
|
||||||
{/* {!isLoading && <PostFileExplorer files={post.files} />} */}
|
{/* {!isLoading && <PostFileExplorer files={post.files} />} */}
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.titleAndBadge}>
|
<div className={styles.titleAndBadge}>
|
||||||
|
|
|
@ -1,28 +1,55 @@
|
||||||
|
import useTheme from "@lib/hooks/use-theme"
|
||||||
import { memo, useEffect, useState } from "react"
|
import { memo, useEffect, useState } from "react"
|
||||||
import ReactMarkdownPreview from "./react-markdown-preview"
|
|
||||||
|
|
||||||
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)
|
||||||
|
const { theme } = useTheme()
|
||||||
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(`/api/markdown/${fileId}`, {
|
||||||
setContent(`~~~${type}
|
method: "GET",
|
||||||
${content}
|
})
|
||||||
~~~
|
if (resp.ok) {
|
||||||
`)
|
const res = await resp.text()
|
||||||
} else {
|
setPreview(res)
|
||||||
setContent(content)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [type, content])
|
} else {
|
||||||
return (<ReactMarkdownPreview height={height} content={contentToRender} />)
|
const resp = await fetch(`/api/render-markdown`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (resp.ok) {
|
||||||
|
const res = await resp.text()
|
||||||
|
setPreview(res)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchPost()
|
||||||
|
}, [content, fileId, title])
|
||||||
|
return (<>
|
||||||
|
{isLoading ? <div>Loading...</div> : <div data-theme={theme} dangerouslySetInnerHTML={{ __html: preview }} style={{
|
||||||
|
height
|
||||||
|
}} />}
|
||||||
|
</>)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(MarkdownPreview)
|
export default memo(MarkdownPreview)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Badge from "@geist-ui/core/dist/badge";
|
import { Badge } from "@geist-ui/core"
|
||||||
import type { PostVisibility } from "@lib/types"
|
import type { PostVisibility } from "@lib/types"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import useSharedState from "./use-shared-state";
|
||||||
|
|
||||||
const useSignedIn = () => {
|
const useSignedIn = () => {
|
||||||
const [signedIn, setSignedIn] = useState(typeof window === 'undefined' ? false : !!Cookies.get("drift-token"));
|
const [signedIn, setSignedIn] = useSharedState('signedIn', typeof window === 'undefined' ? false : !!Cookies.get("drift-token"));
|
||||||
const token = Cookies.get("drift-token")
|
const token = Cookies.get("drift-token")
|
||||||
|
const signin = (token: string) => {
|
||||||
|
setSignedIn(true);
|
||||||
|
Cookies.set("drift-token", token);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
|
@ -11,9 +16,9 @@ const useSignedIn = () => {
|
||||||
} else {
|
} else {
|
||||||
setSignedIn(false);
|
setSignedIn(false);
|
||||||
}
|
}
|
||||||
}, [token]);
|
}, [setSignedIn, token]);
|
||||||
|
|
||||||
return { signedIn, token };
|
return { signedIn, signin, token };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useSignedIn;
|
export default useSignedIn;
|
||||||
|
|
27
client/lib/hooks/use-theme.ts
Normal file
27
client/lib/hooks/use-theme.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { useCallback, useEffect } from "react"
|
||||||
|
import useSharedState from "./use-shared-state"
|
||||||
|
|
||||||
|
const useTheme = () => {
|
||||||
|
const isClient = typeof window === "object"
|
||||||
|
const [themeType, setThemeType] = useSharedState<string>('theme', 'light')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isClient) return
|
||||||
|
const storedTheme = localStorage.getItem('drift-theme')
|
||||||
|
if (storedTheme) {
|
||||||
|
setThemeType(storedTheme)
|
||||||
|
}
|
||||||
|
}, [isClient, setThemeType])
|
||||||
|
|
||||||
|
const changeTheme = useCallback(() => {
|
||||||
|
setThemeType(last => {
|
||||||
|
const newTheme = last === 'dark' ? 'light' : 'dark'
|
||||||
|
localStorage.setItem('drift-theme', newTheme)
|
||||||
|
return newTheme
|
||||||
|
})
|
||||||
|
}, [setThemeType])
|
||||||
|
|
||||||
|
return { theme: themeType, changeTheme }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTheme
|
|
@ -1,4 +1,3 @@
|
||||||
import Link from '@components/Link'
|
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import Highlight, { defaultProps, Language } from 'prism-react-renderer'
|
import Highlight, { defaultProps, Language } from 'prism-react-renderer'
|
||||||
import { renderToStaticMarkup } from 'react-dom/server'
|
import { renderToStaticMarkup } from 'react-dom/server'
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
"cookie": "^0.4.2",
|
"cookie": "^0.4.2",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
|
"marked": "^4.0.12",
|
||||||
"next": "^12.1.1-canary.15",
|
"next": "^12.1.1-canary.15",
|
||||||
|
"prism-react-renderer": "^1.3.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-dropzone": "^12.0.4",
|
"react-dropzone": "^12.0.4",
|
||||||
|
@ -33,6 +35,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "^12.1.0",
|
"@next/bundle-analyzer": "^12.1.0",
|
||||||
|
"@types/marked": "^4.0.3",
|
||||||
"@types/node": "17.0.21",
|
"@types/node": "17.0.21",
|
||||||
"@types/react": "17.0.39",
|
"@types/react": "17.0.39",
|
||||||
"@types/react-dom": "^17.0.14",
|
"@types/react-dom": "^17.0.14",
|
||||||
|
|
|
@ -1,46 +1,21 @@
|
||||||
import '@styles/globals.css'
|
import '@styles/globals.css'
|
||||||
import GeistProvider from '@geist-ui/core/dist/geist-provider'
|
|
||||||
import CssBaseline from '@geist-ui/core/dist/css-baseline'
|
|
||||||
import useTheme from '@geist-ui/core/dist/use-theme'
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
import type { AppProps as NextAppProps } from "next/app";
|
import type { AppProps as NextAppProps } from "next/app";
|
||||||
import useSharedState from '@lib/hooks/use-shared-state';
|
|
||||||
|
|
||||||
import 'react-loading-skeleton/dist/skeleton.css'
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
import { SkeletonTheme } from 'react-loading-skeleton';
|
import { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import type { ThemeProps } from '@lib/types';
|
import useTheme from '@lib/hooks/use-theme';
|
||||||
import Cookies from 'js-cookie';
|
import { CssBaseline, GeistProvider } from '@geist-ui/core';
|
||||||
|
|
||||||
type AppProps<P = any> = {
|
type AppProps<P = any> = {
|
||||||
pageProps: P;
|
pageProps: P;
|
||||||
} & Omit<NextAppProps<P>, "pageProps">;
|
} & Omit<NextAppProps<P>, "pageProps">;
|
||||||
|
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps<ThemeProps>) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
const [themeType, setThemeType] = useSharedState<string>('theme', Cookies.get('drift-theme') || 'light')
|
const { theme } = useTheme()
|
||||||
|
const skeletonBaseColor = 'var(--light-gray)'
|
||||||
useEffect(() => {
|
const skeletonHighlightColor = 'var(--lighter-gray)'
|
||||||
const storedTheme = Cookies.get('drift-theme')
|
|
||||||
if (storedTheme) setThemeType(storedTheme)
|
|
||||||
// TODO: useReducer?
|
|
||||||
}, [setThemeType, themeType])
|
|
||||||
|
|
||||||
const changeTheme = () => {
|
|
||||||
const newTheme = themeType === 'dark' ? 'light' : 'dark'
|
|
||||||
localStorage.setItem('drift-theme', newTheme)
|
|
||||||
setThemeType(last => (last === 'dark' ? 'light' : 'dark'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const skeletonBaseColor = useMemo(() => {
|
|
||||||
if (themeType === 'dark') return '#333'
|
|
||||||
return '#eee'
|
|
||||||
}, [themeType])
|
|
||||||
const skeletonHighlightColor = useMemo(() => {
|
|
||||||
if (themeType === 'dark') return '#555'
|
|
||||||
return '#ddd'
|
|
||||||
}, [themeType])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -58,10 +33,10 @@ function MyApp({ Component, pageProps }: AppProps<ThemeProps>) {
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<title>Drift</title>
|
<title>Drift</title>
|
||||||
</Head>
|
</Head>
|
||||||
<GeistProvider themeType={themeType} >
|
<GeistProvider themeType={theme} >
|
||||||
<SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}>
|
<SkeletonTheme baseColor={skeletonBaseColor} highlightColor={skeletonHighlightColor}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Component {...pageProps} theme={themeType || 'light'} changeTheme={changeTheme} />
|
<Component {...pageProps} />
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
</GeistProvider>
|
</GeistProvider>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { CssBaseline } from '@geist-ui/core'
|
||||||
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
|
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
|
||||||
import CssBaseline from '@geist-ui/core/dist/css-baseline'
|
|
||||||
|
|
||||||
class MyDocument extends Document {
|
class MyDocument extends Document {
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
|
|
|
@ -12,17 +12,16 @@ const renderMarkdown: NextApiHandler = async (req, res) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const json = await file.json()
|
const json = await file.json()
|
||||||
const { content, title } = json
|
const { content, title } = json
|
||||||
const renderAsMarkdown = ['m', 'markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
|
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
|
||||||
const fileType = () => {
|
const fileType = () => {
|
||||||
const pathParts = title.split(".")
|
const pathParts = title.split(".")
|
||||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||||
return language
|
return language
|
||||||
}
|
}
|
||||||
const type = fileType()
|
const type = fileType()
|
||||||
let contentToRender: string = content;
|
let contentToRender: string = '\n' + content;
|
||||||
|
|
||||||
if (!renderAsMarkdown.includes(type)) {
|
if (!renderAsMarkdown.includes(type)) {
|
||||||
contentToRender = `~~~${type}
|
contentToRender = `~~~${type}
|
30
client/pages/api/render-markdown.ts
Normal file
30
client/pages/api/render-markdown.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import type { NextApiHandler } from "next";
|
||||||
|
|
||||||
|
import markdown from "@lib/render-markdown";
|
||||||
|
|
||||||
|
const renderMarkdown: NextApiHandler = async (req, res) => {
|
||||||
|
const { content, title } = req.body
|
||||||
|
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']
|
||||||
|
const fileType = () => {
|
||||||
|
const pathParts = title.split(".")
|
||||||
|
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
const type = fileType()
|
||||||
|
let contentToRender: string = '\n' + (content || '');
|
||||||
|
|
||||||
|
if (!renderAsMarkdown.includes(type)) {
|
||||||
|
contentToRender = `~~~${type}
|
||||||
|
${content}
|
||||||
|
~~~`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof contentToRender !== 'string') {
|
||||||
|
res.status(400).send('content must be a string')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(200).write(markdown(contentToRender))
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default renderMarkdown
|
|
@ -1,13 +1,10 @@
|
||||||
import styles from '@styles/Home.module.css'
|
import styles from '@styles/Home.module.css'
|
||||||
import Page from '@geist-ui/core/dist/page'
|
|
||||||
import Spacer from '@geist-ui/core/dist/spacer'
|
|
||||||
import Text from '@geist-ui/core/dist/text'
|
|
||||||
import Header from '@components/header'
|
import Header from '@components/header'
|
||||||
import Document from '@components/document'
|
import Document from '@components/document'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import ShiftBy from '@components/shift-by'
|
import ShiftBy from '@components/shift-by'
|
||||||
import PageSeo from '@components/page-seo'
|
import PageSeo from '@components/page-seo'
|
||||||
import { ThemeProps } from '@lib/types'
|
import { Page, Text, Spacer } from '@geist-ui/core'
|
||||||
|
|
||||||
export function getStaticProps() {
|
export function getStaticProps() {
|
||||||
const introDoc = process.env.WELCOME_CONTENT
|
const introDoc = process.env.WELCOME_CONTENT
|
||||||
|
@ -19,19 +16,19 @@ export function getStaticProps() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = ThemeProps & {
|
type Props = {
|
||||||
introContent: string
|
introContent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Home = ({ theme, changeTheme, introContent }: Props) => {
|
const Home = ({ introContent }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Page className={styles.container} width="100%">
|
<Page className={styles.container}>
|
||||||
<PageSeo />
|
<PageSeo />
|
||||||
|
|
||||||
<Page.Header>
|
<Page.Header>
|
||||||
<Header theme={theme} changeTheme={changeTheme} />
|
<Header />
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
<Page.Content width={"var(--main-content-width)"} margin="auto" paddingTop={"var(--gap)"}>
|
<Page.Content className={styles.main}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<ShiftBy y={-2}><Image src={'/assets/logo-optimized.svg'} width={'48px'} height={'48px'} alt="" /></ShiftBy>
|
<ShiftBy y={-2}><Image src={'/assets/logo-optimized.svg'} width={'48px'} height={'48px'} alt="" /></ShiftBy>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import styles from '@styles/Home.module.css'
|
import styles from '@styles/Home.module.css'
|
||||||
import Page from '@geist-ui/core/dist/page'
|
|
||||||
|
|
||||||
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 cookie from "cookie";
|
||||||
import type { GetServerSideProps } from 'next';
|
import type { GetServerSideProps } from 'next';
|
||||||
import type { ThemeProps } from '@lib/types';
|
import { Post } from '@lib/types';
|
||||||
|
import { Page } from '@geist-ui/core';
|
||||||
|
|
||||||
const Home = ({ posts, error, theme, changeTheme }: ThemeProps & { posts: any; error: any; }) => {
|
const Home = ({ posts, error }: { posts: Post[]; error: any; }) => {
|
||||||
return (
|
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 error={error} posts={posts} />
|
<MyPosts error={error} posts={posts} />
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page >
|
</Page >
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
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/dist/page'
|
|
||||||
import Header from '@components/header'
|
import Header from '@components/header'
|
||||||
import PageSeo from '@components/page-seo'
|
import PageSeo from '@components/page-seo'
|
||||||
import type { ThemeProps } from '@lib/types'
|
import { Page } from '@geist-ui/core'
|
||||||
|
|
||||||
const New = ({ theme, changeTheme }: ThemeProps) => {
|
const New = () => {
|
||||||
return (
|
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}>
|
||||||
<NewPost />
|
<NewPost />
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page >
|
</Page >
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||||
|
|
||||||
import type { Post, ThemeProps } from "@lib/types";
|
import type { Post } from "@lib/types";
|
||||||
import PostPage from "@components/post-page";
|
import PostPage from "@components/post-page";
|
||||||
|
|
||||||
export type PostProps = ThemeProps & {
|
export type PostProps = {
|
||||||
post: Post
|
post: Post
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostView = ({ post, theme, changeTheme }: PostProps) => {
|
const PostView = ({ post }: PostProps) => {
|
||||||
return <PostPage post={post} theme={theme} changeTheme={changeTheme} />
|
return <PostPage post={post} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
|
|
|
@ -1,28 +1,14 @@
|
||||||
import cookie from "cookie";
|
import cookie from "cookie";
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import { PostVisibility, ThemeProps } from "@lib/types";
|
import { Post } from "@lib/types";
|
||||||
import PostPage from "@components/post-page";
|
import PostPage from "@components/post-page";
|
||||||
|
|
||||||
type File = {
|
export type PostProps = {
|
||||||
id: string
|
post: Post
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Files = File[]
|
const Post = ({ post, }: PostProps) => {
|
||||||
|
return (<PostPage post={post} />)
|
||||||
export type PostProps = ThemeProps & {
|
|
||||||
post: {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
visibility: PostVisibility
|
|
||||||
files: Files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Post = ({ post, theme, changeTheme }: PostProps) => {
|
|
||||||
return (<PostPage post={post} changeTheme={changeTheme} theme={theme} />)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import useToasts from "@geist-ui/core/dist/use-toasts";
|
import { Page, useToasts } from '@geist-ui/core';
|
||||||
import Page from "@geist-ui/core/dist/page";
|
|
||||||
|
|
||||||
import type { Post, ThemeProps } from "@lib/types";
|
import type { Post } from "@lib/types";
|
||||||
import PasswordModal from "@components/new-post/password";
|
import PasswordModal from "@components/new-post/password";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import PostPage from "@components/post-page";
|
import PostPage from "@components/post-page";
|
||||||
|
|
||||||
const Post = ({ theme, changeTheme }: ThemeProps) => {
|
const Post = () => {
|
||||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true);
|
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true);
|
||||||
const [post, setPost] = useState<Post>()
|
const [post, setPost] = useState<Post>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -74,7 +73,7 @@ const Post = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
return <Page><PasswordModal creating={false} onClose={onClose} onSubmit={onSubmit} isOpen={isPasswordModalOpen} /></Page>
|
return <Page><PasswordModal creating={false} onClose={onClose} onSubmit={onSubmit} isOpen={isPasswordModalOpen} /></Page>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<PostPage post={post} changeTheme={changeTheme} theme={theme} />)
|
return (<PostPage post={post} />)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Post
|
export default Post
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import Page from "@geist-ui/core/dist/page";
|
import { Page } from '@geist-ui/core';
|
||||||
import PageSeo from "@components/page-seo";
|
import PageSeo from "@components/page-seo";
|
||||||
import Auth from "@components/auth";
|
import Auth from "@components/auth";
|
||||||
import Header from "@components/header";
|
import Header from "@components/header/header";
|
||||||
import type { ThemeProps } from "@lib/types";
|
import styles from '@styles/Home.module.css'
|
||||||
|
const SignIn = () => (
|
||||||
const SignIn = ({ theme, changeTheme }: ThemeProps) => (
|
|
||||||
<Page width={"100%"}>
|
<Page width={"100%"}>
|
||||||
<PageSeo title="Drift - Sign In" />
|
<PageSeo title="Drift - Sign In" />
|
||||||
|
|
||||||
<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="auto">
|
<Page.Content className={styles.main}>
|
||||||
<Auth page="signin" />
|
<Auth page="signin" />
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import Page from "@geist-ui/core/dist/page";
|
import { Page } from '@geist-ui/core';
|
||||||
import Auth from "@components/auth";
|
import Auth from "@components/auth";
|
||||||
import Header from "@components/header";
|
import Header from "@components/header/header";
|
||||||
import PageSeo from '@components/page-seo';
|
import PageSeo from '@components/page-seo';
|
||||||
import type { ThemeProps } from "@lib/types";
|
import styles from '@styles/Home.module.css'
|
||||||
|
|
||||||
const SignUp = ({ theme, changeTheme }: ThemeProps) => (
|
const SignUp = () => (
|
||||||
<Page width="100%">
|
<Page width="100%">
|
||||||
<PageSeo title="Drift - Sign Up" />
|
<PageSeo title="Drift - Sign Up" />
|
||||||
|
|
||||||
<Page.Header>
|
<Page.Header>
|
||||||
<Header theme={theme} changeTheme={changeTheme} />
|
<Header />
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
<Page.Content width={"var(--main-content-width)"} paddingTop={"var(--gap)"} margin="auto">
|
<Page.Content className={styles.main}>
|
||||||
<Auth page="signup" />
|
<Auth page="signup" />
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -25,29 +25,7 @@
|
||||||
--transition: 0.1s ease-in-out;
|
--transition: 0.1s ease-in-out;
|
||||||
--transition-slow: 0.3s ease-in-out;
|
--transition-slow: 0.3s ease-in-out;
|
||||||
|
|
||||||
/* Dark Mode Colors */
|
--page-nav-height: 64px;
|
||||||
--bg: #131415;
|
|
||||||
--fg: #fafbfc;
|
|
||||||
--gray: #666;
|
|
||||||
--light-gray: #444;
|
|
||||||
--lighter-gray: #222;
|
|
||||||
--lightest-gray: #1a1a1a;
|
|
||||||
--article-color: #eaeaea;
|
|
||||||
--header-bg: rgba(19, 20, 21, 0.45);
|
|
||||||
--gray-alpha: rgba(255, 255, 255, 0.5);
|
|
||||||
--selection: rgba(255, 255, 255, 0.99);
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
--input-height: 2.5rem;
|
|
||||||
--input-border: 1px solid var(--light-gray);
|
|
||||||
--input-border-focus: 1px solid var(--gray);
|
|
||||||
--input-border-error: 1px solid var(--red);
|
|
||||||
--input-bg: var(--bg);
|
|
||||||
--input-fg: var(--fg);
|
|
||||||
--input-placeholder-fg: var(--light-gray);
|
|
||||||
--input-bg-hover: var(--lightest-gray);
|
|
||||||
|
|
||||||
/* Syntax Highlighting */
|
|
||||||
--token: #999;
|
--token: #999;
|
||||||
--comment: #999;
|
--comment: #999;
|
||||||
--keyword: #fff;
|
--keyword: #fff;
|
||||||
|
@ -56,17 +34,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
--bg: #fff;
|
|
||||||
--fg: #000;
|
|
||||||
--gray: #888;
|
|
||||||
--light-gray: #dedede;
|
|
||||||
--lighter-gray: #f5f5f5;
|
|
||||||
--lightest-gray: #fafafa;
|
|
||||||
--article-color: #212121;
|
|
||||||
--header-bg: rgba(255, 255, 255, 0.8);
|
|
||||||
--gray-alpha: rgba(19, 20, 21, 0.5);
|
|
||||||
--selection: rgba(0, 0, 0, 0.99);
|
|
||||||
|
|
||||||
--token: #666;
|
--token: #666;
|
||||||
--comment: #999;
|
--comment: #999;
|
||||||
--keyword: #000;
|
--keyword: #000;
|
||||||
|
@ -81,11 +48,6 @@
|
||||||
::selection {
|
::selection {
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
background: var(--selection);
|
background: var(--selection);
|
||||||
color: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
@ -93,8 +55,6 @@ body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
@ -113,44 +73,6 @@ li {
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.25;
|
|
||||||
letter-spacing: -0.89px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
letter-spacing: -0.69px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
letter-spacing: -0.47px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
letter-spacing: -0.33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--light-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -158,52 +80,16 @@ blockquote {
|
||||||
border-left: 3px solid var(--light-gray);
|
border-left: 3px solid var(--light-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
line-height: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
p a,
|
|
||||||
a.reset {
|
a.reset {
|
||||||
outline: none;
|
outline: none;
|
||||||
color: var(--fg);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
p a:hover,
|
|
||||||
p a:focus,
|
|
||||||
p a:active,
|
|
||||||
a.reset:hover,
|
|
||||||
a.reset:focus {
|
|
||||||
color: var(--gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
pre,
|
pre,
|
||||||
code {
|
code {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clamp {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clamp-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
@ -213,18 +99,6 @@ kbd {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: var(--lightest-gray);
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
:root {
|
:root {
|
||||||
--bg: #fff;
|
--bg: #fff;
|
||||||
|
|
|
@ -197,6 +197,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||||
|
|
||||||
|
"@types/marked@^4.0.3":
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.3.tgz#2098f4a77adaba9ce881c9e0b6baf29116e5acc4"
|
||||||
|
integrity sha512-HnMWQkLJEf/PnxZIfbm0yGJRRZYYMhb++O9M36UCTA9z53uPvVoSlAwJr3XOpDEryb7Hwl1qAx/MV6YIW1RXxg==
|
||||||
|
|
||||||
"@types/mdast@^3.0.0":
|
"@types/mdast@^3.0.0":
|
||||||
version "3.0.10"
|
version "3.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
|
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
|
||||||
|
@ -1804,6 +1809,11 @@ markdown-table@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c"
|
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c"
|
||||||
integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==
|
integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==
|
||||||
|
|
||||||
|
marked@^4.0.12:
|
||||||
|
version "4.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d"
|
||||||
|
integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==
|
||||||
|
|
||||||
mdast-util-definitions@^5.0.0:
|
mdast-util-definitions@^5.0.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817"
|
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817"
|
||||||
|
@ -2559,6 +2569,11 @@ prelude-ls@^1.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||||
|
|
||||||
|
prism-react-renderer@^1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
|
||||||
|
integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ==
|
||||||
|
|
||||||
prismjs@^1.25.0, prismjs@~1.27.0:
|
prismjs@^1.25.0, prismjs@~1.27.0:
|
||||||
version "1.27.0"
|
version "1.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
|
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
|
||||||
|
|
Loading…
Reference in a new issue