From 8058be42c177997472ea63418c58148e06394755 Mon Sep 17 00:00:00 2001 From: smanylov Date: Wed, 24 Dec 2025 15:41:06 +0700 Subject: [PATCH] file upload develop --- src/app/[locale]/pages/marking-photo/page.tsx | 23 +-- src/app/actions/definitions.ts | 18 +- src/app/actions/file-upload.ts | 82 ++++++++- src/app/components/upload-section-file.tsx | 156 +++++++++++------- src/app/styles/pages-styles.scss | 2 + src/app/ui/page-title.tsx | 12 ++ src/i18n/messages/en.json | 13 +- src/i18n/messages/ru.json | 13 +- 8 files changed, 235 insertions(+), 84 deletions(-) create mode 100644 src/app/ui/page-title.tsx diff --git a/src/app/[locale]/pages/marking-photo/page.tsx b/src/app/[locale]/pages/marking-photo/page.tsx index 677a7ed..502b9a0 100644 --- a/src/app/[locale]/pages/marking-photo/page.tsx +++ b/src/app/[locale]/pages/marking-photo/page.tsx @@ -1,21 +1,22 @@ -import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table'; import ProtectionSummary from '@/app/ui/marking-page/protection-summary'; -import TestSection from '@/app/ui/marking-page/test-section'; +import PageTitle from '@/app/ui/page-title'; import UploadSectionFile from '@/app/components/upload-section-file'; -import { useTranslations } from 'next-intl'; import FilesTable from '@/app/ui/dashboard/files-table'; +import { FileExtension } from '@/app/actions/definitions'; +import { getAllowedFilesExtensions } from '@/app/actions/file-upload'; + +const ALLOWED_EXTENSIONS: FileExtension[] = ['jpg', 'jpeg', 'png', 'gif', 'bmp']; +const FILE_TYPE = "IMAGE" + +export default async function Page() { + /* const allowedFilesExtensions = await getAllowedFilesExtensions(FILE_TYPE); + console.log(allowedFilesExtensions); */ -export default function Page() { - const t = useTranslations('Global'); return (
-
-

- {t('image-protection')} -

