migrate off chakra entirely to tailwind

pull/134/head
Alex Kern 1 year ago
parent e73acc3eda
commit dd25e6f7d8
No known key found for this signature in database
GPG Key ID: EF051FACCACBEE25

5
next-env.d.ts vendored

@ -1,4 +1,5 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
declare module 'xkcd-password'
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

@ -20,50 +20,48 @@
"url": "https://github.com/kern/filepizza/issues"
},
"dependencies": {
"@chakra-ui/react": "^1.3.2",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
"debug": "^4.2.0",
"express": "^4.12.0",
"fp-ts": "^2.9.3",
"framer-motion": "^3.3.0",
"immer": "^8.0.0",
"io-ts": "^2.2.13",
"ioredis": "^4.17.3",
"next": "^9.5.3",
"nodemon": "^1.4.1",
"autoprefixer": "^10.4.20",
"debug": "^4.3.6",
"express": "^4.19.2",
"fp-ts": "^2.16.9",
"framer-motion": "^3.10.6",
"immer": "^8.0.4",
"io-ts": "^2.2.21",
"ioredis": "^4.28.5",
"next": "^14.2.7",
"nodemon": "^1.19.4",
"peer": "^0.5.3",
"peerjs": "^1.3.1",
"react": "^16.13.1",
"react-device-detect": "^1.15.0",
"react-dom": "^16.13.1",
"peerjs": "^1.5.4",
"postcss": "^8.4.44",
"react": "^18.3.1",
"react-device-detect": "^1.17.0",
"react-dom": "^18.3.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",
"use-http": "^1.0.16",
"web-streams-polyfill": "^3.0.1",
"react-qr-code": "^1.1.1",
"streamsaver": "^2.0.6",
"tailwindcss": "^3.4.10",
"twilio": "^2.11.1",
"use-http": "^1.0.28",
"web-streams-polyfill": "^3.3.3",
"webrtcsupport": "^2.2.0",
"xkcd-password": "^1.2.0"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/ioredis": "^4.17.4",
"@types/node": "^14.11.1",
"@types/react": "^16.9.49",
"@types/styled-components": "^5.1.3",
"@typescript-eslint/eslint-plugin": "^4.1.1",
"@typescript-eslint/parser": "^4.1.1",
"eslint": "^7.9.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"prettier": "^2.1.2",
"typescript": "^4.0.3"
"@types/debug": "^4.1.12",
"@types/ioredis": "^4.28.10",
"@types/node": "^14.18.63",
"@types/react": "^16.14.60",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-react": "^7.35.1",
"husky": "^4.3.8",
"lint-staged": "^10.5.4",
"prettier": "^2.8.8",
"typescript": "^4.9.5"
},
"husky": {
"hooks": {
@ -76,4 +74,4 @@
"git add"
]
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

@ -1,5 +1,4 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
onClick: React.MouseEventHandler
@ -7,9 +6,12 @@ type Props = {
const CancelButton: React.FC<Props> = ({ onClick }: Props) => {
return (
<Button onClick={onClick} variant="outline">
<button
onClick={onClick}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Cancel
</Button>
</button>
)
}

@ -1,5 +1,4 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
onClick?: React.MouseEventHandler
@ -7,9 +6,12 @@ type Props = {
const DownloadButton: React.FC<Props> = ({ onClick }: Props) => {
return (
<Button onClick={onClick} colorScheme="green">
<button
onClick={onClick}
className="px-4 py-2 bg-green-500 text-white font-semibold rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
>
Download
</Button>
</button>
)
}

@ -14,7 +14,6 @@ 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'
@ -313,42 +312,42 @@ export default function Downloader({
if (done && filesInfo) {
return (
<VStack spacing="20px" w="100%">
<Text textStyle="description">
<div className="flex flex-col space-y-5 w-full">
<p className="text-description">
You downloaded {filesInfo.length} files.
</Text>
</p>
<UploadFileList files={filesInfo} />
<Box w="100%">
<div className="w-full">
<ProgressBar value={bytesDownloaded} max={totalSize} />
</Box>
</VStack>
</div>
</div>
)
}
if (downloading && filesInfo) {
return (
<VStack spacing="20px" w="100%">
<Text textStyle="description">
<div className="flex flex-col space-y-5 w-full">
<p className="text-description">
You are about to start downloading {filesInfo.length} files.
</Text>
</p>
<UploadFileList files={filesInfo} />
<Box w="100%">
<div className="w-full">
<ProgressBar value={bytesDownloaded} max={totalSize} />
</Box>
</div>
<StopButton onClick={handleStopDownload} isDownloading />
</VStack>
</div>
)
}
if (open && filesInfo) {
return (
<VStack spacing="20px" w="100%">
<Text textStyle="description">
<div className="flex flex-col space-y-5 w-full">
<p className="text-description">
You are about to start downloading {filesInfo.length} files.
</Text>
</p>
<UploadFileList files={filesInfo} />
<DownloadButton onClick={handleStartDownload} />
</VStack>
</div>
)
}
@ -362,19 +361,17 @@ export default function Downloader({
}
return (
<chakra.form
<form
action="#"
method="post"
onSubmit={handleSubmitPassword}
w="100%"
className="w-full"
>
<VStack spacing="20px" w="100%">
<div className="flex flex-col space-y-5 w-full">
{errorMessage ? (
<Text textStyle="descriptionError">{errorMessage}</Text>
<p className="text-description-error">{errorMessage}</p>
) : (
<Text textStyle="description">
This download requires a password.
</Text>
<p className="text-description">This download requires a password.</p>
)}
<PasswordField
value={password}
@ -383,7 +380,7 @@ export default function Downloader({
isInvalid={Boolean(errorMessage)}
/>
<UnlockButton onClick={handleSubmitPassword} />
</VStack>
</chakra.form>
</div>
</form>
)
}

@ -1,65 +1,109 @@
import React, { useState, useRef, useCallback } from 'react'
import styled from 'styled-components'
import React, { useState, useCallback, useEffect, useRef } from 'react'
import { extractFileList } from '../fs'
const Wrapper = styled.div`
display: block;
`
const Overlay = styled.div`
display: block;
`
export default function DropZone({
children,
onDrop,
}: {
onDrop: (files: File[]) => void
children?: React.ReactNode
}): JSX.Element {
const overlay = useRef()
const [focus, setFocus] = useState(false)
const [isDragging, setIsDragging] = useState(false)
const [fileCount, setFileCount] = useState(0)
const fileInputRef = useRef<HTMLInputElement>(null)
const handleDragEnter = useCallback(() => {
setFocus(true)
const handleDragEnter = useCallback((e: DragEvent) => {
e.preventDefault()
setIsDragging(true)
setFileCount(e.dataTransfer?.items.length || 0)
}, [])
const handleDragLeave = useCallback(
(e: React.DragEvent) => {
if (e.target !== overlay.current) {
return
}
const handleDragLeave = useCallback((e: DragEvent) => {
e.preventDefault()
setFocus(false)
},
[overlay.current],
)
const currentTarget =
e.currentTarget === window ? window.document : e.currentTarget
if (
e.relatedTarget &&
currentTarget instanceof Node &&
currentTarget.contains(e.relatedTarget as Node)
) {
return
}
const handleDragOver = useCallback((e: React.DragEvent) => {
setIsDragging(false)
}, [])
const handleDragOver = useCallback((e: DragEvent) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy'
}
}, [])
const handleDrop = useCallback(
async (e: React.DragEvent) => {
async (e: DragEvent) => {
e.preventDefault()
setFocus(false)
setIsDragging(false)
const files = await extractFileList(e)
onDrop(files)
if (e.dataTransfer) {
const files = await extractFileList(e)
onDrop(files)
}
},
[onDrop],
)
const handleClick = useCallback(() => {
fileInputRef.current?.click()
}, [])
const handleFileInputChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files)
onDrop(files)
}
},
[onDrop],
)
useEffect(() => {
window.addEventListener('dragenter', handleDragEnter)
window.addEventListener('dragleave', handleDragLeave)
window.addEventListener('dragover', handleDragOver)
window.addEventListener('drop', handleDrop)
return () => {
window.removeEventListener('dragenter', handleDragEnter)
window.removeEventListener('dragleave', handleDragLeave)
window.removeEventListener('dragover', handleDragOver)
window.removeEventListener('drop', handleDrop)
}
}, [handleDragEnter, handleDragLeave, handleDragOver, handleDrop])
return (
<Wrapper
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<Overlay ref={overlay} hidden={!focus} />
{children}
</Wrapper>
<>
<div
className={`fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center text-2xl text-white transition-opacity duration-300 backdrop-blur-sm z-50 ${
isDragging ? 'opacity-100 visible' : 'opacity-0 invisible'
}`}
>
Drop to select {fileCount} file{fileCount !== 1 ? 's' : ''}
</div>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileInputChange}
multiple
/>
<button
className="inline-block cursor-pointer relative py-3 px-6 text-base font-bold text-gray-700 bg-white border-2 border-gray-700 rounded-lg transition-all duration-300 ease-in-out outline-none hover:shadow-md active:shadow-inner focus:shadow-outline"
onClick={handleClick}
>
<span className="text-center text-stone-700">
Drop a file to get started
</span>
</button>
</>
)
}

@ -1,54 +1,56 @@
import { chakra, Text, Link, Button, VStack, HStack } from '@chakra-ui/react'
import React, { useCallback } from 'react'
const DONATE_HREF =
'https://commerce.coinbase.com/checkout/247b6ffe-fb4e-47a8-9a76-e6b7ef83ea22'
export const Footer: React.FC = () => {
const FooterLink: React.FC<{ href: string; children: React.ReactNode }> = ({
href,
children,
}) => (
<a
className="text-stone-600 underline hover:text-stone-800"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
)
export function Footer(): JSX.Element {
const handleDonate = useCallback(() => {
window.location.href = DONATE_HREF
}, [])
return (
<chakra.footer textAlign="center" textStyle="footer" paddingY="10px">
<VStack spacing="4px">
<HStack>
<Text>
<footer className="text-center py-2.5 pb-4 text-xs border-t border-stone-200 shadow-[0_-1px_2px_rgba(0,0,0,0.04)]">
<div className="flex flex-col items-center space-y-1 px-4 sm:px-6 md:px-8">
<div className="flex items-center space-x-2">
<p className="text-stone-600">
<strong>Like FilePizza?</strong> Support its development!{' '}
</Text>
<Button size="xs" onClick={handleDonate}>
donate
</Button>
</HStack>
</p>
<button
className="px-1.5 py-0.5 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200 font-medium text-[10px]"
onClick={handleDonate}
>
Donate
</button>
</div>
<Text>
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>{' '}
<p className="text-stone-600">
Cooked up by <FooterLink href="http://kern.io">Alex Kern</FooterLink>{' '}
&amp; <FooterLink href="http://neeraj.io">Neeraj Baid</FooterLink>{' '}
while eating <strong>Sliver</strong> @ UC Berkeley &middot;{' '}
<Link
textStyle="footerLink"
href="https://github.com/kern/filepizza#faq"
isExternal
>
<FooterLink href="https://github.com/kern/filepizza#faq">
FAQ
</Link>{' '}
</FooterLink>{' '}
&middot;{' '}
<Link
textStyle="footerLink"
href="https://github.com/kern/filepizza"
isExternal
>
<FooterLink href="https://github.com/kern/filepizza">
Fork us
</Link>
</Text>
</VStack>
</chakra.footer>
</FooterLink>
</p>
</div>
</footer>
)
}

@ -1,11 +1,10 @@
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>
<div className="flex flex-col items-center">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
<p className="text-sm text-gray-600 mt-2">{text}</p>
</div>
)
}

@ -1,33 +1,35 @@
import React, { useCallback } from 'react'
import { Input } from '@chakra-ui/react'
interface Props {
value: string
onChange: (value: string) => void
isRequired?: boolean
isInvalid?: boolean
}
export const PasswordField: React.FC<Props> = ({
export function PasswordField({
value,
onChange,
isRequired,
isInvalid,
}: Props) => {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value)
}, [])
}: {
value: string
onChange: (v: string) => void
isRequired?: boolean
isInvalid?: boolean
}): JSX.Element {
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value)
},
[onChange],
)
return (
<Input
<input
autoFocus
type="password"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isInvalid ? 'border-red-500' : 'border-gray-300'
}`}
placeholder={
isRequired ? 'Enter password...' : 'Add password (optional)...'
}
value={value}
onChange={handleChange}
isInvalid={isInvalid}
/>
)
}

@ -1,4 +1,4 @@
import { Progress } from '@chakra-ui/react'
import React from 'react'
export default function ProgressBar({
value,
@ -7,13 +7,17 @@ export default function ProgressBar({
value: number
max: number
}): JSX.Element {
const percentage = (value / max) * 100
const isComplete = value === max
return (
<Progress
height="48px"
colorScheme={value === max ? 'green' : 'blue'}
value={value}
max={max}
borderRadius="md"
/>
<div className="w-full h-12 bg-gray-200 rounded-md overflow-hidden">
<div
className={`h-full ${
isComplete ? 'bg-green-500' : 'bg-blue-500'
} transition-all duration-300 ease-in-out`}
style={{ width: `${percentage}%` }}
></div>
</div>
)
}

@ -1,11 +1,5 @@
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) }
`
import Image from 'next/image'
export default function Spinner({
direction,
@ -16,14 +10,22 @@ export default function Spinner({
}): JSX.Element {
const src = `/images/${direction}.png`
return (
<Box pos="relative" w="300px" h="300px">
<Img
<div className="relative w-[300px] h-[300px]">
<Image
src="/images/pizza.png"
animation={isRotating ? `${rotate} 5s infinite linear` : 'none'}
alt="Pizza"
width={300}
height={300}
className={isRotating ? 'animate-spin-slow' : ''}
/>
<Center pos="absolute" top="0" left="0" w="100%" h="100%">
<Img src={src} w="120px" />
</Center>
</Box>
<div className="absolute inset-0 flex items-center justify-center">
<Image
src={src}
alt={`Arrow pointing ${direction}`}
width={120}
height={120}
/>
</div>
</div>
)
}

@ -1,15 +1,17 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
export function StartButton({
onClick,
}: {
onClick: React.MouseEventHandler
}
const StartButton: React.FC<Props> = ({ onClick }: Props) => {
}): JSX.Element {
return (
<Button onClick={onClick} colorScheme="green">
<button
onClick={onClick}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200"
>
Start
</Button>
</button>
)
}

@ -1,5 +1,4 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
onClick: React.MouseEventHandler
@ -8,9 +7,12 @@ type Props = {
const StopButton: React.FC<Props> = ({ isDownloading, onClick }: Props) => {
return (
<Button size="xs" colorScheme="orange" variant="ghost" onClick={onClick}>
<button
className="px-2 py-1 text-xs text-orange-500 bg-transparent hover:bg-orange-100 rounded transition-colors duration-200"
onClick={onClick}
>
{isDownloading ? 'Stop Download' : 'Stop Upload'}
</Button>
</button>
)
}

@ -1,5 +1,4 @@
import React from 'react'
import { Button } from '@chakra-ui/react'
type Props = {
onClick?: React.MouseEventHandler
@ -7,9 +6,12 @@ type Props = {
const UnlockButton: React.FC<Props> = ({ onClick }: Props) => {
return (
<Button onClick={onClick} colorScheme="green">
<button
onClick={onClick}
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200"
>
Unlock
</Button>
</button>
)
}

@ -1,38 +1,74 @@
import React from 'react'
import { Box, Text, Badge, HStack, VStack } from '@chakra-ui/react'
type UploadedFileLike = {
fullPath: string
fullPath?: string
name?: string
type: string
}
interface Props {
files: UploadedFileLike[]
onChange?: (updatedFiles: UploadedFileLike[]) => void
}
const UploadFileList: React.FC<Props> = ({ files }: Props) => {
const items = files.map((f: UploadedFileLike, i: number) => (
<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>
))
const getFileName = (file: UploadedFileLike): string => {
if (file.fullPath) {
return file.fullPath.slice(1)
}
return file.name || 'Unknown'
}
export function TypeBadge({ type }: { type: string }): JSX.Element {
const getTypeColor = (fileType: string): string => {
if (fileType.startsWith('image/')) return 'bg-blue-100 text-blue-800'
if (fileType.startsWith('text/')) return 'bg-green-100 text-green-800'
if (fileType.startsWith('audio/')) return 'bg-purple-100 text-purple-800'
if (fileType.startsWith('video/')) return 'bg-red-100 text-red-800'
return 'bg-gray-100 text-gray-800'
}
return (
<VStack w="100%" spacing="0" borderRadius="md" boxShadow="base">
{items}
</VStack>
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${getTypeColor(
type,
)} transition-all duration-300 mr-2`}
>
{type}
</span>
)
}
export function UploadFileList({ files, onChange }: Props): JSX.Element {
const handleRemove = (index: number) => {
if (onChange) {
const updatedFiles = files.filter((_, i) => i !== index)
onChange(updatedFiles)
}
}
const items = files.map((f: UploadedFileLike, i: number) => (
<div
key={getFileName(f)}
className="w-full border border-stone-300 rounded-md mb-2 group"
>
<div className="flex justify-between items-center py-2 px-2.5">
<p className="truncate text-sm font-medium">{getFileName(f)}</p>
<div className="flex items-center">
<TypeBadge type={f.type} />
{onChange && (
<button
onClick={() => handleRemove(i)}
className="text-stone-500 hover:text-stone-700 focus:outline-none"
>
</button>
)}
</div>
</div>
</div>
))
return <div className="w-full">{items}</div>
}
export default UploadFileList

@ -4,19 +4,12 @@ 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'
import useClipboard from '../hooks/useClipboard'
enum UploaderConnectionStatus {
Pending = 'PENDING',
@ -44,9 +37,7 @@ type UploaderConnection = {
const RENEW_INTERVAL = 5000 // 20 minutes
const MAX_CHUNK_SIZE = 10 * 1024 * 1024 // 10 Mi
function useUploaderChannel(
uploaderPeerID: string,
): {
function useUploaderChannel(uploaderPeerID: string): {
loading: boolean
error: Error | null
longSlug: string
@ -141,7 +132,7 @@ function useUploaderConnections(
return
}
fn(updatedConn)
fn(updatedConn as UploaderConnection)
}),
)
}
@ -240,7 +231,7 @@ function useUploaderConnections(
if (final) {
draft.status = UploaderConnectionStatus.Paused
} else {
sendChunkTimeout = setTimeout(() => {
sendChunkTimeout = window.setTimeout(() => {
sendNextChunk()
}, 0)
}
@ -328,12 +319,10 @@ export default function Uploader({
: ':' + 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,
)
const { hasCopied: hasCopiedLongURL, onCopy: onCopyLongURL } =
useClipboard(longURL)
const { hasCopied: hasCopiedShortURL, onCopy: onCopyShortURL } =
useClipboard(shortURL)
if (!longSlug || !shortSlug) {
return <Loading text="Creating channel" />
@ -341,39 +330,47 @@ export default function Uploader({
return (
<>
<HStack w="100%">
<Box flex="none">
<div className="flex w-full">
<div className="flex-none">
<QRCode value={shortURL} size={88} />
</Box>
<VStack flex="auto">
<HStack w="100%">
<Input value={longURL} isReadOnly fontSize="10px" />
<Button
</div>
<div className="flex-auto flex flex-col">
<div className="flex w-full">
<input
className="flex-grow px-2 py-1 text-xs border rounded-l"
value={longURL}
readOnly
/>
<button
className="px-4 py-1 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-r"
onClick={onCopyLongURL}
variant="ghost"
colorScheme="blackAlpha"
>
{hasCopiedLongURL ? 'Copied' : 'Copy'}
</Button>
</HStack>
<HStack w="100%">
<Input value={shortURL} isReadOnly fontSize="10px" />
<Button
</button>
</div>
<div className="flex w-full mt-2">
<input
className="flex-grow px-2 py-1 text-xs border rounded-l"
value={shortURL}
readOnly
/>
<button
className="px-4 py-1 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-r"
onClick={onCopyShortURL}
variant="ghost"
colorScheme="blackAlpha"
>
{hasCopiedShortURL ? 'Copied' : 'Copy'}
</Button>
</HStack>
</VStack>
</HStack>
</button>
</div>
</div>
</div>
{connections.map((conn, i) => (
<Box key={i} w="100%">
<div key={i} className="w-full mt-4">
{/* TODO(@kern): Make this look nicer */}
{conn.status} {conn.browserName} {conn.browserVersion}
<div className="text-sm">
{conn.status} {conn.browserName} {conn.browserVersion}
</div>
<ProgressBar value={50} max={100} />
</Box>
</div>
))}
</>
)

@ -3,7 +3,7 @@ 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
const Peer = typeof window !== 'undefined' ? require('peerjs').default : null
export type WebRTCValue = PeerType | null
@ -18,7 +18,12 @@ const ICE_SERVERS: RTCConfiguration = {
const WebRTCContext = React.createContext<WebRTCValue | null>(null)
export const useWebRTC = (): WebRTCValue => {
return useContext(WebRTCContext)!
const value = useContext(WebRTCContext)
if (value === null) {
throw new Error('useWebRTC must be used within a WebRTCProvider')
}
return value
}
export function WebRTCProvider({
@ -33,11 +38,22 @@ export function WebRTCProvider({
useEffect(() => {
const effect = async () => {
const peerObj = new Peer(undefined, {
host: '/',
port: '9000',
const peerConfig: {
host?: string
port?: string
path?: string
config: RTCConfiguration
} = {
config: servers,
})
}
if (process.env.NEXT_PUBLIC_PEERJS_HOST) {
peerConfig.host = process.env.NEXT_PUBLIC_PEERJS_HOST
peerConfig.port = process.env.NEXT_PUBLIC_PEERJS_PORT || '9000'
peerConfig.path = process.env.NEXT_PUBLIC_PEERJS_PATH || '/'
}
const peerObj = new Peer(undefined, peerConfig)
peerObj.on('open', () => {
peer.current = peerObj
@ -46,7 +62,7 @@ export function WebRTCProvider({
}
effect()
}, [])
}, [servers])
if (!loaded || !peer.current) {
return <Loading text="Initializing WebRTC" />

@ -1,6 +1,14 @@
import React from 'react'
import { Img } from '@chakra-ui/react'
import Image from 'next/image'
export default function Wordmark(): JSX.Element {
return <Img src="/images/wordmark.png" maxH="48px" />
return (
<Image
src="/images/wordmark.png"
className="max-h-12"
alt="FilePizza Wordmark"
width={200}
height={45}
/>
)
}

@ -37,28 +37,33 @@ const scanDirectoryEntry = async (entry: any): Promise<File[]> => {
}
}
export const extractFileList = async (e: React.DragEvent): Promise<File[]> => {
if (!e.dataTransfer.items.length) {
export const extractFileList = async (
e: React.DragEvent | DragEvent,
): Promise<File[]> => {
if (!e.dataTransfer || !e.dataTransfer.items.length) {
return []
}
const items = e.dataTransfer.items
const scans = []
const files = []
const scans: Promise<File[]>[] = []
const files: Promise<File>[] = []
for (const item of items) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
const entry = item.webkitGetAsEntry()
if (entry.isDirectory) {
scans.push(scanDirectoryEntry(entry))
} else {
files.push(getAsFile(entry))
if (entry) {
if (entry.isDirectory) {
scans.push(scanDirectoryEntry(entry))
} else {
files.push(getAsFile(entry))
}
}
}
const scanResults = await Promise.all(scans)
const fileResults = await Promise.all(files)
return [scanResults, fileResults].flat(2)
return scanResults.flat().concat(fileResults)
}
// Borrowed from StackOverflow

@ -0,0 +1,25 @@
import { useState, useCallback, useEffect } from 'react'
export default function useClipboard(text: string, delay = 1000) {
const [hasCopied, setHasCopied] = useState(false)
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(() => {
setHasCopied(true)
})
}, [text])
useEffect(() => {
let timeoutId: NodeJS.Timeout
if (hasCopied) {
timeoutId = setTimeout(() => {
setHasCopied(false)
}, delay)
}
return () => {
clearTimeout(timeoutId)
}
}, [hasCopied, delay])
return { hasCopied, onCopy }
}

@ -2,55 +2,8 @@ import React from 'react'
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<AppProps> = ({ Component, pageProps }: AppProps) => (
<>
<Head>
@ -68,15 +21,11 @@ const App: React.FC<AppProps> = ({ Component, pageProps }: AppProps) => (
<meta property="og:title" content="FilePizza" key="title" />
</Head>
<ChakraProvider theme={theme}>
<Container flex="auto">
<Component {...pageProps} />
</Container>
<main>
<Component {...pageProps} />
</main>
<Container flex="none">
<Footer />
</Container>
</ChakraProvider>
<Footer />
</>
)

@ -3,7 +3,6 @@ import WebRTCProvider from '../../components/WebRTCProvider'
import Downloader from '../../components/Downloader'
import { NextPage, GetServerSideProps } from 'next'
import { channelRepo } from '../../channel'
import { VStack } from '@chakra-ui/react'
import Spinner from '../../components/Spinner'
import Wordmark from '../../components/Wordmark'
@ -15,13 +14,13 @@ type Props = {
const DownloadPage: NextPage<Props> = ({ uploaderPeerID }) => {
return (
<VStack spacing="20px" paddingY="40px" w="100%">
<div className="flex flex-col items-center space-y-5 py-10 w-full">
<Spinner direction="down" />
<Wordmark />
<WebRTCProvider>
<Downloader uploaderPeerID={uploaderPeerID} />
</WebRTCProvider>
</VStack>
</div>
)
}

@ -9,7 +9,6 @@ import StopButton from '../components/StopButton'
import { UploadedFile } from '../types'
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'
@ -39,55 +38,59 @@ export const IndexPage: NextPage = () => {
setUploading(false)
}, [])
const handleFileListChange = useCallback((updatedFiles: UploadedFile[]) => {
setUploadedFiles(updatedFiles)
}, [])
if (!uploadedFiles.length) {
return (
<VStack spacing="20px" paddingY="40px">
<div className="flex flex-col items-center space-y-5 py-10">
<Spinner direction="up" />
<Wordmark />
<VStack spacing="4px">
<Text textStyle="description">
<div className="flex flex-col items-center space-y-1">
<p className="text-lg text-center text-stone-800">
Peer-to-peer file transfers in your browser.
</Text>
<Text textStyle="descriptionSmall">
</p>
<p className="text-sm text-center text-stone-600">
We never store anything. Files only served fresh.
</Text>
</VStack>
<DropZone onDrop={handleDrop}>Drop a file to get started.</DropZone>
</VStack>
</p>
</div>
<DropZone onDrop={handleDrop} />
</div>
)
}
if (!uploading) {
return (
<VStack spacing="20px" paddingY="40px">
<div className="flex flex-col items-center space-y-5 py-10">
<Spinner direction="up" />
<Wordmark />
<Text textStyle="description">
<p className="text-lg text-center text-stone-800">
You are about to start uploading {uploadedFiles.length} files.
</Text>
<UploadFileList files={uploadedFiles} />
</p>
<UploadFileList files={uploadedFiles} onChange={handleFileListChange} />
<PasswordField value={password} onChange={handleChangePassword} />
<ButtonGroup>
<div className="flex space-x-4">
<CancelButton onClick={handleCancel} />
<StartButton onClick={handleStart} />
</ButtonGroup>
</VStack>
</div>
</div>
)
}
return (
<VStack spacing="20px" paddingY="40px">
<div className="flex flex-col items-center space-y-5 py-10">
<Spinner direction="up" isRotating />
<Wordmark />
<Text textStyle="description">
<p className="text-lg text-center text-stone-800">
You are uploading {uploadedFiles.length} files.
</Text>
</p>
<UploadFileList files={uploadedFiles} />
<WebRTCProvider>
<Uploader files={uploadedFiles} password={password} />
</WebRTCProvider>
<StopButton onClick={handleStop} />
</VStack>
</div>
)
}

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

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {},
},
plugins: [],
}

@ -17,9 +17,11 @@
"resolveJsonModule": true,
"isolatedModules": true,
"downlevelIteration": true,
"jsx": "preserve"
"jsx": "preserve",
"incremental": true
},
"include": [
"tailwind.config.js",
"next-env.d.ts",
"src/**/*.js",
"src/**/*.ts",
@ -28,4 +30,4 @@
"exclude": [
"node_modules"
]
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save