559 lines
24 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
};
|