264 lines
7.2 KiB
TypeScript
264 lines
7.2 KiB
TypeScript
|
|
import toast from "react-hot-toast";
|
||
|
|
import { Upload } from "~/lib/storage_manager";
|
||
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
||
|
|
import { api } from "~/utils/api";
|
||
|
|
import {
|
||
|
|
Credenza,
|
||
|
|
CredenzaBody,
|
||
|
|
CredenzaContent,
|
||
|
|
CredenzaDescription,
|
||
|
|
CredenzaHeader,
|
||
|
|
CredenzaTitle,
|
||
|
|
CredenzaTrigger,
|
||
|
|
} from "~/components/custom_ui/credenza";
|
||
|
|
import { Button } from "~/components/ui/button";
|
||
|
|
import { CloudUpload, UploadIcon, X } from "lucide-react";
|
||
|
|
import {
|
||
|
|
FileUpload,
|
||
|
|
FileUploadDropzone,
|
||
|
|
FileUploadItem,
|
||
|
|
FileUploadItemDelete,
|
||
|
|
FileUploadItemMetadata,
|
||
|
|
FileUploadItemPreview,
|
||
|
|
FileUploadItemProgress,
|
||
|
|
FileUploadList,
|
||
|
|
FileUploadTrigger,
|
||
|
|
} from "~/components/custom_ui/fileUpload";
|
||
|
|
import type { UsersId } from "~/schemas/public/Users";
|
||
|
|
import type { StorageindexId } from "~/schemas/public/Storageindex";
|
||
|
|
import { useCallback, useState } from "react";
|
||
|
|
|
||
|
|
type UploadModalProps = {
|
||
|
|
cb_onUpload?: () => void;
|
||
|
|
isAdmin?: boolean;
|
||
|
|
userId?: UsersId;
|
||
|
|
};
|
||
|
|
|
||
|
|
export const UploadModal = ({
|
||
|
|
cb_onUpload,
|
||
|
|
isAdmin,
|
||
|
|
userId,
|
||
|
|
}: UploadModalProps) => {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Credenza>
|
||
|
|
<CredenzaTrigger asChild>
|
||
|
|
<Button className="flex w-fit gap-2">
|
||
|
|
<CloudUpload className="size-6" /> {t.allegati.carica_nuovi}
|
||
|
|
</Button>
|
||
|
|
</CredenzaTrigger>
|
||
|
|
<CredenzaContent className="max-h-[90vh]">
|
||
|
|
<CredenzaHeader>
|
||
|
|
<CredenzaTitle className="text-center text-2xl font-semibold">
|
||
|
|
{t.allegati.carica_nuovi}
|
||
|
|
</CredenzaTitle>
|
||
|
|
<CredenzaDescription className="sr-only">
|
||
|
|
Allegati modal
|
||
|
|
</CredenzaDescription>
|
||
|
|
</CredenzaHeader>
|
||
|
|
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
|
||
|
|
<UploadComponent
|
||
|
|
cb_onUpload={cb_onUpload}
|
||
|
|
isAdmin={isAdmin}
|
||
|
|
userId={userId}
|
||
|
|
/>
|
||
|
|
</CredenzaBody>
|
||
|
|
</CredenzaContent>
|
||
|
|
</Credenza>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
type UploadComponentProps = {
|
||
|
|
cb_onUpload?: () => void;
|
||
|
|
isAdmin?: boolean;
|
||
|
|
userId?: UsersId;
|
||
|
|
maxMB?: number;
|
||
|
|
maxFiles?: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
export const UploadComponent = ({
|
||
|
|
cb_onUpload,
|
||
|
|
isAdmin,
|
||
|
|
userId,
|
||
|
|
maxMB = 5, // Default max size is 5MB
|
||
|
|
maxFiles,
|
||
|
|
}: UploadComponentProps) => {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const [files, setFiles] = useState<File[]>([]);
|
||
|
|
const [isUploading, setIsUploading] = useState(false);
|
||
|
|
|
||
|
|
const { mutateAsync: getToken } = api.storage.getStorageTokenM.useMutation();
|
||
|
|
|
||
|
|
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
|
||
|
|
onMutate: () => {
|
||
|
|
toast(t.allegati.allegato_caricato, { icon: "📤" });
|
||
|
|
},
|
||
|
|
onSettled: async () => {
|
||
|
|
await utils.storage.getAll.invalidate();
|
||
|
|
await utils.storage.getUserStorage.invalidate({
|
||
|
|
userId,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const acceptedMimeTypes = [
|
||
|
|
"image/jpeg", // jpg, jpeg
|
||
|
|
"image/png", // png
|
||
|
|
"image/webp", // webp
|
||
|
|
"text/plain", // txt
|
||
|
|
"application/pdf", // pdf
|
||
|
|
"application/msword", // doc
|
||
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", // docx
|
||
|
|
"application/vnd.ms-excel", // xls
|
||
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // xlsx
|
||
|
|
"application/vnd.oasis.opendocument.text", // odt
|
||
|
|
"application/vnd.oasis.opendocument.spreadsheet", // ods
|
||
|
|
];
|
||
|
|
|
||
|
|
const utils = api.useUtils();
|
||
|
|
|
||
|
|
const onFileReject = useCallback((file: File, message: string) => {
|
||
|
|
toast(
|
||
|
|
`${message}\n\n"${file.name.length > 15 ? `${file.name.slice(0, 15)}...` : file.name}" ${t.allegati.file_rejected}`,
|
||
|
|
);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const onFileValidate = useCallback(
|
||
|
|
(file: File): string | null => {
|
||
|
|
if (maxFiles) {
|
||
|
|
// Validate max files
|
||
|
|
if (files.length >= maxFiles) {
|
||
|
|
return `${t.allegati.file_too_many} ${maxFiles}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate file type
|
||
|
|
if (!acceptedMimeTypes.includes(file.type)) {
|
||
|
|
return t.allegati.invalid_file_type;
|
||
|
|
}
|
||
|
|
if (maxMB) {
|
||
|
|
// Validate file size
|
||
|
|
const MAX_SIZE = maxMB * 1024 * 1024;
|
||
|
|
if (file.size > MAX_SIZE) {
|
||
|
|
return `${t.allegati.file_too_large} ${MAX_SIZE / (1024 * 1024)}MB`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
[files],
|
||
|
|
);
|
||
|
|
|
||
|
|
const onUpload = useCallback(
|
||
|
|
async (
|
||
|
|
files: File[],
|
||
|
|
{
|
||
|
|
onProgress,
|
||
|
|
}: {
|
||
|
|
onProgress: (file: File, progress: number) => void;
|
||
|
|
},
|
||
|
|
) => {
|
||
|
|
try {
|
||
|
|
setIsUploading(true);
|
||
|
|
const { status, attachments } = await Upload({
|
||
|
|
files: files,
|
||
|
|
getToken,
|
||
|
|
from_admin: isAdmin ?? true,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (userId) {
|
||
|
|
for (const storageId of attachments) {
|
||
|
|
setUserStorage({
|
||
|
|
userId,
|
||
|
|
fileId: storageId as StorageindexId,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
files.forEach((file) => {
|
||
|
|
const randomDelay = Math.random() * 1000 + 500; // Random delay between 500ms and 1500ms
|
||
|
|
let progress = 0;
|
||
|
|
|
||
|
|
const interval = setInterval(() => {
|
||
|
|
progress += Math.random() * 10; // Increment progress randomly
|
||
|
|
if (progress >= 100) {
|
||
|
|
progress = 100;
|
||
|
|
clearInterval(interval);
|
||
|
|
}
|
||
|
|
onProgress(file, progress);
|
||
|
|
}, randomDelay / 1); // Adjust interval based on random delay
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!status) {
|
||
|
|
toast.error(t.allegati.errore_caricamento, {
|
||
|
|
icon: "❌",
|
||
|
|
});
|
||
|
|
setIsUploading(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
toast.success(t.allegati.success_caricamento, {
|
||
|
|
icon: "✅",
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
setIsUploading(false);
|
||
|
|
|
||
|
|
toast.error(
|
||
|
|
error instanceof Error
|
||
|
|
? error.message
|
||
|
|
: t.allegati.errore_sconosciuto,
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
setIsUploading(false);
|
||
|
|
setFiles([]);
|
||
|
|
cb_onUpload?.();
|
||
|
|
await utils.storage.getAll.invalidate();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<FileUpload
|
||
|
|
onFileValidate={onFileValidate}
|
||
|
|
className="w-full"
|
||
|
|
value={files}
|
||
|
|
onValueChange={setFiles}
|
||
|
|
onFileReject={onFileReject}
|
||
|
|
onUpload={onUpload}
|
||
|
|
disabled={isUploading}
|
||
|
|
multiple
|
||
|
|
>
|
||
|
|
<FileUploadDropzone>
|
||
|
|
<div className="flex flex-col items-center gap-1">
|
||
|
|
<div className="flex items-center justify-center rounded-full border p-2.5">
|
||
|
|
<UploadIcon className="text-muted-foreground size-6" />
|
||
|
|
</div>
|
||
|
|
<p className="text-sm font-medium">{t.allegati.drag_drop}</p>
|
||
|
|
<p className="text-muted-foreground text-xs">{t.allegati.oppure}</p>
|
||
|
|
</div>
|
||
|
|
<FileUploadTrigger asChild>
|
||
|
|
<Button variant="outline" size="sm" className="mt-2 w-fit">
|
||
|
|
{t.allegati.seleziona}
|
||
|
|
</Button>
|
||
|
|
</FileUploadTrigger>
|
||
|
|
</FileUploadDropzone>
|
||
|
|
<FileUploadList>
|
||
|
|
{files.map((file, index) => (
|
||
|
|
<FileUploadItem key={index} value={file}>
|
||
|
|
<FileUploadItemPreview />
|
||
|
|
<FileUploadItemMetadata />
|
||
|
|
<FileUploadItemDelete asChild>
|
||
|
|
<Button variant="ghost" size="icon" className="size-7">
|
||
|
|
<X />
|
||
|
|
</Button>
|
||
|
|
</FileUploadItemDelete>
|
||
|
|
<FileUploadItemProgress />
|
||
|
|
</FileUploadItem>
|
||
|
|
))}
|
||
|
|
</FileUploadList>
|
||
|
|
</FileUpload>
|
||
|
|
);
|
||
|
|
};
|