706 lines
39 KiB
TypeScript
706 lines
39 KiB
TypeScript
import React from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
FileText,
|
|
File,
|
|
Eye,
|
|
MoreVertical,
|
|
Edit3,
|
|
Share2,
|
|
Trash2,
|
|
Lock,
|
|
ExternalLink,
|
|
Download,
|
|
Clock,
|
|
AlertCircle,
|
|
Globe,
|
|
Sparkles
|
|
} from 'lucide-react';
|
|
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 {
|
|
const urlObj = new URL(urlStr);
|
|
return urlObj.hostname.replace('www.', '');
|
|
} catch {
|
|
return 'website.com';
|
|
}
|
|
};
|
|
|
|
interface AssetCardProps {
|
|
asset: Asset;
|
|
user: User | null;
|
|
activeMenuId: string | null;
|
|
setActiveMenuId: (id: string | null) => void;
|
|
onViewDetails: (asset: Asset) => void;
|
|
onEdit: (asset: Asset) => void;
|
|
onShare: (asset: Asset) => void;
|
|
onDelete: (assetId: string) => void;
|
|
onOpenViewer: (asset: Asset) => void;
|
|
onDownload: (asset: Asset) => void;
|
|
onRequestDownload: (asset: Asset) => void;
|
|
isSelected?: boolean;
|
|
onToggleSelect?: (assetId: string) => void;
|
|
isExpanded?: boolean;
|
|
onToggleExpand?: () => void;
|
|
isRecommended?: boolean;
|
|
}
|
|
|
|
export const AssetCard: React.FC<AssetCardProps> = ({
|
|
asset,
|
|
user,
|
|
activeMenuId,
|
|
setActiveMenuId,
|
|
onViewDetails,
|
|
onEdit,
|
|
onShare,
|
|
onDelete,
|
|
onOpenViewer,
|
|
onDownload,
|
|
onRequestDownload,
|
|
isSelected = false,
|
|
onToggleSelect,
|
|
isExpanded = false,
|
|
onToggleExpand,
|
|
isRecommended = false
|
|
}) => {
|
|
const isMenuOpen = activeMenuId === asset.id;
|
|
const canDirectDownload = user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED';
|
|
const requestStatus = asset.downloadRequests?.[0]?.status;
|
|
|
|
const formatBytes = (bytes: number, decimals = 2) => {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
};
|
|
|
|
|
|
|
|
const isRenderable = (type: string, url: string) => {
|
|
const isOffice = type.includes('word') || type.includes('presentation') || type.includes('sheet') ||
|
|
url.toLowerCase().endsWith('.docx') || url.toLowerCase().endsWith('.doc') ||
|
|
url.toLowerCase().endsWith('.pptx') || url.toLowerCase().endsWith('.ppt') ||
|
|
url.toLowerCase().endsWith('.xlsx') || url.toLowerCase().endsWith('.xls') ||
|
|
url.toLowerCase().endsWith('.csv') || type.includes('csv');
|
|
|
|
const isTextOrCode = url.toLowerCase().endsWith('.md') ||
|
|
url.toLowerCase().endsWith('.txt') ||
|
|
url.toLowerCase().endsWith('.json') ||
|
|
url.toLowerCase().endsWith('.js') ||
|
|
url.toLowerCase().endsWith('.ts') ||
|
|
url.toLowerCase().endsWith('.tsx') ||
|
|
url.toLowerCase().endsWith('.jsx') ||
|
|
url.toLowerCase().endsWith('.py') ||
|
|
url.toLowerCase().endsWith('.yaml') ||
|
|
url.toLowerCase().endsWith('.yml') ||
|
|
url.toLowerCase().endsWith('.css') ||
|
|
url.toLowerCase().endsWith('.html') ||
|
|
url.toLowerCase().endsWith('.sh') ||
|
|
type.includes('text') ||
|
|
type.includes('markdown') ||
|
|
type.includes('json') ||
|
|
type.includes('javascript');
|
|
|
|
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg') || isOffice || isTextOrCode;
|
|
};
|
|
|
|
const getFullAssetUrl = (url: string) => {
|
|
let resolvedUrl = url;
|
|
if (!url.startsWith('http')) {
|
|
const backendBase = axiosInstance.defaults.baseURL || '/api/v1';
|
|
const relativeHost = backendBase.replace('/api/v1', '');
|
|
resolvedUrl = relativeHost.startsWith('/') || relativeHost === ''
|
|
? `${window.location.origin}${relativeHost}${url}`
|
|
: `${relativeHost}${url}`;
|
|
}
|
|
|
|
const currentOrigin = window.location.origin;
|
|
const isCurrentOriginLocal = currentOrigin.includes('localhost') || currentOrigin.includes('127.0.0.1');
|
|
if (!isCurrentOriginLocal && (resolvedUrl.includes('localhost') || resolvedUrl.includes('127.0.0.1'))) {
|
|
resolvedUrl = resolvedUrl.replace(/https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/, currentOrigin);
|
|
}
|
|
|
|
return resolvedUrl;
|
|
};
|
|
|
|
|
|
|
|
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');
|
|
const isWord = asset.type.includes('word') || asset.url.toLowerCase().endsWith('.docx') || asset.url.toLowerCase().endsWith('.doc');
|
|
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.thumbnailUrl;
|
|
const bannerSrc = asset.thumbnailUrl ? asset.thumbnailUrl : asset.url;
|
|
|
|
return (
|
|
<motion.div
|
|
layout
|
|
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
|
onClick={(e) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('button') || target.closest('a') || target.closest('input') || target.closest('.toggle-expand-btn')) {
|
|
return;
|
|
}
|
|
onViewDetails(asset);
|
|
}}
|
|
className={`group bg-ink-0 border rounded-xl p-4 transition-all duration-300 flex flex-col justify-between cursor-pointer min-h-[410px] ${
|
|
isExpanded
|
|
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-300 bg-ink-0'
|
|
: isRecommended
|
|
? 'relative w-full h-full border-amber-500/35 bg-gradient-to-br from-amber-500/[0.02] via-ink-0 to-ink-0 hover:border-amber-500 hover:shadow-lg hover:shadow-amber-500/10 hover:-translate-y-0.5'
|
|
: 'relative w-full h-full border-ink-200 hover:border-ink-300 hover:shadow-md hover:-translate-y-0.5'
|
|
} ${isSelected ? 'border-ink-900 ring-1 ring-ink-900 bg-ink-50/30' : ''}`}
|
|
>
|
|
<div className="flex-grow flex flex-col">
|
|
{/* Visual Thumbnail Area */}
|
|
<div className="w-full h-36 bg-ink-50 border border-ink-200 rounded-lg mb-3 flex items-center justify-center overflow-hidden relative select-none group-hover:border-ink-300 transition-colors bg-gradient-to-br from-ink-50 to-ink-100/50">
|
|
{/* Absolute overlays for controls */}
|
|
<div className="absolute top-2 left-2 z-10 flex items-center gap-1.5">
|
|
{user?.role === 'ADMIN' && onToggleSelect && (
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={() => onToggleSelect(asset.id)}
|
|
className="w-3.5 h-3.5 rounded border-ink-300 bg-ink-0 text-ink-900 focus:ring-ink-900/10 cursor-pointer shadow-sm"
|
|
/>
|
|
)}
|
|
{isRecommended && (
|
|
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-amber-500 text-ink-950 text-[9px] font-extrabold uppercase tracking-wider shadow-sm border border-amber-400">
|
|
<Sparkles className="w-2.5 h-2.5 fill-ink-950" />
|
|
<span>Recommended for You</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="absolute top-2 right-2 z-10 flex items-center gap-1">
|
|
{isRenderable(asset.type, asset.url) && (
|
|
<button
|
|
onClick={() => onOpenViewer(asset)}
|
|
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105"
|
|
title="Preview Online"
|
|
>
|
|
<Eye className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setActiveMenuId(isMenuOpen ? null : asset.id)}
|
|
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105"
|
|
>
|
|
<MoreVertical className="w-3.5 h-3.5" />
|
|
</button>
|
|
|
|
{isMenuOpen && (
|
|
<>
|
|
<div className="fixed inset-0 z-10" onClick={() => setActiveMenuId(null)} />
|
|
<div className="absolute right-0 mt-1.5 w-44 bg-ink-0 border border-ink-200 rounded-lg shadow-lg z-20 overflow-hidden py-1">
|
|
<button
|
|
onClick={() => {
|
|
onViewDetails(asset);
|
|
setActiveMenuId(null);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
|
>
|
|
<File className="w-3.5 h-3.5" />
|
|
View Details
|
|
</button>
|
|
{user?.role === 'ADMIN' && (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
onEdit(asset);
|
|
setActiveMenuId(null);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
|
>
|
|
<Edit3 className="w-3.5 h-3.5" />
|
|
Edit Asset
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
onShare(asset);
|
|
setActiveMenuId(null);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
|
>
|
|
<Share2 className="w-3.5 h-3.5" />
|
|
Share Settings
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
onDelete(asset.id);
|
|
setActiveMenuId(null);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-red-650 hover:bg-red-500/10 flex items-center gap-2"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
Delete Asset
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Thumbnail Preview Area Content */}
|
|
{hasBanner ? (
|
|
<img
|
|
src={getFullAssetUrl(bannerSrc)}
|
|
alt={asset.title}
|
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
onError={(e) => {
|
|
e.currentTarget.style.display = 'none';
|
|
const placeholder = document.getElementById(`fallback-${asset.id}`);
|
|
if (placeholder) placeholder.style.display = 'flex';
|
|
}}
|
|
/>
|
|
) : isPdf ? (
|
|
<PdfThumbnail
|
|
url={getFullAssetUrl(asset.url)}
|
|
title={asset.title}
|
|
fallback={
|
|
<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.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-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-1 mt-1 font-sans">
|
|
<span>PDF RESOURCE</span>
|
|
<FileText className="w-3 h-3 text-red-650" />
|
|
</div>
|
|
</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' ? (
|
|
<div className="w-full h-full flex flex-col bg-slate-900 border border-slate-800 relative overflow-hidden group/browser">
|
|
{/* Browser Header Bar */}
|
|
<div className="h-6 bg-slate-800/80 border-b border-slate-700/50 flex items-center px-3 gap-2 shrink-0 select-none">
|
|
{/* Windows Dots */}
|
|
<div className="flex gap-1.5 shrink-0">
|
|
<span className="w-2.5 h-2.5 rounded-full bg-red-500/80" />
|
|
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/80" />
|
|
<span className="w-2.5 h-2.5 rounded-full bg-green-500/80" />
|
|
</div>
|
|
{/* URL Bar */}
|
|
<div className="flex-1 max-w-[150px] mx-auto bg-slate-900/60 border border-slate-700/30 rounded px-2 py-0.5 flex items-center gap-1 text-[9px] text-slate-400 font-mono select-none truncate">
|
|
<Globe className="w-2.5 h-2.5 text-slate-500 shrink-0" />
|
|
<span className="truncate">{getFriendlyHostname(asset.url)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Browser Body Web Mockup */}
|
|
<div className="flex-1 bg-gradient-to-tr from-slate-950 via-slate-900 to-indigo-950 p-3 flex flex-col justify-between relative overflow-hidden">
|
|
{/* Decorative Grid Pattern */}
|
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:10px_10px]" />
|
|
|
|
{/* Mock Web Page Hero Abstract Layout */}
|
|
<div className="space-y-2 mt-1.5 relative z-10">
|
|
{/* Mock Navbar */}
|
|
<div className="flex justify-between items-center bg-white/[0.03] border border-white/5 rounded px-2 py-0.5">
|
|
<div className="w-8 h-1.5 bg-indigo-400/30 rounded" />
|
|
<div className="flex gap-1">
|
|
<div className="w-4 h-1 bg-white/20 rounded" />
|
|
<div className="w-4 h-1 bg-white/20 rounded" />
|
|
</div>
|
|
</div>
|
|
{/* Mock Page Content */}
|
|
<div className="space-y-1 pt-1">
|
|
<div className="h-2.5 bg-gradient-to-r from-indigo-400 to-cyan-400 rounded w-3/4" />
|
|
<div className="h-1.5 bg-white/20 rounded w-11/12" />
|
|
<div className="h-1.5 bg-white/10 rounded w-5/6" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mock Card Domain Display */}
|
|
<div className="relative z-10 bg-slate-900/80 border border-slate-800/80 rounded-lg p-2 flex items-center gap-2 select-none">
|
|
<div className="w-6 h-6 rounded bg-indigo-650 flex items-center justify-center text-[10px] font-black text-white shrink-0">
|
|
{getFriendlyHostname(asset.url).charAt(0).toUpperCase()}
|
|
</div>
|
|
<div className="flex-1 min-w-0 text-left">
|
|
<div className="text-[10px] font-bold text-white font-mono truncate leading-none">
|
|
{getFriendlyHostname(asset.url)}
|
|
</div>
|
|
<div className="text-[7px] text-slate-500 font-mono mt-0.5 leading-none">
|
|
EXTERNAL PORTAL
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div
|
|
id={`fallback-${asset.id}`}
|
|
className="w-full h-full flex items-center justify-center bg-ink-50 relative overflow-hidden"
|
|
>
|
|
{isPresentation ? (
|
|
<div className="w-full h-full flex bg-slate-900 border border-slate-800 text-left select-none relative overflow-hidden">
|
|
{/* Left slide thumbnails rail */}
|
|
<div className="w-10 bg-slate-950 border-r border-slate-850 flex flex-col items-center py-2 gap-1.5 shrink-0">
|
|
<div className="w-7 h-5 rounded bg-amber-600/30 border border-amber-500/50 flex items-center justify-center text-[7px] text-amber-500 font-bold">1</div>
|
|
<div className="w-7 h-5 rounded bg-slate-850 border border-slate-750 flex items-center justify-center text-[7px] text-slate-500">2</div>
|
|
<div className="w-7 h-5 rounded bg-slate-850 border border-slate-750 flex items-center justify-center text-[7px] text-slate-500">3</div>
|
|
</div>
|
|
{/* Main slide content area */}
|
|
<div className="flex-1 bg-gradient-to-tr from-slate-950 via-slate-900 to-amber-950/60 p-3.5 flex flex-col justify-between relative">
|
|
<div className="space-y-1.5 mt-2 relative z-10">
|
|
<span className="inline-flex px-1.5 py-0.5 rounded text-[7px] font-extrabold uppercase bg-amber-500 text-slate-950 tracking-wider">
|
|
Slide Show
|
|
</span>
|
|
<h4 className="text-[11px] font-extrabold text-white leading-tight font-sans line-clamp-2 mt-1">
|
|
{asset.title}
|
|
</h4>
|
|
<div className="space-y-1 pt-1.5">
|
|
<div className="flex items-center gap-1">
|
|
<span className="w-1 h-1 rounded-full bg-amber-500" />
|
|
<div className="h-1 bg-white/20 rounded w-5/6" />
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<span className="w-1 h-1 rounded-full bg-amber-500" />
|
|
<div className="h-1 bg-white/20 rounded w-3/4" />
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<span className="w-1 h-1 rounded-full bg-amber-500" />
|
|
<div className="h-1 bg-white/10 rounded w-4/6" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Slide Footer */}
|
|
<div className="flex items-center justify-between text-[7px] text-slate-405 font-bold border-t border-slate-800/80 pt-1.5 mt-auto relative z-10 font-mono">
|
|
<span>KEYNOTE PRESENTATION</span>
|
|
<span className="font-extrabold text-amber-550 text-[6px]">PPTX</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : isWord ? (
|
|
<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">
|
|
{/* Document Margins decorative elements */}
|
|
<div className="absolute top-0 right-0 left-0 h-1 bg-indigo-600" />
|
|
|
|
{/* Word Header */}
|
|
<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>
|
|
|
|
{/* Page Title & Body Text Mockup */}
|
|
<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-100 rounded w-full" />
|
|
<div className="h-1.5 bg-slate-50 rounded w-3/4" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Signature / Metadata stamp at the bottom */}
|
|
<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 ? (
|
|
<div className="w-full h-full bg-white border border-slate-200 text-left select-none relative overflow-hidden flex flex-col justify-between">
|
|
{/* Excel Top Bar */}
|
|
<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>
|
|
|
|
{/* Sheet Grid Layout */}
|
|
<div className="flex-1 flex flex-col min-h-0 bg-white font-mono text-[6px]">
|
|
{/* Headers Row */}
|
|
<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>
|
|
{/* Grid Rows */}
|
|
<div className="flex-1 flex flex-col divide-y divide-slate-100">
|
|
{/* Row 1 - Title */}
|
|
<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>
|
|
{/* Row 2 - Headers */}
|
|
<div className="h-4 flex items-center bg-slate-50 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>
|
|
{/* Row 3 - Data */}
|
|
<div className="h-4 flex items-center divide-x divide-slate-100">
|
|
<div className="w-6 bg-slate-100 flex items-center justify-center text-slate-500 font-bold shrink-0">3</div>
|
|
<div className="flex-1 px-1 text-slate-600 truncate">{asset.categoryId || 'General'}</div>
|
|
<div className="flex-1 px-1 text-slate-600 truncate">{new Date(asset.createdAt).toLocaleDateString()}</div>
|
|
<div className="flex-1 px-1 text-slate-600 truncate">{formatBytes(asset.size)}</div>
|
|
<div className="flex-1 px-1 text-emerald-650 font-bold truncate">Published</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Excel Sheet Tab Footer */}
|
|
<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>
|
|
) : (
|
|
<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>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-1 mb-4 flex-grow flex flex-col">
|
|
<div className="flex items-center gap-1.5 mb-2">
|
|
<div className="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-500 uppercase tracking-wider bg-ink-50 border border-ink-200">
|
|
{asset.categoryId || 'General'}
|
|
</div>
|
|
{!asset.isDownloadable && (
|
|
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-600 bg-ink-100 border border-ink-200">
|
|
<Lock className="w-2.5 h-2.5" />
|
|
<span>Strict View Only</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<h3 className="font-bold text-ink-900 text-sm asset-card-title leading-snug line-clamp-2 group-hover:text-ink-950 transition-colors" title={asset.title}>
|
|
{asset.title}
|
|
</h3>
|
|
{asset.description && (
|
|
<p className="text-xs text-ink-500 line-clamp-2 mt-1.5 leading-relaxed" title={asset.description}>
|
|
{asset.description}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center justify-between mt-auto pt-2.5">
|
|
<p className="text-[11px] font-medium text-ink-400">
|
|
{asset.type === 'case_study' ? 'Case Study' : asset.type === 'url' ? 'External Link' : formatBytes(asset.size)} • {new Date(asset.createdAt).toLocaleDateString()}
|
|
</p>
|
|
{asset.type === 'case_study' && (asset.problemStatement || asset.solution) && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleExpand?.();
|
|
}}
|
|
className="toggle-expand-btn text-[10px] font-bold text-ink-650 hover:text-ink-900 bg-ink-50 hover:bg-ink-100 border border-ink-200 px-2 py-0.5 rounded transition-all cursor-pointer"
|
|
>
|
|
{isExpanded ? 'Hide Summary' : 'Show Summary'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<AnimatePresence initial={false}>
|
|
{asset.type === 'case_study' && isExpanded && (asset.problemStatement || asset.solution) && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.25, ease: 'easeInOut' }}
|
|
className="mt-3.5 space-y-2.5 border-t border-ink-100 pt-3 text-[11px] overflow-hidden"
|
|
>
|
|
{asset.problemStatement && (
|
|
<div>
|
|
<span className="font-bold text-red-650 uppercase tracking-wider text-[9px] block mb-0.5">Problem / Challenge</span>
|
|
<p className="text-ink-700 leading-relaxed bg-red-500/[0.01] border border-red-500/5 p-2 rounded" title={asset.problemStatement}>
|
|
{asset.problemStatement}
|
|
</p>
|
|
</div>
|
|
)}
|
|
{asset.solution && (
|
|
<div>
|
|
<span className="font-bold text-emerald-650 uppercase tracking-wider text-[9px] block mb-0.5">Our Approach</span>
|
|
<p className="text-ink-700 leading-relaxed bg-emerald-500/[0.01] border border-emerald-500/5 p-2 rounded" title={asset.solution}>
|
|
{asset.solution}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div className="pt-2 flex justify-end">
|
|
<a
|
|
href={asset.url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex items-center gap-1.5 text-[10px] font-bold text-ink-600 hover:text-ink-900 transition-colors bg-ink-50 hover:bg-ink-100 border border-ink-200 px-2.5 py-1 rounded-md"
|
|
>
|
|
<span>Read Full Case Study</span>
|
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
</a>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-3 border-t border-ink-100 flex items-center justify-between mt-auto">
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-ink-400">
|
|
{asset.type === 'url' || asset.type === 'case_study' ? 'Type' : 'Downloads'}
|
|
</span>
|
|
<span className="text-xs font-extrabold text-ink-900 mt-0.5">
|
|
{asset.type === 'case_study' ? 'Case Study' : asset.type === 'url' ? 'URL Link' : asset.downloadsCount}
|
|
</span>
|
|
</div>
|
|
|
|
{asset.type === 'url' || asset.type === 'case_study' ? (
|
|
<a
|
|
href={asset.url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="w-7 h-7 rounded-md bg-ink-900 border border-ink-900 flex items-center justify-center text-ink-0 hover:bg-ink-800 transition-all shadow-sm"
|
|
title={asset.type === 'case_study' ? 'Open Case Study' : 'Open External URL'}
|
|
>
|
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
</a>
|
|
) : canDirectDownload ? (
|
|
<button
|
|
onClick={() => onDownload(asset)}
|
|
className="w-7 h-7 rounded-md bg-ink-50 border border-ink-200 flex items-center justify-center text-ink-600 hover:bg-ink-100 hover:border-ink-300 hover:text-ink-900 transition-all shadow-sm"
|
|
title="Download Asset"
|
|
>
|
|
<Download className="w-3.5 h-3.5" />
|
|
</button>
|
|
) : requestStatus === 'PENDING' ? (
|
|
<div className="inline-flex items-center gap-1 text-[10px] font-bold text-ink-500 bg-ink-50 border border-ink-200 px-2 py-1 rounded-md">
|
|
<Clock className="w-3 h-3 text-ink-500 animate-pulse" />
|
|
<span>Pending Access</span>
|
|
</div>
|
|
) : requestStatus === 'REJECTED' ? (
|
|
<button
|
|
onClick={() => onRequestDownload(asset)}
|
|
className="inline-flex items-center gap-1 text-[10px] font-bold text-red-650 bg-red-500/10 border border-red-500/20 px-2 py-1 rounded-md hover:bg-red-500/25 transition-all"
|
|
>
|
|
<AlertCircle className="w-3 h-3" />
|
|
<span>Rejected (Retry)</span>
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={() => onRequestDownload(asset)}
|
|
className="inline-flex items-center gap-1 text-[10px] font-bold text-ink-900 bg-ink-100 border border-ink-300 hover:bg-ink-200 px-2 py-1 rounded-md transition-all cursor-pointer"
|
|
>
|
|
<Lock className="w-3 h-3" />
|
|
<span>Request Download</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|