diff options
| author | Jacky Zhao <[email protected]> | 2021-03-06 17:57:24 -0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2021-03-06 17:57:24 -0800 |
| commit | 5dd02b5c2acd7a4c408ce9ffa1d95e208d20bbc8 (patch) | |
| tree | 5a09bea364331dd0f41510153924065b815b702e /frontend/src | |
| parent | fix(typo): public api docs endpoint (diff) | |
| parent | fix password modal (diff) | |
| download | ctrl-v-5dd02b5c2acd7a4c408ce9ffa1d95e208d20bbc8.tar.xz ctrl-v-5dd02b5c2acd7a4c408ce9ffa1d95e208d20bbc8.zip | |
Merge pull request #70 from jackyzha0/visual-overhaul
Diffstat (limited to 'frontend/src')
31 files changed, 614 insertions, 572 deletions
diff --git a/frontend/src/App.js b/frontend/src/App.js new file mode 100644 index 0000000..a8da469 --- /dev/null +++ b/frontend/src/App.js @@ -0,0 +1,58 @@ +import React from 'react'; +import NewPaste from './components/NewPaste' +import ViewPaste from './components/ViewPaste' +import styled from 'styled-components' +import { + BrowserRouter as Router, + Switch, + Route, + useParams +} from "react-router-dom"; +import Raw from './components/renderers/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/App.js b/frontend/src/components/App.js deleted file mode 100644 index 543cd46..0000000 --- a/frontend/src/components/App.js +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import NewPaste from './NewPaste' -import ViewPaste from './ViewPaste' -import Footer from './Footer' -import styled from 'styled-components' -import { - BrowserRouter as Router, - Switch, - Route, - Link, - useParams -} from "react-router-dom"; -import Raw from './renderers/Raw' - -const SpacedTitle = styled.div` - margin-top: 10vh -` - -const Desc = () => { - return ( - <h3>a modern, <a href="https://github.com/jackyzha0/ctrl-v" target="_blank" rel="noopener noreferrer">open-source</a> pastebin with latex and markdown rendering support</h3> - ); -} - -const GetPasteWithParam = () => { - let { hash } = useParams(); - - return ( - <ViewPaste hash = {hash} /> - ); -} - -const GetRawWithParam = () => { - let { hash } = useParams(); - - return ( - <Raw hash={hash} /> - ); -} - -const App = () => { - return ( - <Router> - <Switch> - <Route path="/raw/:hash" - children={<GetRawWithParam />} - /> - <Route> - <div className="lt-content-column"> - <SpacedTitle> - <nav> - <h1 className="mainLogo"> - <span role="img" aria-label="clipboard">📋 </span> - <Link to="/">ctrl-v</Link> - </h1> - <Desc /> - </nav> - </SpacedTitle> - - <main id="appElement"> - <Switch> - <Route path="/:hash" - children={<GetPasteWithParam />} - /> - <Route path="/"> - <NewPaste /> - </Route> - </Switch> - </main> - - <Footer /> - </div> - </Route> - </Switch> - </Router> - ); -} - - -export default App; diff --git a/frontend/src/components/Common/Button.js b/frontend/src/components/Common/Button.js new file mode 100644 index 0000000..59e148b --- /dev/null +++ b/frontend/src/components/Common/Button.js @@ -0,0 +1,32 @@ +import styled, {css} from 'styled-components' +import {Border, ButtonLike, DropShadow, Rounded} from "./mixins"; + +const Base = css` + ${DropShadow} + ${Rounded} + ${ButtonLike} + margin-right: 2em; + height: calc(16px + 1.6em); + cursor: pointer; +` + +const Primary = css` + ${Base}; + border: none; + color: ${p => p.theme.colors.background}; + background-color: ${p => p.theme.colors.text}; +` +const Secondary = css` + ${Base}; + ${Border}; + color: ${p => p.theme.colors.text}; + background-color: ${p => p.theme.colors.background}; +` + +export const Button = styled.button` + ${p => p.secondary ? css`${Secondary}` : css`${Primary}` } +` + +export const SubmitButton = styled.input` + ${Primary} +`
\ No newline at end of file diff --git a/frontend/src/components/Common/Input.js b/frontend/src/components/Common/Input.js new file mode 100644 index 0000000..e000cfb --- /dev/null +++ b/frontend/src/components/Common/Input.js @@ -0,0 +1,9 @@ +import styled from 'styled-components' +import {Border, DropShadow, InputLike, Rounded} from "./mixins"; + +export const Input = styled.input` + ${Border} + ${Rounded} + ${DropShadow} + ${InputLike} +`
\ No newline at end of file diff --git a/frontend/src/components/Common/mixins.js b/frontend/src/components/Common/mixins.js new file mode 100644 index 0000000..ff2759f --- /dev/null +++ b/frontend/src/components/Common/mixins.js @@ -0,0 +1,52 @@ +import {css} from 'styled-components'; + +export const DropShadow = css` + box-shadow: 0 14px 28px rgba(27, 33, 48,0.06), 0 10px 10px rgba(27, 33, 48,0.02); +` + +export const Hover = css` + opacity: 0.5; + transition: all 0.5s cubic-bezier(.25,.8,.25,1); + + & ~ pre { + transition: all 0.5s cubic-bezier(.25,.8,.25,1); + opacity: 0.5; + } + + &:focus, &:hover, &:focus span, &:focus ~ pre { + opacity: 1; + } +` + +export const Rounded = css` + border-radius: 3px; +` + +export const Border = css` + border: 1px solid ${p => p.theme.colors.border}; +` + +export const InputLike = css` + ${Hover} + font-family: 'JetBrains Mono', monospace; + width: 100%; + font-size: 0.8em; + padding: 0.6em; + outline: none; + margin: 1.7em 0; +` + +export const CodeLike = css` + font-family: JetBrains Mono !important; + font-size: 13px !important; + line-height: 1.6em !important; + white-space: pre-wrap; +` + +export const ButtonLike = css` + font-family: 'JetBrains Mono', serif; + font-weight: 700; + padding: 0.6em 1.5em; + margin: 2em 0; + outline: 0; +`
\ No newline at end of file diff --git a/frontend/src/components/Footer.js b/frontend/src/components/Footer.js index c7878e5..4afc7ba 100644 --- a/frontend/src/components/Footer.js +++ b/frontend/src/components/Footer.js @@ -2,7 +2,12 @@ import React from 'react'; import styled from 'styled-components' const SpacedFooter = styled.div` - margin: 2em 0; + & > p { + font-size: 0.8em; + } + & a { + color: ${p => p.theme.colors.text}; + } ` const Link = (props) => { @@ -14,7 +19,7 @@ const Link = (props) => { const Footer = () => { return ( <SpacedFooter> - © 2020 — <Link link="https://jzhao.xyz/" name="jacky" />, <Link link="https://ryanmehri.tech/" name="ryan" /> + <p>(c) 2020 // <Link link="https://jzhao.xyz/" name="jacky" />, <Link link="https://ryanmehri.tech/" name="ryan" /></p> </SpacedFooter> ); } diff --git a/frontend/src/components/Inputs.js b/frontend/src/components/Inputs.js deleted file mode 100644 index 3adad92..0000000 --- a/frontend/src/components/Inputs.js +++ /dev/null @@ -1,200 +0,0 @@ -import React, {useEffect, useRef} from 'react'; -import CharLimit from './decorators/CharLimit' -import styled from 'styled-components' -import FloatingLabel from './decorators/FloatingLabel' -import Dropdown from 'react-dropdown'; -import { LANGS, THEMES } from './renderers/Code'; -import * as indentation from 'indent-textarea'; - -const RelPositioning = styled.div` - position: relative; - height: calc(100% - 4em); -` - -const FlexChild = styled.div` - display: block; - margin-left: 2em; -` - -const TitleInput = (props) => { - return ( - <RelPositioning> - <FloatingLabel - label="title" - id={props.id} - value={props.value} /> - <input - name="title" - readOnly={props.readOnly} - className="lt-shadow" - placeholder="Title" - id={props.id} - type="text" - autoFocus - autoComplete="off" - onChange={props.onChange} - value={props.value} /> - <CharLimit - content={props.value} - maxLength={props.maxLength} /> - </RelPositioning> - ); -} - -const PasteInput = ({content, ...props}) => { - const textInput = useRef(null); - - useEffect(() => { - indentation.watch(textInput.current); - }, [textInput]) - - return ( - <RelPositioning> - <FloatingLabel - label="content" - id={props.id} - value={content} /> - <textarea - name="content" - readOnly={props.readOnly} - placeholder="Paste your text here" - value={content} - id={props.id} - ref={textInput} - required - onChange={props.onChange} - className="lt-shadow" /> - <CharLimit - content={content} - maxLength={props.maxLength} /> - </RelPositioning> - ); -} - -const PassInput = (props) => { - return ( - <FlexChild> - <RelPositioning> - <FloatingLabel - label="password" - id={props.id} - value={props.value} /> - <input - name="pass" - className="lt-shadow" - placeholder="password" - type="password" - autoComplete="off" - onChange={props.onChange} - value={props.value} - id={props.id} /> - </RelPositioning> - </FlexChild> - ); -} - -const GenericDropdown = (props) => { - function _onSelect(option) { - props.onChange({ - target: { - name: props.label, - value: option.label - } - }); - } - - return ( - <FlexChild> - <Dropdown - options={props.options} - onChange={_onSelect} - value={props.value} - placeholder={props.placeholder} - id={props.id} /> - <FloatingLabel - label={props.label} - id={props.id} - value={props.value} /> - </FlexChild> - ); -} - -const ExpiryInput = (props) => { - const options = [ - '5 years', - '1 year', - '1 month', - '1 week', - '1 day', - '1 hour', - '10 min', - ]; - - return ( - <GenericDropdown - {...props} - options={options} - placeholder='1 week' - label='expiry' - /> - ); -} - -const LangInput = (props) => { - const options = Object.entries(LANGS).map((key, _) => { - return { - 'value': key[1], - 'label': key[0] - } - }) - - return ( - <GenericDropdown - {...props} - options={options} - placeholder={LANGS.raw} - label='language' - /> - ); -} - -const ThemeInput = (props) => { - const options = Object.entries(THEMES).map((key, _) => { - return { - 'value': key[1], - 'label': key[0] - } - }) - - return ( - <GenericDropdown - {...props} - options={options} - placeholder={'atom'} - label='theme' - /> - ); -} - -const PasteURLInput = ({id, fullURL}) => { - return ( - <FlexChild> - <RelPositioning> - <FloatingLabel - label="url" - id={id} - value={id} /> - <input - name="paste_url" - className="lt-shadow" - type="text" - readOnly - autoFocus - id={id} - value={fullURL} /> - </RelPositioning> - </FlexChild> - ); -} - -export { TitleInput, PasteInput, PassInput, ExpiryInput, PasteURLInput, LangInput, ThemeInput }
\ No newline at end of file diff --git a/frontend/src/components/Inputs/Code.js b/frontend/src/components/Inputs/Code.js new file mode 100644 index 0000000..adb1536 --- /dev/null +++ b/frontend/src/components/Inputs/Code.js @@ -0,0 +1,54 @@ +import React from "react"; +import styled from 'styled-components' +import CharLimit from "../decorators/CharLimit"; +import Editor from 'react-simple-code-editor'; +import {Highlighter} from "../renderers/Code"; +import {CodeLike, Hover} from "../Common/mixins"; + +const Wrapper = styled.div` + position: relative; +` +const EditorWrapper = styled(Editor)` + overflow: visible !important; + + & > * { + padding: 0 !important; + width: 100%; + } + + & pre, & code, & > textarea { + ${CodeLike} + min-height: 40vh; + } + + & pre { + z-index: -1 !important; + } + + & > textarea { + ${Hover} + padding: 0.6em !important; + outline: none !important; + } +` + +export const Code = ({content, id, readOnly, setContentCallback, ...props}) => { + return ( + <Wrapper> + <EditorWrapper + name="content" + readOnly={readOnly} + placeholder="Paste your text here" + value={content} + id={id} + required + highlight={code => <Highlighter theme="atom">{code}</Highlighter> } + onValueChange={code => setContentCallback(code)} + padding={15} + /> + <CharLimit + content={content} + maxLength={props.maxLength} /> + </Wrapper> + ); +}
\ No newline at end of file diff --git a/frontend/src/components/Inputs/Dropdown.js b/frontend/src/components/Inputs/Dropdown.js new file mode 100644 index 0000000..9fde6ed --- /dev/null +++ b/frontend/src/components/Inputs/Dropdown.js @@ -0,0 +1,120 @@ +import Dropdown from "react-dropdown"; +import React from "react"; +import styled from 'styled-components'; +import {LANGS, THEMES} from "../renderers/Code"; +import {Labelled} from "../decorators/Labelled"; +import {Border, DropShadow, InputLike, Rounded} from "../Common/mixins"; + +const StyledDropdown = styled(Dropdown)` + ${Border} + ${Rounded} + ${DropShadow} + ${InputLike} + cursor: pointer; + + & .Dropdown-root { + cursor: pointer; + + &:hover, &.is-open { + opacity: 1; + } + } + + & .Dropdown-placeholder { + width: 5.5em; + } + + & .Dropdown-menu { + border-top: 1px solid ${p => p.theme.colors.text}; + margin-top: 0.5em; + bottom: auto; + } + + & .Dropdown-option { + margin-top: 0.5em; + transition: all 0.5s cubic-bezier(.25,.8,.25,1); + + &:hover { + font-weight: 700; + opacity: 0.4; + } + } +` + +const GenericDropdown = (props) => { + function _onSelect(option) { + props.onChange({ + target: { + name: props.label, + value: option.label + } + }); + } + + return ( + <Labelled + label={props.label} + id={props.id} + value={props.value}> + <StyledDropdown + options={props.options} + onChange={_onSelect} + value={props.value} + placeholder={props.placeholder} + id={props.id} /> + </Labelled> + ); +} + +export const Expiry = (props) => { + const options = [ + '5 years', + '1 year', + '1 month', + '1 week', + '1 day', + '1 hour', + '10 min', + ]; + + return ( + <GenericDropdown + {...props} + options={options} + placeholder='1 week' + label='expiry' + /> + ); +} + +export const Language = (props) => { + const options = Object.entries(LANGS).map((key, _) => ({ + 'value': key[1], + 'label': key[0] + })) + + return ( + <GenericDropdown + {...props} + options={options} + placeholder='detect' + label='language' + /> + ); +} + +export const Theme = (props) => { + const options = Object.entries(THEMES).map((key) => ({ + 'value': key[1], + 'label': key[0] + })) + + return ( + <GenericDropdown + {...props} + options={options} + placeholder='atom' + label='theme' + /> + ); +}
\ No newline at end of file diff --git a/frontend/src/components/Inputs/Password.js b/frontend/src/components/Inputs/Password.js new file mode 100644 index 0000000..099aae4 --- /dev/null +++ b/frontend/src/components/Inputs/Password.js @@ -0,0 +1,14 @@ +import React from "react"; +import {Labelled} from "../decorators/Labelled"; +import {Input} from "../Common/Input"; + +export const Password = (props) => <Labelled label="password"> + <Input + name="pass" + placeholder={props.placeholder ?? "add password"} + type="password" + autoComplete="off" + onChange={props.onChange} + value={props.value} + id={props.id} /> +</Labelled>
\ No newline at end of file diff --git a/frontend/src/components/Inputs/Text.js b/frontend/src/components/Inputs/Text.js new file mode 100644 index 0000000..600e653 --- /dev/null +++ b/frontend/src/components/Inputs/Text.js @@ -0,0 +1,25 @@ +import CharLimit from "../decorators/CharLimit"; +import React from "react"; +import {Labelled} from "../decorators/Labelled"; +import {Input} from "../Common/Input"; + +export const Text = React.forwardRef(({label, id, readOnly, onChange, value, maxLength, autoFocus}, ref) => { + return ( + <Labelled label={label} value={value}> + <Input + ref={ref} + name={label} + readOnly={readOnly} + placeholder="Title" + id={id} + type="text" + autoFocus={autoFocus} + autoComplete="off" + onChange={onChange} + value={value} /> + <CharLimit + content={value} + maxLength={maxLength} /> + </Labelled> + ); +})
\ No newline at end of file diff --git a/frontend/src/components/Inputs/index.js b/frontend/src/components/Inputs/index.js new file mode 100644 index 0000000..b16deb3 --- /dev/null +++ b/frontend/src/components/Inputs/index.js @@ -0,0 +1,6 @@ +import {Code} from './Code'; +import {Expiry, Language, Theme} from "./Dropdown"; +import {Password} from "./Password"; +import {Text} from "./Text"; + +export {Code, Expiry, Language, Theme, Password, Text};
\ No newline at end of file diff --git a/frontend/src/components/NewPaste.js b/frontend/src/components/NewPaste.js index ff945a0..b322351 100644 --- a/frontend/src/components/NewPaste.js +++ b/frontend/src/components/NewPaste.js @@ -1,20 +1,14 @@ import React, { useEffect, useState, useRef } from 'react'; -import { TitleInput, PasteInput } from './Inputs' +import { Text, Code } from './Inputs' import OptionsContainer from './Options' import Error from './Err' import { PostNewPaste } from '../helpers/httpHelper' import PasteModal from './modals/PasteModal' -import { LANGS } from './renderers/Code' import styled from 'styled-components' import CodeRenderer from './renderers/Code' import Latex from './renderers/Latex' import Markdown from './renderers/Markdown' - -const Button = styled.button` - margin-right: 0 !important; - margin-left: 2em !important; - height: calc(16px + 1.6em + 2px); -` +import {Button, SubmitButton} from "./Common/Button"; const Flex = styled.div` display: flex; @@ -39,7 +33,7 @@ const NewPaste = () => { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [pass, setPass] = useState(''); - const [language, setLanguage] = useState(LANGS.raw); + const [language, setLanguage] = useState('detect'); const [expiry, setExpiry] = useState(''); const [hash, setHash] = useState(''); const [isPreview, setIsPreview] = useState(false); @@ -78,11 +72,10 @@ const NewPaste = () => { } function renderPreview() { - const pasteInput = <PasteInput - onChange={(e) => { setContent(e.target.value) }} + const pasteInput = <Code + setContentCallback={setContent} content={content} - maxLength="100000" - id="pasteInput" /> + maxLength="100000" /> if (isPreview) { var preview @@ -129,9 +122,11 @@ const NewPaste = () => { return ( <form onSubmit={handleSubmit}> <PasteModal hash={hash} /> - <TitleInput + <Text + label="title" onChange={(e) => {setTitle(e.target.value)}} value={title} + autoFocus maxLength="100" id="titleInput" /> {renderPreview()} @@ -142,13 +137,15 @@ const NewPaste = () => { onPassChange={(e) => { setPass(e.target.value) }} onLangChange={(e) => { setLanguage(e.target.value) }} onExpiryChange={(e) => { setExpiry(e.target.value) }} /> - <input className="lt-button lt-shadow lt-hover" type="submit" value="new paste" /> - <Button - className="lt-shadow lt-hover" + <div> + <SubmitButton type="submit" value="new paste" /> + {language !== 'detect' && <Button + secondary type="button" onClick={() => setIsPreview(!isPreview)}> preview - </Button> + </Button>} + </div> <br /> <Error ref={ErrorLabel} /> </form> diff --git a/frontend/src/components/Options.js b/frontend/src/components/Options.js index 09f92f3..97d3127 100644 --- a/frontend/src/components/Options.js +++ b/frontend/src/components/Options.js @@ -1,31 +1,34 @@ import React from 'react'; import styled from 'styled-components' -import { PassInput, ExpiryInput, LangInput } from './Inputs' +import { Password, Expiry, Language } from './Inputs' const Flex = styled.div` float: right; display: flex; flex-direction: row; transform: translateY(0.2em); + + & > *:not(:first-child) { + margin-left: 2em; + } @media (max-width: 850px) { float: none !important; - transform: translateX(-2em); } ` const OptionsContainer = ({pass, lang, expiry, onPassChange, onLangChange, onExpiryChange}) => { return ( <Flex> - <PassInput + <Password value={pass} onChange={onPassChange} id="passwordInput" /> - <LangInput + <Language value={lang} onChange={onLangChange} id="langInput" /> - <ExpiryInput + <Expiry value={expiry} onChange={onExpiryChange} id="expiryInput" /> diff --git a/frontend/src/components/PasteInfo.js b/frontend/src/components/PasteInfo.js index bab7e23..6ab5b19 100644 --- a/frontend/src/components/PasteInfo.js +++ b/frontend/src/components/PasteInfo.js @@ -1,8 +1,8 @@ import React from 'react'; import styled from 'styled-components' import { useHistory } from 'react-router-dom'; -import { ThemeInput } from './Inputs' -import { exportComponentAsPNG } from "react-component-export-image"; +import { Theme } from './Inputs' +import {Button} from "./Common/Button"; const Bold = styled.span` font-weight: 700 @@ -13,10 +13,7 @@ const StyledDiv = styled.div` margin: 2em 0; ` -const Button = styled.button` - margin-right: 0 !important; - margin-left: 2em !important; - height: calc(16px + 1.6em); +const ShiftedButton = styled(Button)` margin-top: 1.6em !important; ` @@ -28,7 +25,6 @@ const Flex = styled.div` float: right; display: flex; flex-direction: row; - transform: translateY(0.2em); ` const PasteInfo = (props) => { @@ -42,12 +38,12 @@ const PasteInfo = (props) => { const buttonTxt = props.isRenderMode ? 'text' : 'render' if (props.lang === 'latex' || props.lang === 'markdown') { return ( - <Button - className="lt-shadow lt-hover" + <ShiftedButton + secondary type="button" onClick={props.toggleRenderCallback}> {buttonTxt} - </Button> + </ShiftedButton> ); } } @@ -55,20 +51,14 @@ const PasteInfo = (props) => { return ( <div> <Flex> - <Button - className="lt-shadow lt-hover" + <ShiftedButton + secondary type="button" onClick={redirRaw}> view raw - </Button> - <Button - className="lt-shadow lt-hover" - type="button" - onClick={() => exportComponentAsPNG(props.compref, `paste-${props.hash}.png`)}> - save png - </Button> + </ShiftedButton> {renderable()} - <ThemeInput + <Theme value={props.theme} onChange={props.onChange} id="themeInput" /> diff --git a/frontend/src/components/ViewPaste.js b/frontend/src/components/ViewPaste.js index 5f9a962..9dd281c 100644 --- a/frontend/src/components/ViewPaste.js +++ b/frontend/src/components/ViewPaste.js @@ -1,13 +1,12 @@ import React, { useEffect, useState, useRef } from 'react'; import Error from './Err'; -import { TitleInput } from './Inputs'; +import { Text } from './Inputs'; import CodeRenderer from './renderers/Code' import PasteInfo from './PasteInfo'; import PasswordModal from './modals/PasswordModal' import { FetchPaste, FetchPasswordPaste } from '../helpers/httpHelper' import { LANGS } from './renderers/Code' import RenderDispatch from './renderers/RenderDispatch' -import MetaTags from 'react-meta-tags'; function fmtDateStr(dateString) { const d = new Date(dateString) @@ -24,14 +23,13 @@ const ViewPaste = (props) => { const [expiry, setExpiry] = useState(''); const [theme, setTheme] = useState('atom'); const [isRenderMode, setIsRenderMode] = useState(false); - const [language, setLanguage] = useState(LANGS.raw); + const [language, setLanguage] = useState(LANGS.detect); useEffect(() => { setIsRenderMode(language === 'latex' || language === 'markdown') }, [language]) const ErrorLabelRef = useRef(null); - const ComponentRef = useRef(null); function validatePass(pass, onErrorCallBack) { FetchPasswordPaste(props.hash, pass) @@ -98,39 +96,26 @@ const ViewPaste = (props) => { }, [props.hash]) function getDisplay() { - if (isRenderMode) { - return ( - <RenderDispatch - language={language} - content={content} - ref={ComponentRef} - /> - ) - } else { - return ( - <CodeRenderer - content={content} - lang={language} - theme={theme} - ref={ComponentRef} - id="pasteInput" /> - ) - } + return isRenderMode ? <RenderDispatch + language={language} + content={content} + /> : <CodeRenderer + content={content} + lang={language} + theme={theme} + id="pasteInput" /> } return ( <div> - <MetaTags> - <meta name="description" content={`${language}, expires ${expiry}. hosted on ctrl-v`} /> - <meta property="og:title" content={title} /> - </MetaTags> <PasswordModal hasPass={hasPass} validPass={validPass} value={enteredPass} onChange={(e) => setEnteredPass(e.target.value)} validateCallback={validatePass} /> - <TitleInput + <Text + label="title" value={title} id="titleInput" readOnly /> @@ -143,7 +128,6 @@ const ViewPaste = (props) => { toggleRenderCallback={() => setIsRenderMode(!isRenderMode)} isRenderMode={isRenderMode} onChange={(e) => setTheme(e.target.value)} - compref={ComponentRef} err={<Error ref={ErrorLabelRef} />} /> </div> diff --git a/frontend/src/components/Watermark.js b/frontend/src/components/Watermark.js new file mode 100644 index 0000000..2f6c8c6 --- /dev/null +++ b/frontend/src/components/Watermark.js @@ -0,0 +1,32 @@ +import styled from "styled-components"; +import React from "react"; +import Footer from "./Footer"; + +const Logo = styled.div` + position: fixed; + bottom: 1em; + left: 2em; + opacity: 0.3; + transition: opacity 0.5s cubic-bezier(.25,.8,.25,1); + + &:hover { + opacity: 1; + } + + & > h1 { + font-size: 50px; + margin: 0 0; + + & > a { + text-decoration: none; + position: relative; + color: ${p => p.theme.colors.text}; + } + } +` +export const Watermark = () => <Logo> + <h1> + <a href="https://github.com/jackyzha0/ctrl-v">ctrl-v</a> + </h1> + <Footer /> +</Logo>
\ No newline at end of file diff --git a/frontend/src/components/decorators/CharLimit.js b/frontend/src/components/decorators/CharLimit.js index 5a6fdca..3d1d981 100644 --- a/frontend/src/components/decorators/CharLimit.js +++ b/frontend/src/components/decorators/CharLimit.js @@ -1,34 +1,24 @@ import React from 'react'; -import styled, { css } from 'styled-components' +import styled from 'styled-components' // show char limit if length > half of max const Chars = styled.p` color: #11111100; - font-family: 'Roboto Mono', monospace; position: absolute; font-size: 0.8em; writing-mode: vertical-rl; top: 50%; - transform: translate(5em, -50%); + transform: translate(4em, -50%); right: 0; transition: all 0.5s cubic-bezier(.25,.8,.25,1); - ${props => - ((props.content.length / props.maxLength) > 0.5) && - css` - color: #111111; - `}; - - ${props => - ((props.content.length / props.maxLength) > 1) && - css` - color: #ee1111; - `}; + ${p => ((p.content.length / p.maxLength) > 0.5) && ` color: ${p.theme.colors.text}; `}; + ${p => ((p.content.length / p.maxLength) > 1) && ` color: ${p.theme.colors.error}; `}; `; const CharLimit = (props) => { return ( - <Chars {...props} >{props.maxLength - props.content.length}/{props.maxLength}</Chars> + <Chars {...props}>{props.maxLength - props.content.length}/{props.maxLength}</Chars> ); } diff --git a/frontend/src/components/decorators/FloatingLabel.js b/frontend/src/components/decorators/FloatingLabel.js deleted file mode 100644 index ef56b44..0000000 --- a/frontend/src/components/decorators/FloatingLabel.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import styled, { css } from 'styled-components' - -const StyledLabel = styled.label` - position: absolute; - top: 0.5em; - font-weight: 700; - font-size: 1em; - opacity: 0; - transition: all 0.5s cubic-bezier(.25,.8,.25,1); - - ${props => - (props.value.length > 0) && - css` - top: -0.1em; - opacity: 1 - `}; -` - -const FloatingLabel = (props) => { - return ( - <StyledLabel - label={props.label} - value={props.value} - className={props.id} - htmlFor={props.id}> - {props.label} - </StyledLabel> - ); -} - -export default FloatingLabel
\ No newline at end of file diff --git a/frontend/src/components/decorators/Labelled.js b/frontend/src/components/decorators/Labelled.js new file mode 100644 index 0000000..0d7fe39 --- /dev/null +++ b/frontend/src/components/decorators/Labelled.js @@ -0,0 +1,30 @@ +import styled from "styled-components"; +import React from "react"; + +const StyledLabel = styled.label` + position: relative; + & > span { + position: absolute; + transform: translateY(-0.2em); + font-weight: 700; + font-size: 1em; + opacity: 0.5; + transition: opacity 0.5s cubic-bezier(.25,.8,.25,1); + } + + &:hover > span { + opacity: 1; + } +` + +const FloatingLabel = (props) => <StyledLabel label={props.label}> + {props.children} +</StyledLabel> + + +export const Labelled = ({label, value, children}) => <div> + <FloatingLabel label={label} value={value} > + <span>{label}</span> + {children} + </FloatingLabel> +</div>
\ No newline at end of file diff --git a/frontend/src/components/hooks/useFetchPaste.js b/frontend/src/components/hooks/useFetchPaste.js new file mode 100644 index 0000000..a61ca1e --- /dev/null +++ b/frontend/src/components/hooks/useFetchPaste.js @@ -0,0 +1,5 @@ +import React from 'react' + +export default () => { + +}
\ No newline at end of file diff --git a/frontend/src/components/modals/PasswordModal.js b/frontend/src/components/modals/PasswordModal.js index bf373cc..a829091 100644 --- a/frontend/src/components/modals/PasswordModal.js +++ b/frontend/src/components/modals/PasswordModal.js @@ -1,19 +1,9 @@ import React, { useRef } from 'react'; import Modal from 'react-modal'; -import { PassInput } from '../Inputs' -import { RightPad, LeftPad, ModalHeader, Padding } from './shared' +import { Password } from '../Inputs' +import {ModalHeader, Padding, modalStyles, Form} from './shared' import Error from '../Err'; - -const modalStyles = { - content: { - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: '400px', - height: '250px', - border: '1px solid #11111188' - } -}; +import {SubmitButton} from "../Common/Button"; const PasswordModal = (props) => { const ErrorLabel = useRef(null); @@ -31,21 +21,16 @@ const PasswordModal = (props) => { style={modalStyles} contentLabel="enter paste password" > - <form onSubmit={submitPassword}> - <LeftPad> - <ModalHeader><span role="img" aria-label="warning">🚧 </span>err: password protected</ModalHeader> - </LeftPad> - <RightPad> - <PassInput - value={props.value} - onChange={props.onChange} /> - </RightPad> - <LeftPad> - <input className="lt-button lt-shadow lt-hover" type="submit" value="continue" /> - <Padding /> - <Error ref={ErrorLabel} /> - </LeftPad> - </form> + <Form onSubmit={submitPassword}> + <ModalHeader><span role="img" aria-label="warning">🚧 </span>err: password protected</ModalHeader> + <Password + placeholder="hunter2" + value={props.value} + onChange={props.onChange} /> + <SubmitButton type="submit" value="continue" /> + <Padding /> + <Error ref={ErrorLabel} /> + </Form> </Modal> ); } diff --git a/frontend/src/components/modals/PasteModal.js b/frontend/src/components/modals/PasteModal.js index 7b28abb..e7dbed2 100644 --- a/frontend/src/components/modals/PasteModal.js +++ b/frontend/src/components/modals/PasteModal.js @@ -1,20 +1,10 @@ import React from 'react'; import Modal from 'react-modal'; -import { LeftPad, ModalHeader, RightPad } from './shared' +import {Form, ModalHeader, modalStyles} from './shared' import { useHistory } from 'react-router-dom'; -import { PasteURLInput } from '../Inputs' +import { Text } from '../Inputs' import { useClipboard } from 'use-clipboard-copy'; - -const modalStyles = { - content: { - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: '500px', - height: '250px', - border: '1px solid #11111188' - } -}; +import {Button} from "../Common/Button"; const PasteModal = (props) => { const history = useHistory(); @@ -34,35 +24,27 @@ const PasteModal = (props) => { style={modalStyles} contentLabel="paste created" > - <form onSubmit={redir}> - <LeftPad> - <ModalHeader><span role="img" aria-label="success">📎 </span>paste created</ModalHeader> - </LeftPad> - <RightPad> - <PasteURLInput - id="paste_modal" - fullURL={fullURL} /> - <input - hidden - type="text" - value={fullURL} - readOnly - ref={clipboard.target} /> - </RightPad> - <LeftPad> - <button - className="lt-button lt-shadow lt-hover" - type="submit"> - view - </button> - <button - className="lt-button lt-shadow lt-hover" - type="button" - onClick={clipboard.copy}> - {clipboard.copied ? 'copied' : 'copy url'} - </button> - </LeftPad> - </form> + <Form onSubmit={redir}> + <ModalHeader> + <span role="img" aria-label="success">📎 </span>paste created + </ModalHeader> + <Text + label="url" + type="text" + value={fullURL} + readOnly + ref={clipboard.target} /> + <Button + type="submit"> + go to paste + </Button> + <Button + secondary + type="button" + onClick={clipboard.copy}> + {clipboard.copied ? 'copied' : 'copy url'} + </Button> + </Form> </Modal> ); } diff --git a/frontend/src/components/modals/shared.js b/frontend/src/components/modals/shared.js index d63be06..0336818 100644 --- a/frontend/src/components/modals/shared.js +++ b/frontend/src/components/modals/shared.js @@ -1,11 +1,19 @@ import styled from 'styled-components' -export const RightPad = styled.div` - margin-right: 3em; -` +export const modalStyles = { + content: { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '500px', + height: '250px', + border: '1px solid #11111188', + }, +}; -export const LeftPad = styled.div` - margin-left: 2em; +export const Form = styled.form` + margin: 2em; + margin-right: 3em; ` export const ModalHeader = styled.h3` diff --git a/frontend/src/components/renderers/Code.js b/frontend/src/components/renderers/Code.js index a312c51..4ab1175 100644 --- a/frontend/src/components/renderers/Code.js +++ b/frontend/src/components/renderers/Code.js @@ -1,7 +1,9 @@ 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 styled from 'styled-components' +import {Border, CodeLike, DropShadow, Rounded} from "../Common/mixins"; export const THEMES = Object.freeze({ 'atom': atomOneLight, @@ -12,66 +14,50 @@ export const THEMES = Object.freeze({ }) export const LANGS = Object.freeze({ - 'bash': 'bash', - 'c': 'c', - 'c++': 'cpp', - 'c#': 'cs', - 'css': 'css', - 'docker': 'dockerfile', - 'go': 'go', - 'haskell': 'haskell', - 'html': 'html', - 'java': 'java', - 'js': 'javascript', - 'jsx': 'jsx', 'latex': 'latex', - 'lisp': 'lisp', - 'makefile': 'makefile', 'markdown': 'markdown', - 'php': 'php', - 'python': 'python', - 'raw': 'text', - 'ruby': 'ruby', - 'scala': 'scala', - 'yaml': 'yaml' + 'detect': 'text', }) -const StyledPre = styled.pre` - width: 100%; - font-size: 0.8em; - min-height: 1.2em; - border-radius: 3px !important; - border: 1px solid #565656 !important; - padding: calc(0.8em - 1px) !important; - outline: none; +export const StyledPre = styled.pre` + ${Rounded}; + ${Border}; + ${DropShadow}; + width: calc(100%); + padding: calc(0.6em - 1px) !important; margin: 1.7em 0; + position: relative; + outline: none; + + & code { + ${CodeLike} + } - & code:first-child { + & code:first-child:not(:only-of-type) { margin-right: 10px !important; border-radius: 0 !important; border-right: 1px solid #11111155 !important; } ` -const CodeRenderer = React.forwardRef((props, ref) => { - - const Pre = (props) => { - return ( - <StyledPre {...props} ref={ref} /> - ); - } - - return ( - <div className="lt-shadow"> - <SyntaxHighlighter - language={LANGS[props.lang]} - style={THEMES[props.theme]} - showLineNumbers - PreTag={Pre}> - {props.content} - </SyntaxHighlighter> - </div> - ); -}); +export const Highlighter = ({language, lineNumbers, theme, pre = StyledPre, children}) => <SyntaxHighlighter + language={LANGS[language]} + style={THEMES[theme]} + showLineNumbers={lineNumbers} + PreTag={pre}> + {children} +</SyntaxHighlighter> + +const CodeRenderer = (props) => { + return (<Highlighter + lineNumbers={true} + language={props.lang} + theme={props.theme} + renderer={virtualizedRenderer()} + pre={StyledPre} + > + {props.content} + </Highlighter>) +}; export default CodeRenderer
\ No newline at end of file diff --git a/frontend/src/components/renderers/Raw.js b/frontend/src/components/renderers/Raw.js index d4dc830..182bfff 100644 --- a/frontend/src/components/renderers/Raw.js +++ b/frontend/src/components/renderers/Raw.js @@ -1,12 +1,10 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components' import { FetchPaste } from '../../helpers/httpHelper' +import {CodeLike} from "../Common/mixins"; const RawText = styled.pre` - word-wrap: break-word; - white-space: pre-wrap; - line-height: initial; - font-size: 0.8em; + ${CodeLike} padding: 0 1em; ` diff --git a/frontend/src/components/renderers/RenderDispatch.js b/frontend/src/components/renderers/RenderDispatch.js index 3f1c87b..365a822 100644 --- a/frontend/src/components/renderers/RenderDispatch.js +++ b/frontend/src/components/renderers/RenderDispatch.js @@ -8,16 +8,16 @@ const RenderWrapper = styled.div` padding: 1em; ` -const RenderDispatch = React.forwardRef((props, ref) => { +const RenderDispatch = (props) => { switch (props.language) { case 'latex': return ( - <RenderWrapper ref={ref}> + <RenderWrapper> <Latex content={props.content} /> </RenderWrapper>) case 'markdown': return ( - <RenderWrapper ref={ref} className="md" > + <RenderWrapper className="md" > <Markdown content={props.content} /> </RenderWrapper>) default: @@ -26,9 +26,8 @@ const RenderDispatch = React.forwardRef((props, ref) => { content={props.content} lang={props.language} theme={props.theme} - ref={ref} id="pasteInput" />) } -}); +}; export default RenderDispatch
\ No newline at end of file diff --git a/frontend/src/css/index.css b/frontend/src/css/index.css index fcda1a3..0340813 100644 --- a/frontend/src/css/index.css +++ b/frontend/src/css/index.css @@ -4,20 +4,13 @@ } } -body { - margin: 0; - padding: 0; - background-color: #faf9f5; - font-family: 'Lora', serif; -} - form { width: 100%; } textarea, input[type=text], input[type=password], .Dropdown-root { width: 100%; - font-family: 'Roboto Mono', monospace; + font-family: 'JetBrains Mono', monospace; font-size: 0.8em; padding: calc(0.8em - 1px); border-radius: 3px; @@ -37,7 +30,7 @@ textarea, input[type=text], input[type=password], .Dropdown-root { code, pre { background: #00000000; - font-family: 'Roboto Mono', monospace; + font-family: 'JetBrains Mono', monospace; padding: initial; border-radius: 3px; outline: none; @@ -105,7 +98,7 @@ a { } input[type=submit], button[type=submit] { - font-family: 'Lora', serif; + font-family: 'JetBrains Mono', serif; font-weight: 700; color: #faf9f5; background-color: #111111; @@ -115,7 +108,7 @@ input[type=submit], button[type=submit] { } button[type=button] { - font-family: 'Lora', serif; + font-family: 'JetBrains Mono', serif; font-weight: 700; width: 8em; padding: calc(0.8em - 1px) 1.5em; @@ -127,32 +120,6 @@ button[type=button] { margin: 2em 2em; } -.mainLogo { - font-size: 50px; - margin: 0 0; - display: inline-block; -} - -.mainLogo a { - text-decoration: none; - position: relative; - margin: 3px; -} - -.mainLogo a::after { - content: ""; - position: absolute; - left: 0; - bottom: 0; - height: 4px; - background-color: #111111; - width: 0; - transition: width 0.25s ease; -} - -.mainLogo:hover a::after { - width: 100%; -} /* fixing markdown renderer */ .md h3 { diff --git a/frontend/src/index.js b/frontend/src/index.js index 437fe03..7173ce5 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,7 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './css/index.css'; -import App from './components/App'; +import App from './App'; ReactDOM.render( <React.StrictMode> diff --git a/frontend/src/theme/GlobalStyle.js b/frontend/src/theme/GlobalStyle.js new file mode 100644 index 0000000..9fe80a5 --- /dev/null +++ b/frontend/src/theme/GlobalStyle.js @@ -0,0 +1,11 @@ +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}; + } +`
\ No newline at end of file diff --git a/frontend/src/theme/ThemeProvider.js b/frontend/src/theme/ThemeProvider.js new file mode 100644 index 0000000..d9edcb0 --- /dev/null +++ b/frontend/src/theme/ThemeProvider.js @@ -0,0 +1,13 @@ +import React from 'react' +import { ThemeProvider } from 'styled-components' + +const theme = { + colors: { + background: '#faf9f5', + border: '#565656', + text: '#111111', + error: '#ee1111', + }, +} + +export default ({ children }) => <ThemeProvider theme={theme}>{children}</ThemeProvider>
\ No newline at end of file |