turash/bugulma/frontend/pages/admin/MediaLibraryPage.tsx
2025-12-15 10:06:41 +01:00

190 lines
6.2 KiB
TypeScript

import { DataTable } from '@/components/admin/DataTable.tsx';
import { Badge, Button } from '@/components/ui';
import { useMediaAssets, useDeleteMediaAsset } from '@/hooks/api/useAdminAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import type { MediaAsset } from '@/services/admin-api.ts';
import { Edit, Plus, Trash2, Image as ImageIcon } from 'lucide-react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const MediaLibraryPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 25;
const [typeFilter, setTypeFilter] = useState<string | undefined>();
const { data, isLoading } = useMediaAssets({ type: typeFilter });
const { mutate: deleteAsset } = useDeleteMediaAsset();
const handleDelete = (asset: MediaAsset) => {
if (window.confirm(t('adminPage.content.media.confirmDelete') || 'Delete this media asset?')) {
deleteAsset(asset.id, {
onSuccess: () => {
// Query will refetch automatically
},
});
}
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
};
const getTypeBadge = (type: string) => {
if (type.startsWith('image/')) {
return <Badge variant="success">{t('adminPage.content.media.type') || 'Image'}</Badge>;
}
if (type.startsWith('video/')) {
return <Badge variant="default">{t('adminPage.content.media.type') || 'Video'}</Badge>;
}
return <Badge variant="secondary">{type}</Badge>;
};
const columns = [
{
key: 'thumbnail',
header: t('adminPage.content.media.table.thumbnail') || 'Thumbnail',
render: (asset: MediaAsset) => (
<div className="flex items-center gap-2">
{asset.type?.startsWith('image/') ? (
<img
src={asset.url}
alt={asset.altText || asset.name}
className="h-12 w-12 rounded object-cover"
/>
) : (
<div className="h-12 w-12 rounded bg-muted flex items-center justify-center">
<ImageIcon className="h-6 w-6 text-muted-foreground" />
</div>
)}
</div>
),
},
{
key: 'name',
header: t('adminPage.content.media.table.name') || 'Name',
render: (asset: MediaAsset) => <span className="font-medium">{asset.name}</span>,
},
{
key: 'type',
header: t('adminPage.content.media.table.type') || 'Type',
render: (asset: MediaAsset) => getTypeBadge(asset.type || 'unknown'),
},
{
key: 'size',
header: t('adminPage.content.media.table.size') || 'Size',
render: (asset: MediaAsset) => (
<span className="text-sm text-muted-foreground">{formatFileSize(asset.size || 0)}</span>
),
},
{
key: 'uploadedAt',
header: t('adminPage.content.media.table.uploaded') || 'Uploaded',
render: (asset: MediaAsset) => {
try {
const date = new Date(asset.uploadedAt || asset.createdAt);
return <span className="text-sm text-muted-foreground">{date.toLocaleDateString()}</span>;
} catch {
return <span className="text-sm text-muted-foreground"></span>;
}
},
},
];
const actions = [
{
label: t('adminPage.content.media.edit') || 'Edit',
icon: <Edit className="h-4 w-4" />,
onClick: (asset: MediaAsset) => navigate(`/admin/content/media/${asset.id}/edit`),
},
{
label: t('adminPage.content.media.delete') || 'Delete',
icon: <Trash2 className="h-4 w-4" />,
variant: 'destructive' as const,
onClick: (asset: MediaAsset) => handleDelete(asset),
},
];
const assets = data?.assets || [];
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">
{t('adminPage.content.media.title') || 'Media Library'}
</h1>
<p className="text-muted-foreground">
{t('adminPage.content.media.description') ||
'Manage uploaded media assets, images, and files'}
</p>
</div>
<Button onClick={() => navigate('/admin/content/media/new')}>
<Plus className="h-4 w-4 mr-2" />
{t('adminPage.content.media.newAsset') || 'Upload Media'}
</Button>
</div>
{/* Filters */}
<div className="flex gap-2">
<Button
variant={typeFilter === undefined ? 'default' : 'outline'}
onClick={() => setTypeFilter(undefined)}
>
{t('adminPage.content.announcements.all') || 'All'}
</Button>
<Button
variant={typeFilter === 'image' ? 'default' : 'outline'}
onClick={() => setTypeFilter('image')}
>
Images
</Button>
<Button
variant={typeFilter === 'video' ? 'default' : 'outline'}
onClick={() => setTypeFilter('video')}
>
Videos
</Button>
</div>
{/* Table */}
<DataTable
columns={columns}
data={assets}
getRowId={(asset) => asset.id}
isLoading={isLoading}
actions={actions}
pagination={
assets.length > 0
? {
currentPage,
totalPages: Math.ceil(assets.length / pageSize),
pageSize,
totalItems: assets.length,
onPageChange: setCurrentPage,
}
: undefined
}
emptyMessage={t('adminPage.content.media.noAssets') || 'No media assets found'}
emptyDescription={
t('adminPage.content.media.createFirst') || 'Upload your first media asset to get started'
}
emptyAction={
<Button onClick={() => navigate('/admin/content/media/new')}>
<Plus className="h-4 w-4 mr-2" />
{t('adminPage.content.media.newAsset') || 'Upload Media'}
</Button>
}
/>
</div>
);
};
export default MediaLibraryPage;