add file preview and download file
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
@@ -5,16 +5,6 @@ import { API_BASE_URL } from '@/app/actions/definitions';
|
|||||||
|
|
||||||
export async function fetchModerationContentList(page?: number, size?: number, sortBy?: string, sortDirection?: 'asc' | 'desc' | string) {
|
export async function fetchModerationContentList(page?: number, size?: number, sortBy?: string, sortDirection?: 'asc' | 'desc' | string) {
|
||||||
const token = await getSessionData('token');
|
const token = await getSessionData('token');
|
||||||
console.log('fetchModerationContentList');
|
|
||||||
/* console.log(
|
|
||||||
{
|
|
||||||
action: 'files_for_moderation',
|
|
||||||
page: page,
|
|
||||||
page_size: size,
|
|
||||||
sort_by: sortBy || '',
|
|
||||||
sort_direction: sortDirection || 'asc',
|
|
||||||
}
|
|
||||||
) */
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
|
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
|
||||||
@@ -26,11 +16,8 @@ export async function fetchModerationContentList(page?: number, size?: number, s
|
|||||||
action: 'files_for_moderation',
|
action: 'files_for_moderation',
|
||||||
page: page,
|
page: page,
|
||||||
page_size: size,
|
page_size: size,
|
||||||
/* sort_by: sortBy || '',
|
sort_by: sortBy || '',
|
||||||
sort_order: sortDirection || 'asc', */
|
sort_order: sortDirection || 'asc',
|
||||||
/* full_name: nameQuery, */
|
|
||||||
/* fileName: '',
|
|
||||||
userId: '' */
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -42,7 +29,6 @@ export async function fetchModerationContentList(page?: number, size?: number, s
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const parsed = await response.json();
|
const parsed = await response.json();
|
||||||
console.log(parsed);
|
|
||||||
|
|
||||||
if (parsed.message_code === 0) {
|
if (parsed.message_code === 0) {
|
||||||
return parsed.message_body.message_body;
|
return parsed.message_body.message_body;
|
||||||
@@ -60,10 +46,9 @@ export async function fetchModerationContentList(page?: number, size?: number, s
|
|||||||
|
|
||||||
export async function changeModerationContentStatus(fileId?: string, fileStatus?: string) {
|
export async function changeModerationContentStatus(fileId?: string, fileStatus?: string) {
|
||||||
const token = await getSessionData('token');
|
const token = await getSessionData('token');
|
||||||
console.log(`changeModerationContentStatus - ${fileId} - ${fileStatus}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
|
const response = await fetch(`${API_BASE_URL}/api/admin/control`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
|
||||||
|
|
||||||
|
export const FileTypeIcon = ({ type }: { type: string }) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'image':
|
||||||
|
return <span className="color-image">
|
||||||
|
<IconImageFile />
|
||||||
|
</span>;
|
||||||
|
case 'video':
|
||||||
|
return <span className="color-video">
|
||||||
|
<IconVideoFile />
|
||||||
|
</span>;
|
||||||
|
case 'audio':
|
||||||
|
return <span className="color-audio">
|
||||||
|
<IconAudioFile />
|
||||||
|
</span>;
|
||||||
|
default:
|
||||||
|
return <span className="color-document">
|
||||||
|
<IconDocument />
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,6 +10,8 @@ export interface ModerationFile {
|
|||||||
userId: number;
|
userId: number;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
status: 'BLOCKED' | 'MODERATION' | string;
|
status: 'BLOCKED' | 'MODERATION' | string;
|
||||||
|
image: string;
|
||||||
|
downloadUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentForModeration {
|
export interface ContentForModeration {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export const getFileType = (fileName: string): 'image' | 'video' | 'audio' | 'document' | 'unknown' => {
|
||||||
|
const lastDotIndex = fileName.lastIndexOf('.');
|
||||||
|
|
||||||
|
if (lastDotIndex <= 0) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = fileName.substring(lastDotIndex + 1).toLowerCase();
|
||||||
|
|
||||||
|
const allowedExtensions = {
|
||||||
|
images: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico'],
|
||||||
|
videos: ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv', 'flv', 'wmv'],
|
||||||
|
audios: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'],
|
||||||
|
documents: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'csv', 'md']
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allowedExtensions.images.includes(extension)) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedExtensions.videos.includes(extension)) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedExtensions.audios.includes(extension)) {
|
||||||
|
return 'audio';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedExtensions.documents.includes(extension)) {
|
||||||
|
return 'document';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
@@ -1199,6 +1199,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-image {
|
||||||
|
color: v.$color-image;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-video {
|
||||||
|
color: v.$color-video;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-audio {
|
||||||
|
color: v.$color-audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-document {
|
||||||
|
color: v.$color-document;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ interface FileModerationModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'BLOCKED':
|
||||||
|
return 'Заблокировано';
|
||||||
|
case 'MODERATION':
|
||||||
|
return 'На модерации';
|
||||||
|
case 'ACTIVE':
|
||||||
|
return 'Одобрено';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const StatusBadge = ({ status }: { status: string }) => {
|
const StatusBadge = ({ status }: { status: string }) => {
|
||||||
const getBadgeClass = () => {
|
const getBadgeClass = () => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -19,29 +32,15 @@ const StatusBadge = ({ status }: { status: string }) => {
|
|||||||
return 'status-badge--blocked';
|
return 'status-badge--blocked';
|
||||||
case 'MODERATION':
|
case 'MODERATION':
|
||||||
return 'status-badge--moderation';
|
return 'status-badge--moderation';
|
||||||
case 'APPROVED':
|
case 'ACTIVE':
|
||||||
return 'status-badge--approved';
|
return 'status-badge--approved';
|
||||||
default:
|
default:
|
||||||
return 'status-badge--default';
|
return 'status-badge--default';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLabel = () => {
|
|
||||||
switch (status) {
|
|
||||||
case 'BLOCKED':
|
|
||||||
return 'Заблокировано';
|
|
||||||
case 'MODERATION':
|
|
||||||
return 'На модерации';
|
|
||||||
case 'APPROVED':
|
|
||||||
return 'Одобрено';
|
|
||||||
default:
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={getBadgeClass()}>
|
<span className={getBadgeClass()}>
|
||||||
{getLabel()}
|
{getLabel(status)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -107,11 +106,12 @@ export default function FileModerationModal({ file, onClose }: FileModerationMod
|
|||||||
<div className="file-moderation-modal__status-change-section">
|
<div className="file-moderation-modal__status-change-section">
|
||||||
<label className="file-moderation-modal__status-change-label">Изменить статус</label>
|
<label className="file-moderation-modal__status-change-label">Изменить статус</label>
|
||||||
<DropDownList
|
<DropDownList
|
||||||
value={selectedStatus}
|
value={getLabel(selectedStatus)}
|
||||||
callBack={(value: string) => setSelectedStatus(value)}
|
callBack={(value: string) => setSelectedStatus(value)}
|
||||||
>
|
>
|
||||||
<li value="MODERATION">На модерации</li>
|
<li value="MODERATION">На модерации</li>
|
||||||
<li value="BLOCKED">Заблокировано</li>
|
<li value="BLOCKED">Заблокировано</li>
|
||||||
|
<li value="ACTIVE">Одобрено</li>
|
||||||
</DropDownList>
|
</DropDownList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
SortingState,
|
SortingState,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } from '@/app/ui/icons/icons';
|
import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload } from '@/app/ui/icons/icons';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import DropDownList from '@/app/components/dropDownList';
|
import DropDownList from '@/app/components/dropDownList';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
@@ -19,6 +19,15 @@ import ModalWindow from '@/app/components/modalWindow';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useContentForModeration, ModerationFile, ContentForModeration } from '@/app/hooks/react-query/useContentForModeration';
|
import { useContentForModeration, ModerationFile, ContentForModeration } from '@/app/hooks/react-query/useContentForModeration';
|
||||||
import FileModerationModal from './FileModerationModal';
|
import FileModerationModal from './FileModerationModal';
|
||||||
|
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||||
|
import { getFileType } from '@/app/lib/getFileType';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
const cutFileExtension = (fileName: string) => {
|
||||||
|
const lastDotIndex = fileName.lastIndexOf('.');
|
||||||
|
const extension = fileName.substring(lastDotIndex + 1);
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
const cutFileName = (fileName: string) => {
|
const cutFileName = (fileName: string) => {
|
||||||
const MAX_FILE_NAME_LENGTH = 30;
|
const MAX_FILE_NAME_LENGTH = 30;
|
||||||
@@ -69,8 +78,8 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||||
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
|
|
||||||
const [selectedFile, setSelectedFile] = useState<ModerationFile | null>(null);
|
const [selectedFile, setSelectedFile] = useState<ModerationFile | null>(null);
|
||||||
|
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||||
|
|
||||||
const t = useTranslations("Global");
|
const t = useTranslations("Global");
|
||||||
|
|
||||||
@@ -82,7 +91,7 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
|
|
||||||
const sort = sorting[0];
|
const sort = sorting[0];
|
||||||
const sortByMap: Record<string, string> = {
|
const sortByMap: Record<string, string> = {
|
||||||
'fileName': 'fileName',
|
'fileName': 'originalFileName',
|
||||||
'userId': 'userId',
|
'userId': 'userId',
|
||||||
'status': 'status',
|
'status': 'status',
|
||||||
'createdAt': 'createdAt',
|
'createdAt': 'createdAt',
|
||||||
@@ -124,6 +133,36 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleDownload = async (file: any) => {
|
||||||
|
setIsFileLoading(true);
|
||||||
|
console.log(file.downloadUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(file.downloadUrl);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Download failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = file._original?.fileName || file.fileName || `file-${file.id}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
toast.success(`${t('file-is-downloading')} - ${file.fileName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download error:', error);
|
||||||
|
toast.error(t('failed-to-download-file'));
|
||||||
|
} finally {
|
||||||
|
setIsFileLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<ModerationFile>[]>(() => {
|
const columns = useMemo<ColumnDef<ModerationFile>[]>(() => {
|
||||||
const allColumns: ColumnDef<ModerationFile>[] = [
|
const allColumns: ColumnDef<ModerationFile>[] = [
|
||||||
{
|
{
|
||||||
@@ -147,9 +186,32 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center table-item">
|
<div className="flex items-center space-x-2 table-item table-item-file-name">
|
||||||
|
{getFileType(row.original.fileName) === 'image' && row.original?.image.length !== 0 ? (
|
||||||
|
<span
|
||||||
|
className="table-item-file-name-image-wrapper"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
sizes="40px"
|
||||||
|
src={row.original?.image}
|
||||||
|
alt={row.original?.fileName}
|
||||||
|
unoptimized
|
||||||
|
fill
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.src = '/images/no-image.png';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<FileTypeIcon type={row.original.fileName} />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
<div className="font-medium w-full truncate" title={row.original.fileName}>
|
<div className="font-medium w-full truncate" title={row.original.fileName}>
|
||||||
|
<span className="font-semibold">
|
||||||
{cutFileName(row.original.fileName)}
|
{cutFileName(row.original.fileName)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -207,7 +269,7 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
/* {
|
||||||
accessorKey: 'moderationInfo.hasActiveAppeal',
|
accessorKey: 'moderationInfo.hasActiveAppeal',
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<div className="column">
|
<div className="column">
|
||||||
@@ -236,7 +298,7 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}
|
} */
|
||||||
];
|
];
|
||||||
|
|
||||||
if (permission === 3) {
|
if (permission === 3) {
|
||||||
@@ -253,6 +315,15 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
>
|
>
|
||||||
<IconEye />
|
<IconEye />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownload(row.original)}
|
||||||
|
disabled={isFileLoading}
|
||||||
|
className="table-action-download"
|
||||||
|
title={t('download')}
|
||||||
|
>
|
||||||
|
<IconFileDownload />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -222,7 +222,8 @@
|
|||||||
"delete-template-warning": "Are you sure you want to delete this template?",
|
"delete-template-warning": "Are you sure you want to delete this template?",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"create-stuff": "Create stuff"
|
"create-stuff": "Create stuff",
|
||||||
|
"download": "Download"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
|
|||||||
@@ -222,7 +222,8 @@
|
|||||||
"delete-template-warning": "Вы уверены, что хотите удалить шаблон?",
|
"delete-template-warning": "Вы уверены, что хотите удалить шаблон?",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"create-stuff": "Создать сотрудника"
|
"create-stuff": "Создать сотрудника",
|
||||||
|
"download": "Скачать"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "и",
|
"and": "и",
|
||||||
|
|||||||
Reference in New Issue
Block a user