feat: add PDF viewer component and integrate with AllegatoIframe for improved document handling

This commit is contained in:
Marco Pedone 2025-09-09 14:51:23 +02:00
parent cb9fce9289
commit 6d69e6c037
5 changed files with 384 additions and 4 deletions

View file

@ -69,6 +69,7 @@
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-is": "^19.1.1", "react-is": "^19.1.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-pdf": "^10.1.0",
"react-phone-number-input": "^3.4.12", "react-phone-number-input": "^3.4.12",
"react-reverse-portal": "^2.3.0", "react-reverse-portal": "^2.3.0",
"react-select": "^5.10.2", "react-select": "^5.10.2",
@ -2499,6 +2500,191 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@napi-rs/canvas": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.79.tgz",
"integrity": "sha512-0SkvRRjyxY35eniEsQsjPYUMWunKlAWvionJOzJJADZF5ZDf/sL+ncJbMTV5LUiHg1iHOvVjWcuDOx/GNXr/lA==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.79",
"@napi-rs/canvas-darwin-arm64": "0.1.79",
"@napi-rs/canvas-darwin-x64": "0.1.79",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.79",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.79",
"@napi-rs/canvas-linux-arm64-musl": "0.1.79",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.79",
"@napi-rs/canvas-linux-x64-gnu": "0.1.79",
"@napi-rs/canvas-linux-x64-musl": "0.1.79",
"@napi-rs/canvas-win32-x64-msvc": "0.1.79"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.79.tgz",
"integrity": "sha512-ih6ZIztNDEXl7axvC4swOwLFrM9lOyJa9VAMq7xIBtEZhR/8IVDa0ZTup2fZEiTCmnjmXolzv7uDviHkOTEMKQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.79.tgz",
"integrity": "sha512-REMz1Fac2VlOYJDg+JjmQWSJc459cCgVom6GvKwWkDqzSjvG9BSo72MDmQY3uhb7r49Xuz5gTFcLYTfNcm4MoA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.79.tgz",
"integrity": "sha512-uQxLg6Bll7zv/ljp/YIeiUFWfV9C/ESv+2ioUh60hIAypuhtg6hhtWE/KnoW7G48wQls5VUStvEnJbnJ7bPKlA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.79.tgz",
"integrity": "sha512-X37B//TVIipL/3RyvyfNlbQK2uyIaK3PJ2bH7ZeU+jpkaYprBsV15GCN/LHTYAi6R0F/c53zK3aSFNKkGHM/Og==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.79.tgz",
"integrity": "sha512-+T1fuau1heabE6zGXiqZBGPH5fTIQF+xEu/u4fuugxEiChRYlhnPjkw26MBi8ePg/jmzxLfJEij6LMJQ4AQa2A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.79.tgz",
"integrity": "sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.79.tgz",
"integrity": "sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.79.tgz",
"integrity": "sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.79.tgz",
"integrity": "sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.79",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.79.tgz",
"integrity": "sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz",
@ -10765,7 +10951,6 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -13540,6 +13725,24 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/make-cancellable-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
"integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
}
},
"node_modules/make-event-props": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
}
},
"node_modules/markdown-it": { "node_modules/markdown-it": {
"version": "14.1.0", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
@ -13611,6 +13814,23 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/merge-refs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
"integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -14720,6 +14940,18 @@
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pdfjs-dist": {
"version": "5.3.93",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.93.tgz",
"integrity": "sha512-w3fQKVL1oGn8FRyx5JUG5tnbblggDqyx2XzA5brsJ5hSuS+I0NdnJANhmeWKLjotdbPQucLBug5t0MeWr0AAdg==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.71"
}
},
"node_modules/peberminta": { "node_modules/peberminta": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
@ -16059,6 +16291,35 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
} }
}, },
"node_modules/react-pdf": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.1.0.tgz",
"integrity": "sha512-iUI1YqWgwwZcsXjrehTp3Yi8nT/bvTaWULaRMMyJWvoqqSlopk4LQQ9GDqUnDtX3gzT2glrqrLbjIPl56a+Q3w==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"dequal": "^2.0.3",
"make-cancellable-promise": "^2.0.0",
"make-event-props": "^2.0.0",
"merge-refs": "^2.0.0",
"pdfjs-dist": "5.3.93",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-phone-number-input": { "node_modules/react-phone-number-input": {
"version": "3.4.12", "version": "3.4.12",
"resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.12.tgz", "resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.12.tgz",
@ -18426,6 +18687,15 @@
"node": "20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.4", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",

View file

@ -18,7 +18,6 @@
"types": "node ./headers/type.js && tsc --noEmit" "types": "node ./headers/type.js && tsc --noEmit"
}, },
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fattureincloud/fattureincloud-ts-sdk": "^2.1.1", "@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
"@hookform/resolvers": "^5.2.1", "@hookform/resolvers": "^5.2.1",
@ -81,6 +80,7 @@
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-is": "^19.1.1", "react-is": "^19.1.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-pdf": "^10.1.0",
"react-phone-number-input": "^3.4.12", "react-phone-number-input": "^3.4.12",
"react-reverse-portal": "^2.3.0", "react-reverse-portal": "^2.3.0",
"react-select": "^5.10.2", "react-select": "^5.10.2",

