From afa94b999c518939049fd8fb2fddb740411757b8 Mon Sep 17 00:00:00 2001 From: maxim Date: Wed, 19 Mar 2025 18:22:53 +0300 Subject: [PATCH] integrate `LinkedItems` into `/article` pages --- src/components/LinkedItems.tsx | 43 +++++++- src/components/MarkdownEditor.tsx | 2 +- src/pages/article/edit.tsx | 7 ++ src/pages/article/show.tsx | 169 +----------------------------- src/pages/article/types.ts | 28 +++++ 5 files changed, 79 insertions(+), 170 deletions(-) create mode 100644 src/pages/article/types.ts diff --git a/src/components/LinkedItems.tsx b/src/components/LinkedItems.tsx index 001374d..bd5b5a6 100644 --- a/src/components/LinkedItems.tsx +++ b/src/components/LinkedItems.tsx @@ -9,6 +9,13 @@ type Field = { data: keyof T } +type ExtraFieldConfig = { + type: 'number' + label: string + minValue: number + maxValue: (linkedItems: any[]) => number +} + type LinkedItemsProps = { parentId: string | number parentResource: string @@ -16,14 +23,16 @@ type LinkedItemsProps = { fields: Field[] title: string type: 'show' | 'edit' + extraField?: ExtraFieldConfig } export const LinkedItems = ({parentId, parentResource, childResource, fields, title, type}: LinkedItemsProps) => { const [items, setItems] = useState([]) const [linkedItems, setLinkedItems] = useState([]) - const [selectedItemId, setSelectedItemId] = useState('') + const [selectedItemId, setSelectedItemId] = useState(null) const [pageNum, setPageNum] = useState(1) const [isLoading, setIsLoading] = useState(true) + const [mediaOrder, setMediaOrder] = useState(1) const theme = useTheme() useEffect(() => { @@ -65,13 +74,18 @@ export const LinkedItems = ({parentI const availableItems = items.filter((item) => !linkedItems.some((linked) => linked.id === item.id)) const linkItem = () => { - if (selectedItemId) { + if (selectedItemId !== null) { const requestData = childResource === 'article' ? { [`${childResource}_id`]: selectedItemId, page_num: pageNum, } + : childResource === 'media' + ? { + [`${childResource}_id`]: selectedItemId, + media_order: mediaOrder, + } : { [`${childResource}_id`]: selectedItemId, } @@ -81,7 +95,7 @@ export const LinkedItems = ({parentI .then(() => { axios.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`).then((response) => { setLinkedItems(response?.data || []) - setSelectedItemId('') + setSelectedItemId(null) if (childResource === 'article') { setPageNum(pageNum + 1) } @@ -107,7 +121,7 @@ export const LinkedItems = ({parentI } return ( - + } sx={{ @@ -160,9 +174,10 @@ export const LinkedItems = ({parentI Добавить {title} Выберите {title} - setSelectedItemId(e.target.value as number)} label={`Выберите ${title}`}> {availableItems.map((item) => ( + {/* {fields.map((field) => item[field.data]).join(' - ')} */} {item[fields[0].data]} ))} @@ -187,6 +202,24 @@ export const LinkedItems = ({parentI )} + {childResource === 'media' && type === 'edit' && ( + + { + const newValue = Number(e.target.value) + const maxValue = linkedItems.length + 1 + const value = Math.max(1, Math.min(newValue, maxValue)) + setMediaOrder(value) + }} + fullWidth + InputLabelProps={{shrink: true}} + /> + + )} + diff --git a/src/components/MarkdownEditor.tsx b/src/components/MarkdownEditor.tsx index ceaa452..263b0fd 100644 --- a/src/components/MarkdownEditor.tsx +++ b/src/components/MarkdownEditor.tsx @@ -66,7 +66,7 @@ const StyledMarkdownEditor = styled('div')(({theme}) => ({ })) export const MarkdownEditor = (props: SimpleMDEReactProps) => ( - + ) diff --git a/src/pages/article/edit.tsx b/src/pages/article/edit.tsx index 2914305..19d4cdb 100644 --- a/src/pages/article/edit.tsx +++ b/src/pages/article/edit.tsx @@ -2,9 +2,12 @@ import {Box, TextField} from '@mui/material' import {Edit} from '@refinedev/mui' import {useForm} from '@refinedev/react-hook-form' import {Controller} from 'react-hook-form' +import {useParams} from 'react-router' import React from 'react' import {MarkdownEditor} from '../../components/MarkdownEditor' +import {LinkedItems} from '../../components/LinkedItems' +import {MediaItem, mediaFields} from './types' import 'easymde/dist/easymde.min.css' const MemoizedSimpleMDE = React.memo(MarkdownEditor) @@ -17,6 +20,8 @@ export const ArticleEdit = () => { formState: {errors}, } = useForm() + const {id: articleId} = useParams<{id: string}>() + const simpleMDEOptions = React.useMemo( () => ({ placeholder: 'Введите контент в формате Markdown...', @@ -56,6 +61,8 @@ export const ArticleEdit = () => { /> )} /> + + {articleId && type="edit" parentId={articleId} parentResource="article" childResource="media" fields={mediaFields} title="медиа" />} ) diff --git a/src/pages/article/show.tsx b/src/pages/article/show.tsx index ae2c7b9..e0efc52 100644 --- a/src/pages/article/show.tsx +++ b/src/pages/article/show.tsx @@ -1,118 +1,18 @@ -import {Stack, Typography, Grid2 as Grid, Button, MenuItem, Select, FormControl, InputLabel, TextField, Card, CardMedia, CardContent, CardActions} from '@mui/material' +import {Stack, Typography} from '@mui/material' import {useShow} from '@refinedev/core' import {Show, TextFieldComponent} from '@refinedev/mui' - -import {useEffect, useState} from 'react' -import axios from 'axios' - -import {BACKEND_URL} from '../../lib/constants' - -type MediaItem = { - id: string - filename: string - media_type: string -} +import {LinkedItems} from '../../components/LinkedItems' +import {MediaItem, articleFields, mediaFields} from './types' export const ArticleShow = () => { const {query} = useShow({}) const {data, isLoading} = query const record = data?.data - const [media, setMedia] = useState([]) - const [linkedMedia, setLinkedMedia] = useState([]) - const [selectedMediaId, setSelectedMediaId] = useState('') - const [mediaOrder, setMediaOrder] = useState(1) - const [mediaLoading, setMediaLoading] = useState(true) - - useEffect(() => { - if (record?.id) { - axios - .get(`${BACKEND_URL}/article/${record.id}/media`) - .then((response) => { - setLinkedMedia(response?.data || []) - }) - .catch(() => { - setLinkedMedia([]) - }) - } - }, [record?.id]) - - useEffect(() => { - axios - .get(`${BACKEND_URL}/media`) - .then((response) => { - setMedia(response?.data || []) - setMediaLoading(false) - }) - .catch(() => { - setMedia([]) - setMediaLoading(false) - }) - }, []) - - const availableMedia = media.filter((mediaItem) => !linkedMedia.some((linkedItem) => linkedItem.id === mediaItem.id)) - - const linkMedia = () => { - if (selectedMediaId) { - const requestData = { - media_id: selectedMediaId, - media_order: mediaOrder, - } - - axios - .post(`${BACKEND_URL}/article/${record?.id}/media`, requestData, { - headers: { - accept: 'application/json', - 'Content-Type': 'application/json', - }, - }) - .then(() => { - axios - .get(`${BACKEND_URL}/article/${record?.id}/media`) - .then((response) => { - setLinkedMedia(response?.data || []) - setMediaOrder(mediaOrder + 1) - setSelectedMediaId('') - }) - .catch(() => { - setLinkedMedia([]) - }) - }) - .catch((error) => { - console.error('Error linking media:', error) - }) - } - } - - const deleteMedia = (mediaId: string) => { - axios - .delete(`${BACKEND_URL}/article/${record?.id}/media`, { - data: {media_id: mediaId}, - }) - .then(() => { - setLinkedMedia((prevMedia) => prevMedia.filter((item) => item.id !== mediaId)) - }) - .catch((error) => { - console.error('Error deleting media:', error) - }) - } - - const fields = [ - // {label: 'ID', data: 'id'}, - {label: 'Заголовок', data: 'heading'}, - {label: 'Контент', data: 'body'}, - ] - - const mediaFields = [ - // {label: 'ID', data: 'id' as keyof MediaItem}, - {label: 'Имя', data: 'filename' as keyof MediaItem}, - {label: 'Тип', data: 'media_type' as keyof MediaItem}, - ] - return ( - {fields.map(({label, data}) => ( + {articleFields.map(({label, data}) => ( {label} @@ -121,66 +21,7 @@ export const ArticleShow = () => { ))} - - - Медиа - - - {mediaLoading ? ( - Загрузка медиа... - ) : linkedMedia.length > 0 ? ( - linkedMedia.map((mediaItem) => ( - - `2px solid ${theme.palette.divider}`, - }} - > - - - {mediaFields.map(({label, data}) => ( - - {label}: {mediaItem?.[data]} - - ))} - - - - - - - )) - ) : ( - Нет привязанных медиа - )} - - - {/* sx={{width: '650px'}} */} - - {' '} - - Привязать медиа - - - Медиа - - - setMediaOrder(Number(e.target.value))} fullWidth InputLabelProps={{shrink: true}} /> - - - + {record?.id && parentId={record.id} parentResource="article" childResource="media" fields={mediaFields} title="медиа" type="show" />} ) diff --git a/src/pages/article/types.ts b/src/pages/article/types.ts new file mode 100644 index 0000000..22e6b8e --- /dev/null +++ b/src/pages/article/types.ts @@ -0,0 +1,28 @@ +export type MediaItem = { + id: number + filename: string + media_type: string + media_order?: number +} + +export type ArticleItem = { + id: number + heading: string + body: string +} + +export type FieldType = { + label: string + data: keyof T + render?: (value: any) => React.ReactNode +} + +export const articleFields: Array> = [ + {label: 'Заголовок', data: 'heading'}, + {label: 'Контент', data: 'body'}, +] + +export const mediaFields: Array> = [ + {label: 'Имя', data: 'filename'}, + {label: 'Тип', data: 'media_type'}, +]