diff options
| author | Jacky Zhao <[email protected]> | 2021-04-11 15:06:48 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2021-04-11 15:06:48 -0700 |
| commit | 82bda5ee85efbd2eae25427a839529d5e230eeaa (patch) | |
| tree | 1f7a88938fd6664a9a048503a5a78d010e3db1e2 /frontend/src | |
| parent | Merge pull request #72 from jackyzha0/no-ip (diff) | |
| parent | readd preset height (diff) | |
| download | ctrl-v-82bda5ee85efbd2eae25427a839529d5e230eeaa.tar.xz ctrl-v-82bda5ee85efbd2eae25427a839529d5e230eeaa.zip | |
Merge pull request #74 from jackyzha0/next-refactor
Diffstat (limited to 'frontend/src')
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: </Bold>{props.lang} - </SpacedText> - <SpacedText> - <Bold>expires: </Bold>{props.expiry} - </SpacedText> + {err ? + <ErrMsg active> {err} </ErrMsg> : + <> + <SpacedText> + <Bold>language: </Bold>{lang} + </SpacedText> + <SpacedText> + <Bold>expires: </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 |