From 593ffe1a95f40d56760cde9fea6c131f01deab28 Mon Sep 17 00:00:00 2001 From: Alex Kern Date: Mon, 8 Feb 2021 17:22:05 -0800 Subject: [PATCH] Add lots of styling --- package.json | 1 + src/components/CancelButton.tsx | 16 ++++ src/components/DownloadButton.tsx | 16 ++++ src/components/Downloader.tsx | 149 ++++++++++++++++++++++++------ src/components/Footer.tsx | 83 ++++++++++------- src/components/Loading.tsx | 11 +++ src/components/PasswordField.tsx | 22 +++-- src/components/ProgressBar.tsx | 19 ++++ src/components/Spinner.tsx | 29 ++++++ src/components/StartButton.tsx | 12 +-- src/components/StopButton.tsx | 15 +-- src/components/UnlockButton.tsx | 16 ++++ src/components/UploadFileList.tsx | 33 ++++++- src/components/Uploader.tsx | 88 ++++++++++++------ src/components/WebRTCProvider.tsx | 3 +- src/components/Wordmark.tsx | 6 ++ src/pages/_app.tsx | 60 +++++++++++- src/pages/download/[...slug].tsx | 13 ++- src/pages/index.tsx | 50 ++++++++-- src/styles.css | 16 ++++ yarn.lock | 28 ++++++ 21 files changed, 562 insertions(+), 124 deletions(-) create mode 100644 src/components/CancelButton.tsx create mode 100644 src/components/DownloadButton.tsx create mode 100644 src/components/Loading.tsx create mode 100644 src/components/ProgressBar.tsx create mode 100644 src/components/Spinner.tsx create mode 100644 src/components/UnlockButton.tsx create mode 100644 src/components/Wordmark.tsx create mode 100644 src/styles.css diff --git a/package.json b/package.json index 1624dcd..f1d6feb 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react-device-detect": "^1.15.0", "react-dom": "^16.13.1", "react-qr": "0.0.2", + "react-qr-code": "^1.0.5", "streamsaver": "^2.0.5", "styled-components": "^5.2.0", "twilio": "^2.9.1", diff --git a/src/components/CancelButton.tsx b/src/components/CancelButton.tsx new file mode 100644 index 0000000..4046594 --- /dev/null +++ b/src/components/CancelButton.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Button } from '@chakra-ui/react' + +type Props = { + onClick: React.MouseEventHandler +} + +const CancelButton: React.FC = ({ onClick }: Props) => { + return ( + + ) +} + +export default CancelButton diff --git a/src/components/DownloadButton.tsx b/src/components/DownloadButton.tsx new file mode 100644 index 0000000..dd3bd2a --- /dev/null +++ b/src/components/DownloadButton.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Button } from '@chakra-ui/react' + +type Props = { + onClick?: React.MouseEventHandler +} + +const DownloadButton: React.FC = ({ onClick }: Props) => { + return ( + + ) +} + +export default DownloadButton diff --git a/src/components/Downloader.tsx b/src/components/Downloader.tsx index e19b36a..3a9d901 100644 --- a/src/components/Downloader.tsx +++ b/src/components/Downloader.tsx @@ -12,6 +12,14 @@ import * as t from 'io-ts' import { ChunkMessage, decodeMessage, Message, MessageType } from '../messages' import { createZipStream } from '../zip-stream' import { DataConnection } from 'peerjs' +import PasswordField from './PasswordField' +import UnlockButton from './UnlockButton' +import { chakra, Box, Text, VStack } from '@chakra-ui/react' +import Loading from './Loading' +import UploadFileList from './UploadFileList' +import DownloadButton from './DownloadButton' +import StopButton from './StopButton' +import ProgressBar from './ProgressBar' const baseURL = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000' @@ -28,6 +36,14 @@ function getZipFilename(): string { return `filepizza-download-${Date.now()}.zip` } +function cleanErrorMessage(errorMessage: string): string { + if (errorMessage.startsWith('Could not connect to peer')) { + return 'Could not connect to the uploader. Did they close their browser?' + } else { + return errorMessage + } +} + type DownloadFileStream = { name: string size: number @@ -97,6 +113,7 @@ export default function Downloader({ const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false) const [open, setOpen] = useState(false) const [downloading, setDownloading] = useState(false) + const [bytesDownloaded, setBytesDownloaded] = useState(0) const [done, setDone] = useState(false) const [errorMessage, setErrorMessage] = useState(null) @@ -111,7 +128,7 @@ export default function Downloader({ setDataConnection(conn) - conn.on('open', () => { + const handleOpen = () => { setOpen(true) const request: t.TypeOf = { @@ -126,13 +143,14 @@ export default function Downloader({ } conn.send(request) - }) + } - conn.on('data', (data) => { + const handleData = (data: unknown) => { try { const message = decodeMessage(data) switch (message.type) { case MessageType.Info: + console.log(message.files) setFilesInfo(message.files) break @@ -149,27 +167,47 @@ export default function Downloader({ } catch (err) { console.error(err) } - }) + } - conn.on('close', () => { + const handleClose = () => { setDataConnection(null) setOpen(false) setDownloading(false) setShouldAttemptConnection(false) - }) + } + + const handlePeerError = (err: Error) => { + console.error(err) + setErrorMessage(cleanErrorMessage(err.message)) + if (conn.open) { + conn.close() + } else { + handleClose() + } + } + + const handleConnectionError = (err: Error) => { + console.error(err) + setErrorMessage(cleanErrorMessage(err.message)) + if (conn.open) conn.close() + } + + conn.on('open', handleOpen) + conn.on('data', handleData) + conn.on('error', handleConnectionError) + conn.on('close', handleClose) + peer.on('error', handlePeerError) return () => { if (conn.open) conn.close() + conn.off('open', handleOpen) + conn.off('data', handleData) + conn.off('error', handleConnectionError) + conn.off('close', handleClose) + peer.off('error', handlePeerError) } }, [peer, password, shouldAttemptConnection]) - const handleChangePassword = useCallback( - (e: React.ChangeEvent) => { - setPassword(e.target.value) - }, - [], - ) - const handleSubmitPassword = useCallback((ev) => { ev.preventDefault() setShouldAttemptConnection(true) @@ -225,6 +263,7 @@ export default function Downloader({ return } + setBytesDownloaded((bd) => bd + (message.bytes as ArrayBuffer).byteLength) const uInt8 = new Uint8Array(message.bytes as ArrayBuffer) fileStream.enqueue(uInt8) if (message.final) { @@ -264,31 +303,87 @@ export default function Downloader({ startNextFileOrFinish() }, [dataConnection, filesInfo]) - if (done) { - return
Done!
+ const handleStopDownload = useCallback(() => { + // TODO(@kern): Implement me + }, []) + + const totalSize = filesInfo + ? filesInfo.reduce((acc, info) => acc + info.size, 0) + : 0 + + if (done && filesInfo) { + return ( + + + You downloaded {filesInfo.length} files. + + + + + + + ) } - if (downloading) { - return
Downloading
+ if (downloading && filesInfo) { + return ( + + + You are about to start downloading {filesInfo.length} files. + + + + + + + + ) } - if (open) { + if (open && filesInfo) { return ( -
- -
+ + + You are about to start downloading {filesInfo.length} files. + + + + ) } + if (open) { + return + } + + // TODO(@kern): Connect immediately, then have server respond if password is needed. if (shouldAttemptConnection) { - return
Loading...
+ return } return ( -
- {errorMessage &&
{errorMessage}
} - - -
+ + + {errorMessage ? ( + {errorMessage} + ) : ( + + This download requires a password. + + )} + + + + ) } diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index b0d5870..228e634 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,36 +1,55 @@ -import React from 'react' +import { chakra, Text, Link, Button, VStack, HStack } from '@chakra-ui/react' +import React, { useCallback } from 'react' -export const Footer: React.FC = () => ( -
-

- Like FilePizza? Support its development!{' '} - - donate - -

+const DONATE_HREF = + 'https://commerce.coinbase.com/checkout/247b6ffe-fb4e-47a8-9a76-e6b7ef83ea22' -

- Cooked up by{' '} - - Alex Kern - {' '} - &{' '} - - Neeraj Baid - {' '} - while eating Sliver @ UC Berkeley ·{' '} - - FAQ - {' '} - ·{' '} - - Fork us - -

-
-) +export const Footer: React.FC = () => { + const handleDonate = useCallback(() => { + window.location.href = DONATE_HREF + }, []) + + return ( + + + + + Like FilePizza? Support its development!{' '} + + + + + + Cooked up by{' '} + + Alex Kern + {' '} + &{' '} + + Neeraj Baid + {' '} + while eating Sliver @ UC Berkeley ·{' '} + + FAQ + {' '} + ·{' '} + + Fork us + + + + + ) +} export default Footer diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx new file mode 100644 index 0000000..76902e7 --- /dev/null +++ b/src/components/Loading.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Spinner, Text, VStack } from '@chakra-ui/react' + +export default function Loading({ text }: { text: string }): JSX.Element { + return ( + + + {text} + + ) +} diff --git a/src/components/PasswordField.tsx b/src/components/PasswordField.tsx index f265257..29b77b2 100644 --- a/src/components/PasswordField.tsx +++ b/src/components/PasswordField.tsx @@ -1,25 +1,33 @@ import React, { useCallback } from 'react' -import styled from 'styled-components' - -const StyledPasswordInput = styled.input` - background: red; -` +import { Input } from '@chakra-ui/react' interface Props { value: string onChange: (value: string) => void + isRequired?: boolean + isInvalid?: boolean } -export const PasswordField: React.FC = ({ value, onChange }: Props) => { +export const PasswordField: React.FC = ({ + value, + onChange, + isRequired, + isInvalid, +}: Props) => { const handleChange = useCallback((e: React.ChangeEvent) => { onChange(e.target.value) }, []) return ( - ) } diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx new file mode 100644 index 0000000..cc553a3 --- /dev/null +++ b/src/components/ProgressBar.tsx @@ -0,0 +1,19 @@ +import { Progress } from '@chakra-ui/react' + +export default function ProgressBar({ + value, + max, +}: { + value: number + max: number +}): JSX.Element { + return ( + + ) +} diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 0000000..1f82eae --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { Box, Center, Img } from '@chakra-ui/react' +import { keyframes } from '@emotion/react' + +const rotate = keyframes` + from { transform: rotate(0deg) } + to { transform: rotate(360deg) } +` + +export default function Spinner({ + direction, + isRotating, +}: { + direction: 'up' | 'down' + isRotating?: boolean +}): JSX.Element { + const src = `/images/${direction}.png` + return ( + + +
+ +
+
+ ) +} diff --git a/src/components/StartButton.tsx b/src/components/StartButton.tsx index 1d4202e..b528bd2 100644 --- a/src/components/StartButton.tsx +++ b/src/components/StartButton.tsx @@ -1,16 +1,16 @@ import React from 'react' -import styled from 'styled-components' - -const StyledStartButton = styled.button` - background: green; -` +import { Button } from '@chakra-ui/react' type Props = { onClick: React.MouseEventHandler } const StartButton: React.FC = ({ onClick }: Props) => { - return Start + return ( + + ) } export default StartButton diff --git a/src/components/StopButton.tsx b/src/components/StopButton.tsx index 43c85a0..59f419c 100644 --- a/src/components/StopButton.tsx +++ b/src/components/StopButton.tsx @@ -1,16 +1,17 @@ import React from 'react' -import styled from 'styled-components' - -const StyledStopButton = styled.button` - background: blue; -` +import { Button } from '@chakra-ui/react' type Props = { onClick: React.MouseEventHandler + isDownloading?: boolean } -const StopButton: React.FC = ({ onClick }: Props) => { - return Stop +const StopButton: React.FC = ({ isDownloading, onClick }: Props) => { + return ( + + ) } export default StopButton diff --git a/src/components/UnlockButton.tsx b/src/components/UnlockButton.tsx new file mode 100644 index 0000000..6049e9b --- /dev/null +++ b/src/components/UnlockButton.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Button } from '@chakra-ui/react' + +type Props = { + onClick?: React.MouseEventHandler +} + +const UnlockButton: React.FC = ({ onClick }: Props) => { + return ( + + ) +} + +export default UnlockButton diff --git a/src/components/UploadFileList.tsx b/src/components/UploadFileList.tsx index 54762e8..e894fdf 100644 --- a/src/components/UploadFileList.tsx +++ b/src/components/UploadFileList.tsx @@ -1,13 +1,38 @@ import React from 'react' -import { UploadedFile } from '../types' +import { Box, Text, Badge, HStack, VStack } from '@chakra-ui/react' + +type UploadedFileLike = { + fullPath: string + type: string +} interface Props { - files: UploadedFile[] + files: UploadedFileLike[] } const UploadFileList: React.FC = ({ files }: Props) => { - const items = files.map((f) =>
  • {f.fullPath}
  • ) - return
      {items}
    + const items = files.map((f: UploadedFileLike, i: number) => ( + + + + {f.fullPath.slice(1)} + + {f.type} + + + )) + + return ( + + {items} + + ) } export default UploadFileList diff --git a/src/components/Uploader.tsx b/src/components/Uploader.tsx index 37cbe28..8c68803 100644 --- a/src/components/Uploader.tsx +++ b/src/components/Uploader.tsx @@ -4,8 +4,19 @@ import { useWebRTC } from './WebRTCProvider' import useFetch from 'use-http' import Peer, { DataConnection } from 'peerjs' import { decodeMessage, Message, MessageType } from '../messages' +import { + Box, + Button, + Input, + HStack, + useClipboard, + VStack, +} from '@chakra-ui/react' +import QRCode from 'react-qr-code' import produce from 'immer' import * as t from 'io-ts' +import Loading from './Loading' +import ProgressBar from './ProgressBar' enum UploaderConnectionStatus { Pending = 'PENDING', @@ -308,37 +319,62 @@ export default function Uploader({ useUploaderChannelRenewal(shortSlug) const connections = useUploaderConnections(peer, files, password) + const hostPrefix = + window.location.protocol + + '//' + + window.location.hostname + + (['80', '443'].includes(window.location.port) + ? '' + : ':' + window.location.port) + const longURL = `${hostPrefix}/download/${longSlug}` + const shortURL = `${hostPrefix}/download/${shortSlug}` + const { hasCopied: hasCopiedLongURL, onCopy: onCopyLongURL } = useClipboard( + longURL, + ) + const { hasCopied: hasCopiedShortURL, onCopy: onCopyShortURL } = useClipboard( + shortURL, + ) + if (!longSlug || !shortSlug) { - return null + return } - const longURL = `/download/${longSlug}` - const shortURL = `/download/${shortSlug}` - - const items = files.map((f) =>
  • {f.fullPath}
  • ) return ( <> -
    - Long: - - {longURL} - -
    -
    - Short: - - {shortURL} - -
    -
      {items}
    -

    Connections

    -
      - {connections.map((conn) => ( -
    • - {conn.status} {conn.browserName} {conn.browserVersion} -
    • - ))} -
    + + + + + + + + + + + + + + + + {connections.map((conn, i) => ( + + {/* TODO(@kern): Make this look nicer */} + {conn.status} {conn.browserName} {conn.browserVersion} + + + ))} ) } diff --git a/src/components/WebRTCProvider.tsx b/src/components/WebRTCProvider.tsx index 034bd9b..19b7784 100644 --- a/src/components/WebRTCProvider.tsx +++ b/src/components/WebRTCProvider.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react' import type { default as PeerType } from 'peerjs' +import Loading from './Loading' // eslint-disable-next-line @typescript-eslint/no-var-requires const Peer = process.browser ? require('peerjs').default : null @@ -46,7 +47,7 @@ export function WebRTCProvider({ }, []) if (!loaded || !peer.current) { - return null + return } return ( diff --git a/src/components/Wordmark.tsx b/src/components/Wordmark.tsx new file mode 100644 index 0000000..ac1f135 --- /dev/null +++ b/src/components/Wordmark.tsx @@ -0,0 +1,6 @@ +import React from 'react' +import { Img } from '@chakra-ui/react' + +export default function Wordmark(): JSX.Element { + return +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 9c1f167..a0315ef 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,7 +1,55 @@ import React from 'react' -import type { AppProps } from 'next/app' +import { AppProps } from 'next/app' import Head from 'next/head' import Footer from '../components/Footer' +import { ChakraProvider, extendTheme, Container } from '@chakra-ui/react' +import '../styles.css' + +const theme = extendTheme({ + colors: { + brand: {}, + }, + textStyles: { + description: { + color: 'gray.500', + fontSize: '18px', + lineHeight: '20px', + letterSpacing: '2%', + }, + descriptionSmall: { + color: 'gray.500', + fontSize: '12px', + lineHeight: '20px', + letterSpacing: '2%', + }, + descriptionError: { + color: 'red.500', + fontSize: '18px', + lineHeight: '20px', + letterSpacing: '2%', + }, + fileName: { + color: 'gray.900', + fontSize: '12px', + lineHeight: '20px', + fontFamily: 'monospace', + }, + footer: { + fontSize: '12px', + lineHeight: '20px', + letterSpacing: '2%', + }, + footerLink: { + color: 'gray.500', + }, + h2: { + fontSize: ['36px', '48px'], + fontWeight: 'semibold', + lineHeight: '110%', + letterSpacing: '-1%', + }, + }, +}) const App: React.FC = ({ Component, pageProps }: AppProps) => ( <> @@ -20,9 +68,15 @@ const App: React.FC = ({ Component, pageProps }: AppProps) => ( - + + + + -