mirror of https://github.com/renzynx/bliss.git
271 lines
7.2 KiB
TypeScript
271 lines
7.2 KiB
TypeScript
import {
|
|
Button,
|
|
Group,
|
|
SimpleGrid,
|
|
Text,
|
|
useMantineTheme,
|
|
} from '@mantine/core';
|
|
import { Dropzone } from '@mantine/dropzone';
|
|
import {
|
|
IconCheck,
|
|
IconCloudUpload,
|
|
IconDownload,
|
|
IconExclamationMark,
|
|
IconX,
|
|
} from '@tabler/icons';
|
|
import dynamic from 'next/dynamic';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { uploadStyles } from './styles';
|
|
import { API_ROUTES, API_URL, CHUNK_SIZE, USER_LIMIT } from '@lib/constants';
|
|
import axios from 'axios';
|
|
import { useAtom } from 'jotai';
|
|
import { userAtom } from '@lib/atoms';
|
|
import { showNotification } from '@mantine/notifications';
|
|
const ProgressCard = dynamic(() => import('./ProgressCard'));
|
|
import { ACCEPT_TYPE } from '@lib/constants';
|
|
import { formatBytes } from '@lib/utils';
|
|
|
|
const UploadZone = () => {
|
|
const [user] = useAtom(userAtom);
|
|
const theme = useMantineTheme();
|
|
const openRef = useRef<() => void>(null);
|
|
const [error, setError] = useState(false);
|
|
const [files, setFiles] = useState<File[]>([]);
|
|
const [currentFileIndex, setCurrentFileIndex] = useState<number | null>(null);
|
|
const [lastUploadedFileIndex, setLastUploadedFileIndex] = useState<
|
|
number | null
|
|
>(null);
|
|
const [currentChunkIndex, setCurrentChunkIndex] = useState<number | null>(
|
|
null
|
|
);
|
|
const { classes } = uploadStyles();
|
|
const [uploading, setUploading] = useState(false);
|
|
|
|
const uploadChunk = (e: ProgressEvent<FileReader>) => {
|
|
if (currentFileIndex === null) return;
|
|
const file = files[currentFileIndex];
|
|
const data = e.target?.result;
|
|
const headers = {
|
|
'Content-Type': 'application/octet-stream',
|
|
'X-File-Name': encodeURIComponent(file.name),
|
|
'X-File-Size': file.size,
|
|
'X-Current-Chunk': currentChunkIndex,
|
|
'X-Total-Chunks': Math.ceil(file.size / CHUNK_SIZE),
|
|
Authorization: user?.apiKey,
|
|
};
|
|
setUploading(true);
|
|
axios
|
|
.post(API_URL + API_ROUTES.UPLOAD_FILE, data, {
|
|
headers,
|
|
onUploadProgress: (evt) => {
|
|
// @ts-ignore
|
|
// prettier-ignore
|
|
file.speed = evt.loaded / ((new Date().getTime() - file.start) / 1000);
|
|
},
|
|
})
|
|
.then((response) => {
|
|
const file = files[currentFileIndex!];
|
|
const filesize = file.size;
|
|
// @ts-ignore
|
|
file.start = new Date().getTime();
|
|
const chunks = Math.ceil(filesize / CHUNK_SIZE) - 1;
|
|
const isLastChunk = currentChunkIndex === chunks;
|
|
if (isLastChunk) {
|
|
// @ts-ignore
|
|
file.final = response.data?.final;
|
|
setLastUploadedFileIndex(currentFileIndex!);
|
|
setCurrentChunkIndex(null);
|
|
|
|
showNotification({
|
|
title: 'Upload complete',
|
|
message: `${file.name} has been uploaded successfully`,
|
|
color: theme.colors.green[7],
|
|
icon: <IconCheck />,
|
|
});
|
|
|
|
const isLastFile = currentFileIndex === files.length - 1;
|
|
|
|
if (isLastFile) {
|
|
setLastUploadedFileIndex(currentFileIndex);
|
|
setCurrentFileIndex(null);
|
|
setUploading(false);
|
|
}
|
|
} else {
|
|
setCurrentChunkIndex(currentChunkIndex! + 1);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
setError(true);
|
|
if (err.response.status === 400) {
|
|
showNotification({
|
|
title: 'Error',
|
|
message: err.response.data.message,
|
|
color: 'red',
|
|
icon: <IconExclamationMark />,
|
|
autoClose: 5000,
|
|
});
|
|
} else {
|
|
showNotification({
|
|
title: 'Error',
|
|
message: 'Something went wrong, please try again later',
|
|
color: 'red',
|
|
icon: <IconExclamationMark />,
|
|
autoClose: 5000,
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const readAndUploadCurrentChunk = () => {
|
|
if (currentFileIndex === null) return;
|
|
const reader = new FileReader();
|
|
const file = files[currentFileIndex];
|
|
if (!file) return;
|
|
const from = currentChunkIndex! * CHUNK_SIZE;
|
|
const to = from + CHUNK_SIZE;
|
|
const blob = file.slice(from, to);
|
|
reader.onload = (e) => uploadChunk(e);
|
|
reader.readAsDataURL(blob);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (lastUploadedFileIndex === null) {
|
|
return;
|
|
}
|
|
const isLastFile = lastUploadedFileIndex === files.length - 1;
|
|
const nextFileIndex = isLastFile ? null : currentFileIndex! + 1;
|
|
setCurrentFileIndex(nextFileIndex);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [lastUploadedFileIndex]);
|
|
|
|
useEffect(() => {
|
|
if (files.length > 0 && currentFileIndex === null) {
|
|
setCurrentFileIndex(
|
|
lastUploadedFileIndex === null ? 0 : lastUploadedFileIndex + 1
|
|
);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [files.length]);
|
|
|
|
useEffect(() => {
|
|
if (currentFileIndex !== null) {
|
|
setCurrentChunkIndex(0);
|
|
}
|
|
}, [currentFileIndex]);
|
|
|
|
useEffect(() => {
|
|
if (currentChunkIndex !== null) {
|
|
readAndUploadCurrentChunk();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [currentChunkIndex]);
|
|
|
|
return (
|
|
<>
|
|
<div className={classes.wrapper}>
|
|
<Dropzone
|
|
loading={uploading}
|
|
openRef={openRef}
|
|
onDrop={(dropzoneFiles) => setFiles([...files, ...dropzoneFiles])}
|
|
onReject={(err) => {
|
|
showNotification({
|
|
title: 'Error',
|
|
message: err[0].errors[0].message,
|
|
color: 'red',
|
|
icon: <IconX />,
|
|
});
|
|
}}
|
|
className={classes.dropzone}
|
|
radius="md"
|
|
accept={ACCEPT_TYPE}
|
|
maxSize={
|
|
USER_LIMIT(
|
|
!!user?.createdAt,
|
|
user?.role === 'OWNER' || user?.role === 'ADMIN'
|
|
) *
|
|
1024 ** 2
|
|
}
|
|
>
|
|
<div style={{ pointerEvents: 'none' }}>
|
|
<Group position="center">
|
|
<Dropzone.Accept>
|
|
<IconDownload
|
|
size={50}
|
|
color={theme.colors[theme.primaryColor][6]}
|
|
stroke={1.5}
|
|
/>
|
|
</Dropzone.Accept>
|
|
<Dropzone.Reject>
|
|
<IconX size={50} color={theme.colors.red[6]} stroke={1.5} />
|
|
</Dropzone.Reject>
|
|
<Dropzone.Idle>
|
|
<IconCloudUpload
|
|
size={50}
|
|
color={
|
|
theme.colorScheme === 'dark'
|
|
? theme.colors.dark[0]
|
|
: theme.black
|
|
}
|
|
stroke={1.5}
|
|
/>
|
|
</Dropzone.Idle>
|
|
</Group>
|
|
|
|
<Text align="center" weight={700} size="lg" mt="xl">
|
|
<Dropzone.Accept>Drop files here</Dropzone.Accept>
|
|
<Dropzone.Reject>
|
|
We can't accept this file. Try another one.
|
|
</Dropzone.Reject>
|
|
<Dropzone.Idle>Upload Files</Dropzone.Idle>
|
|
</Text>
|
|
<Text align="center" size="sm" mt="xs" color="dimmed">
|
|
Drag'n'drop files here to upload. We can accept only
|
|
file that are less than 5GB in size.
|
|
</Text>
|
|
</div>
|
|
</Dropzone>
|
|
|
|
<Button
|
|
className={classes.control}
|
|
size="md"
|
|
radius="xl"
|
|
onClick={() => openRef.current?.()}
|
|
>
|
|
Select files
|
|
</Button>
|
|
</div>
|
|
<SimpleGrid cols={2} breakpoints={[{ maxWidth: 768, cols: 1 }]}>
|
|
{files.map((file, idx) => {
|
|
let progress = 0;
|
|
// @ts-ignore
|
|
if (file.final) {
|
|
progress = 100;
|
|
} else {
|
|
const uploading = idx === currentFileIndex;
|
|
const chunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
|
|
if (uploading) {
|
|
const rounder = (currentChunkIndex! / chunks) * 100;
|
|
progress = +rounder.toFixed(2);
|
|
} else {
|
|
progress = 0;
|
|
}
|
|
}
|
|
return (
|
|
<ProgressCard
|
|
error={error}
|
|
key={idx}
|
|
filename={file.name}
|
|
progress={progress}
|
|
// @ts-ignore
|
|
speed={file.speed ? `${formatBytes(file.speed)}/s` : 'Waiting...'}
|
|
/>
|
|
);
|
|
})}
|
|
</SimpleGrid>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default UploadZone;
|