CampFireCritics/src/pages/AdminMediaPage.jsx
2025-05-21 11:05:20 +03:00

325 lines
14 KiB
JavaScript
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 React, { useState, useEffect, useMemo } from 'react';
import { Link } from 'react-router-dom';
import {
listMedia,
deleteMedia,
getFileUrl, // Import getFileUrl
mediaTypes // Import mediaTypes
} from '../services/pocketbaseService';
import { useAuth } from '../contexts/AuthContext';
import Modal from '../components/Modal';
import MediaForm from '../components/admin/MediaForm'; // Import MediaForm
const AdminMediaPage = () => {
const [media, setMedia] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentMedia, setCurrentMedia] = useState(null); // For editing
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [filterType, setFilterType] = useState(''); // State for type filter
const [filterPublished, setFilterPublished] = useState(''); // State for published filter
const [sortField, setSortField] = useState('-created'); // State for sorting
const { userProfile } = useAuth();
const isAdminOrCritic = userProfile && (userProfile.role === 'admin' || userProfile.is_critic === true);
// Define sort options
const sortOptions = useMemo(() => [
{ value: '-created', label: 'Дата создания (новые)' },
{ value: 'created', label: 'Дата создания (старые)' },
{ value: '-title', label: 'Название (Я-А)' },
{ value: 'title', label: 'Название (А-Я)' },
{ value: '-average_rating', label: 'Рейтинг (убыв.)' },
{ value: 'average_rating', label: 'Рейтинг (возр.)' },
{ value: '-review_count', label: 'Рецензии (убыв.)' },
{ value: 'review_count', label: 'Рецензии (возр.)' },
], []);
const fetchMedia = async (currentPage, type, published, sort) => {
setLoading(true);
setError(null);
try {
// Pass filter and sort parameters to listMedia
const publishedFilter = published === '' ? null : published === 'true'; // Convert string to boolean or null
const { data, totalPages } = await listMedia(type || null, currentPage, 20, userProfile, false, publishedFilter, sort);
setMedia(data);
setTotalPages(totalPages);
} catch (err) {
console.error("Error fetching media:", err);
setError("Не удалось загрузить список контента.");
} finally {
setLoading(false);
}
};
useEffect(() => {
// Fetch media when page, filter, or sort changes
fetchMedia(page, filterType, filterPublished, sortField);
}, [page, filterType, filterPublished, sortField, userProfile]); // Add userProfile as dependency
const handleDelete = async (id) => {
if (window.confirm("Вы уверены, что хотите удалить этот контент?")) {
try {
await deleteMedia(id);
// Refresh the list after deletion
fetchMedia(page, filterType, filterPublished, sortField);
} catch (err) {
console.error("Error deleting media:", err);
setError("Не удалось удалить контент.");
}
}
};
const handleCreateClick = () => {
setCurrentMedia(null); // Clear currentMedia for creation
setIsModalOpen(true);
};
const handleEditClick = (mediaItem) => {
setCurrentMedia(mediaItem); // Set mediaItem for editing
setIsModalOpen(true);
};
const handleModalClose = () => {
setIsModalOpen(false);
setCurrentMedia(null); // Clear currentMedia
// Refresh the list after modal close (assuming create/edit happened)
fetchMedia(page, filterType, filterPublished, sortField);
};
if (!isAdminOrCritic) {
return (
<div className="min-h-screen bg-campfire-dark pt-20 flex justify-center items-center text-campfire-light">
<p>У вас нет прав для просмотра этой страницы.</p>
</div>
);
}
if (loading) {
return (
<div className="min-h-screen bg-campfire-dark pt-20 flex justify-center items-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-campfire-amber"></div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-campfire-dark pt-20 flex justify-center items-center">
<div className="bg-status-error/20 text-status-error p-4 rounded-lg text-center">
{error}
</div>
</div>
);
}
return (
<div className="min-h-screen bg-campfire-dark pt-20">
<div className="container-custom py-12">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-campfire-light">
Управление <span className="font-semibold text-campfire-amber">контентом</span>
</h1>
<button onClick={handleCreateClick} className="btn-primary">
Добавить контент
</button>
</div>
{/* Filter and Sort Controls */}
<div className="mb-8 grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label htmlFor="filterType" className="block text-sm font-medium text-campfire-light mb-1">
Фильтр по типу
</label>
<select
id="filterType"
value={filterType}
onChange={(e) => { setFilterType(e.target.value); setPage(1); }} // Reset page on filter change
className="input w-full px-3 py-2 bg-campfire-dark text-campfire-light border border-campfire-ash/30 rounded-md focus:outline-none focus:ring-campfire-amber focus:border-campfire-amber transition-colors"
>
<option value="">Все типы</option>
{Object.entries(mediaTypes).map(([key, value]) => (
<option key={key} value={key}>{value.label}</option>
))}
</select>
</div>
<div>
<label htmlFor="filterPublished" className="block text-sm font-medium text-campfire-light mb-1">
Фильтр по статусу публикации
</label>
<select
id="filterPublished"
value={filterPublished}
onChange={(e) => { setFilterPublished(e.target.value); setPage(1); }} // Reset page on filter change
className="input w-full px-3 py-2 bg-campfire-dark text-campfire-light border border-campfire-ash/30 rounded-md focus:outline-none focus:ring-campfire-amber focus:border-campfire-amber transition-colors"
>
<option value="">Все статусы</option>
<option value="true">Опубликовано</option>
<option value="false">Не опубликовано</option>
</select>
</div>
<div>
<label htmlFor="sortField" className="block text-sm font-medium text-campfire-light mb-1">
Сортировка
</label>
<select
id="sortField"
value={sortField}
onChange={(e) => { setSortField(e.target.value); setPage(1); }} // Reset page on sort change
className="input w-full px-3 py-2 bg-campfire-dark text-campfire-light border border-campfire-ash/30 rounded-md focus:outline-none focus:ring-campfire-amber focus:border-campfire-amber transition-colors"
>
{sortOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
</div>
{media.length > 0 ? (
<div className="overflow-x-auto bg-campfire-charcoal rounded-lg shadow-md border border-campfire-ash/20">
<table className="min-w-full divide-y divide-campfire-ash/20">
<thead className="bg-campfire-dark">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-campfire-ash uppercase tracking-wider"
>
Постер
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-campfire-ash uppercase tracking-wider"
>
Название
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-campfire-ash uppercase tracking-wider"
>
Тип
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-campfire-ash uppercase tracking-wider"
>
Опубликовано
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-campfire-ash uppercase tracking-wider"
>
Популярное
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-campfire-ash uppercase tracking-wider"
>
Рейтинг
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-campfire-ash uppercase tracking-wider"
>
Рецензии
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Действия</span>
</th>
</tr>
</thead>
<tbody className="bg-campfire-charcoal divide-y divide-campfire-ash/20 text-campfire-light">
{media.map((item) => (
<tr key={item.id}>
<td className="px-6 py-4 whitespace-nowrap">
<img
src={getFileUrl(item, 'poster', { thumb: '50x50' })} // Use getFileUrl
alt={item.title}
className="h-10 w-10 rounded object-cover"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-campfire-light">
<Link to={`/media/${item.path}`} className="hover:text-campfire-amber transition-colors">
{item.title}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-campfire-ash">
{mediaTypes[item.type]?.label || item.type}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-campfire-ash">
{item.is_published ? 'Да' : 'Нет'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-campfire-ash">
{item.is_popular ? 'Да' : 'Нет'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-campfire-ash">
{item.average_rating !== null && item.average_rating !== undefined ? item.average_rating.toFixed(1) : 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-campfire-ash">
{item.review_count !== null && item.review_count !== undefined ? item.review_count : 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleEditClick(item)}
className="text-campfire-amber hover:text-campfire-ember mr-4 transition-colors"
>
Редактировать
</button>
<button
onClick={() => handleDelete(item.id)}
className="text-status-error hover:text-red-700 transition-colors"
>
Удалить
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-12 text-campfire-ash text-lg">
Нет доступного контента.
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center mt-8 space-x-4">
<button
onClick={() => setPage(prev => Math.max(prev - 1, 1))}
disabled={page === 1}
className="btn-secondary disabled:opacity-50 disabled:cursor-not-allowed"
>
Предыдущая
</button>
<span className="text-campfire-light text-lg font-medium">
Страница {page} из {totalPages}
</span>
<button
onClick={() => setPage(prev => Math.min(prev + 1, totalPages))}
disabled={page === totalPages}
className="btn-secondary disabled:opacity-50 disabled:cursor-not-allowed"
>
Следующая
</button>
</div>
)}
{/* Media Create/Edit Modal */}
<Modal isOpen={isModalOpen} onClose={handleModalClose}>
<MediaForm media={currentMedia} onSuccess={handleModalClose} />
</Modal>
</div>
</div>
);
};
export default AdminMediaPage;