Версия 1.2.0 Полностью переделано окно выбора авторов при редактировании статей.
This commit is contained in:
parent
d6b4bfdb2c
commit
d8d57b71a9
52
package-lock.json
generated
52
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-react-typescript-starter",
|
"name": "vite-react-typescript-starter",
|
||||||
"version": "0.0.0",
|
"version": "1.1.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vite-react-typescript-starter",
|
"name": "vite-react-typescript-starter",
|
||||||
"version": "0.0.0",
|
"version": "1.1.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.525.0",
|
"@aws-sdk/client-s3": "^3.525.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.525.0",
|
"@aws-sdk/s3-request-presigner": "^3.525.0",
|
||||||
@ -35,6 +35,7 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"sitemap": "^8.0.0",
|
"sitemap": "^8.0.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
@ -52,6 +53,7 @@
|
|||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@types/winston": "^2.4.4",
|
"@types/winston": "^2.4.4",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
@ -1203,6 +1205,15 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||||
@ -3977,6 +3988,16 @@
|
|||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-transition-group": {
|
||||||
|
"version": "4.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||||
|
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/sax": {
|
"node_modules/@types/sax": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
|
||||||
@ -4977,7 +4998,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
@ -5063,6 +5083,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@ -7640,6 +7670,22 @@
|
|||||||
"react-dom": ">=16.8"
|
"react-dom": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-react-typescript-starter",
|
"name": "vite-react-typescript-starter",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.5",
|
"version": "1.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -44,6 +44,7 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"sitemap": "^8.0.0",
|
"sitemap": "^8.0.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
@ -61,6 +62,7 @@
|
|||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@types/winston": "^2.4.4",
|
"@types/winston": "^2.4.4",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
|||||||
@ -20,15 +20,9 @@ import RolesWord from "./Words/RolesWord";
|
|||||||
import { ArrowLeft, Trash2, UserPlus } from "lucide-react";
|
import { ArrowLeft, Trash2, UserPlus } from "lucide-react";
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import { NewGalleryManager } from "./Gallery";
|
import { NewGalleryManager } from "./Gallery";
|
||||||
|
import AuthorSelectionModal from './AuthorSelectionModal';
|
||||||
|
|
||||||
|
|
||||||
const ArticleAuthorRoleLabels: Record<AuthorRole, string> = {
|
|
||||||
WRITER: 'Автор статьи',
|
|
||||||
PHOTOGRAPHER: 'Фотограф',
|
|
||||||
EDITOR: 'Редактор',
|
|
||||||
TRANSLATOR: 'Переводчик',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
title: string;
|
title: string;
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
@ -92,9 +86,8 @@ export function ArticleForm({
|
|||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Добавляем флаг для отслеживания отправки
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Добавляем флаг для отслеживания отправки
|
||||||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); // Состояние для модального окна
|
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); // Состояние для модального окна
|
||||||
|
|
||||||
const [newRole, setNewRole] = useState('');
|
// Добавляем новое состояние для модального окна
|
||||||
const [newAuthorId, setNewAuthorId] = useState('');
|
const [isAuthorModalOpen, setIsAuthorModalOpen] = useState(false);
|
||||||
const [showAddAuthorModal, setShowAddAuthorModal] = useState(false);
|
|
||||||
|
|
||||||
// Изменяем логику отслеживания состояния загрузки
|
// Изменяем логику отслеживания состояния загрузки
|
||||||
const [dataLoaded, setDataLoaded] = useState(false);
|
const [dataLoaded, setDataLoaded] = useState(false);
|
||||||
@ -240,12 +233,6 @@ export function ArticleForm({
|
|||||||
isInitialized
|
isInitialized
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filteredAuthors = authors.filter(
|
|
||||||
(a) =>
|
|
||||||
a.roles.includes(newRole as AuthorRole) && // 🔹 автор имеет нужную роль
|
|
||||||
!selectedAuthors.some(sel => sel.authorId === a.id && sel.role === newRole) // 🔹 не выбран уже
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent, closeForm: boolean = true) => {
|
const handleSubmit = async (e: React.FormEvent, closeForm: boolean = true) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -311,23 +298,19 @@ export function ArticleForm({
|
|||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обновляем обработчики
|
||||||
const handleOpenModal = () => {
|
const handleOpenModal = () => {
|
||||||
setNewAuthorId('');
|
setIsAuthorModalOpen(true);
|
||||||
setNewRole('WRITER');
|
|
||||||
setShowAddAuthorModal(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setIsConfirmModalOpen(false);
|
setIsAuthorModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddAuthor = () => {
|
const handleAddAuthor = (authorId: string, role: AuthorRole) => {
|
||||||
const author = authors.find(a => a.id === newAuthorId);
|
const author = authors.find(a => a.id === authorId);
|
||||||
if (author) {
|
if (author) {
|
||||||
setSelectedAuthors([...selectedAuthors, { authorId: author.id, role: newRole as AuthorRole, author }]);
|
setSelectedAuthors([...selectedAuthors, { authorId: author.id, role, author }]);
|
||||||
setShowAddAuthorModal(false);
|
|
||||||
setNewAuthorId('');
|
|
||||||
setNewRole('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -578,60 +561,14 @@ export function ArticleForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно выбора автора */}
|
{/* Модальное окно выбора автора */}
|
||||||
{showAddAuthorModal && (
|
{isAuthorModalOpen && (
|
||||||
<>
|
<AuthorSelectionModal
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex justify-center items-center">
|
isOpen={isAuthorModalOpen}
|
||||||
<div className="bg-white p-6 rounded shadow-md max-w-md w-full">
|
onClose={handleCloseModal}
|
||||||
<h4 className="text-lg font-bold mb-4">Добавить автора</h4>
|
onAddAuthor={handleAddAuthor}
|
||||||
|
authors={authors}
|
||||||
<div className="mb-4">
|
existingAuthors={selectedAuthors}
|
||||||
<div className="space-y-2">
|
/>
|
||||||
{Object.entries(ArticleAuthorRoleLabels).map(([key, label]) => (
|
|
||||||
<label key={key} className="flex items-center space-x-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="author-role"
|
|
||||||
value={key}
|
|
||||||
checked={newRole === key}
|
|
||||||
onChange={() => {
|
|
||||||
setNewRole(key);
|
|
||||||
setNewAuthorId('');
|
|
||||||
}}
|
|
||||||
className="text-blue-600 focus:ring-blue-500 border-gray-300"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">{label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Выбор автора</label>
|
|
||||||
<select
|
|
||||||
value={newAuthorId}
|
|
||||||
onChange={(e) => setNewAuthorId(e.target.value)}
|
|
||||||
className="w-full mb-4 border rounded px-2 py-1"
|
|
||||||
>
|
|
||||||
<option value="">Выберите автора</option>
|
|
||||||
{filteredAuthors.map(a => (
|
|
||||||
<option key={a.id} value={a.id}>
|
|
||||||
{a.displayName}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2">
|
|
||||||
<button onClick={() => setShowAddAuthorModal(false)} className="text-gray-500">Отмена</button>
|
|
||||||
<button
|
|
||||||
onClick={handleAddAuthor}
|
|
||||||
disabled={!newAuthorId || !newRole}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Добавить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
332
src/components/AuthorSelectionModal.tsx
Normal file
332
src/components/AuthorSelectionModal.tsx
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Author, AuthorRole } from '../types';
|
||||||
|
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||||
|
|
||||||
|
interface AuthorSelectionModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onAddAuthor: (authorId: string, role: AuthorRole) => void;
|
||||||
|
authors: Author[];
|
||||||
|
existingAuthors: { authorId: string; role: AuthorRole }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleLabels: Record<AuthorRole, string> = {
|
||||||
|
WRITER: 'Авторы',
|
||||||
|
PHOTOGRAPHER: 'Фотографы',
|
||||||
|
EDITOR: 'Редакторы',
|
||||||
|
TRANSLATOR: 'Переводчики',
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onAddAuthor,
|
||||||
|
authors,
|
||||||
|
existingAuthors,
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<AuthorRole>(AuthorRole.WRITER);
|
||||||
|
const [selectedAuthorId, setSelectedAuthorId] = useState<string | null>(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 10;
|
||||||
|
|
||||||
|
// Используем useMemo для оптимальной фильтрации и сортировки авторов
|
||||||
|
const { popularAuthors, otherAuthors } = useMemo(() => {
|
||||||
|
// Фильтруем авторов по активной вкладке и исключаем уже добавленных
|
||||||
|
const filteredAuthors = authors.filter(author =>
|
||||||
|
author.roles.includes(activeTab) &&
|
||||||
|
!existingAuthors.some(existing => existing.authorId === author.id && existing.role === activeTab)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Сортируем по полю order (по возрастанию) и берем первые 6
|
||||||
|
const sortedByOrder = [...filteredAuthors].sort((a, b) => a.order - b.order);
|
||||||
|
const popularAuthors = sortedByOrder.slice(0, 6);
|
||||||
|
|
||||||
|
// Остальных авторов сортируем по алфавиту
|
||||||
|
const otherAuthors = filteredAuthors
|
||||||
|
.filter(author => !popularAuthors.includes(author))
|
||||||
|
.sort((a, b) => a.displayName.localeCompare(b.displayName, 'ru'));
|
||||||
|
|
||||||
|
return { popularAuthors, otherAuthors };
|
||||||
|
}, [authors, existingAuthors, activeTab]);
|
||||||
|
|
||||||
|
// Вычисляем данные для пагинации только для остальных авторов
|
||||||
|
const paginationData = useMemo(() => {
|
||||||
|
const showPagination = otherAuthors.length > itemsPerPage;
|
||||||
|
|
||||||
|
// Если пагинация не нужна, показываем всех остальных авторов
|
||||||
|
if (!showPagination) {
|
||||||
|
return {
|
||||||
|
showPagination: false,
|
||||||
|
displayedOtherAuthors: otherAuthors,
|
||||||
|
totalPages: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если пагинация нужна
|
||||||
|
const totalPages = Math.ceil(otherAuthors.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const displayedOtherAuthors = otherAuthors.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return {
|
||||||
|
showPagination: true,
|
||||||
|
displayedOtherAuthors,
|
||||||
|
totalPages
|
||||||
|
};
|
||||||
|
}, [otherAuthors, currentPage, itemsPerPage]);
|
||||||
|
|
||||||
|
const handleAddAuthor = () => {
|
||||||
|
if (selectedAuthorId) {
|
||||||
|
onAddAuthor(selectedAuthorId, activeTab);
|
||||||
|
setSelectedAuthorId(null);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (role: AuthorRole) => {
|
||||||
|
setActiveTab(role);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSelectedAuthorId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
// Прокручиваем к началу списка остальных авторов
|
||||||
|
document.getElementById('other-authors-section')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const { showPagination, displayedOtherAuthors, totalPages } = paginationData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col">
|
||||||
|
{/* Заголовок */}
|
||||||
|
<div className="flex justify-between items-center p-6 border-b">
|
||||||
|
<h2 className="text-xl font-bold">Добавить автора</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Вкладки */}
|
||||||
|
<div className="flex border-b">
|
||||||
|
{Object.entries(roleLabels).map(([role, label]) => {
|
||||||
|
const roleAuthorsCount = authors.filter(author =>
|
||||||
|
author.roles.includes(role as AuthorRole) &&
|
||||||
|
!existingAuthors.some(existing => existing.authorId === author.id && existing.role === role)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={role}
|
||||||
|
className={`px-6 py-3 font-medium text-sm flex items-center ${
|
||||||
|
activeTab === role
|
||||||
|
? 'text-blue-600 border-b-2 border-blue-600'
|
||||||
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleTabChange(role as AuthorRole)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{roleAuthorsCount > 0 && (
|
||||||
|
<span className="ml-2 bg-gray-200 text-gray-700 text-xs px-2 py-0.5 rounded-full">
|
||||||
|
{roleAuthorsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Содержимое вкладки */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
{/* Популярные авторы (всегда отображаются, если есть) */}
|
||||||
|
{popularAuthors.length > 0 && (
|
||||||
|
<div 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">
|
||||||
|
По рейтингу
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<TransitionGroup className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{popularAuthors.map(author => (
|
||||||
|
<CSSTransition
|
||||||
|
key={`${author.id}-${activeTab}`}
|
||||||
|
timeout={300}
|
||||||
|
classNames="author-card"
|
||||||
|
>
|
||||||
|
<AuthorCard
|
||||||
|
key={author.id}
|
||||||
|
author={author}
|
||||||
|
isSelected={selectedAuthorId === author.id}
|
||||||
|
onSelect={() => setSelectedAuthorId(author.id)}
|
||||||
|
showOrder={true}
|
||||||
|
/>
|
||||||
|
</CSSTransition>
|
||||||
|
))}
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Все остальные авторы с пагинацией */}
|
||||||
|
<div id="other-authors-section">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-700">
|
||||||
|
Остальные {roleLabels[activeTab].toLowerCase()}
|
||||||
|
</h3>
|
||||||
|
{showPagination && (
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
Страница {currentPage} из {totalPages}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{displayedOtherAuthors.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{displayedOtherAuthors.map(author => (
|
||||||
|
<AuthorCard
|
||||||
|
key={author.id}
|
||||||
|
author={author}
|
||||||
|
isSelected={selectedAuthorId === author.id}
|
||||||
|
onSelect={() => setSelectedAuthorId(author.id)}
|
||||||
|
showOrder={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Пагинация */}
|
||||||
|
{showPagination && totalPages > 1 && (
|
||||||
|
<div className="flex justify-center mt-8">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className={`p-2 rounded-full ${
|
||||||
|
currentPage === 1
|
||||||
|
? 'text-gray-300 cursor-not-allowed'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => handlePageChange(page)}
|
||||||
|
className={`w-10 h-10 rounded-full ${
|
||||||
|
currentPage === page
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className={`p-2 rounded-full ${
|
||||||
|
currentPage === totalPages
|
||||||
|
? 'text-gray-300 cursor-not-allowed'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-center py-8">
|
||||||
|
Нет доступных
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</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 }) => {
|
||||||
|
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'
|
||||||
|
}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{author.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={author.avatarUrl}
|
||||||
|
alt={author.displayName}
|
||||||
|
className="w-12 h-12 rounded-full object-cover mr-3"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center mr-3">
|
||||||
|
<span className="text-gray-500 font-medium">
|
||||||
|
{author.displayName.charAt(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-medium text-gray-900">{author.displayName}</h4>
|
||||||
|
{showOrder && (
|
||||||
|
<span className="text-xs font-medium bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
|
||||||
|
#{author.order}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{author.email && (
|
||||||
|
<p className="text-sm text-gray-500">{author.email}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthorSelectionModal;
|
||||||
@ -43,6 +43,29 @@
|
|||||||
p.image-caption {
|
p.image-caption {
|
||||||
@apply mt-2 text-lg text-gray-600 italic text-center max-w-[80%];
|
@apply mt-2 text-lg text-gray-600 italic text-center max-w-[80%];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.author-card-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-card-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
transition: opacity 300ms, transform 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-card-exit {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-card-exit-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: opacity 300ms, transform 300ms;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user