Add lots of styling

pull/152/head
Alex Kern 5 years ago
parent a7a6c5a1b3
commit 593ffe1a95
No known key found for this signature in database
GPG Key ID: F3141D5EDF48F89F

@ -37,6 +37,7 @@
"react-device-detect": "^1.15.0", "react-device-detect": "^1.15.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-qr": "0.0.2", "react-qr": "0.0.2",
"react-qr-code": "^1.0.5",
"streamsaver": "^2.0.5", "streamsaver": "^2.0.5",
"styled-components": "^5.2.0", "styled-components": "^5.2.0",
"twilio": "^2.9.1", "twilio": "^2.9.1",

@ -0,0 +1,16 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
onClick: React.MouseEventHandler
}
const CancelButton: React.FC<Props> = ({ onClick }: Props) => {
return (
<Button onClick={onClick} variant="outline">
Cancel
</Button>
)
}
export default CancelButton

@ -0,0 +1,16 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
onClick?: React.MouseEventHandler
}
const DownloadButton: React.FC<Props> = ({ onClick }: Props) => {
return (
<Button onClick={onClick} colorScheme="green">
Download
</Button>
)
}
export default DownloadButton

@ -12,6 +12,14 @@ import * as t from 'io-ts'
import { ChunkMessage, decodeMessage, Message, MessageType } from '../messages' import { ChunkMessage, decodeMessage, Message, MessageType } from '../messages'
import { createZipStream } from '../zip-stream' import { createZipStream } from '../zip-stream'
import { DataConnection } from 'peerjs' 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' const baseURL = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
@ -28,6 +36,14 @@ function getZipFilename(): string {
return `filepizza-download-${Date.now()}.zip` 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 = { type DownloadFileStream = {
name: string name: string
size: number size: number
@ -97,6 +113,7 @@ export default function Downloader({
const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false) const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [downloading, setDownloading] = useState(false) const [downloading, setDownloading] = useState(false)
const [bytesDownloaded, setBytesDownloaded] = useState(0)
const [done, setDone] = useState(false) const [done, setDone] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null) const [errorMessage, setErrorMessage] = useState<string | null>(null)
@ -111,7 +128,7 @@ export default function Downloader({
setDataConnection(conn) setDataConnection(conn)
conn.on('open', () => { const handleOpen = () => {
setOpen(true) setOpen(true)
const request: t.TypeOf<typeof Message> = { const request: t.TypeOf<typeof Message> = {
@ -126,13 +143,14 @@ export default function Downloader({
} }
conn.send(request) conn.send(request)
}) }
conn.on('data', (data) => { const handleData = (data: unknown) => {
try { try {
const message = decodeMessage(data) const message = decodeMessage(data)
switch (message.type) { switch (message.type) {
case MessageType.Info: case MessageType.Info:
console.log(message.files)
setFilesInfo(message.files) setFilesInfo(message.files)
break break
@ -149,27 +167,47 @@ export default function Downloader({
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
}) }
conn.on('close', () => { const handleClose = () => {
setDataConnection(null) setDataConnection(null)
setOpen(false) setOpen(false)
setDownloading(false) setDownloading(false)
setShouldAttemptConnection(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 () => { return () => {
if (conn.open) conn.close() 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]) }, [peer, password, shouldAttemptConnection])
const handleChangePassword = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
},
[],
)
const handleSubmitPassword = useCallback((ev) => { const handleSubmitPassword = useCallback((ev) => {
ev.preventDefault() ev.preventDefault()
setShouldAttemptConnection(true) setShouldAttemptConnection(true)
@ -225,6 +263,7 @@ export default function Downloader({
return return
} }
setBytesDownloaded((bd) => bd + (message.bytes as ArrayBuffer).byteLength)
const uInt8 = new Uint8Array(message.bytes as ArrayBuffer) const uInt8 = new Uint8Array(message.bytes as ArrayBuffer)
fileStream.enqueue(uInt8) fileStream.enqueue(uInt8)
if (message.final) { if (message.final) {
@ -264,31 +303,87 @@ export default function Downloader({
startNextFileOrFinish() startNextFileOrFinish()
}, [dataConnection, filesInfo]) }, [dataConnection, filesInfo])
if (done) { const handleStopDownload = useCallback(() => {
return <div>Done!</div> // TODO(@kern): Implement me
}, [])
const totalSize = filesInfo
? filesInfo.reduce((acc, info) => acc + info.size, 0)
: 0
if (done && filesInfo) {
return (
<VStack spacing="20px" w="100%">
<Text textStyle="description">
You downloaded {filesInfo.length} files.
</Text>
<UploadFileList files={filesInfo} />
<Box w="100%">
<ProgressBar value={bytesDownloaded} max={totalSize} />
</Box>
</VStack>
)
} }
if (downloading) { if (downloading && filesInfo) {
return <div>Downloading</div> return (
<VStack spacing="20px" w="100%">
<Text textStyle="description">
You are about to start downloading {filesInfo.length} files.
</Text>
<UploadFileList files={filesInfo} />
<Box w="100%">
<ProgressBar value={bytesDownloaded} max={totalSize} />
</Box>
<StopButton onClick={handleStopDownload} isDownloading />
</VStack>
)
} }
if (open) { if (open && filesInfo) {
return ( return (
<div> <VStack spacing="20px" w="100%">
<button onClick={handleStartDownload}>Download</button> <Text textStyle="description">
</div> You are about to start downloading {filesInfo.length} files.
</Text>
<UploadFileList files={filesInfo} />
<DownloadButton onClick={handleStartDownload} />
</VStack>
) )
} }
if (open) {
return <Loading text="Listing uploaded files" />
}
// TODO(@kern): Connect immediately, then have server respond if password is needed.
if (shouldAttemptConnection) { if (shouldAttemptConnection) {
return <div>Loading...</div> return <Loading text="Connecting to uploader" />
} }
return ( return (
<form action="#" method="post" onSubmit={handleSubmitPassword}> <chakra.form
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>} action="#"
<input type="password" value={password} onChange={handleChangePassword} /> method="post"
<button>Unlock</button> onSubmit={handleSubmitPassword}
</form> w="100%"
>
<VStack spacing="20px" w="100%">
{errorMessage ? (
<Text textStyle="descriptionError">{errorMessage}</Text>
) : (
<Text textStyle="description">
This download requires a password.
</Text>
)}
<PasswordField
value={password}
onChange={setPassword}
isRequired
isInvalid={Boolean(errorMessage)}
/>
<UnlockButton onClick={handleSubmitPassword} />
</VStack>
</chakra.form>
) )
} }

@ -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 = () => ( const DONATE_HREF =
<footer className="footer"> 'https://commerce.coinbase.com/checkout/247b6ffe-fb4e-47a8-9a76-e6b7ef83ea22'
<p>
<strong>Like FilePizza?</strong> Support its development!{' '}
<a
href="https://commerce.coinbase.com/checkout/247b6ffe-fb4e-47a8-9a76-e6b7ef83ea22"
className="donate-button"
>
donate
</a>
</p>
<p className="byline"> export const Footer: React.FC = () => {
Cooked up by{' '} const handleDonate = useCallback(() => {
<a href="http://kern.io" target="_blank"> window.location.href = DONATE_HREF
Alex Kern }, [])
</a>{' '}
&amp;{' '} return (
<a href="http://neeraj.io" target="_blank"> <chakra.footer textAlign="center" textStyle="footer" paddingY="10px">
Neeraj Baid <VStack spacing="4px">
</a>{' '} <HStack>
while eating <strong>Sliver</strong> @ UC Berkeley &middot;{' '} <Text>
<a href="https://github.com/kern/filepizza#faq" target="_blank"> <strong>Like FilePizza?</strong> Support its development!{' '}
FAQ </Text>
</a>{' '} <Button size="xs" onClick={handleDonate}>
&middot;{' '} donate
<a href="https://github.com/kern/filepizza" target="_blank"> </Button>
Fork us </HStack>
</a>
</p> <Text>
</footer> Cooked up by{' '}
) <Link textStyle="footerLink" href="http://kern.io" isExternal>
Alex Kern
</Link>{' '}
&amp;{' '}
<Link textStyle="footerLink" href="http://neeraj.io" isExternal>
Neeraj Baid
</Link>{' '}
while eating <strong>Sliver</strong> @ UC Berkeley &middot;{' '}
<Link
textStyle="footerLink"
href="https://github.com/kern/filepizza#faq"
isExternal
>
FAQ
</Link>{' '}
&middot;{' '}
<Link
textStyle="footerLink"
href="https://github.com/kern/filepizza"
isExternal
>
Fork us
</Link>
</Text>
</VStack>
</chakra.footer>
)
}
export default Footer export default Footer

@ -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 (
<VStack>
<Spinner color="blue.500" />
<Text textStyle="descriptionSmall">{text}</Text>
</VStack>
)
}

@ -1,25 +1,33 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import styled from 'styled-components' import { Input } from '@chakra-ui/react'
const StyledPasswordInput = styled.input`
background: red;
`
interface Props { interface Props {
value: string value: string
onChange: (value: string) => void onChange: (value: string) => void
isRequired?: boolean
isInvalid?: boolean
} }
export const PasswordField: React.FC<Props> = ({ value, onChange }: Props) => { export const PasswordField: React.FC<Props> = ({
value,
onChange,
isRequired,
isInvalid,
}: Props) => {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value) onChange(e.target.value)
}, []) }, [])
return ( return (
<StyledPasswordInput <Input
autoFocus
type="password" type="password"
placeholder={
isRequired ? 'Enter password...' : 'Add password (optional)...'
}
value={value} value={value}
onChange={handleChange} onChange={handleChange}
isInvalid={isInvalid}
/> />
) )
} }

@ -0,0 +1,19 @@
import { Progress } from '@chakra-ui/react'
export default function ProgressBar({
value,
max,
}: {
value: number
max: number
}): JSX.Element {
return (
<Progress
height="48px"
colorScheme={value === max ? 'green' : 'blue'}
value={value}
max={max}
borderRadius="md"
/>
)
}

@ -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 (
<Box pos="relative" w="300px" h="300px">
<Img
src="/images/pizza.png"
animation={isRotating ? `${rotate} 5s infinite linear` : 'none'}
/>
<Center pos="absolute" top="0" left="0" w="100%" h="100%">
<Img src={src} w="120px" />
</Center>
</Box>
)
}

@ -1,16 +1,16 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import { Button } from '@chakra-ui/react'
const StyledStartButton = styled.button`
background: green;
`
type Props = { type Props = {
onClick: React.MouseEventHandler onClick: React.MouseEventHandler
} }
const StartButton: React.FC<Props> = ({ onClick }: Props) => { const StartButton: React.FC<Props> = ({ onClick }: Props) => {
return <StyledStartButton onClick={onClick}>Start</StyledStartButton> return (
<Button onClick={onClick} colorScheme="green">
Start
</Button>
)
} }
export default StartButton export default StartButton

@ -1,16 +1,17 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import { Button } from '@chakra-ui/react'
const StyledStopButton = styled.button`
background: blue;
`
type Props = { type Props = {
onClick: React.MouseEventHandler onClick: React.MouseEventHandler
isDownloading?: boolean
} }
const StopButton: React.FC<Props> = ({ onClick }: Props) => { const StopButton: React.FC<Props> = ({ isDownloading, onClick }: Props) => {
return <StyledStopButton onClick={onClick}>Stop</StyledStopButton> return (
<Button size="xs" colorScheme="orange" variant="ghost" onClick={onClick}>
{isDownloading ? 'Stop Download' : 'Stop Upload'}
</Button>
)
} }
export default StopButton export default StopButton

@ -0,0 +1,16 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
onClick?: React.MouseEventHandler
}
const UnlockButton: React.FC<Props> = ({ onClick }: Props) => {
return (
<Button onClick={onClick} colorScheme="green">
Unlock
</Button>
)
}
export default UnlockButton

@ -1,13 +1,38 @@
import React from 'react' 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 { interface Props {
files: UploadedFile[] files: UploadedFileLike[]
} }
const UploadFileList: React.FC<Props> = ({ files }: Props) => { const UploadFileList: React.FC<Props> = ({ files }: Props) => {
const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>) const items = files.map((f: UploadedFileLike, i: number) => (
return <ul>{items}</ul> <Box key={f.fullPath} w="100%">
<HStack
justify="space-between"
paddingY="8px"
paddingX="10px"
borderTopColor="gray.100"
borderTopWidth={i === 0 ? '0' : '1px'}
>
<Text textStyle="fileName" isTruncated>
{f.fullPath.slice(1)}
</Text>
<Badge size="sm">{f.type}</Badge>
</HStack>
</Box>
))
return (
<VStack w="100%" spacing="0" borderRadius="md" boxShadow="base">
{items}
</VStack>
)
} }
export default UploadFileList export default UploadFileList

@ -4,8 +4,19 @@ import { useWebRTC } from './WebRTCProvider'
import useFetch from 'use-http' import useFetch from 'use-http'
import Peer, { DataConnection } from 'peerjs' import Peer, { DataConnection } from 'peerjs'
import { decodeMessage, Message, MessageType } from '../messages' 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 produce from 'immer'
import * as t from 'io-ts' import * as t from 'io-ts'
import Loading from './Loading'
import ProgressBar from './ProgressBar'
enum UploaderConnectionStatus { enum UploaderConnectionStatus {
Pending = 'PENDING', Pending = 'PENDING',
@ -308,37 +319,62 @@ export default function Uploader({
useUploaderChannelRenewal(shortSlug) useUploaderChannelRenewal(shortSlug)
const connections = useUploaderConnections(peer, files, password) 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) { if (!longSlug || !shortSlug) {
return null return <Loading text="Creating channel" />
} }
const longURL = `/download/${longSlug}`
const shortURL = `/download/${shortSlug}`
const items = files.map((f) => <li key={f.fullPath}>{f.fullPath}</li>)
return ( return (
<> <>
<div> <HStack w="100%">
Long: <Box flex="none">
<a href={longURL} target="_blank"> <QRCode value={shortURL} size={88} />
{longURL} </Box>
</a> <VStack flex="auto">
</div> <HStack w="100%">
<div> <Input value={longURL} isReadOnly fontSize="10px" />
Short: <Button
<a href={shortURL} target="_blank"> onClick={onCopyLongURL}
{shortURL} variant="ghost"
</a> colorScheme="blackAlpha"
</div> >
<ul>{items}</ul> {hasCopiedLongURL ? 'Copied' : 'Copy'}
<h2>Connections</h2> </Button>
<ul> </HStack>
{connections.map((conn) => ( <HStack w="100%">
<li> <Input value={shortURL} isReadOnly fontSize="10px" />
{conn.status} {conn.browserName} {conn.browserVersion} <Button
</li> onClick={onCopyShortURL}
))} variant="ghost"
</ul> colorScheme="blackAlpha"
>
{hasCopiedShortURL ? 'Copied' : 'Copy'}
</Button>
</HStack>
</VStack>
</HStack>
{connections.map((conn, i) => (
<Box key={i} w="100%">
{/* TODO(@kern): Make this look nicer */}
{conn.status} {conn.browserName} {conn.browserVersion}
<ProgressBar value={50} max={100} />
</Box>
))}
</> </>
) )
} }

@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef, useContext } from 'react' import React, { useState, useEffect, useRef, useContext } from 'react'
import type { default as PeerType } from 'peerjs' import type { default as PeerType } from 'peerjs'
import Loading from './Loading'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Peer = process.browser ? require('peerjs').default : null const Peer = process.browser ? require('peerjs').default : null
@ -46,7 +47,7 @@ export function WebRTCProvider({
}, []) }, [])
if (!loaded || !peer.current) { if (!loaded || !peer.current) {
return null return <Loading text="Initializing WebRTC" />
} }
return ( return (

@ -0,0 +1,6 @@
import React from 'react'
import { Img } from '@chakra-ui/react'
export default function Wordmark(): JSX.Element {
return <Img src="/images/wordmark.png" maxH="48px" />
}

@ -1,7 +1,55 @@
import React from 'react' import React from 'react'
import type { AppProps } from 'next/app' import { AppProps } from 'next/app'
import Head from 'next/head' import Head from 'next/head'
import Footer from '../components/Footer' 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<AppProps> = ({ Component, pageProps }: AppProps) => ( const App: React.FC<AppProps> = ({ Component, pageProps }: AppProps) => (
<> <>
@ -20,9 +68,15 @@ const App: React.FC<AppProps> = ({ Component, pageProps }: AppProps) => (
<meta property="og:title" content="FilePizza" key="title" /> <meta property="og:title" content="FilePizza" key="title" />
</Head> </Head>
<Component {...pageProps} /> <ChakraProvider theme={theme}>
<Container flex="auto">
<Component {...pageProps} />
</Container>
<Footer /> <Container flex="none">
<Footer />
</Container>
</ChakraProvider>
</> </>
) )

@ -3,6 +3,9 @@ import WebRTCProvider from '../../components/WebRTCProvider'
import Downloader from '../../components/Downloader' import Downloader from '../../components/Downloader'
import { NextPage, GetServerSideProps } from 'next' import { NextPage, GetServerSideProps } from 'next'
import { channelRepo } from '../../channel' import { channelRepo } from '../../channel'
import { VStack } from '@chakra-ui/react'
import Spinner from '../../components/Spinner'
import Wordmark from '../../components/Wordmark'
type Props = { type Props = {
slug: string slug: string
@ -12,9 +15,13 @@ type Props = {
const DownloadPage: NextPage<Props> = ({ uploaderPeerID }) => { const DownloadPage: NextPage<Props> = ({ uploaderPeerID }) => {
return ( return (
<WebRTCProvider> <VStack spacing="20px" paddingY="40px" w="100%">
<Downloader uploaderPeerID={uploaderPeerID} /> <Spinner direction="down" />
</WebRTCProvider> <Wordmark />
<WebRTCProvider>
<Downloader uploaderPeerID={uploaderPeerID} />
</WebRTCProvider>
</VStack>
) )
} }

@ -8,6 +8,10 @@ import StartButton from '../components/StartButton'
import StopButton from '../components/StopButton' import StopButton from '../components/StopButton'
import { UploadedFile } from '../types' import { UploadedFile } from '../types'
import { NextPage } from 'next' import { NextPage } from 'next'
import Spinner from '../components/Spinner'
import { ButtonGroup, Text, VStack } from '@chakra-ui/react'
import Wordmark from '../components/Wordmark'
import CancelButton from '../components/CancelButton'
export const IndexPage: NextPage = () => { export const IndexPage: NextPage = () => {
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]) const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
@ -30,30 +34,60 @@ export const IndexPage: NextPage = () => {
setUploading(false) setUploading(false)
}, []) }, [])
const handleCancel = useCallback(() => {
setUploadedFiles([])
setUploading(false)
}, [])
if (!uploadedFiles.length) { if (!uploadedFiles.length) {
return ( return (
<> <VStack spacing="20px" paddingY="40px">
<Spinner direction="up" />
<Wordmark />
<VStack spacing="4px">
<Text textStyle="description">
Peer-to-peer file transfers in your browser.
</Text>
<Text textStyle="descriptionSmall">
We never store anything. Files only served fresh.
</Text>
</VStack>
<DropZone onDrop={handleDrop}>Drop a file to get started.</DropZone> <DropZone onDrop={handleDrop}>Drop a file to get started.</DropZone>
</> </VStack>
) )
} }
if (!uploading) { if (!uploading) {
return ( return (
<> <VStack spacing="20px" paddingY="40px">
<Spinner direction="up" />
<Wordmark />
<Text textStyle="description">
You are about to start uploading {uploadedFiles.length} files.
</Text>
<UploadFileList files={uploadedFiles} /> <UploadFileList files={uploadedFiles} />
<PasswordField value={password} onChange={handleChangePassword} /> <PasswordField value={password} onChange={handleChangePassword} />
<StartButton onClick={handleStart} /> <ButtonGroup>
</> <CancelButton onClick={handleCancel} />
<StartButton onClick={handleStart} />
</ButtonGroup>
</VStack>
) )
} }
return ( return (
<WebRTCProvider> <VStack spacing="20px" paddingY="40px">
<Spinner direction="up" isRotating />
<Wordmark />
<Text textStyle="description">
You are uploading {uploadedFiles.length} files.
</Text>
<UploadFileList files={uploadedFiles} /> <UploadFileList files={uploadedFiles} />
<WebRTCProvider>
<Uploader files={uploadedFiles} password={password} />
</WebRTCProvider>
<StopButton onClick={handleStop} /> <StopButton onClick={handleStop} />
<Uploader files={uploadedFiles} password={password} /> </VStack>
</WebRTCProvider>
) )
} }

@ -0,0 +1,16 @@
html {
height: -webkit-fill-available;
}
body {
min-height: 100vh;
/* mobile viewport bug fix */
min-height: -webkit-fill-available;
}
#__next {
display: flex;
flex-direction: column;
height: 100vh;
}

@ -1755,6 +1755,15 @@
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
"@react-native-community/art@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@react-native-community/art/-/art-1.2.0.tgz#386d95393f6042d9006f9d4bc6063fb898794460"
integrity sha512-a+ZcRGl/BzLa89yi33Mbn5SHavsEXqKUMdbfLf3U8MDLElndPqUetoJyGkv63+BcPO49UMWiQRP1YUz6/zfJ+A==
dependencies:
art "^0.10.3"
invariant "^2.2.4"
prop-types "^15.7.2"
"@types/body-parser@*": "@types/body-parser@*":
version "1.19.0" version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
@ -2422,6 +2431,11 @@ array.prototype.flatmap@^1.2.3:
es-abstract "^1.17.0-next.1" es-abstract "^1.17.0-next.1"
function-bind "^1.1.1" function-bind "^1.1.1"
art@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/art/-/art-0.10.3.tgz#b01d84a968ccce6208df55a733838c96caeeaea2"
integrity sha512-HXwbdofRTiJT6qZX/FnchtldzJjS3vkLJxQilc3Xj+ma2MXjY4UAyQ0ls1XZYVnDvVIBiFZbC6QsvtW86TD6tQ==
asn1.js@^5.2.0: asn1.js@^5.2.0:
version "5.4.1" version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
@ -6944,6 +6958,11 @@ qr-image@^3.1.0:
resolved "https://registry.yarnpkg.com/qr-image/-/qr-image-3.2.0.tgz#9fa8295beae50c4a149cf9f909a1db464a8672e8" resolved "https://registry.yarnpkg.com/qr-image/-/qr-image-3.2.0.tgz#9fa8295beae50c4a149cf9f909a1db464a8672e8"
integrity sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug= integrity sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=
qr.js@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
qs@6.7.0: qs@6.7.0:
version "6.7.0" version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@ -7059,6 +7078,15 @@ react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-qr-code@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-1.0.5.tgz#06d8b64111ee731d286093f96ca7585370811fc9"
integrity sha512-gpiry47RixhxJ2XLtVpEqSGzZjOhQ9/2xRrTqaPnBG4asMM267+mpkxDTNis9aPzJr+EcpYNmMzwjjCqXDjp8w==
dependencies:
"@react-native-community/art" "^1.2.0"
prop-types "^15.7.2"
qr.js "0.0.0"
react-qr@0.0.2: react-qr@0.0.2:
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/react-qr/-/react-qr-0.0.2.tgz#1892a37520a092ba1118a3a14f8cf85c23661187" resolved "https://registry.yarnpkg.com/react-qr/-/react-qr-0.0.2.tgz#1892a37520a092ba1118a3a14f8cf85c23661187"

Loading…
Cancel
Save