Версия 1.2.11 Доработка окна выбора авторов в админке.
This commit is contained in:
parent
9dd25faf8a
commit
593a620f29
@ -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",
|
||||||
|
|||||||
@ -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=
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user