infoalloggi-monorepo/apps/infoalloggi/src/components/upload_modal.tsx

267 lines
6.5 KiB
TypeScript
Raw Normal View History

2025-08-28 18:27:07 +02:00
import { CloudUpload, UploadIcon, X } from "lucide-react";
import { useCallback, useState } from "react";
2025-08-04 17:45:44 +02:00
import toast from "react-hot-toast";
import {
2025-08-28 18:27:07 +02:00
Credenza,
CredenzaBody,
CredenzaContent,
CredenzaDescription,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
2025-08-04 17:45:44 +02:00
} from "~/components/custom_ui/credenza";
import {
2025-08-28 18:27:07 +02:00
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadItemProgress,
FileUploadList,
FileUploadTrigger,
2025-08-04 17:45:44 +02:00
} from "~/components/custom_ui/fileUpload";
2025-08-28 18:27:07 +02:00
import { Button } from "~/components/ui/button";
2025-10-24 16:10:59 +02:00
import { uploadHandler } from "~/hooks/storageClienSideHooks";
2025-08-28 18:27:07 +02:00
import { useTranslation } from "~/providers/I18nProvider";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
2025-08-04 17:45:44 +02:00
type UploadModalProps = {
2025-08-28 18:27:07 +02:00
cb_onUpload?: () => void;
isAdmin?: boolean;
userId?: UsersId;
2025-08-04 17:45:44 +02:00
};
export const UploadModal = ({
2025-08-28 18:27:07 +02:00
cb_onUpload,
isAdmin,
userId,
2025-08-04 17:45:44 +02:00
}: UploadModalProps) => {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
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>
2025-10-10 16:18:43 +02:00
<CredenzaTitle className="text-center font-semibold text-2xl">
2025-08-28 18:27:07 +02:00
{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>
);
2025-08-04 17:45:44 +02:00
};
type UploadComponentProps = {
2025-08-28 18:27:07 +02:00
cb_onUpload?: () => void;
isAdmin?: boolean;
userId?: UsersId;
maxMB?: number;
maxFiles?: number;
2025-08-04 17:45:44 +02:00
};
export const UploadComponent = ({
2025-08-28 18:27:07 +02:00
cb_onUpload,
isAdmin,
userId,
maxMB = 5, // Default max size is 5MB
maxFiles,
2025-08-04 17:45:44 +02:00
}: UploadComponentProps) => {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation();
const [files, setFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
2025-08-04 17:45:44 +02:00
2025-10-24 16:10:59 +02:00
const { mutateAsync: setUserStorage } =
api.storage.addUserStorage.useMutation({
onMutate: () => {
toast(t.allegati.allegato_caricato, { icon: "📤" });
},
onSettled: async () => {
await utils.storage.getAll.invalidate();
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
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
];
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
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}`,
);
}, []);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const onFileValidate = useCallback(
(file: File): string | null => {
if (maxFiles) {
// Validate max files
if (files.length >= maxFiles) {
return `${t.allegati.file_too_many} ${maxFiles}`;
}
}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
// 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`;
}
}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return null;
},
[files],
);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const onUpload = useCallback(
async (
files: File[],
{
onProgress,
}: {
onProgress: (file: File, progress: number) => void;
},
) => {
2025-10-24 16:10:59 +02:00
if (files.length === 0) {
return;
}
2025-08-28 18:27:07 +02:00
try {
2025-10-24 16:10:59 +02:00
const from_admin = isAdmin ?? true;
2025-08-28 18:27:07 +02:00
setIsUploading(true);
2025-10-24 16:10:59 +02:00
const { status, attachments } = await uploadHandler({
files,
from_admin,
2025-08-28 18:27:07 +02:00
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if (userId) {
for (const storageId of attachments) {
2025-10-24 16:10:59 +02:00
await setUserStorage({
storageId,
2025-08-29 16:18:32 +02:00
userId,
2025-10-24 16:10:59 +02:00
from_admin,
2025-08-28 18:27:07 +02:00
});
}
}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
files.forEach((file) => {
const randomDelay = Math.random() * 1000 + 500; // Random delay between 500ms and 1500ms
let progress = 0;
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
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
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if (!status) {
toast.error(t.allegati.errore_caricamento, {
icon: "❌",
});
setIsUploading(false);
return;
}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
toast.success(t.allegati.success_caricamento, {
icon: "✅",
});
} catch (error) {
setIsUploading(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
toast.error(
error instanceof Error
? error.message
: t.allegati.errore_sconosciuto,
);
} finally {
setIsUploading(false);
setFiles([]);
cb_onUpload?.();
await utils.storage.getAll.invalidate();
}
},
[],
);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<FileUpload
className="w-full"
disabled={isUploading}
multiple
2025-08-29 16:18:32 +02:00
onFileReject={onFileReject}
onFileValidate={onFileValidate}
onUpload={onUpload}
onValueChange={setFiles}
value={files}
2025-08-28 18:27:07 +02:00
>
<FileUploadDropzone>
<div className="flex flex-col items-center gap-1">
<div className="flex items-center justify-center rounded-full border p-2.5">
2025-10-10 16:18:43 +02:00
<UploadIcon className="size-6 text-muted-foreground" />
2025-08-28 18:27:07 +02:00
</div>
2025-10-10 16:18:43 +02:00
<p className="font-medium text-sm">{t.allegati.drag_drop}</p>
2025-08-28 18:27:07 +02:00
<p className="text-muted-foreground text-xs">{t.allegati.oppure}</p>
</div>
<FileUploadTrigger asChild>
2025-08-29 16:18:32 +02:00
<Button className="mt-2 w-fit" size="sm" variant="outline">
2025-08-28 18:27:07 +02:00
{t.allegati.seleziona}
</Button>
</FileUploadTrigger>
</FileUploadDropzone>
<FileUploadList>
{files.map((file, index) => (
<FileUploadItem key={index} value={file}>
<FileUploadItemPreview />
<FileUploadItemMetadata />
<FileUploadItemDelete asChild>
2025-08-29 16:18:32 +02:00
<Button className="size-7" size="icon" variant="ghost">
2025-08-28 18:27:07 +02:00
<X />
</Button>
</FileUploadItemDelete>
<FileUploadItemProgress />
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
);
2025-08-04 17:45:44 +02:00
};