Feat: Add client-side dynamic thumbnail generators for DOCX and Spreadsheet assets

This commit is contained in:
kenilkb 2026-07-16 09:58:39 +05:30
parent 63b3835ecf
commit 6d694efeb0
3 changed files with 232 additions and 0 deletions

View File

@ -19,6 +19,8 @@ import type { Asset } from '../../../types/assets';
import type { User } from '../../../types/auth';
import { axiosInstance } from '../../../services/axios';
import { PdfThumbnail } from './PdfThumbnail';
import { DocxThumbnail } from './DocxThumbnail';
import { SpreadsheetThumbnail } from './SpreadsheetThumbnail';
const getFriendlyHostname = (urlStr: string) => {
try {
@ -259,6 +261,76 @@ export const AssetCard: React.FC<AssetCardProps> = ({
</div>
}
/>
) : isWord ? (
<DocxThumbnail
url={getFullAssetUrl(asset.url)}
fallback={
<div className="w-full h-full bg-white border border-slate-200 text-left select-none relative overflow-hidden p-3.5 flex flex-col justify-between">
<div className="absolute top-0 right-0 left-0 h-1 bg-indigo-600" />
<div className="flex justify-between items-center text-[7px] text-slate-400 font-bold tracking-wide border-b border-slate-100 pb-1.5">
<span>PARTNER ASSET LIBRARY</span>
<span className="font-extrabold text-indigo-650">DOCX</span>
</div>
<div className="space-y-2 flex-grow mt-3">
<h4 className="text-[11px] font-extrabold text-slate-900 leading-snug font-sans line-clamp-2">
{asset.title}
</h4>
<div className="space-y-1.5">
<div className="h-1.5 bg-slate-100 rounded w-full" />
<div className="h-1.5 bg-slate-100 rounded w-11/12" />
<div className="h-1.5 bg-slate-55 rounded w-3/4" />
</div>
</div>
<div className="flex items-center justify-between text-[7px] text-slate-400 font-bold border-t border-slate-100 pt-1.5 mt-auto font-mono">
<span>PAGE 1 OF 1</span>
<span className="text-slate-350">CONFIDENTIAL</span>
</div>
</div>
}
/>
) : isSpreadsheet ? (
<SpreadsheetThumbnail
url={getFullAssetUrl(asset.url)}
fallback={
<div className="w-full h-full bg-white border border-slate-200 text-left select-none relative overflow-hidden flex flex-col justify-between">
<div className="h-5 bg-emerald-700 border-b border-emerald-800 flex items-center px-2 shrink-0 justify-between select-none">
<span className="text-[8px] font-extrabold text-white tracking-wider">Spreadsheet Editor</span>
<span className="text-[6px] font-extrabold text-emerald-100 bg-emerald-800/80 px-1 py-0.5 rounded uppercase">XLSX</span>
</div>
<div className="flex-1 flex flex-col min-h-0 bg-white font-mono text-[6px]">
<div className="h-4 bg-slate-100 border-b border-slate-200 flex shrink-0">
<div className="w-6 border-r border-slate-200 bg-slate-150 flex items-center justify-center text-slate-500 font-bold shrink-0"></div>
<div className="flex-1 border-r border-slate-200 flex items-center justify-center text-slate-500 font-bold">A</div>
<div className="flex-1 border-r border-slate-200 flex items-center justify-center text-slate-500 font-bold">B</div>
<div className="flex-1 border-r border-slate-200 flex items-center justify-center text-slate-500 font-bold">C</div>
<div className="flex-1 flex items-center justify-center text-slate-500 font-bold">D</div>
</div>
<div className="flex-1 flex flex-col divide-y divide-slate-100">
<div className="h-4 flex items-center bg-emerald-500/5 divide-x divide-slate-100">
<div className="w-6 bg-slate-100 flex items-center justify-center text-slate-500 font-bold shrink-0">1</div>
<div className="flex-1 px-1 font-bold text-emerald-900 truncate">
{asset.title}
</div>
</div>
<div className="h-4 flex items-center bg-slate-55 divide-x divide-slate-100">
<div className="w-6 bg-slate-100 flex items-center justify-center text-slate-500 font-bold shrink-0">2</div>
<div className="flex-1 px-1 font-bold text-slate-700 truncate">Category</div>
<div className="flex-1 px-1 font-bold text-slate-700 truncate">Date</div>
<div className="flex-1 px-1 font-bold text-slate-700 truncate">Size</div>
<div className="flex-1 px-1 font-bold text-slate-700 truncate">Status</div>
</div>
</div>
</div>
<div className="h-4 bg-slate-50 border-t border-slate-200 flex items-center px-2 shrink-0 select-none justify-between">
<div className="flex gap-1">
<span className="text-[6px] font-extrabold text-emerald-700 bg-white border-x border-t border-slate-200 px-1.5 py-0.5 rounded-t select-none leading-none">Sheet1</span>
<span className="text-[6px] text-slate-400 px-1 py-0.5 select-none leading-none">Sheet2</span>
</div>
<span className="text-[5px] text-slate-350 font-bold">READY</span>
</div>
</div>
}
/>
) : asset.type === 'url' ? (
<div className="w-full h-full flex flex-col bg-slate-900 border border-slate-800 relative overflow-hidden group/browser">
{/* Browser Header Bar */}

View File

@ -0,0 +1,68 @@
import React, { useEffect, useRef, useState } from 'react';
import { axiosInstance } from '../../../services/axios';
interface DocxThumbnailProps {
url: string;
fallback: React.ReactNode;
}
export const DocxThumbnail: React.FC<DocxThumbnailProps> = ({ url, fallback }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
let isMounted = true;
axiosInstance.get(url, { responseType: 'arraybuffer' })
.then(async (res) => {
if (!isMounted) return;
try {
const { renderAsync } = await import('docx-preview');
if (containerRef.current) {
containerRef.current.innerHTML = '';
await renderAsync(res.data, containerRef.current);
setLoading(false);
}
} catch (err) {
console.error('[DocxThumbnail] Render error:', err);
setError(true);
setLoading(false);
}
})
.catch(err => {
console.error('[DocxThumbnail] Fetch error:', err);
if (isMounted) {
setError(true);
setLoading(false);
}
});
return () => {
isMounted = false;
};
}, [url]);
if (error) return <>{fallback}</>;
return (
<div className="w-full h-full relative overflow-hidden bg-white select-none pointer-events-none">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-slate-50 z-10">
<div className="w-5 h-5 rounded-full border-2 border-indigo-600 border-t-transparent animate-spin" />
</div>
)}
<div
ref={containerRef}
className="absolute top-0 left-0 origin-top-left"
style={{
width: '760px',
height: '980px',
transform: 'scale(0.18)',
padding: '16px',
overflow: 'hidden',
}}
/>
</div>
);
};

