forked from Qortal/q-blog
1587 lines
49 KiB
TypeScript
1587 lines
49 KiB
TypeScript
import React, { useCallback, useEffect } from 'react'
|
|
|
|
import BlogEditor from '../../components/editor/BlogEditor'
|
|
import ShortUniqueId from 'short-unique-id'
|
|
import ReadOnlySlate from '../../components/editor/ReadOnlySlate'
|
|
import { useDispatch, useSelector } from 'react-redux'
|
|
import { RootState } from '../../state/store'
|
|
import TextField from '@mui/material/TextField'
|
|
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate'
|
|
import ImageUploader from '../../components/common/ImageUploader'
|
|
import AudiotrackIcon from '@mui/icons-material/Audiotrack'
|
|
import DeleteIcon from '@mui/icons-material/Delete'
|
|
import { Button, Box, useTheme, Dialog, DialogTitle, DialogContent, Tabs, Tab, Typography } from '@mui/material'
|
|
import { styled } from '@mui/system'
|
|
import { Descendant } from 'slate'
|
|
import EditIcon from '@mui/icons-material/Edit'
|
|
import { extractTextFromSlate } from '../../utils/extractTextFromSlate'
|
|
import { setNotification } from '../../state/features/notificationsSlice'
|
|
import { VideoPanel } from '../../components/common/VideoPanel'
|
|
import PostPublishModal from '../../components/common/PostPublishModal'
|
|
import DynamicHeightItem from '../../components/DynamicHeightItem'
|
|
import { Responsive, WidthProvider } from 'react-grid-layout'
|
|
import '/node_modules/react-grid-layout/css/styles.css'
|
|
import '/node_modules/react-resizable/css/styles.css'
|
|
import { ReusableModal } from '../../components/modals/ReusableModal'
|
|
import { VideoPlayer } from '../../components/common/VideoPlayer'
|
|
import { EditorToolbar } from './components/Toolbar/EditorToolbar'
|
|
import { Navbar } from './components/Navbar/NavbarBuilder'
|
|
import { UserNavbar } from '../../components/common/UserNavbar/UserNavbar'
|
|
import { setCurrentBlog } from '../../state/features/globalSlice'
|
|
import AudioElement from '../../components/AudioElement'
|
|
import { AudioPanel } from '../../components/common/AudioPanel'
|
|
import {
|
|
addPostToBeginning,
|
|
addToHashMap,
|
|
updateInHashMap,
|
|
updatePost
|
|
} from '../../state/features/blogSlice'
|
|
import { removePrefix } from '../../utils/blogIdformats'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { BuilderButton } from './CreatePost-styles'
|
|
import FileElement from '../../components/FileElement'
|
|
const ResponsiveGridLayout = WidthProvider(Responsive)
|
|
const initialMinHeight = 2 // Define an initial minimum height for grid items
|
|
const uid = new ShortUniqueId()
|
|
const md = [
|
|
{ i: 'a', x: 0, y: 0, w: 4, h: initialMinHeight },
|
|
{ i: 'b', x: 6, y: 0, w: 4, h: initialMinHeight }
|
|
]
|
|
const sm = [
|
|
{ i: 'a', x: 0, y: 0, w: 6, h: initialMinHeight },
|
|
{ i: 'b', x: 6, y: 0, w: 6, h: initialMinHeight }
|
|
]
|
|
const xs = [
|
|
{ i: 'a', x: 0, y: 0, w: 6, h: initialMinHeight },
|
|
{ i: 'b', x: 6, y: 0, w: 6, h: initialMinHeight }
|
|
]
|
|
const initialValue: Descendant[] = [
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ text: '' }]
|
|
}
|
|
]
|
|
|
|
const BlogTitleInput = styled(TextField)(({ theme }) => ({
|
|
marginBottom: '15px',
|
|
'& .MuiInputBase-input': {
|
|
fontSize: '28px',
|
|
height: '28px',
|
|
background: 'transparent',
|
|
'&::placeholder': {
|
|
fontSize: '28px',
|
|
color: theme.palette.text.primary
|
|
}
|
|
},
|
|
'& .MuiInputLabel-root': {
|
|
fontSize: '28px'
|
|
},
|
|
'& .MuiInputBase-root': {
|
|
background: 'transparent',
|
|
'&:hover': {
|
|
background: 'transparent'
|
|
},
|
|
'&.Mui-focused': {
|
|
background: 'transparent'
|
|
}
|
|
},
|
|
'& .MuiOutlinedInput-root': {
|
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: theme.palette.primary.main
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: theme.palette.primary.main
|
|
}
|
|
}
|
|
}))
|
|
interface CreatePostBuilderProps {
|
|
blogContentForEdit?: any
|
|
postIdForEdit?: string
|
|
blogMetadataForEdit?: any
|
|
switchType?: () => void
|
|
}
|
|
|
|
export const CreatePostBuilder = ({
|
|
blogContentForEdit,
|
|
postIdForEdit,
|
|
blogMetadataForEdit,
|
|
switchType
|
|
}: CreatePostBuilderProps) => {
|
|
const navigate = useNavigate()
|
|
|
|
const theme = useTheme()
|
|
const { user } = useSelector((state: RootState) => state.auth)
|
|
const { currentBlog } = useSelector((state: RootState) => state.global)
|
|
const [editingSection, setEditingSection] = React.useState<any>(null)
|
|
const [layouts, setLayouts] = React.useState<any>({ md, sm, xs })
|
|
const [currentBreakpoint, setCurrentBreakpoint] = React.useState<any>()
|
|
const handleLayoutChange = (layout: any, layoutss: any) => {
|
|
setLayouts(layoutss)
|
|
}
|
|
const [newPostContent, setNewPostContent] = React.useState<any[]>([])
|
|
const [title, setTitle] = React.useState<string>('')
|
|
const [isOpenPostModal, setIsOpenPostModal] = React.useState<boolean>(false)
|
|
const [isOpenEditTextModal, setIsOpenEditTextModal] =
|
|
React.useState<boolean>(false)
|
|
const [value, setValue] = React.useState(initialValue)
|
|
const [editorKey, setEditorKey] = React.useState(1)
|
|
const [count, setCount] = React.useState<number>(1)
|
|
const [isOpenAddTextModal, setIsOpenAddTextModal] =
|
|
React.useState<boolean>(false)
|
|
const [paddingValue, onChangePadding] = React.useState(5)
|
|
const [coverPickerOpen, setCoverPickerOpen] = React.useState(false)
|
|
const [coverTab, setCoverTab] = React.useState<'upload' | 'existing'>('upload')
|
|
const [pendingVideo, setPendingVideo] = React.useState<any | null>(null)
|
|
const [selectedPoster, setSelectedPoster] = React.useState<string>('')
|
|
const [existingImages, setExistingImages] = React.useState<any[]>([])
|
|
const [existingImagesLoading, setExistingImagesLoading] = React.useState(false)
|
|
const [coverPickerMode, setCoverPickerMode] = React.useState<'add' | 'edit'>('add')
|
|
const [coverPickerTarget, setCoverPickerTarget] = React.useState<any | null>(null)
|
|
const qdnResourceUrl = React.useCallback(
|
|
(service: string, name: string, identifier: string) => `/arbitrary/${service}/${name}/${identifier}`,
|
|
[]
|
|
)
|
|
const fetchExistingImages = React.useCallback(async () => {
|
|
if (!user?.name) return
|
|
setExistingImagesLoading(true)
|
|
try {
|
|
const SERVICES = ['IMAGE', 'THUMBNAIL', 'QCHAT_IMAGE']
|
|
const lists = await Promise.all(
|
|
SERVICES.map(async (svc) => {
|
|
const res = await fetch(
|
|
`/arbitrary/resources?service=${svc}&name=${user.name}&includemetadata=true&limit=100&offset=0&reverse=true`
|
|
)
|
|
const data = await res.json()
|
|
return Array.isArray(data)
|
|
? data.map((it: any) => ({ ...it, service: it.service || svc }))
|
|
: []
|
|
})
|
|
)
|
|
const merged = lists.flat()
|
|
setExistingImages(merged)
|
|
} catch (e) {
|
|
setExistingImages([])
|
|
} finally {
|
|
setExistingImagesLoading(false)
|
|
}
|
|
}, [user])
|
|
const [isEditNavOpen, setIsEditNavOpen] = React.useState<boolean>(false)
|
|
const dispatch = useDispatch()
|
|
const [navbarConfig, setNavbarConfig] = React.useState<any>(null)
|
|
const addPostSection = React.useCallback((content: any) => {
|
|
const section = {
|
|
type: 'editor',
|
|
version: 1,
|
|
content,
|
|
id: uid()
|
|
}
|
|
|
|
setNewPostContent((prev) => [...prev, section])
|
|
setEditorKey((prev) => prev + 1)
|
|
}, [])
|
|
|
|
async function getBlog(name: string, identifier: string, blog: any) {
|
|
const urlBlog = `/arbitrary/BLOG/${name}/${identifier}`
|
|
const response = await fetch(urlBlog, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
const responseData = await response.json()
|
|
dispatch(
|
|
setCurrentBlog({
|
|
createdAt: responseData?.createdAt || '',
|
|
blogId: blog.identifier,
|
|
title: responseData?.title || '',
|
|
description: responseData?.description || '',
|
|
blogImage: responseData?.blogImage || '',
|
|
category: blog.metadata?.category,
|
|
tags: blog.metadata?.tags || [],
|
|
navbarConfig: responseData?.navbarConfig || null
|
|
})
|
|
)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (blogContentForEdit && postIdForEdit && blogMetadataForEdit) {
|
|
setTitle(blogContentForEdit?.title || '')
|
|
setLayouts(
|
|
blogContentForEdit?.layouts || {
|
|
rows: []
|
|
}
|
|
)
|
|
setNewPostContent(blogContentForEdit?.postContent || [])
|
|
onChangePadding(blogContentForEdit?.layoutGeneralSettings?.padding || 5)
|
|
}
|
|
}, [blogContentForEdit, postIdForEdit, blogMetadataForEdit])
|
|
|
|
const editBlog = React.useCallback(
|
|
async (navbarConfig: any) => {
|
|
if (!user || !user.name)
|
|
throw new Error('Cannot update: your Qortal name is not accessible')
|
|
|
|
if (!currentBlog)
|
|
throw new Error('Your blog is not available. Refresh and try again.')
|
|
|
|
const name = user.name
|
|
const formattedTags: { [key: string]: string } = {}
|
|
const tags = currentBlog?.tags || []
|
|
const category = currentBlog?.category || ''
|
|
const title = currentBlog?.title || ''
|
|
const description = currentBlog?.description || ''
|
|
tags.forEach((tag: string, i: number) => {
|
|
formattedTags[`tag${i + 1}`] = tag
|
|
})
|
|
|
|
const blogProps: any = {
|
|
...currentBlog,
|
|
navbarConfig
|
|
}
|
|
|
|
const blogPostToBase64 = await objectToBase64(blogProps)
|
|
try {
|
|
const resourceResponse = await qortalRequest({
|
|
action: 'PUBLISH_QDN_RESOURCE',
|
|
name: name,
|
|
service: 'BLOG',
|
|
data64: blogPostToBase64,
|
|
title,
|
|
description,
|
|
category,
|
|
...formattedTags,
|
|
identifier: currentBlog.blogId
|
|
})
|
|
|
|
await new Promise<void>((res, rej) => {
|
|
setTimeout(() => {
|
|
res()
|
|
}, 1000)
|
|
})
|
|
|
|
// getBlog(name, currentBlog.blogId, currentBlog)
|
|
dispatch(setCurrentBlog(blogProps))
|
|
dispatch(
|
|
setNotification({
|
|
msg: 'Blog successfully updated',
|
|
alertType: 'success'
|
|
})
|
|
)
|
|
} catch (error: any) {
|
|
let notificationObj: any = null
|
|
if (typeof error === 'string') {
|
|
notificationObj = {
|
|
msg: error || 'Failed to save blog',
|
|
alertType: 'error'
|
|
}
|
|
} else if (typeof error?.error === 'string') {
|
|
notificationObj = {
|
|
msg: error?.error || 'Failed to save blog',
|
|
alertType: 'error'
|
|
}
|
|
} else {
|
|
notificationObj = {
|
|
msg: error?.message || 'Failed to save blog',
|
|
alertType: 'error'
|
|
}
|
|
}
|
|
if (!notificationObj) return
|
|
dispatch(setNotification(notificationObj))
|
|
if (error instanceof Error) {
|
|
throw new Error(error.message)
|
|
} else {
|
|
throw new Error('An unknown error occurred')
|
|
}
|
|
}
|
|
},
|
|
[user, currentBlog]
|
|
)
|
|
|
|
const handleSaveNavBar = useCallback(
|
|
async (navMenu: any, navbarConfig: any) => {
|
|
try {
|
|
const config = {
|
|
type: 'topNav',
|
|
title: '',
|
|
logo: '',
|
|
...navbarConfig,
|
|
navItems: navMenu
|
|
}
|
|
await editBlog(config)
|
|
setIsEditNavOpen(false)
|
|
setNavbarConfig(config)
|
|
} catch (error: any) {
|
|
dispatch(
|
|
setNotification({
|
|
msg: error?.message || 'Could not save the navbar',
|
|
alertType: 'error'
|
|
})
|
|
)
|
|
}
|
|
},
|
|
[]
|
|
)
|
|
|
|
const handleRemoveNavBar = useCallback(async () => {
|
|
try {
|
|
await editBlog(null)
|
|
setNavbarConfig(null)
|
|
setIsEditNavOpen(false)
|
|
} catch (error: any) {
|
|
dispatch(
|
|
setNotification({
|
|
msg: error?.message || 'Could not save the navbar',
|
|
alertType: 'error'
|
|
})
|
|
)
|
|
}
|
|
}, [])
|
|
|
|
function objectToBase64(obj: any) {
|
|
// Step 1: Convert the object to a JSON string
|
|
const jsonString = JSON.stringify(obj)
|
|
|
|
// Step 2: Create a Blob from the JSON string
|
|
const blob = new Blob([jsonString], { type: 'application/json' })
|
|
|
|
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
|
|
return new Promise<string>((resolve, reject) => {
|
|
const reader = new FileReader()
|
|
reader.onloadend = () => {
|
|
if (typeof reader.result === 'string') {
|
|
// Remove 'data:application/json;base64,' prefix
|
|
const base64 = reader.result.replace(
|
|
'data:application/json;base64,',
|
|
''
|
|
)
|
|
resolve(base64)
|
|
} else {
|
|
reject(
|
|
new Error('Failed to read the Blob as a base64-encoded string')
|
|
)
|
|
}
|
|
}
|
|
reader.onerror = () => {
|
|
reject(reader.error)
|
|
}
|
|
reader.readAsDataURL(blob)
|
|
})
|
|
}
|
|
|
|
const description = React.useMemo(() => {
|
|
let description = ''
|
|
const findText = newPostContent.find((data) => data?.type === 'editor')
|
|
if (findText && findText.content) {
|
|
description = extractTextFromSlate(findText?.content)
|
|
description = description.slice(0, 180)
|
|
}
|
|
return description
|
|
}, [newPostContent])
|
|
const post = React.useMemo(() => {
|
|
return {
|
|
description,
|
|
title
|
|
}
|
|
}, [title, description])
|
|
|
|
async function publishQDNResource(params: any) {
|
|
let address
|
|
let name
|
|
let errorMsg = ''
|
|
|
|
address = user?.address
|
|
name = user?.name || ''
|
|
|
|
const missingFields = []
|
|
if (!address) {
|
|
errorMsg = "Cannot post: your address isn't available"
|
|
}
|
|
if (!name) {
|
|
errorMsg = 'Cannot post without a name'
|
|
}
|
|
if (!title) missingFields.push('title')
|
|
if (missingFields.length > 0) {
|
|
const missingFieldsString = missingFields.join(', ')
|
|
const errMsg = `Missing: ${missingFieldsString}`
|
|
errorMsg = errMsg
|
|
}
|
|
if (newPostContent.length === 0) {
|
|
errorMsg = 'Your post has no content'
|
|
}
|
|
|
|
if (!currentBlog) {
|
|
errorMsg = 'Cannot publish without first creating a blog.'
|
|
}
|
|
|
|
if (errorMsg) {
|
|
dispatch(
|
|
setNotification({
|
|
msg: errorMsg,
|
|
alertType: 'error'
|
|
})
|
|
)
|
|
throw new Error(errorMsg)
|
|
}
|
|
|
|
const layoutGeneralSettings = {
|
|
padding: paddingValue ?? 0,
|
|
blogPostType: 'builder'
|
|
}
|
|
|
|
const postObject = {
|
|
title,
|
|
createdAt: Date.now(),
|
|
postContent: newPostContent,
|
|
layouts,
|
|
layoutGeneralSettings
|
|
}
|
|
try {
|
|
if (!currentBlog) return
|
|
const id = uid()
|
|
let createTitleId = title
|
|
.replace(/[^a-zA-Z0-9\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.trim()
|
|
|
|
if (createTitleId.toLowerCase().includes('post')) {
|
|
createTitleId = createTitleId.replace(/post/gi, '')
|
|
}
|
|
if (createTitleId.toLowerCase().includes('q-blog')) {
|
|
createTitleId = createTitleId.replace(/q-blog/gi, '')
|
|
}
|
|
|
|
if (createTitleId.endsWith('-')) {
|
|
createTitleId = createTitleId.slice(0, -1)
|
|
}
|
|
if (createTitleId.startsWith('-')) {
|
|
createTitleId = createTitleId.slice(1)
|
|
}
|
|
createTitleId = createTitleId.slice(0, 24)
|
|
const identifier = `${currentBlog.blogId}-post-${createTitleId}-${id}`
|
|
const blogPostToBase64 = await objectToBase64(postObject)
|
|
let description = ''
|
|
const findText = newPostContent.find((data) => data?.type === 'editor')
|
|
if (findText && findText.content) {
|
|
description = extractTextFromSlate(findText?.content)
|
|
description = description.slice(0, 180)
|
|
}
|
|
|
|
let requestBody: any = {
|
|
action: 'PUBLISH_QDN_RESOURCE',
|
|
name: name,
|
|
service: 'BLOG_POST',
|
|
data64: blogPostToBase64,
|
|
title: title,
|
|
description: params?.description || description,
|
|
category: params?.category || '',
|
|
identifier: identifier
|
|
}
|
|
|
|
const formattedTags: { [key: string]: string } = {}
|
|
let tag4 = ''
|
|
let tag5 = ''
|
|
if (params?.tags) {
|
|
params.tags.slice(0, 3).forEach((tag: string, i: number) => {
|
|
formattedTags[`tag${i + 1}`] = tag
|
|
})
|
|
}
|
|
|
|
const findVideo: any = postObject?.postContent?.find(
|
|
(data: any) => data?.type === 'video'
|
|
)
|
|
const findAudio: any = postObject?.postContent?.find(
|
|
(data: any) => data?.type === 'audio'
|
|
)
|
|
const findImage: any = postObject?.postContent?.find(
|
|
(data: any) => data?.type === 'image'
|
|
)
|
|
|
|
const tag5Array = ['t']
|
|
if (findVideo) tag5Array.push('v')
|
|
if (findAudio) tag5Array.push('a')
|
|
if (findImage) {
|
|
tag5Array.push('i')
|
|
const imageElement = document.querySelector(
|
|
`[id="${findImage.id}"] img`
|
|
) as HTMLImageElement | null
|
|
if (imageElement) {
|
|
tag4 = `v1.${imageElement?.width}x${imageElement?.height}`
|
|
} else {
|
|
tag4 = 'v1.0x0'
|
|
}
|
|
}
|
|
if (!findImage) {
|
|
tag4 = 'v1.0x0'
|
|
}
|
|
tag5 = tag5Array.join(', ')
|
|
requestBody = {
|
|
...requestBody,
|
|
...formattedTags,
|
|
tag4: tag4,
|
|
tag5: tag5
|
|
}
|
|
|
|
const resourceResponse = await qortalRequest(requestBody)
|
|
|
|
const postobj: any = {
|
|
...postObject,
|
|
title: title,
|
|
description: params?.description || description,
|
|
category: params?.category || '',
|
|
tags: [...(params?.tags || []), tag4, tag5],
|
|
id: identifier,
|
|
user: name,
|
|
postImage: findImage ? findImage?.content?.image : ''
|
|
}
|
|
|
|
const withoutImage = { ...postobj }
|
|
delete withoutImage.postImage
|
|
dispatch(addPostToBeginning(withoutImage))
|
|
dispatch(addToHashMap(postobj))
|
|
dispatch(
|
|
setNotification({
|
|
msg: 'Blog post successfully published',
|
|
alertType: 'success'
|
|
})
|
|
)
|
|
const str = identifier
|
|
const arr = str.split('-post-')
|
|
const str1 = arr[0]
|
|
const str2 = arr[1]
|
|
const blogId = removePrefix(str1)
|
|
navigate(`/${name}/${blogId}/${str2}`)
|
|
} catch (error: any) {
|
|
let notificationObj = null
|
|
if (typeof error === 'string') {
|
|
notificationObj = {
|
|
msg: error || 'Failed to publish post',
|
|
alertType: 'error'
|
|
}
|
|
} else if (typeof error?.error === 'string') {
|
|
notificationObj = {
|
|
msg: error?.error || 'Failed to publish post',
|
|
alertType: 'error'
|
|
}
|
|
} else {
|
|
notificationObj = {
|
|
msg: error?.message || 'Failed to publish post',
|
|
alertType: 'error'
|
|
}
|
|
}
|
|
if (!notificationObj) return
|
|
dispatch(setNotification(notificationObj))
|
|
|
|
throw new Error('Failed to publish post')
|
|
}
|
|
}
|
|
|
|
async function updateQDNResource(params: any) {
|
|
if (!blogContentForEdit || !postIdForEdit || !blogMetadataForEdit) return
|
|
let address
|
|
let name
|
|
let errorMsg = ''
|
|
|
|
address = user?.address
|
|
name = user?.name || ''
|
|
|
|
const missingFields = []
|
|
if (!address) {
|
|
errorMsg = "Cannot post: your address isn't available"
|
|
}
|
|
if (!name) {
|
|
errorMsg = 'Cannot post without a name'
|
|
}
|
|
if (!title) missingFields.push('title')
|
|
if (missingFields.length > 0) {
|
|
const missingFieldsString = missingFields.join(', ')
|
|
const errMsg = `Missing: ${missingFieldsString}`
|
|
errorMsg = errMsg
|
|
}
|
|
if (newPostContent.length === 0) {
|
|
errorMsg = 'Your post has no content'
|
|
}
|
|
|
|
if (!currentBlog) {
|
|
errorMsg = 'Cannot publish without first creating a blog.'
|
|
}
|
|
|
|
if (errorMsg) {
|
|
dispatch(
|
|
setNotification({
|
|
msg: errorMsg,
|
|
alertType: 'error'
|
|
})
|
|
)
|
|
throw new Error(errorMsg)
|
|
}
|
|
|
|
const layoutGeneralSettings = {
|
|
padding: paddingValue ?? 0,
|
|
blogPostType: 'builder'
|
|
}
|
|
|
|
const postObject = {
|
|
...blogContentForEdit,
|
|
title,
|
|
postContent: newPostContent,
|
|
layouts,
|
|
layoutGeneralSettings
|
|
}
|
|
try {
|
|
if (!currentBlog) return
|
|
|
|
const identifier = postIdForEdit
|
|
const blogPostToBase64 = await objectToBase64(postObject)
|
|
let description = ''
|
|
const findText = newPostContent.find((data) => data?.type === 'editor')
|
|
if (findText && findText.content) {
|
|
description = extractTextFromSlate(findText?.content)
|
|
description = description.slice(0, 180)
|
|
}
|
|
|
|
let requestBody: any = {
|
|
action: 'PUBLISH_QDN_RESOURCE',
|
|
name: name,
|
|
service: 'BLOG_POST',
|
|
data64: blogPostToBase64,
|
|
title: title,
|
|
description: params?.description || description,
|
|
category: params?.category || '',
|
|
identifier: identifier
|
|
}
|
|
|
|
const formattedTags: { [key: string]: string } = {}
|
|
let tag4 = ''
|
|
let tag5 = ''
|
|
if (params?.tags) {
|
|
params.tags.slice(0, 3).forEach((tag: string, i: number) => {
|
|
formattedTags[`tag${i + 1}`] = tag
|
|
})
|
|
}
|
|
|
|
const findVideo: any = postObject?.postContent?.find(
|
|
(data: any) => data?.type === 'video'
|
|
)
|
|
const findAudio: any = postObject?.postContent?.find(
|
|
(data: any) => data?.type === 'audio'
|
|
)
|
|
const findImage: any = postObject?.postContent?.find(
|
|
(data: any) => data?.type === 'image'
|
|
)
|
|
|
|
const tag5Array = ['t']
|
|
if (findVideo) tag5Array.push('v')
|
|
if (findAudio) tag5Array.push('a')
|
|
if (findImage) {
|
|
tag5Array.push('i')
|
|
const imageElement = document.querySelector(
|
|
`[id="${findImage.id}"] img`
|
|
) as HTMLImageElement | null
|
|
if (imageElement) {
|
|
tag4 = `v1.${imageElement?.width}x${imageElement?.height}`
|
|
} else {
|
|
tag4 = 'v1.0x0'
|
|
}
|
|
}
|
|
if (!findImage) {
|
|
tag4 = 'v1.0x0'
|
|
}
|
|
tag5 = tag5Array.join(', ')
|
|
requestBody = {
|
|
...requestBody,
|
|
...formattedTags,
|
|
tag4: tag4,
|
|
tag5: tag5
|
|
}
|
|
|
|
const resourceResponse = await qortalRequest(requestBody)
|
|
const postobj = {
|
|
...postObject,
|
|
title: title,
|
|
description: params?.description || description,
|
|
category: params?.category || '',
|
|
tags: [...(params?.tags || []), tag4, tag5],
|
|
id: identifier,
|
|
user: name
|
|
}
|
|
const withoutImage = { ...postobj }
|
|
delete withoutImage.postImage
|
|
dispatch(updatePost(withoutImage))
|
|
dispatch(updateInHashMap(postobj))
|
|
dispatch(
|
|
setNotification({
|
|
msg: 'Blog post successfully updated',
|
|
alertType: 'success'
|
|
})
|
|
)
|
|
} catch (error: any) {
|
|
let notificationObj = null
|
|
if (typeof error === 'string') {
|
|
notificationObj = {
|
|
msg: error || 'Failed to update post',
|
|
alertType: 'error'
|
|
}
|
|
} else if (typeof error?.error === 'string') {
|
|
notificationObj = {
|
|
msg: error?.error || 'Failed to update post',
|
|
alertType: 'error'
|
|
}
|
|
} else {
|
|
notificationObj = {
|
|
msg: error?.message || 'Failed to update post',
|
|
alertType: 'error'
|
|
}
|
|
}
|
|
if (!notificationObj) return
|
|
dispatch(setNotification(notificationObj))
|
|
|
|
throw new Error('Failed to update post')
|
|
}
|
|
}
|
|
const addImage = (base64: string) => {
|
|
const section = {
|
|
type: 'image',
|
|
version: 1,
|
|
content: {
|
|
image: base64,
|
|
caption: ''
|
|
},
|
|
id: uid()
|
|
}
|
|
setNewPostContent((prev) => [...prev, section])
|
|
}
|
|
|
|
interface IaddVideo {
|
|
name: string
|
|
identifier: string
|
|
service: string
|
|
title: string
|
|
description: string
|
|
mimeType?: string
|
|
poster?: string
|
|
}
|
|
|
|
const addVideo = ({
|
|
name,
|
|
identifier,
|
|
service,
|
|
title,
|
|
description,
|
|
poster
|
|
}: IaddVideo) => {
|
|
const section = {
|
|
type: 'video',
|
|
version: 1,
|
|
content: {
|
|
name: name,
|
|
identifier: identifier,
|
|
service: service,
|
|
title,
|
|
description,
|
|
...(poster ? { poster } : {})
|
|
},
|
|
id: uid()
|
|
}
|
|
setNewPostContent((prev) => [...prev, section])
|
|
}
|
|
|
|
const addAudio = ({
|
|
name,
|
|
identifier,
|
|
service,
|
|
title,
|
|
description
|
|
}: IaddVideo) => {
|
|
const section = {
|
|
type: 'audio',
|
|
version: 1,
|
|
content: {
|
|
name: name,
|
|
identifier: identifier,
|
|
service: service,
|
|
title,
|
|
description
|
|
},
|
|
id: uid()
|
|
}
|
|
setNewPostContent((prev) => [...prev, section])
|
|
}
|
|
|
|
const addFile = ({
|
|
name,
|
|
identifier,
|
|
service,
|
|
title,
|
|
description,
|
|
mimeType
|
|
}: IaddVideo) => {
|
|
const id = uid()
|
|
const type = 'file'
|
|
const section = {
|
|
type,
|
|
version: 1,
|
|
content: {
|
|
name: name,
|
|
identifier: identifier,
|
|
service: service,
|
|
title,
|
|
description,
|
|
mimeType
|
|
},
|
|
id
|
|
}
|
|
setNewPostContent((prev) => [...prev, section])
|
|
}
|
|
|
|
const addSection = () => {
|
|
addPostSection(value)
|
|
setValue(initialValue)
|
|
}
|
|
|
|
const removeSection = (section: any) => {
|
|
const newContent = newPostContent.filter((s) => s.id !== section.id)
|
|
setNewPostContent(newContent)
|
|
}
|
|
const editImage = (base64: string, section: any) => {
|
|
const newSection = {
|
|
...section,
|
|
content: {
|
|
image: base64,
|
|
caption: section.content.caption
|
|
}
|
|
}
|
|
const findSectionIndex = newPostContent.findIndex(
|
|
(s) => s.id === section.id
|
|
)
|
|
if (findSectionIndex !== -1) {
|
|
const copyNewPostContent = [...newPostContent]
|
|
copyNewPostContent[findSectionIndex] = newSection
|
|
|
|
setNewPostContent(copyNewPostContent)
|
|
}
|
|
}
|
|
const editVideo = (
|
|
{ name, identifier, service, description, title, poster }: IaddVideo,
|
|
section: any
|
|
) => {
|
|
const nextContent: any = {
|
|
name,
|
|
identifier,
|
|
service,
|
|
description,
|
|
title
|
|
}
|
|
if (poster === undefined) {
|
|
if (typeof section.content?.poster !== 'undefined') {
|
|
nextContent.poster = section.content.poster
|
|
}
|
|
} else if (poster) {
|
|
nextContent.poster = poster
|
|
}
|
|
const newSection = {
|
|
...section,
|
|
content: nextContent
|
|
}
|
|
const findSectionIndex = newPostContent.findIndex(
|
|
(s) => s.id === section.id
|
|
)
|
|
if (findSectionIndex !== -1) {
|
|
const copyNewPostContent = [...newPostContent]
|
|
copyNewPostContent[findSectionIndex] = newSection
|
|
|
|
setNewPostContent(copyNewPostContent)
|
|
}
|
|
}
|
|
const editAudio = (
|
|
{ name, identifier, service, description, title }: IaddVideo,
|
|
section: any
|
|
) => {
|
|
const newSection = {
|
|
...section,
|
|
content: {
|
|
name: name,
|
|
identifier: identifier,
|
|
service: service,
|
|
description,
|
|
title
|
|
}
|
|
}
|
|
const findSectionIndex = newPostContent.findIndex(
|
|
(s) => s.id === section.id
|
|
)
|
|
if (findSectionIndex !== -1) {
|
|
const copyNewPostContent = [...newPostContent]
|
|
copyNewPostContent[findSectionIndex] = newSection
|
|
|
|
setNewPostContent(copyNewPostContent)
|
|
}
|
|
}
|
|
const editSection = (section: any) => {
|
|
setIsOpenEditTextModal(true)
|
|
setEditingSection(section)
|
|
setValue(section.content)
|
|
}
|
|
const editPostSection = React.useCallback(
|
|
(content: any, section: any) => {
|
|
const findSectionIndex = newPostContent.findIndex(
|
|
(s) => s.id === section.id
|
|
)
|
|
if (findSectionIndex !== -1) {
|
|
const copyNewPostContent = [...newPostContent]
|
|
copyNewPostContent[findSectionIndex] = {
|
|
...section,
|
|
content
|
|
}
|
|
|
|
setNewPostContent(copyNewPostContent)
|
|
}
|
|
|
|
setEditingSection(null)
|
|
setIsOpenEditTextModal(false)
|
|
},
|
|
[newPostContent]
|
|
)
|
|
|
|
const onSelectVideo = React.useCallback((video: any) => {
|
|
setCoverPickerMode('add')
|
|
setCoverPickerTarget(null)
|
|
setPendingVideo(video)
|
|
setSelectedPoster('')
|
|
setCoverTab('upload')
|
|
setCoverPickerOpen(true)
|
|
fetchExistingImages()
|
|
}, [fetchExistingImages])
|
|
|
|
const onSelectAudio = React.useCallback((video: any) => {
|
|
addAudio({
|
|
name: video.name,
|
|
identifier: video.identifier,
|
|
service: video.service,
|
|
title: video?.metadata?.title,
|
|
description: video?.metadata?.description
|
|
})
|
|
}, [])
|
|
|
|
const onBreakpointChange = (newBreakpoint: any) => {
|
|
setCurrentBreakpoint(newBreakpoint)
|
|
}
|
|
|
|
const onSelectFile = React.useCallback((video: any) => {
|
|
addFile({
|
|
name: video.name,
|
|
identifier: video.identifier,
|
|
service: video.service,
|
|
title: video?.metadata?.title,
|
|
description: video?.metadata?.description,
|
|
mimeType: video?.metadata?.mimeType
|
|
})
|
|
}, [])
|
|
|
|
const closeAddTextModal = React.useCallback(() => {
|
|
setIsOpenAddTextModal(false)
|
|
}, [])
|
|
|
|
const closeEditTextModal = React.useCallback(() => {
|
|
setIsOpenEditTextModal(false)
|
|
setEditingSection(null)
|
|
}, [])
|
|
|
|
const onResizeStop = (layout: any, layoutItem: any) => {
|
|
setCount((prev) => prev + 1)
|
|
}
|
|
const handleResize = () => {
|
|
setCount((prev) => prev + 1)
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
window.addEventListener('resize', handleResize)
|
|
|
|
return () => {
|
|
window.removeEventListener('resize', handleResize)
|
|
}
|
|
}, [])
|
|
|
|
const gridItemCount =
|
|
currentBreakpoint === 'md' ? 4 : currentBreakpoint === 'sm' ? 3 : 1
|
|
|
|
const addNav = () => {
|
|
setIsEditNavOpen(true)
|
|
}
|
|
return (
|
|
<>
|
|
<EditorToolbar
|
|
setIsOpenAddTextModal={setIsOpenAddTextModal}
|
|
addImage={addImage}
|
|
onSelectVideo={onSelectVideo}
|
|
onSelectAudio={onSelectAudio}
|
|
onSelectFile={onSelectFile}
|
|
paddingValue={paddingValue}
|
|
onChangePadding={onChangePadding}
|
|
addNav={addNav}
|
|
switchType={switchType}
|
|
/>
|
|
{/* {navbarConfig && Array.isArray(navbarConfig?.navItems) && (
|
|
<UserNavbar
|
|
title="Test"
|
|
menuItems={navbarConfig?.navItems || []}
|
|
name=""
|
|
blogId=""
|
|
/>
|
|
)} */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
flexDirection: 'column',
|
|
padding: '10px'
|
|
}}
|
|
>
|
|
<BlogTitleInput
|
|
id="modal-title-input"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
fullWidth
|
|
placeholder="Title"
|
|
variant="filled"
|
|
multiline
|
|
maxRows={2}
|
|
InputLabelProps={{ shrink: false }}
|
|
sx={{ maxWidth: '700px' }}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
maxWidth: '1400px',
|
|
width: '95%',
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
<div
|
|
className="test-grid"
|
|
style={{
|
|
gridTemplateColumns: `repeat(${gridItemCount}, 1fr)`
|
|
}}
|
|
>
|
|
{Array.from({ length: gridItemCount }, (_, i) => (
|
|
<div key={`grid-item-${i}`} className="test-grid-item"></div>
|
|
))}
|
|
</div>
|
|
<ResponsiveGridLayout
|
|
layouts={layouts}
|
|
breakpoints={{ md: 996, sm: 768, xs: 480 }}
|
|
cols={{ md: 4, sm: 3, xs: 1 }}
|
|
onLayoutChange={handleLayoutChange}
|
|
measureBeforeMount={false}
|
|
autoSize={true}
|
|
compactType={null}
|
|
isBounded={false}
|
|
resizeHandles={['se', 'sw', 'ne', 'nw']}
|
|
rowHeight={25}
|
|
onBreakpointChange={onBreakpointChange}
|
|
onResizeStop={onResizeStop}
|
|
margin={[0, 0]}
|
|
preventCollision={true}
|
|
>
|
|
{newPostContent.map((section: any) => {
|
|
if (section.type === 'editor') {
|
|
return (
|
|
<div key={section.id} className="grid-item">
|
|
<DynamicHeightItem
|
|
layouts={layouts}
|
|
setLayouts={setLayouts}
|
|
i={section.id}
|
|
breakpoint={currentBreakpoint}
|
|
count={count}
|
|
padding={paddingValue}
|
|
>
|
|
{editingSection &&
|
|
editingSection.id === section.id ? null : (
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
height: 'auto'
|
|
}}
|
|
>
|
|
<ReadOnlySlate
|
|
key={section.id}
|
|
content={section.content}
|
|
/>
|
|
<EditButtons>
|
|
<DeleteIcon
|
|
onClick={() => removeSection(section)}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '18px',
|
|
width: 'auto'
|
|
}}
|
|
/>
|
|
<EditIcon
|
|
onClick={() => editSection(section)}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '18px',
|
|
width: 'auto'
|
|
}}
|
|
/>
|
|
</EditButtons>
|
|
</Box>
|
|
)}
|
|
</DynamicHeightItem>
|
|
</div>
|
|
)
|
|
}
|
|
if (section.type === 'image') {
|
|
return (
|
|
<div id={section.id} key={section.id} className="grid-item">
|
|
<DynamicHeightItem
|
|
layouts={layouts}
|
|
setLayouts={setLayouts}
|
|
i={section.id}
|
|
breakpoint={currentBreakpoint}
|
|
count={count}
|
|
type="image"
|
|
padding={paddingValue}
|
|
>
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
height: '100%'
|
|
}}
|
|
>
|
|
<img
|
|
src={section.content.image}
|
|
className="post-image"
|
|
/>
|
|
<EditButtons>
|
|
<DeleteIcon
|
|
onClick={() => removeSection(section)}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '18px',
|
|
width: 'auto'
|
|
}}
|
|
/>
|
|
<ImageUploader
|
|
onPick={(base64) => editImage(base64, section)}
|
|
>
|
|
<EditIcon
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '18px',
|
|
width: 'auto'
|
|
}}
|
|
/>
|
|
</ImageUploader>
|
|
</EditButtons>
|
|
</Box>
|
|
</DynamicHeightItem>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (section.type === 'video') {
|
|
return (
|
|
<div key={section.id} className="grid-item">
|
|
<DynamicHeightItem
|
|
layouts={layouts}
|
|
setLayouts={setLayouts}
|
|
i={section.id}
|
|
breakpoint={currentBreakpoint}
|
|
count={count}
|
|
padding={paddingValue}
|
|
>
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
height: '100%'
|
|
}}
|
|
>
|
|
<VideoPlayer
|
|
name={section.content.name}
|
|
service={section.content.service}
|
|
identifier={section.content.identifier}
|
|
poster={section.content.poster}
|
|
from="create"
|
|
/>
|
|
<EditButtons>
|
|
<DeleteIcon
|
|
onClick={() => removeSection(section)}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '18px',
|
|
width: 'auto'
|
|
}}
|
|
/>
|
|
<VideoPanel
|
|
width="auto"
|
|
height="18px"
|
|
onSelect={(video) =>
|
|
editVideo(
|
|
{
|
|
name: video.name,
|
|
identifier: video.identifier,
|
|
service: video.service,
|
|
title: video?.metadata?.title,
|
|
description: video?.metadata?.description
|
|
},
|
|
section
|
|
)
|
|
}
|
|
/>
|
|
<AddPhotoAlternateIcon
|
|
onClick={() => {
|
|
setCoverPickerMode('edit')
|
|
setCoverPickerTarget(section)
|
|
setPendingVideo(null)
|
|
setSelectedPoster(section.content?.poster || '')
|
|
setCoverTab('upload')
|
|
setCoverPickerOpen(true)
|
|
fetchExistingImages()
|
|
}}
|
|
sx={{ cursor: 'pointer', height: '18px', width: 'auto' }}
|
|
/>
|
|
</EditButtons>
|
|
</Box>
|
|
</DynamicHeightItem>
|
|
</div>
|
|
)
|
|
}
|
|
if (section.type === 'audio') {
|
|
return (
|
|
<div key={section.id} className="grid-item">
|
|
<DynamicHeightItem
|
|
layouts={layouts}
|
|
setLayouts={setLayouts}
|
|
i={section.id}
|
|
breakpoint={currentBreakpoint}
|
|
count={count}
|
|
padding={paddingValue}
|
|
>
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
height: '100%'
|
|
}}
|
|
>
|
|
<AudioElement
|
|
key={section.id}
|
|
onClick={() => {}}
|
|
title={section.content?.title}
|
|
description={section.content?.description}
|
|
author=""
|
|
/>
|
|
<EditButtons>
|
|
<DeleteIcon
|
|
onClick={() => removeSection(section)}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '18px',
|
|
width: 'auto'
|
|
}}
|
|
/>
|
|
<AudioPanel
|
|
width="auto"
|
|
height="18px"
|
|
onSelect={(audio) =>
|
|
editAudio(
|
|
{
|
|
name: audio.name,
|
|
identifier: audio.identifier,
|
|
service: audio.service,
|
|
title: audio?.metadata?.title,
|
|
description: audio?.metadata?.description
|
|
},
|
|
section
|
|
)
|
|
}
|
|
/>
|
|
</EditButtons>
|
|
</Box>
|
|
</DynamicHeightItem>
|
|
</div>
|
|
)
|
|
}
|
|
if (section.type === 'file') {
|
|
return (
|
|
<div key={section.id} className="grid-item">
|
|
<DynamicHeightItem
|
|
layouts={layouts}
|
|
setLayouts={setLayouts}
|
|
i={section.id}
|
|
breakpoint={currentBreakpoint}
|
|
count={count}
|
|
padding={paddingValue}
|
|
>
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
width: '100%',
|
|
height: '100%'
|
|
}}
|
|
>
|
|
<FileElement
|
|
key={section.id}
|
|
fileInfo={section.content}
|
|
title={section.content?.title}
|
|
description={section.content?.description}
|
|
mimeType={section.content?.mimeType}
|
|
author=""
|
|
disable={true}
|
|
/>
|
|
|
|
<EditButtons>
|
|
<DeleteIcon
|
|
onClick={() => removeSection(section)}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
height: '18px',
|
|
width: 'auto'
|
|
}}
|
|
/>
|
|
</EditButtons>
|
|
</Box>
|
|
</DynamicHeightItem>
|
|
</div>
|
|
)
|
|
}
|
|
})}
|
|
</ResponsiveGridLayout>
|
|
|
|
<Box
|
|
sx={{
|
|
position: 'fixed',
|
|
bottom: '30px',
|
|
right: '30px',
|
|
zIndex: 15,
|
|
background: 'deepskyblue',
|
|
padding: '10px',
|
|
borderRadius: '5px'
|
|
}}
|
|
>
|
|
<Button
|
|
onClick={() => {
|
|
setIsOpenPostModal(true)
|
|
}}
|
|
>
|
|
Publish
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
<ReusableModal open={isOpenAddTextModal}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1
|
|
}}
|
|
>
|
|
<BlogEditor
|
|
addPostSection={addPostSection}
|
|
value={value}
|
|
setValue={setValue}
|
|
editorKey={editorKey}
|
|
/>
|
|
</Box>
|
|
<BuilderButton onClick={addSection}>Add Text</BuilderButton>
|
|
<BuilderButton onClick={closeAddTextModal}>Close</BuilderButton>
|
|
</ReusableModal>
|
|
<ReusableModal open={isOpenEditTextModal}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1
|
|
}}
|
|
>
|
|
<BlogEditor
|
|
value={value}
|
|
setValue={setValue}
|
|
editorKey={editorKey}
|
|
/>
|
|
</Box>
|
|
<BuilderButton onClick={() => editPostSection(value, editingSection)}>
|
|
Update Text
|
|
</BuilderButton>
|
|
<BuilderButton onClick={closeEditTextModal}>Close</BuilderButton>
|
|
</ReusableModal>
|
|
<ReusableModal open={isEditNavOpen}>
|
|
<Navbar
|
|
saveNav={handleSaveNavBar}
|
|
removeNav={handleRemoveNavBar}
|
|
close={() => setIsEditNavOpen(false)}
|
|
/>
|
|
</ReusableModal>
|
|
|
|
{!blogContentForEdit && (
|
|
<PostPublishModal
|
|
onClose={() => {
|
|
setIsOpenPostModal(false)
|
|
}}
|
|
open={isOpenPostModal}
|
|
post={post}
|
|
onPublish={publishQDNResource}
|
|
/>
|
|
)}
|
|
|
|
{blogContentForEdit && blogMetadataForEdit?.metadata && (
|
|
<PostPublishModal
|
|
onClose={() => {
|
|
setIsOpenPostModal(false)
|
|
}}
|
|
open={isOpenPostModal}
|
|
post={post}
|
|
onPublish={updateQDNResource}
|
|
mode="edit"
|
|
metadata={blogMetadataForEdit?.metadata}
|
|
/>
|
|
)}
|
|
</Box>
|
|
<Dialog open={coverPickerOpen} onClose={() => setCoverPickerOpen(false)} maxWidth="sm" fullWidth>
|
|
<DialogTitle>Select a cover image</DialogTitle>
|
|
<DialogContent>
|
|
<Tabs value={coverTab} onChange={(_, v) => setCoverTab(v)}>
|
|
<Tab label="Upload" value="upload" />
|
|
<Tab label="Existing" value="existing" />
|
|
</Tabs>
|
|
|
|
{coverTab === 'upload' && (
|
|
<Box sx={{ mt: 2 }}>
|
|
<ImageUploader onPick={(base64) => setSelectedPoster(base64)}>
|
|
<Button variant="outlined">Choose image</Button>
|
|
</ImageUploader>
|
|
{selectedPoster && (
|
|
<Box sx={{ mt: 2 }}>
|
|
<img src={selectedPoster} style={{ maxWidth: '100%' }} />
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{coverTab === 'existing' && (
|
|
<Box sx={{ mt: 2, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1 }}>
|
|
{existingImages.map((img: any) => {
|
|
const service = img.service || 'IMAGE'
|
|
const url = qdnResourceUrl(service, img.name, img.identifier)
|
|
const isActive = selectedPoster === url
|
|
return (
|
|
<Box
|
|
key={`${service}:${img.name}:${img.identifier}`}
|
|
sx={{
|
|
border: isActive ? '2px solid #1976d2' : '1px solid rgba(255,255,255,0.12)',
|
|
borderRadius: 1,
|
|
overflow: 'hidden',
|
|
cursor: 'pointer'
|
|
}}
|
|
onClick={() => setSelectedPoster(url)}
|
|
>
|
|
<img src={url} style={{ display: 'block', width: '100%' }} />
|
|
<Typography variant="caption" sx={{ display: 'block', p: 0.5 }} noWrap>
|
|
{img?.metadata?.title || img.identifier}
|
|
</Typography>
|
|
</Box>
|
|
)
|
|
})}
|
|
{existingImagesLoading && (
|
|
<Typography variant="body2">Loading...</Typography>
|
|
)}
|
|
{!existingImagesLoading && !existingImages.length && (
|
|
<Typography variant="body2">No published images found.</Typography>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
<Box sx={{ mt: 3, display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
|
<Button onClick={() => setCoverPickerOpen(false)}>Cancel</Button>
|
|
<Button
|
|
onClick={() => {
|
|
if (coverPickerMode === 'edit' && coverPickerTarget) {
|
|
editVideo(
|
|
{
|
|
name: coverPickerTarget.content.name,
|
|
identifier: coverPickerTarget.content.identifier,
|
|
service: coverPickerTarget.content.service,
|
|
title: coverPickerTarget.content.title,
|
|
description: coverPickerTarget.content.description,
|
|
poster: ''
|
|
},
|
|
coverPickerTarget
|
|
)
|
|
setSelectedPoster('')
|
|
setCoverPickerOpen(false)
|
|
} else {
|
|
setSelectedPoster('')
|
|
}
|
|
}}
|
|
>
|
|
Clear cover
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
disabled={coverPickerMode === 'add' && !pendingVideo}
|
|
onClick={() => {
|
|
const poster = selectedPoster || undefined
|
|
if (coverPickerMode === 'add' && pendingVideo) {
|
|
addVideo({
|
|
name: pendingVideo.name,
|
|
identifier: pendingVideo.identifier,
|
|
service: pendingVideo.service,
|
|
title: pendingVideo?.metadata?.title,
|
|
description: pendingVideo?.metadata?.description,
|
|
poster
|
|
})
|
|
} else if (coverPickerMode === 'edit' && coverPickerTarget) {
|
|
editVideo(
|
|
{
|
|
name: coverPickerTarget.content.name,
|
|
identifier: coverPickerTarget.content.identifier,
|
|
service: coverPickerTarget.content.service,
|
|
title: coverPickerTarget.content.title,
|
|
description: coverPickerTarget.content.description,
|
|
poster
|
|
},
|
|
coverPickerTarget
|
|
)
|
|
}
|
|
setPendingVideo(null)
|
|
setCoverPickerTarget(null)
|
|
setSelectedPoster('')
|
|
setCoverPickerOpen(false)
|
|
}}
|
|
>
|
|
{coverPickerMode === 'add' ? 'Add video' : 'Update cover'}
|
|
</Button>
|
|
</Box>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export const EditButtons = ({ children }: any) => {
|
|
const theme = useTheme()
|
|
return (
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
right: '5px',
|
|
zIndex: 500,
|
|
top: '5px',
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
gap: '8px',
|
|
background: 'white',
|
|
padding: '5px',
|
|
borderRadius: '5px',
|
|
alignItems: 'center',
|
|
backgroundColor: theme.palette.background.paper
|
|
}}
|
|
>
|
|
{children}
|
|
</Box>
|
|
)
|
|
}
|