Версия 1.2.11 Доработка окна выбора авторов в админке.
This commit is contained in:
parent
9dd25faf8a
commit
593a620f29
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"private": true,
|
||||
"version": "1.2.10",
|
||||
"version": "1.2.11",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -6,6 +6,8 @@ Disallow: /admin/
|
||||
Disallow: /admin/*
|
||||
Disallow: /search
|
||||
Disallow: /search*
|
||||
Disallow: /bookmarks
|
||||
Disallow: /bookmarks*
|
||||
Disallow: /*?page=
|
||||
Disallow: /*?category=
|
||||
|
||||
|
||||
@ -26,10 +26,26 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
existingAuthors,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<AuthorRole>(AuthorRole.WRITER);
|
||||
const [selectedAuthorId, setSelectedAuthorId] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const itemsPerPage = 10;
|
||||
const itemsPerPage = 6;
|
||||
|
||||
// Обработчик нажатия клавиши Esc
|
||||
useEffect(() => {
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Используем useMemo для оптимальной фильтрации и сортировки авторов
|
||||
const { popularAuthors, otherAuthors } = useMemo(() => {
|
||||
@ -39,7 +55,7 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
!existingAuthors.some(existing => existing.authorId === author.id && existing.role === activeTab)
|
||||
);
|
||||
|
||||
// Сортируем по полю order (по возрастанию) и берем первые 6
|
||||
// Сортируем по полю order (по возрастанию) и берем первые 5
|
||||
const sortedByOrder = [...filteredAuthors].sort((a, b) => a.order - b.order);
|
||||
const popularAuthors = sortedByOrder.slice(0, 6);
|
||||
|
||||
@ -51,6 +67,20 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
return { popularAuthors, otherAuthors };
|
||||
}, [authors, existingAuthors, activeTab]);
|
||||
|
||||
// Получаем уникальные первые буквы имен остальных авторов
|
||||
const firstLetters = useMemo(() => {
|
||||
const letters = new Set<string>();
|
||||
|
||||
otherAuthors.forEach(author => {
|
||||
// Берем первую букву имени, приводим к верхнему регистру
|
||||
const firstLetter = author.displayName.charAt(0).toUpperCase();
|
||||
letters.add(firstLetter);
|
||||
});
|
||||
|
||||
// Преобразуем Set в массив и сортируем
|
||||
return Array.from(letters).sort();
|
||||
}, [otherAuthors]);
|
||||
|
||||
// Фильтруем остальных авторов по поисковому запросу
|
||||
const filteredOtherAuthors = useMemo(() => {
|
||||
if (!searchTerm.trim()) {
|
||||
@ -65,27 +95,17 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
|
||||
// Вычисляем данные для пагинации только для отфильтрованных остальных авторов
|
||||
const paginationData = useMemo(() => {
|
||||
const showPagination = filteredOtherAuthors.length > itemsPerPage;
|
||||
|
||||
// Если пагинация не нужна, показываем всех отфильтрованных остальных авторов
|
||||
if (!showPagination) {
|
||||
return {
|
||||
showPagination: false,
|
||||
displayedOtherAuthors: filteredOtherAuthors,
|
||||
totalPages: 1
|
||||
};
|
||||
}
|
||||
|
||||
// Если пагинация нужна
|
||||
const totalPages = Math.ceil(filteredOtherAuthors.length / itemsPerPage);
|
||||
const totalAuthors = filteredOtherAuthors.length;
|
||||
const totalPages = Math.ceil(totalAuthors / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const displayedOtherAuthors = filteredOtherAuthors.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
showPagination: true,
|
||||
totalAuthors,
|
||||
totalPages,
|
||||
displayedOtherAuthors,
|
||||
totalPages
|
||||
showPagination: totalPages > 1
|
||||
};
|
||||
}, [filteredOtherAuthors, currentPage, itemsPerPage]);
|
||||
|
||||
@ -94,19 +114,16 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
setCurrentPage(1);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleAddAuthor = () => {
|
||||
if (selectedAuthorId) {
|
||||
onAddAuthor(selectedAuthorId, activeTab);
|
||||
setSelectedAuthorId(null);
|
||||
onClose();
|
||||
}
|
||||
// Обработчик выбора автора
|
||||
const handleAuthorSelect = (authorId: string) => {
|
||||
onAddAuthor(authorId, activeTab);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleTabChange = (role: AuthorRole) => {
|
||||
setActiveTab(role);
|
||||
setCurrentPage(1);
|
||||
setSearchTerm('');
|
||||
setSelectedAuthorId(null);
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
@ -123,9 +140,13 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
setSearchTerm('');
|
||||
};
|
||||
|
||||
const handleLetterClick = (letter: string) => {
|
||||
setSearchTerm(letter.toLowerCase());
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const { showPagination, displayedOtherAuthors, totalPages } = paginationData;
|
||||
const { totalAuthors, totalPages, displayedOtherAuthors, showPagination } = paginationData;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
@ -174,7 +195,7 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{/* Популярные авторы (всегда отображаются, если есть) */}
|
||||
{popularAuthors.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div key={`popular-${activeTab}`} className="mb-8">
|
||||
<div className="flex items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-700">Популярные</h3>
|
||||
<span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full">
|
||||
@ -191,8 +212,7 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
<AuthorCard
|
||||
key={author.id}
|
||||
author={author}
|
||||
isSelected={selectedAuthorId === author.id}
|
||||
onSelect={() => setSelectedAuthorId(author.id)}
|
||||
onSelect={() => handleAuthorSelect(author.id)}
|
||||
showOrder={true}
|
||||
/>
|
||||
</CSSTransition>
|
||||
@ -202,10 +222,10 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
)}
|
||||
|
||||
{/* Все остальные авторы с поиском и пагинацией */}
|
||||
<div id="other-authors-section">
|
||||
<div id="other-authors-section" key={`other-${activeTab}`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-700">
|
||||
Остальные {roleLabels[activeTab].toLowerCase()}
|
||||
Все {roleLabels[activeTab].toLowerCase()}
|
||||
</h3>
|
||||
{showPagination && (
|
||||
<span className="text-sm text-gray-500">
|
||||
@ -214,6 +234,37 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Кнопки с первыми буквами */}
|
||||
{firstLetters.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={clearSearch}
|
||||
className={`px-3 py-1.5 text-sm rounded-full ${
|
||||
searchTerm === ''
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
{firstLetters.map(letter => (
|
||||
<button
|
||||
key={letter}
|
||||
onClick={() => handleLetterClick(letter)}
|
||||
className={`px-3 py-1.5 text-sm rounded-full ${
|
||||
searchTerm.toLowerCase() === letter.toLowerCase()
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{letter}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Поле поиска */}
|
||||
<div className="mb-6">
|
||||
<div className="relative">
|
||||
@ -238,7 +289,7 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
</div>
|
||||
{searchTerm && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Найдено: {filteredOtherAuthors.length} {filteredOtherAuthors.length === 1 ? 'автор' : 'авторов'}
|
||||
Найдено: {totalAuthors} {totalAuthors === 1 ? 'автор' : 'авторов'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -250,15 +301,14 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
<AuthorCard
|
||||
key={author.id}
|
||||
author={author}
|
||||
isSelected={selectedAuthorId === author.id}
|
||||
onSelect={() => setSelectedAuthorId(author.id)}
|
||||
onSelect={() => handleAuthorSelect(author.id)}
|
||||
showOrder={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Пагинация */}
|
||||
{showPagination && totalPages > 1 && (
|
||||
{showPagination && (
|
||||
<div className="flex justify-center mt-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
@ -312,47 +362,23 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопки действий */}
|
||||
<div className="flex justify-end space-x-3 p-6 border-t">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddAuthor}
|
||||
disabled={!selectedAuthorId}
|
||||
className={`px-4 py-2 rounded-md text-white ${
|
||||
selectedAuthorId
|
||||
? 'bg-blue-600 hover:bg-blue-700'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Компонент карточки автора (без изменений)
|
||||
// Обновленный компонент карточки автора
|
||||
interface AuthorCardProps {
|
||||
author: Author;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
showOrder: boolean;
|
||||
}
|
||||
|
||||
const AuthorCard: React.FC<AuthorCardProps> = ({ author, isSelected, onSelect, showOrder }) => {
|
||||
const AuthorCard: React.FC<AuthorCardProps> = ({ author, onSelect, showOrder }) => {
|
||||
return (
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-blue-500 bg-blue-50 shadow-sm'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all hover:border-blue-500 hover:bg-blue-50 hover:shadow-md ${
|
||||
showOrder ? 'border-gray-200' : 'border-gray-200'
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user