Добавлен фильтр списка статей по пользователю. Админ видит все.
This commit is contained in:
parent
e254ef0dc0
commit
00376c124f
@ -36,7 +36,7 @@ export function AuthGuard({ children, requiredPermissions }: AuthGuardProps) {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h2>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
You don't have permission to {action} articles in the {categoryId} category.
|
У вас нет прав на {action} статьи в {categoryId} категории.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,8 @@ import {
|
|||||||
AlignCenter,
|
AlignCenter,
|
||||||
Plus,
|
Plus,
|
||||||
Minus,
|
Minus,
|
||||||
SquareUser
|
SquareUser,
|
||||||
|
ImagePlus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
@ -339,6 +340,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
className={`p-1 rounded hover:bg-gray-200 ${
|
className={`p-1 rounded hover:bg-gray-200 ${
|
||||||
editor?.isActive('bold') ? 'bg-gray-200' : ''
|
editor?.isActive('bold') ? 'bg-gray-200' : ''
|
||||||
}`}
|
}`}
|
||||||
|
title="Выделение слов жирным шрифтом"
|
||||||
>
|
>
|
||||||
<Bold size={18} />
|
<Bold size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -346,6 +348,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className="p-1 rounded hover:bg-gray-200"
|
className="p-1 rounded hover:bg-gray-200"
|
||||||
|
title="Автоматическое выделение жирным имен"
|
||||||
>
|
>
|
||||||
<SquareUser size={18} />
|
<SquareUser size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -355,6 +358,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
className={`p-1 rounded hover:bg-gray-200 ${
|
className={`p-1 rounded hover:bg-gray-200 ${
|
||||||
editor?.isActive('italic') ? 'bg-gray-200' : ''
|
editor?.isActive('italic') ? 'bg-gray-200' : ''
|
||||||
}`}
|
}`}
|
||||||
|
title="Выделение слов наклонным шрифтом"
|
||||||
>
|
>
|
||||||
<Italic size={18} />
|
<Italic size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -362,6 +366,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => editor?.chain().focus().setParagraph().run()}
|
onClick={() => editor?.chain().focus().setParagraph().run()}
|
||||||
className={`p-1 rounded hover:bg-gray-200 ${editor?.isActive('paragraph') ? 'bg-gray-200' : ''}`}
|
className={`p-1 rounded hover:bg-gray-200 ${editor?.isActive('paragraph') ? 'bg-gray-200' : ''}`}
|
||||||
|
title="Выравнивание по левому краю"
|
||||||
>
|
>
|
||||||
<AlignLeft size={18} />
|
<AlignLeft size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -370,6 +375,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
onClick={() => editor?.chain().focus().undo().run()}
|
onClick={() => editor?.chain().focus().undo().run()}
|
||||||
disabled={!editor?.can().chain().focus().undo().run()}
|
disabled={!editor?.can().chain().focus().undo().run()}
|
||||||
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50"
|
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50"
|
||||||
|
title="Отменить действие"
|
||||||
>
|
>
|
||||||
<Undo size={18} />
|
<Undo size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -378,6 +384,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
onClick={() => editor?.chain().focus().redo().run()}
|
onClick={() => editor?.chain().focus().redo().run()}
|
||||||
disabled={!editor?.can().chain().focus().redo().run()}
|
disabled={!editor?.can().chain().focus().redo().run()}
|
||||||
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50"
|
className="p-1 rounded hover:bg-gray-200 disabled:opacity-50"
|
||||||
|
title="Вернуть действие"
|
||||||
>
|
>
|
||||||
<Redo size={18} />
|
<Redo size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -387,6 +394,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
className={`p-1 rounded hover:bg-gray-200 ${
|
className={`p-1 rounded hover:bg-gray-200 ${
|
||||||
editor?.isActive('bulletList') ? 'bg-gray-200' : ''
|
editor?.isActive('bulletList') ? 'bg-gray-200' : ''
|
||||||
}`}
|
}`}
|
||||||
|
title="Создать список"
|
||||||
>
|
>
|
||||||
<List size={18} />
|
<List size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -396,6 +404,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
className={`p-1 rounded hover:bg-gray-200 ${
|
className={`p-1 rounded hover:bg-gray-200 ${
|
||||||
editor?.isActive('orderedList') ? 'bg-gray-200' : ''
|
editor?.isActive('orderedList') ? 'bg-gray-200' : ''
|
||||||
}`}
|
}`}
|
||||||
|
title="Создать упорядоченный список"
|
||||||
>
|
>
|
||||||
<ListOrdered size={18} />
|
<ListOrdered size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -406,6 +415,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
className={`p-1 rounded hover:bg-gray-200 ${
|
className={`p-1 rounded hover:bg-gray-200 ${
|
||||||
editor?.isActive('blockquote') ? 'bg-gray-200' : ''
|
editor?.isActive('blockquote') ? 'bg-gray-200' : ''
|
||||||
}`}
|
}`}
|
||||||
|
title="Цитирование (параграф)"
|
||||||
>
|
>
|
||||||
<Quote size={18} />
|
<Quote size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -416,6 +426,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
className={`p-1 rounded hover:bg-gray-200 ${
|
className={`p-1 rounded hover:bg-gray-200 ${
|
||||||
editor?.getAttributes('textAlign')?.textAlign === 'center' ? 'bg-gray-200' : ''
|
editor?.getAttributes('textAlign')?.textAlign === 'center' ? 'bg-gray-200' : ''
|
||||||
}`}
|
}`}
|
||||||
|
title="Выравнивание по центру"
|
||||||
>
|
>
|
||||||
<AlignCenter size={18} />
|
<AlignCenter size={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -427,8 +438,9 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
|
|||||||
editor?.chain().focus().setImage({ src: url }).run();
|
editor?.chain().focus().setImage({ src: url }).run();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
title="Вставить изображение"
|
||||||
>
|
>
|
||||||
🖼️ Фото
|
<ImagePlus size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -20,8 +20,8 @@ export function useUserManagement() {
|
|||||||
setUsers(fetchedUsers);
|
setUsers(fetchedUsers);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to fetch users');
|
setError('Ошибка получения списка пользователей');
|
||||||
console.error('Error fetching users:', err);
|
console.error('Ошибка получения списка пользователей:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export function AdminPage() {
|
|||||||
const [authors, setAuthors] = useState<Author[]>([]);
|
const [authors, setAuthors] = useState<Author[]>([]);
|
||||||
const [authorId, setAuthorId] = useState<string>('');
|
const [authorId, setAuthorId] = useState<string>('');
|
||||||
|
|
||||||
// Загрузка списка авторов
|
// Загрузка списка авторов
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAuthors = async () => {
|
const fetchAuthors = async () => {
|
||||||
try {
|
try {
|
||||||
@ -72,7 +72,9 @@ export function AdminPage() {
|
|||||||
page: currentPage,
|
page: currentPage,
|
||||||
categoryId: filterCategoryId,
|
categoryId: filterCategoryId,
|
||||||
cityId: filterCityId,
|
cityId: filterCityId,
|
||||||
isDraft: showDraftOnly
|
isDraft: showDraftOnly,
|
||||||
|
userId: user?.id,
|
||||||
|
isAdmin: isAdmin
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setArticles(response.data.articles);
|
setArticles(response.data.articles);
|
||||||
@ -90,7 +92,7 @@ export function AdminPage() {
|
|||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Фильтр для категорий и городов, основанный на разрешениях пользователя
|
// Фильтр для категорий, основанный на разрешениях пользователя
|
||||||
const availableCategoryIds = useMemo(() => {
|
const availableCategoryIds = useMemo(() => {
|
||||||
if (!user) return [];
|
if (!user) return [];
|
||||||
if (isAdmin) return allCategoryIds;
|
if (isAdmin) return allCategoryIds;
|
||||||
@ -101,6 +103,7 @@ export function AdminPage() {
|
|||||||
);
|
);
|
||||||
}, [user, isAdmin]);
|
}, [user, isAdmin]);
|
||||||
|
|
||||||
|
// Фильтр для городов, основанный на разрешениях пользователя
|
||||||
const availableCityIds = useMemo(() => {
|
const availableCityIds = useMemo(() => {
|
||||||
if (!user) return [];
|
if (!user) return [];
|
||||||
if (isAdmin) return allCityIds;
|
if (isAdmin) return allCityIds;
|
||||||
@ -309,7 +312,7 @@ export function AdminPage() {
|
|||||||
{editingId ? 'Редактировать статью' : 'Создать новую статью'}
|
{editingId ? 'Редактировать статью' : 'Создать новую статью'}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{error && (
|
{error && isAdmin && (
|
||||||
<div className="mb-6 bg-red-50 text-red-700 p-4 rounded-md">
|
<div className="mb-6 bg-red-50 text-red-700 p-4 rounded-md">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
@ -581,7 +584,7 @@ export function AdminPage() {
|
|||||||
<img
|
<img
|
||||||
src={article.author.avatarUrl}
|
src={article.author.avatarUrl}
|
||||||
alt={article.author.displayName}
|
alt={article.author.displayName}
|
||||||
className="h-4 w-4 rounded-full mr-1"
|
className="h-6 w-6 rounded-full mr-1"
|
||||||
/>
|
/>
|
||||||
<span className="italic font-bold"> {article.author.displayName}</span>
|
<span className="italic font-bold"> {article.author.displayName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,6 +7,7 @@ import { ImagePlus, X, UserPlus, Pencil } from 'lucide-react';
|
|||||||
import { imageResolutions } from '../config/imageResolutions';
|
import { imageResolutions } from '../config/imageResolutions';
|
||||||
import { CategoryIds, CategoryTitles, CityIds, CityTitles } from "../types";
|
import { CategoryIds, CategoryTitles, CityIds, CityTitles } from "../types";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import {useAuthStore} from "../stores/authStore.ts";
|
||||||
|
|
||||||
|
|
||||||
const initialFormData: UserFormData = {
|
const initialFormData: UserFormData = {
|
||||||
@ -36,6 +37,8 @@ export function UserManagementPage() {
|
|||||||
const [formData, setFormData] = useState<UserFormData>(initialFormData);
|
const [formData, setFormData] = useState<UserFormData>(initialFormData);
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { user } = useAuthStore();
|
||||||
|
|
||||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>, userId: string) => {
|
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>, userId: string) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@ -78,7 +81,7 @@ export function UserManagementPage() {
|
|||||||
}
|
}
|
||||||
setFormData(initialFormData);
|
setFormData(initialFormData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormError(error instanceof Error ? error.message : 'An error occurred');
|
setFormError(error instanceof Error ? error.message : 'Произошла ошибка');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,11 +104,14 @@ export function UserManagementPage() {
|
|||||||
Управление пользователями
|
Управление пользователями
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => {
|
||||||
|
setFormData(initialFormData);
|
||||||
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<UserPlus className="h-5 w-5 mr-2" />
|
<UserPlus className="h-5 w-5 mr-2" />
|
||||||
Add New User
|
Новый пользователь
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -240,7 +246,7 @@ export function UserManagementPage() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Create/Edit User Modal */}
|
{/* Create/Edit User Modal */}
|
||||||
{(showCreateModal || showEditModal) && (
|
{((showCreateModal || showEditModal) && user?.permissions.isAdmin) && (
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
|
||||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
@ -318,12 +324,13 @@ export function UserManagementPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
Пароль {showEditModal && '(оставить пустым чтобы использовать текущий)'}
|
Пароль {showEditModal}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
|
||||||
|
placeholder={showEditModal ? 'Оставьте пустым, чтобы не менять' : ''}
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||||
required={!showEditModal}
|
required={!showEditModal}
|
||||||
/>
|
/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user