itcloud/frontend/src/components/MediaCard.tsx

194 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
// Try to get thumbnail first, fallback to original for photos
const url = asset.storage_key_thumb
? await api.getDownloadUrl(asset.id, 'thumb')
: asset.type === 'photo'
? await api.getDownloadUrl(asset.id, 'original')
: '';
setThumbnailUrl(url);
} 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>
);
}