From 4734fb3f72ac94655d171894bd4cdd5f79be694e Mon Sep 17 00:00:00 2001 From: jackyzha0 Date: Sun, 7 Mar 2021 07:57:10 -0800 Subject: refactor to use useFetchPaste hook --- frontend/src/App.js | 6 +- frontend/src/components/NewPaste.js | 155 ------------------------ frontend/src/components/ViewPaste.js | 137 --------------------- frontend/src/components/hooks/shared.js | 74 +++++++++++ frontend/src/components/hooks/useFetchPaste.js | 66 +++++++++- frontend/src/components/modals/PasswordModal.js | 3 +- frontend/src/components/modals/shared.js | 5 - frontend/src/components/pages/NewPaste.js | 150 +++++++++++++++++++++++ frontend/src/components/pages/Raw.js | 16 +++ frontend/src/components/pages/ViewPaste.js | 65 ++++++++++ frontend/src/components/renderers/Raw.js | 44 ------- frontend/src/helpers/httpHelper.js | 68 ----------- 12 files changed, 373 insertions(+), 416 deletions(-) delete mode 100644 frontend/src/components/NewPaste.js delete mode 100644 frontend/src/components/ViewPaste.js create mode 100644 frontend/src/components/hooks/shared.js create mode 100644 frontend/src/components/pages/NewPaste.js create mode 100644 frontend/src/components/pages/Raw.js create mode 100644 frontend/src/components/pages/ViewPaste.js delete mode 100644 frontend/src/components/renderers/Raw.js delete mode 100644 frontend/src/helpers/httpHelper.js diff --git a/frontend/src/App.js b/frontend/src/App.js index a8da469..0a47baa 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,6 +1,6 @@ import React from 'react'; -import NewPaste from './components/NewPaste' -import ViewPaste from './components/ViewPaste' +import NewPaste from './components/pages/NewPaste' +import ViewPaste from './components/pages/ViewPaste' import styled from 'styled-components' import { BrowserRouter as Router, @@ -8,7 +8,7 @@ import { Route, useParams } from "react-router-dom"; -import Raw from './components/renderers/Raw' +import Raw from './components/pages/Raw' import ThemeProvider from './theme/ThemeProvider' import GlobalStyle from './theme/GlobalStyle' import {Watermark} from "./components/Watermark"; diff --git a/frontend/src/components/NewPaste.js b/frontend/src/components/NewPaste.js deleted file mode 100644 index b322351..0000000 --- a/frontend/src/components/NewPaste.js +++ /dev/null @@ -1,155 +0,0 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { Text, Code } from './Inputs' -import OptionsContainer from './Options' -import Error from './Err' -import { PostNewPaste } from '../helpers/httpHelper' -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"; - -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(() => { - if (title === "") { - document.title = `ctrl-v`; - } else { - document.title = `ctrl-v | ${title}`; - } - }, [title]) - - function handleSubmit(e) { - e.preventDefault(); - - // prevent resubmission - if (!hash) { - PostNewPaste(title, content, language, pass, expiry) - .then((response) => { - // on success, redir - setHash(response.data.hash) - }).catch((error) => { - const resp = error.response - - // some weird err - if (resp !== undefined) { - const errTxt = `${resp.status}: ${resp.data}` - ErrorLabel.current.showMessage(errTxt) - } else { - // some weird err (e.g. network) - ErrorLabel.current.showMessage(error) - } - }); - } - } - - function renderPreview() { - const pasteInput = - - if (isPreview) { - var preview - switch (language) { - case 'latex': - preview = - - - - break - case 'markdown': - preview = - - - - break - default: - preview = - - } - - return ( - - - {pasteInput} - - - {preview} - - - ); - } else { - return ( - pasteInput - ); - } - } - - return ( -
- - {setTitle(e.target.value)}} - value={title} - autoFocus - maxLength="100" - id="titleInput" /> - {renderPreview()} - { setPass(e.target.value) }} - onLangChange={(e) => { setLanguage(e.target.value) }} - onExpiryChange={(e) => { setExpiry(e.target.value) }} /> -
- - {language !== 'detect' && } -
-
- - - ); -} - -export default NewPaste \ No newline at end of file diff --git a/frontend/src/components/ViewPaste.js b/frontend/src/components/ViewPaste.js deleted file mode 100644 index 9dd281c..0000000 --- a/frontend/src/components/ViewPaste.js +++ /dev/null @@ -1,137 +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 { FetchPaste, FetchPasswordPaste } from '../helpers/httpHelper' -import { LANGS } from './renderers/Code' -import RenderDispatch from './renderers/RenderDispatch' - -function fmtDateStr(dateString) { - const d = new Date(dateString) - const options = { hour: '2-digit', minute: '2-digit', year: 'numeric', month: 'long', day: 'numeric' } - return d.toLocaleDateString("en-US", options).toLocaleLowerCase() -} - -const ViewPaste = (props) => { - const [title, setTitle] = useState('fetching paste...'); - const [content, setContent] = useState(''); - const [hasPass, setHasPass] = useState(false); - const [enteredPass, setEnteredPass] = useState(''); - const [validPass, setValidPass] = useState(false); - const [expiry, setExpiry] = useState(''); - const [theme, setTheme] = useState('atom'); - const [isRenderMode, setIsRenderMode] = useState(false); - const [language, setLanguage] = useState(LANGS.detect); - - useEffect(() => { - setIsRenderMode(language === 'latex' || language === 'markdown') - }, [language]) - - const ErrorLabelRef = useRef(null); - - function validatePass(pass, onErrorCallBack) { - FetchPasswordPaste(props.hash, pass) - .then((response) => { - setValidPass(true) - setStateFromData(response.data) - }).catch((error) => { - const resp = error.response - - // 401 unauth (bad pass) - if (resp.status === 401) { - onErrorCallBack("incorrect pass") - return - } - - // otherwise, just log it lmao - if (resp !== undefined) { - const errTxt = `${resp.status}: ${resp.data}` - onErrorCallBack(errTxt) - } else { - // some weird err (e.g. network) - onErrorCallBack(error) - } - }); - } - - function setStateFromData(data) { - document.title = data.title - setTitle(data.title) - setContent(data.content) - setLanguage(data.language) - setExpiry(fmtDateStr(data.expiry)) - } - - useEffect(() => { - FetchPaste(props.hash) - .then((response) => { - setStateFromData(response.data) - }).catch((error) => { - const resp = error.response - - // network err - if (!resp) { - ErrorLabelRef.current.showMessage(error) - return - } - - // catch 401 unauth (password protected) - if (resp.status === 401) { - setHasPass(true) - return - } - - // some weird err - if (resp !== undefined) { - const errTxt = `${resp.status}: ${resp.data}` - ErrorLabelRef.current.showMessage(errTxt, -1) - return - } - - // some weird err (e.g. network) - ErrorLabelRef.current.showMessage(error, -1) - }) - }, [props.hash]) - - function getDisplay() { - return isRenderMode ? : - } - - return ( -
- setEnteredPass(e.target.value)} - validateCallback={validatePass} /> - - {getDisplay()} - setIsRenderMode(!isRenderMode)} - isRenderMode={isRenderMode} - onChange={(e) => setTheme(e.target.value)} - err={} - /> -
- ); -} - -export default ViewPaste \ No newline at end of file diff --git a/frontend/src/components/hooks/shared.js b/frontend/src/components/hooks/shared.js new file mode 100644 index 0000000..00d41e9 --- /dev/null +++ b/frontend/src/components/hooks/shared.js @@ -0,0 +1,74 @@ +import axios from 'axios'; + +// uncomment for local dev +// const base = `http://localhost:8080/api` +const base = `https://api.ctrl-v.app/api` +export function fetchPaste(hash, pass = "") { + const serverURL = `${base}/${hash}` + + if (pass === "") { + return axios.get(serverURL) + } else { + const bodyFormData = new FormData(); + bodyFormData.set('password', pass); + return axios({ + method: 'post', + url: `${base}/${hash}`, + data: bodyFormData, + headers: { 'Content-Type': 'multipart/form-data' }, + }) + } +} + +export function newPaste(paste) { + const {title, content, language, pass, expiry} = paste + const bodyFormData = new FormData(); + bodyFormData.set('title', title); + bodyFormData.set('content', content); + bodyFormData.set('language', language); + bodyFormData.set('password', pass); + bodyFormData.set('expiry', parseExpiry(expiry)); + + return axios({ + method: 'post', + url: base, + data: bodyFormData, + headers: { 'Content-Type': 'multipart/form-data' }, + }) +} + +export function parseExpiry(e) { + var cur = new Date(); + var inSeconds = 0 + switch (e) { + case '5 years': + inSeconds = 600 * 6 * 24 * 7 * 4 * 12 * 5 + break; + case '1 year': + inSeconds = 600 * 6 * 24 * 7 * 4 * 12 + break; + case '1 month': + inSeconds = 600 * 6 * 24 * 7 * 4 + break; + case '1 day': + inSeconds = 600 * 6 * 24 + break; + case '1 hour': + inSeconds = 600 * 6 + break; + case '10 min': + inSeconds = 600 + break; + case '1 week': + default: + inSeconds = 600 * 6 * 24 * 7 + break; + } + return new Date(cur.getTime() + inSeconds * 1000).toISOString(); +} + +export function fmtDateStr(dateString) { + const d = new Date(dateString) + const options = { hour: '2-digit', minute: '2-digit', year: 'numeric', month: 'long', day: 'numeric' } + return d.toLocaleDateString("en-US", options).toLocaleLowerCase() +} \ No newline at end of file diff --git a/frontend/src/components/hooks/useFetchPaste.js b/frontend/src/components/hooks/useFetchPaste.js index a61ca1e..c394f5b 100644 --- a/frontend/src/components/hooks/useFetchPaste.js +++ b/frontend/src/components/hooks/useFetchPaste.js @@ -1,5 +1,67 @@ -import React from 'react' +import {useEffect, useState} from 'react' +import {fetchPaste, fmtDateStr} from './shared' +import {LANGS} from "../renderers/Code"; -export default () => { +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/PasswordModal.js b/frontend/src/components/modals/PasswordModal.js index a829091..5c4ab87 100644 --- a/frontend/src/components/modals/PasswordModal.js +++ b/frontend/src/components/modals/PasswordModal.js @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import Modal from 'react-modal'; import { Password } from '../Inputs' -import {ModalHeader, Padding, modalStyles, Form} from './shared' +import {ModalHeader, modalStyles, Form} from './shared' import Error from '../Err'; import {SubmitButton} from "../Common/Button"; @@ -28,7 +28,6 @@ const PasswordModal = (props) => { value={props.value} onChange={props.onChange} /> - diff --git a/frontend/src/components/modals/shared.js b/frontend/src/components/modals/shared.js index 0336818..4d5987a 100644 --- a/frontend/src/components/modals/shared.js +++ b/frontend/src/components/modals/shared.js @@ -18,9 +18,4 @@ export const Form = styled.form` export const ModalHeader = styled.h3` font-weight: 700 -` - -export const Padding = styled.span` - content: ' '; - margin-right: 2em; ` \ No newline at end of file diff --git a/frontend/src/components/pages/NewPaste.js b/frontend/src/components/pages/NewPaste.js new file mode 100644 index 0000000..19161da --- /dev/null +++ b/frontend/src/components/pages/NewPaste.js @@ -0,0 +1,150 @@ +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 = + + if (isPreview) { + var preview + switch (language) { + case 'latex': + preview = + + + + break + case 'markdown': + preview = + + + + break + default: + preview = + + } + + return ( + + + {pasteInput} + + + {preview} + + + ); + } else { + return ( + pasteInput + ); + } + } + + return ( +
+ + {setTitle(e.target.value)}} + value={title} + autoFocus + maxLength="100" + id="titleInput" /> + {renderPreview()} + { setPass(e.target.value) }} + onLangChange={(e) => { setLanguage(e.target.value) }} + onExpiryChange={(e) => { setExpiry(e.target.value) }} /> +
+ + {language !== 'detect' && } +
+
+ + + ); +} + +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 new file mode 100644 index 0000000..23ef6bf --- /dev/null +++ b/frontend/src/components/pages/Raw.js @@ -0,0 +1,16 @@ +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 {result?.content || err} +} + +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 new file mode 100644 index 0000000..bc61314 --- /dev/null +++ b/frontend/src/components/pages/ViewPaste.js @@ -0,0 +1,65 @@ +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 ? : + } + + return ( +
+ setEnteredPass(e.target.value)} + validateCallback={getWithPassword} /> + + {getDisplay()} + setIsRenderMode(!isRenderMode)} + isRenderMode={isRenderMode} + onChange={(e) => setTheme(e.target.value)} + err={} + /> +
+ ); +} + +export default ViewPaste \ No newline at end of file diff --git a/frontend/src/components/renderers/Raw.js b/frontend/src/components/renderers/Raw.js deleted file mode 100644 index 182bfff..0000000 --- a/frontend/src/components/renderers/Raw.js +++ /dev/null @@ -1,44 +0,0 @@ -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` - ${CodeLike} - padding: 0 1em; -` - -const Raw = ({hash}) => { - const [content, setContent] = useState(''); - - useEffect(() => { - FetchPaste(hash) - .then((response) => { - const data = response.data - setContent(data.content) - }).catch((error) => { - const resp = error.response - - // catch 401 unauth (password protected) - if (resp.status === 401) { - setContent('err: password protected') - return - } - - // some weird err - if (resp !== undefined) { - const errTxt = `${resp.statusText}: ${resp.data}` - setContent(errTxt) - return - } - - // some weird err (e.g. network) - setContent(error) - })}, [hash]) - - return ( - {content} - ); -} - -export default Raw \ No newline at end of file diff --git a/frontend/src/helpers/httpHelper.js b/frontend/src/helpers/httpHelper.js deleted file mode 100644 index 99b9513..0000000 --- a/frontend/src/helpers/httpHelper.js +++ /dev/null @@ -1,68 +0,0 @@ -import axios from 'axios'; - -// uncomment for local dev -// const base = `http://localhost:8080/api` -const base = `https://api.ctrl-v.app/api` - -export function FetchPaste(hash) { - const serverURL = `${base}/${hash}` - return axios.get(serverURL) -} - -export function FetchPasswordPaste(hash, pass) { - var bodyFormData = new FormData(); - bodyFormData.set('password', pass); - - return axios({ - method: 'post', - url: `${base}/${hash}`, - data: bodyFormData, - headers: { 'Content-Type': 'multipart/form-data' }, - }) -} - -export function PostNewPaste(title, content, language, pass, expiry) { - var bodyFormData = new FormData(); - bodyFormData.set('title', title); - bodyFormData.set('content', content); - bodyFormData.set('language', language); - bodyFormData.set('password', pass); - bodyFormData.set('expiry', parseExpiry(expiry)); - - return axios({ - method: 'post', - url: base, - data: bodyFormData, - headers: { 'Content-Type': 'multipart/form-data' }, - }) -} - -function parseExpiry(e) { - var cur = new Date(); - var inSeconds = 0 - switch (e) { - case '5 years': - inSeconds = 600 * 6 * 24 * 7 * 4 * 12 * 5 - break; - case '1 year': - inSeconds = 600 * 6 * 24 * 7 * 4 * 12 - break; - case '1 month': - inSeconds = 600 * 6 * 24 * 7 * 4 - break; - case '1 day': - inSeconds = 600 * 6 * 24 - break; - case '1 hour': - inSeconds = 600 * 6 - break; - case '10 min': - inSeconds = 600 - break; - case '1 week': - default: - inSeconds = 600 * 6 * 24 * 7 - break; - } - return new Date(cur.getTime() + inSeconds * 1000).toISOString(); -} \ No newline at end of file -- cgit v1.2.3