From 0f2b7095b51503f2b0c64fd910708f3fa0a15a76 Mon Sep 17 00:00:00 2001 From: maxim Date: Fri, 4 Apr 2025 16:33:02 +0300 Subject: [PATCH] [2] upgrade `CreateSightArticle` for `/sight` route --- package.json | 1 + pnpm-lock.yaml | 30 +++++ src/components/CreateSightArticle.tsx | 154 +++++++++++++++++++++++++- 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8b21de9..959e5f7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "jwt-decode": "^4.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", + "react-dropzone": "^14.3.8", "react-hook-form": "^7.30.0", "react-i18next": "^15.4.1", "react-router": "^7.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fe1e28..2b05bc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: react-dom: specifier: ^18.0.0 version: 18.3.1(react@18.3.1) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@18.3.1) react-hook-form: specifier: ^7.30.0 version: 7.54.2(react@18.3.1) @@ -1162,6 +1165,10 @@ packages: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + axios@1.7.9: resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} @@ -1674,6 +1681,10 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2598,6 +2609,12 @@ packages: peerDependencies: react: ^18.3.1 + react-dropzone@14.3.8: + resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-hook-form@7.54.2: resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} engines: {node: '>=18.0.0'} @@ -4433,6 +4450,8 @@ snapshots: atomically@1.7.0: {} + attr-accept@2.2.5: {} + axios@1.7.9: dependencies: follow-redirects: 1.15.9(debug@4.4.0) @@ -5001,6 +5020,10 @@ snapshots: dependencies: flat-cache: 3.2.0 + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -5964,6 +5987,13 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dropzone@14.3.8(react@18.3.1): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 18.3.1 + react-hook-form@7.54.2(react@18.3.1): dependencies: react: 18.3.1 diff --git a/src/components/CreateSightArticle.tsx b/src/components/CreateSightArticle.tsx index dece4bf..b5f6025 100644 --- a/src/components/CreateSightArticle.tsx +++ b/src/components/CreateSightArticle.tsx @@ -4,10 +4,19 @@ import {axiosInstance} from '../providers/data' import {BACKEND_URL} from '../lib/constants' import {useForm, Controller} from 'react-hook-form' import {MarkdownEditor} from './MarkdownEditor' -import React from 'react' +import React, {useState, useCallback} from 'react' +import {useDropzone} from 'react-dropzone' +import {ALLOWED_IMAGE_TYPES, ALLOWED_VIDEO_TYPES} from '../components/media/MediaFormUtils' const MemoizedSimpleMDE = React.memo(MarkdownEditor) +type MediaFile = { + file: File + preview: string + uploading: boolean + mediaId?: number +} + type Props = { parentId: string | number parentResource: string @@ -17,6 +26,7 @@ type Props = { export const CreateSightArticle = ({parentId, parentResource, childResource, title}: Props) => { const theme = useTheme() + const [mediaFiles, setMediaFiles] = useState([]) const { register: registerItem, @@ -39,32 +49,86 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit [], ) + const onDrop = useCallback((acceptedFiles: File[]) => { + const newFiles = acceptedFiles.map((file) => ({ + file, + preview: URL.createObjectURL(file), + uploading: false, + })) + setMediaFiles((prev) => [...prev, ...newFiles]) + }, []) + + const {getRootProps, getInputProps, isDragActive} = useDropzone({ + onDrop, + accept: { + 'image/*': ALLOWED_IMAGE_TYPES, + 'video/*': ALLOWED_VIDEO_TYPES, + }, + multiple: true, + }) + + const uploadMedia = async (mediaFile: MediaFile) => { + const formData = new FormData() + formData.append('media_name', mediaFile.file.name) + formData.append('filename', mediaFile.file.name) + formData.append('type', mediaFile.file.type.startsWith('image/') ? '1' : '2') + formData.append('file', mediaFile.file) + + const response = await axiosInstance.post(`${BACKEND_URL}/media`, formData) + return response.data.id + } + const handleCreate = async (data: {heading: string; body: string}) => { try { + // Создаем статью const response = await axiosInstance.post(`${BACKEND_URL}/${childResource}`, data) const itemId = response.data.id + // Получаем существующие статьи для определения порядкового номера const existingItemsResponse = await axiosInstance.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`) const existingItems = existingItemsResponse.data || [] - const nextPageNum = existingItems.length + 1 + // Привязываем статью к достопримечательности await axiosInstance.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}/`, { [`${childResource}_id`]: itemId, page_num: nextPageNum, }) + // Загружаем все медиа файлы и получаем их ID + const mediaIds = await Promise.all( + mediaFiles.map(async (mediaFile) => { + return await uploadMedia(mediaFile) + }), + ) + + // Привязываем все медиа к статье + await Promise.all( + mediaIds.map((mediaId, index) => + axiosInstance.post(`${BACKEND_URL}/article/${itemId}/media/`, { + media_id: mediaId, + media_order: index + 1, + }), + ), + ) + resetItem() + setMediaFiles([]) window.location.reload() } catch (err: any) { console.error('Error creating item:', err) - if (err?.response) { - console.error('Error response:', err.response.data) - console.error('Error status:', err.response.status) - } } } + const removeMedia = (index: number) => { + setMediaFiles((prev) => { + const newFiles = [...prev] + URL.revokeObjectURL(newFiles[index].preview) + newFiles.splice(index, 1) + return newFiles + }) + } + return ( } /> + {/* Dropzone для медиа файлов */} + + + + {isDragActive ? 'Перетащите файлы сюда...' : 'Перетащите файлы сюда или кликните для выбора'} + + + {/* Превью загруженных файлов */} + + {mediaFiles.map((mediaFile, index) => ( + + {mediaFile.file.type.startsWith('image/') ? ( + {mediaFile.file.name} + ) : ( + + {mediaFile.file.name} + + )} + + + ))} + + +