Compare commits

...

2 Commits

3 changed files with 140 additions and 42 deletions

View File

@ -271,35 +271,58 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
</div> </div>
</div> </div>
) : asset.type === 'url' ? ( ) : asset.type === 'url' ? (
<div className="text-center p-12 max-w-md space-y-4 flex flex-col justify-center items-center"> <div className="w-full h-full flex flex-col min-h-0 bg-ink-0">
<div className="w-16 h-16 rounded-2xl bg-ink-100 border border-ink-200 flex items-center justify-center text-ink-900 shadow-sm"> <div className="bg-ink-100 px-4 py-2 border-b border-ink-200 flex justify-between items-center text-xs text-ink-600 font-bold select-none shrink-0">
<Globe className="w-8 h-8" /> <span className="flex items-center gap-1.5">
</div> <Globe className="w-4 h-4 text-ink-950" />
<div> <span className="font-sans">External Web Link Preview</span>
<h4 className="text-sm font-bold text-ink-900 font-sans">External Resource Portal</h4> </span>
<p className="text-xs text-ink-500 mt-2 leading-relaxed font-sans"> <span className="text-[10px] text-ink-400 font-mono font-normal truncate max-w-xs">{asset.url}</span>
This asset points to an external destination outside of the local CDN container.
</p>
<div className="bg-ink-100 border border-ink-200 rounded-xl px-4 py-2 text-xs font-mono text-ink-600 truncate mt-3 max-w-sm">
{asset.url}
</div>
</div> </div>
<div className="flex-1 overflow-auto p-6 md:p-12 flex items-center justify-center bg-ink-50">
<div className="bg-ink-0 border border-ink-200 rounded-2xl p-6 shadow-xl max-w-xl w-full flex flex-col items-center text-center space-y-5">
{asset.thumbnailUrl ? (
<div className="w-full h-48 border border-ink-100 rounded-xl overflow-hidden shadow-sm shrink-0">
<img
src={asset.thumbnailUrl}
alt="Web Preview Banner"
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-16 h-16 rounded-2xl bg-ink-50 border border-ink-200 flex items-center justify-center text-ink-900 shadow-sm shrink-0">
<Globe className="w-8 h-8 text-ink-500" />
</div>
)}
{(user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') ? ( <div className="space-y-2 w-full">
<a <h3 className="text-base font-extrabold text-ink-950 font-sans leading-snug">{asset.title}</h3>
href={asset.url} {asset.description ? (
target="_blank" <p className="text-xs text-ink-500 font-sans leading-relaxed px-4">{asset.description}</p>
rel="noopener noreferrer" ) : (
className="inline-flex items-center justify-center gap-2 px-6 py-2.5 rounded-lg bg-ink-900 text-ink-0 text-xs font-bold hover:bg-ink-800 transition-all shadow-sm w-full font-sans" <p className="text-xs text-ink-400 font-sans italic leading-relaxed">No description provided for this external link.</p>
> )}
<span>Open Link in New Tab</span> </div>
<ExternalLink className="w-4 h-4" />
</a> <div className="w-full pt-2 px-4">
) : ( {(user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') ? (
<div className="bg-amber-50 border border-amber-205 text-amber-900 rounded-xl p-4 text-xs text-center font-medium max-w-sm font-sans"> <a
Access Restricted: You must request and receive download approval from the Administrator to open this resource link. href={asset.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 px-6 py-2.5 rounded-xl bg-ink-900 text-ink-0 text-xs font-bold hover:bg-ink-850 transition-all hover:scale-[1.02] active:scale-95 shadow-md w-full font-sans cursor-pointer"
>
<span>Open Website in New Tab</span>
<ExternalLink className="w-4 h-4" />
</a>
) : (
<div className="bg-amber-50 border border-amber-200 text-amber-900 rounded-xl p-4 text-xs font-medium text-center font-sans">
Access Restricted: You must request and receive approval from the Administrator to open this external link.
</div>
)}
</div>
</div> </div>
)} </div>
</div> </div>
) : asset.type.includes('pdf') ? ( ) : asset.type.includes('pdf') ? (
<div className="w-full h-full flex flex-col min-h-0"> <div className="w-full h-full flex flex-col min-h-0">

View File

@ -95,6 +95,30 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
} }
}; };
const handleFetchUrlDetails = async () => {
if (!uploadUrl) {
error('URL Required', 'Please enter a URL first.');
return;
}
if (!uploadUrl.startsWith('http')) {
error('Invalid URL', 'Please enter a valid URL starting with http:// or https://');
return;
}
setIsScraping(true);
try {
const data = await scrapeCaseStudy(uploadUrl);
setUploadTitle(data.title);
setThumbnailUrl(data.thumbnailUrl || '');
success('Page Details Fetched', 'Title and banner image successfully retrieved.');
} catch (err: any) {
console.error('Failed to scrape URL details', err);
error('Failed to retrieve page details', err.response?.data?.error || 'Make sure the URL is accessible.');
} finally {
setIsScraping(false);
}
};
const formatBytes = (bytes: number, decimals = 2) => { const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;
@ -318,14 +342,29 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
) : uploadTab === 'url' ? ( ) : uploadTab === 'url' ? (
<div> <div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">External Asset URL</label> <label className="text-xs font-semibold text-ink-500 mb-1.5 block">External Asset URL</label>
<input <div className="flex gap-2">
type="url" <input
value={uploadUrl} type="url"
onChange={(e) => setUploadUrl(e.target.value)} value={uploadUrl}
placeholder="https://example.com/partner-docs" onChange={(e) => setUploadUrl(e.target.value)}
required={uploadTab === 'url'} placeholder="https://example.com/partner-docs"
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400" required={uploadTab === 'url'}
/> className="flex-1 bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400"
/>
<Button
type="button"
onClick={handleFetchUrlDetails}
disabled={isScraping || !uploadUrl}
variant="primary"
size="sm"
className="flex-shrink-0"
>
{isScraping ? 'Fetching...' : 'Fetch Details'}
</Button>
</div>
<span className="text-[10px] text-ink-450 mt-1 block">
Fetches title and thumbnail banner image from the target webpage automatically.
</span>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">

View File

@ -29,6 +29,7 @@ import { ManageGroupsModal } from "../features/assets/components/ManageGroupsMod
import { PageHeader } from "../components/ui/PageHeader"; import { PageHeader } from "../components/ui/PageHeader";
import Button from "../components/ui/Button"; import Button from "../components/ui/Button";
import { PageLayout } from "../components/layout/PageLayout"; import { PageLayout } from "../components/layout/PageLayout";
import Modal from "../components/ui/Modal";
// Type Definitions // Type Definitions
import type { Asset, Organization } from "../types/assets"; import type { Asset, Organization } from "../types/assets";
@ -74,6 +75,7 @@ export const AssetsPage = () => {
const [activeMenuId, setActiveMenuId] = useState<string | null>(null); const [activeMenuId, setActiveMenuId] = useState<string | null>(null);
const [selectedAssetIds, setSelectedAssetIds] = useState<string[]>([]); const [selectedAssetIds, setSelectedAssetIds] = useState<string[]>([]);
const [expandedAssetId, setExpandedAssetId] = useState<string | null>(null); const [expandedAssetId, setExpandedAssetId] = useState<string | null>(null);
const [assetToDelete, setAssetToDelete] = useState<{ id: string, title: string } | null>(null);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@ -138,16 +140,19 @@ export const AssetsPage = () => {
setIsDetailsOpen(true); setIsDetailsOpen(true);
}; };
const handleDeleteAsset = async (id: string) => { const handleDeleteAsset = (id: string) => {
const asset = assets.find(a => a.id === id); const asset = assets.find(a => a.id === id);
const title = asset ? asset.title : "this asset"; if (asset) {
if ( setAssetToDelete({ id: asset.id, title: asset.title });
!window.confirm(`Are you sure you want to permanently delete "${title}"?`) }
) };
return;
const confirmDeleteAsset = async () => {
if (!assetToDelete) return;
try { try {
await deleteAsset(id); await deleteAsset(assetToDelete.id);
success("Asset deleted successfully", `"${title}" has been permanently removed.`); success("Asset deleted successfully", `"${assetToDelete.title}" has been permanently removed.`);
setAssetToDelete(null);
await fetchData(); await fetchData();
} catch (err: any) { } catch (err: any) {
console.error("Failed to delete asset", err); console.error("Failed to delete asset", err);
@ -474,6 +479,37 @@ export const AssetsPage = () => {
groups={groups} groups={groups}
onRefresh={fetchData} onRefresh={fetchData}
/> />
{/* Delete Confirmation Modal */}
<Modal
isOpen={assetToDelete !== null}
onClose={() => setAssetToDelete(null)}
title="Delete Asset"
subtitle="Confirm permanent resource deletion."
size="sm"
>
<div className="space-y-4 font-sans">
<p className="text-xs text-ink-600 leading-relaxed">
Are you sure you want to permanently delete <span className="font-bold text-ink-900">"{assetToDelete?.title}"</span>? This action cannot be undone.
</p>
<div className="flex gap-3 justify-end pt-2">
<Button
onClick={() => setAssetToDelete(null)}
variant="secondary"
size="sm"
>
Cancel
</Button>
<Button
onClick={confirmDeleteAsset}
variant="danger"
size="sm"
>
Delete Permanently
</Button>
</div>
</div>
</Modal>
</PageLayout> </PageLayout>
); );
}; };