[2] upgrade CreateSightArticle for /sight route

This commit is contained in:
maxim
2025-04-04 16:33:02 +03:00
parent 18dda0cb0b
commit 0f2b7095b5
3 changed files with 179 additions and 6 deletions

View File

@ -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<MediaFile[]>([])
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 (
<Accordion>
<AccordionSummary
@ -96,6 +160,82 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit
<Controller control={controlItem} name="body" rules={{required: 'Это поле является обязательным'}} defaultValue="" render={({field: {onChange, value}}) => <MemoizedSimpleMDE value={value} onChange={onChange} options={simpleMDEOptions} className="my-markdown-editor" />} />
{/* Dropzone для медиа файлов */}
<Box sx={{mt: 2, mb: 2}}>
<Box
{...getRootProps()}
sx={{
border: '2px dashed',
borderColor: isDragActive ? 'primary.main' : 'grey.300',
borderRadius: 1,
p: 2,
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: 'primary.main',
},
}}
>
<input {...getInputProps()} />
<Typography>{isDragActive ? 'Перетащите файлы сюда...' : 'Перетащите файлы сюда или кликните для выбора'}</Typography>
</Box>
{/* Превью загруженных файлов */}
<Box sx={{mt: 2, display: 'flex', flexWrap: 'wrap', gap: 1}}>
{mediaFiles.map((mediaFile, index) => (
<Box
key={mediaFile.preview}
sx={{
position: 'relative',
width: 100,
height: 100,
}}
>
{mediaFile.file.type.startsWith('image/') ? (
<img
src={mediaFile.preview}
alt={mediaFile.file.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
) : (
<Box
sx={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'grey.200',
}}
>
<Typography variant="caption">{mediaFile.file.name}</Typography>
</Box>
)}
<Button
size="small"
color="error"
onClick={() => removeMedia(index)}
sx={{
position: 'absolute',
top: 0,
right: 0,
minWidth: 'auto',
width: 20,
height: 20,
p: 0,
}}
>
×
</Button>
</Box>
))}
</Box>
</Box>
<Box sx={{mt: 2, display: 'flex', gap: 2}}>
<Button variant="contained" color="primary" type="submit">
Создать
@ -104,6 +244,8 @@ export const CreateSightArticle = ({parentId, parentResource, childResource, tit
variant="outlined"
onClick={() => {
resetItem()
mediaFiles.forEach((file) => URL.revokeObjectURL(file.preview))
setMediaFiles([])
}}
>
Очистить