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

372 lines
9.6 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";
2025-10-24 16:10:59 +02:00
2025-08-28 18:27:07 +02:00
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import type { UsersId } from "~/schemas/public/Users";
2025-10-24 16:10:59 +02:00
import type { FileMetadata } from "~/server/services/storage.service";
2025-08-28 18:27:07 +02:00
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 } =
2025-10-24 16:10:59 +02:00
api.storage.retrieveUserFileData.useQuery({ userId });
2025-08-04 17:45:44 +02:00
2025-10-24 16:10:59 +02:00
const { mutate: deleteFile } = api.storage.deleteUserStorageEntry.useMutation(
{
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.retrieveUserFileData.invalidate({
userId: userId,
});
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
id: context?.toastId,
});
},
2025-08-28 18:27:07 +02:00
},
2025-10-24 16:10:59 +02:00
);
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) => {
2025-10-24 16:10:59 +02:00
await utils.storage.retrieveUserFileData.invalidate({
2025-08-28 18:27:07 +02:00
userId: userId,
});
toast.success(t.successo, {
icon: "👍",
id: context?.toastId,
});
},
});
const [editData, setEditData] = useState<{
2025-10-24 16:10:59 +02:00
id: string;
2025-08-28 18:27:07 +02:00
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({
2025-10-24 16:10:59 +02:00
fileId: formData.get("id") as string,
2025-08-28 18:27:07 +02:00
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">
2025-10-10 16:18:43 +02:00
<h1 className="font-bold text-2xl text-primary">{t.allegati.title}</h1>
<h1 className="font-semibold text-xl">{t.allegati.da_info}</h1>
2025-08-28 18:27:07 +02:00
{from_admin && (
<FileList
2025-10-24 16:10:59 +02:00
delete_onClick={(id) => deleteFile({ storageId: id, userId })}
2025-08-29 16:18:32 +02:00
files={from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ filename, id });
setEditOpen(true);
}}
2025-08-28 18:27:07 +02:00
/>
)}
<Separator />
<div className="flex items-center gap-3">
2025-08-29 16:18:32 +02:00
<UploadModal isAdmin={isAdmin} userId={userId} />{" "}
2025-08-28 18:27:07 +02:00
<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
2025-10-24 16:10:59 +02:00
delete_onClick={(id) => deleteFile({ storageId: id, userId })}
2025-08-29 16:18:32 +02:00
files={not_from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ filename, id });
setEditOpen(true);
}}
2025-08-28 18:27:07 +02:00
/>
)}
<EditFileDialog
data={editData}
handleEditSubmit={handleEditSubmit}
2025-08-29 16:18:32 +02:00
onOpenChange={setEditOpen}
open={editOpen}
2025-08-28 18:27:07 +02:00
/>
</div>
);
2025-08-04 17:45:44 +02:00
};
export const ExtIcon = ({
2025-10-24 16:10:59 +02:00
mimeType,
2025-08-28 18:27:07 +02:00
className,
2025-08-04 17:45:44 +02:00
}: {
2025-10-24 16:10:59 +02:00
mimeType: string;
2025-08-28 18:27:07 +02:00
className?: string;
2025-08-04 17:45:44 +02:00
}) => {
2025-10-24 16:10:59 +02:00
const lower = mimeType?.toLowerCase() || "";
if (!lower) return <File className={cn("size-5 text-gray-500", className)} />;
// PDF
if (lower.includes("pdf")) {
2025-08-28 18:27:07 +02:00
return <FileText className={cn("size-5 text-red-500", className)} />;
}
2025-10-24 16:10:59 +02:00
// Images
if (
2025-10-24 16:10:59 +02:00
lower.startsWith("image/") ||
/\b(jpg|jpeg|png|gif|svg|webp)\b/.test(lower)
2025-08-28 18:27:07 +02:00
) {
return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
}
2025-10-24 16:10:59 +02:00
// Code files
if (/\b(html|css|js|jsx|ts|tsx|json|xml)\b/.test(lower)) {
2025-08-28 18:27:07 +02:00
return <FileCode className={cn("size-5 text-emerald-500", className)} />;
}
2025-10-24 16:10:59 +02:00
// Spreadsheets
if (/\b(xls|xlsx|csv)\b/.test(lower)) {
2025-08-28 18:27:07 +02:00
return (
<FileSpreadsheet className={cn("size-5 text-green-600", className)} />
);
}
2025-10-24 16:10:59 +02:00
// Archives
if (/\b(zip|rar|7z|tar|gz)\b/.test(lower)) {
2025-08-28 18:27:07 +02:00
return <FileArchive className={cn("size-5 text-amber-600", className)} />;
}
2025-10-24 16:10:59 +02:00
// Audio
if (/\b(mp3|wav|ogg|flac)\b/.test(lower)) {
2025-08-28 18:27:07 +02:00
return <FileAudio className={cn("size-5 text-purple-500", className)} />;
}
2025-10-24 16:10:59 +02:00
// Video
if (/\b(mp4|avi|mov|wmv|webm)\b/.test(lower)) {
2025-08-28 18:27:07 +02:00
return <FileVideo className={cn("size-5 text-pink-500", className)} />;
}
2025-10-24 16:10:59 +02:00
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: {
2025-10-24 16:10:59 +02:00
id: string;
2025-08-28 18:27:07 +02:00
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 (
2025-08-29 16:18:32 +02:00
<Dialog onOpenChange={onOpenChange} open={open}>
2025-08-28 18:27:07 +02:00
<DialogContent>
<DialogHeader>
<DialogTitle>{t.modifica}</DialogTitle>
<DialogDescription className="sr-only">Edit</DialogDescription>
</DialogHeader>
{data && (
2025-08-29 16:18:32 +02:00
<form method="post" onSubmit={handleEditSubmit}>
2025-08-28 18:27:07 +02:00
<div className="flex items-center gap-3">
2025-08-29 16:18:32 +02:00
<label className="sr-only" htmlFor="id">
2025-08-28 18:27:07 +02:00
ID
</label>
<Input
className="hidden"
name="id"
readOnly
2025-08-29 16:18:32 +02:00
type="text"
value={data.id}
2025-08-28 18:27:07 +02:00
/>
2025-08-29 16:18:32 +02:00
<label className="sr-only" htmlFor="filename">
2025-08-28 18:27:07 +02:00
filename
</label>
<Input
className="mt-0"
2025-08-29 16:18:32 +02:00
defaultValue={data.filename || undefined}
2025-08-28 18:27:07 +02:00
id="filename"
name="filename"
2025-08-29 16:18:32 +02:00
placeholder="Modifica messaggio"
type="text"
2025-08-28 18:27:07 +02:00
/>
<Button type="submit">{t.salva}</Button>
</div>
</form>
)}
</DialogContent>
</Dialog>
);
2025-08-04 17:45:44 +02:00
};
interface FileListProps {
2025-10-24 16:10:59 +02:00
files: FileMetadata[];
delete_onClick: (id: string) => void;
2025-08-28 18:27:07 +02:00
isAdmin: boolean;
2025-10-24 16:10:59 +02:00
selectHandler: (id: string, filename: string) => void;
2025-08-04 17:45:44 +02:00
}
function FileList({
2025-08-28 18:27:07 +02:00
files,
delete_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">
2025-10-10 16:18:43 +02:00
<div className="grid grid-cols-12 gap-2 bg-muted/50 p-4 font-medium text-muted-foreground text-sm">
2025-08-28 18:27:07 +02:00
<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
2025-10-10 16:18:43 +02:00
className="grid grid-cols-12 items-center gap-2 p-4 text-sm transition-colors hover:bg-muted/50"
2025-08-29 16:18:32 +02:00
key={file.id}
2025-08-28 18:27:07 +02:00
>
<div className="col-span-6 flex items-center gap-2 md:col-span-8">
<div className="shrink-0">
2025-10-24 16:10:59 +02:00
<ExtIcon mimeType={file.mimeType} />
2025-08-28 18:27:07 +02:00
</div>
<Link
aria-label="Allegato"
2025-10-10 16:18:43 +02:00
className="truncate font-medium text-foreground hover:underline"
2025-08-28 18:27:07 +02:00
href={`/area-riservata/allegato-view/${file.id}`}
target="_blank"
>
2025-10-24 16:10:59 +02:00
{file.originalName}
2025-08-28 18:27:07 +02:00
</Link>
</div>
2025-08-04 17:45:44 +02:00
2025-10-10 16:18:43 +02:00
<div className="col-span-4 text-muted-foreground md:col-span-3">
2025-10-24 16:10:59 +02:00
{format(new Date(file.uploadedAt), "d MMM yyyy")}
2025-08-28 18:27:07 +02:00
</div>
<div className="col-span-2 flex justify-end gap-1 md:col-span-1">
2025-10-24 16:10:59 +02:00
<Link href={`/storage-api/get/${file.id}?mode=download`}>
2025-08-28 18:27:07 +02:00
<Button
2025-10-24 16:10:59 +02:00
className="flex size-8 gap-1 text-sm"
size="icon"
2025-08-29 16:18:32 +02:00
variant="outline"
2025-08-28 18:27:07 +02:00
>
<Download className="size-4" />
</Button>
2025-10-24 16:10:59 +02:00
</Link>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
{isAdmin && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
2025-08-29 16:18:32 +02:00
<Button className="size-8" size="icon" variant="ghost">
2025-08-28 18:27:07 +02:00
<MoreVertical className="size-4" />
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
aria-label="Rinomina"
onClick={() =>
2025-10-24 16:10:59 +02:00
selectHandler(file.id, file.originalName || "")
2025-08-28 18:27:07 +02:00
}
>
<Pencil className="mr-2 size-4" />
Rinomina
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Elimina"
className="text-destructive focus:text-destructive"
2025-08-29 16:18:32 +02:00
onClick={() => delete_onClick(file.id)}
2025-08-28 18:27:07 +02:00
>
<Trash2 className="mr-2 size-4" />
Elimina
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
))}
</div>
</div>
</div>
);
2025-08-04 17:45:44 +02:00
}