Tech4biz-channel/Channel-Frontend/src/pages/AssetsPage.tsx
2026-07-10 18:13:08 +05:30

438 lines
14 KiB
TypeScript

import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import type { Variants } from "framer-motion";
import { UploadCloud, Search, File, CheckCircle, Share2 } from "lucide-react";
import { useAuthStore } from "../hooks/use-auth";
import { axiosInstance } from "../services/axios";
import {
getAssets,
getOrganizations,
deleteAsset,
requestDownload,
approveDownloadRequest,
rejectDownloadRequest,
downloadAssetFile,
} from "../services/assets-api";
import { useToast } from "../hooks/use-toast";
// Subcomponents
import { AssetCard } from "../features/assets/components/AssetCard";
import { UploadAssetModal } from "../features/assets/components/UploadAssetModal";
import { EditAssetModal } from "../features/assets/components/EditAssetModal";
import { ShareAssetModal } from "../features/assets/components/ShareAssetModal";
import { AssetDetailsModal } from "../features/assets/components/AssetDetailsModal";
import { AssetViewerModal } from "../features/assets/components/AssetViewerModal";
import { DownloadRequestsModal } from "../features/assets/components/DownloadRequestsModal";
import { PageHeader } from "../components/ui/PageHeader";
import Button from "../components/ui/Button";
import { PageLayout } from "../components/layout/PageLayout";
// Type Definitions
import type { Asset, Organization } from "../types/assets";
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 },
},
};
export const AssetsPage = () => {
const { success, error } = useToast();
const user = useAuthStore((state) => state.user);
const [assets, setAssets] = useState<Asset[]>([]);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [loading, setLoading] = useState(true);
// Search & Filter
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("ALL");
// Modal Control States
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<Asset | null>(null);
const [activeMenuId, setActiveMenuId] = useState<string | null>(null);
const [selectedAssetIds, setSelectedAssetIds] = useState<string[]>([]);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
try {
const assetsData = await getAssets();
setAssets(assetsData);
setSelectedAssetIds([]);
if (user?.role === "ADMIN") {
const orgsData = await getOrganizations();
setOrganizations(orgsData);
}
} catch (err) {
console.error("Failed to fetch assets data", err);
} finally {
setLoading(false);
}
};
const openEditModal = (asset: Asset) => {
setActiveAsset(asset);
setIsEditOpen(true);
};
const openShareModal = (asset: Asset) => {
setSelectedAssetIds([]);
setActiveAsset(asset);
setIsShareOpen(true);
};
const handleToggleSelectAsset = (assetId: string) => {
setSelectedAssetIds((prev) =>
prev.includes(assetId)
? prev.filter((id) => id !== assetId)
: [...prev, assetId]
);
};
const openViewerModal = (asset: Asset) => {
setActiveAsset(asset);
setIsViewerOpen(true);
};
const openDetailsModal = (asset: Asset) => {
setActiveAsset(asset);
setIsDetailsOpen(true);
};
const handleDeleteAsset = async (id: string) => {
const asset = assets.find(a => a.id === id);
const title = asset ? asset.title : "this asset";
if (
!window.confirm(`Are you sure you want to permanently delete "${title}"?`)
)
return;
try {
await deleteAsset(id);
success("Asset deleted successfully", `"${title}" has been permanently removed.`);
await fetchData();
} catch (err: any) {
console.error("Failed to delete asset", err);
error("Failed to delete asset", err.response?.data?.error || "Something went wrong.");
}
};
const handleRequestDownload = async (asset: Asset) => {
try {
await requestDownload(asset.id);
success("Download request submitted", "An administrator has been notified of your request.");
await fetchData();
} catch (err: any) {
console.error("Failed to request download access", err);
error("Failed to submit request", err.response?.data?.error || "Something went wrong.");
}
};
const handleApproveRequest = async (assetId: string, requestId: string) => {
try {
await approveDownloadRequest(assetId, requestId);
success("Download request approved", "The partner can now download this asset.");
await fetchData();
} catch (err: any) {
console.error("Failed to approve request", err);
error("Failed to approve request", err.response?.data?.error || "Something went wrong.");
}
};
const handleRejectRequest = async (assetId: string, requestId: string) => {
try {
await rejectDownloadRequest(assetId, requestId);
success("Download request rejected", "The access request was denied.");
await fetchData();
} catch (err: any) {
console.error("Failed to reject request", err);
error("Failed to reject request", err.response?.data?.error || "Something went wrong.");
}
};
const handleDownload = async (asset: Asset) => {
success("Download started", `Downloading "${asset.title}"...`);
try {
await downloadAssetFile(asset.id);
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: any) {
console.error("Failed to process download", err);
error("Download failed", err.response?.data?.error || "Could not retrieve asset file.");
}
};
const CATEGORIES = [
"ALL",
"Marketing",
"Presentations",
"Branding",
"Resources",
"Technical",
];
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;
});
const pendingRequestsCount = assets.reduce((acc, asset) => {
return (
acc +
(asset.downloadRequests?.filter((r) => r.status === "PENDING").length ||
0)
);
}, 0);
// Header component
const headerNode = (
<PageHeader
title="Asset Library"
subtitle="Securely manage, distribute, and track marketing collateral and partner resources."
badge={
<div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-ink-100 border border-ink-200 text-[10px] font-bold text-ink-900 tracking-wider uppercase shrink-0">
<CheckCircle className="w-3.5 h-3.5 text-ink-950" />
<span>Global CDN Active</span>
</div>
}
/>
);
// Toolbar component
const toolbarNode = (
<div className="flex flex-col md:flex-row gap-4 items-center justify-between p-3.5 bg-ink-0 border border-ink-200 rounded-xl shadow-sm">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 flex-1 w-full max-w-2xl">
<div className="relative flex-1 group">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="w-4 h-4 text-ink-400 group-focus-within:text-ink-900 transition-colors" />
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-ink-50 border border-ink-200 rounded-lg py-1.5 pl-9 pr-4 text-xs font-semibold text-ink-900 placeholder-ink-400 outline-none transition-all focus:border-ink-900/50 focus:ring-2 ring-ink-900/10 hover:border-ink-300 font-sans"
placeholder="Search by title, desc, tag, category..."
/>
</div>
<div className="flex gap-1.5 overflow-x-auto pb-1 sm:pb-0 scrollbar-none">
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-2.5 py-1.5 rounded-lg border text-[10px] uppercase tracking-wider font-bold transition-all whitespace-nowrap shadow-sm cursor-pointer ${
selectedCategory === cat
? "bg-ink-900 text-ink-0 border-ink-900"
: "bg-ink-50 text-ink-700 border-ink-200 hover:bg-ink-100"
}`}
>
{cat}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2 shrink-0 w-full md:w-auto justify-end">
{user?.role === "ADMIN" && pendingRequestsCount > 0 && (
<Button
onClick={() => setIsRequestsOpen(true)}
variant="secondary"
size="sm"
icon={
<span className="w-4 h-4 rounded-full bg-ink-900 text-ink-0 text-[10px] flex items-center justify-center font-extrabold mr-0.5">
{pendingRequestsCount}
</span>
}
>
Download Requests
</Button>
)}
{user?.role === "ADMIN" && selectedAssetIds.length > 0 && (
<Button
onClick={() => {
setActiveAsset(null);
setIsShareOpen(true);
}}
variant="secondary"
size="sm"
icon={<Share2 className="w-3.5 h-3.5" />}
>
Share Selected ({selectedAssetIds.length})
</Button>
)}
{user?.role === "ADMIN" && (
<Button
onClick={() => setIsUploadOpen(true)}
variant="primary"
size="sm"
icon={<UploadCloud className="w-3.5 h-3.5" />}
>
Create / Upload Asset
</Button>
)}
</div>
</div>
);
return (
<PageLayout header={headerNode} toolbar={toolbarNode}>
<div className="p-5 flex-1 min-h-0 overflow-y-auto">
{loading ? (
<div className="py-20 flex justify-center items-center">
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin" />
</div>
) : filteredAssets.length === 0 ? (
<div className="py-12 text-center bg-ink-0 border border-ink-200 rounded-xl">
<File className="w-12 h-12 text-ink-300 mx-auto mb-4" />
<h3 className="text-lg font-bold text-ink-900">No assets found</h3>
<p className="text-ink-500 text-sm mt-1">
There are no assets matching your criteria.
</p>
</div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4"
>
{filteredAssets.map((asset) => (
<motion.div key={asset.id} variants={itemVariants}>
<AssetCard
asset={asset}
user={user}
activeMenuId={activeMenuId}
setActiveMenuId={setActiveMenuId}
onViewDetails={openDetailsModal}
onEdit={openEditModal}
onShare={openShareModal}
onDelete={handleDeleteAsset}
onOpenViewer={openViewerModal}
onDownload={handleDownload}
onRequestDownload={handleRequestDownload}
isSelected={selectedAssetIds.includes(asset.id)}
onToggleSelect={handleToggleSelectAsset}
/>
</motion.div>
))}
</motion.div>
)}
</div>
{/* Modals Container */}
<UploadAssetModal
isOpen={isUploadOpen}
onClose={() => setIsUploadOpen(false)}
onSuccess={fetchData}
/>
<EditAssetModal
isOpen={isEditOpen}
onClose={() => {
setIsEditOpen(false);
setActiveAsset(null);
}}
asset={activeAsset}
onSuccess={fetchData}
/>
<ShareAssetModal
isOpen={isShareOpen}
onClose={() => {
setIsShareOpen(false);
setActiveAsset(null);
setSelectedAssetIds([]);
}}
asset={activeAsset}
assetIds={selectedAssetIds}
organizations={organizations}
onSuccess={fetchData}
/>
<AssetDetailsModal
isOpen={isDetailsOpen}
onClose={() => {
setIsDetailsOpen(false);
setActiveAsset(null);
}}
asset={activeAsset}
userRole={user?.role}
/>
<AssetViewerModal
isOpen={isViewerOpen}
onClose={() => {
setIsViewerOpen(false);
setActiveAsset(null);
}}
asset={activeAsset}
user={user}
onDownload={handleDownload}
/>
<DownloadRequestsModal
isOpen={isRequestsOpen}
onClose={() => setIsRequestsOpen(false)}
assets={assets}
onApprove={handleApproveRequest}
onReject={handleRejectRequest}
/>
</PageLayout>
);
};
export default AssetsPage;