Feat: Add client-side dynamic thumbnail generators for DOCX and Spreadsheet assets
This commit is contained in:
parent
63b3835ecf
commit
6d694efeb0
@ -19,6 +19,8 @@ import type { Asset } from '../../../types/assets';
|
|||||||
import type { User } from '../../../types/auth';
|
import type { User } from '../../../types/auth';
|
||||||
import { axiosInstance } from '../../../services/axios';
|
import { axiosInstance } from '../../../services/axios';
|
||||||
import { PdfThumbnail } from './PdfThumbnail';
|
import { PdfThumbnail } from './PdfThumbnail';
|
||||||
|
import { DocxThumbnail } from './DocxThumbnail';
|
||||||
|
import { SpreadsheetThumbnail } from './SpreadsheetThumbnail';
|
||||||
|
|
||||||
const getFriendlyHostname = (urlStr: string) => {
|
const getFriendlyHostname = (urlStr: string) => {
|
||||||
try {
|
try {
|
||||||
@ -259,6 +261,76 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
</div>
|
</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' ? (
|
) : asset.type === 'url' ? (
|
||||||
<div className="w-full h-full flex flex-col bg-slate-900 border border-slate-800 relative overflow-hidden group/browser">
|
<div className="w-full h-full flex flex-col bg-slate-900 border border-slate-800 relative overflow-hidden group/browser">
|
||||||
{/* Browser Header Bar */}
|
{/* Browser Header Bar */}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user