Refactor: Add Open in New Tab for PDFs/Docs, upgrade details modal to 2-column lg, and enhance document cover thumbnails

This commit is contained in:
kenilkb 2026-07-15 18:23:19 +05:30
parent e7f50a6667
commit ea7d688f7c
3 changed files with 178 additions and 145 deletions

View File

@ -1,9 +1,7 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Globe,
FileText,
Image as ImageIcon,
File,
Eye,
MoreVertical,
@ -14,8 +12,7 @@ import {
ExternalLink,
Download,
Clock,
AlertCircle,
BookOpen
AlertCircle
} from 'lucide-react';
import type { Asset } from '../../../types/assets';
import type { User } from '../../../types/auth';
@ -69,13 +66,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
const getAssetIcon = (type: string) => {
if (type === 'case_study') return BookOpen;
if (type === 'url') return Globe;
if (type.includes('pdf')) return FileText;
if (type.includes('image') || type.includes('png') || type.includes('jpg')) return ImageIcon;
return File;
};
const isRenderable = (type: string, url: string) => {
const isOffice = type.includes('word') || type.includes('presentation') || type.includes('sheet') ||
@ -104,7 +95,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
return resolvedUrl;
};
const Icon = getAssetIcon(asset.type);
const isImage = asset.type.includes('image') || asset.type.includes('png') || asset.type.includes('jpg') || asset.url.match(/\.(png|jpe?g|gif|svg|webp)$/i);
const isPdf = asset.type.includes('pdf') || asset.url.toLowerCase().endsWith('.pdf');
@ -112,8 +103,8 @@ export const AssetCard: React.FC<AssetCardProps> = ({
const isPresentation = asset.type.includes('presentation') || asset.url.toLowerCase().endsWith('.pptx') || asset.url.toLowerCase().endsWith('.ppt');
const isSpreadsheet = asset.type.includes('sheet') || asset.url.toLowerCase().endsWith('.xlsx') || asset.url.toLowerCase().endsWith('.xls') || asset.url.toLowerCase().endsWith('.csv');
const hasBanner = isImage || (asset.type === 'case_study' && !!asset.thumbnailUrl);
const bannerSrc = (asset.type === 'case_study' && asset.thumbnailUrl) ? asset.thumbnailUrl : asset.url;
const hasBanner = isImage || !!asset.thumbnailUrl;
const bannerSrc = asset.thumbnailUrl ? asset.thumbnailUrl : asset.url;
return (
<motion.div
@ -234,83 +225,95 @@ export const AssetCard: React.FC<AssetCardProps> = ({
/>
) : null}
{/* Fallbacks / simulated thumbnails */}
<div
id={`fallback-${asset.id}`}
className="w-full h-full flex items-center justify-center"
className="w-full h-full flex items-center justify-center bg-ink-50"
style={{ display: hasBanner ? 'none' : 'flex' }}
>
{isPdf ? (
<div className="flex flex-col items-center justify-center w-full h-full p-4 relative">
<div className="w-14 h-18 bg-ink-0 border border-ink-200 shadow-sm rounded flex flex-col justify-between p-1.5 relative overflow-hidden">
<div className="absolute top-0 right-0 left-0 bg-red-600 text-ink-0 py-0.5 text-[8px] font-extrabold uppercase text-center tracking-wider">
PDF
<div className="flex flex-col items-center justify-center w-full h-full p-4 relative bg-red-500/[0.03] hover:bg-red-500/[0.06] transition-colors">
<div className="w-[105px] h-[135px] bg-ink-0 border border-red-500/20 shadow-md rounded-lg flex flex-col justify-between p-3 relative overflow-hidden transition-all duration-300 group-hover:scale-105">
<div className="absolute top-0 right-0 left-0 bg-red-600 text-ink-0 py-1 text-[8px] font-extrabold uppercase text-center tracking-wider font-sans">
PDF Document
</div>
<div className="space-y-1 mt-4">
<div className="h-1 bg-ink-200 rounded w-5/6" />
<div className="space-y-1.5 mt-6 flex-grow pt-1">
<div className="h-1 bg-ink-200 rounded w-11/12" />
<div className="h-1 bg-ink-200 rounded w-full" />
<div className="h-1 bg-ink-200 rounded w-4/5" />
<div className="h-1 bg-ink-250 rounded w-10/12" />
<div className="h-1 bg-ink-200 rounded w-full" />
<div className="h-1 bg-ink-200 rounded w-3/4" />
</div>
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-0.5 mt-1">
<span>PDF</span>
<FileText className="w-2.5 h-2.5 text-red-500" />
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-1 mt-1 font-sans">
<span>PDF RESOURCE</span>
<FileText className="w-3 h-3 text-red-650" />
</div>
</div>
</div>
) : isPresentation ? (
<div className="flex flex-col items-center justify-center w-full h-full p-4">
<div className="w-18 h-13 bg-amber-500 border border-amber-600 shadow-sm rounded-lg flex flex-col justify-between p-1.5 relative overflow-hidden">
<div className="h-1.5 bg-ink-0/90 rounded w-3/4 mb-1" />
<div className="space-y-1">
<div className="flex flex-col items-center justify-center w-full h-full p-4 bg-amber-500/[0.03] hover:bg-amber-500/[0.06] transition-colors">
<div className="w-[125px] h-[90px] bg-amber-500 border border-amber-600 shadow-md rounded-lg flex flex-col justify-between p-2.5 relative overflow-hidden transition-all duration-300 group-hover:scale-105">
<div className="h-2 bg-ink-0/90 rounded w-3/4 mb-1" />
<div className="space-y-1 flex-grow">
<div className="h-1 bg-ink-0/60 rounded w-full" />
<div className="h-1 bg-ink-0/60 rounded w-5/6" />
<div className="h-1 bg-ink-0/60 rounded w-11/12" />
<div className="h-1 bg-ink-0/60 rounded w-10/12" />
</div>
<div className="flex items-center justify-between text-[7px] text-ink-0/80 font-bold pt-0.5">
<span>SLIDE</span>
<span className="font-extrabold">PPTX</span>
<div className="flex items-center justify-between text-[7px] text-ink-0/80 font-bold pt-1 border-t border-amber-400">
<span>PRESENTATION</span>
<span className="font-extrabold uppercase">PPTX</span>
</div>
</div>
</div>
) : isWord ? (
<div className="flex flex-col items-center justify-center w-full h-full p-4">
<div className="w-14 h-18 bg-ink-0 border border-ink-200 shadow-sm rounded flex flex-col justify-between p-1.5 relative overflow-hidden">
<div className="absolute top-0 right-0 left-0 bg-blue-600 text-ink-0 py-0.5 text-[8px] font-extrabold uppercase text-center tracking-wider">
DOC
<div className="flex flex-col items-center justify-center w-full h-full p-4 bg-blue-500/[0.03] hover:bg-blue-500/[0.06] transition-colors">
<div className="w-[105px] h-[135px] bg-ink-0 border border-blue-500/20 shadow-md rounded-lg flex flex-col justify-between p-3 relative overflow-hidden transition-all duration-300 group-hover:scale-105">
<div className="absolute top-0 right-0 left-0 bg-blue-600 text-ink-0 py-1 text-[8px] font-extrabold uppercase text-center tracking-wider font-sans">
Word Doc
</div>
<div className="space-y-1 mt-4">
<div className="space-y-1.5 mt-6 flex-grow pt-1">
<div className="h-1 bg-ink-200 rounded w-11/12" />
<div className="h-1 bg-ink-200 rounded w-full" />
<div className="h-1 bg-ink-200 rounded w-5/6" />
<div className="h-1 bg-ink-250 rounded w-10/12" />
<div className="h-1 bg-ink-200 rounded w-full" />
</div>
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-0.5 mt-1">
<span>WORD</span>
<FileText className="w-2.5 h-2.5 text-blue-500" />
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-1 mt-1 font-sans">
<span>DOCX RESOURCE</span>
<FileText className="w-3 h-3 text-blue-650" />
</div>
</div>
</div>
) : isSpreadsheet ? (
<div className="flex flex-col items-center justify-center w-full h-full p-4">
<div className="w-16 h-13 bg-emerald-600 border border-emerald-700 shadow-sm rounded-lg flex flex-col justify-between p-1.5 relative overflow-hidden">
<div className="grid grid-cols-3 gap-0.5 mt-0.5">
<div className="h-1.5 bg-ink-0/90 rounded-sm" />
<div className="h-1.5 bg-ink-0/70 rounded-sm" />
<div className="h-1.5 bg-ink-0/70 rounded-sm" />
<div className="h-1.5 bg-ink-0/60 rounded-sm" />
<div className="h-1.5 bg-ink-0/80 rounded-sm" />
<div className="h-1.5 bg-ink-0/60 rounded-sm" />
<div className="flex flex-col items-center justify-center w-full h-full p-4 bg-emerald-500/[0.03] hover:bg-emerald-500/[0.06] transition-colors">
<div className="w-[115px] h-[90px] bg-ink-0 border border-emerald-500/20 shadow-md rounded-lg flex flex-col justify-between p-2 relative overflow-hidden transition-all duration-300 group-hover:scale-105">
<div className="absolute top-0 right-0 left-0 bg-emerald-650 text-ink-0 py-1 text-[7px] font-extrabold uppercase text-center tracking-wider font-sans">
Spreadsheet
</div>
<div className="flex items-center justify-between text-[7px] text-emerald-100 font-bold pt-0.5">
<span>SHEET</span>
<span className="font-extrabold">XLSX</span>
<div className="grid grid-cols-3 gap-1 mt-5 flex-grow pt-1">
<div className="h-2 bg-emerald-50 border border-emerald-100 rounded" />
<div className="h-2 bg-emerald-50 border border-emerald-100 rounded" />
<div className="h-2 bg-emerald-50 border border-emerald-100 rounded" />
<div className="h-2 bg-ink-50 border border-ink-100 rounded" />
<div className="h-2 bg-ink-50 border border-ink-100 rounded" />
<div className="h-2 bg-ink-50 border border-ink-100 rounded" />
</div>
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-1 mt-1 font-sans">
<span>XLSX SHEET</span>
<span className="font-extrabold text-emerald-650 text-[6px]">EXCEL</span>
</div>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center w-full h-full p-4">
<div className="w-12 h-12 rounded-xl bg-ink-100 border border-ink-200 flex items-center justify-center shadow-inner relative group-hover:scale-105 transition-transform">
<Icon className="w-6 h-6 text-ink-650" />
<div className="absolute -bottom-1 -right-1 bg-ink-800 border border-ink-700 rounded px-1 py-0.5 text-[7px] font-bold text-ink-0 uppercase">
{asset.type === 'case_study' ? 'CASE STUDY' : asset.type === 'url' ? 'LINK' : 'FILE'}
<div className="flex flex-col items-center justify-center w-full h-full p-4 bg-ink-500/[0.03] hover:bg-ink-500/[0.06] transition-colors">
<div className="w-[105px] h-[135px] bg-ink-0 border border-ink-200 shadow-md rounded-lg flex flex-col justify-between p-3 relative overflow-hidden transition-all duration-300 group-hover:scale-105">
<div className="absolute top-0 right-0 left-0 bg-ink-700 text-ink-0 py-1 text-[8px] font-extrabold uppercase text-center tracking-wider font-sans">
Resource File
</div>
<div className="flex-grow flex items-center justify-center pt-4">
<File className="w-8 h-8 text-ink-400" />
</div>
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-1 mt-1 font-sans">
<span>BINARY</span>
<span className="font-extrabold uppercase text-[6px]">{asset.type.split('/').pop() || 'FILE'}</span>
</div>
</div>
</div>

View File

@ -30,7 +30,7 @@ export const AssetDetailsModal: React.FC<AssetDetailsModalProps> = ({
isOpen={isOpen && !!asset}
onClose={onClose}
title="Asset Details"
size="md"
size="2xl"
footer={
<Button
onClick={onClose}
@ -42,101 +42,107 @@ export const AssetDetailsModal: React.FC<AssetDetailsModalProps> = ({
}
>
{asset && (
<div className="space-y-4 text-xs font-medium text-ink-900">
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Title</h4>
<p className="text-sm font-extrabold text-ink-900 mt-1 font-sans">{asset.title}</p>
</div>
{asset.description && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 text-xs font-medium text-ink-900">
{/* Left Column */}
<div className="space-y-4">
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Description</h4>
<p className="text-ink-700 mt-1 leading-relaxed font-sans">{asset.description}</p>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Title</h4>
<p className="text-sm font-extrabold text-ink-900 mt-1 font-sans">{asset.title}</p>
</div>
)}
{asset.type === 'case_study' && asset.thumbnailUrl && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans mb-1.5">Banner Image</h4>
<div className="w-full h-40 rounded-lg overflow-hidden border border-ink-200 shadow-sm bg-ink-50">
<img
src={asset.thumbnailUrl}
alt="Case Study Banner"
className="w-full h-full object-cover"
/>
{asset.description && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Description</h4>
<p className="text-ink-700 mt-1 leading-relaxed font-sans">{asset.description}</p>
</div>
)}
{(asset.type === 'case_study' || asset.thumbnailUrl) && asset.thumbnailUrl && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans mb-1.5">Banner Image</h4>
<div className="w-full h-44 rounded-lg overflow-hidden border border-ink-200 shadow-sm bg-ink-50">
<img
src={asset.thumbnailUrl}
alt="Asset Banner"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Category</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.categoryId || 'General'}</p>
</div>
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Subcategory</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.subcategory || '-'}</p>
</div>
</div>
)}
{asset.type === 'case_study' && asset.problemStatement && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-red-600 font-sans mb-1">Problem / Challenge</h4>
<p className="text-ink-700 leading-relaxed font-sans whitespace-pre-wrap text-xs bg-red-500/[0.02] border border-red-500/10 p-3 rounded-lg shadow-sm">
{asset.problemStatement}
</p>
</div>
)}
{asset.type === 'case_study' && asset.solution && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-emerald-650 font-sans mb-1">Suggested Solution</h4>
<p className="text-ink-700 leading-relaxed font-sans whitespace-pre-wrap text-xs bg-emerald-500/[0.02] border border-emerald-500/10 p-3 rounded-lg shadow-sm">
{asset.solution}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Category</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.categoryId || 'General'}</p>
</div>
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Subcategory</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.subcategory || '-'}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">File Size</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.type === 'url' ? 'N/A' : formatBytes(asset.size)}</p>
</div>
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">File Type</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.type}</p>
</div>
</div>
{asset.tags.length > 0 && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Tags</h4>
<div className="flex flex-wrap gap-1.5 mt-1.5">
{asset.tags.map(tag => (
<span key={tag} className="px-2 py-0.5 rounded bg-ink-50 border border-ink-200 text-[10px] font-bold text-ink-600 font-sans">
{tag}
</span>
))}
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">File Size</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.type === 'url' ? 'N/A' : formatBytes(asset.size)}</p>
</div>
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">File Type</h4>
<p className="text-ink-900 mt-1 font-bold font-sans">{asset.type}</p>
</div>
</div>
)}
</div>
{userRole === 'ADMIN' && asset.sharedWith && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Shared With</h4>
<div className="flex flex-wrap gap-1.5 mt-1.5">
{asset.sharedWith.length === 0 ? (
<span className="text-ink-500 font-semibold italic font-sans">Not shared with any organization</span>
) : (
asset.sharedWith.map(sw => (
<span key={sw.userId ? `${sw.organizationId}-${sw.userId}` : sw.organizationId} className="px-2 py-0.5 rounded bg-ink-900 text-ink-0 text-[10px] font-bold font-sans">
{sw.organization?.name || 'Unknown Organization'} {sw.user ? `(${sw.user.email})` : '(Entire Org)'}
{/* Right Column */}
<div className="space-y-4">
{asset.type === 'case_study' && asset.problemStatement && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-red-650 font-sans mb-1.5">Problem / Challenge</h4>
<p className="text-ink-700 leading-relaxed font-sans whitespace-pre-wrap text-xs bg-red-500/[0.02] border border-red-500/10 p-3 rounded-lg shadow-sm">
{asset.problemStatement}
</p>
</div>
)}
{asset.type === 'case_study' && asset.solution && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-emerald-650 font-sans mb-1.5">Suggested Solution</h4>
<p className="text-ink-700 leading-relaxed font-sans whitespace-pre-wrap text-xs bg-emerald-500/[0.02] border border-emerald-500/10 p-3 rounded-lg shadow-sm">
{asset.solution}
</p>
</div>
)}
{asset.tags.length > 0 && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Tags</h4>
<div className="flex flex-wrap gap-1.5 mt-1.5">
{asset.tags.map(tag => (
<span key={tag} className="px-2 py-0.5 rounded bg-ink-50 border border-ink-200 text-[10px] font-bold text-ink-600 font-sans">
{tag}
</span>
))
)}
))}
</div>
</div>
</div>
)}
)}
{userRole === 'ADMIN' && asset.sharedWith && (
<div>
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Shared With</h4>
<div className="flex flex-wrap gap-1.5 mt-1.5">
{asset.sharedWith.length === 0 ? (
<span className="text-ink-500 font-semibold italic font-sans">Not shared with any organization</span>
) : (
asset.sharedWith.map(sw => (
<span key={sw.userId ? `${sw.organizationId}-${sw.userId}` : sw.organizationId} className="px-2 py-0.5 rounded bg-ink-900 text-ink-0 text-[10px] font-bold font-sans">
{sw.organization?.name || 'Unknown Organization'} {sw.user ? `(${sw.user.email})` : '(Entire Org)'}
</span>
))
)}
</div>
</div>
)}
</div>
</div>
)}
</Modal>

View File

@ -229,6 +229,30 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
Close Preview
</Button>
{asset && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && (
<Button
onClick={() => {
const url = getFullAssetUrl(asset.url);
const isOffice = 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');
if (isOffice && !isLocalUrl(asset.url)) {
window.open(`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`, '_blank');
} else {
window.open(url, '_blank');
}
}}
variant="secondary"
size="sm"
className="flex items-center gap-2"
>
<ExternalLink className="w-4 h-4" />
<span>Open in New Tab</span>
</Button>
)}
{asset && asset.type !== 'url' && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && (
<Button
onClick={() => onDownload(asset)}