244 lines
9.7 KiB
TypeScript
244 lines
9.7 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Globe,
|
|
FileText,
|
|
Image as ImageIcon,
|
|
File,
|
|
Eye,
|
|
MoreVertical,
|
|
Edit3,
|
|
Share2,
|
|
Trash2,
|
|
Lock,
|
|
ExternalLink,
|
|
Download,
|
|
Clock,
|
|
AlertCircle
|
|
} from 'lucide-react';
|
|
import type { Asset } from '../../../types/assets';
|
|
import type { User } from '../../../types/auth';
|
|
|
|
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;
|
|
}
|
|
|
|
export const AssetCard: React.FC<AssetCardProps> = ({
|
|
asset,
|
|
user,
|
|
activeMenuId,
|
|
setActiveMenuId,
|
|
onViewDetails,
|
|
onEdit,
|
|
onShare,
|
|
onDelete,
|
|
onOpenViewer,
|
|
onDownload,
|
|
onRequestDownload,
|
|
isSelected = false,
|
|
onToggleSelect
|
|
}) => {
|
|
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 getAssetIcon = (type: string) => {
|
|
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) => {
|
|
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg');
|
|
};
|
|
|
|
const Icon = getAssetIcon(asset.type);
|
|
|
|
return (
|
|
<div className={`group relative bg-ink-0 border rounded-xl p-4 transition-all duration-300 shadow-sm hover:shadow-md hover:-translate-y-0.5 flex flex-col justify-between ${isSelected ? 'border-ink-900 ring-1 ring-ink-900 bg-ink-50/30' : 'border-ink-200 hover:border-ink-300'}`}>
|
|
<div>
|
|
<div className="flex justify-between items-start mb-3 relative">
|
|
<div className="flex items-center gap-2">
|
|
{user?.role === 'ADMIN' && onToggleSelect && (
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={() => onToggleSelect(asset.id)}
|
|
className="w-3.5 h-3.5 rounded border-ink-300 text-ink-900 focus:ring-ink-900/10 cursor-pointer mr-1"
|
|
/>
|
|
)}
|
|
<div className="w-10 h-10 rounded-lg flex items-center justify-center text-ink-900 bg-ink-100 border border-ink-200">
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex items-center gap-1.5">
|
|
{isRenderable(asset.type) && (
|
|
<button
|
|
onClick={() => onOpenViewer(asset)}
|
|
className="p-1 rounded-lg text-ink-500 hover:text-ink-900 hover:bg-ink-100 transition-colors"
|
|
title="Preview Online"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => setActiveMenuId(isMenuOpen ? null : asset.id)}
|
|
className="p-1 rounded-lg text-ink-400 hover:text-ink-900 hover:bg-ink-100 transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
|
|
>
|
|
<MoreVertical className="w-4 h-4" />
|
|
</button>
|
|
|
|
{isMenuOpen && (
|
|
<>
|
|
<div className="fixed inset-0 z-10" onClick={() => setActiveMenuId(null)} />
|
|
<div className="absolute right-0 mt-8 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>
|
|
|
|
<div className="space-y-1 mb-4">
|
|
<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 leading-snug line-clamp-2 group-hover:text-ink-950 transition-colors" title={asset.title}>
|
|
{asset.title}
|
|
</h3>
|
|
<p className="text-[11px] font-medium text-ink-400 mt-1">
|
|
{asset.type === 'url' ? 'External Link' : formatBytes(asset.size)} • {new Date(asset.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</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' ? 'Type' : 'Downloads'}
|
|
</span>
|
|
<span className="text-xs font-extrabold text-ink-900 mt-0.5">
|
|
{asset.type === 'url' ? 'URL Link' : asset.downloadsCount}
|
|
</span>
|
|
</div>
|
|
|
|
{asset.type === 'url' ? (
|
|
<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="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>
|
|
</div>
|
|
);
|
|
};
|