turash/bugulma/frontend/pages/admin/MediaLibraryPage.tsx
Damir Mukimov 673e8d4361
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
fix: resolve all frontend lint errors (85 issues fixed)
- Replace all 'any' types with proper TypeScript interfaces
- Fix React hooks setState in useEffect issues with lazy initialization
- Remove unused variables and imports across all files
- Fix React Compiler memoization dependency issues
- Add comprehensive i18n translation keys for admin interfaces
- Apply consistent prettier formatting throughout codebase
- Clean up unused bulk editing functionality
- Improve type safety and code quality across frontend

Files changed: 39
- ImpactMetrics.tsx: Fixed any types and interfaces
- AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars
- LocalizationUIPage.tsx: Fixed memoization, added translations
- LocalizationDataPage.tsx: Added type safety and translations
- And 35+ other files with various lint fixes
2025-12-25 14:14:58 +01:00

190 lines
6.3 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')}
>
{t('admin.media.filter.images')}
</Button>
<Button
variant={typeFilter === 'video' ? 'default' : 'outline'}
onClick={() => setTypeFilter('video')}
>
{t('admin.media.filter.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;