itcloud/frontend/src/components/MediaCard.tsx

194 lines
4.9 KiB
TypeScript
Raw Normal View History

2025-12-30 13:35:19 +01:00
import { useState, useEffect } from 'react';
import {
Card,
CardMedia,
CardActionArea,
Box,
IconButton,
CircularProgress,
Typography,
Checkbox,
} from '@mui/material';
import {
PlayCircleOutline as VideoIcon,
CheckCircle as CheckedIcon,
} from '@mui/icons-material';
import type { Asset } from '../types';
import api from '../services/api';
interface MediaCardProps {
asset: Asset;
selected?: boolean;
onSelect?: (assetId: string, selected: boolean) => void;
onClick?: () => void;
}
export default function MediaCard({ asset, selected, onSelect, onClick }: MediaCardProps) {
const [thumbnailUrl, setThumbnailUrl] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
loadThumbnail();
}, [asset.id]);
const loadThumbnail = async () => {
try {
setLoading(true);
setError(false);
2025-12-30 15:53:16 +01:00
// Load media through backend proxy with auth
const kind = asset.storage_key_thumb ? 'thumb' : 'original';
if (asset.type === 'photo' || asset.storage_key_thumb) {
const blob = await api.getMediaBlob(asset.id, kind);
const url = URL.createObjectURL(blob);
setThumbnailUrl(url);
}
2025-12-30 13:35:19 +01:00
} catch (err) {
console.error('Failed to load thumbnail:', err);
setError(true);
} finally {
setLoading(false);
}
};
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
};
const handleSelect = (e: React.MouseEvent) => {
e.stopPropagation();
if (onSelect) {
onSelect(asset.id, !selected);
}
};
return (
<Card
sx={{
position: 'relative',
aspectRatio: '1',
borderRadius: 2,
overflow: 'hidden',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 4,
},
}}
>
<CardActionArea onClick={onClick} sx={{ height: '100%' }}>
{loading && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
bgcolor: 'grey.200',
}}
>
<CircularProgress />
</Box>
)}
{error && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
bgcolor: 'grey.300',
}}
>
<Typography color="error">Ошибка загрузки</Typography>
</Box>
)}
{!loading && !error && thumbnailUrl && (
<CardMedia
component="img"
image={thumbnailUrl}
alt={asset.original_filename}
sx={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
)}
{asset.type === 'video' && (
<Box
sx={{
position: 'absolute',
top: 8,
right: 8,
color: 'white',
bgcolor: 'rgba(0,0,0,0.5)',
borderRadius: '50%',
}}
>
<VideoIcon fontSize="large" />
</Box>
)}
<Box
sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(0,0,0,0.6)',
color: 'white',
p: 1,
}}
>
<Typography variant="caption" display="block" noWrap>
{asset.original_filename}
</Typography>
<Typography variant="caption" display="block">
{formatFileSize(asset.size_bytes)} {formatDate(asset.created_at)}
</Typography>
</Box>
{onSelect && (
<Checkbox
checked={selected}
onClick={handleSelect}
icon={
<Box
sx={{
width: 24,
height: 24,
borderRadius: '50%',
border: '2px solid white',
bgcolor: 'rgba(0,0,0,0.3)',
}}
/>
}
checkedIcon={<CheckedIcon sx={{ color: 'primary.main' }} />}
sx={{
position: 'absolute',
top: 8,
left: 8,
}}
/>
)}
</CardActionArea>
</Card>
);
}