View file

@ -1,9 +1,17 @@
import dynamic from "next/dynamic";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import type { TempTokensToken } from "~/schemas/public/TempTokens"; import type { TempTokensToken } from "~/schemas/public/TempTokens";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
const PDFViewer = dynamic(
() => import("./pdf-viewer").then((mod) => mod.PDFViewer),
{
ssr: false,
loading: () => <LoadingPage />,
},
);
export const AllegatoIframe = ({ export const AllegatoIframe = ({
className, className,
allegato, allegato,
@ -14,8 +22,27 @@ export const AllegatoIframe = ({
const if_ref = useRef<HTMLIFrameElement>(null); const if_ref = useRef<HTMLIFrameElement>(null);
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation(); const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const [token, setToken] = useState<TempTokensToken | null>(null); const [token, setToken] = useState<TempTokensToken | null>(null);
const [isMobile, setIsMobile] = useState(false);
const [isIOS, setIsIOS] = useState(false);
const [isPDF, setIsPDF] = useState(false);
useEffect(() => {
// Detect mobile and iOS
const userAgent = navigator.userAgent.toLowerCase();
setIsMobile(
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
userAgent,
),
);
setIsIOS(/iphone|ipad|ipod/i.test(userAgent));
// Try to detect if file is PDF from extension
setIsPDF(allegato.toLowerCase().endsWith(".pdf"));
}, [allegato]);
const handleIframeLoad = () => { const handleIframeLoad = () => {
if (if_ref.current?.contentWindow) { if (if_ref.current?.contentWindow) {
// For images
const img = if_ref.current.contentWindow.document.querySelector("img"); const img = if_ref.current.contentWindow.document.querySelector("img");
if (img) { if (img) {
img.style.width = "100%"; img.style.width = "100%";
@ -24,6 +51,7 @@ export const AllegatoIframe = ({
} }
} }
}; };
useEffect(() => { useEffect(() => {
async function set() { async function set() {
const token = await getToken(); const token = await getToken();
@ -35,6 +63,19 @@ export const AllegatoIframe = ({
if (!token) return <LoadingPage />; if (!token) return <LoadingPage />;
const requestUrl = `/go-api/storage/get/${allegato}?token=${String(token)}`; const requestUrl = `/go-api/storage/get/${allegato}?token=${String(token)}`;
// For PDF on iOS, provide download link instead
if (isPDF && (isIOS || isMobile)) {
return (
<div
className={cn(
"flex h-full w-full items-center justify-center object-contain",
className,
)}
>
<PDFViewer url={requestUrl} />
</div>
);
}
return ( return (
<div <div

View file

@ -0,0 +1,71 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/Page/AnnotationLayer.css";
import "react-pdf/dist/Page/TextLayer.css";
import type { PDFDocumentProxy } from "pdfjs-dist";
import { LoadingPage } from "./loading";
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url,
).toString();
const options = {
cMapUrl: "/cmaps/",
standardFontDataUrl: "/standard_fonts/",
wasmUrl: "/wasm/",
};
export const PDFViewer = ({ url }: { url: string }) => {
const [numPages, setNumPages] = useState<number>();
const containerRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
const file = useMemo(() => ({ url }), [url]);
const onResize = useCallback<ResizeObserverCallback>((entries) => {
const [entry] = entries;
if (entry) {
setWidth(entry.contentRect.width);
}
}, []);
const observer = new ResizeObserver(onResize);
useEffect(() => {
if (containerRef.current) {
observer.observe(containerRef.current);
setWidth(containerRef.current.offsetWidth);
}
}, []);
function onDocumentLoadSuccess({
numPages: nextNumPages,
}: PDFDocumentProxy): void {
setNumPages(nextNumPages);
}
return (
<div
className="w-full flex justify-center outline mx-1 shadow-lg rounded-md overflow-clip"
ref={containerRef}
>
<Document
className={"overflow-auto aspect-[1/1.4142]"}
file={file}
loading={<LoadingPage />}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
>
{Array.from(new Array(numPages), (_el, index) => (
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={width}
/>
))}
</Document>
</div>
);
};

View file

@ -731,9 +731,7 @@ const AdminLavoraConferma = () => {
<CredenzaBody className="max-h-[80vh] w-full overflow-y-auto pb-5"> <CredenzaBody className="max-h-[80vh] w-full overflow-y-auto pb-5">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<DocSection handleUpdate={handleUpdate} /> <DocSection handleUpdate={handleUpdate} />
<Separator />
<Caparra handleUpdate={handleUpdate} /> <Caparra handleUpdate={handleUpdate} />
<Separator />
<SendConferma handleUpdate={handleUpdate} /> <SendConferma handleUpdate={handleUpdate} />
</div> </div>
</CredenzaBody> </CredenzaBody>