russ_react/src/pages/AuthorManagementPage.tsx

583 lines
26 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 React, { useState } from 'react';
import { Header } from '../components/Header';
import { AuthGuard } from '../components/AuthGuard';
import { AuthorFormData } from '../types/auth';
import { useAuthorManagement } from '../hooks/useAuthorManagement';
import {
ImagePlus,
X,
UserPlus,
Pencil,
Link as LinkIcon,
Link2Off as LinkOff,
ToggleLeft,
ToggleRight,
Trash2, ChevronDown, ChevronUp
} from 'lucide-react';
import { imageResolutions } from '../config/imageResolutions';
import { useAuthStore } from '../stores/authStore';
import axios from "axios";
const initialFormData: AuthorFormData = {
id: '',
displayName: '',
email: '',
bio: '',
avatarUrl: '/images/avatar.jpg',
order: 0,
okUrl: '',
vkUrl: '',
websiteUrl: '',
articlesCount: 0,
isActive: true
};
export function AuthorManagementPage() {
const {
authors,
users,
selectedAuthor,
loading,
error,
setSelectedAuthor,
createAuthor,
updateAuthor,
linkUser,
unlinkUser,
orderMoveUp,
orderMoveDown,
toggleActive,
deleteAuthor,
fetchAuthors,
fetchUsers,
} = useAuthorManagement();
const { user } = useAuthStore();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showLinkUserModal, setShowLinkUserModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState<string | null>(null);
const [formData, setFormData] = useState<AuthorFormData>(initialFormData);
const [formError, setFormError] = useState<string | null>(null);
const [selectedUserId, setSelectedUserId] = useState<string>('');
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>, authorId: string) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const resolution = imageResolutions.find(r => r.id === 'thumbnail');
if (!resolution) throw new Error('Invalid resolution');
const formData = new FormData();
formData.append('file', file);
formData.append('resolutionId', resolution.id);
formData.append('folder', 'authors/' + authorId);
const response = await axios.post('/api/images/upload-url', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': `Bearer ${localStorage.getItem('token')}`, // Передача токена аутентификации
},
});
setFormData(prev => ({ ...prev, avatarUrl: response.data?.fileUrl }));
} catch (error) {
setFormError('Ошибка загрузки аватара. Повторите попытку.');
console.error('Ошибка загрузки:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
try {
if (showCreateModal) {
await createAuthor(formData);
setShowCreateModal(false);
} else if (showEditModal && selectedAuthor) {
await updateAuthor(selectedAuthor.id, formData);
setShowEditModal(false);
await fetchAuthors();
}
setFormData(initialFormData);
} catch (error) {
setFormError(error instanceof Error ? error.message : 'An error occurred');
}
};
const handleLinkUser = async (authorId: string, userId: string) => {
if (!selectedAuthor || !selectedUserId) return;
try {
await linkUser(authorId, userId);
setShowLinkUserModal(false);
setSelectedUserId('');
await fetchAuthors();
await fetchUsers();
} catch {
setFormError('Failed to link user to author');
}
};
const handleUnlinkUser = async (authorId: string) => {
try {
await unlinkUser(authorId);
await fetchAuthors();
await fetchUsers();
} catch {
setFormError('Failed to unlink user from author');
}
};
const handleMoveUp = async (authorId: string) => {
await orderMoveUp(authorId);
fetchAuthors();
};
const handleMoveDown = async (authorId: string) => {
await orderMoveDown(authorId);
fetchAuthors();
};
const handleToggleActive = async (authorId: string, isActive: boolean) => {
try {
await toggleActive(authorId, isActive);
await fetchAuthors();
} catch {
setFormError('Failed to toggle author status');
}
};
const handleDeleteAuthor = async (authorId: string) => {
try {
await deleteAuthor(authorId);
setShowDeleteModal(null)
} catch {
setFormError('Failed to delete author');
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<AuthGuard>
<div className="min-h-screen bg-gray-50">
<Header />
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold text-gray-900">
Управление авторами
</h1>
<button
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
<UserPlus className="h-5 w-5 mr-2" />
Добавить автора
</button>
</div>
{error && (
<div className="mb-8 bg-red-50 text-red-700 p-4 rounded-md">
{error}
</div>
)}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{authors.map((author, index) => (
<li key={author.id} className="px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<img
src={author.avatarUrl || '/images/avatar.jpg'}
alt={author.displayName}
className="h-10 w-10 rounded-full"
/>
<div className="ml-4">
<h2 className="text-lg font-medium text-gray-900">
{author.displayName} {author.userId ? `=> пользователь ${author.displayName}` : ``}
</h2>
<p className="text-sm text-gray-500">{author.bio}</p>
<div className="flex mt-1 space-x-4">
{author.okUrl && (
<a
href={author.okUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-500 hover:text-blue-500"
>
Одноклассники
</a>
)}
{author.vkUrl && (
<a
href={author.vkUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-500 hover:text-pink-500"
>
Вконтакте
</a>
)}
{author.websiteUrl && (
<a
href={author.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-500 hover:text-gray-700"
>
Website
</a>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex flex-col items-center space-y-1">
<button
onClick={() => handleMoveUp(author.id)}
disabled={index === 0}
className="p-1 text-gray-400 hover:text-blue-500 disabled:opacity-30"
title="Переместить вверх"
>
<ChevronUp size={18} />
</button>
<button
onClick={() => handleMoveDown(author.id)}
disabled={index === authors.length - 1}
className="p-1 text-gray-400 hover:text-blue-500 disabled:opacity-30"
title="Переместить вниз"
>
<ChevronDown size={18} />
</button>
</div>
<button
onClick={() => handleToggleActive(author.id, !author.isActive)}
className={`p-2 rounded-full hover:bg-gray-100 ${
author.isActive ? 'text-green-600' : 'text-gray-400'
}`}
title={author.isActive ? 'Деактивировать автора' : 'Активировать автора'}
>
{author.isActive ? <ToggleRight size={20} /> : <ToggleLeft size={20} />}
</button>
{author.userId ? (
<button
onClick={() => handleUnlinkUser(author.id)}
className="p-2 rounded-full hover:bg-gray-100 text-gray-400"
>
<LinkOff size={20} />
</button>
) : (
<button
onClick={() => {
setSelectedAuthor(author);
setShowLinkUserModal(true);
}}
className="p-2 rounded-full hover:bg-gray-100 text-gray-400"
>
<LinkIcon size={20} />
</button>
)}
<button
onClick={() => {
setSelectedAuthor(author);
setFormData({
id: author.id,
displayName: author.displayName,
email: author.email || '',
bio: author.bio || '',
avatarUrl: author.avatarUrl || '',
order: author.order,
okUrl: author.okUrl || '',
vkUrl: author.vkUrl || '',
websiteUrl: author.websiteUrl || '',
articlesCount: author.articlesCount,
isActive: author.isActive || true
});
setShowEditModal(true);
}}
className="p-2 rounded-full hover:bg-gray-100 text-gray-400"
>
<Pencil size={20} />
</button>
<button
onClick={() => setShowDeleteModal(author.id)}
className="p-2 rounded-full hover:bg-gray-100 text-red-400"
>
<Trash2 size={20} />
</button>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
</main>
{/* Create/Edit Author Modal */}
{((showCreateModal || showEditModal) && user?.permissions.isAdmin ) && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">
{showCreateModal ? 'Новый автор' : 'Изменить автора'}
</h3>
<button
onClick={() => {
setShowCreateModal(false);
setShowEditModal(false);
setFormData(initialFormData);
setFormError(null);
}}
className="text-gray-400 hover:text-gray-500"
>
<X size={20} />
</button>
</div>
{formError && (
<div className="mb-8 bg-red-50 text-red-700 p-4 rounded-md">
{formError}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Аватар
</label>
<div className="mt-1 flex items-center space-x-4">
<img
src={formData.avatarUrl || 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=150&h=150'}
alt="Author avatar"
className="h-12 w-12 rounded-full object-cover"
/>
<label className="cursor-pointer inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<ImagePlus size={16} className="mr-2" />
Сменить Аватар
<input
type="file"
className="hidden"
accept="image/*"
onChange={(e) => handleAvatarUpload(e, formData.id)} // Передаем authorId
/>
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">Имя</span> <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.displayName}
onChange={(e) => setFormData(prev => ({ ...prev, displayName: e.target.value }))}
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">Биографические данные</span>
</label>
<textarea
value={formData.bio}
onChange={(e) => setFormData(prev => ({ ...prev, bio: e.target.value }))}
rows={3}
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">Одноклассники URL</span>
</label>
<input
type="url"
value={formData.okUrl}
onChange={(e) => setFormData(prev => ({ ...prev, okUrl: e.target.value }))}
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
placeholder="https://twitter.com/username"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">E-mail</span>
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
placeholder="author@email.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">Вконтакте URL</span>
</label>
<input
type="url"
value={formData.vkUrl}
onChange={(e) => setFormData(prev => ({ ...prev, vkUrl: e.target.value }))}
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
placeholder="https://instagram.com/username"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
<span className="italic font-bold">Сайт URL</span>
</label>
<input
type="url"
value={formData.websiteUrl}
onChange={(e) => setFormData(prev => ({ ...prev, websiteUrl: e.target.value }))}
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
placeholder="https://example.com"
/>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={() => {
setShowCreateModal(false);
setShowEditModal(false);
setFormData(initialFormData);
setFormError(null);
}}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Отмена
</button>
<button
type="submit"
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
{showCreateModal ? 'Создать' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Link User Modal */}
{showLinkUserModal && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">
Связать автора с пользователем
</h3>
<button
onClick={() => {
setShowLinkUserModal(false);
setSelectedUserId('');
}}
className="text-gray-400 hover:text-gray-500"
>
<X size={20} />
</button>
</div>
<div className="space-y-4">
<div>
<select
value={selectedUserId}
onChange={(e) => setSelectedUserId(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
<option value="">Выберите пользователя...</option>
{users
.filter(u => !u.isLinkedToAuthor) // Only show users not already linked to an author
.map(user => (
<option key={user.id} value={user.id}>
{user.displayName})
</option>
))
}
</select>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={() => {
setShowLinkUserModal(false);
setSelectedUserId('');
}}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Отмена
</button>
<button
type="button"
onClick={() => {
if (selectedAuthor && selectedUserId) {
handleLinkUser(selectedAuthor.id, selectedUserId);
}
}}
disabled={!selectedUserId}
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Связать
</button>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Удалить автора
</h3>
<p className="text-sm text-gray-500 mb-6">
Вы уверены, что хотите удалить этого автора? Это действие невозможно отменить.
Все статьи, связанные с этим автором, останутся, но у них больше не будет активного автора.
</p>
<div className="flex justify-end gap-4">
<button
onClick={() => setShowDeleteModal(null)}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Отмена
</button>
<button
onClick={() => handleDeleteAuthor(showDeleteModal)}
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700"
>
Удалить
</button>
</div>
</div>
</div>
)}
</div>
</AuthGuard>
);
}