mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-12 11:21:14 -05:00
186 lines
6.5 KiB
TypeScript
186 lines
6.5 KiB
TypeScript
// @ts-nocheck
|
|
import { IconUpload } from '@tabler/icons-react';
|
|
import { motion } from 'framer-motion';
|
|
import { useRef, useState } from 'react';
|
|
import { useDropzone } from 'react-dropzone';
|
|
import { cn } from '../../lib/utils';
|
|
|
|
const mainVariant = {
|
|
initial: {
|
|
x: 0,
|
|
y: 0,
|
|
},
|
|
animate: {
|
|
x: 20,
|
|
y: -20,
|
|
opacity: 0.9,
|
|
},
|
|
};
|
|
|
|
const secondaryVariant = {
|
|
initial: {
|
|
opacity: 0,
|
|
},
|
|
animate: {
|
|
opacity: 1,
|
|
},
|
|
};
|
|
|
|
export const FileUpload = ({ onChange }: { onChange?: (files: File[]) => void }) => {
|
|
const [files, setFiles] = useState<File[]>([]);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handleFileChange = (newFiles: File[]) => {
|
|
setFiles((prevFiles) => [...prevFiles, ...newFiles]);
|
|
onChange && onChange(newFiles);
|
|
};
|
|
|
|
const handleClick = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const { getRootProps, isDragActive } = useDropzone({
|
|
multiple: false,
|
|
noClick: true,
|
|
onDrop: handleFileChange,
|
|
onDropRejected: (error) => {
|
|
console.log(error);
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div className='w-full' {...getRootProps()}>
|
|
<motion.div
|
|
onClick={handleClick}
|
|
whileHover='animate'
|
|
className='group/file relative block w-full cursor-pointer overflow-hidden rounded-lg p-10'
|
|
>
|
|
<input
|
|
ref={fileInputRef}
|
|
id='file-upload-handle'
|
|
type='file'
|
|
onChange={(e) => handleFileChange(Array.from(e.target.files || []))}
|
|
className='hidden'
|
|
/>
|
|
<div className='absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)]'>
|
|
<GridPattern />
|
|
</div>
|
|
<div className='flex flex-col items-center justify-center'>
|
|
<p className='relative z-20 font-sans text-base font-bold text-neutral-700 dark:text-neutral-300'>
|
|
Upload file
|
|
</p>
|
|
<p className='relative z-20 mt-2 font-sans text-base font-normal text-neutral-400 dark:text-neutral-400'>
|
|
Drag or drop your files here or click to upload
|
|
</p>
|
|
<div className='relative mx-auto mt-10 w-full max-w-xl'>
|
|
{files.length > 0 &&
|
|
files.map((file, idx) => (
|
|
<motion.div
|
|
key={'file' + idx}
|
|
layoutId={idx === 0 ? 'file-upload' : 'file-upload-' + idx}
|
|
className={cn(
|
|
'relative z-40 mx-auto mt-4 flex w-full flex-col items-start justify-start overflow-hidden rounded-md bg-white p-4 md:h-24 dark:bg-neutral-900',
|
|
'shadow-sm',
|
|
)}
|
|
>
|
|
<div className='flex w-full items-center justify-between gap-4'>
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
layout
|
|
className='max-w-xs truncate text-base text-neutral-700 dark:text-neutral-300'
|
|
>
|
|
{file.name}
|
|
</motion.p>
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
layout
|
|
className='shadow-input w-fit flex-shrink-0 rounded-lg px-2 py-1 text-sm text-neutral-600 dark:bg-neutral-800 dark:text-white'
|
|
>
|
|
{(file.size / (1024 * 1024)).toFixed(2)} MB
|
|
</motion.p>
|
|
</div>
|
|
|
|
<div className='mt-2 flex w-full flex-col items-start justify-between text-sm text-neutral-600 md:flex-row md:items-center dark:text-neutral-400'>
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
layout
|
|
className='rounded-md bg-gray-100 px-1 py-0.5 dark:bg-neutral-800'
|
|
>
|
|
{file.type}
|
|
</motion.p>
|
|
|
|
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} layout>
|
|
modified {new Date(file.lastModified).toLocaleDateString()}
|
|
</motion.p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
{!files.length && (
|
|
<motion.div
|
|
layoutId='file-upload'
|
|
variants={mainVariant}
|
|
transition={{
|
|
type: 'spring',
|
|
stiffness: 300,
|
|
damping: 20,
|
|
}}
|
|
className={cn(
|
|
'relative z-40 mx-auto mt-4 flex h-32 w-full max-w-[8rem] items-center justify-center rounded-md bg-white group-hover/file:shadow-2xl dark:bg-neutral-900',
|
|
'shadow-[0px_10px_50px_rgba(0,0,0,0.1)]',
|
|
)}
|
|
>
|
|
{isDragActive ? (
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className='flex flex-col items-center text-neutral-600'
|
|
>
|
|
Drop it
|
|
<IconUpload className='h-4 w-4 text-neutral-600 dark:text-neutral-400' />
|
|
</motion.p>
|
|
) : (
|
|
<IconUpload className='h-4 w-4 text-neutral-600 dark:text-neutral-300' />
|
|
)}
|
|
</motion.div>
|
|
)}
|
|
|
|
{!files.length && (
|
|
<motion.div
|
|
variants={secondaryVariant}
|
|
className='absolute inset-0 z-30 mx-auto mt-4 flex h-32 w-full max-w-[8rem] items-center justify-center rounded-md border border-dashed border-sky-400 bg-transparent opacity-0'
|
|
></motion.div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export function GridPattern() {
|
|
const columns = 41;
|
|
const rows = 11;
|
|
return (
|
|
<div className='flex flex-shrink-0 scale-105 flex-wrap items-center justify-center gap-x-px gap-y-px bg-gray-100 dark:bg-neutral-900'>
|
|
{Array.from({ length: rows }).map((_, row) =>
|
|
Array.from({ length: columns }).map((_, col) => {
|
|
const index = row * columns + col;
|
|
return (
|
|
<div
|
|
key={`${col}-${row}`}
|
|
className={`flex h-10 w-10 flex-shrink-0 rounded-[2px] ${
|
|
index % 2 === 0
|
|
? 'bg-gray-50 dark:bg-neutral-950'
|
|
: 'bg-gray-50 shadow-[0px_0px_1px_3px_rgba(255,255,255,1)_inset] dark:bg-neutral-950 dark:shadow-[0px_0px_1px_3px_rgba(0,0,0,1)_inset]'
|
|
}`}
|
|
/>
|
|
);
|
|
}),
|
|
)}
|
|
</div>
|
|
);
|
|
}
|