mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: Enhance EmbedObjectsComponent with YouTube support and responsive design, overall improvements
This commit is contained in:
parent
632cc79838
commit
5dd9d5d749
2 changed files with 351 additions and 52 deletions
|
|
@ -2,7 +2,7 @@ import { NodeViewWrapper } from '@tiptap/react'
|
||||||
import React, { useState, useRef, useEffect, useMemo } from 'react'
|
import React, { useState, useRef, useEffect, useMemo } from 'react'
|
||||||
import { Upload, Link as LinkIcon, GripVertical, GripHorizontal, AlignCenter, Cuboid, Code } from 'lucide-react'
|
import { Upload, Link as LinkIcon, GripVertical, GripHorizontal, AlignCenter, Cuboid, Code } from 'lucide-react'
|
||||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||||
import { SiGithub, SiReplit, SiSpotify, SiLoom, SiGooglemaps, SiCodepen, SiCanva, SiNotion, SiGoogledocs, SiGitlab, SiX, SiFigma, SiGiphy } from '@icons-pack/react-simple-icons'
|
import { SiGithub, SiReplit, SiSpotify, SiLoom, SiGooglemaps, SiCodepen, SiCanva, SiNotion, SiGoogledocs, SiGitlab, SiX, SiFigma, SiGiphy, SiYoutube } from '@icons-pack/react-simple-icons'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
|
@ -14,6 +14,21 @@ const SCRIPT_BASED_EMBEDS = {
|
||||||
// Add more platforms as needed
|
// Add more platforms as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to convert YouTube URLs to embed format
|
||||||
|
const getYouTubeEmbedUrl = (url: string): string => {
|
||||||
|
// Handle different YouTube URL formats
|
||||||
|
const youtubeRegex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i;
|
||||||
|
const match = url.match(youtubeRegex);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
// Return the embed URL with the video ID
|
||||||
|
return `https://www.youtube.com/embed/${match[1]}?autoplay=0&rel=0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match found, return the original URL
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
// Add new memoized component for the embed content
|
// Add new memoized component for the embed content
|
||||||
const MemoizedEmbed = React.memo(({ embedUrl, sanitizedEmbedCode, embedType }: {
|
const MemoizedEmbed = React.memo(({ embedUrl, sanitizedEmbedCode, embedType }: {
|
||||||
embedUrl: string;
|
embedUrl: string;
|
||||||
|
|
@ -43,9 +58,14 @@ const MemoizedEmbed = React.memo(({ embedUrl, sanitizedEmbedCode, embedType }: {
|
||||||
}, [embedType, sanitizedEmbedCode]);
|
}, [embedType, sanitizedEmbedCode]);
|
||||||
|
|
||||||
if (embedType === 'url' && embedUrl) {
|
if (embedType === 'url' && embedUrl) {
|
||||||
|
// Process the URL if it's a YouTube URL
|
||||||
|
const processedUrl = embedUrl.includes('youtube.com') || embedUrl.includes('youtu.be')
|
||||||
|
? getYouTubeEmbedUrl(embedUrl)
|
||||||
|
: embedUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
src={embedUrl}
|
src={processedUrl}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
|
|
@ -69,13 +89,61 @@ function EmbedObjectsComponent(props: any) {
|
||||||
const [embedWidth, setEmbedWidth] = useState(props.node.attrs.embedWidth || '100%')
|
const [embedWidth, setEmbedWidth] = useState(props.node.attrs.embedWidth || '100%')
|
||||||
const [alignment, setAlignment] = useState(props.node.attrs.alignment || 'left')
|
const [alignment, setAlignment] = useState(props.node.attrs.alignment || 'left')
|
||||||
const [isResizing, setIsResizing] = useState(false)
|
const [isResizing, setIsResizing] = useState(false)
|
||||||
|
const [parentWidth, setParentWidth] = useState<number | null>(null)
|
||||||
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
|
||||||
const resizeRef = useRef<HTMLDivElement>(null)
|
const resizeRef = useRef<HTMLDivElement>(null)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const editorState = useEditorProvider() as any
|
const editorState = useEditorProvider() as any
|
||||||
const isEditable = editorState.isEditable
|
const isEditable = editorState.isEditable
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Add ResizeObserver to track parent container size changes
|
||||||
|
useEffect(() => {
|
||||||
|
const updateDimensions = () => {
|
||||||
|
if (containerRef.current && containerRef.current.parentElement) {
|
||||||
|
const parentElement = containerRef.current.parentElement;
|
||||||
|
const newParentWidth = parentElement.offsetWidth;
|
||||||
|
setParentWidth(newParentWidth);
|
||||||
|
|
||||||
|
// Check if we're in a mobile viewport
|
||||||
|
setIsMobile(newParentWidth < 640); // 640px is a common breakpoint for small screens
|
||||||
|
|
||||||
|
// If embedWidth is set to a percentage, maintain that percentage
|
||||||
|
// Otherwise, adjust to fit parent width
|
||||||
|
if (typeof embedWidth === 'string' && embedWidth.endsWith('%')) {
|
||||||
|
const percentage = parseInt(embedWidth, 10);
|
||||||
|
const newWidth = `${Math.min(100, percentage)}%`;
|
||||||
|
setEmbedWidth(newWidth);
|
||||||
|
props.updateAttributes({ embedWidth: newWidth });
|
||||||
|
} else if (newParentWidth < parseInt(String(embedWidth), 10)) {
|
||||||
|
// If parent is smaller than current width, adjust to fit
|
||||||
|
setEmbedWidth('100%');
|
||||||
|
props.updateAttributes({ embedWidth: '100%' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize dimensions
|
||||||
|
updateDimensions();
|
||||||
|
|
||||||
|
// Set up ResizeObserver
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateDimensions();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (containerRef.current && containerRef.current.parentElement) {
|
||||||
|
resizeObserver.observe(containerRef.current.parentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const supportedProducts = [
|
const supportedProducts = [
|
||||||
|
{ name: 'YouTube', icon: SiYoutube, color: '#FF0000', guide: 'https://support.google.com/youtube/answer/171780?hl=en' },
|
||||||
{ name: 'GitHub', icon: SiGithub, color: '#181717', guide: 'https://emgithub.com/' },
|
{ name: 'GitHub', icon: SiGithub, color: '#181717', guide: 'https://emgithub.com/' },
|
||||||
{ name: 'Replit', icon: SiReplit, color: '#F26207', guide: 'https://docs.replit.com/hosting/embedding-repls' },
|
{ name: 'Replit', icon: SiReplit, color: '#F26207', guide: 'https://docs.replit.com/hosting/embedding-repls' },
|
||||||
{ name: 'Spotify', icon: SiSpotify, color: '#1DB954', guide: 'https://developer.spotify.com/documentation/embeds' },
|
{ name: 'Spotify', icon: SiSpotify, color: '#1DB954', guide: 'https://developer.spotify.com/documentation/embeds' },
|
||||||
|
|
@ -196,6 +264,30 @@ function EmbedObjectsComponent(props: any) {
|
||||||
window.open(guide, '_blank', 'noopener,noreferrer')
|
window.open(guide, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate responsive styles based on parent width
|
||||||
|
const getResponsiveStyles = () => {
|
||||||
|
// Default styles
|
||||||
|
const styles: React.CSSProperties = {
|
||||||
|
height: `${embedHeight}px`,
|
||||||
|
width: embedWidth,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If parent width is available, ensure we don't exceed it
|
||||||
|
if (parentWidth) {
|
||||||
|
// For mobile viewports, always use 100% width
|
||||||
|
if (isMobile) {
|
||||||
|
styles.width = '100%';
|
||||||
|
styles.minWidth = 'unset';
|
||||||
|
} else {
|
||||||
|
// For desktop, use the set width but ensure it's not wider than parent
|
||||||
|
styles.minWidth = Math.min(parentWidth, 400) + 'px';
|
||||||
|
styles.maxWidth = '100%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles;
|
||||||
|
};
|
||||||
|
|
||||||
// Memoize the embed content
|
// Memoize the embed content
|
||||||
const embedContent = useMemo(() => (
|
const embedContent = useMemo(() => (
|
||||||
!isResizing && (embedUrl || sanitizedEmbedCode) ? (
|
!isResizing && (embedUrl || sanitizedEmbedCode) ? (
|
||||||
|
|
@ -209,73 +301,280 @@ function EmbedObjectsComponent(props: any) {
|
||||||
)
|
)
|
||||||
), [embedUrl, sanitizedEmbedCode, embedType, isResizing]);
|
), [embedUrl, sanitizedEmbedCode, embedType, isResizing]);
|
||||||
|
|
||||||
|
// Input states
|
||||||
|
const [activeInput, setActiveInput] = useState<'none' | 'url' | 'code'>('none');
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<typeof supportedProducts[0] | null>(null);
|
||||||
|
const urlInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const codeInputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
// Handle direct input from product selection
|
||||||
|
const handleProductSelection = (product: typeof supportedProducts[0]) => {
|
||||||
|
// Set the input type to URL by default
|
||||||
|
setEmbedType('url');
|
||||||
|
setActiveInput('url');
|
||||||
|
|
||||||
|
// Store the selected product for the popup
|
||||||
|
setSelectedProduct(product);
|
||||||
|
|
||||||
|
// Focus the URL input after a short delay to allow rendering
|
||||||
|
setTimeout(() => {
|
||||||
|
if (urlInputRef.current) {
|
||||||
|
urlInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle input submission
|
||||||
|
const handleInputSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveInput('none');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle escape key to cancel input
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setActiveInput('none');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle opening documentation
|
||||||
|
const handleOpenDocs = (guide: string) => {
|
||||||
|
window.open(guide, '_blank', 'noopener,noreferrer');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className="embed-block">
|
<NodeViewWrapper className="embed-block w-full" ref={containerRef}>
|
||||||
<div
|
<div
|
||||||
ref={resizeRef}
|
ref={resizeRef}
|
||||||
className={`relative bg-gray-100 rounded-lg overflow-hidden flex justify-center items-center ${alignment === 'center' ? 'mx-auto' : ''}`}
|
className={`relative bg-gray-100 rounded-lg overflow-hidden flex justify-center items-center ${alignment === 'center' ? 'mx-auto' : ''}`}
|
||||||
style={{ height: `${embedHeight}px`, width: embedWidth, minWidth: '400px' }}
|
style={getResponsiveStyles()}
|
||||||
>
|
>
|
||||||
{(embedUrl || sanitizedEmbedCode) ? embedContent : (
|
{(embedUrl || sanitizedEmbedCode) ? (
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center p-6">
|
// Show the embed content if we have a URL or code
|
||||||
<p className="text-gray-500 mb-4 font-medium tracking-tighter text-lg">Add an embed from :</p>
|
<>
|
||||||
<div className="flex flex-wrap gap-5 justify-center">
|
{embedContent}
|
||||||
|
|
||||||
|
{/* Minimal toolbar for existing embeds */}
|
||||||
|
{isEditable && (
|
||||||
|
<div className="absolute top-2 right-2 flex items-center gap-1.5 bg-white bg-opacity-90 backdrop-blur-sm rounded-lg p-1 shadow-sm transition-opacity opacity-70 hover:opacity-100">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveInput(embedType)}
|
||||||
|
className="p-1.5 rounded-md hover:bg-gray-100 text-gray-600"
|
||||||
|
title="Edit embed"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3Z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCenterBlock}
|
||||||
|
className="p-1.5 rounded-md hover:bg-gray-100 text-gray-600"
|
||||||
|
title={alignment === 'center' ? 'Align left' : 'Center align'}
|
||||||
|
>
|
||||||
|
<AlignCenter size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEmbedUrl('');
|
||||||
|
setEmbedCode('');
|
||||||
|
props.updateAttributes({
|
||||||
|
embedUrl: '',
|
||||||
|
embedCode: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="p-1.5 rounded-md hover:bg-gray-100 text-gray-600"
|
||||||
|
title="Remove embed"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M3 6h18"></path>
|
||||||
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
||||||
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Show the embed selection UI if we don't have content yet
|
||||||
|
<div className="w-full h-full flex flex-col items-center justify-center p-2 sm:p-6">
|
||||||
|
<p className="text-gray-500 mb-2 sm:mb-4 font-medium tracking-tighter text-base sm:text-lg text-center">Add an embed from :</p>
|
||||||
|
<div className="flex flex-wrap gap-2 sm:gap-5 justify-center">
|
||||||
{supportedProducts.map((product) => (
|
{supportedProducts.map((product) => (
|
||||||
<button
|
<button
|
||||||
key={product.name}
|
key={product.name}
|
||||||
className="flex flex-col items-center group transition-transform hover:scale-110"
|
className="flex flex-col items-center group transition-transform hover:scale-110"
|
||||||
onClick={() => handleProductClick(product.guide)}
|
onClick={() => handleProductSelection(product)}
|
||||||
|
title={`Add ${product.name} embed`}
|
||||||
>
|
>
|
||||||
<div className="w-12 h-12 rounded-lg flex items-center justify-center shadow-md group-hover:shadow-lg transition-shadow" style={{ backgroundColor: product.color }}>
|
<div className="w-8 h-8 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-md group-hover:shadow-lg transition-shadow" style={{ backgroundColor: product.color }}>
|
||||||
<product.icon size={24} color="#FFFFFF" />
|
<product.icon size={isMobile ? 16 : 24} color="#FFFFFF" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs mt-2 text-gray-700 group-hover:text-gray-900 font-medium">{product.name}</span>
|
<span className="text-xs mt-1 sm:mt-2 text-gray-700 group-hover:text-gray-900 font-medium">{product.name}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500 mt-3 mb-2 text-center max-w-md">
|
||||||
|
Click a service to add an embed
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Direct input options */}
|
||||||
|
{isEditable && (
|
||||||
|
<div className="mt-4 flex gap-3 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEmbedType('url');
|
||||||
|
setActiveInput('url');
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-white rounded-lg shadow-sm hover:shadow-md transition-all text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
<LinkIcon size={14} />
|
||||||
|
<span>URL</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEmbedType('code');
|
||||||
|
setActiveInput('code');
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-white rounded-lg shadow-sm hover:shadow-md transition-all text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
<Code size={14} />
|
||||||
|
<span>Code</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute top-2 left-2 p-1 bg-white bg-opacity-70 rounded-md">
|
|
||||||
<Cuboid size={16} className="text-gray-600" />
|
|
||||||
</div>
|
</div>
|
||||||
{isEditable && (
|
)}
|
||||||
|
|
||||||
|
{/* Inline input UI - appears in place without covering content */}
|
||||||
|
{isEditable && activeInput !== 'none' && (
|
||||||
|
<div className="absolute inset-0 bg-gray-100 bg-opacity-95 backdrop-blur-sm flex items-center justify-center p-4 z-10">
|
||||||
|
<form
|
||||||
|
onSubmit={handleInputSubmit}
|
||||||
|
className="w-full max-w-lg bg-white rounded-xl shadow-lg p-4"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedProduct && activeInput === 'url' && (
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded-lg flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: selectedProduct.color }}
|
||||||
|
>
|
||||||
|
<selectedProduct.icon size={18} color="#FFFFFF" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="text-lg font-medium text-gray-800">
|
||||||
|
{activeInput === 'url'
|
||||||
|
? (selectedProduct ? `Add ${selectedProduct.name} Embed` : 'Add Embed URL')
|
||||||
|
: 'Add Embed Code'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveInput('none')}
|
||||||
|
className="p-1 rounded-full hover:bg-gray-100 text-gray-500"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeInput === 'url' ? (
|
||||||
<>
|
<>
|
||||||
<div className="absolute bottom-2 left-2 flex gap-2">
|
<div className="relative mb-2">
|
||||||
<button
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-blue-500">
|
||||||
onClick={() => handleEmbedTypeChange('url')}
|
|
||||||
className={`p-2 rounded-md transition-colors ${embedType === 'url' ? 'bg-blue-500 text-white' : 'bg-white bg-opacity-70 text-gray-600'}`}
|
|
||||||
>
|
|
||||||
<LinkIcon size={16} />
|
<LinkIcon size={16} />
|
||||||
</button>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => handleEmbedTypeChange('code')}
|
|
||||||
className={`p-2 rounded-md transition-colors ${embedType === 'code' ? 'bg-blue-500 text-white' : 'bg-white bg-opacity-70 text-gray-600'}`}
|
|
||||||
>
|
|
||||||
<Code size={16} />
|
|
||||||
</button>
|
|
||||||
{embedType === 'url' ? (
|
|
||||||
<input
|
<input
|
||||||
|
ref={urlInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={embedUrl}
|
value={embedUrl}
|
||||||
onChange={handleUrlChange}
|
onChange={handleUrlChange}
|
||||||
className="p-2 bg-white bg-opacity-70 rounded-md w-64"
|
className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none transition-all"
|
||||||
placeholder="Enter embed URL"
|
placeholder={selectedProduct ? `Paste ${selectedProduct.name} embed URL` : "Paste embed URL (YouTube, Spotify, etc.)"}
|
||||||
/>
|
autoFocus
|
||||||
) : (
|
|
||||||
<textarea
|
|
||||||
value={embedCode}
|
|
||||||
onChange={handleCodeChange}
|
|
||||||
className="p-2 bg-white bg-opacity-70 rounded-md w-64 h-20"
|
|
||||||
placeholder="Enter embed code"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Tip: Paste any {selectedProduct?.name || "YouTube, Spotify, or other"} embed URL directly
|
||||||
|
</p>
|
||||||
|
{selectedProduct && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleOpenDocs(selectedProduct.guide)}
|
||||||
|
className="text-xs text-blue-500 hover:text-blue-700 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
How to embed {selectedProduct.name}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="relative mb-2">
|
||||||
|
<textarea
|
||||||
|
ref={codeInputRef}
|
||||||
|
value={embedCode}
|
||||||
|
onChange={handleCodeChange}
|
||||||
|
className="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl h-32 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none transition-all font-mono text-sm"
|
||||||
|
placeholder="Paste embed code (iframe, embed script, etc.)"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Tip: Paste iframe or embed code from any platform
|
||||||
|
</p>
|
||||||
|
{selectedProduct && (
|
||||||
<button
|
<button
|
||||||
onClick={handleCenterBlock}
|
type="button"
|
||||||
className="absolute bottom-2 right-2 p-2 bg-white bg-opacity-70 rounded-md hover:bg-opacity-100 transition-opacity"
|
onClick={() => handleOpenDocs(selectedProduct.guide)}
|
||||||
|
className="text-xs text-blue-500 hover:text-blue-700 flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<AlignCenter size={16} className="text-gray-600" />
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
How to embed {selectedProduct.name}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveInput('none')}
|
||||||
|
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 rounded-lg"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors"
|
||||||
|
disabled={(activeInput === 'url' && !embedUrl) || (activeInput === 'code' && !embedCode)}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Resize handles */}
|
||||||
|
{isEditable && (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-0 bottom-0 w-4 cursor-ew-resize flex items-center justify-center bg-white bg-opacity-70 hover:bg-opacity-100 transition-opacity"
|
className="absolute right-0 top-0 bottom-0 w-4 cursor-ew-resize flex items-center justify-center bg-white bg-opacity-70 hover:bg-opacity-100 transition-opacity"
|
||||||
onMouseDown={(e) => handleResizeStart(e, 'horizontal')}
|
onMouseDown={(e) => handleResizeStart(e, 'horizontal')}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<ToolTip content={'YouTube video'}>
|
<ToolTip content={'YouTube video'}>
|
||||||
<ToolBtn onClick={() => addYoutubeVideo()}>
|
<ToolBtn onClick={() => editor.chain().focus().insertContent({ type: 'blockEmbed' }).run()}>
|
||||||
<SiYoutube size={15} />
|
<SiYoutube size={15} />
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue