Feat: Implement dynamic, interactive client-side document previews for Word, Excel, and CSV in AssetViewerModal
This commit is contained in:
parent
6a22f954c7
commit
63b3835ecf
213
Channel-Frontend/package-lock.json
generated
213
Channel-Frontend/package-lock.json
generated
@ -13,12 +13,14 @@
|
||||
"@tanstack/react-query": "^5.101.2",
|
||||
"@tanstack/react-router": "^1.170.17",
|
||||
"axios": "^1.18.1",
|
||||
"docx-preview": "^0.4.0",
|
||||
"framer-motion": "^12.42.2",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-hook-form": "^7.81.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.4.3",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
@ -1211,6 +1213,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -1359,6 +1370,28 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@ -1390,6 +1423,24 @@
|
||||
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@ -1433,6 +1484,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/docx-preview": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.4.0.tgz",
|
||||
"integrity": "sha512-OdKtE/uj3M4RfGarLkGjahUzRg8/kBp0Sraj1r1NAY1tp/sTpHOBqDrzVf9onMBt9vxP6SdQ6bpLCUCsFwjgcA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jszip": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -1577,6 +1637,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
@ -1750,6 +1819,24 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isbot": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.2.0.tgz",
|
||||
@ -1769,6 +1856,27 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
@ -2178,6 +2286,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -2234,6 +2348,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
@ -2318,6 +2438,21 @@
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
||||
@ -2352,6 +2487,12 @@
|
||||
"@rolldown/binding-win32-x64-msvc": "1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@ -2385,6 +2526,12 @@
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@ -2395,6 +2542,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
|
||||
@ -2500,6 +2668,12 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz",
|
||||
@ -2578,6 +2752,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
|
||||
@ -15,12 +15,14 @@
|
||||
"@tanstack/react-query": "^5.101.2",
|
||||
"@tanstack/react-router": "^1.170.17",
|
||||
"axios": "^1.18.1",
|
||||
"docx-preview": "^0.4.0",
|
||||
"framer-motion": "^12.42.2",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-hook-form": "^7.81.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.4.3",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
|
||||
@ -6,6 +6,7 @@ import type { User } from '../../../types/auth';
|
||||
import Modal from '../../../components/ui/Modal';
|
||||
import Button from '../../../components/ui/Button';
|
||||
import MarkdownViewer from '../../../components/ui/MarkdownViewer';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
interface AssetViewerModalProps {
|
||||
isOpen: boolean;
|
||||
@ -25,6 +26,11 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const [isLoadingText, setIsLoadingText] = useState(false);
|
||||
const [textPreviewContent, setTextPreviewContent] = useState('');
|
||||
const [docBlob, setDocBlob] = useState<ArrayBuffer | null>(null);
|
||||
const [excelHtml, setExcelHtml] = useState<string>('');
|
||||
const [isLoadingDoc, setIsLoadingDoc] = useState(false);
|
||||
const [docLoadError, setDocLoadError] = useState(false);
|
||||
const wordContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const getFullAssetUrl = (url: string) => {
|
||||
let resolvedUrl = url;
|
||||
@ -130,10 +136,17 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const isWord = asset ? (asset.type.includes('word') || asset.url.toLowerCase().endsWith('.docx') || asset.url.toLowerCase().endsWith('.doc')) : false;
|
||||
const isSpreadsheet = asset ? (asset.type.includes('sheet') || asset.url.toLowerCase().endsWith('.xlsx') || asset.url.toLowerCase().endsWith('.xls') || asset.url.toLowerCase().endsWith('.csv')) : false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && asset) {
|
||||
setTextPreviewContent('');
|
||||
setIsLoadingText(false);
|
||||
setDocBlob(null);
|
||||
setExcelHtml('');
|
||||
setIsLoadingDoc(false);
|
||||
setDocLoadError(false);
|
||||
|
||||
if (
|
||||
asset.type !== 'url' &&
|
||||
@ -179,9 +192,49 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (isWord || isSpreadsheet) {
|
||||
setIsLoadingDoc(true);
|
||||
axiosInstance.get(getFullAssetUrl(asset.url), { responseType: 'arraybuffer' })
|
||||
.then(res => {
|
||||
const data = res.data;
|
||||
setDocBlob(data);
|
||||
|
||||
if (isSpreadsheet) {
|
||||
try {
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const html = XLSX.utils.sheet_to_html(worksheet, { header: '', footer: '' });
|
||||
setExcelHtml(html);
|
||||
} catch (err) {
|
||||
console.error("SheetJS Parsing error", err);
|
||||
setDocLoadError(true);
|
||||
}
|
||||
}
|
||||
setIsLoadingDoc(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
setDocLoadError(true);
|
||||
setIsLoadingDoc(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isOpen, asset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWord && docBlob && wordContainerRef.current) {
|
||||
wordContainerRef.current.innerHTML = '';
|
||||
import('docx-preview').then(({ renderAsync }) => {
|
||||
renderAsync(docBlob, wordContainerRef.current!)
|
||||
.catch(err => {
|
||||
console.error("Docx Preview error:", err);
|
||||
wordContainerRef.current!.innerHTML = '<div class="p-4 text-red-550 font-sans">Failed to render Word document preview. Please download the file to inspect.</div>';
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [docBlob, isWord]);
|
||||
|
||||
const formatBytes = (bytes: number, decimals = 2) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
@ -507,33 +560,97 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (asset.type.includes('word') || asset.type.includes('presentation') || asset.type.includes('sheet') || asset.url.toLowerCase().endsWith('.docx') || asset.url.toLowerCase().endsWith('.doc') || asset.url.toLowerCase().endsWith('.pptx') || asset.url.toLowerCase().endsWith('.ppt') || asset.url.toLowerCase().endsWith('.xlsx') || asset.url.toLowerCase().endsWith('.xls')) ? (
|
||||
) : (isWord || isSpreadsheet || asset.type.includes('presentation') || asset.url.toLowerCase().endsWith('.pptx') || asset.url.toLowerCase().endsWith('.ppt')) ? (
|
||||
<div className="w-full h-full flex flex-col min-h-0 bg-ink-0">
|
||||
<div className="bg-ink-100 px-4 py-2 border-b border-ink-200 flex items-center text-xs text-ink-600 font-bold select-none">
|
||||
<FileText className="w-4 h-4 text-ink-950 mr-1.5" />
|
||||
<span className="font-sans">Office Document Preview</span>
|
||||
<div className="bg-ink-100 px-4 py-2 border-b border-ink-200 flex justify-between items-center text-xs text-ink-600 font-bold select-none shrink-0">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FileText className="w-4 h-4 text-ink-950" />
|
||||
<span className="font-sans">
|
||||
{isWord ? 'Interactive Word Document Viewer' : isSpreadsheet ? 'Interactive Spreadsheet Grid Viewer' : 'Interactive Presentation Viewer'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-[10px] text-ink-400 font-mono font-normal truncate max-w-xs">{asset.title}</span>
|
||||
</div>
|
||||
{isLocalUrl(asset.url) ? (
|
||||
|
||||
{isLoadingDoc ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center space-y-3 bg-ink-50">
|
||||
<div className="w-8 h-8 rounded-full border-4 border-ink-900 border-t-transparent animate-spin" />
|
||||
<span className="text-xs text-ink-500 font-medium font-sans">Loading and rendering document...</span>
|
||||
</div>
|
||||
) : docLoadError ? (
|
||||
<div className="flex-1 p-8 text-center flex flex-col justify-center items-center max-w-lg mx-auto space-y-4 bg-ink-0">
|
||||
<div className="w-16 h-16 rounded-2xl bg-ink-55 border border-ink-100 flex items-center justify-center text-ink-900 shadow-sm">
|
||||
<div className="w-16 h-16 rounded-2xl bg-red-50 border border-red-100 flex items-center justify-center text-red-650 shadow-sm shrink-0">
|
||||
<FileText className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-ink-900 font-sans">Office Document Preview</h4>
|
||||
<p className="text-xs text-ink-500 mt-2 leading-relaxed font-sans">
|
||||
This asset is a Microsoft Office document ({asset.type.split('/').pop()?.toUpperCase() || 'DOCX'}).
|
||||
<h4 className="text-sm font-bold text-red-950 font-sans">Failed to Render Document</h4>
|
||||
<p className="text-xs text-slate-500 mt-2 leading-relaxed font-sans">
|
||||
We encountered an error loading or rendering this document ({asset.type.split('/').pop()?.toUpperCase() || 'FILE'}).
|
||||
</p>
|
||||
<p className="text-xs text-ink-500 mt-2 leading-relaxed bg-ink-50 border border-ink-100 rounded-xl p-3 text-left font-sans">
|
||||
<strong>Note:</strong> Microsoft Office Online Viewer is optimized for staging/production environments. In development (localhost or ngrok tunnel), external Microsoft services cannot fetch your local files. Please download this asset using the button below to view it locally.
|
||||
<p className="text-xs text-slate-500 mt-2 leading-relaxed bg-slate-50 border border-slate-100 rounded-xl p-3 text-left font-sans">
|
||||
Please download the asset directly to view it on your device.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : isSpreadsheet ? (
|
||||
<div className="flex-1 overflow-auto bg-slate-100 p-4 flex flex-col">
|
||||
<div className="excel-preview-container flex-1 bg-white border border-slate-200 shadow-sm rounded-xl overflow-auto p-4">
|
||||
<style>{`
|
||||
.excel-preview-container table {
|
||||
border-collapse: collapse;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
font-size: 11px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
.excel-preview-container th, .excel-preview-container td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 6px 12px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.excel-preview-container tr:nth-child(even) {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
.excel-preview-container tr:hover {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
`}</style>
|
||||
<div dangerouslySetInnerHTML={{ __html: excelHtml || '<div class="p-4 text-slate-400 font-sans italic">No data found in this spreadsheet.</div>' }} />
|
||||
</div>
|
||||
</div>
|
||||
) : isWord ? (
|
||||
<div className="flex-1 overflow-auto bg-slate-100 p-6 md:p-8 flex justify-center">
|
||||
<div
|
||||
ref={wordContainerRef}
|
||||
className="w-full max-w-4xl bg-white shadow-md border border-slate-200 rounded-xl p-8 md:p-12 overflow-y-auto"
|
||||
style={{ minHeight: '842px' }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<iframe
|
||||
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(getFullAssetUrl(asset.url))}`}
|
||||
className="w-full flex-1 border-0 min-h-0"
|
||||
title={asset.title}
|
||||
/>
|
||||
/* PowerPoint / Presentation */
|
||||
isLocalUrl(asset.url) ? (
|
||||
<div className="flex-1 p-8 text-center flex flex-col justify-center items-center max-w-lg mx-auto space-y-4 bg-ink-0">
|
||||
<div className="w-16 h-16 rounded-2xl bg-amber-50 border border-amber-200 flex items-center justify-center text-amber-950 shadow-sm shrink-0">
|
||||
<FileText className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-amber-950 font-sans">Presentation Slide Viewer</h4>
|
||||
<p className="text-xs text-amber-900/80 mt-2 leading-relaxed font-sans">
|
||||
This asset is a PowerPoint Presentation ({asset.type.split('/').pop()?.toUpperCase() || 'PPTX'}).
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-2 leading-relaxed bg-slate-50 border border-slate-100 rounded-xl p-3 text-left font-sans">
|
||||
<strong>Note:</strong> Microsoft Office Online Viewer is optimized for staging/production environments. In development (localhost or ngrok tunnel), external Microsoft services cannot fetch your local files. Please download this asset using the button below to view it locally.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<iframe
|
||||
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(getFullAssetUrl(asset.url))}`}
|
||||
className="w-full flex-1 border-0 min-h-0"
|
||||
title={asset.title}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : asset.type.includes('image') || asset.type.includes('png') || asset.type.includes('jpg') ? (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user