2025-08-11 09:16:16 +01:00
|
|
|
import { useState } from "react";
|
2026-04-17 10:50:16 +01:00
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
Stack,
|
|
|
|
|
Text,
|
|
|
|
|
Group,
|
|
|
|
|
Badge,
|
|
|
|
|
Button,
|
|
|
|
|
Box,
|
|
|
|
|
Image,
|
|
|
|
|
ThemeIcon,
|
|
|
|
|
ActionIcon,
|
|
|
|
|
Tooltip,
|
|
|
|
|
Loader,
|
|
|
|
|
} from "@mantine/core";
|
2025-06-04 19:04:56 +01:00
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
|
|
|
|
import StorageIcon from "@mui/icons-material/Storage";
|
2025-06-20 17:51:24 +01:00
|
|
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
|
|
|
|
import EditIcon from "@mui/icons-material/Edit";
|
2025-06-04 19:04:56 +01:00
|
|
|
|
2025-10-28 10:29:36 +00:00
|
|
|
import { StirlingFileStub } from "@app/types/fileContext";
|
|
|
|
|
import { getFileSize, getFileDate } from "@app/utils/fileUtils";
|
|
|
|
|
import { useIndexedDBThumbnail } from "@app/hooks/useIndexedDBThumbnail";
|
2025-06-04 19:04:56 +01:00
|
|
|
|
|
|
|
|
interface FileCardProps {
|
2025-08-21 17:30:26 +01:00
|
|
|
file: File;
|
2025-09-16 15:08:11 +01:00
|
|
|
fileStub?: StirlingFileStub;
|
2025-06-04 19:04:56 +01:00
|
|
|
onRemove: () => void;
|
|
|
|
|
onDoubleClick?: () => void;
|
2025-06-20 17:51:24 +01:00
|
|
|
onView?: () => void;
|
|
|
|
|
onEdit?: () => void;
|
|
|
|
|
isSelected?: boolean;
|
|
|
|
|
onSelect?: () => void;
|
2025-08-01 16:08:04 +01:00
|
|
|
isSupported?: boolean; // Whether the file format is supported by the current tool
|
2025-06-04 19:04:56 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 17:41:19 +01:00
|
|
|
const FileCard = ({
|
|
|
|
|
file,
|
|
|
|
|
fileStub,
|
|
|
|
|
onRemove,
|
|
|
|
|
onDoubleClick,
|
|
|
|
|
onView,
|
|
|
|
|
onEdit,
|
|
|
|
|
isSelected,
|
|
|
|
|
onSelect,
|
|
|
|
|
isSupported = true,
|
|
|
|
|
}: FileCardProps) => {
|
2025-06-04 19:04:56 +01:00
|
|
|
const { t } = useTranslation();
|
2025-08-21 17:30:26 +01:00
|
|
|
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
|
2026-04-17 10:50:16 +01:00
|
|
|
const { thumbnail: indexedDBThumb, isGenerating } =
|
|
|
|
|
useIndexedDBThumbnail(fileStub);
|
2025-09-16 15:08:11 +01:00
|
|
|
const thumb = fileStub?.thumbnailUrl || indexedDBThumb;
|
2025-06-20 17:51:24 +01:00
|
|
|
const [isHovered, setIsHovered] = useState(false);
|
2025-06-04 19:04:56 +01:00
|
|
|
|
2026-01-06 11:54:14 -05:00
|
|
|
// Show loading state during hydration: PDF file without thumbnail yet
|
2026-04-10 17:41:19 +01:00
|
|
|
const isPdf = file.type === "application/pdf";
|
2026-01-06 11:54:14 -05:00
|
|
|
const isHydrating = isPdf && !thumb && !isGenerating;
|
|
|
|
|
|
2025-06-04 19:04:56 +01:00
|
|
|
return (
|
|
|
|
|
<Card
|
|
|
|
|
shadow="xs"
|
|
|
|
|
radius="md"
|
|
|
|
|
withBorder
|
|
|
|
|
p="xs"
|
2025-06-20 23:00:26 +01:00
|
|
|
style={{
|
|
|
|
|
width: 225,
|
|
|
|
|
minWidth: 180,
|
|
|
|
|
maxWidth: 260,
|
2025-08-01 16:08:04 +01:00
|
|
|
cursor: onDoubleClick && isSupported ? "pointer" : undefined,
|
2026-04-10 17:41:19 +01:00
|
|
|
position: "relative",
|
2026-04-17 10:50:16 +01:00
|
|
|
border: isSelected
|
|
|
|
|
? "2px solid var(--mantine-color-blue-6)"
|
|
|
|
|
: undefined,
|
2026-04-10 17:41:19 +01:00
|
|
|
backgroundColor: isSelected ? "var(--mantine-color-blue-0)" : undefined,
|
2025-08-01 16:08:04 +01:00
|
|
|
opacity: isSupported ? 1 : 0.5,
|
2026-04-10 17:41:19 +01:00
|
|
|
filter: isSupported ? "none" : "grayscale(50%)",
|
2025-06-04 19:04:56 +01:00
|
|
|
}}
|
|
|
|
|
onDoubleClick={onDoubleClick}
|
2025-06-20 17:51:24 +01:00
|
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
|
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
|
|
|
onClick={onSelect}
|
2025-08-01 16:08:04 +01:00
|
|
|
data-testid="file-card"
|
2025-10-20 15:07:40 +01:00
|
|
|
data-tour="file-card-checkbox"
|
2025-06-04 19:04:56 +01:00
|
|
|
>
|
|
|
|
|
<Stack gap={6} align="center">
|
|
|
|
|
<Box
|
|
|
|
|
style={{
|
|
|
|
|
border: "2px solid #e0e0e0",
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
width: 90,
|
|
|
|
|
height: 120,
|
|
|
|
|
display: "flex",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
margin: "0 auto",
|
|
|
|
|
background: "#fafbfc",
|
2026-04-10 17:41:19 +01:00
|
|
|
position: "relative",
|
2025-06-04 19:04:56 +01:00
|
|
|
}}
|
|
|
|
|
>
|
2025-06-20 17:51:24 +01:00
|
|
|
{/* Hover action buttons */}
|
|
|
|
|
{isHovered && (onView || onEdit) && (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
2026-04-10 17:41:19 +01:00
|
|
|
position: "absolute",
|
2025-06-20 17:51:24 +01:00
|
|
|
top: 4,
|
|
|
|
|
right: 4,
|
2026-04-10 17:41:19 +01:00
|
|
|
display: "flex",
|
2025-06-20 17:51:24 +01:00
|
|
|
gap: 4,
|
|
|
|
|
zIndex: 10,
|
2026-04-10 17:41:19 +01:00
|
|
|
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
2025-06-20 17:51:24 +01:00
|
|
|
borderRadius: 4,
|
2026-04-10 17:41:19 +01:00
|
|
|
padding: 2,
|
2025-06-20 17:51:24 +01:00
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
{onView && (
|
|
|
|
|
<Tooltip label="View in Viewer">
|
|
|
|
|
<ActionIcon
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="subtle"
|
|
|
|
|
color="blue"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onView();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<VisibilityIcon style={{ fontSize: 16 }} />
|
|
|
|
|
</ActionIcon>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
|
|
|
|
{onEdit && (
|
|
|
|
|
<Tooltip label="Open in File Editor">
|
|
|
|
|
<ActionIcon
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="subtle"
|
|
|
|
|
color="orange"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onEdit();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<EditIcon style={{ fontSize: 16 }} />
|
|
|
|
|
</ActionIcon>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-06-04 19:04:56 +01:00
|
|
|
{thumb ? (
|
2026-04-17 10:50:16 +01:00
|
|
|
<Image
|
|
|
|
|
src={thumb}
|
|
|
|
|
alt="PDF thumbnail"
|
|
|
|
|
height={110}
|
|
|
|
|
width={80}
|
|
|
|
|
fit="contain"
|
|
|
|
|
radius="sm"
|
|
|
|
|
/>
|
2026-04-10 17:41:19 +01:00
|
|
|
) : isGenerating || isHydrating ? (
|
2026-01-06 11:54:14 -05:00
|
|
|
<Stack align="center" justify="center" gap="xs">
|
|
|
|
|
<Loader size="sm" />
|
2026-04-10 17:41:19 +01:00
|
|
|
<Text size="xs" c="dimmed">
|
|
|
|
|
Loading...
|
|
|
|
|
</Text>
|
2026-01-06 11:54:14 -05:00
|
|
|
</Stack>
|
2025-06-04 19:04:56 +01:00
|
|
|
) : (
|
2026-04-10 17:41:19 +01:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-06-04 19:04:56 +01:00
|
|
|
<ThemeIcon
|
|
|
|
|
variant="light"
|
|
|
|
|
color={file.size > 100 * 1024 * 1024 ? "orange" : "red"}
|
|
|
|
|
size={60}
|
|
|
|
|
radius="sm"
|
2026-04-17 10:50:16 +01:00
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
}}
|
2025-06-04 19:04:56 +01:00
|
|
|
>
|
|
|
|
|
<PictureAsPdfIcon style={{ fontSize: 40 }} />
|
|
|
|
|
</ThemeIcon>
|
|
|
|
|
{file.size > 100 * 1024 * 1024 && (
|
2026-04-10 17:41:19 +01:00
|
|
|
<Text size="xs" c="dimmed" mt={4}>
|
|
|
|
|
Large File
|
|
|
|
|
</Text>
|
2025-06-04 19:04:56 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
2025-06-20 23:00:26 +01:00
|
|
|
|
2025-06-04 19:04:56 +01:00
|
|
|
<Text fw={500} size="sm" lineClamp={1} ta="center">
|
|
|
|
|
{file.name}
|
|
|
|
|
</Text>
|
2025-06-20 23:00:26 +01:00
|
|
|
|
2025-06-04 19:04:56 +01:00
|
|
|
<Group gap="xs" justify="center">
|
2025-06-20 23:00:26 +01:00
|
|
|
<Badge color="red" variant="light" size="sm">
|
2025-06-04 19:04:56 +01:00
|
|
|
{getFileSize(file)}
|
|
|
|
|
</Badge>
|
|
|
|
|
<Badge color="blue" variant="light" size="sm">
|
|
|
|
|
{getFileDate(file)}
|
|
|
|
|
</Badge>
|
2025-09-16 15:08:11 +01:00
|
|
|
{fileStub?.id && (
|
2026-04-17 10:50:16 +01:00
|
|
|
<Badge
|
|
|
|
|
color="green"
|
|
|
|
|
variant="light"
|
|
|
|
|
size="sm"
|
|
|
|
|
leftSection={<StorageIcon style={{ fontSize: 12 }} />}
|
|
|
|
|
>
|
2025-06-04 19:04:56 +01:00
|
|
|
DB
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
2025-08-01 16:08:04 +01:00
|
|
|
{!isSupported && (
|
|
|
|
|
<Badge color="orange" variant="filled" size="sm">
|
|
|
|
|
{t("fileManager.unsupported", "Unsupported")}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
2025-06-04 19:04:56 +01:00
|
|
|
</Group>
|
2025-06-20 23:00:26 +01:00
|
|
|
|
2025-06-04 19:04:56 +01:00
|
|
|
<Button
|
|
|
|
|
color="red"
|
|
|
|
|
size="xs"
|
|
|
|
|
variant="light"
|
2025-06-20 17:51:24 +01:00
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onRemove();
|
|
|
|
|
}}
|
2025-06-04 19:04:56 +01:00
|
|
|
mt={4}
|
|
|
|
|
>
|
|
|
|
|
{t("delete", "Remove")}
|
|
|
|
|
</Button>
|
|
|
|
|
</Stack>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-20 23:00:26 +01:00
|
|
|
export default FileCard;
|