Tech4biz-channel/Channel-Frontend/src/features/assets/components/UploadAssetModal.tsx
2026-07-17 15:05:23 +05:30

559 lines
24 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { X, UploadCloud, Eye, FileText, File } from 'lucide-react';
import { uploadAsset, scrapeCaseStudy } from '../../../services/assets-api';
import Modal from '../../../components/ui/Modal';
import Button from '../../../components/ui/Button';
import { useToast } from '../../../hooks/use-toast';
interface UploadAssetModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}
export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
isOpen,
onClose,
onSuccess
}) => {
const { success, error } = useToast();
const [uploadTab, setUploadTab] = useState<'file' | 'url' | 'case_study'>('file');
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [fullImagePreviewUrl, setFullImagePreviewUrl] = useState<string | null>(null);
const [uploadUrl, setUploadUrl] = useState('');
const [caseStudyUrl, setCaseStudyUrl] = useState('');
const [thumbnailUrl, setThumbnailUrl] = useState('');
const [problemStatement, setProblemStatement] = useState('');
const [solution, setSolution] = useState('');
const [isScraping, setIsScraping] = useState(false);
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);
useEffect(() => {
if (!uploadFile) {
setPreviewUrl(null);
setFullImagePreviewUrl(null);
return;
}
if (uploadFile.type.startsWith('image/')) {
const url = URL.createObjectURL(uploadFile);
setPreviewUrl(url);
return () => {
URL.revokeObjectURL(url);
};
} else {
setPreviewUrl(null);
setFullImagePreviewUrl(null);
}
}, [uploadFile]);
const handleFetchCaseStudy = async () => {
if (!caseStudyUrl) {
error('URL Required', 'Please enter a case study URL first.');
return;
}
if (!caseStudyUrl.startsWith('http')) {
error('Invalid URL', 'Please enter a valid URL starting with http:// or https://');
return;
}
setIsScraping(true);
try {
const data = await scrapeCaseStudy(caseStudyUrl);
setUploadTitle(data.title);
setThumbnailUrl(data.thumbnailUrl);
setProblemStatement(data.problemStatement);
setSolution(data.solution);
// Auto-set tag as case-study
setUploadTags(prev => {
const tags = prev.split(',').map(t => t.trim()).filter(Boolean);
if (!tags.includes('case-study')) {
tags.push('case-study');
}
return tags.join(', ');
});
// Auto-set category and subcategory
setUploadCategory('Resources');
setUploadSubcategory('Case Study');
success('Case Study Details Fetched', 'Title, banner, problem, and solution successfully retrieved.');
} catch (err: any) {
console.error('Failed to scrape case study', err);
error('Failed to retrieve case study details', err.response?.data?.error || 'Make sure the URL is a valid showcase case study.');
} finally {
setIsScraping(false);
}
};
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) => {
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 handleUploadSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (uploadTab === 'file' && !uploadFile) return;
if (uploadTab === 'url' && !uploadUrl) return;
if (uploadTab === 'case_study' && !caseStudyUrl) return;
setIsUploading(true);
const formData = new FormData();
if (uploadTab === 'file' && uploadFile) {
formData.append('file', uploadFile);
} else {
formData.append('isUrlAsset', 'true');
formData.append('url', uploadTab === 'case_study' ? caseStudyUrl : uploadUrl);
}
formData.append('type', uploadTab === 'case_study' ? 'case_study' : (uploadTab === 'url' ? 'url' : ''));
formData.append('title', uploadTitle || (uploadFile ? uploadFile.name : (uploadTab === 'case_study' ? caseStudyUrl : 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));
if (thumbnailUrl) {
formData.append('thumbnailUrl', thumbnailUrl);
}
if (uploadTab === 'case_study') {
formData.append('problemStatement', problemStatement);
formData.append('solution', solution);
}
try {
await uploadAsset(formData);
setUploadFile(null);
setUploadUrl('');
setCaseStudyUrl('');
setThumbnailUrl('');
setProblemStatement('');
setSolution('');
setUploadTitle('');
setUploadDescription('');
setUploadCategory('Marketing');
setUploadSubcategory('');
setUploadTags('');
setUploadGithubUrl('');
setUploadIsDownloadable(true);
success('Asset published successfully', 'The asset has been added to the catalog.');
onSuccess();
onClose();
} catch (err: any) {
console.error('Failed to upload asset', err);
error('Failed to publish asset', err.response?.data?.error || 'Something went wrong.');
} finally {
setIsUploading(false);
}
};
return (
<>
<Modal
isOpen={isOpen}
onClose={onClose}
title="Upload / Link Asset"
size="lg"
footer={
<>
<Button
type="button"
onClick={onClose}
variant="ghost"
size="sm"
>
Cancel
</Button>
<Button
type="submit"
form="upload-asset-form"
disabled={isUploading}
variant="primary"
size="sm"
>
{isUploading ? 'Publishing...' : 'Publish Asset'}
</Button>
</>
}
>
{/* Toggle upload tabs */}
<div className="flex bg-ink-50 p-1 rounded-xl border border-ink-200 mt-2">
<button
type="button"
onClick={() => setUploadTab('file')}
className={`flex-1 py-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer ${uploadTab === 'file' ? 'bg-ink-0 text-ink-900 shadow-sm border border-ink-200' : 'text-ink-500 hover:text-ink-800'}`}
>
Secure File Upload
</button>
<button
type="button"
onClick={() => setUploadTab('url')}
className={`flex-1 py-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer ${uploadTab === 'url' ? 'bg-ink-0 text-ink-900 shadow-sm border border-ink-200' : 'text-ink-500 hover:text-ink-800'}`}
>
External Web URL
</button>
<button
type="button"
onClick={() => setUploadTab('case_study')}
className={`flex-1 py-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer ${uploadTab === 'case_study' ? 'bg-ink-0 text-ink-900 shadow-sm border border-ink-200' : 'text-ink-500 hover:text-ink-800'}`}
>
Showcase Case Study
</button>
</div>
<form id="upload-asset-form" onSubmit={handleUploadSubmit} className="space-y-4 mt-4">
{uploadTab === 'file' ? (
<div className={`border-2 border-dashed border-ink-200 hover:border-ink-400 rounded-xl p-5 text-center transition-colors relative bg-ink-50 max-h-48 overflow-y-auto scrollbar-thin ${!uploadFile ? 'cursor-pointer' : ''}`}>
{!uploadFile && (
<input
type="file"
onChange={(e) => {
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 z-10"
/>
)}
{uploadFile && !previewUrl && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setUploadFile(null);
}}
className="absolute top-2 right-2 p-1 rounded-lg bg-ink-0 hover:bg-ink-100 text-ink-500 hover:text-ink-900 border border-ink-200 transition-colors z-20 shadow-sm cursor-pointer"
title="Remove file"
>
<X className="w-3.5 h-3.5" />
</button>
)}
{previewUrl && uploadFile ? (
<div className="relative z-20 py-1">
<div className="relative w-24 h-24 mx-auto mb-3">
<img
src={previewUrl}
alt="Upload preview"
className="w-full h-full object-cover rounded-lg shadow-sm border border-ink-200"
/>
</div>
<div className="flex items-center justify-between gap-2 max-w-xs mx-auto px-2.5 py-1.5 bg-ink-0 border border-ink-200 rounded-lg shadow-sm relative z-30 mb-1.5">
<span className="text-[11px] font-semibold text-ink-900 truncate flex-1 text-left" title={uploadFile?.name}>
{uploadFile?.name}
</span>
<div className="flex items-center gap-1 flex-shrink-0">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setFullImagePreviewUrl(previewUrl);
}}
className="p-1 rounded-lg bg-ink-55 hover:bg-ink-100 text-ink-600 hover:text-ink-900 border border-ink-200 transition-all duration-200 hover:scale-105 active:scale-95 flex items-center justify-center cursor-pointer"
title="Preview Image"
>
<Eye className="w-3 h-3" />
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setUploadFile(null);
}}
className="p-1 rounded-lg bg-ink-55 hover:bg-red-500/10 text-ink-500 hover:text-red-655 border border-ink-200 transition-all duration-200 hover:scale-105 active:scale-95 flex items-center justify-center cursor-pointer"
title="Remove File"
>
<X className="w-3 h-3" />
</button>
</div>
</div>
<p className="text-[10px] text-ink-500">
{formatBytes(uploadFile?.size || 0)}
</p>
</div>
) : uploadFile ? (
<div className="relative z-0 py-1">
<div className="w-12 h-12 rounded-lg bg-ink-100 border border-ink-200 flex items-center justify-center mx-auto mb-2">
{uploadFile?.name?.endsWith('.pdf') ? (
<FileText className="w-6 h-6 text-ink-600" />
) : (
<File className="w-6 h-6 text-ink-600" />
)}
</div>
<p className="text-xs font-bold text-ink-900 truncate max-w-xs mx-auto">
{uploadFile?.name}
</p>
<p className="text-[10px] text-ink-500 mt-0.5">
{formatBytes(uploadFile?.size || 0)}
</p>
</div>
) : (
<div className="relative z-0">
<UploadCloud className="w-8 h-8 text-ink-400 mx-auto mb-1.5" />
<p className="text-xs font-bold text-ink-900">
Drag & drop or click to upload file
</p>
<p className="text-[10px] text-ink-500 mt-0.5">PDF, ZIP, PNG, JPG up to 50MB</p>
</div>
)}
</div>
) : uploadTab === 'url' ? (
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">External Asset URL</label>
<div className="flex gap-2">
<input
type="url"
value={uploadUrl}
onChange={(e) => setUploadUrl(e.target.value)}
placeholder="https://example.com/partner-docs"
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 className="space-y-4">
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Case Study URL</label>
<div className="flex gap-2">
<input
type="url"
value={caseStudyUrl}
onChange={(e) => setCaseStudyUrl(e.target.value)}
placeholder="https://showcase.tech4bizsolutions.com/ai-forecasting-pricing-for-fmcg-tech4biz-case-study/"
required={uploadTab === 'case_study'}
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={handleFetchCaseStudy}
disabled={isScraping || !caseStudyUrl}
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, banner image, problem statement, and solution from the showcase platform.
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Scraped Problem Statement</label>
<textarea
value={problemStatement}
onChange={(e) => setProblemStatement(e.target.value)}
placeholder="Problem Statement will appear here..."
rows={4}
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-xs focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400 resize-y font-sans leading-relaxed"
/>
</div>
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Scraped Solution</label>
<textarea
value={solution}
onChange={(e) => setSolution(e.target.value)}
placeholder="Solution details will appear here..."
rows={4}
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-xs focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400 resize-y font-sans leading-relaxed"
/>
</div>
</div>
</div>
)}
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Thumbnail Image URL (Optional)</label>
<input
type="url"
value={thumbnailUrl}
onChange={(e) => setThumbnailUrl(e.target.value)}
placeholder="https://example.com/image.png"
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"
/>
</div>
{thumbnailUrl && (
<div>
<label className="text-xs font-semibold text-ink-500 mb-1 block">Thumbnail Preview</label>
<div className="w-full h-32 border border-ink-200 rounded-lg overflow-hidden relative bg-ink-50">
<img
src={thumbnailUrl}
alt="Thumbnail Preview"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Asset Title</label>
<input
type="text"
value={uploadTitle}
onChange={(e) => setUploadTitle(e.target.value)}
placeholder="Enter descriptive title"
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"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Category</label>
<select
value={uploadCategory}
onChange={(e) => setUploadCategory(e.target.value)}
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"
>
<option value="Marketing">Marketing</option>
<option value="Presentations">Presentations</option>
<option value="Branding">Branding</option>
<option value="Resources">Resources</option>
<option value="Technical">Technical</option>
</select>
</div>
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Subcategory</label>
<input
type="text"
value={uploadSubcategory}
onChange={(e) => setUploadSubcategory(e.target.value)}
placeholder="e.g. Slide Deck"
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"
/>
</div>
</div>
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Description <span className="text-red-500">*</span></label>
<textarea
value={uploadDescription}
onChange={(e) => setUploadDescription(e.target.value)}
placeholder="Enter short description about this asset..."
rows={3}
required
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 resize-none"
/>
</div>
{uploadTab === 'file' && (
<div className="flex items-center gap-3 p-3 bg-ink-50 border border-ink-200 rounded-lg">
<input
type="checkbox"
id="isDownloadable"
checked={uploadIsDownloadable}
onChange={(e) => setUploadIsDownloadable(e.target.checked)}
className="w-4 h-4 rounded border-ink-300 text-ink-900 focus:ring-ink-900/10 cursor-pointer"
/>
<div>
<label htmlFor="isDownloadable" className="text-xs font-bold text-ink-900 cursor-pointer block">
Allow Direct Download
</label>
<span className="text-[10px] text-ink-500">
If unchecked, clients must request manual download access (Strict View Only).
</span>
</div>
</div>
)}
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Tags (comma-separated)</label>
<input
type="text"
value={uploadTags}
onChange={(e) => setUploadTags(e.target.value)}
placeholder="branding, guideline, pitch"
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"
/>
</div>
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">GitHub/Documentation URL (Optional)</label>
<input
type="url"
value={uploadGithubUrl}
onChange={(e) => setUploadGithubUrl(e.target.value)}
placeholder="https://github.com/..."
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"
/>
</div>
</form>
</Modal>
{/* Full Local Image Preview Modal */}
<Modal
isOpen={!!fullImagePreviewUrl}
onClose={() => setFullImagePreviewUrl(null)}
title="Asset Image Preview"
size="lg"
>
<div className="flex flex-col items-center justify-center p-1">
<img
src={fullImagePreviewUrl || ''}
alt="Full preview"
className="max-w-full max-h-[60vh] object-contain rounded-xl shadow-md border border-ink-200 bg-ink-50"
/>
</div>
</Modal>
</>
);
};