changemaker.lite/admin/src/components/media/UploadVideoDrawer.tsx

286 lines
8.7 KiB
TypeScript

import { useState } from 'react';
import {
Drawer,
Upload,
Form,
Input,
Progress,
Alert,
Space,
Typography,
List,
Tag,
Button,
Grid,
} from 'antd';
import { InboxOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
import { mediaApi } from '@/lib/media-api';
const { Dragger } = Upload;
const { Text } = Typography;
interface UploadVideoDrawerProps {
open: boolean;
onClose: () => void;
onSuccess: () => void;
}
interface UploadResult {
filename: string;
success: boolean;
error?: string;
}
export default function UploadVideoDrawer({ open, onClose, onSuccess }: UploadVideoDrawerProps) {
const [form] = Form.useForm();
const screens = Grid.useBreakpoint();
const isMobile = !screens.md;
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [results, setResults] = useState<UploadResult[]>([]);
const [showResults, setShowResults] = useState(false);
const maxFileSize = 10 * 1024 * 1024 * 1024; // 10GB
const handleClose = () => {
if (!uploading) {
form.resetFields();
setFileList([]);
setResults([]);
setShowResults(false);
setUploadProgress(0);
onClose();
}
};
const beforeUpload = (file: File) => {
// Validate file size
if (file.size > maxFileSize) {
return Upload.LIST_IGNORE;
}
// Validate file type
const validTypes = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska', 'video/webm', 'video/x-m4v', 'video/x-flv'];
const validExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.flv'];
const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
if (!validTypes.includes(file.type) && !validExtensions.includes(ext || '')) {
return Upload.LIST_IGNORE;
}
return false; // Prevent auto-upload
};
const handleUpload = async () => {
if (fileList.length === 0) {
return;
}
setUploading(true);
setShowResults(false);
setResults([]);
setUploadProgress(0);
const uploadResults: UploadResult[] = [];
let completed = 0;
// Upload files sequentially
for (const file of fileList) {
try {
const formData = new FormData();
formData.append('file', file.originFileObj as File);
// Add metadata for single file uploads
if (fileList.length === 1) {
const values = form.getFieldsValue();
if (values.title) formData.append('title', values.title);
if (values.producer) formData.append('producer', values.producer);
if (values.creator) formData.append('creator', values.creator);
}
await mediaApi.post('/videos/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const fileProgress = (progressEvent.loaded / progressEvent.total) * 100;
const totalProgress = ((completed + fileProgress / 100) / fileList.length) * 100;
setUploadProgress(Math.round(totalProgress));
}
},
});
uploadResults.push({
filename: file.name,
success: true,
});
} catch (error: any) {
uploadResults.push({
filename: file.name,
success: false,
error: error.response?.data?.message || 'Upload failed',
});
}
completed++;
setUploadProgress(Math.round((completed / fileList.length) * 100));
}
setResults(uploadResults);
setShowResults(true);
setUploading(false);
// Auto-close and refresh if all successful
const allSuccess = uploadResults.every((r) => r.success);
if (allSuccess) {
setTimeout(() => {
onSuccess();
handleClose();
}, 1500);
}
};
const successCount = results.filter((r) => r.success).length;
const failCount = results.length - successCount;
return (
<Drawer
title="Upload Videos"
open={open}
onClose={handleClose}
placement="right"
width={isMobile ? '100%' : 520}
mask={false}
destroyOnClose
closable={!uploading}
styles={{
wrapper: { top: 64, height: 'calc(100vh - 64px)' },
body: { padding: 24, overflowY: 'auto' },
}}
extra={
<Space>
<Button onClick={handleClose} disabled={uploading}>
Cancel
</Button>
<Button
type="primary"
onClick={handleUpload}
disabled={fileList.length === 0 || uploading}
loading={uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</Button>
</Space>
}
>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{!showResults && (
<>
<Dragger
multiple
fileList={fileList}
onChange={({ fileList: newFileList }) => setFileList(newFileList)}
beforeUpload={beforeUpload}
accept="video/*,.mp4,.mov,.avi,.mkv,.webm,.m4v,.flv"
disabled={uploading}
showUploadList={{ showRemoveIcon: !uploading }}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">Click or drag video files to this area to upload</p>
<p className="ant-upload-hint">
Support for single or bulk upload. Maximum file size: 10GB per file.
<br />
Accepted formats: MP4, MOV, AVI, MKV, WebM, M4V, FLV
</p>
</Dragger>
{fileList.length === 1 && (
<Form form={form} layout="vertical">
<Alert
message="Optional Metadata"
description="Add metadata for this video (optional)"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Form.Item label="Title" name="title">
<Input placeholder="Video title (defaults to filename)" />
</Form.Item>
<Form.Item label="Producer" name="producer">
<Input placeholder="Producer name" />
</Form.Item>
<Form.Item label="Creator" name="creator">
<Input placeholder="Creator name" />
</Form.Item>
</Form>
)}
{fileList.length > 1 && (
<Alert
message={`${fileList.length} files selected for batch upload`}
description="Metadata fields are not available for batch uploads. Titles will default to filenames."
type="info"
showIcon
/>
)}
</>
)}
{uploading && (
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<Text>
Uploading {fileList.length} file(s)... This may take a while for large files.
</Text>
<Progress percent={uploadProgress} status="active" />
</Space>
)}
{showResults && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Alert
message={`Upload Complete: ${successCount} succeeded, ${failCount} failed`}
type={failCount === 0 ? 'success' : 'warning'}
showIcon
/>
<List
size="small"
bordered
dataSource={results}
renderItem={(result) => (
<List.Item>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Text ellipsis style={{ maxWidth: 340 }}>
{result.filename}
</Text>
{result.success ? (
<Tag icon={<CheckCircleOutlined />} color="success">
Success
</Tag>
) : (
<Tag icon={<CloseCircleOutlined />} color="error" title={result.error}>
Failed
</Tag>
)}
</Space>
{result.error && (
<div style={{ marginTop: 4 }}>
<Text type="danger" style={{ fontSize: 12 }}>
{result.error}
</Text>
</div>
)}
</List.Item>
)}
/>
</Space>
)}
</Space>
</Drawer>
);
}