Версия 1.2.11 Доработка окна выбора авторов в админке.

This commit is contained in:
anibilag 2025-11-09 23:13:50 +03:00
parent 9dd25faf8a
commit 593a620f29
3 changed files with 92 additions and 64 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "vite-react-typescript-starter", "name": "vite-react-typescript-starter",
"private": true, "private": true,
"version": "1.2.10", "version": "1.2.11",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -6,6 +6,8 @@ Disallow: /admin/
Disallow: /admin/* Disallow: /admin/*
Disallow: /search Disallow: /search
Disallow: /search* Disallow: /search*
Disallow: /bookmarks
Disallow: /bookmarks*
Disallow: /*?page= Disallow: /*?page=
Disallow: /*?category= Disallow: /*?category=

View File

@ -26,10 +26,26 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
existingAuthors, existingAuthors,
}) => { }) => {
const [activeTab, setActiveTab] = useState<AuthorRole>(AuthorRole.WRITER); const [activeTab, setActiveTab] = useState<AuthorRole>(AuthorRole.WRITER);
const [selectedAuthorId, setSelectedAuthorId] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState(''); 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 для оптимальной фильтрации и сортировки авторов // Используем useMemo для оптимальной фильтрации и сортировки авторов
const { popularAuthors, otherAuthors } = useMemo(() => { const { popularAuthors, otherAuthors } = useMemo(() => {
@ -39,7 +55,7 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
!existingAuthors.some(existing => existing.authorId === author.id && existing.role === activeTab) !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 sortedByOrder = [...filteredAuthors].sort((a, b) => a.order - b.order);
const popularAuthors = sortedByOrder.slice(0, 6); const popularAuthors = sortedByOrder.slice(0, 6);
@ -51,6 +67,20 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
return { popularAuthors, otherAuthors }; return { popularAuthors, otherAuthors };
}, [authors, existingAuthors, activeTab]); }, [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(() => { const filteredOtherAuthors = useMemo(() => {
if (!searchTerm.trim()) { if (!searchTerm.trim()) {
@ -65,27 +95,17 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
// Вычисляем данные для пагинации только для отфильтрованных остальных авторов // Вычисляем данные для пагинации только для отфильтрованных остальных авторов
const paginationData = useMemo(() => { const paginationData = useMemo(() => {
const showPagination = filteredOtherAuthors.length > itemsPerPage; const totalAuthors = filteredOtherAuthors.length;
const totalPages = Math.ceil(totalAuthors / itemsPerPage);
// Если пагинация не нужна, показываем всех отфильтрованных остальных авторов
if (!showPagination) {
return {
showPagination: false,
displayedOtherAuthors: filteredOtherAuthors,
totalPages: 1
};
}
// Если пагинация нужна
const totalPages = Math.ceil(filteredOtherAuthors.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage; const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage; const endIndex = startIndex + itemsPerPage;
const displayedOtherAuthors = filteredOtherAuthors.slice(startIndex, endIndex); const displayedOtherAuthors = filteredOtherAuthors.slice(startIndex, endIndex);
return { return {
showPagination: true, totalAuthors,
totalPages,
displayedOtherAuthors, displayedOtherAuthors,
totalPages showPagination: totalPages > 1
}; };
}, [filteredOtherAuthors, currentPage, itemsPerPage]); }, [filteredOtherAuthors, currentPage, itemsPerPage]);
@ -94,19 +114,16 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
setCurrentPage(1); setCurrentPage(1);
}, [searchTerm]); }, [searchTerm]);
const handleAddAuthor = () => { // Обработчик выбора автора
if (selectedAuthorId) { const handleAuthorSelect = (authorId: string) => {
onAddAuthor(selectedAuthorId, activeTab); onAddAuthor(authorId, activeTab);
setSelectedAuthorId(null);
onClose(); onClose();
}
}; };
const handleTabChange = (role: AuthorRole) => { const handleTabChange = (role: AuthorRole) => {
setActiveTab(role); setActiveTab(role);
setCurrentPage(1); setCurrentPage(1);
setSearchTerm(''); setSearchTerm('');
setSelectedAuthorId(null);
}; };
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
@ -123,9 +140,13 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
setSearchTerm(''); setSearchTerm('');
}; };
const handleLetterClick = (letter: string) => {
setSearchTerm(letter.toLowerCase());
};
if (!isOpen) return null; if (!isOpen) return null;
const { showPagination, displayedOtherAuthors, totalPages } = paginationData; const { totalAuthors, totalPages, displayedOtherAuthors, showPagination } = paginationData;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <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"> <div className="flex-1 overflow-y-auto p-6">
{/* Популярные авторы (всегда отображаются, если есть) */} {/* Популярные авторы (всегда отображаются, если есть) */}
{popularAuthors.length > 0 && ( {popularAuthors.length > 0 && (
<div className="mb-8"> <div key={`popular-${activeTab}`} className="mb-8">
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
<h3 className="text-lg font-semibold text-gray-700">Популярные</h3> <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"> <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 <AuthorCard
key={author.id} key={author.id}
author={author} author={author}
isSelected={selectedAuthorId === author.id} onSelect={() => handleAuthorSelect(author.id)}
onSelect={() => setSelectedAuthorId(author.id)}
showOrder={true} showOrder={true}
/> />
</CSSTransition> </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"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700"> <h3 className="text-lg font-semibold text-gray-700">
Остальные {roleLabels[activeTab].toLowerCase()} Все {roleLabels[activeTab].toLowerCase()}
</h3> </h3>
{showPagination && ( {showPagination && (
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
@ -214,6 +234,37 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
)} )}
</div> </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="mb-6">
<div className="relative"> <div className="relative">
@ -238,7 +289,7 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
</div> </div>
{searchTerm && ( {searchTerm && (
<p className="mt-2 text-sm text-gray-500"> <p className="mt-2 text-sm text-gray-500">
Найдено: {filteredOtherAuthors.length} {filteredOtherAuthors.length === 1 ? 'автор' : 'авторов'} Найдено: {totalAuthors} {totalAuthors === 1 ? 'автор' : 'авторов'}
</p> </p>
)} )}
</div> </div>
@ -250,15 +301,14 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
<AuthorCard <AuthorCard
key={author.id} key={author.id}
author={author} author={author}
isSelected={selectedAuthorId === author.id} onSelect={() => handleAuthorSelect(author.id)}
onSelect={() => setSelectedAuthorId(author.id)}
showOrder={false} showOrder={false}
/> />
))} ))}
</div> </div>
{/* Пагинация */} {/* Пагинация */}
{showPagination && totalPages > 1 && ( {showPagination && (
<div className="flex justify-center mt-8"> <div className="flex justify-center mt-8">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
@ -312,47 +362,23 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
)} )}
</div> </div>
</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>
</div> </div>
); );
}; };
// Компонент карточки автора (без изменений) // Обновленный компонент карточки автора
interface AuthorCardProps { interface AuthorCardProps {
author: Author; author: Author;
isSelected: boolean;
onSelect: () => void; onSelect: () => void;
showOrder: boolean; showOrder: boolean;
} }
const AuthorCard: React.FC<AuthorCardProps> = ({ author, isSelected, onSelect, showOrder }) => { const AuthorCard: React.FC<AuthorCardProps> = ({ author, onSelect, showOrder }) => {
return ( return (
<div <div
className={`border rounded-lg p-4 cursor-pointer transition-all ${ className={`border rounded-lg p-4 cursor-pointer transition-all hover:border-blue-500 hover:bg-blue-50 hover:shadow-md ${
isSelected showOrder ? 'border-gray-200' : 'border-gray-200'
? 'border-blue-500 bg-blue-50 shadow-sm'
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
}`} }`}
onClick={onSelect} onClick={onSelect}
> >