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