feat: integrate PdfPreviewModal across apps

This commit is contained in:
estifanos
2026-08-19 08:59:08 +00:00
parent 4d87d41e55
commit b860e57227
7 changed files with 118 additions and 38 deletions

View File

@@ -8,7 +8,6 @@ import {
Divider,
Group,
Loader,
Modal,
Paper,
SimpleGrid,
Stack,
@@ -36,6 +35,7 @@ import {
useGetMySeaServiceRecordsQuery,
useGetMyMedicalCertificatesQuery,
} from '@ema-platform/api';
import { PdfPreviewModal } from '@ema-platform/ui';
// ---------------------------------------------------------------------------
// Mock data
@@ -353,21 +353,12 @@ export function CertificatesPage() {
)}
</Paper>
{/* Preview modal */}
<Modal
<PdfPreviewModal
opened={!!previewUrl}
onClose={() => setPreviewUrl(null)}
title={<Text fw={700} fz="sm">{previewTitle}</Text>}
size="95vw"
radius="lg"
fullScreen
>
<iframe
src={previewUrl ?? ''}
style={{ width: '100%', height: '90vh', border: 'none', borderRadius: 8 }}
title={previewTitle}
/>
</Modal>
url={previewUrl ?? ''}
title={previewTitle}
/>
</Stack>
);
}

View File

@@ -23,6 +23,7 @@ import {
type DocumentRequirement,
} from '@ema-platform/api';
import { useTranslation } from 'react-i18next';
import { PdfPreviewModal } from '@ema-platform/ui';
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
@@ -61,6 +62,9 @@ export function DocumentSlots({
const { t } = useTranslation();
const [busy, setBusy] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [preview, setPreview] = useState<{ url: string; title: string } | null>(
null,
);
const resetRefs = useRef<Record<string, () => void>>({});
const required = requirements.filter(
@@ -100,6 +104,7 @@ export function DocumentSlots({
{required.map((requirement) => {
const existing = attachments.find((a) => a.documentKey === requirement.key);
const uploaded = Boolean(existing?.files?.length);
const fileUrl = existing?.files?.[0]?.url;
const flagRemark = flagged[requirement.key];
const locked = readOnly || (restrictToFlagged && !flagRemark);
@@ -153,13 +158,16 @@ export function DocumentSlots({
</div>
<Group gap="xs" wrap="nowrap">
{existing?.files?.[0]?.url && (
{fileUrl && (
<Button
size="xs"
variant="subtle"
component="a"
href={existing.files[0].url}
target="_blank"
onClick={() =>
setPreview({
url: fileUrl,
title: localized(requirement.name),
})
}
>
{t('licensing.documents.view')}
</Button>
@@ -198,6 +206,12 @@ export function DocumentSlots({
</Card>
);
})}
<PdfPreviewModal
opened={Boolean(preview)}
onClose={() => setPreview(null)}
url={preview?.url ?? ''}
title={preview?.title}
/>
</Stack>
);
}

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Button, FileButton, Group, Loader } from '@mantine/core';
import { ActionIcon, Button, FileButton, Group, Loader } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconCheck } from '@tabler/icons-react';
import { IconCheck, IconEye } from '@tabler/icons-react';
import {
useLocalized,
uploadDocument,
@@ -9,6 +9,7 @@ import {
type StaffEvidenceRequirement,
} from '@ema-platform/api';
import { useTranslation } from 'react-i18next';
import { PdfPreviewModal } from '@ema-platform/ui';
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
@@ -31,6 +32,9 @@ export function StaffEvidence({ staffId, evidence, readOnly, onUploaded }: Props
ownerId: staffId,
});
const [busy, setBusy] = useState<string | null>(null);
const [preview, setPreview] = useState<{ url: string; title: string } | null>(
null,
);
const localized = useLocalized();
const { t } = useTranslation();
@@ -39,12 +43,14 @@ export function StaffEvidence({ staffId, evidence, readOnly, onUploaded }: Props
return (
<Group gap="xs">
{evidence.map((item) => {
const uploaded = attachments.some(
const attachment = attachments.find(
(a) => a.documentKey === item.docKey && a.files?.length,
);
const uploaded = Boolean(attachment);
const fileUrl = attachment?.files?.[0]?.url;
return (
<Group key={item.docKey} gap={4} wrap="nowrap">
<FileButton
key={item.docKey}
accept="application/pdf,image/jpeg,image/png"
onChange={async (file) => {
if (!file) return;
@@ -89,8 +95,27 @@ export function StaffEvidence({ staffId, evidence, readOnly, onUploaded }: Props
</Button>
)}
</FileButton>
{uploaded && fileUrl && (
<ActionIcon
size="sm"
variant="subtle"
aria-label={t('licensing.documents.view', 'View')}
onClick={() =>
setPreview({ url: fileUrl, title: localized(item.label) })
}
>
<IconEye size={14} />
</ActionIcon>
)}
</Group>
);
})}
<PdfPreviewModal
opened={Boolean(preview)}
onClose={() => setPreview(null)}
url={preview?.url ?? ''}
title={preview?.title}
/>
</Group>
);
}

View File

@@ -28,7 +28,13 @@ import {
} from '@tabler/icons-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AdvancedTable, AmharicDatePicker, notify, useServerTable } from '@ema-platform/ui';
import {
AdvancedTable,
AmharicDatePicker,
notify,
PdfPreviewModal,
useServerTable,
} from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import {
extractErrorMessage,
@@ -74,6 +80,9 @@ function EvidenceModal({
{ ownerType, ownerId: ownerId ?? '' },
{ skip: !ownerId },
);
const [preview, setPreview] = useState<{ url: string; title: string } | null>(
null,
);
const files = (attachments ?? []).flatMap((a) => a.files);
@@ -91,7 +100,14 @@ function EvidenceModal({
<Group key={file.id} gap="xs">
<IconPaperclip size={16} />
{file.url ? (
<Anchor href={file.url} target="_blank" size="sm">
<Anchor
component="button"
type="button"
size="sm"
onClick={() =>
setPreview({ url: file.url as string, title: file.originalName })
}
>
{file.originalName}
</Anchor>
) : (
@@ -101,6 +117,12 @@ function EvidenceModal({
))
)}
</Stack>
<PdfPreviewModal
opened={Boolean(preview)}
onClose={() => setPreview(null)}
url={preview?.url ?? ''}
title={preview?.title}
/>
</Modal>
);
}