Final_changes
This commit is contained in:
parent
7bd5f9f0e4
commit
8bb20a99aa
@ -17,6 +17,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { Asset } from '../../../types/assets';
|
||||
import type { User } from '../../../types/auth';
|
||||
import { axiosInstance } from '../../../services/axios';
|
||||
|
||||
interface AssetCardProps {
|
||||
asset: Asset;
|
||||
@ -69,102 +70,229 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
||||
return File;
|
||||
};
|
||||
|
||||
const isRenderable = (type: string) => {
|
||||
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg');
|
||||
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');
|
||||
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg') || isOffice;
|
||||
};
|
||||
|
||||
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 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');
|
||||
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');
|
||||
|
||||
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">
|
||||
{/* 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 text-ink-900 focus:ring-ink-900/10 cursor-pointer mr-1"
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
<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) && (
|
||||
|
||||
<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-lg text-ink-500 hover:text-ink-900 hover:bg-ink-100 transition-colors"
|
||||
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-4 h-4" />
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
</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 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 */}
|
||||
{isImage ? (
|
||||
<img
|
||||
src={getFullAssetUrl(asset.url)}
|
||||
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';
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Fallbacks / simulated thumbnails */}
|
||||
<div
|
||||
id={`fallback-${asset.id}`}
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
style={{ display: isImage ? '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>
|
||||
<div className="space-y-1 mt-4">
|
||||
<div className="h-1 bg-ink-200 rounded w-5/6" />
|
||||
<div className="h-1 bg-ink-200 rounded w-full" />
|
||||
<div className="h-1 bg-ink-200 rounded w-4/5" />
|
||||
</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>
|
||||
</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="h-1 bg-ink-0/60 rounded w-full" />
|
||||
<div className="h-1 bg-ink-0/60 rounded w-5/6" />
|
||||
</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>
|
||||
</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>
|
||||
<div className="space-y-1 mt-4">
|
||||
<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-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>
|
||||
</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>
|
||||
<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>
|
||||
</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 === 'url' ? 'LINK' : 'FILE'}
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
@ -241,3 +369,4 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -26,14 +26,30 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
const [isLoadingText, setIsLoadingText] = useState(false);
|
||||
const [textPreviewContent, setTextPreviewContent] = useState('');
|
||||
|
||||
const isLocalUrl = (url: string) => {
|
||||
return url.includes('localhost') || url.includes('127.0.0.1');
|
||||
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}`;
|
||||
}
|
||||
|
||||
// If current origin is a public domain (like ngrok) but the resolved URL is local,
|
||||
// rewrite the local URL host to match the public origin so that external viewers can fetch it.
|
||||
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 getFullAssetUrl = (url: string) => {
|
||||
return url.startsWith('http')
|
||||
? url
|
||||
: `${axiosInstance.defaults.baseURL?.replace('/api/v1', '')}${url}`;
|
||||
const isLocalUrl = (url: string) => {
|
||||
const resolved = getFullAssetUrl(url);
|
||||
return resolved.includes('localhost') || resolved.includes('127.0.0.1');
|
||||
};
|
||||
|
||||
const getGithubRawUrl = (url: string) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user