add preview
functionality for /article
route
This commit is contained in:
parent
b105bfe395
commit
2fe19516f6
@ -28,6 +28,7 @@
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hook-form": "^7.30.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.0.2",
|
||||
"react-simplemde-editor": "^5.2.0"
|
||||
},
|
||||
|
658
pnpm-lock.yaml
658
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,9 @@
|
||||
import {Box, TextField} from '@mui/material'
|
||||
import {Box, TextField, Typography, Paper} from '@mui/material'
|
||||
import {Create} from '@refinedev/mui'
|
||||
import {useForm} from '@refinedev/react-hook-form'
|
||||
import {Controller} from 'react-hook-form'
|
||||
import React from 'react'
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import {MarkdownEditor} from '../../components/MarkdownEditor'
|
||||
import 'easymde/dist/easymde.min.css'
|
||||
@ -15,6 +16,7 @@ export const ArticleCreate = () => {
|
||||
refineCore: {formLoading},
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
formState: {errors},
|
||||
} = useForm({
|
||||
refineCoreProps: {
|
||||
@ -22,6 +24,21 @@ export const ArticleCreate = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const [preview, setPreview] = useState('')
|
||||
const [headingPreview, setHeadingPreview] = useState('')
|
||||
|
||||
// Следим за изменениями в полях body и heading
|
||||
const bodyContent = watch('body')
|
||||
const headingContent = watch('heading')
|
||||
|
||||
useEffect(() => {
|
||||
setPreview(bodyContent || '')
|
||||
}, [bodyContent])
|
||||
|
||||
useEffect(() => {
|
||||
setHeadingPreview(headingContent || '')
|
||||
}, [headingContent])
|
||||
|
||||
const simpleMDEOptions = React.useMemo(
|
||||
() => ({
|
||||
placeholder: 'Введите контент в формате Markdown...',
|
||||
@ -32,7 +49,9 @@ export const ArticleCreate = () => {
|
||||
|
||||
return (
|
||||
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
||||
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
||||
<Box sx={{display: 'flex', gap: 2}}>
|
||||
{/* Форма создания */}
|
||||
<Box component="form" sx={{flex: 1, display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
||||
<TextField
|
||||
{...register('heading', {
|
||||
required: 'Это поле является обязательным',
|
||||
@ -43,24 +62,86 @@ export const ArticleCreate = () => {
|
||||
fullWidth
|
||||
InputLabelProps={{shrink: true}}
|
||||
type="text"
|
||||
label={'Заголовок *'}
|
||||
label="Заголовок *"
|
||||
name="heading"
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="body"
|
||||
rules={{required: 'Это поле является обязательным'}}
|
||||
defaultValue=""
|
||||
render={({field: {onChange, value}}) => (
|
||||
<MemoizedSimpleMDE
|
||||
value={value} // markdown
|
||||
onChange={onChange}
|
||||
options={simpleMDEOptions}
|
||||
className="my-markdown-editor"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller control={control} name="body" rules={{required: 'Это поле является обязательным'}} defaultValue="" render={({field: {onChange, value}}) => <MemoizedSimpleMDE value={value} onChange={onChange} options={simpleMDEOptions} className="my-markdown-editor" />} />
|
||||
</Box>
|
||||
|
||||
{/* Блок предпросмотра */}
|
||||
<Paper
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 2,
|
||||
maxHeight: 'calc(100vh - 200px)',
|
||||
overflowY: 'auto',
|
||||
position: 'sticky',
|
||||
top: 16,
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'),
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom color="primary">
|
||||
Предпросмотр
|
||||
</Typography>
|
||||
|
||||
{/* Заголовок статьи */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
sx={{
|
||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{headingPreview}
|
||||
</Typography>
|
||||
|
||||
{/* Markdown контент */}
|
||||
<Box
|
||||
sx={{
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: 1,
|
||||
},
|
||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
color: 'primary.main',
|
||||
mt: 2,
|
||||
mb: 1,
|
||||
},
|
||||
'& p': {
|
||||
mb: 2,
|
||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
||||
},
|
||||
'& a': {
|
||||
color: 'primary.main',
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'primary.main',
|
||||
pl: 2,
|
||||
my: 2,
|
||||
color: 'text.secondary',
|
||||
},
|
||||
'& code': {
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100'),
|
||||
p: 0.5,
|
||||
borderRadius: 0.5,
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown>{preview}</ReactMarkdown>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Create>
|
||||
)
|
||||
|
@ -1,13 +1,16 @@
|
||||
import {Box, TextField} from '@mui/material'
|
||||
import {Box, TextField, Typography, Paper} 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 React, {useState, useEffect} from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import {useList} from '@refinedev/core'
|
||||
|
||||
import {MarkdownEditor} from '../../components/MarkdownEditor'
|
||||
import {LinkedItems} from '../../components/LinkedItems'
|
||||
import {MediaItem, mediaFields} from './types'
|
||||
import {TOKEN_KEY} from '../../authProvider'
|
||||
import 'easymde/dist/easymde.min.css'
|
||||
|
||||
const MemoizedSimpleMDE = React.memo(MarkdownEditor)
|
||||
@ -17,10 +20,33 @@ export const ArticleEdit = () => {
|
||||
saveButtonProps,
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
formState: {errors},
|
||||
} = useForm()
|
||||
|
||||
const {id: articleId} = useParams<{id: string}>()
|
||||
const [preview, setPreview] = useState('')
|
||||
const [headingPreview, setHeadingPreview] = useState('')
|
||||
|
||||
// Получаем привязанные медиа
|
||||
const {data: mediaData} = useList<MediaItem>({
|
||||
resource: `article/${articleId}/media`,
|
||||
queryOptions: {
|
||||
enabled: !!articleId,
|
||||
},
|
||||
})
|
||||
|
||||
// Следим за изменениями в полях body и heading
|
||||
const bodyContent = watch('body')
|
||||
const headingContent = watch('heading')
|
||||
|
||||
useEffect(() => {
|
||||
setPreview(bodyContent || '')
|
||||
}, [bodyContent])
|
||||
|
||||
useEffect(() => {
|
||||
setHeadingPreview(headingContent || '')
|
||||
}, [headingContent])
|
||||
|
||||
const simpleMDEOptions = React.useMemo(
|
||||
() => ({
|
||||
@ -32,7 +58,9 @@ export const ArticleEdit = () => {
|
||||
|
||||
return (
|
||||
<Edit saveButtonProps={saveButtonProps}>
|
||||
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
||||
<Box sx={{display: 'flex', gap: 2}}>
|
||||
{/* Форма редактирования */}
|
||||
<Box component="form" sx={{flex: 1, display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
||||
<TextField
|
||||
{...register('heading', {
|
||||
required: 'Это поле является обязательным',
|
||||
@ -64,6 +92,122 @@ export const ArticleEdit = () => {
|
||||
|
||||
{articleId && <LinkedItems<MediaItem> type="edit" parentId={articleId} parentResource="article" childResource="media" fields={mediaFields} title="медиа" />}
|
||||
</Box>
|
||||
|
||||
{/* Блок предпросмотра */}
|
||||
<Paper
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 2,
|
||||
maxHeight: 'calc(100vh - 200px)',
|
||||
overflowY: 'auto',
|
||||
position: 'sticky',
|
||||
top: 16,
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'),
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom color="primary">
|
||||
Предпросмотр
|
||||
</Typography>
|
||||
|
||||
{/* Заголовок статьи */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
sx={{
|
||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{headingPreview}
|
||||
</Typography>
|
||||
|
||||
{/* Markdown контент */}
|
||||
<Box
|
||||
sx={{
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: 1,
|
||||
},
|
||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
color: 'primary.main',
|
||||
mt: 2,
|
||||
mb: 1,
|
||||
},
|
||||
'& p': {
|
||||
mb: 2,
|
||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
||||
},
|
||||
'& a': {
|
||||
color: 'primary.main',
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'primary.main',
|
||||
pl: 2,
|
||||
my: 2,
|
||||
color: 'text.secondary',
|
||||
},
|
||||
'& code': {
|
||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100'),
|
||||
p: 0.5,
|
||||
borderRadius: 0.5,
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown>{preview}</ReactMarkdown>
|
||||
</Box>
|
||||
|
||||
{/* Привязанные медиа */}
|
||||
{mediaData?.data && mediaData.data.length > 0 && (
|
||||
<Box sx={{mb: 3}}>
|
||||
<Typography variant="subtitle1" gutterBottom color="primary">
|
||||
Привязанные медиа:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
{mediaData.data.map((media) => (
|
||||
<Box
|
||||
key={media.id}
|
||||
sx={{
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`https://wn.krbl.ru/media/${media.id}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
|
||||
alt={media.media_name}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Edit>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user