Files
MyTube/frontend/src/pages/ManagePage.tsx

311 lines
13 KiB
TypeScript

import {
ArrowBack,
Delete,
Folder,
Search,
VideoLibrary
} from '@mui/icons-material';
import {
Alert,
Box,
Button,
CircularProgress,
Container,
IconButton,
InputAdornment,
Pagination,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
Typography
} from '@mui/material';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import ConfirmationModal from '../components/ConfirmationModal';
import DeleteCollectionModal from '../components/DeleteCollectionModal';
import { useLanguage } from '../contexts/LanguageContext';
import { Collection, Video } from '../types';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
interface ManagePageProps {
videos: Video[];
onDeleteVideo: (id: string) => Promise<any>;
collections: Collection[];
onDeleteCollection: (id: string, deleteVideos: boolean) => Promise<any>;
}
const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collections = [], onDeleteCollection }) => {
const [searchTerm, setSearchTerm] = useState<string>('');
const { t } = useLanguage();
const [deletingId, setDeletingId] = useState<string | null>(null);
const [collectionToDelete, setCollectionToDelete] = useState<Collection | null>(null);
const [isDeletingCollection, setIsDeletingCollection] = useState<boolean>(false);
const [videoToDelete, setVideoToDelete] = useState<string | null>(null);
const [showVideoDeleteModal, setShowVideoDeleteModal] = useState<boolean>(false);
// Pagination state
const [collectionPage, setCollectionPage] = useState(1);
const [videoPage, setVideoPage] = useState(1);
const ITEMS_PER_PAGE = 10;
const filteredVideos = videos.filter(video =>
video.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
video.author.toLowerCase().includes(searchTerm.toLowerCase())
);
// Pagination logic
const totalCollectionPages = Math.ceil(collections.length / ITEMS_PER_PAGE);
const displayedCollections = collections.slice(
(collectionPage - 1) * ITEMS_PER_PAGE,
collectionPage * ITEMS_PER_PAGE
);
const totalVideoPages = Math.ceil(filteredVideos.length / ITEMS_PER_PAGE);
const displayedVideos = filteredVideos.slice(
(videoPage - 1) * ITEMS_PER_PAGE,
videoPage * ITEMS_PER_PAGE
);
const handleCollectionPageChange = (_: React.ChangeEvent<unknown>, value: number) => {
setCollectionPage(value);
};
const handleVideoPageChange = (_: React.ChangeEvent<unknown>, value: number) => {
setVideoPage(value);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const confirmDeleteVideo = async () => {
if (!videoToDelete) return;
setDeletingId(videoToDelete);
await onDeleteVideo(videoToDelete);
setDeletingId(null);
setVideoToDelete(null);
setShowVideoDeleteModal(false); // Close the modal after deletion
};
const handleDelete = (id: string) => {
setVideoToDelete(id);
setShowVideoDeleteModal(true);
};
const confirmDeleteCollection = (collection: Collection) => {
setCollectionToDelete(collection);
};
const handleCollectionDeleteOnly = async () => {
if (!collectionToDelete) return;
setIsDeletingCollection(true);
await onDeleteCollection(collectionToDelete.id, false);
setIsDeletingCollection(false);
setCollectionToDelete(null);
};
const handleCollectionDeleteAll = async () => {
if (!collectionToDelete) return;
setIsDeletingCollection(true);
await onDeleteCollection(collectionToDelete.id, true);
setIsDeletingCollection(false);
setCollectionToDelete(null);
};
const getThumbnailSrc = (video: Video) => {
if (video.thumbnailPath) {
return `${BACKEND_URL}${video.thumbnailPath}`;
}
return video.thumbnailUrl || 'https://via.placeholder.com/120x90?text=No+Thumbnail';
};
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Typography variant="h4" component="h1" fontWeight="bold">
{t('manageContent')}
</Typography>
<Button
component={Link}
to="/"
variant="outlined"
startIcon={<ArrowBack />}
>
{t('backToHome')}
</Button>
</Box>
<DeleteCollectionModal
isOpen={!!collectionToDelete}
onClose={() => !isDeletingCollection && setCollectionToDelete(null)}
onDeleteCollectionOnly={handleCollectionDeleteOnly}
onDeleteCollectionAndVideos={handleCollectionDeleteAll}
collectionName={collectionToDelete?.name || ''}
videoCount={collectionToDelete?.videos.length || 0}
/>
<ConfirmationModal
isOpen={showVideoDeleteModal}
onClose={() => {
setShowVideoDeleteModal(false);
setVideoToDelete(null);
}}
onConfirm={confirmDeleteVideo}
title={t('deleteVideo')}
message={t('confirmDelete')}
confirmText={t('delete')}
cancelText={t('cancel')}
isDanger={true}
/>
<Box sx={{ mb: 6 }}>
<Typography variant="h5" sx={{ mb: 2, display: 'flex', alignItems: 'center' }}>
<Folder sx={{ mr: 1, color: 'secondary.main' }} />
{t('collections')} ({collections.length})
</Typography>
{collections.length > 0 ? (
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell>{t('name')}</TableCell>
<TableCell>{t('videos')}</TableCell>
<TableCell>{t('created')}</TableCell>
<TableCell align="right">{t('actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayedCollections.map(collection => (
<TableRow key={collection.id} hover>
<TableCell component="th" scope="row" sx={{ fontWeight: 500 }}>
{collection.name}
</TableCell>
<TableCell>{collection.videos.length} videos</TableCell>
<TableCell>{new Date(collection.createdAt).toLocaleDateString()}</TableCell>
<TableCell align="right">
<Tooltip title={t('deleteCollection')}>
<IconButton
color="error"
onClick={() => confirmDeleteCollection(collection)}
size="small"
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Alert severity="info" variant="outlined">{t('noCollections')}</Alert>
)}
{totalCollectionPages > 1 && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Pagination
count={totalCollectionPages}
page={collectionPage}
onChange={handleCollectionPageChange}
color="secondary"
showFirstButton
showLastButton
/>
</Box>
)}
</Box>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5" sx={{ display: 'flex', alignItems: 'center' }}>
<VideoLibrary sx={{ mr: 1, color: 'primary.main' }} />
{t('videos')} ({filteredVideos.length})
</Typography>
<TextField
placeholder="Search videos..."
size="small"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
sx={{ width: 300 }}
/>
</Box>
{filteredVideos.length > 0 ? (
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell>{t('thumbnail')}</TableCell>
<TableCell>{t('title')}</TableCell>
<TableCell>{t('author')}</TableCell>
<TableCell align="right">{t('actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayedVideos.map(video => (
<TableRow key={video.id} hover>
<TableCell sx={{ width: 140 }}>
<Box
component="img"
src={getThumbnailSrc(video)}
alt={video.title}
sx={{ width: 120, height: 68, objectFit: 'cover', borderRadius: 1 }}
/>
</TableCell>
<TableCell sx={{ fontWeight: 500 }}>
{video.title}
</TableCell>
<TableCell>{video.author}</TableCell>
<TableCell align="right">
<Tooltip title={t('deleteVideo')}>
<IconButton
color="error"
onClick={() => handleDelete(video.id)}
disabled={deletingId === video.id}
>
{deletingId === video.id ? <CircularProgress size={24} /> : <Delete />}
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Alert severity="info" variant="outlined">{t('noVideosFoundMatching')}</Alert>
)}
</Box>
{totalVideoPages > 1 && (
<Box sx={{ mt: 2, mb: 4, display: 'flex', justifyContent: 'center' }}>
<Pagination
count={totalVideoPages}
page={videoPage}
onChange={handleVideoPageChange}
color="primary"
showFirstButton
showLastButton
/>
</Box>
)}
</Container>
);
};
export default ManagePage;