-
- {author.articlesCount} статей
-
+
+
+
{author.articlesCount} ·
+
+
+ {author.totalLikes}
+
+
+
- {bookmarked ? 'Remove bookmark' : 'Add bookmark'}
+ {bookmarked ? 'Убрать закладку' : 'Добавить закладку'}
);
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index fcacc06..d499663 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -1,6 +1,7 @@
import { Link } from 'react-router-dom';
-import { Mail, Phone, Instagram, Twitter, Facebook, ExternalLink } from 'lucide-react';
+import { Mail, Phone, Instagram, Facebook, ExternalLink } from 'lucide-react';
import { DesignStudioLogo } from './DesignStudioLogo';
+import { VkIcon } from "../../icons/custom/VkIcon1";
export function Footer() {
return (
@@ -17,7 +18,7 @@ export function Footer() {
-
+
diff --git a/src/components/Words/ArticlesWord.tsx b/src/components/Words/ArticlesWord.tsx
new file mode 100644
index 0000000..98a6a97
--- /dev/null
+++ b/src/components/Words/ArticlesWord.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+// Описание типа для пропсов компонента
+interface ArticlesWordProps {
+ articles: number; // Количество статей
+}
+
+const ArticlesWord: React.FC = ({ articles }) => {
+ const getArticleWord = (articles: number): string => {
+ if (articles === 1) {
+ return "статья";
+ }
+ if (articles >= 2 && articles <= 4) {
+ return "статьи";
+ }
+ return "статей";
+ };
+
+ return <>{getArticleWord(articles)}>;
+};
+
+export default ArticlesWord;
diff --git a/src/components/MinutesWord.tsx b/src/components/Words/MinutesWord.tsx
similarity index 100%
rename from src/components/MinutesWord.tsx
rename to src/components/Words/MinutesWord.tsx
diff --git a/src/hooks/useAuthorManagement.ts b/src/hooks/useAuthorManagement.ts
new file mode 100644
index 0000000..a1d0880
--- /dev/null
+++ b/src/hooks/useAuthorManagement.ts
@@ -0,0 +1,140 @@
+import { useState, useEffect } from 'react';
+import {Author, AuthorFormData, User} from '../types/auth';
+import { authorService } from '../services/authorService';
+import { userService } from "../services/userService";
+
+
+export function useAuthorManagement() {
+ const [authors, setAuthors] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [selectedAuthor, setSelectedAuthor] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetchAuthors();
+ fetchUsers();
+ }, []);
+
+ const fetchAuthors = async () => {
+ try {
+ setLoading(true);
+ const fetchedAuthors = await authorService.getAuthors();
+ setAuthors(fetchedAuthors);
+ setError(null);
+ } catch (err) {
+ setError('Ошибка получения списка авторов');
+ console.error('Ошибка получения списка авторов:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchUsers = async () => {
+ try {
+ setLoading(true);
+ const fetchedUsers = await userService.getUsers();
+ setUsers(fetchedUsers);
+ setError(null);
+ } catch (err) {
+ setError('Ошибка получения списка авторов');
+ console.error('Ошибка получения списка авторов:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const createAuthor = async (formData: AuthorFormData) => {
+ try {
+ const newAuthor = await authorService.createAuthor(formData);
+ setAuthors([...authors, newAuthor]);
+ setError(null);
+ } catch (err) {
+ setError('Ошибка создания автора');
+ throw err;
+ }
+ };
+
+ const updateAuthor = async (authorId: string, formData: AuthorFormData) => {
+ try {
+ const updatedAuthor = await authorService.updateAuthor(authorId, formData);
+ setAuthors(authors.map(author => author.id === authorId ? updatedAuthor : author));
+ setError(null);
+ } catch (err) {
+ setError('Ошибка редакторования данных автора');
+ throw err;
+ }
+ };
+
+ const linkUser = async (authorId: string, userId: string) => {
+ try {
+ await authorService.linkUser(authorId, userId);
+ setError(null);
+ } catch (err) {
+ setError('Ошибка связывания пользователя с автором');
+ throw err;
+ }
+ };
+
+ const unlinkUser = async (authorId: string) => {
+ try {
+ await authorService.unlinkUser(authorId);
+ setError(null);
+ } catch (err) {
+ setError('Ошибка отвязывания пользователя от автора');
+ throw err;
+ }
+ };
+
+ const orderMoveUp = async (authorId: string) => {
+ await authorService.reorderAuthor(authorId, 'up');
+ };
+
+ const orderMoveDown = async (authorId: string) => {
+ await authorService.reorderAuthor(authorId, 'down');
+ };
+
+ const toggleActive = async (authorId: string, isActive: boolean) => {
+ try {
+ console.log(isActive);
+ await authorService.toggleActive(authorId, isActive);
+ setError(null);
+ } catch (err) {
+ setError('Ошибка отвязывания пользователя от автора');
+ throw err;
+ }
+ };
+
+ const deleteAuthor = async (authorId: string) => {
+ try {
+ await authorService.deleteAuthor(authorId);
+ setAuthors(authors.filter(author => author.id !== authorId));
+ if (selectedAuthor?.id === authorId) {
+ setSelectedAuthor(null);
+ }
+ setError(null);
+ } catch (err) {
+ setError('Ошибка удаления автора');
+ throw err;
+ }
+ };
+
+ return {
+ authors,
+ users,
+ selectedAuthor,
+ loading,
+ error,
+ setSelectedAuthor,
+ createAuthor,
+ updateAuthor,
+ linkUser,
+ unlinkUser,
+ orderMoveUp,
+ orderMoveDown,
+ toggleActive,
+ deleteAuthor,
+ fetchAuthors,
+ fetchUsers,
+ };
+}
\ No newline at end of file
diff --git a/src/icons/custom/OkIcon.tsx b/src/icons/custom/OkIcon.tsx
new file mode 100644
index 0000000..e06d0ad
--- /dev/null
+++ b/src/icons/custom/OkIcon.tsx
@@ -0,0 +1,18 @@
+import { LucideProps } from "lucide-react";
+
+export const OkIcon = (props: LucideProps) => (
+
+
+
+
+
+);
diff --git a/src/icons/custom/VkIcon.tsx b/src/icons/custom/VkIcon.tsx
new file mode 100644
index 0000000..2618041
--- /dev/null
+++ b/src/icons/custom/VkIcon.tsx
@@ -0,0 +1,16 @@
+import { LucideProps } from "lucide-react";
+
+export const VkIcon = (props: LucideProps) => (
+
+
+
+);
diff --git a/src/icons/custom/VkIcon1.tsx b/src/icons/custom/VkIcon1.tsx
new file mode 100644
index 0000000..fca13b3
--- /dev/null
+++ b/src/icons/custom/VkIcon1.tsx
@@ -0,0 +1,17 @@
+import { LucideProps } from "lucide-react";
+
+export const VkIcon = (props: LucideProps) => (
+
+
+
+
+);
diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx
index d438a37..0458f30 100644
--- a/src/pages/AdminPage.tsx
+++ b/src/pages/AdminPage.tsx
@@ -7,6 +7,8 @@ import { GalleryModal } from '../components/GalleryManager/GalleryModal';
import { ArticleDeleteModal } from '../components/ArticleDeleteModal';
import { Article, Author, GalleryImage, ArticleData } from '../types';
import { usePermissions } from '../hooks/usePermissions';
+import { FileJson, Users, UserSquare2 } from "lucide-react";
+import { Link } from 'react-router-dom';
interface FormState {
title: string;
@@ -38,7 +40,7 @@ export function AdminPage() {
useEffect(() => {
const fetchAuthors = async () => {
try {
- const response = await axios.get('/api/users/', {
+ const response = await axios.get('/api/authors/', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
@@ -171,6 +173,33 @@ export function AdminPage() {
+ {/* Admin Navigation */}
+ {isAdmin && (
+
+
+
+ Пользователи
+
+
+
+ Авторы
+
+
+
+ Импорт статей
+
+
+ )}
+
{error && isAdmin && (
{error}
)}
diff --git a/src/pages/ArticlePage.tsx b/src/pages/ArticlePage.tsx
index a483ef1..33769d6 100644
--- a/src/pages/ArticlePage.tsx
+++ b/src/pages/ArticlePage.tsx
@@ -9,7 +9,7 @@ import { SEO } from '../components/SEO';
import { ArticleContent } from '../components/ArticleContent';
import { ShareButton } from '../components/ShareButton';
import { BookmarkButton } from '../components/BookmarkButton';
-import MinutesWord from '../components/MinutesWord';
+import MinutesWord from '../components/Words/MinutesWord';
import axios from "axios";
import api from "../utils/api";
@@ -178,6 +178,16 @@ export function ArticlePage() {
/>
+
+
+
+
+
+ К списку статей
+
);
diff --git a/src/pages/AuthorManagementPage.tsx b/src/pages/AuthorManagementPage.tsx
new file mode 100644
index 0000000..9e83333
--- /dev/null
+++ b/src/pages/AuthorManagementPage.tsx
@@ -0,0 +1,583 @@
+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
(null);
+ const [formData, setFormData] = useState(initialFormData);
+ const [formError, setFormError] = useState(null);
+ const [selectedUserId, setSelectedUserId] = useState('');
+
+ const handleAvatarUpload = async (event: React.ChangeEvent, 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 (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ Управление авторами
+
+ 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"
+ >
+
+ Добавить автора
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ {authors.map((author, index) => (
+
+
+
+
+
+
+ {author.displayName} {author.userId ? `=> пользователь ${author.displayName}` : ``}
+
+
{author.bio}
+
+
+
+
+
+ handleMoveUp(author.id)}
+ disabled={index === 0}
+ className="p-1 text-gray-400 hover:text-blue-500 disabled:opacity-30"
+ title="Переместить вверх"
+ >
+
+
+ handleMoveDown(author.id)}
+ disabled={index === authors.length - 1}
+ className="p-1 text-gray-400 hover:text-blue-500 disabled:opacity-30"
+ title="Переместить вниз"
+ >
+
+
+
+
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 ? : }
+
+ {author.userId ? (
+
handleUnlinkUser(author.id)}
+ className="p-2 rounded-full hover:bg-gray-100 text-gray-400"
+ >
+
+
+ ) : (
+
{
+ setSelectedAuthor(author);
+ setShowLinkUserModal(true);
+ }}
+ className="p-2 rounded-full hover:bg-gray-100 text-gray-400"
+ >
+
+
+ )}
+
{
+ 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"
+ >
+
+
+
setShowDeleteModal(author.id)}
+ className="p-2 rounded-full hover:bg-gray-100 text-red-400"
+ >
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* Create/Edit Author Modal */}
+ {((showCreateModal || showEditModal) && user?.permissions.isAdmin ) && (
+
+
+
+
+ {showCreateModal ? 'Новый автор' : 'Изменить автора'}
+
+ {
+ setShowCreateModal(false);
+ setShowEditModal(false);
+ setFormData(initialFormData);
+ setFormError(null);
+ }}
+ className="text-gray-400 hover:text-gray-500"
+ >
+
+
+
+
+ {formError && (
+
+ {formError}
+
+ )}
+
+
+
+
+ )}
+
+ {/* Link User Modal */}
+ {showLinkUserModal && (
+
+
+
+
+ Связать автора с пользователем
+
+ {
+ setShowLinkUserModal(false);
+ setSelectedUserId('');
+ }}
+ className="text-gray-400 hover:text-gray-500"
+ >
+
+
+
+
+
+
+ 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"
+ >
+ Выберите пользователя...
+ {users
+ .filter(u => !u.isLinkedToAuthor) // Only show users not already linked to an author
+ .map(user => (
+
+ {user.displayName})
+
+ ))
+ }
+
+
+
+
+ {
+ 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"
+ >
+ Отмена
+
+ {
+ 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"
+ >
+ Связать
+
+
+
+
+
+ )}
+
+ {/* Delete Confirmation Modal */}
+ {showDeleteModal && (
+
+
+
+ Удалить автора
+
+
+
+ Вы уверены, что хотите удалить этого автора? Это действие невозможно отменить.
+ Все статьи, связанные с этим автором, останутся, но у них больше не будет активного автора.
+
+
+
+ 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"
+ >
+ Отмена
+
+ 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"
+ >
+ Удалить
+
+
+
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/ImportArticlesPage.tsx b/src/pages/ImportArticlesPage.tsx
index 7393918..1971a7a 100644
--- a/src/pages/ImportArticlesPage.tsx
+++ b/src/pages/ImportArticlesPage.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useRef, useCallback } from 'react';
+import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Header } from '../components/Header';
import { AuthGuard } from '../components/AuthGuard';
import { Article, CategoryTitles, CityTitles } from '../types';
@@ -9,6 +9,11 @@ const ARTICLES_PER_PAGE = 10;
export function ImportArticlesPage() {
const [articles, setArticles] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
+
+ useEffect(() => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }, [currentPage]);
+
const [editingArticle, setEditingArticle] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
@@ -31,7 +36,14 @@ export function ImportArticlesPage() {
const jsonData = JSON.parse(text as string);
if (Array.isArray(jsonData)) {
- setArticles(jsonData);
+ const normalized = jsonData.map((article: Article) => ({
+ ...article,
+ id: article.importId.toString(), // 👈 теперь будет использоваться корректный id
+ images: Array.isArray(article.images) ? [...article.images] : [],
+ imageSubs: Array.isArray(article.imageSubs) ? [...article.imageSubs] : [],
+ }));
+
+ setArticles(normalized);
setCurrentPage(1);
setError(null);
} else {
@@ -44,10 +56,21 @@ export function ImportArticlesPage() {
reader.readAsText(file);
};
- const handleEditField = (articleId: string, field: K, value: Article[K]) => {
- setArticles(prev => prev.map(article =>
- article.id === articleId ? { ...article, [field]: value } : article
- ));
+ const handleEditField = (
+ articleId: string,
+ field: K,
+ value: Article[K]
+ ) => {
+ setArticles(prev =>
+ prev.map(article => {
+ if (article.id !== articleId) return article;
+
+ return {
+ ...article,
+ [field]: Array.isArray(value) ? [...value] : value, // гарантированно новая ссылка
+ };
+ })
+ );
};
const handleSaveToBackend = useCallback(async () => {
@@ -78,6 +101,22 @@ export function ImportArticlesPage() {
}
}, [articles]);
+ const handleImageSubEdit = (articleId: string, index: number, newValue: string) => {
+ setArticles(prev =>
+ prev.map(article => {
+ if (article.id !== articleId) return article;
+
+ const updatedSubs = article.imageSubs ? [...article.imageSubs] : [];
+ updatedSubs[index] = newValue;
+
+ return {
+ ...article,
+ imageSubs: updatedSubs, // новая ссылка!
+ };
+ })
+ );
+ };
+
return (
@@ -152,6 +191,19 @@ export function ImportArticlesPage() {
{CategoryTitles[article.categoryId]} · {CityTitles[article.cityId]} · {article.readTime} min read
+
+ Автор:
+
+ handleEditField(article.id, 'authorName', e.target.value)
+ }
+ className="w-full border border-gray-300 rounded px-3 py-1 text-sm"
+ placeholder="Введите имя автора"
+ />
+
+
{(article.images?.length || 0) > 0 && (
Изображения с подписями:
@@ -166,11 +218,7 @@ export function ImportArticlesPage() {
{
- const newSubs = [...(article.imageSubs || [])];
- newSubs[index] = e.target.value;
- handleEditField(article.id, 'imageSubs', newSubs);
- }}
+ onChange={(e) => handleImageSubEdit(article.id, index, e.target.value)}
placeholder={`Подпись ${index + 1}`}
className="w-full border border-gray-300 rounded px-2 py-1 text-sm"
/>
diff --git a/src/pages/UserManagementPage.tsx b/src/pages/UserManagementPage.tsx
index 0ddf42f..f424aa9 100644
--- a/src/pages/UserManagementPage.tsx
+++ b/src/pages/UserManagementPage.tsx
@@ -8,6 +8,7 @@ import { imageResolutions } from '../config/imageResolutions';
import { CategoryIds, CategoryTitles, CityIds, CityTitles } from "../types";
import axios from "axios";
import { useAuthStore } from "../stores/authStore";
+//import { AuthorModal } from "./AuthorModal";
const initialFormData: UserFormData = {
@@ -15,7 +16,6 @@ const initialFormData: UserFormData = {
email: '',
password: '',
displayName: '',
- bio: '',
avatarUrl: '/images/avatar.jpg'
};
@@ -86,8 +86,6 @@ export function UserManagementPage() {
}
};
- const isAttributionOnly = formData.attributionOnly || (!selectedUser?.email && !selectedUser?.permissions.isAdmin);
-
if (loading) {
return (
@@ -145,7 +143,7 @@ export function UserManagementPage() {
/>
{user.displayName}
-
{user.email || 'Без входа'}
+
{user.email}
{
@@ -155,9 +153,7 @@ export function UserManagementPage() {
email: user.email,
password: '',
displayName: user.displayName,
- bio: user.bio,
- avatarUrl: user.avatarUrl,
- attributionOnly: !user.email
+ avatarUrl: user.avatarUrl
});
setShowEditModal(true);
}}
@@ -172,7 +168,7 @@ export function UserManagementPage() {
{/* Permissions Editor */}
- {selectedUser && selectedUser.email && (
+ {selectedUser && (
Редактирование прав пользователя "{selectedUser.displayName}"
@@ -310,66 +306,37 @@ export function UserManagementPage() {
value={formData.displayName}
onChange={(e) => setFormData(prev => ({ ...prev, displayName: e.target.value }))}
autoComplete="off"
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
+ 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
/>
-
-
setFormData(prev => ({ ...prev, attributionOnly: e.target.checked }))}
- className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
- />
-
- Без возможности входа
+
+
+ E-mail
+ setFormData(prev => ({ ...prev, email: e.target.value }))}
+ autoComplete="off"
+ 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
+ />
- {!isAttributionOnly && (
- <>
-
-
- E-mail
-
- setFormData(prev => ({ ...prev, email: e.target.value }))}
- autoComplete="off"
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
- required={!isAttributionOnly}
- />
-
-
-
-
- Пароль
-
- setFormData(prev => ({ ...prev, password: e.target.value }))}
- autoComplete="new-password"
- placeholder={showEditModal ? 'Оставьте пустым, чтобы не менять' : ''}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
- required={!isAttributionOnly && !showEditModal}
- />
-
- >
- )}
-
- Биографические данные
+ Пароль
- setFormData(prev => ({ ...prev, bio: e.target.value }))}
- rows={4}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
+ setFormData(prev => ({ ...prev, password: e.target.value }))}
+ autoComplete="new-password"
+ placeholder={showEditModal ? 'Оставьте пустым, чтобы не менять' : ''}
+ 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={!showEditModal}
/>
diff --git a/src/services/authorService.ts b/src/services/authorService.ts
new file mode 100644
index 0000000..6f996bd
--- /dev/null
+++ b/src/services/authorService.ts
@@ -0,0 +1,87 @@
+import axios from '../utils/api';
+import { Author } from '../types/auth';
+
+interface AuthorFormData {
+ displayName: string;
+ bio?: string;
+ avatarUrl?: string;
+ email?: string;
+ order?: number;
+ twitterUrl?: string;
+ instagramUrl?: string;
+ websiteUrl?: string;
+ isActive?: boolean;
+}
+
+export const authorService = {
+ getAuthors: async (): Promise => {
+ try {
+ const response = await axios.get('/authors');
+ return response.data;
+ } catch (error) {
+ console.error('Ошибка получения списка авторов:', error);
+ throw new Error('Невозможно получить список авторов');
+ }
+ },
+
+ createAuthor: async (authorData: AuthorFormData): Promise => {
+ try {
+ const response = await axios.post('/authors', authorData);
+ return response.data;
+ } catch (error) {
+ console.error('Ошибка создания автора:', error);
+ throw new Error('Невозможно создать автора');
+ }
+ },
+
+ updateAuthor: async (authorId: string, authorData: Partial): Promise => {
+ try {
+ const response = await axios.put(`/authors/${authorId}`, authorData);
+ return response.data;
+ } catch (error) {
+ console.error('Ошибка изменения данных автора:', error);
+ throw new Error('Невозможно изменить данные автора');
+ }
+ },
+
+ deleteAuthor: async (authorId: string): Promise => {
+ try {
+ await axios.delete(`/authors/${authorId}`);
+ } catch (error) {
+ console.error('Error deleting author:', error);
+ throw new Error('Failed to delete author');
+ }
+ },
+
+ reorderAuthor: async (authorId: string, direction: 'up' | 'down') => {
+ await axios.put(`/authors/${authorId}/reorder`, { direction });
+ },
+
+ toggleActive: async (authorId: string, isActive: boolean): Promise => {
+ try {
+ const response = await axios.put(`/authors/${authorId}/toggle-active`, { isActive });
+ return response.data;
+ } catch (error) {
+ console.error('Error toggling author status:', error);
+ throw new Error('Failed to toggle author status');
+ }
+ },
+
+ linkUser: async (authorId: string, userId: string): Promise => {
+ try {
+ await axios.put(`/authors/${authorId}/link-user`, { userId });
+ } catch (error) {
+ console.error('Error linking user to author:', error);
+ throw new Error('Failed to link user to author');
+ }
+ },
+
+ unlinkUser: async (authorId: string): Promise => {
+ try {
+ await axios.put(`/authors/${authorId}/unlink-user`);
+ } catch (error) {
+ console.error('Error unlinking user from author:', error);
+ throw new Error('Failed to unlink user from author');
+ }
+ }
+};
\ No newline at end of file
diff --git a/src/types/auth.ts b/src/types/auth.ts
index 63afdb8..6e98b2f 100644
--- a/src/types/auth.ts
+++ b/src/types/auth.ts
@@ -15,6 +15,7 @@ export interface User {
avatarUrl: string;
bio: string;
permissions: UserPermissions;
+ isLinkedToAuthor: boolean;
}
export interface Author {
@@ -23,7 +24,14 @@ export interface Author {
displayName: string;
avatarUrl: string;
bio: string;
+ order: number;
+ okUrl: string;
+ vkUrl: string;
+ websiteUrl: string;
articlesCount: number;
+ totalLikes: number;
+ userId?: string;
+ isActive?: boolean;
}
export interface UserFormData {
@@ -31,7 +39,22 @@ export interface UserFormData {
email?: string;
password?: string;
displayName: string;
+ avatarUrl: string;
+}
+
+export interface AuthorFormData {
+ id: string;
+ displayName: string;
+ email: string;
bio: string;
avatarUrl: string;
- attributionOnly?: boolean;
+ order: number;
+ okUrl: string;
+ vkUrl: string;
+ websiteUrl: string;
+ articlesCount: number;
+ totalLikes: number;
+ userId?: string;
+ isActive: boolean;
}
+
diff --git a/src/types/index.ts b/src/types/index.ts
index c3276d3..4204296 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -7,6 +7,7 @@ export interface Article {
categoryId: number;
cityId: number;
author: Author;
+ authorName: string;
coverImage: string;
images?: string[];
imageSubs?: string[];
@@ -57,7 +58,14 @@ export interface Author {
id: string;
displayName: string;
avatarUrl: string;
- email: string;
+ email?: string;
+ twitterUrl?: string;
+ instagramUrl?: string;
+ websiteUrl?: string;
+ order: number;
+ createdAt: string;
+ updatedAt: string;
+ isActive: boolean;
}
export const CategoryIds: number[] = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8];