mirror of https://github.com/kern/filepizza
migrate off chakra entirely to tailwind
parent
e73acc3eda
commit
dd25e6f7d8
@ -1,4 +1,5 @@
|
|||||||
/// <reference types="next" />
|
/// <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.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,65 +1,109 @@
|
|||||||
import React, { useState, useRef, useCallback } from 'react'
|
import React, { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import styled from 'styled-components'
|
|
||||||
import { extractFileList } from '../fs'
|
import { extractFileList } from '../fs'
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
display: block;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Overlay = styled.div`
|
|
||||||
display: block;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function DropZone({
|
export default function DropZone({
|
||||||
children,
|
|
||||||
onDrop,
|
onDrop,
|
||||||
}: {
|
}: {
|
||||||
onDrop: (files: File[]) => void
|
onDrop: (files: File[]) => void
|
||||||
children?: React.ReactNode
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const overlay = useRef()
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [focus, setFocus] = useState(false)
|
const [fileCount, setFileCount] = useState(0)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const handleDragEnter = useCallback(() => {
|
const handleDragEnter = useCallback((e: DragEvent) => {
|
||||||
setFocus(true)
|
e.preventDefault()
|
||||||
|
setIsDragging(true)
|
||||||
|
setFileCount(e.dataTransfer?.items.length || 0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDragLeave = useCallback(
|
const handleDragLeave = useCallback((e: DragEvent) => {
|
||||||
(e: React.DragEvent) => {
|
e.preventDefault()
|
||||||
if (e.target !== overlay.current) {
|
|
||||||
|
const currentTarget =
|
||||||
|
e.currentTarget === window ? window.document : e.currentTarget
|
||||||
|
if (
|
||||||
|
e.relatedTarget &&
|
||||||
|
currentTarget instanceof Node &&
|
||||||
|
currentTarget.contains(e.relatedTarget as Node)
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setFocus(false)
|
setIsDragging(false)
|
||||||
},
|
}, [])
|
||||||
[overlay.current],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
const handleDragOver = useCallback((e: DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (e.dataTransfer) {
|
||||||
e.dataTransfer.dropEffect = 'copy'
|
e.dataTransfer.dropEffect = 'copy'
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
async (e: React.DragEvent) => {
|
async (e: DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setFocus(false)
|
setIsDragging(false)
|
||||||
|
|
||||||
|
if (e.dataTransfer) {
|
||||||
const files = await extractFileList(e)
|
const files = await extractFileList(e)
|
||||||
onDrop(files)
|
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],
|
[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 (
|
return (
|
||||||
<Wrapper
|
<>
|
||||||
onDragEnter={handleDragEnter}
|
<div
|
||||||
onDragLeave={handleDragLeave}
|
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 ${
|
||||||
onDragOver={handleDragOver}
|
isDragging ? 'opacity-100 visible' : 'opacity-0 invisible'
|
||||||
onDrop={handleDrop}
|
}`}
|
||||||
|
>
|
||||||
|
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}
|
||||||
>
|
>
|
||||||
<Overlay ref={overlay} hidden={!focus} />
|
<span className="text-center text-stone-700">
|
||||||
{children}
|
Drop a file to get started
|
||||||
</Wrapper>
|
</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Spinner, Text, VStack } from '@chakra-ui/react'
|
|
||||||
|
|
||||||
export default function Loading({ text }: { text: string }): JSX.Element {
|
export default function Loading({ text }: { text: string }): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<div className="flex flex-col items-center">
|
||||||
<Spinner color="blue.500" />
|
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||||
<Text textStyle="descriptionSmall">{text}</Text>
|
<p className="text-sm text-gray-600 mt-2">{text}</p>
|
||||||
</VStack>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +1,74 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Box, Text, Badge, HStack, VStack } from '@chakra-ui/react'
|
|
||||||
|
|
||||||
type UploadedFileLike = {
|
type UploadedFileLike = {
|
||||||
fullPath: string
|
fullPath?: string
|
||||||
|
name?: string
|
||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files: UploadedFileLike[]
|
files: UploadedFileLike[]
|
||||||
|
onChange?: (updatedFiles: UploadedFileLike[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadFileList: React.FC<Props> = ({ files }: Props) => {
|
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 (
|
||||||
|
<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) => (
|
const items = files.map((f: UploadedFileLike, i: number) => (
|
||||||
<Box key={f.fullPath} w="100%">
|
<div
|
||||||
<HStack
|
key={getFileName(f)}
|
||||||
justify="space-between"
|
className="w-full border border-stone-300 rounded-md mb-2 group"
|
||||||
paddingY="8px"
|
|
||||||
paddingX="10px"
|
|
||||||
borderTopColor="gray.100"
|
|
||||||
borderTopWidth={i === 0 ? '0' : '1px'}
|
|
||||||
>
|
>
|
||||||
<Text textStyle="fileName" isTruncated>
|
<div className="flex justify-between items-center py-2 px-2.5">
|
||||||
{f.fullPath.slice(1)}
|
<p className="truncate text-sm font-medium">{getFileName(f)}</p>
|
||||||
</Text>
|
<div className="flex items-center">
|
||||||
<Badge size="sm">{f.type}</Badge>
|
<TypeBadge type={f.type} />
|
||||||
</HStack>
|
{onChange && (
|
||||||
</Box>
|
<button
|
||||||
|
onClick={() => handleRemove(i)}
|
||||||
|
className="text-stone-500 hover:text-stone-700 focus:outline-none"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
|
|
||||||
return (
|
return <div className="w-full">{items}</div>
|
||||||
<VStack w="100%" spacing="0" borderRadius="md" boxShadow="base">
|
|
||||||
{items}
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UploadFileList
|
export default UploadFileList
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Img } from '@chakra-ui/react'
|
import Image from 'next/image'
|
||||||
|
|
||||||
export default function Wordmark(): JSX.Element {
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 }
|
||||||
|
}
|
||||||
@ -1,16 +1,17 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: -webkit-fill-available;
|
height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100dvh;
|
||||||
|
|
||||||
/* mobile viewport bug fix */
|
|
||||||
min-height: -webkit-fill-available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#__next {
|
#__next {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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: [],
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue