From df82b7e451c2181e2dc85868935dc0c658518533 Mon Sep 17 00:00:00 2001 From: smanylov Date: Wed, 27 May 2026 12:56:13 +0700 Subject: [PATCH] add file preview and download file --- public/images/no-image.png | Bin 0 -> 2595 bytes src/app/actions/contentActions.ts | 21 +---- src/app/components/FileTypeIcon.tsx | 22 +++++ .../react-query/useContentForModeration.ts | 2 + src/app/lib/getFileType.ts | 34 +++++++ src/app/styles/global-styles.scss | 18 +++- src/app/ui/content/FileModerationModal.tsx | 34 +++---- .../ui/content/content-moderation-table.tsx | 87 ++++++++++++++++-- src/i18n/messages/en.json | 3 +- src/i18n/messages/ru.json | 3 +- 10 files changed, 178 insertions(+), 46 deletions(-) create mode 100644 public/images/no-image.png create mode 100644 src/app/components/FileTypeIcon.tsx create mode 100644 src/app/lib/getFileType.ts diff --git a/public/images/no-image.png b/public/images/no-image.png new file mode 100644 index 0000000000000000000000000000000000000000..942404a7accdc02b89a3496bb53c7ef64fff2867 GIT binary patch literal 2595 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 X!TRU`fB!8+(V_MRg!$L+KA8ItVCM-Y literal 0 HcmV?d00001 diff --git a/src/app/actions/contentActions.ts b/src/app/actions/contentActions.ts index 55604cc..a8015ef 100644 --- a/src/app/actions/contentActions.ts +++ b/src/app/actions/contentActions.ts @@ -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) { 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 { 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', page: page, page_size: size, -/* sort_by: sortBy || '', - sort_order: sortDirection || 'asc', */ - /* full_name: nameQuery, */ - /* fileName: '', - userId: '' */ + sort_by: sortBy || '', + sort_order: sortDirection || 'asc', } }), headers: { @@ -42,7 +29,6 @@ export async function fetchModerationContentList(page?: number, size?: number, s if (response.ok) { const parsed = await response.json(); - console.log(parsed); if (parsed.message_code === 0) { 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) { const token = await getSessionData('token'); - console.log(`changeModerationContentStatus - ${fileId} - ${fileStatus}`); try { - const response = await fetch(`${API_BASE_URL}/api/admin/info`, { + const response = await fetch(`${API_BASE_URL}/api/admin/control`, { method: 'POST', body: JSON.stringify({ version: 1, diff --git a/src/app/components/FileTypeIcon.tsx b/src/app/components/FileTypeIcon.tsx new file mode 100644 index 0000000..3b57350 --- /dev/null +++ b/src/app/components/FileTypeIcon.tsx @@ -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 + + ; + case 'video': + return + + ; + case 'audio': + return + + ; + default: + return + + ; + } +}; \ No newline at end of file diff --git a/src/app/hooks/react-query/useContentForModeration.ts b/src/app/hooks/react-query/useContentForModeration.ts index d4d5077..91d2ed9 100644 --- a/src/app/hooks/react-query/useContentForModeration.ts +++ b/src/app/hooks/react-query/useContentForModeration.ts @@ -10,6 +10,8 @@ export interface ModerationFile { userId: number; fileId: string; status: 'BLOCKED' | 'MODERATION' | string; + image: string; + downloadUrl: string; } export interface ContentForModeration { diff --git a/src/app/lib/getFileType.ts b/src/app/lib/getFileType.ts new file mode 100644 index 0000000..450badd --- /dev/null +++ b/src/app/lib/getFileType.ts @@ -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'; +} \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index 6e8f9e1..87f4b79 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -356,7 +356,7 @@ flex-wrap: wrap; gap: 12px; -/* &-actions { + /* &-actions { display: flex; gap: 12px; flex-wrap: wrap; @@ -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 { diff --git a/src/app/ui/content/FileModerationModal.tsx b/src/app/ui/content/FileModerationModal.tsx index d03f16a..7e1c791 100644 --- a/src/app/ui/content/FileModerationModal.tsx +++ b/src/app/ui/content/FileModerationModal.tsx @@ -12,6 +12,19 @@ interface FileModerationModalProps { 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 getBadgeClass = () => { switch (status) { @@ -19,29 +32,15 @@ const StatusBadge = ({ status }: { status: string }) => { return 'status-badge--blocked'; case 'MODERATION': return 'status-badge--moderation'; - case 'APPROVED': + case 'ACTIVE': return 'status-badge--approved'; default: return 'status-badge--default'; } }; - - const getLabel = () => { - switch (status) { - case 'BLOCKED': - return 'Заблокировано'; - case 'MODERATION': - return 'На модерации'; - case 'APPROVED': - return 'Одобрено'; - default: - return status; - } - }; - return ( - {getLabel()} + {getLabel(status)} ); }; @@ -107,11 +106,12 @@ export default function FileModerationModal({ file, onClose }: FileModerationMod
setSelectedStatus(value)} >
  • На модерации
  • Заблокировано
  • +
  • Одобрено
  • diff --git a/src/app/ui/content/content-moderation-table.tsx b/src/app/ui/content/content-moderation-table.tsx index d448359..c430e1f 100644 --- a/src/app/ui/content/content-moderation-table.tsx +++ b/src/app/ui/content/content-moderation-table.tsx @@ -11,7 +11,7 @@ import { SortingState, ColumnFiltersState, } 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 DropDownList from '@/app/components/dropDownList'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -19,6 +19,15 @@ import ModalWindow from '@/app/components/modalWindow'; import { toast } from 'sonner'; import { useContentForModeration, ModerationFile, ContentForModeration } from '@/app/hooks/react-query/useContentForModeration'; 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 MAX_FILE_NAME_LENGTH = 30; @@ -69,8 +78,8 @@ export default function ContentModerationTable({ permission }: { permission: num pageSize: 10, }); const [openWindow, setOpenWindow] = useState(false); - const [openWindowChildren, setOpenWindowChildren] = useState(null); const [selectedFile, setSelectedFile] = useState(null); + const [isFileLoading, setIsFileLoading] = useState(false); const t = useTranslations("Global"); @@ -82,7 +91,7 @@ export default function ContentModerationTable({ permission }: { permission: num const sort = sorting[0]; const sortByMap: Record = { - 'fileName': 'fileName', + 'fileName': 'originalFileName', 'userId': 'userId', 'status': 'status', 'createdAt': 'createdAt', @@ -124,6 +133,36 @@ export default function ContentModerationTable({ permission }: { permission: num 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[]>(() => { const allColumns: ColumnDef[] = [ { @@ -147,9 +186,32 @@ export default function ContentModerationTable({ permission }: { permission: num ), cell: ({ row }) => ( -
    -
    - {cutFileName(row.original.fileName)} +
    + {getFileType(row.original.fileName) === 'image' && row.original?.image.length !== 0 ? ( + + {row.original?.fileName} { + const target = e.target as HTMLImageElement; + target.src = '/images/no-image.png'; + }} + /> + + ) : ( + + )} +
    +
    + + {cutFileName(row.original.fileName)} + +
    ), @@ -207,7 +269,7 @@ export default function ContentModerationTable({ permission }: { permission: num
    ), }, - { + /* { accessorKey: 'moderationInfo.hasActiveAppeal', header: ({ column }) => (
    @@ -236,7 +298,7 @@ export default function ContentModerationTable({ permission }: { permission: num )}
    ), - } + } */ ]; if (permission === 3) { @@ -253,6 +315,15 @@ export default function ContentModerationTable({ permission }: { permission: num > + +
    ), diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index f02b0ac..142c588 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -222,7 +222,8 @@ "delete-template-warning": "Are you sure you want to delete this template?", "save": "Save", "delete": "Delete", - "create-stuff": "Create stuff" + "create-stuff": "Create stuff", + "download": "Download" }, "Login-register-form": { "and": "and", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index db4004c..36d1bd2 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -222,7 +222,8 @@ "delete-template-warning": "Вы уверены, что хотите удалить шаблон?", "save": "Сохранить", "delete": "Удалить", - "create-stuff": "Создать сотрудника" + "create-stuff": "Создать сотрудника", + "download": "Скачать" }, "Login-register-form": { "and": "и",