infoalloggi-monorepo/apps/infoalloggi/src/components/area-riservata/allegati.tsx

384 lines
10 KiB
TypeScript
Raw Normal View History

2025-08-28 18:27:07 +02:00
import { format } from "date-fns";
import {
Download,
File,
FileArchive,
FileAudio,
FileCode,
FileSpreadsheet,
FileText,
FileVideo,
ImageIcon,
MoreVertical,
Pencil,
Trash2,
TriangleAlert,
} from "lucide-react";
import Link from "next/link";
import { type FormEvent, useState } from "react";
2025-08-04 17:45:44 +02:00
import toast from "react-hot-toast";
2025-08-28 18:27:07 +02:00
import Input from "~/components/custom_ui/input";
2025-08-04 17:45:44 +02:00
import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
import {
2025-08-28 18:27:07 +02:00
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/dialog";
import {
2025-08-28 18:27:07 +02:00
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/dropdown-menu";
2025-08-28 18:27:07 +02:00
import { Separator } from "~/components/ui/separator";
import { UploadModal } from "~/components/upload_modal";
import { handleDownload } from "~/hooks/filesHooks";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
2025-08-04 17:45:44 +02:00
export const AllegatiComp = ({
2025-08-28 18:27:07 +02:00
userId,
isAdmin,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
userId: UsersId;
isAdmin: boolean;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation();
const { data: allegati, isLoading: isLoadingAllegati } =
api.storage.getUserStorage.useQuery({ userId: userId });
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate: deleteFile } = api.storage.deleteUserStorage.useMutation({
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate({
userId: userId,
});
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
id: context?.toastId,
});
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate: renameFile } = api.storage.renameFile.useMutation({
onMutate: () => {
setEditOpen(false);
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate({
userId: userId,
});
toast.success(t.successo, {
icon: "👍",
id: context?.toastId,
});
},
});
const [editData, setEditData] = useState<{
id: StorageindexId;
filename: string;
} | null>(null);
const [editOpen, setEditOpen] = useState(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const handleEditSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
if (!formData.get("id") || !formData.get("filename")) {
return;
}
renameFile({
fileId: formData.get("id") as StorageindexId,
name: formData.get("filename") as string,
});
};
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
if (isLoadingAllegati) return <LoadingPage />;
if (!allegati) return <Status500 />;
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const from_admin = allegati.filter((a) => a.from_admin);
const not_from_admin = allegati.filter((a) => !a.from_admin);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<div className="flex flex-col space-y-5">
<h1 className="text-primary text-2xl font-bold">{t.allegati.title}</h1>
<h1 className="text-xl font-semibold">{t.allegati.da_info}</h1>
{from_admin && (
<FileList
files={from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ id, filename });
setEditOpen(true);
}}
delete_onClick={(id) => deleteFile({ userId, fileId: id })}
download_onClick={(file) =>
handleDownload({
file,
getToken: async () => await getToken(),
})
}
/>
)}
<Separator />
<div className="flex items-center gap-3">
<UploadModal userId={userId} isAdmin={isAdmin} />{" "}
<span className="text-sm">Max 5 MB per file</span>
</div>
<div className="space-y-2 rounded-md border border-orange-500 p-2">
<div className="flex items-center gap-2 text-orange-500">
<TriangleAlert className="size-5" />
{t.importante}:
</div>
<span className="text-sm">{t.allegati.disclamer}</span>
</div>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
{not_from_admin && (
<FileList
files={not_from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ id, filename });
setEditOpen(true);
}}
delete_onClick={(id) => deleteFile({ userId, fileId: id })}
download_onClick={(file) =>
handleDownload({
file,
getToken: async () => await getToken(),
})
}
/>
)}
<EditFileDialog
open={editOpen}
onOpenChange={setEditOpen}
data={editData}
handleEditSubmit={handleEditSubmit}
/>
</div>
);
2025-08-04 17:45:44 +02:00
};
export const ExtIcon = ({
2025-08-28 18:27:07 +02:00
ext,
className,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
ext: string | null | undefined;
className?: string;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
if (!ext) return <File className={cn("size-5 text-gray-500", className)} />;
if (ext.includes("pdf")) {
return <FileText className={cn("size-5 text-red-500", className)} />;
} else if (
ext.includes("image") ||
["jpg", "jpeg", "png", "gif", "svg", "webp"].some((ext) =>
ext.includes(ext),
)
) {
return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
} else if (
["html", "css", "js", "jsx", "ts", "tsx", "json", "xml"].some((ext) =>
ext.includes(ext),
)
) {
return <FileCode className={cn("size-5 text-emerald-500", className)} />;
} else if (["xls", "xlsx", "csv"].some((ext) => ext.includes(ext))) {
return (
<FileSpreadsheet className={cn("size-5 text-green-600", className)} />
);
} else if (
["zip", "rar", "7z", "tar", "gz"].some((ext) => ext.includes(ext))
) {
return <FileArchive className={cn("size-5 text-amber-600", className)} />;
} else if (["mp3", "wav", "ogg", "flac"].some((ext) => ext.includes(ext))) {
return <FileAudio className={cn("size-5 text-purple-500", className)} />;
} else if (
["mp4", "avi", "mov", "wmv", "webm"].some((ext) => ext.includes(ext))
) {
return <FileVideo className={cn("size-5 text-pink-500", className)} />;
} else {
return <File className={cn("size-5 text-gray-500", className)} />;
}
2025-08-04 17:45:44 +02:00
};
const EditFileDialog = ({
2025-08-28 18:27:07 +02:00
open,
onOpenChange,
data,
handleEditSubmit,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
open: boolean;
onOpenChange: (status: boolean) => void;
data: {
id: StorageindexId;
filename: string;
} | null;
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t.modifica}</DialogTitle>
<DialogDescription className="sr-only">Edit</DialogDescription>
</DialogHeader>
{data && (
<form onSubmit={handleEditSubmit} method="post">
<div className="flex items-center gap-3">
<label htmlFor="id" className="sr-only">
ID
</label>
<Input
type="text"
className="hidden"
name="id"
value={data.id}
readOnly
/>
<label htmlFor="filename" className="sr-only">
filename
</label>
<Input
type="text"
placeholder="Modifica messaggio"
className="mt-0"
id="filename"
name="filename"
defaultValue={data.filename || undefined}
/>
<Button type="submit">{t.salva}</Button>
</div>
</form>
)}
</DialogContent>
</Dialog>
);
2025-08-04 17:45:44 +02:00
};
interface FileListProps {
2025-08-28 18:27:07 +02:00
files: Storageindex[];
delete_onClick: (id: StorageindexId) => void;
download_onClick: (file: Storageindex) => void;
isAdmin: boolean;
selectHandler: (id: StorageindexId, filename: string) => void;
2025-08-04 17:45:44 +02:00
}
function FileList({
2025-08-28 18:27:07 +02:00
files,
delete_onClick,
download_onClick,
isAdmin,
selectHandler,
2025-08-04 17:45:44 +02:00
}: FileListProps) {
2025-08-28 18:27:07 +02:00
return (
<div className="w-full">
<div className="rounded-md border">
<div className="bg-muted/50 text-muted-foreground grid grid-cols-12 gap-2 p-4 text-sm font-medium">
<div className="col-span-6 md:col-span-8">File</div>
<div className="col-span-4 md:col-span-3">Data</div>
<div className="col-span-2 text-right md:col-span-1">Azioni</div>
</div>
<div className="divide-y">
{files.map((file) => (
<div
key={file.id}
className="hover:bg-muted/50 grid grid-cols-12 items-center gap-2 p-4 text-sm transition-colors"
>
<div className="col-span-6 flex items-center gap-2 md:col-span-8">
<div className="shrink-0">
<ExtIcon ext={file.ext} />
</div>
<Link
aria-label="Allegato"
href={`/area-riservata/allegato-view/${file.id}`}
target="_blank"
className="text-foreground truncate font-medium hover:underline"
>
{file.filename}
</Link>
</div>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<div className="text-muted-foreground col-span-4 md:col-span-3">
{format(file.created_at, "d MMM yyyy")}
</div>
<div className="col-span-2 flex justify-end gap-1 md:col-span-1">
<Button
aria-label="Download"
variant="ghost"
size="icon"
asChild
className="size-8"
title="Download"
>
<Button
variant="outline"
className="flex gap-1 text-sm"
onClick={() => download_onClick(file)}
>
<Download className="size-4" />
</Button>
</Button>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
{isAdmin && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<MoreVertical className="size-4" />
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
aria-label="Rinomina"
onClick={() =>
selectHandler(file.id, file.filename || "")
}
>
<Pencil className="mr-2 size-4" />
Rinomina
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Elimina"
onClick={() => delete_onClick(file.id)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 size-4" />
Elimina
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
))}
</div>
</div>
</div>
);
2025-08-04 17:45:44 +02:00
}