aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.js58
-rw-r--r--frontend/src/components/Common/Button.js2
-rw-r--r--frontend/src/components/Err.js2
-rw-r--r--frontend/src/components/Inputs/Code.js6
-rw-r--r--frontend/src/components/NextHead.js16
-rw-r--r--frontend/src/components/PasteInfo.js40
-rw-r--r--frontend/src/components/hooks/useFetchPaste.js67
-rw-r--r--frontend/src/components/modals/PasteModal.js8
-rw-r--r--frontend/src/components/pages/NewPaste.js150
-rw-r--r--frontend/src/components/pages/Raw.js16
-rw-r--r--frontend/src/components/pages/ViewPaste.js65
-rw-r--r--frontend/src/components/renderers/Code.js11
-rw-r--r--frontend/src/components/renderers/InlineCode.js2
-rw-r--r--frontend/src/components/renderers/Latex.js13
-rw-r--r--frontend/src/components/renderers/Markdown.js35
-rw-r--r--frontend/src/css/index.css150
-rw-r--r--frontend/src/http/resolvePaste.js46
-rw-r--r--frontend/src/http/shared.js (renamed from frontend/src/components/hooks/shared.js)0
-rw-r--r--frontend/src/index.js10
-rw-r--r--frontend/src/pages/[hash].js81
-rw-r--r--frontend/src/pages/_app.js46
-rw-r--r--frontend/src/pages/_document.js30
-rw-r--r--frontend/src/pages/index.js163
-rw-r--r--frontend/src/pages/raw/[hash].js26
-rw-r--r--frontend/src/theme/GlobalStyle.js16
-rw-r--r--frontend/src/theme/ThemeProvider.js5
26 files changed, 513 insertions, 551 deletions
diff --git a/frontend/src/App.js b/frontend/src/App.js
deleted file mode 100644
index 0a47baa..0000000
--- a/frontend/src/App.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import NewPaste from './components/pages/NewPaste'
-import ViewPaste from './components/pages/ViewPaste'
-import styled from 'styled-components'
-import {
- BrowserRouter as Router,
- Switch,
- Route,
- useParams
-} from "react-router-dom";
-import Raw from './components/pages/Raw'
-import ThemeProvider from './theme/ThemeProvider'
-import GlobalStyle from './theme/GlobalStyle'
-import {Watermark} from "./components/Watermark";
-
-const Main = styled.div`
- margin-top: 10vh;
- padding: 0 20vw 30px 20vw;
-`
-
-const GetPasteWithParam = () => {
- let { hash } = useParams();
- return <ViewPaste hash = {hash} />;
-}
-
-const GetRawWithParam = () => {
- let { hash } = useParams();
- return <Raw hash={hash} />;
-}
-
-const App = () => {
- return (
- <ThemeProvider>
- <GlobalStyle />
- <Router>
- <Switch>
- <Route path="/raw/:hash"><GetRawWithParam /></Route>
- <Route>
- <Watermark/>
- <Main id="appElement">
- <Switch>
- <Route path="/:hash">
- <GetPasteWithParam />
- </Route>
- <Route path="/">
- <NewPaste />
- </Route>
- </Switch>
- </Main>
- </Route>
- </Switch>
- </Router>
- </ThemeProvider>
- );
-}
-
-
-export default App;
diff --git a/frontend/src/components/Common/Button.js b/frontend/src/components/Common/Button.js
index 59e148b..d853d3a 100644
--- a/frontend/src/components/Common/Button.js
+++ b/frontend/src/components/Common/Button.js
@@ -6,7 +6,7 @@ const Base = css`
${Rounded}
${ButtonLike}
margin-right: 2em;
- height: calc(16px + 1.6em);
+ height: calc(16px + 1.4em);
cursor: pointer;
`
diff --git a/frontend/src/components/Err.js b/frontend/src/components/Err.js
index c87f6a8..fae0e0d 100644
--- a/frontend/src/components/Err.js
+++ b/frontend/src/components/Err.js
@@ -1,7 +1,7 @@
import React from 'react';
import styled, { css } from 'styled-components'
-const ErrMsg = styled.p`
+export const ErrMsg = styled.p`
display: inline;
font-weight: 700;
color: #ff3333;
diff --git a/frontend/src/components/Inputs/Code.js b/frontend/src/components/Inputs/Code.js
index adb1536..6d1b35c 100644
--- a/frontend/src/components/Inputs/Code.js
+++ b/frontend/src/components/Inputs/Code.js
@@ -6,11 +6,15 @@ import {Highlighter} from "../renderers/Code";
import {CodeLike, Hover} from "../Common/mixins";
const Wrapper = styled.div`
+ display: block;
position: relative;
+ width: calc(100%);
`
+
const EditorWrapper = styled(Editor)`
overflow: visible !important;
-
+ position: relative;
+
& > * {
padding: 0 !important;
width: 100%;
diff --git a/frontend/src/components/NextHead.js b/frontend/src/components/NextHead.js
new file mode 100644
index 0000000..1019f61
--- /dev/null
+++ b/frontend/src/components/NextHead.js
@@ -0,0 +1,16 @@
+import Head from 'next/head'
+
+const NextHead = ({data}) => {
+ const title = data.title || "untitled paste"
+ const description = `${data.content.slice(0, 100)}... expires: ${data.expiry}`
+ return (<Head>
+ <title>ctrl-v | {title}</title>
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={description} />
+ <meta name="twitter:title" content={title} />
+ <meta name="twitter:description" content={description} />
+ <meta name="description" content={description} />
+ </Head>)
+}
+
+export default NextHead \ No newline at end of file
diff --git a/frontend/src/components/PasteInfo.js b/frontend/src/components/PasteInfo.js
index 6ab5b19..1ef69cc 100644
--- a/frontend/src/components/PasteInfo.js
+++ b/frontend/src/components/PasteInfo.js
@@ -1,8 +1,9 @@
import React from 'react';
import styled from 'styled-components'
-import { useHistory } from 'react-router-dom';
import { Theme } from './Inputs'
import {Button} from "./Common/Button";
+import {useRouter} from "next/router";
+import {ErrMsg} from "./Err";
const Bold = styled.span`
font-weight: 700
@@ -27,21 +28,21 @@ const Flex = styled.div`
flex-direction: row;
`
-const PasteInfo = (props) => {
- const history = useHistory();
+const PasteInfo = ({hash, lang, theme, expiry, toggleRenderCallback, isRenderMode, onChange, err}) => {
+ const router = useRouter()
const redirRaw = () => {
- const redirUrl = `/raw/${props.hash}`
- history.push(redirUrl);
+ const redirUrl = `/raw/${hash}`
+ router.push(redirUrl);
}
const renderable = () => {
- const buttonTxt = props.isRenderMode ? 'text' : 'render'
- if (props.lang === 'latex' || props.lang === 'markdown') {
+ const buttonTxt = isRenderMode ? 'text' : 'render'
+ if (lang === 'latex' || lang === 'markdown') {
return (
<ShiftedButton
secondary
type="button"
- onClick={props.toggleRenderCallback}>
+ onClick={toggleRenderCallback}>
{buttonTxt}
</ShiftedButton>
);
@@ -59,20 +60,23 @@ const PasteInfo = (props) => {
</ShiftedButton>
{renderable()}
<Theme
- value={props.theme}
- onChange={props.onChange}
+ value={theme}
+ onChange={onChange}
id="themeInput" />
</Flex>
<StyledDiv>
- <SpacedText>
- <Bold>language:&nbsp;</Bold>{props.lang}
- </SpacedText>
- <SpacedText>
- <Bold>expires:&nbsp;</Bold>{props.expiry}
- </SpacedText>
+ {err ?
+ <ErrMsg active> {err} </ErrMsg> :
+ <>
+ <SpacedText>
+ <Bold>language:&nbsp;</Bold>{lang}
+ </SpacedText>
+ <SpacedText>
+ <Bold>expires:&nbsp;</Bold>{expiry}
+ </SpacedText>
+ </>
+ }
</StyledDiv>
- <br />
- {props.err}
</div>
);
}
diff --git a/frontend/src/components/hooks/useFetchPaste.js b/frontend/src/components/hooks/useFetchPaste.js
deleted file mode 100644
index c394f5b..0000000
--- a/frontend/src/components/hooks/useFetchPaste.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import {useEffect, useState} from 'react'
-import {fetchPaste, fmtDateStr} from './shared'
-import {LANGS} from "../renderers/Code";
-
-export default (id) => {
- const [loading, setLoading] = useState(true)
- const [err, setErr] = useState()
- const [requiresAuth, setRequiresAuth] = useState(false)
- const [validPass, setValidPass] = useState(false)
- const [result, setResult] = useState({
- title: 'fetching paste...',
- content: '',
- language: LANGS.detect,
- expiry: '',
- })
-
- const handleErr = error => {
- const resp = error.response
-
- // network err
- if (!resp) {
- setErr(error.toString())
- return
- }
-
- // password protected
- if (resp.status === 401) {
- setRequiresAuth(true)
- return
- }
-
- // catch all
- const errTxt = `${resp.status}: ${resp.data}`
- setErr(errTxt)
- }
-
- // callback to try verifying with password
- const getWithPassword = (password, errorCallback) => {
- fetchPaste(id, password)
- .then(resp => {
- setValidPass(true)
- setStateFromData(resp.data)
- })
- .catch(e => errorCallback(e.response.data))
- }
-
-
- const setStateFromData = (data) => {
- document.title = data.title
- setResult({
- title: data.title,
- content: data.content,
- language: data.language,
- expiry: fmtDateStr(data.expiry)
- })
- }
-
- // initial fetch
- useEffect(() => {
- fetchPaste(id)
- .then(resp => setStateFromData(resp.data))
- .catch(handleErr)
- .finally(() => setLoading(false))
- }, [id])
-
- return { loading, err, requiresAuth, validPass, getWithPassword, result }
-} \ No newline at end of file
diff --git a/frontend/src/components/modals/PasteModal.js b/frontend/src/components/modals/PasteModal.js
index e7dbed2..a0fd309 100644
--- a/frontend/src/components/modals/PasteModal.js
+++ b/frontend/src/components/modals/PasteModal.js
@@ -1,21 +1,21 @@
import React from 'react';
import Modal from 'react-modal';
import {Form, ModalHeader, modalStyles} from './shared'
-import { useHistory } from 'react-router-dom';
+import { useRouter } from 'next/router'
import { Text } from '../Inputs'
import { useClipboard } from 'use-clipboard-copy';
import {Button} from "../Common/Button";
const PasteModal = (props) => {
- const history = useHistory();
- const fullURL = `${window.location.origin}/${props.hash}`;
+ const fullURL = `https://ctrl-v.app/${props.hash}`;
const clipboard = useClipboard({ copiedTimeout: 3000 });
Modal.setAppElement('body');
+ const router = useRouter()
const redir = (e) => {
e.preventDefault();
const redirUrl = `/${props.hash}`
- history.push(redirUrl);
+ router.push(redirUrl);
}
return (
diff --git a/frontend/src/components/pages/NewPaste.js b/frontend/src/components/pages/NewPaste.js
deleted file mode 100644
index 19161da..0000000
--- a/frontend/src/components/pages/NewPaste.js
+++ /dev/null
@@ -1,150 +0,0 @@
-import React, { useEffect, useState, useRef } from 'react';
-import { Text, Code } from '../Inputs'
-import OptionsContainer from '../Options'
-import Error from '../Err'
-import PasteModal from '../modals/PasteModal'
-import styled from 'styled-components'
-import CodeRenderer from '../renderers/Code'
-import Latex from '../renderers/Latex'
-import Markdown from '../renderers/Markdown'
-import {Button, SubmitButton} from "../Common/Button";
-import {newPaste} from "../hooks/shared";
-
-const Flex = styled.div`
- display: flex;
- flex-direction: row;
-`
-
-const FlexLeft = styled.div`
- flex: 0 0 calc(50% - 1em - 2px);
-`
-
-const FlexRight = styled.div`
- flex: 0 0 50%;
- max-width: calc(50% - 1em + 2px);
- margin-left: 2em;
-`
-
-const PreviewWrapper = styled.div`
- margin: 2em;
-`
-
-const NewPaste = () => {
- const [title, setTitle] = useState('');
- const [content, setContent] = useState('');
- const [pass, setPass] = useState('');
- const [language, setLanguage] = useState('detect');
- const [expiry, setExpiry] = useState('');
- const [hash, setHash] = useState('');
- const [isPreview, setIsPreview] = useState(false);
- const ErrorLabel = useRef(null);
-
- useEffect(() => {
- document.title = title === "" ? `ctrl-v` : `ctrl-v | ${title}`;
- }, [title])
-
- function handleSubmit(e) {
- e.preventDefault();
-
- // prevent resubmission
- if (!hash) {
- newPaste({title, content, language, pass, expiry})
- .then(resp => {setHash(resp.data.hash)})
- .catch((error) => {
- const resp = error.response
-
- // some weird err (e.g. network)
- if (!resp) {
- ErrorLabel.current.showMessage(error)
- return
- }
-
- // some weird err
- const errTxt = `${resp.status}: ${resp.data}`
- ErrorLabel.current.showMessage(errTxt)
- });
- }
- }
-
- function renderPreview() {
- const pasteInput = <Code
- setContentCallback={setContent}
- content={content}
- maxLength="100000" />
-
- if (isPreview) {
- var preview
- switch (language) {
- case 'latex':
- preview =
- <PreviewWrapper>
- <Latex
- content={content} />
- </PreviewWrapper>
- break
- case 'markdown':
- preview =
- <PreviewWrapper className='md' >
- <Markdown
- content={content} />
- </PreviewWrapper>
- break
- default:
- preview =
- <CodeRenderer
- lang={language}
- theme='atom'
- content={content} />
- }
-
- return (
- <Flex>
- <FlexLeft>
- {pasteInput}
- </FlexLeft>
- <FlexRight className='preview' >
- {preview}
- </FlexRight>
- </Flex>
- );
- } else {
- return (
- pasteInput
- );
- }
- }
-
- return (
- <form onSubmit={handleSubmit}>
- <PasteModal hash={hash} />
- <Text
- label="title"
- onChange={(e) => {setTitle(e.target.value)}}
- value={title}
- autoFocus
- maxLength="100"
- id="titleInput" />
- {renderPreview()}
- <OptionsContainer
- pass={pass}
- expiry={expiry}
- lang={language}
- onPassChange={(e) => { setPass(e.target.value) }}
- onLangChange={(e) => { setLanguage(e.target.value) }}
- onExpiryChange={(e) => { setExpiry(e.target.value) }} />
- <div>
- <SubmitButton type="submit" value="new paste" />
- {language !== 'detect' && <Button
- secondary
- type="button"
- onClick={() => setIsPreview(!isPreview)}>
- preview
- </Button>}
- </div>
- <br />
- <Error ref={ErrorLabel} />
- </form>
- );
-}
-
-export default NewPaste \ No newline at end of file
diff --git a/frontend/src/components/pages/Raw.js b/frontend/src/components/pages/Raw.js
deleted file mode 100644
index 23ef6bf..0000000
--- a/frontend/src/components/pages/Raw.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-import styled from 'styled-components'
-import {CodeLike} from "../Common/mixins";
-import useFetchPaste from "../hooks/useFetchPaste";
-
-const RawText = styled.pre`
- ${CodeLike}
- padding: 0 1em;
-`
-
-const Raw = ({hash}) => {
- const { err, result } = useFetchPaste(hash)
- return <RawText>{result?.content || err}</RawText>
-}
-
-export default Raw \ No newline at end of file
diff --git a/frontend/src/components/pages/ViewPaste.js b/frontend/src/components/pages/ViewPaste.js
deleted file mode 100644
index bc61314..0000000
--- a/frontend/src/components/pages/ViewPaste.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { useEffect, useState, useRef } from 'react';
-import Error from '../Err';
-import { Text } from '../Inputs';
-import CodeRenderer from '../renderers/Code'
-import PasteInfo from '../PasteInfo';
-import PasswordModal from '../modals/PasswordModal'
-import RenderDispatch from '../renderers/RenderDispatch'
-import useFetchPaste from "../hooks/useFetchPaste";
-
-const ViewPaste = (props) => {
- const { err, requiresAuth, validPass, getWithPassword, result } = useFetchPaste(props.hash)
- const {content, language, expiry, title} = result ?? {}
- const [theme, setTheme] = useState('atom');
- const [isRenderMode, setIsRenderMode] = useState(false);
- const [enteredPass, setEnteredPass] = useState('');
- const ErrorLabelRef = useRef(null);
-
- if (err) {
- ErrorLabelRef.current.showMessage(err, -1)
- }
-
- useEffect(() => {
- setIsRenderMode(language === 'latex' || language === 'markdown')
- }, [language])
-
- function getDisplay() {
- return isRenderMode ? <RenderDispatch
- language={language}
- content={content}
- /> : <CodeRenderer
- content={content}
- lang={language}
- theme={theme}
- id="pasteInput" />
- }
-
- return (
- <div>
- <PasswordModal
- hasPass={requiresAuth}
- validPass={validPass}
- value={enteredPass}
- onChange={(e) => setEnteredPass(e.target.value)}
- validateCallback={getWithPassword} />
- <Text
- label="title"
- value={title}
- id="titleInput"
- readOnly />
- {getDisplay()}
- <PasteInfo
- hash={props.hash}
- lang={language}
- theme={theme}
- expiry={expiry}
- toggleRenderCallback={() => setIsRenderMode(!isRenderMode)}
- isRenderMode={isRenderMode}
- onChange={(e) => setTheme(e.target.value)}
- err={<Error ref={ErrorLabelRef} />}
- />
- </div>
- );
-}
-
-export default ViewPaste \ No newline at end of file
diff --git a/frontend/src/components/renderers/Code.js b/frontend/src/components/renderers/Code.js
index 29531fc..c932add 100644
--- a/frontend/src/components/renderers/Code.js
+++ b/frontend/src/components/renderers/Code.js
@@ -1,7 +1,7 @@
import React from 'react';
import SyntaxHighlighter from 'react-syntax-highlighter';
import virtualizedRenderer from 'react-syntax-highlighter-virtualized-renderer';
-import { atomOneLight, ascetic, atomOneDark, dracula, ocean } from 'react-syntax-highlighter/dist/esm/styles/hljs';
+import { atomOneLight, ascetic, atomOneDark, dracula, ocean } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import styled from 'styled-components'
import {Border, CodeLike, DropShadow, Rounded} from "../Common/mixins";
@@ -40,11 +40,18 @@ export const StyledPre = styled.pre`
}
`
+const PreWithBr = (props) => (
+ <StyledPre {...props}>
+ {props.children}
+ <br />
+ </StyledPre>
+)
+
export const Highlighter = ({language, lineNumbers, theme, pre = StyledPre, children}) => <SyntaxHighlighter
language={LANGS[language]}
style={THEMES[theme]}
showLineNumbers={lineNumbers}
- PreTag={pre}>
+ PreTag={PreWithBr}>
{children}
</SyntaxHighlighter>
diff --git a/frontend/src/components/renderers/InlineCode.js b/frontend/src/components/renderers/InlineCode.js
index 44a3f58..2156503 100644
--- a/frontend/src/components/renderers/InlineCode.js
+++ b/frontend/src/components/renderers/InlineCode.js
@@ -1,6 +1,6 @@
import React from 'react';
import SyntaxHighlighter from 'react-syntax-highlighter';
-import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs';
+import { atomOneDark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import { LANGS } from './Code'
const MarkdownCodeRenderer = ({language, value}) => {
diff --git a/frontend/src/components/renderers/Latex.js b/frontend/src/components/renderers/Latex.js
index fd3d4e2..b2be6da 100644
--- a/frontend/src/components/renderers/Latex.js
+++ b/frontend/src/components/renderers/Latex.js
@@ -40,4 +40,15 @@ const Latex = (props) => {
}
}
-export default Latex \ No newline at end of file
+const StyledLatex = styled(Latex)`
+ /* fix weird symbol height in render mode */
+ .large-op {
+ transform: translateY(-0.55em);
+ }
+
+ .small-op {
+ transform: translateY(-0.1em);
+ }
+`
+
+export default StyledLatex \ No newline at end of file
diff --git a/frontend/src/components/renderers/Markdown.js b/frontend/src/components/renderers/Markdown.js
index 16f5e37..f69d176 100644
--- a/frontend/src/components/renderers/Markdown.js
+++ b/frontend/src/components/renderers/Markdown.js
@@ -9,6 +9,41 @@ const Content = styled.div`
img {
max-width: 100%;
}
+
+ h3 {
+ font-weight: bold;
+ }
+
+ hr {
+ border-top: 1px solid ${p => p.theme.colors.text};
+ border-style: solid;
+ }
+
+ code {
+ background: ${p => p.theme.colors.codeHighlight};
+ font-size: 0.8em;
+ }
+
+ pre {
+ padding: 0.7em;
+ background: ${p => p.theme.colors.codeHighlight};
+ }
+
+ pre > code {
+ background: none;
+ }
+
+ table {
+ width: 100%;
+ }
+
+ code, pre {
+ background: none;
+ font-family: 'JetBrains Mono', monospace;
+ padding: initial;
+ border-radius: 3px;
+ outline: none;
+ }
`
const Markdown = ({content}) => {
return <Content>
diff --git a/frontend/src/css/index.css b/frontend/src/css/index.css
deleted file mode 100644
index 0340813..0000000
--- a/frontend/src/css/index.css
+++ /dev/null
@@ -1,150 +0,0 @@
-@media all and (max-width: 1000px) {
- .lt-content-column {
- padding: 0 calc(5vw + 1em) 0 5vw !important;
- }
-}
-
-form {
- width: 100%;
-}
-
-textarea, input[type=text], input[type=password], .Dropdown-root {
- width: 100%;
- font-family: 'JetBrains Mono', monospace;
- font-size: 0.8em;
- padding: calc(0.8em - 1px);
- border-radius: 3px;
- border: 1px solid #565656;
- outline: none;
- margin: 1.7em 0;
-}
-
-/* fix weird symbol height in render mode */
-.large-op {
- transform: translateY(-0.55em);
-}
-
-.small-op {
- transform: translateY(-0.1em);
-}
-
-code, pre {
- background: #00000000;
- font-family: 'JetBrains Mono', monospace;
- padding: initial;
- border-radius: 3px;
- outline: none;
-}
-
-.Dropdown-root {
- cursor: pointer;
-}
-
-.Dropdown-root:hover, .Dropdown-root.is-open {
- opacity: 1;
-}
-
-.Dropdown-root + label {
- top: 0.5em;
- opacity: 0;
-}
-
-.Dropdown-root.is-open + label, .Dropdown-root:hover + label {
- opacity: 1;
- top: -0.1em;
-}
-
-.Dropdown-placeholder {
- width: 5.5em;
-}
-
-.Dropdown-menu {
- border-top: 1px solid #111111;
- margin-top: 0.5em;
- bottom: auto;
-}
-
-.Dropdown-option {
- margin-top: 0.5em;
- transition: all 0.5s cubic-bezier(.25,.8,.25,1);
-}
-
-.Dropdown-option:hover {
- font-weight: 700;
- opacity: 0.4;
-}
-
-textarea, input[type=text], input[type=password], .Dropdown-root {
- opacity: 0.5;
- transition: opacity 0.5s cubic-bezier(.25,.8,.25,1);
-}
-
-textarea:focus, input[type=text]:focus, input[type=password]:focus {
- opacity: 1;
-}
-
-input[type=password] {
- font-weight: 700;
-}
-
-textarea {
- height: max(40vh, 100%);
- resize: vertical;
- min-height: 40vh;
-}
-
-a {
- color: #111111;
-}
-
-input[type=submit], button[type=submit] {
- font-family: 'JetBrains Mono', serif;
- font-weight: 700;
- color: #faf9f5;
- background-color: #111111;
- padding: 0.8em 2em;
- margin: 2em 0;
- outline: 0;
-}
-
-button[type=button] {
- font-family: 'JetBrains Mono', serif;
- font-weight: 700;
- width: 8em;
- padding: calc(0.8em - 1px) 1.5em;
- border-radius: 3px;
- color: #111111;
- background-color: #faf9f5;
- border: 1px solid #565656;
- outline: none;
- margin: 2em 2em;
-}
-
-
-/* fixing markdown renderer */
-.md h3 {
- font-weight: bold;
-}
-
-.md hr {
- border-top: 1px solid #000;
- border-style: solid;
-}
-
-.md code {
- background: #00000008;
- font-size: 0.8em;
-}
-
-.md pre {
- padding: 0.7em;
- background: #00000008;
-}
-
-.md pre > code {
- background: none;
-}
-
-.md table {
- width: 100%;
-} \ No newline at end of file
diff --git a/frontend/src/http/resolvePaste.js b/frontend/src/http/resolvePaste.js
new file mode 100644
index 0000000..aa4f8b6
--- /dev/null
+++ b/frontend/src/http/resolvePaste.js
@@ -0,0 +1,46 @@
+import {useEffect, useState} from 'react'
+import {fetchPaste, fmtDateStr} from './shared'
+import {LANGS} from "../components/renderers/Code";
+
+export const defaultResponse = {
+ data: {
+ title: '',
+ content: '',
+ language: LANGS.detect,
+ expiry: '',
+ },
+ unauthorized: false,
+ error: '',
+}
+
+const resolvePaste = async (id, password = "") => {
+ const response = {...defaultResponse}
+ try {
+ return await fetchPaste(id, password)
+ .then(resp => {
+ const data = resp.data
+ response.data = {
+ ...data,
+ expiry: fmtDateStr(data.expiry)
+ }
+ return response
+ })
+ } catch (err) {
+ const resp = err.response
+ if (!resp) {
+ response.error = 'network error'
+ return response
+ }
+
+ if (resp.status === 401) {
+ response.error = 'unauthorized'
+ response.unauthorized = true
+ return response
+ }
+
+ response.error = `${resp.status}: ${resp.data}`
+ return response
+ }
+}
+
+export default resolvePaste \ No newline at end of file
diff --git a/frontend/src/components/hooks/shared.js b/frontend/src/http/shared.js
index 00d41e9..00d41e9 100644
--- a/frontend/src/components/hooks/shared.js
+++ b/frontend/src/http/shared.js
diff --git a/frontend/src/index.js b/frontend/src/index.js
deleted file mode 100644
index 7173ce5..0000000
--- a/frontend/src/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import App from './App';
-
-ReactDOM.render(
- <React.StrictMode>
- <App />
- </React.StrictMode>,
- document.getElementById('root')
-); \ No newline at end of file
diff --git a/frontend/src/pages/[hash].js b/frontend/src/pages/[hash].js
new file mode 100644
index 0000000..f281621
--- /dev/null
+++ b/frontend/src/pages/[hash].js
@@ -0,0 +1,81 @@
+import React, { useEffect, useState } from 'react';
+import { Text } from '../components/Inputs';
+import CodeRenderer from '../components/renderers/Code'
+import PasteInfo from '../components/PasteInfo';
+import PasswordModal from '../components/modals/PasswordModal'
+import RenderDispatch from '../components/renderers/RenderDispatch'
+import {Watermark} from "../components/Watermark";
+import { useRouter } from 'next/router'
+import resolvePaste from "../http/resolvePaste";
+import NextHead from "../components/NextHead";
+
+export async function getServerSideProps(ctx) {
+ const data = await resolvePaste(ctx.params.hash)
+ return { props: { ...data } }
+}
+
+const ViewPaste = ({data, unauthorized, error}) => {
+ const router = useRouter()
+ const { hash } = router.query
+ const [theme, setTheme] = useState('atom');
+ const [isRenderMode, setIsRenderMode] = useState(false);
+ const [enteredPass, setEnteredPass] = useState('');
+ const [correctPass, setCorrectPass] = useState(!unauthorized);
+ const [clientData, setClientData] = useState(data)
+ const {content, language, expiry, title} = clientData;
+
+ const getWithPassword = (password, errorCallback) => {
+ resolvePaste(hash, password)
+ .then(resp => {
+ setCorrectPass(true)
+ setClientData(resp.data)
+ })
+ .catch(e => errorCallback(e.response.data))
+ }
+
+ useEffect(() => {
+ setIsRenderMode(language === 'latex' || language === 'markdown')
+ }, [language])
+
+ function getDisplay() {
+ return isRenderMode ? <RenderDispatch
+ language={language}
+ content={content}
+ /> : <CodeRenderer
+ content={content}
+ lang={language}
+ theme={theme}
+ id="pasteInput" />
+ }
+
+ return (
+ <div>
+ {!error && <NextHead data={data} />}
+ <PasswordModal
+ hasPass={unauthorized}
+ validPass={correctPass}
+ value={enteredPass}
+ onChange={(e) => setEnteredPass(e.target.value)}
+ validateCallback={getWithPassword} />
+ <Text
+ label="title"
+ value={title}
+ id="titleInput"
+ readOnly />
+ {getDisplay()}
+ <PasteInfo
+ hash={hash}
+ lang={language}
+ theme={theme}
+ expiry={expiry}
+ toggleRenderCallback={() => setIsRenderMode(!isRenderMode)}
+ isRenderMode={isRenderMode}
+ onChange={(e) => setTheme(e.target.value)}
+ err={unauthorized ? '' : error}
+ />
+ <Watermark/>
+ </div>
+ );
+}
+
+export default ViewPaste \ No newline at end of file
diff --git a/frontend/src/pages/_app.js b/frontend/src/pages/_app.js
new file mode 100644
index 0000000..115f47f
--- /dev/null
+++ b/frontend/src/pages/_app.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import ThemeProvider from "../theme/ThemeProvider";
+import GlobalStyle from "../theme/GlobalStyle";
+import styled from "styled-components";
+import Head from "next/head";
+
+const Main = styled.div`
+ margin-top: 10vh;
+ padding: 0 20vw 30px 20vw;
+`
+
+const App = ({ Component, pageProps }) => (
+ <ThemeProvider>
+ <GlobalStyle />
+ <Head>
+ <meta charSet="utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
+ <meta name="theme-color" content="#ffffff"/>
+ <meta
+ name="description"
+ content="a modern, open-source pastebin with latex and markdown rendering support"
+ />
+ <link rel="icon" href="/favicon.png" />
+ <link rel="preconnect" href="https://fonts.gstatic.com" />
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;700&display=swap"
+ rel="stylesheet" />
+ <title>ctrl-v | a modern, open-source pastebin</title>
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-DE1TYY2F24" />
+ <script
+ dangerouslySetInnerHTML={{
+ __html: `
+ window.dataLayer = window.dataLayer || [];
+ function gtag() {dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DE1TYY2F24');
+ `
+ }}
+ />
+ </Head>
+ <Main id="appElement">
+ <Component {...pageProps} />
+ </Main>
+ </ThemeProvider>
+)
+
+export default App \ No newline at end of file
diff --git a/frontend/src/pages/_document.js b/frontend/src/pages/_document.js
new file mode 100644
index 0000000..0cbd6a3
--- /dev/null
+++ b/frontend/src/pages/_document.js
@@ -0,0 +1,30 @@
+import Document from 'next/document'
+import { ServerStyleSheet } from 'styled-components'
+
+export default class StyledDocument extends Document {
+ static async getInitialProps(ctx) {
+ const sheet = new ServerStyleSheet()
+ const originalRenderPage = ctx.renderPage
+
+ try {
+ ctx.renderPage = () =>
+ originalRenderPage({
+ enhanceApp: (App) => (props) =>
+ sheet.collectStyles(<App {...props} />),
+ })
+
+ const initialProps = await Document.getInitialProps(ctx)
+ return {
+ ...initialProps,
+ styles: (
+ <>
+ {initialProps.styles}
+ {sheet.getStyleElement()}
+ </>
+ ),
+ }
+ } finally {
+ sheet.seal()
+ }
+ }
+} \ No newline at end of file
diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.js
new file mode 100644
index 0000000..867a074
--- /dev/null
+++ b/frontend/src/pages/index.js
@@ -0,0 +1,163 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Text, Code } from '../components/Inputs'
+import OptionsContainer from '../components/Options'
+import Error from '../components/Err'
+import PasteModal from '../components/modals/PasteModal'
+import styled from 'styled-components'
+import CodeRenderer from '../components/renderers/Code'
+import Latex from '../components/renderers/Latex'
+import Markdown from '../components/renderers/Markdown'
+import {Button, SubmitButton} from "../components/Common/Button";
+import {newPaste} from "../http/shared";
+import {Watermark} from "../components/Watermark";
+import {Labelled} from "../components/decorators/Labelled";
+
+const Container = styled.form`
+ width: 100%;
+`
+
+const Flex = styled.div`
+ display: flex;
+ flex-direction: row;
+`
+
+const FlexLeft = styled.div`
+ flex: 0 0 calc(50% - 1em - 2px);
+`
+
+const FlexRight = styled.div`
+ flex: 0 0 50%;
+ max-width: calc(50% - 1em + 2px);
+ margin-left: 2em;
+`
+
+const PreviewWrapper = styled.div`
+ margin: 2em;
+`
+
+const NewPaste = () => {
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [pass, setPass] = useState('');
+ const [language, setLanguage] = useState('detect');
+ const [expiry, setExpiry] = useState('');
+ const [hash, setHash] = useState('');
+ const [isPreview, setIsPreview] = useState(false);
+ const ErrorLabel = useRef(null);
+
+ useEffect(() => {
+ document.title = title === "" ? `ctrl-v` : `ctrl-v | ${title}`;
+ }, [title])
+
+ function handleSubmit(e) {
+ e.preventDefault();
+
+ // prevent resubmission
+ if (!hash) {
+ newPaste({title, content, language, pass, expiry})
+ .then(resp => {setHash(resp.data.hash)})
+ .catch((error) => {
+ const resp = error.response
+
+ // some weird err (e.g. network)
+ if (!resp) {
+ ErrorLabel.current.showMessage(error)
+ return
+ }
+
+ // some weird err
+ const errTxt = `${resp.status}: ${resp.data}`
+ ErrorLabel.current.showMessage(errTxt)
+ });
+ }
+ }
+
+ function renderPreview() {
+ const pasteInput = <Code
+ setContentCallback={setContent}
+ content={content}
+ maxLength="100000" />
+
+ if (isPreview) {
+ var preview
+ switch (language) {
+ case 'latex':
+ preview =
+ <PreviewWrapper>
+ <Latex
+ content={content} />
+ </PreviewWrapper>
+ break
+ case 'markdown':
+ preview =
+ <PreviewWrapper className='md' >
+ <Markdown
+ content={content} />
+ </PreviewWrapper>
+ break
+ default:
+ preview =
+ <CodeRenderer
+ lang={language}
+ theme='atom'
+ content={content} />
+ }
+
+ return (
+ <Flex>
+ <FlexLeft>
+ <Labelled label="content">
+ {pasteInput}
+ </Labelled>
+ </FlexLeft>
+ <FlexRight className='preview' >
+ <Labelled label="preview">
+ {preview}
+ </Labelled>
+ </FlexRight>
+ </Flex>
+ );
+ } else {
+ return (
+ <Labelled label="content">
+ {pasteInput}
+ </Labelled>
+ );
+ }
+ }
+
+ return (
+ <Container onSubmit={handleSubmit}>
+ <PasteModal hash={hash} />
+ <Text
+ label="title"
+ onChange={(e) => {setTitle(e.target.value)}}
+ value={title}
+ autoFocus
+ maxLength="100"
+ id="titleInput" />
+ {renderPreview()}
+ <OptionsContainer
+ pass={pass}
+ expiry={expiry}
+ lang={language}
+ onPassChange={(e) => { setPass(e.target.value) }}
+ onLangChange={(e) => { setLanguage(e.target.value) }}
+ onExpiryChange={(e) => { setExpiry(e.target.value) }} />
+ <div>
+ <SubmitButton type="submit" value="new paste" />
+ {language !== 'detect' && <Button
+ secondary
+ type="button"
+ onClick={() => setIsPreview(!isPreview)}>
+ preview
+ </Button>}
+ </div>
+ <br />
+ <Error ref={ErrorLabel} />
+ <Watermark/>
+ </Container>
+ );
+}
+
+export default NewPaste \ No newline at end of file
diff --git a/frontend/src/pages/raw/[hash].js b/frontend/src/pages/raw/[hash].js
new file mode 100644
index 0000000..9edde36
--- /dev/null
+++ b/frontend/src/pages/raw/[hash].js
@@ -0,0 +1,26 @@
+import React from 'react';
+import resolvePaste from "../../http/resolvePaste";
+import {CodeLike} from "../../components/Common/mixins";
+import styled from 'styled-components'
+import NextHead from "../../components/NextHead";
+
+const RawText = styled.pre`
+ ${CodeLike}
+ padding: 0 1em;
+`
+
+export async function getServerSideProps(ctx) {
+ const data = await resolvePaste(ctx.params.hash)
+ return { props: { ...data } }
+}
+
+const Raw = ({error, data}) => {
+ return <>
+ {!error && <NextHead data={data} />}
+ <RawText>
+ {data?.content || error}
+ </RawText>
+ </>
+}
+
+export default Raw \ No newline at end of file
diff --git a/frontend/src/theme/GlobalStyle.js b/frontend/src/theme/GlobalStyle.js
index 9fe80a5..2942687 100644
--- a/frontend/src/theme/GlobalStyle.js
+++ b/frontend/src/theme/GlobalStyle.js
@@ -2,10 +2,16 @@ import { createGlobalStyle } from 'styled-components'
export default createGlobalStyle`
body {
- margin: 0;
- padding: 0;
- background: ${(p) => p.theme.colors.background};
- font-family: 'JetBrains Mono', monospace;
- color: ${(p) => p.theme.colors.text};
+ margin: 0;
+ padding: 0;
+ background: ${(p) => p.theme.colors.background};
+ font-family: 'JetBrains Mono', monospace;
+ color: ${(p) => p.theme.colors.text};
+ }
+
+ @media all and (max-width: 1000px) {
+ .lt-content-column {
+ padding: 0 calc(5vw + 1em) 0 5vw !important;
+ }
}
` \ No newline at end of file
diff --git a/frontend/src/theme/ThemeProvider.js b/frontend/src/theme/ThemeProvider.js
index d9edcb0..57d57ef 100644
--- a/frontend/src/theme/ThemeProvider.js
+++ b/frontend/src/theme/ThemeProvider.js
@@ -4,10 +4,13 @@ import { ThemeProvider } from 'styled-components'
const theme = {
colors: {
background: '#faf9f5',
+ codeHighlight: '#00000008',
border: '#565656',
text: '#111111',
error: '#ee1111',
},
}
-export default ({ children }) => <ThemeProvider theme={theme}>{children}</ThemeProvider> \ No newline at end of file
+const Provider = ({ children }) => <ThemeProvider theme={theme}>{children}</ThemeProvider>
+
+export default Provider \ No newline at end of file