From 552d21046e3f8549200f59da9cad5f1019f0c4c9 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 26 Feb 2025 10:20:41 +0100 Subject: [PATCH 1/7] feat: Improve ImageBlockComponent with responsive resizing and layout --- .../Extensions/Image/ImageBlockComponent.tsx | 103 +++++++++++------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/apps/web/components/Objects/Editor/Extensions/Image/ImageBlockComponent.tsx b/apps/web/components/Objects/Editor/Extensions/Image/ImageBlockComponent.tsx index 786a7384..50219349 100644 --- a/apps/web/components/Objects/Editor/Extensions/Image/ImageBlockComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/Image/ImageBlockComponent.tsx @@ -18,7 +18,7 @@ function ImageBlockComponent(props: any) { const course = useCourse() as any const editorState = useEditorProvider() as any const session = useLHSession() as any - const access_token = session?.data?.tokens?.access_token; + const access_token = session?.data?.tokens?.access_token; const isEditable = editorState.isEditable const [image, setImage] = React.useState(null) @@ -29,6 +29,7 @@ function ImageBlockComponent(props: any) { const [imageSize, setImageSize] = React.useState({ width: props.node.attrs.size ? props.node.attrs.size.width : 300, }) + const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null @@ -54,48 +55,70 @@ function ImageBlockComponent(props: any) { useEffect(() => {}, [course, org]) return ( - + - {blockObject && ( - { - props.updateAttributes({ - size: { - width: imageSize.width + d.width, + {blockObject && isEditable && ( +
+ + }} + style={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + maxWidth: '100%', + }} + maxWidth="100%" + minWidth={200} + enable={{ right: true }} + onResizeStop={(e, direction, ref, d) => { + const newWidth = Math.min(imageSize.width + d.width, ref.parentElement?.clientWidth || 1000); + props.updateAttributes({ + size: { + width: newWidth, + }, + }) + setImageSize({ + width: newWidth, + }) + }} + > + + +
+ )} + + {blockObject && !isEditable && ( +
- +
)} + {isLoading && (
From 10b1e89bf01e63f4f63845aaefd1d7878a6bc19d Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 26 Feb 2025 10:38:49 +0100 Subject: [PATCH 2/7] feat: Enhance MathEquationBlockComponent with advanced LaTeX editing features --- .../MathEquationBlockComponent.tsx | 499 +++++++++++++++--- 1 file changed, 423 insertions(+), 76 deletions(-) diff --git a/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx b/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx index df903a40..b1ef87bb 100644 --- a/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx @@ -3,20 +3,241 @@ import React from 'react' import styled from 'styled-components' import 'katex/dist/katex.min.css' import { BlockMath } from 'react-katex' -import { Save } from 'lucide-react' +import { Save, Sigma, ExternalLink, ChevronDown, BookOpen, Lightbulb } from 'lucide-react' import Link from 'next/link' import { useEditorProvider } from '@components/Contexts/Editor/EditorContext' +import { motion } from 'framer-motion' + +// Predefined LaTeX templates +const mathTemplates = [ + { + name: 'Fraction', + latex: '\\frac{a}{b}', + description: 'Simple fraction' + }, + { + name: 'Square Root', + latex: '\\sqrt{x}', + description: 'Square root' + }, + { + name: 'Summation', + latex: '\\sum_{i=1}^{n} x_i', + description: 'Sum with limits' + }, + { + name: 'Integral', + latex: '\\int_{a}^{b} f(x) \\, dx', + description: 'Definite integral' + }, + { + name: 'Limit', + latex: '\\lim_{x \\to \\infty} f(x)', + description: 'Limit expression' + }, + { + name: 'Matrix 2×2', + latex: '\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}', + description: '2×2 matrix with parentheses' + }, + { + name: 'Binomial', + latex: '\\binom{n}{k}', + description: 'Binomial coefficient' + }, + { + name: 'Quadratic Formula', + latex: 'x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}', + description: 'Solution to quadratic equation' + }, + { + name: 'Vector', + latex: '\\vec{v} = \\begin{pmatrix} x \\\\ y \\\\ z \\end{pmatrix}', + description: '3D vector' + }, + { + name: 'System of Equations', + latex: '\\begin{cases} a_1x + b_1y = c_1 \\\\ a_2x + b_2y = c_2 \\end{cases}', + description: 'System of linear equations' + } +]; + +// Common LaTeX symbols +const mathSymbols = [ + { symbol: '\\alpha', display: 'α' }, + { symbol: '\\beta', display: 'β' }, + { symbol: '\\gamma', display: 'γ' }, + { symbol: '\\delta', display: 'δ' }, + { symbol: '\\theta', display: 'θ' }, + { symbol: '\\pi', display: 'π' }, + { symbol: '\\sigma', display: 'σ' }, + { symbol: '\\infty', display: '∞' }, + { symbol: '\\pm', display: '±' }, + { symbol: '\\div', display: '÷' }, + { symbol: '\\cdot', display: '·' }, + { symbol: '\\leq', display: '≤' }, + { symbol: '\\geq', display: '≥' }, + { symbol: '\\neq', display: '≠' }, + { symbol: '\\approx', display: '≈' }, +]; + +// Styled components +const MathEqWrapper = styled.div` + transition: all 0.2s ease; + background-color: #f9f9f9; + border: 1px solid #eaeaea; +` + +const EditBar = styled.div` + display: flex; + justify-content: space-between; + border-radius: 8px; + padding: 0 5px 0 12px; + background-color: white; + color: #5252528d; + align-items: center; + height: 45px; + border: solid 1px #e2e2e2; + transition: all 0.2s ease; + + &:focus-within { + border-color: #d1d1d1; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.03); + } + + input { + border: none; + background: none; + font-size: 14px; + color: #494949; + width: 100%; + font-family: 'DM Sans', sans-serif; + + &:focus { + outline: none; + } + + &::placeholder { + color: #49494980; + } + } +` + +const SaveButton = styled(motion.button)` + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 6px; + border: none; + background: rgba(217, 217, 217, 0.5); + color: #494949; + cursor: pointer; +` + +const InfoLink = styled.div` + padding-left: 2px; +` + +const TemplateButton = styled.button` + display: flex; + align-items: center; + padding: 6px 10px; + background: rgba(217, 217, 217, 0.4); + border-radius: 6px; + border: none; + font-size: 13px; + color: #494949; + cursor: pointer; +` + +const TemplateDropdown = styled.div` + background: white; + border-radius: 8px; + border: 1px solid #e2e2e2; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + overflow: hidden; +` + +const TemplateItem = styled.div` + padding: 8px 12px; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: rgba(217, 217, 217, 0.24); + } +` + +const SymbolsDropdown = styled.div` + background: white; + border-radius: 8px; + border: 1px solid #e2e2e2; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + overflow: hidden; +` + +const SymbolButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + margin: 2px; + background: rgba(217, 217, 217, 0.3); + border-radius: 4px; + border: none; + font-size: 16px; + color: #494949; + cursor: pointer; +` + +const HelpDropdown = styled.div` + background: white; + border-radius: 8px; + border: 1px solid #e2e2e2; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + overflow: hidden; +` function MathEquationBlockComponent(props: any) { const [equation, setEquation] = React.useState(props.node.attrs.math_equation) const [isEditing, setIsEditing] = React.useState(true) + const [showTemplates, setShowTemplates] = React.useState(false) + const [showSymbols, setShowSymbols] = React.useState(false) + const [showHelp, setShowHelp] = React.useState(false) const editorState = useEditorProvider() as any const isEditable = editorState.isEditable + const inputRef = React.useRef(null) + const templatesRef = React.useRef(null) + const symbolsRef = React.useRef(null) + const helpRef = React.useRef(null) - const handleEquationChange = (event: React.ChangeEvent) => { + // Close dropdowns when clicking outside + React.useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (templatesRef.current && !templatesRef.current.contains(event.target as Node)) { + setShowTemplates(false) + } + if (symbolsRef.current && !symbolsRef.current.contains(event.target as Node)) { + setShowSymbols(false) + } + if (helpRef.current && !helpRef.current.contains(event.target as Node)) { + setShowHelp(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + const handleEquationChange = (event: React.ChangeEvent) => { setEquation(event.target.value) props.updateAttributes({ - math_equation: equation, + math_equation: event.target.value, }) } @@ -27,83 +248,209 @@ function MathEquationBlockComponent(props: any) { //setIsEditing(false); } + const insertTemplate = (template: string) => { + setEquation(template) + props.updateAttributes({ + math_equation: template, + }) + setShowTemplates(false) + + // Focus the input and place cursor at the end + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.setSelectionRange(template.length, template.length) + } + } + + const insertSymbol = (symbol: string) => { + const cursorPosition = inputRef.current?.selectionStart || equation.length + const newEquation = equation.substring(0, cursorPosition) + symbol + equation.substring(cursorPosition) + + setEquation(newEquation) + props.updateAttributes({ + math_equation: newEquation, + }) + + // Focus the input and place cursor after the inserted symbol + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.setSelectionRange(cursorPosition + symbol.length, cursorPosition + symbol.length) + } + }, 0) + } + return ( - - {equation} - {isEditing && isEditable && ( - <> - - - - - - Please refer to this{' '} - - {' '} - guide - {' '} - for supported TeX functions{' '} - - - )} - + + +
+ + Math Equation +
+ +
+ {equation} +
+ + {isEditing && isEditable && ( + +
+
+ setShowTemplates(!showTemplates)} + className="flex items-center space-x-1" + > + + Templates + + + + {showTemplates && ( + +
+ Select a template to insert +
+ {mathTemplates.map((template, index) => ( + insertTemplate(template.latex)} + > +
+ {template.name} + {template.description} +
+
+ ))} +
+ )} +
+ +
+ setShowSymbols(!showSymbols)} + className="flex items-center space-x-1" + > + + Symbols + + + + {showSymbols && ( + +
+ Click a symbol to insert +
+
+ {mathSymbols.map((symbol, index) => ( + insertSymbol(symbol.symbol)} + title={symbol.symbol} + > + {symbol.display} + + ))} +
+
+ )} +
+ +
+ setShowHelp(!showHelp)} + className="flex items-center space-x-1" + > + + Help + + + + {showHelp && ( + +
+ LaTeX Math Quick Reference +
+
+
+ Fractions: \frac{'{'}'numerator'{'}'}{'{'}denominator{'}'} +
+
+ Exponents: x^{'{'}'power'{'}'} +
+
+ Subscripts: x_{'{'}'subscript'{'}'} +
+
+ Square root: \sqrt{'{'}'x'{'}'} +
+
+ Summation: \sum_{'{'}'lower'{'}'}^{'{'}'upper'{'}'} +
+
+ Integral: \int_{'{'}'lower'{'}'}^{'{'}'upper'{'}'} +
+
+ + View complete reference + + +
+
+
+ )} +
+
+ + + + saveEquation()} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + + + + + + Please refer to this + + guide + + + for supported TeX functions + +
+ )} +
+
) } export default MathEquationBlockComponent - -const MathEqWrapper = styled.div`` - -const EditBar = styled.div` - display: flex; - justify-content: flex-end; - margin-top: 10px; - background-color: white; - border-radius: 10px; - padding: 5px; - color: #5252528d; - align-items: center; - justify-content: space-between; - height: 50px; - border: solid 1px #52525224; - - button { - margin-left: 10px; - margin-right: 7px; - cursor: pointer; - border: none; - background: none; - font-size: 14px; - color: #494949; - } - - input { - border: none; - background: none; - font-size: 14px; - color: #494949; - width: 100%; - font-family: 'DM Sans', sans-serif; - padding-left: 10px; - &:focus { - outline: none; - } - - &::placeholder { - color: #49494936; - } - } -` From 632cc79838dd8dffb68cea1320539091d8a4d3cf Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 26 Feb 2025 11:15:57 +0100 Subject: [PATCH 3/7] feat: Add advanced configuration and styling for Callout components --- .../Callout/Info/InfoCalloutComponent.tsx | 142 +++++++++++++--- .../Warning/WarningCalloutComponent.tsx | 152 ++++++++++++++---- 2 files changed, 246 insertions(+), 48 deletions(-) diff --git a/apps/web/components/Objects/Editor/Extensions/Callout/Info/InfoCalloutComponent.tsx b/apps/web/components/Objects/Editor/Extensions/Callout/Info/InfoCalloutComponent.tsx index 8d3501e1..85db7c9f 100644 --- a/apps/web/components/Objects/Editor/Extensions/Callout/Info/InfoCalloutComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/Callout/Info/InfoCalloutComponent.tsx @@ -1,29 +1,71 @@ import { useEditorProvider } from '@components/Contexts/Editor/EditorContext' import { NodeViewContent, NodeViewWrapper } from '@tiptap/react' -import { AlertCircle } from 'lucide-react' -import React from 'react' +import { AlertCircle, X } from 'lucide-react' +import React, { useState } from 'react' import styled from 'styled-components' -function InfoCalloutComponent(props: any) { - const editorState = useEditorProvider() as any - const isEditable = editorState.isEditable - - return ( - - - {' '} - - - - ) +interface CalloutOptions { + dismissible?: boolean; + variant?: 'default' | 'filled' | 'outlined'; + size?: 'sm' | 'md' | 'lg'; } -const InfoCalloutWrapper = styled.div` +const IconWrapper = styled.div<{ size?: string }>` + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-right: 0.5rem; + padding-left: 0.5rem; + svg { - padding: 3px; + width: 20px; + height: 20px; + min-width: 20px; + } + + @media (max-width: 640px) { + margin-right: 0.25rem; + padding-left: 0.375rem; + padding-top: ${props => props.size === 'sm' ? '0' : '0.5rem'}; + align-self: ${props => props.size === 'sm' ? 'center' : 'flex-start'}; + } +` + +const ContentWrapper = styled.div` + width: 100%; + overflow-wrap: break-word; +` + +const DismissButton = styled.button` + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + margin-left: 8px; + border-radius: 50%; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } +` + +const InfoCalloutWrapper = styled.div<{ size?: string }>` + width: 100%; + display: flex; + position: relative; + margin: 1rem 0; + + @media (max-width: 640px) { + flex-direction: ${props => props.size === 'sm' ? 'row' : 'column'}; + align-items: ${props => props.size === 'sm' ? 'center' : 'flex-start'}; + } + + svg { + padding: 0; } .content { @@ -32,7 +74,69 @@ const InfoCalloutWrapper = styled.div` border: ${(props) => props.contentEditable ? '2px dashed #1f3a8a12' : 'none'}; border-radius: 0.5rem; + + @media (max-width: 640px) { + margin: ${props => props.size === 'sm' ? '3px' : '5px 0'}; + padding: ${props => props.size === 'sm' ? '0.25rem' : '0.5rem'}; + width: 100%; + } } ` +function InfoCalloutComponent(props: any) { + const editorState = useEditorProvider() as any + const isEditable = editorState.isEditable + const [dismissed, setDismissed] = useState(false) + + // Extract options from props or use defaults + const options: CalloutOptions = { + dismissible: props.node?.attrs?.dismissible || false, + variant: props.node?.attrs?.variant || 'default', + size: props.node?.attrs?.size || 'md', + } + + if (dismissed) return null; + + const getVariantClasses = () => { + switch(options.variant) { + case 'filled': + return 'bg-blue-500 text-white'; + case 'outlined': + return 'bg-transparent border-2 border-blue-500 text-blue-700'; + default: + return 'bg-blue-200 text-blue-900'; + } + } + + const getSizeClasses = () => { + switch(options.size) { + case 'sm': return 'py-1 px-2 text-sm'; + case 'lg': return 'py-3 px-4 text-lg'; + default: return 'py-2 px-3'; + } + } + + return ( + + + + + + + + + {options.dismissible && !isEditable && ( + setDismissed(true)}> + + + )} + + + ) +} + export default InfoCalloutComponent diff --git a/apps/web/components/Objects/Editor/Extensions/Callout/Warning/WarningCalloutComponent.tsx b/apps/web/components/Objects/Editor/Extensions/Callout/Warning/WarningCalloutComponent.tsx index c28c2b0f..f5ed7f81 100644 --- a/apps/web/components/Objects/Editor/Extensions/Callout/Warning/WarningCalloutComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/Callout/Warning/WarningCalloutComponent.tsx @@ -1,29 +1,71 @@ import { useEditorProvider } from '@components/Contexts/Editor/EditorContext' import { NodeViewContent, NodeViewWrapper } from '@tiptap/react' -import { AlertTriangle } from 'lucide-react' -import React from 'react' +import { AlertTriangle, X } from 'lucide-react' +import React, { useState } from 'react' import styled from 'styled-components' -function WarningCalloutComponent(props: any) { - const editorState = useEditorProvider() as any - const isEditable = editorState.isEditable - - return ( - - - {' '} - - - - ) +interface CalloutOptions { + dismissible?: boolean; + variant?: 'default' | 'filled' | 'outlined'; + size?: 'sm' | 'md' | 'lg'; } -const CalloutWrapper = styled.div` +const IconWrapper = styled.div<{ size?: string }>` + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-right: 0.5rem; + padding-left: 0.5rem; + svg { - padding: 3px; + width: 20px; + height: 20px; + min-width: 20px; + } + + @media (max-width: 640px) { + margin-right: 0.25rem; + padding-left: 0.375rem; + padding-top: ${props => props.size === 'sm' ? '0' : '0.5rem'}; + align-self: ${props => props.size === 'sm' ? 'center' : 'flex-start'}; + } +` + +const ContentWrapper = styled.div` + width: 100%; + overflow-wrap: break-word; +` + +const DismissButton = styled.button` + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + margin-left: 8px; + border-radius: 50%; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } +` + +const CalloutWrapper = styled.div<{ size?: string }>` + width: 100%; + display: flex; + position: relative; + margin: 1rem 0; + + @media (max-width: 640px) { + flex-direction: ${props => props.size === 'sm' ? 'row' : 'column'}; + align-items: ${props => props.size === 'sm' ? 'center' : 'flex-start'}; + } + + svg { + padding: 0; } .content { @@ -32,17 +74,69 @@ const CalloutWrapper = styled.div` border: ${(props) => props.contentEditable ? '2px dashed #713f1117' : 'none'}; border-radius: 0.5rem; + + @media (max-width: 640px) { + margin: ${props => props.size === 'sm' ? '3px' : '5px 0'}; + padding: ${props => props.size === 'sm' ? '0.25rem' : '0.5rem'}; + width: 100%; + } } ` -const DragHandle = styled.div` - position: absolute; - top: 0; - left: 0; - width: 1rem; - height: 100%; - cursor: move; - z-index: 1; -` +function WarningCalloutComponent(props: any) { + const editorState = useEditorProvider() as any + const isEditable = editorState.isEditable + const [dismissed, setDismissed] = useState(false) + + // Extract options from props or use defaults + const options: CalloutOptions = { + dismissible: props.node?.attrs?.dismissible || false, + variant: props.node?.attrs?.variant || 'default', + size: props.node?.attrs?.size || 'md', + } + + if (dismissed) return null; + + const getVariantClasses = () => { + switch(options.variant) { + case 'filled': + return 'bg-yellow-500 text-white'; + case 'outlined': + return 'bg-transparent border-2 border-yellow-500 text-yellow-700'; + default: + return 'bg-yellow-200 text-yellow-900'; + } + } + + const getSizeClasses = () => { + switch(options.size) { + case 'sm': return 'py-1 px-2 text-sm'; + case 'lg': return 'py-3 px-4 text-lg'; + default: return 'py-2 px-3'; + } + } -export default WarningCalloutComponent + return ( + + + + + + + + + {options.dismissible && !isEditable && ( + setDismissed(true)}> + + + )} + + + ) +} + +export default WarningCalloutComponent \ No newline at end of file From 5dd9d5d749f7c6b14558e4ed216ae2e1b5961f36 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 26 Feb 2025 11:37:29 +0100 Subject: [PATCH 4/7] feat: Enhance EmbedObjectsComponent with YouTube support and responsive design, overall improvements --- .../EmbedObjects/EmbedObjectsComponent.tsx | 401 +++++++++++++++--- .../Objects/Editor/Toolbar/ToolbarButtons.tsx | 2 +- 2 files changed, 351 insertions(+), 52 deletions(-) diff --git a/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx index c993d4d4..385955e6 100644 --- a/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx @@ -2,7 +2,7 @@ import { NodeViewWrapper } from '@tiptap/react' import React, { useState, useRef, useEffect, useMemo } from 'react' import { Upload, Link as LinkIcon, GripVertical, GripHorizontal, AlignCenter, Cuboid, Code } from 'lucide-react' 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 DOMPurify from 'dompurify' @@ -14,6 +14,21 @@ const SCRIPT_BASED_EMBEDS = { // 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 const MemoizedEmbed = React.memo(({ embedUrl, sanitizedEmbedCode, embedType }: { embedUrl: string; @@ -43,9 +58,14 @@ const MemoizedEmbed = React.memo(({ embedUrl, sanitizedEmbedCode, embedType }: { }, [embedType, sanitizedEmbedCode]); 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 (