View File

@ -0,0 +1,92 @@
import React, { useEffect, useState } from 'react';
import { axiosInstance } from '../../../services/axios';
import * as XLSX from 'xlsx';
interface SpreadsheetThumbnailProps {
url: string;
fallback: React.ReactNode;
}
export const SpreadsheetThumbnail: React.FC<SpreadsheetThumbnailProps> = ({ url, fallback }) => {
const [html, setHtml] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
let isMounted = true;
axiosInstance.get(url, { responseType: 'arraybuffer' })
.then(res => {
if (!isMounted) return;
try {
const workbook = XLSX.read(res.data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const sheetHtml = XLSX.utils.sheet_to_html(worksheet, { header: '', footer: '' });
if (isMounted) {
setHtml(sheetHtml);
setLoading(false);
}
} catch (err) {
console.error('[SpreadsheetThumbnail] Render error:', err);
if (isMounted) {
setError(true);
setLoading(false);
}
}
})
.catch(err => {
console.error('[SpreadsheetThumbnail] Fetch error:', err);
if (isMounted) {
setError(true);
setLoading(false);
}
});
return () => {
isMounted = false;
};
}, [url]);
if (error) return <>{fallback}</>;
return (
<div className="w-full h-full relative overflow-hidden bg-white select-none pointer-events-none p-1">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-slate-50 z-10">
<div className="w-5 h-5 rounded-full border-2 border-emerald-600 border-t-transparent animate-spin" />
</div>
)}
<div
className="excel-thumbnail-container absolute top-0 left-0 origin-top-left"
style={{
width: '780px',
height: '500px',
transform: 'scale(0.22)',
overflow: 'hidden',
}}
>
<style>{`
.excel-thumbnail-container table {
border-collapse: collapse;
width: 100%;
font-size: 10px;
font-family: monospace;
}
.excel-thumbnail-container td {
border: 1px solid #cbd5e1;
padding: 4px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.excel-thumbnail-container tr:nth-child(even) {
background-color: #f8fafc;
}
`}</style>
<div dangerouslySetInnerHTML={{ __html: html }} />
</div>
</div>
);
};