aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages')
-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
5 files changed, 346 insertions, 0 deletions
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