-
+ - + {/* */} {/* */} diff --git a/src/app/actions/definitions.ts b/src/app/actions/definitions.ts index 6b35521..95672ba 100644 --- a/src/app/actions/definitions.ts +++ b/src/app/actions/definitions.ts @@ -24,10 +24,10 @@ export const SignupFormSchema = z .string() .min(2, { error: 'register-error-name' }) .regex(/^[^0-9]*$/, { message: 'register-error-no-digits' }) -/* .refine( - (value) => !/\p{Emoji}/u.test(value), - { message: 'no-emoji-allowed' } - ) */ + /* .refine( + (value) => !/\p{Emoji}/u.test(value), + { message: 'no-emoji-allowed' } + ) */ .trim(), email: z .string() @@ -48,10 +48,10 @@ export const SignupFormSchema = z .min(8, { error: 'register-error-password-symbols' }) .regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter' }) .regex(/[0-9]/, { error: 'register-error-password-one-digit' }) -/* .refine( - (value) => !/\p{Emoji}/u.test(value), - { message: 'no-emoji-allowed' } - ) */ + /* .refine( + (value) => !/\p{Emoji}/u.test(value), + { message: 'no-emoji-allowed' } + ) */ .trim(), confirm_password: z .string(), @@ -78,6 +78,8 @@ export const loginFormSchema = z export const localDevelopmentUrl = 'http://localhost'; +export type FileExtension = 'jpg' | 'jpeg' | 'png' | 'gif' | 'bmp'; + export const testUserData = { fullName: 'test', email: 'test@mail.com', diff --git a/src/app/actions/file-upload.ts b/src/app/actions/file-upload.ts index 1b95387..af336e9 100644 --- a/src/app/actions/file-upload.ts +++ b/src/app/actions/file-upload.ts @@ -2,7 +2,6 @@ import { localDevelopmentUrl } from '@/app/actions/definitions'; import { getSessionData, createSession } from '@/app/actions/session'; -import { number } from 'zod'; const API_BASE_URL = process.env.NODE_ENV === 'development' ? localDevelopmentUrl @@ -51,7 +50,6 @@ export async function fileUpload(messageBody: initMessageBody) { }, message_code: string }; - console.log(parsed); if (parsed.message_desc === 'Operation successful') { return parsed.message_body; @@ -69,6 +67,7 @@ export async function fileUpload(messageBody: initMessageBody) { export async function cancelUpload(udloadId: string) { console.log('cancelUpload'); + console.log(udloadId); try { const response = await fetch(`${API_BASE_URL}/api/v1/data`, { @@ -114,20 +113,52 @@ export async function cancelUpload(udloadId: string) { } export async function chunkUpload(formData: FormData) { - console.log('chunkUpload'); - console.log(`${API_BASE_URL}/api/v1/files/chunk`); - try { const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, { method: 'POST', body: formData }); - console.log(response); + if (response.ok) { + let parsed = await response.json(); + + if (parsed.message_desc === 'Upload cancelled successfully') { + console.log('chunkUpload'); + console.log(parsed.message_body); + return parsed.message_body; + } else { + console.log('error cancel chunkUpload'); + throw parsed; + } + } else { + throw (`${response.status}`); + } + + } catch (error) { + return error + } +} + +export async function checkChunkStatus(upload_id: string) { + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 20004, + message_body: { + action: 'chunks', + upload_id + } + }), + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); if (response.ok) { let parsed = await response.json(); - console.log('response.ok'); console.log(parsed); if (parsed.message_desc === 'Upload cancelled successfully') { @@ -140,6 +171,43 @@ export async function chunkUpload(formData: FormData) { throw (`${response.status}`); } + } catch (error) { + return error + } +} + +export async function getAllowedFilesExtensions(type: string) { + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 20004, + message_body: { + action: 'file_extension', + file_type: type + } + }), + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + + if (response.ok) { + let parsed = await response.json(); + console.log(parsed); + + if (parsed.message_desc === 'Operation successful') { + return parsed.message_body.file_extension; + } else { + console.log('error cancel getAllowedFilesExtensions'); + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { return error } diff --git a/src/app/components/upload-section-file.tsx b/src/app/components/upload-section-file.tsx index 89e1f40..b681511 100644 --- a/src/app/components/upload-section-file.tsx +++ b/src/app/components/upload-section-file.tsx @@ -2,7 +2,9 @@ import React, { useRef, useState, useCallback, ChangeEvent, DragEvent } from 'react'; import { IconShieldAdd } from '@/app/ui/icons/icons'; -import { fileUpload, cancelUpload, chunkUpload } from '@/app/actions/file-upload'; +import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/file-upload'; +import { useTranslations } from 'next-intl'; +import { FileExtension } from '@/app/actions/definitions'; interface SelectedFile { file: File; @@ -23,28 +25,40 @@ interface FileUploadInitResponse { status: string } -type SupportedFormat = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/bmp'; -type FileExtension = 'jpg' | 'jpeg' | 'png' | 'gif' | 'bmp'; +interface UploadSectionFile { + fileType: string + allowedExtensions: FileExtension[] +} -export default function UploadSectionFile({ fileType }: { fileType: string }) { +/* type SupportedFormat = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/bmp'; */ + +export default function UploadSectionFile({ fileType, allowedExtensions }: UploadSectionFile) { const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [error, setError] = useState(null); - const [uploadId, setUploadId] = useState(null); + const [uploadId, setUploadId] = useState(null); + const [isFileUploaded, setIsFileUploaded] = useState(false); - const SUPPORTED_FORMATS: SupportedFormat[] = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/bmp' - ]; + const t = useTranslations('Global'); + console.log('fileType'); + console.log(allowedExtensions); + let SUPPORTED_FORMATS: string[] = [] - const ALLOWED_EXTENSIONS: FileExtension[] = ['jpg', 'jpeg', 'png', 'gif', 'bmp']; + if (fileType === "IMAGE") { + SUPPORTED_FORMATS = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/bmp' + ]; + } + + const ALLOWED_EXTENSIONS: FileExtension[] = allowedExtensions; const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => { - if (!SUPPORTED_FORMATS.includes(file.type as SupportedFormat)) { + if (!SUPPORTED_FORMATS.includes(file.type as string)) { const extension = file.name.split('.').pop()?.toLowerCase(); if (!extension || !ALLOWED_EXTENSIONS.includes(extension as FileExtension)) { return { @@ -171,9 +185,11 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { URL.revokeObjectURL(selectedFile.preview); } - const response = await cancelUpload(uploadId); - console.log(response); + if (uploadId) { + await cancelUpload(uploadId); + } + setIsFileUploaded(false); setSelectedFile(null); setError(null); }; @@ -185,38 +201,21 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { extension: file.name.split('.')[1], file_size: file.file.size }; - console.log("initMessageBody"); const response = await fileUpload(initMessageBody) as FileUploadInitResponse; if (response?.upload_id) { setUploadId(response.upload_id); - console.log(response); - console.log(file.file); - /* { - chunk_size: 5242880 - field_errors: null - file_name: "ooop.jpg" - status: "INITIATED" - total_chunks: 1 - upload_id: "623261f0-f13b-4d06-8df5-365cb19d7ada" - } */ - /* file.file */ const CHUNK_SIZE = response.chunk_size; const totalChunks = response.total_chunks; const fileSize = file.file.size; let start = 0; let chunkIndex = 0; - console.log('response.upload_id'); - console.log(response.upload_id); - try { while (start < fileSize) { const end = Math.min(start + CHUNK_SIZE, fileSize); const chunk = file.file.slice(start, end); - console.log(file.file); - console.log(chunk); const formData = new FormData(); formData.append('upload_id', response.upload_id); @@ -224,10 +223,18 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { formData.append('chunk', chunk); const chunkResponse = await chunkUpload(formData); - console.log(chunkResponse); + if (chunkResponse.message_desc !== 'Chunk uploaded successfully') { + throw 'Er1' + } if (chunkIndex === totalChunks - 1) { - console.log('upload done'); + const chunkStatus = await checkChunkStatus(response.upload_id); + if (chunkStatus.message_body.missing_chunks === 0) { + setUploadId(null); + setIsFileUploaded(true); + } else { + throw 'Er2' + } } start = end; @@ -236,7 +243,8 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { console.log('File uploaded successfully'); } catch (error) { - console.error('Upload error:', error); + setError('Chunk uploaded error ' + error); + console.error('Chunk upload error:', error); } } } @@ -251,7 +259,9 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { return (
-

