/* eslint-disable */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import type { ReactNode } from 'react'; import type { TPlaceholderElement } from '@udecode/plate-media'; import { cn } from '@udecode/cn'; import { AudioPlugin, FilePlugin, ImagePlugin, PlaceholderPlugin, PlaceholderProvider, updateUploadHistory, VideoPlugin, } from '@udecode/plate-media/react'; import { PlateElement, useEditorPlugin, withHOC, withRef, } from '@udecode/plate/react'; import { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react'; import { useFilePicker } from 'use-file-picker'; import { useUploadFile } from '../../lib/uploadthing'; import { Spinner } from './spinner'; const CONTENT: Record< string, { accept: string[]; content: ReactNode; icon: ReactNode; } > = { [AudioPlugin.key]: { accept: ['audio/*'], content: 'Add an audio file', icon: , }, [FilePlugin.key]: { accept: ['*'], content: 'Add a file', icon: , }, [ImagePlugin.key]: { accept: ['image/*'], content: 'Add an image', icon: , }, [VideoPlugin.key]: { accept: ['video/*'], content: 'Add a video', icon: , }, }; export const MediaPlaceholderElement = withHOC( PlaceholderProvider, withRef( ({ children, className, nodeProps, ...props }, ref) => { const editor = props.editor; const element = props.element as TPlaceholderElement; const { api } = useEditorPlugin(PlaceholderPlugin); const { isUploading, progress, uploadedFile, uploadFile, uploadingFile } = useUploadFile(); const loading = isUploading && uploadingFile; const currentContent = CONTENT[element.mediaType]; const isImage = element.mediaType === ImagePlugin.key; const imageRef = useRef(null); const { openFilePicker } = useFilePicker({ accept: currentContent.accept, multiple: true, onFilesSelected: ({ plainFiles: updatedFiles }) => { const firstFile = updatedFiles[0]; const restFiles = updatedFiles.slice(1); replaceCurrentPlaceholder(firstFile); restFiles.length > 0 && (editor as any).tf.insert.media(restFiles); }, }); const replaceCurrentPlaceholder = useCallback( (file: File) => { void uploadFile(file); api.placeholder.addUploadingFile(element.id as string, file); }, [api.placeholder, element.id, uploadFile], ); useEffect(() => { if (!uploadedFile) return; const path = editor.api.findPath(element); editor.tf.withoutSaving(() => { editor.tf.removeNodes({ at: path }); const node = { children: [{ text: '' }], initialHeight: imageRef.current?.height, initialWidth: imageRef.current?.width, isUpload: true, name: element.mediaType === FilePlugin.key ? uploadedFile.name : '', placeholderId: element.id as string, type: element.mediaType!, url: uploadedFile.url, }; editor.tf.insertNodes(node, { at: path }); updateUploadHistory(editor, node); }); api.placeholder.removeUploadingFile(element.id as string); // eslint-disable-next-line react-hooks/exhaustive-deps }, [uploadedFile, element.id]); // React dev mode will call useEffect twice const isReplaced = useRef(false); /** Paste and drop */ useEffect(() => { if (isReplaced.current) return; isReplaced.current = true; const currentFiles = api.placeholder.getUploadingFile( element.id as string, ); if (!currentFiles) return; replaceCurrentPlaceholder(currentFiles); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isReplaced]); return ( {(!loading || !isImage) && (
!loading && openFilePicker()} contentEditable={false} >
{currentContent.icon}
{loading ? uploadingFile?.name : currentContent.content}
{loading && !isImage && (
{formatBytes(uploadingFile?.size ?? 0)}
–
{progress ?? 0}%
)}
)} {isImage && loading && ( )} {children}
); }, ), ); export function ImageProgress({ className, file, imageRef, progress = 0, }: { file: File; className?: string; imageRef?: React.RefObject; progress?: number; }) { const [objectUrl, setObjectUrl] = useState(null); useEffect(() => { const url = URL.createObjectURL(file); setObjectUrl(url); return () => { URL.revokeObjectURL(url); }; }, [file]); if (!objectUrl) { return null; } return (
{file.name} {progress < 100 && (
{Math.round(progress)}%
)}
); } export function formatBytes( bytes: number, opts: { decimals?: number; sizeType?: 'accurate' | 'normal'; } = {}, ) { const { decimals = 0, sizeType = 'normal' } = opts; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB']; if (bytes === 0) return '0 Byte'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${ sizeType === 'accurate' ? (accurateSizes[i] ?? 'Bytest') : (sizes[i] ?? 'Bytes') }`; }