import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import type { Variants } from 'framer-motion'; import { UploadCloud, Search, FileText, Image as ImageIcon, File, MoreVertical, Download, CheckCircle, X, Trash2, Share2, Eye, Lock, Clock, Check, AlertCircle, ChevronDown, ChevronUp, Edit3, Globe, ExternalLink, Maximize2, Minimize2 } from 'lucide-react'; import { useAuthStore } from '../hooks/use-auth'; import { axiosInstance } from '../services/axios'; const containerVariants: Variants = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.05 } } }; const itemVariants: Variants = { hidden: { opacity: 0, y: 15, scale: 0.98 }, show: { opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 350, damping: 25 } } }; interface SharedWithOrg { organizationId: string; userId: string | null; organization: { id: string; name: string; }; user?: { id: string; email: string; } | null; } interface DownloadRequest { id: string; assetId: string; userId: string; status: string; createdAt: string; user?: { id: string; email: string; }; asset?: { title: string; }; } interface Asset { id: string; title: string; type: string; size: number; url: string; version: number; uploadedBy: string; description: string | null; categoryId: string | null; subcategory: string | null; tags: string[]; downloadsCount: number; githubUrl: string | null; status: string; isDownloadable: boolean; createdAt: string; sharedWith?: SharedWithOrg[]; downloadRequests?: DownloadRequest[]; } interface OrgUser { id: string; email: string; role: string; } interface Organization { id: string; name: string; status: string; users?: OrgUser[]; } interface ShareItem { organizationId: string; userId: string | null; } export const AssetsPage = () => { const user = useAuthStore((state) => state.user); const [assets, setAssets] = useState([]); const [organizations, setOrganizations] = useState([]); const [loading, setLoading] = useState(true); // Search & Filter const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('ALL'); // Modals & Popovers const [isUploadOpen, setIsUploadOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false); const [isShareOpen, setIsShareOpen] = useState(false); const [isDetailsOpen, setIsDetailsOpen] = useState(false); const [isViewerOpen, setIsViewerOpen] = useState(false); const [isRequestsOpen, setIsRequestsOpen] = useState(false); const [activeAsset, setActiveAsset] = useState(null); const [activeMenuId, setActiveMenuId] = useState(null); const [isMaximized, setIsMaximized] = useState(false); const [textPreviewContent, setTextPreviewContent] = useState(''); const [isLoadingText, setIsLoadingText] = useState(false); const [expandedOrgId, setExpandedOrgId] = useState(null); // Upload Form const [uploadTab, setUploadTab] = useState<'file' | 'url'>('file'); const [uploadFile, setUploadFile] = useState(null); const [uploadUrl, setUploadUrl] = useState(''); const [uploadTitle, setUploadTitle] = useState(''); const [uploadDescription, setUploadDescription] = useState(''); const [uploadCategory, setUploadCategory] = useState('Marketing'); const [uploadSubcategory, setUploadSubcategory] = useState(''); const [uploadTags, setUploadTags] = useState(''); const [uploadGithubUrl, setUploadGithubUrl] = useState(''); const [uploadIsDownloadable, setUploadIsDownloadable] = useState(true); const [isUploading, setIsUploading] = useState(false); // Edit Form const [editTitle, setEditTitle] = useState(''); const [editDescription, setEditDescription] = useState(''); const [editCategory, setEditCategory] = useState('Marketing'); const [editSubcategory, setEditSubcategory] = useState(''); const [editTags, setEditTags] = useState(''); const [editGithubUrl, setEditGithubUrl] = useState(''); const [editIsDownloadable, setEditIsDownloadable] = useState(true); const [isSavingEdit, setIsSavingEdit] = useState(false); // Share Form state const [sharesList, setSharesList] = useState([]); const [isSavingShare, setIsSavingShare] = useState(false); useEffect(() => { fetchData(); }, []); const fetchData = async () => { setLoading(true); try { const assetsRes = await axiosInstance.get('/assets'); setAssets(assetsRes.data); if (user?.role === 'ADMIN') { const orgsRes = await axiosInstance.get('/organizations'); setOrganizations(orgsRes.data); } } catch (err) { console.error('Failed to fetch assets data', err); } finally { setLoading(false); } }; const isLocalUrl = (url: string) => { return url.includes('localhost') || url.includes('127.0.0.1'); }; const getGithubRawUrl = (url: string) => { let raw = url.trim(); if (raw.endsWith('.git')) { raw = raw.slice(0, -4); } if (raw.endsWith('/')) { raw = raw.slice(0, -1); } if (raw.includes('/blob/')) { return raw.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/'); } const match = raw.match(/github\.com\/([^/]+)\/([^/]+)/); if (match && match[1] && match[2]) { const owner = match[1]; const repo = match[2]; return { owner, repo, mainUrl: `https://raw.githubusercontent.com/${owner}/${repo}/main/README.md`, masterUrl: `https://raw.githubusercontent.com/${owner}/${repo}/master/README.md`, }; } return null; }; useEffect(() => { if (isViewerOpen && activeAsset) { setTextPreviewContent(''); setIsLoadingText(false); const assetUrl = activeAsset.url.toLowerCase(); if ( activeAsset.type !== 'url' && (assetUrl.endsWith('.md') || assetUrl.endsWith('.txt') || activeAsset.type.includes('text') || activeAsset.type.includes('markdown')) ) { setIsLoadingText(true); axiosInstance.get(getFullAssetUrl(activeAsset.url), { responseType: 'text' }) .then(res => { setTextPreviewContent(res.data); setIsLoadingText(false); }) .catch(err => { console.error(err); setTextPreviewContent('Failed to load text preview.'); setIsLoadingText(false); }); } else if (activeAsset.type === 'url' && activeAsset.url.includes('github.com')) { const ghInfo = getGithubRawUrl(activeAsset.url); if (ghInfo) { setIsLoadingText(true); const targetUrl = typeof ghInfo === 'string' ? ghInfo : ghInfo.mainUrl; fetch(targetUrl) .then(async (res) => { if (!res.ok && typeof ghInfo !== 'string') { const fallbackRes = await fetch(ghInfo.masterUrl); if (fallbackRes.ok) { return fallbackRes.text(); } throw new Error('README not found'); } return res.text(); }) .then(data => { setTextPreviewContent(data); setIsLoadingText(false); }) .catch(err => { console.error(err); setTextPreviewContent(`### GitHub Repository: ${typeof ghInfo === 'string' ? activeAsset.url : `${ghInfo.owner}/${ghInfo.repo}`}\n\nCould not fetch README.md automatically. The repository might be private or not contain a README.md file.`); setIsLoadingText(false); }); } } } }, [isViewerOpen, activeAsset]); const handleUploadSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (uploadTab === 'file' && !uploadFile) return; if (uploadTab === 'url' && !uploadUrl) return; setIsUploading(true); const formData = new FormData(); if (uploadTab === 'file' && uploadFile) { formData.append('file', uploadFile); } else { formData.append('isUrlAsset', 'true'); formData.append('url', uploadUrl); } formData.append('title', uploadTitle || (uploadFile ? uploadFile.name : uploadUrl)); formData.append('description', uploadDescription); formData.append('categoryId', uploadCategory); formData.append('subcategory', uploadSubcategory); formData.append('tags', JSON.stringify(uploadTags.split(',').map(t => t.trim()).filter(Boolean))); formData.append('githubUrl', uploadGithubUrl); formData.append('isDownloadable', String(uploadIsDownloadable)); try { await axiosInstance.post('/assets/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); setUploadFile(null); setUploadUrl(''); setUploadTitle(''); setUploadDescription(''); setUploadCategory('Marketing'); setUploadSubcategory(''); setUploadTags(''); setUploadGithubUrl(''); setUploadIsDownloadable(true); setIsUploadOpen(false); await fetchData(); } catch (err) { console.error('Failed to upload asset', err); } finally { setIsUploading(false); } }; const handleEditSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!activeAsset) return; setIsSavingEdit(true); try { await axiosInstance.patch(`/assets/${activeAsset.id}`, { title: editTitle, description: editDescription, categoryId: editCategory, subcategory: editSubcategory, tags: editTags.split(',').map(t => t.trim()).filter(Boolean), githubUrl: editGithubUrl, isDownloadable: editIsDownloadable, }); setIsEditOpen(false); setActiveAsset(null); await fetchData(); } catch (err) { console.error('Failed to save asset details', err); } finally { setIsSavingEdit(false); } }; const openEditModal = (asset: Asset) => { setActiveAsset(asset); setEditTitle(asset.title); setEditDescription(asset.description || ''); setEditCategory(asset.categoryId || 'Marketing'); setEditSubcategory(asset.subcategory || ''); setEditTags(asset.tags.join(', ')); setEditGithubUrl(asset.githubUrl || ''); setEditIsDownloadable(asset.isDownloadable); setIsEditOpen(true); setActiveMenuId(null); }; const handleShareSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!activeAsset) return; setIsSavingShare(true); try { await axiosInstance.patch(`/assets/${activeAsset.id}`, { shares: sharesList }); setIsShareOpen(false); setActiveAsset(null); await fetchData(); } catch (err) { console.error('Failed to update share permissions', err); } finally { setIsSavingShare(false); } }; const openShareModal = (asset: Asset) => { setActiveAsset(asset); setSharesList( asset.sharedWith?.map(s => ({ organizationId: s.organizationId, userId: s.userId })) || [] ); setIsShareOpen(true); setActiveMenuId(null); }; const openViewerModal = (asset: Asset) => { setActiveAsset(asset); setIsViewerOpen(true); setActiveMenuId(null); }; const openDetailsModal = (asset: Asset) => { setActiveAsset(asset); setIsDetailsOpen(true); setActiveMenuId(null); }; const handleDeleteAsset = async (id: string) => { if (!window.confirm('Are you sure you want to permanently delete this asset?')) return; try { await axiosInstance.delete(`/assets/${id}`); setActiveMenuId(null); await fetchData(); } catch (err) { console.error('Failed to delete asset', err); } }; const handleRequestDownload = async (asset: Asset) => { try { await axiosInstance.post(`/assets/${asset.id}/request-download`); await fetchData(); } catch (err) { console.error('Failed to request download access', err); } }; const handleApproveRequest = async (assetId: string, requestId: string) => { try { await axiosInstance.post(`/assets/${assetId}/approve-download/${requestId}`); await fetchData(); } catch (err) { console.error('Failed to approve request', err); } }; const handleRejectRequest = async (assetId: string, requestId: string) => { try { await axiosInstance.post(`/assets/${assetId}/reject-download/${requestId}`); await fetchData(); } catch (err) { console.error('Failed to reject request', err); } }; const handleDownload = async (asset: Asset) => { try { await axiosInstance.post(`/assets/${asset.id}/download`); const downloadUrl = asset.url.startsWith('http') ? asset.url : `${axiosInstance.defaults.baseURL?.replace('/api/v1', '')}${asset.url}`; const a = document.createElement('a'); a.href = downloadUrl; a.download = asset.title; a.target = '_blank'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setAssets(prev => prev.map(item => item.id === asset.id ? { ...item, downloadsCount: item.downloadsCount + 1 } : item )); } catch (err) { console.error('Failed to process download', err); } }; 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 getFullAssetUrl = (url: string) => { return url.startsWith('http') ? url : `${axiosInstance.defaults.baseURL?.replace('/api/v1', '')}${url}`; }; const CATEGORIES = ['ALL', 'Marketing', 'Presentations', 'Branding', 'Resources', 'Technical']; // Improved search considering all attributes const filteredAssets = assets.filter(asset => { const query = searchQuery.toLowerCase().trim(); const matchesSearch = !query || asset.title.toLowerCase().includes(query) || (asset.description && asset.description.toLowerCase().includes(query)) || (asset.categoryId && asset.categoryId.toLowerCase().includes(query)) || (asset.subcategory && asset.subcategory.toLowerCase().includes(query)) || (asset.githubUrl && asset.githubUrl.toLowerCase().includes(query)) || asset.type.toLowerCase().includes(query) || asset.tags.some(tag => tag.toLowerCase().includes(query)); const matchesCategory = selectedCategory === 'ALL' || asset.categoryId === selectedCategory; return matchesSearch && matchesCategory; }); // Calculate pending download requests count const pendingRequestsCount = assets.reduce((acc, asset) => { return acc + (asset.downloadRequests?.filter(r => r.status === 'PENDING').length || 0); }, 0); // Helper sharing logic const isOrgSharedEntirely = (orgId: string) => { return sharesList.some(s => s.organizationId === orgId && s.userId === null); }; const isUserSharedSpecifically = (orgId: string, userId: string) => { return sharesList.some(s => s.organizationId === orgId && s.userId === userId); }; const handleToggleOrg = (orgId: string) => { const isShared = isOrgSharedEntirely(orgId); if (isShared) { // Remove org and its users setSharesList(prev => prev.filter(s => s.organizationId !== orgId)); } else { // Remove any specific users from this org first, then add org-wide share setSharesList(prev => [ ...prev.filter(s => s.organizationId !== orgId), { organizationId: orgId, userId: null } ]); } }; const handleToggleUser = (orgId: string, userId: string) => { const isShared = isUserSharedSpecifically(orgId, userId); if (isShared) { setSharesList(prev => prev.filter(s => !(s.organizationId === orgId && s.userId === userId))); } else { // Remove org-wide share first if it exists setSharesList(prev => [ ...prev.filter(s => !(s.organizationId === orgId && s.userId === null)), { organizationId: orgId, userId } ]); } }; return ( {/* Header Section */}
Global CDN Active

Asset Library

Securely manage, distribute, and track marketing collateral and partner resources.

{user?.role === 'ADMIN' && pendingRequestsCount > 0 && ( )} {user?.role === 'ADMIN' && ( )}
{/* Action Bar */}
setSearchQuery(e.target.value)} className="w-full bg-ink-0 border border-ink-200 rounded-xl py-3.5 pl-11 pr-4 text-ink-900 placeholder-ink-400 outline-none transition-all focus:border-ink-900/50 focus:ring-4 ring-ink-900/10 font-medium shadow-sm hover:border-ink-300" placeholder="Search by title, desc, tag, category, URL..." />
{CATEGORIES.map((cat) => ( ))}
{/* Grid Content */} {loading ? (
) : filteredAssets.length === 0 ? (

No assets found

There are no assets matching your criteria.

) : ( {filteredAssets.map((asset) => { const Icon = getAssetIcon(asset.type); const isMenuOpen = activeMenuId === asset.id; const canDirectDownload = user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED'; const requestStatus = asset.downloadRequests?.[0]?.status; return (
{isRenderable(asset.type) && ( )} {isMenuOpen && ( <>
setActiveMenuId(null)} />
{user?.role === 'ADMIN' && ( <> )}
)}
{asset.categoryId || 'General'}
{!asset.isDownloadable && (
Strict View Only
)}

{asset.title}

{asset.type === 'url' ? 'External Link' : formatBytes(asset.size)} • {new Date(asset.createdAt).toLocaleDateString()}

{asset.type === 'url' ? 'Type' : 'Downloads'} {asset.type === 'url' ? 'URL Link' : asset.downloadsCount}
{asset.type === 'url' ? ( ) : canDirectDownload ? ( ) : requestStatus === 'PENDING' ? (
Pending Access
) : requestStatus === 'REJECTED' ? ( ) : ( )}
); })} )} {/* Upload Asset Modal */} {isUploadOpen && (
setIsUploadOpen(false)} className="absolute inset-0 bg-ink-950/40 backdrop-blur-sm" />

Upload / Link Asset

{/* Toggle upload tabs */}
{uploadTab === 'file' ? (
{ if (e.target.files?.[0]) { setUploadFile(e.target.files[0]); setUploadTitle(e.target.files[0].name); } }} required={uploadTab === 'file'} className="absolute inset-0 opacity-0 cursor-pointer" />

{uploadFile ? uploadFile.name : 'Drag & drop or click to upload file'}

PDF, ZIP, PNG, JPG up to 50MB

) : (
setUploadUrl(e.target.value)} placeholder="https://example.com/partner-docs" required={uploadTab === 'url'} className="w-full bg-ink-50 border border-ink-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400" />
)}
setUploadTitle(e.target.value)} placeholder="Enter descriptive title" className="w-full bg-ink-50 border border-ink-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400" />
setUploadSubcategory(e.target.value)} placeholder="e.g. Slide Deck" className="w-full bg-ink-50 border border-ink-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400" />