Загрузить изображение для защиты

+

+ {t('download-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })} +

-

Перетащите фаил сюда

-

или

+

+ {t('drag-the-file-here')} +

+

+ {t('or')} +

`.${e}`).join(',') + } style={{ display: 'none' }} onChange={handleFileInputChange} aria-label="Выбор файла для защиты" @@ -284,11 +300,11 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { onClick={handleButtonClick} type="button" > - Выбрать файлы для защиты + {t('select-files-to-protect')}

- Разрешение изображение: 100x100 - 10000x10000 пикселей + Разрешение изображение: 100x100 - 10000x10000

Размер файла: до 20 МБ @@ -299,7 +315,7 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { {error && (

-

❌ {error}

+

{error}

)} @@ -308,18 +324,31 @@ export default function UploadSectionFile({ fileType }: { fileType: string }) { className="selected-file" >
-

- Файл: {selectedFile.name} -

-

- Размер: {selectedFile.size} -

-

- Стоимость защиты: 0 -

-

- Текущий баланс: 0 -

+
+

+ + {t('file')}: + {selectedFile.name} +

+

+ + {t('size')}: + {selectedFile.size} +

+

+ Стоимость защиты: 0 +

+

+ Текущий баланс: 0 +

+
+
+ {isFileUploaded && ( +
+ {t('file-successfully-uploaded')} +
+ )} +
- Отмена + {isFileUploaded ? t('close') : t('cancel')} +
diff --git a/src/app/styles/pages-styles.scss b/src/app/styles/pages-styles.scss index a08fab2..0b138c2 100644 --- a/src/app/styles/pages-styles.scss +++ b/src/app/styles/pages-styles.scss @@ -492,6 +492,8 @@ &-file-info { text-align: left; + display: flex; + justify-content: space-between; } .uploaded-image { diff --git a/src/app/ui/page-title.tsx b/src/app/ui/page-title.tsx new file mode 100644 index 0000000..9d2fb91 --- /dev/null +++ b/src/app/ui/page-title.tsx @@ -0,0 +1,12 @@ +import { useTranslations } from 'next-intl'; + +export default function PageTitle({ title }: { title: string }) { + const t = useTranslations('Global'); + return ( +
+

+ {t(title)} +

+
+ ) +} \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index ef00ba2..ba0fa26 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -17,7 +17,9 @@ "checks": "Checks", "check": "Checks", "videos": "Videos", + "video": "vides", "audios": "Audio", + "audio": "audio", "images": "Images", "main": "Main", "marking": "Marking", @@ -116,7 +118,16 @@ "view-all": "View all", "days-ago": "days ago", "there-are-no-files-yet": "There are no files yet", - "add": "Add" + "add": "Add", + "file-successfully-uploaded": "File has been successfully uploaded", + "cancel": "Cancel", + "close": "Close", + "upload-file": "Upload file", + "or": "or", + "drag-the-file-here": "Drag the file here", + "select-files-to-protect": "Select files to protect", + "download-for-protection": "Download {fileType} for protection", + "image": "image" }, "Login-register-form": { "and": "and", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index a7fcdfb..458d77a 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -17,7 +17,9 @@ "checks": "Проверки", "check": "Проверок", "videos": "Видео", + "video": "видео", "audios": "Аудио", + "audio": "аудио", "images": "Изображения", "main": "Главная", "marking": "Маркировка", @@ -116,7 +118,16 @@ "view-all": "Посмотреть все", "days-ago": "дней назад", "there-are-no-files-yet": "Файлов пока нет", - "add": "Добавить" + "add": "Добавить", + "file-successfully-uploaded": "Файл успешно загружен", + "cancel": "Отмена", + "close": "Закрыть", + "upload-file": "Загрузить файл", + "or": "или", + "drag-the-file-here": "Перетащите файл сюда", + "select-files-to-protect": "Выбрать файлы для защиты", + "download-for-protection": "Загрузить {fileType} для защиты", + "image": "изображений" }, "Login-register-form": { "and": "и",