merge appeal and file moderation winodw
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "no-copy-admin-panel-frontend",
|
"name": "no-copy-admin-panel-frontend",
|
||||||
"version": "0.16.0",
|
"version": "0.17.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 2996",
|
"dev": "next dev -p 2996",
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { fetchModerationContentList } from '@/app/actions/contentActions';
|
import { fetchModerationContentList } from '@/app/actions/contentActions';
|
||||||
|
|
||||||
|
export interface AppealInfo {
|
||||||
|
additionalInfo: string | null;
|
||||||
|
adminComment: null;
|
||||||
|
appealId: string | null;
|
||||||
|
appealReason: string | null;
|
||||||
|
createdAt: string | null;
|
||||||
|
fileId: string | null;
|
||||||
|
fileName: string | null;
|
||||||
|
resolvedAt: string | null;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
export interface ModerationFile {
|
export interface ModerationFile {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
moderationInfo: {
|
moderationInfo: {
|
||||||
appealInfo: null | any;
|
appealInfo: null | AppealInfo;
|
||||||
hasActiveAppeal: boolean;
|
hasActiveAppeal: boolean;
|
||||||
};
|
};
|
||||||
userId: number;
|
userId: number;
|
||||||
|
|||||||
@@ -125,6 +125,62 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs-modal__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-modal__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-modal__tab {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #64748b;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-modal__tab:hover:not(:disabled) {
|
||||||
|
color: #3b82f6;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-modal__tab--active {
|
||||||
|
color: #3b82f6;
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-modal__tab--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-modal__tab-badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-modal__content {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
|
|||||||
@@ -1035,6 +1035,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.table-item-protected {
|
.table-item-protected {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import DropDownList from '@/app/components/dropDownList';
|
|||||||
import { AppealsFile } from '@/app/hooks/react-query/useContentForAppeals';
|
import { AppealsFile } from '@/app/hooks/react-query/useContentForAppeals';
|
||||||
import { changeAppealContentStatus } from '@/app/actions/contentActions';
|
import { changeAppealContentStatus } from '@/app/actions/contentActions';
|
||||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||||
|
import { AppealInfo } from '@/app/hooks/react-query/useContentForModeration';
|
||||||
|
|
||||||
interface AppealModerationModalProps {
|
interface AppealModerationModalProps {
|
||||||
file: AppealsFile | null;
|
file: AppealInfo | AppealsFile | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +101,7 @@ export default function AppealModerationModal({ file, onClose }: AppealModeratio
|
|||||||
if (!file) return null;
|
if (!file) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-moderation-modal">
|
<>
|
||||||
<h2 className="file-moderation-modal__title">Управление апелляцией</h2>
|
|
||||||
|
|
||||||
<div className="file-moderation-modal__content">
|
|
||||||
{/* Информация о файле */}
|
{/* Информация о файле */}
|
||||||
<div className="file-moderation-modal__info-section">
|
<div className="file-moderation-modal__info-section">
|
||||||
<label className="file-moderation-modal__label">Название файла</label>
|
<label className="file-moderation-modal__label">Название файла</label>
|
||||||
@@ -129,7 +127,9 @@ export default function AppealModerationModal({ file, onClose }: AppealModeratio
|
|||||||
<div className="file-moderation-modal__info-section">
|
<div className="file-moderation-modal__info-section">
|
||||||
<label className="file-moderation-modal__label">Дата создания</label>
|
<label className="file-moderation-modal__label">Дата создания</label>
|
||||||
<p className="file-moderation-modal__value">
|
<p className="file-moderation-modal__value">
|
||||||
{formatDate(file.createdAt)}, {formatDateTime(file.createdAt)}
|
{file.createdAt && (
|
||||||
|
`${formatDate(file.createdAt)}, ${formatDateTime(file.createdAt)}`
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,7 +166,6 @@ export default function AppealModerationModal({ file, onClose }: AppealModeratio
|
|||||||
value={getLabel(selectedStatus)}
|
value={getLabel(selectedStatus)}
|
||||||
callBack={(value: string) => setSelectedStatus(value)}
|
callBack={(value: string) => setSelectedStatus(value)}
|
||||||
>
|
>
|
||||||
{/* <li value="MODERATION">На модерации</li> */}
|
|
||||||
<li value="REJECTED">Заблокировано</li>
|
<li value="REJECTED">Заблокировано</li>
|
||||||
<li value="APPROVED">Одобрено</li>
|
<li value="APPROVED">Одобрено</li>
|
||||||
</DropDownList>
|
</DropDownList>
|
||||||
@@ -189,7 +188,6 @@ export default function AppealModerationModal({ file, onClose }: AppealModeratio
|
|||||||
{changeStatusMutation.isPending ? 'Сохранение...' : 'Сохранить'}
|
{changeStatusMutation.isPending ? 'Сохранение...' : 'Сохранить'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,6 @@ export default function FileModerationModal({ file, onClose }: FileModerationMod
|
|||||||
if (!file) return null;
|
if (!file) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-moderation-modal">
|
|
||||||
<h2 className="file-moderation-modal__title">Управление файлом</h2>
|
|
||||||
|
|
||||||
<div className="file-moderation-modal__content">
|
<div className="file-moderation-modal__content">
|
||||||
{/* Информация о файле */}
|
{/* Информация о файле */}
|
||||||
<div className="file-moderation-modal__info-section">
|
<div className="file-moderation-modal__info-section">
|
||||||
@@ -133,6 +130,5 @@ export default function FileModerationModal({ file, onClose }: FileModerationMod
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ModerationFile } from '@/app/hooks/react-query/useContentForModeration';
|
||||||
|
import FileModerationModal from './FileModerationModal';
|
||||||
|
import AppealModerationModal from './AppealModerationModal';
|
||||||
|
|
||||||
|
interface FileWithAppealModerationModalProps {
|
||||||
|
file: ModerationFile | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabType = 'file' | 'appeal';
|
||||||
|
|
||||||
|
export default function FileWithAppealModerationModal({ file, onClose }: FileWithAppealModerationModalProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('file');
|
||||||
|
|
||||||
|
if (!file) return null;
|
||||||
|
|
||||||
|
const hasAppeal = !!file.moderationInfo?.appealInfo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-moderation-modal tabs-modal">
|
||||||
|
<div className="tabs-modal__header">
|
||||||
|
<h2 className="file-moderation-modal__title">
|
||||||
|
Модерация контента
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="tabs-modal__tabs">
|
||||||
|
<button
|
||||||
|
className={`tabs-modal__tab ${activeTab === 'file' ? 'tabs-modal__tab--active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('file')}
|
||||||
|
>
|
||||||
|
Модерация файла
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`tabs-modal__tab ${activeTab === 'appeal' ? 'tabs-modal__tab--active' : ''} ${!hasAppeal ? 'tabs-modal__tab--disabled' : ''}`}
|
||||||
|
onClick={() => hasAppeal && setActiveTab('appeal')}
|
||||||
|
disabled={!hasAppeal}
|
||||||
|
title={!hasAppeal ? 'Апелляция отсутствует' : ''}
|
||||||
|
>
|
||||||
|
Модерация апелляции
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tabs-modal__content">
|
||||||
|
{activeTab === 'file' && (
|
||||||
|
<FileModerationModal file={file} onClose={onClose} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'appeal' && hasAppeal && (
|
||||||
|
<AppealModerationModal
|
||||||
|
file={file.moderationInfo.appealInfo}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import { downloadFile } from '@/app/actions/contentActions';
|
|||||||
import { useContentForAppeals, AppealsFile } from '@/app/hooks/react-query/useContentForAppeals';
|
import { useContentForAppeals, AppealsFile } from '@/app/hooks/react-query/useContentForAppeals';
|
||||||
import AppealModerationModal from '@/app/ui/content/AppealModerationModal';
|
import AppealModerationModal from '@/app/ui/content/AppealModerationModal';
|
||||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||||
|
import {AppealInfo} from '@/app/hooks/react-query/useContentForModeration';
|
||||||
|
|
||||||
const cutFileExtension = (fileName: string) => {
|
const cutFileExtension = (fileName: string) => {
|
||||||
const lastDotIndex = fileName.lastIndexOf('.');
|
const lastDotIndex = fileName.lastIndexOf('.');
|
||||||
@@ -79,7 +80,7 @@ export default function ContentAppealsTable({ permission }: { permission: number
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||||
const [selectedFile, setSelectedFile] = useState<AppealsFile | null>(null);
|
const [selectedFile, setSelectedFile] = useState<AppealsFile | AppealInfo | null>(null);
|
||||||
const [isFileLoading, setIsFileLoading] = useState(false);
|
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||||
|
|
||||||
const t = useTranslations("Global");
|
const t = useTranslations("Global");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useMemo, useEffect, ReactNode, useCallback } from 'react';
|
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
useReactTable,
|
useReactTable,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
@@ -17,12 +17,12 @@ import DropDownList from '@/app/components/dropDownList';
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import ModalWindow from '@/app/components/modalWindow';
|
import ModalWindow from '@/app/components/modalWindow';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useContentForModeration, ModerationFile, ContentForModeration, ModerationStatus } from '@/app/hooks/react-query/useContentForModeration';
|
import { useContentForModeration, ModerationFile, ModerationStatus } from '@/app/hooks/react-query/useContentForModeration';
|
||||||
import FileModerationModal from './FileModerationModal';
|
|
||||||
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||||
import { getFileType } from '@/app/lib/getFileType';
|
import { getFileType } from '@/app/lib/getFileType';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { downloadFile } from '@/app/actions/contentActions';
|
import { downloadFile } from '@/app/actions/contentActions';
|
||||||
|
import FileWithAppealModerationModal from './FileWithAppealModerationModal';
|
||||||
|
|
||||||
const cutFileExtension = (fileName: string) => {
|
const cutFileExtension = (fileName: string) => {
|
||||||
const lastDotIndex = fileName.lastIndexOf('.');
|
const lastDotIndex = fileName.lastIndexOf('.');
|
||||||
@@ -71,6 +71,23 @@ const StatusBadge = ({ status }: { status: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AppealStatusBadge = ({ status }: { status: string }) => {
|
||||||
|
const statusConfig: Record<string, { className: string; label: string }> = {
|
||||||
|
'REJECTED': { className: 'bg-red-100 text-red-800', label: 'Заблокировано' },
|
||||||
|
'MODERATION': { className: 'bg-yellow-100 text-yellow-800', label: 'На модерации' },
|
||||||
|
'APPROVED': { className: 'bg-green-100 text-green-800', label: 'Одобрено' },
|
||||||
|
'PENDING': { className: 'bg-gray-100 text-gray-800', label: 'На рассмотрении' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status] || { className: 'bg-gray-100 text-gray-800', label: status };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.className}`}>
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function ContentModerationTable({ permission }: { permission: number }) {
|
export default function ContentModerationTable({ permission }: { permission: number }) {
|
||||||
// Состояния
|
// Состояния
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -127,10 +144,6 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(moderationData);
|
|
||||||
}, [moderationData])
|
|
||||||
|
|
||||||
const toggleStatus = (status: ModerationStatus) => {
|
const toggleStatus = (status: ModerationStatus) => {
|
||||||
setSelectedStatuses(prev => {
|
setSelectedStatuses(prev => {
|
||||||
if (prev.includes(status)) {
|
if (prev.includes(status)) {
|
||||||
@@ -305,9 +318,16 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-center">
|
<div className="table-status">
|
||||||
|
<div>
|
||||||
<StatusBadge status={row.original.status} />
|
<StatusBadge status={row.original.status} />
|
||||||
</div>
|
</div>
|
||||||
|
{row.original.moderationInfo.appealInfo?.status && (
|
||||||
|
<div>
|
||||||
|
{t('appeal')}: <AppealStatusBadge status={row.original.moderationInfo.appealInfo?.status} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -401,7 +421,7 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
return (
|
return (
|
||||||
<div className="tanstak-table-wrapper">
|
<div className="tanstak-table-wrapper">
|
||||||
<ModalWindow state={openWindow} callBack={handleCloseModal}>
|
<ModalWindow state={openWindow} callBack={handleCloseModal}>
|
||||||
<FileModerationModal file={selectedFile} onClose={handleCloseModal} />
|
<FileWithAppealModerationModal file={selectedFile} onClose={handleCloseModal} />
|
||||||
</ModalWindow>
|
</ModalWindow>
|
||||||
<h3>
|
<h3>
|
||||||
Таблица модерации контента
|
Таблица модерации контента
|
||||||
|
|||||||
@@ -225,7 +225,8 @@
|
|||||||
"create-stuff": "Create stuff",
|
"create-stuff": "Create stuff",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"has-appeal": "Has appeal"
|
"has-appeal": "Has appeal",
|
||||||
|
"appeal": "Appeal"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
|
|||||||
@@ -225,7 +225,8 @@
|
|||||||
"create-stuff": "Создать сотрудника",
|
"create-stuff": "Создать сотрудника",
|
||||||
"download": "Скачать",
|
"download": "Скачать",
|
||||||
"all": "Все",
|
"all": "Все",
|
||||||
"has-appeal": "Есть апелляция"
|
"has-appeal": "Есть апелляция",
|
||||||
|
"appeal": "Апелляция"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "и",
|
"and": "и",
|
||||||
|
|||||||
Reference in New Issue
Block a user