My documents
- Employment contractPDF · 4 pages · signed 2 March 2026
- Payslip, September 2026PDF · 1 page · issued 30 September 2026
- P60, tax year 2025 to 2026PDF · 1 page · issued 31 May 2026
document-preview.tsx
"use client";
import { useState } from "react";
import { LuDownload, LuEye, LuFileText, LuRotateCcw, LuZoomIn, LuZoomOut } from "react-icons/lu";
import { Button, IconButton, Image, ListItem, Modal } from "@nexera-ui/react";
const DOCUMENTS = [
{ id: "contract", name: "Employment contract", meta: "PDF · 4 pages · signed 2 March 2026" },
{
id: "payslip",
name: "Payslip, September 2026",
meta: "PDF · 1 page · issued 30 September 2026",
},
{ id: "p60", name: "P60, tax year 2025 to 2026", meta: "PDF · 1 page · issued 31 May 2026" },
];
const ZOOMS = [50, 75, 100, 125, 150, 200];
/** Placeholder page drawn from theme tokens; replace with a rendered page image of the file. */
const PAGE = (
<svg viewBox="0 0 300 400" aria-hidden>
<rect width="300" height="400" className="fill-surface" />
<rect x="28" y="30" width="60" height="16" rx="4" className="fill-brand-primary" />
<rect x="28" y="70" width="180" height="12" rx="3" className="fill-strong" />
{[100, 118, 136, 154, 190, 208, 226, 262, 280, 298].map((y, i) => (
<rect
key={y}
x="28"
y={y}
width={i % 3 === 2 ? 160 : 244}
height="8"
rx="3"
className="fill-sunken"
/>
))}
<rect x="28" y="340" width="90" height="1.5" className="fill-strong" />
<rect x="182" y="340" width="90" height="1.5" className="fill-strong" />
</svg>
);
/** Document preview: open a file from a list in a modal and zoom in and out. */
export default function DocumentPreview() {
const [doc, setDoc] = useState<(typeof DOCUMENTS)[number] | null>(null);
const [zoom, setZoom] = useState(2);
const percent = ZOOMS[zoom] ?? 100;
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<section
aria-labelledby="docs-title"
className="mx-auto grid max-w-2xl gap-4 rounded-xl border border-default bg-surface p-5 shadow-sm @3xl:p-6"
>
<h2 id="docs-title" className="text-heading-h3 text-primary">
My documents
</h2>
<ul className="grid divide-y divide-subtle">
{DOCUMENTS.map((d) => (
<li key={d.id} className="flex flex-wrap items-center gap-2">
<ListItem
className="min-w-0 flex-1"
icon={<LuFileText />}
title={d.name}
subtitle={d.meta}
/>
<Button
variant="ghost"
leadingIcon={<LuEye />}
aria-label={`Preview ${d.name}`}
onPress={() => {
setZoom(2);
setDoc(d);
}}
>
Preview
</Button>
</li>
))}
</ul>
</section>
<Modal
title={doc?.name ?? "Document"}
subtitle={doc?.meta}
size="lg"
isOpen={doc !== null}
onOpenChange={(open) => {
if (!open) setDoc(null);
}}
secondaryAction={
<Button variant="secondary" size="md" slot="close">
Close
</Button>
}
primaryAction={
<Button size="md" leadingIcon={<LuDownload />}>
Download
</Button>
}
>
<div className="grid gap-3">
<div role="toolbar" aria-label="Zoom" className="flex items-center justify-center gap-2">
<IconButton
icon={<LuZoomOut />}
aria-label="Zoom out"
variant="secondary"
isDisabled={zoom === 0}
onPress={() => {
setZoom(zoom - 1);
}}
/>
<output
aria-live="polite"
className="min-w-14 text-center text-body-strong text-primary"
>
{percent}%
</output>
<IconButton
icon={<LuZoomIn />}
aria-label="Zoom in"
variant="secondary"
isDisabled={zoom === ZOOMS.length - 1}
onPress={() => {
setZoom(zoom + 1);
}}
/>
<IconButton
icon={<LuRotateCcw />}
aria-label="Reset zoom"
variant="ghost"
isDisabled={zoom === 2}
onPress={() => {
setZoom(2);
}}
/>
</div>
<div
role="region"
aria-label="Page 1"
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- a scrollable region must be reachable by keyboard (WCAG 2.1.1); it is labelled
tabIndex={0}
className="max-h-[60vh] overflow-auto rounded-lg bg-sunken p-4 focus-visible:outline-2"
>
<div className="mx-auto shadow-md" style={{ width: `${(percent / 100) * 18}rem` }}>
<Image
src={null}
alt={`Page 1 of ${doc?.name ?? "the document"}`}
ratio="3-4"
fallback={PAGE}
/>
</div>
</div>
</div>
</Modal>
</div>
);
}