244 lines
7.2 KiB
TypeScript
244 lines
7.2 KiB
TypeScript
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
|
import { DocumentPlusIcon } from "@heroicons/react/24/outline";
|
|
import classNames from "classnames";
|
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "../Button";
|
|
import Separator, { ESeperatorColor, ESeperatorDirection } from "../Separator";
|
|
import classes from "./classes.module.scss";
|
|
import DocumentFileElement from "./DocumentFileElement";
|
|
|
|
/**
|
|
* @description Drag and drop component to upload files
|
|
* @param {string} title - Title of the component
|
|
* @param {string} description - Description of the component
|
|
* @param {IDocumentFileWithUid[]} defaultFiles - Default files to display
|
|
* @param {(fileUid: string) => Promise<any>} onDelete - Function to delete a file (must be used with defaultFiles)
|
|
* @param {(file: File) => Promise<any>} onAddFile - Function to add a file (must be used with defaultFiles)
|
|
*/
|
|
type IProps = {
|
|
title: string;
|
|
description?: string;
|
|
defaultFiles?: IDocumentFileWithUid[];
|
|
onDelete?: (fileUid: string) => Promise<any>;
|
|
onAddFile?: (file: File) => Promise<any>;
|
|
name?: string;
|
|
onChange?: (files: File[]) => void;
|
|
} & (
|
|
| { onDelete: (fileUid: string) => Promise<any>; onAddFile?: never; defaultFiles: IDocumentFileWithUid[] }
|
|
| { onDelete?: never; onAddFile: (file: File) => Promise<any>; defaultFiles: IDocumentFileWithUid[] }
|
|
| { onDelete?: (fileUid: string) => Promise<any>; onAddFile?: (file: File) => Promise<any>; defaultFiles: IDocumentFileWithUid[] }
|
|
| { onDelete?: never; onAddFile?: never; defaultFiles?: never }
|
|
);
|
|
|
|
type IMimeTypes = {
|
|
extension: string;
|
|
size: number;
|
|
};
|
|
|
|
const mimeTypesAccepted: { [key: string]: IMimeTypes } = {
|
|
"application/pdf": {
|
|
extension: "pdf",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"image/jpeg": {
|
|
extension: "jpeg",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"image/png": {
|
|
extension: "png",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"image/jpg": {
|
|
extension: "jpg",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"text/csv": {
|
|
extension: "csv",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
|
|
extension: "xlsx",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"application/vnd.ms-excel": {
|
|
extension: "xls",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"application/msword": {
|
|
extension: "doc",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
|
|
extension: "docx",
|
|
size: 104857600, // 100MB
|
|
},
|
|
"text/plain": {
|
|
extension: "txt",
|
|
size: 104857600, // 100MB
|
|
},
|
|
};
|
|
|
|
type IDocumentFileBase = {
|
|
id: string;
|
|
file: File | null;
|
|
uid?: string;
|
|
isLoading?: boolean;
|
|
error?: string;
|
|
};
|
|
|
|
export type IDocumentFileWithUid = IDocumentFileBase & {
|
|
uid: string;
|
|
};
|
|
|
|
type IDocumentFile = IDocumentFileBase | IDocumentFileWithUid;
|
|
|
|
export default function DragAndDrop(props: IProps) {
|
|
const { title, description, defaultFiles, onDelete, onAddFile, name, onChange } = props;
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [documentFiles, setDocumentFiles] = useState<IDocumentFile[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (defaultFiles) {
|
|
setDocumentFiles(defaultFiles);
|
|
}
|
|
}, [defaultFiles]);
|
|
|
|
useEffect(() => onChange?.(documentFiles.map((doc) => doc.file).filter((file) => file !== null) as File[]), [documentFiles, onChange]);
|
|
|
|
const handleAddFiles = useCallback(
|
|
(files: File[]) => {
|
|
files.forEach((file) => {
|
|
setDocumentFiles((prevDocs) => [...prevDocs, { id: file.name, file: file, isLoading: true }]);
|
|
try {
|
|
if (!mimeTypesAccepted[file.type]) {
|
|
throw new Error("Type de fichier non accepté");
|
|
}
|
|
const newDoc: IDocumentFile = { id: file.name, file, isLoading: false };
|
|
|
|
if (onAddFile) {
|
|
// As onAddFile is used along defaultFiles prop we dont need to update the state here but the parent component should update the defaultFiles prop
|
|
return onAddFile(file);
|
|
}
|
|
|
|
return setTimeout(async () => {
|
|
setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === newDoc.id ? newDoc : doc)));
|
|
}, 1000);
|
|
} catch (error: any) {
|
|
const errorDoc: IDocumentFile = { id: file.name, file: null, isLoading: false, error: error.message };
|
|
return setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === errorDoc.id ? errorDoc : doc)));
|
|
}
|
|
});
|
|
},
|
|
[onAddFile],
|
|
);
|
|
|
|
const handleDrop = useCallback(
|
|
(event: React.DragEvent<HTMLDivElement>) => {
|
|
event.preventDefault();
|
|
const files = Array.from(event.dataTransfer.files);
|
|
handleAddFiles(files);
|
|
},
|
|
[handleAddFiles],
|
|
);
|
|
|
|
const handleRemove = useCallback(
|
|
(documentFile: IDocumentFile) => {
|
|
const loadingDoc = { ...documentFile, isLoading: true };
|
|
setDocumentFiles((prevDocs) => prevDocs.map((doc) => (doc.id === documentFile.id ? loadingDoc : doc)));
|
|
|
|
if (documentFile.uid) {
|
|
return onDelete?.(documentFile.uid);
|
|
}
|
|
return setDocumentFiles((prevDocs) => prevDocs.filter((doc) => doc.id !== documentFile.id));
|
|
},
|
|
[onDelete],
|
|
);
|
|
|
|
const handleBrowse = useCallback(
|
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = Array.from(event.target.files || []);
|
|
handleAddFiles(files);
|
|
},
|
|
[handleAddFiles],
|
|
);
|
|
|
|
const triggerFileInput = () => {
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.click();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={classNames(classes["root"], documentFiles.length > 0 && classes["filled"])}
|
|
onDrop={handleDrop}
|
|
onDragOver={(e) => e.preventDefault()}>
|
|
<div className={classes["content"]}>
|
|
<DocumentPlusIcon />
|
|
<Separator direction={ESeperatorDirection.VERTICAL} color={ESeperatorColor.STRONG} size={64} />
|
|
<div className={classes["browse-document-container"]}>
|
|
<div className={classNames(classes["browse-document"], classes["desktop"])}>
|
|
<Typography typo={ETypo.TEXT_LG_SEMIBOLD} color={ETypoColor.TEXT_PRIMARY}>
|
|
{title}
|
|
</Typography>
|
|
<Button
|
|
variant={EButtonVariant.PRIMARY}
|
|
styletype={EButtonstyletype.TEXT}
|
|
size={EButtonSize.SM}
|
|
onClick={triggerFileInput}>
|
|
{inputFile()}
|
|
parcourir
|
|
</Button>
|
|
</div>
|
|
|
|
<div className={classNames(classes["browse-document"], classes["mobile"])}>
|
|
<Button
|
|
variant={EButtonVariant.PRIMARY}
|
|
styletype={EButtonstyletype.TEXT}
|
|
size={EButtonSize.SM}
|
|
onClick={triggerFileInput}>
|
|
{inputFile()}
|
|
Ajouter un document
|
|
</Button>
|
|
</div>
|
|
|
|
{description && (
|
|
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.TEXT_SECONDARY}>
|
|
{description}
|
|
</Typography>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{documentFiles.length > 0 && (
|
|
<div className={classes["documents"]}>
|
|
{documentFiles.map((documentFile, index) => (
|
|
<DocumentFileElement
|
|
key={documentFile.uid || `${documentFile.id}-${index}`}
|
|
isLoading={documentFile.isLoading}
|
|
file={documentFile.file}
|
|
onRemove={() => handleRemove(documentFile)}
|
|
error={documentFile.error}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
function inputFile() {
|
|
return (
|
|
<input
|
|
name={name}
|
|
ref={fileInputRef}
|
|
type="file"
|
|
multiple
|
|
accept={Object.keys(mimeTypesAccepted).join(",")}
|
|
onChange={handleBrowse}
|
|
hidden
|
|
/>
|
|
);
|
|
}
|
|
}
|