diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx index 53c8b4f5..33bf798b 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx @@ -44,6 +44,12 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta }); const handleFileChange = async (event: any) => { + // Check if user is authenticated + if (!access_token) { + setError('Authentication required. Please sign in to upload files.'); + return; + } + const file = event.target.files[0] setLocalUploadFile(file) @@ -69,21 +75,30 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta setIsLoading(false) setError('') } - } async function getAssignmentTaskSubmissionFromUserUI() { + if (!access_token) { + // Silently fail if not authenticated + return; + } + if (assignmentTaskUUID) { const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token); if (res.success) { setUserSubmissions(res.data.task_submission); setInitialUserSubmissions(res.data.task_submission); } - } } const submitFC = async () => { + // Check if user is authenticated + if (!access_token) { + toast.error('Authentication required. Please sign in to submit your task.'); + return; + } + // Save the quiz to the server const values = { task_submission: userSubmissions, @@ -105,13 +120,17 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta }; async function getAssignmentTaskUI() { + if (!access_token) { + // Silently fail if not authenticated + return; + } + if (assignmentTaskUUID) { const res = await getAssignmentTask(assignmentTaskUUID, access_token); if (res.success) { setAssignmentTask(res.data); setAssignmentTaskOutsideProvider(res.data); } - } } @@ -129,6 +148,11 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta /* GRADING VIEW CODE */ const [userSubmissionObject, setUserSubmissionObject] = useState(null); async function getAssignmentTaskSubmissionFromIdentifiedUserUI() { + if (!access_token) { + // Silently fail if not authenticated + return; + } + if (assignmentTaskUUID && user_id) { const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token); if (res.success) { @@ -136,7 +160,6 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta setUserSubmissionObject(res.data); setInitialUserSubmissions(res.data.task_submission); } - } } @@ -186,31 +209,29 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta return ( {view === 'teacher' && ( -
- +
+

User will be able to submit a file for this task, you'll be able to review it in the Submissions Tab

)} {view === 'custom-grading' && ( -
-
- +
+
+

Please download the file and grade it manually, then input the grade above

{userSubmissions.fileUUID && !isLoading && assignmentTaskUUID && ( -
- + className='flex flex-col rounded-lg bg-white text-gray-500 shadow-sm hover:shadow-md transition-shadow border border-gray-100 px-4 sm:px-5 py-4 space-y-1 items-center relative w-full sm:w-auto mx-auto'> +
+
-
- -
+
+ +
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
@@ -220,66 +241,72 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta )} {view === 'student' && ( <> -
-
-
-
+
+
+
+
{error && ( -
-
{error}
+
+
{error}
)} -
{localUploadFile && !isLoading && ( -
-
- +
+
+
-
- - -
- {localUploadFile.name} +
+ +
+ {localUploadFile.name.length > 20 + ? `${localUploadFile.name.slice(0, 10)}...${localUploadFile.name.slice(-10)}` + : localUploadFile.name}
)} {userSubmissions.fileUUID && !isLoading && !localUploadFile && ( -
-
- +
+
+
-
- - -
+
+ +
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
)} -
- -

Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx, zip

+
+ +

Allowed formats: pdf, docx, mp4, jpg, jpeg, png, pptx, zip

- {isLoading ? ( -
+ {!access_token ? ( +
+
+ +
Please sign in to upload files
+
+
+ ) : isLoading ? ( +
-
- +
+ Loading
) : ( -
+
diff --git a/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx b/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx index 73070c8a..b7aab009 100644 --- a/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx +++ b/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx @@ -1,6 +1,7 @@ import { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext' import { BookPlus, BookUser, EllipsisVertical, FileUp, Forward, InfoIcon, ListTodo, Save } from 'lucide-react' import React, { useEffect } from 'react' +import { useLHSession } from '@components/Contexts/LHSessionContext' type AssignmentBoxProps = { type: 'quiz' | 'file' @@ -13,21 +14,25 @@ type AssignmentBoxProps = { gradeCustomFC?: (grade: number) => void showSavingDisclaimer?: boolean children: React.ReactNode - } function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitFC, gradeFC, gradeCustomFC, showSavingDisclaimer, children }: AssignmentBoxProps) { const [customGrade, setCustomGrade] = React.useState(0) const submission = useAssignmentSubmission() as any + const session = useLHSession() as any + useEffect(() => { console.log(submission) - } - , [submission]) + }, [submission]) + + // Check if user is authenticated + const isAuthenticated = session?.status === 'authenticated' return ( -
-
-
+
+
+ {/* Left side with type and badges */} +
{type === 'quiz' &&
@@ -41,26 +46,27 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF
}
-
{view === 'teacher' && -
+

Teacher view

} {maxPoints && -
+

{maxPoints} points

}
-
+ + {/* Right side with buttons and actions */} +
{showSavingDisclaimer && -
+

Don't forget to save your progress

@@ -70,17 +76,17 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF {view === 'teacher' &&
saveFC && saveFC()} - className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'> + className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>

Save

} - {/* Student button */} - {view === 'student' && submission && submission.length <= 0 && + {/* Student button - only show if authenticated */} + {view === 'student' && isAuthenticated && submission && submission.length <= 0 &&
submitFC && submitFC()} - className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'> + className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center justify-center mx-auto w-full sm:w-auto bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>

Save your progress

@@ -89,11 +95,11 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF {/* Grading button */} {view === 'grading' &&
-

Current points : {currentPoints}

+ className='flex flex-wrap sm:flex-nowrap w-full sm:w-auto px-0.5 py-0.5 cursor-pointer rounded-md gap-2 sm:space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'> +

Current points: {currentPoints}

gradeFC && gradeFC()} - className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'> + className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2 ml-auto'>

Grade

@@ -103,16 +109,21 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF {/* CustomGrading button */} {view === 'custom-grading' && maxPoints &&
-

Current points : {currentPoints}

- setCustomGrade(parseInt(e.target.value))} - placeholder={maxPoints.toString()} className='w-[100px] light-shadow text-sm py-0.5 outline outline-gray-200 rounded-lg px-2' type="number" /> -
gradeCustomFC && gradeCustomFC(customGrade)} - className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'> - -

Grade

+ className='flex flex-wrap sm:flex-nowrap w-full sm:w-auto px-0.5 py-0.5 cursor-pointer rounded-md gap-2 sm:space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'> +

Current points: {currentPoints}

+
+ setCustomGrade(parseInt(e.target.value))} + placeholder={maxPoints.toString()} + className='w-full sm:w-[100px] light-shadow text-sm py-0.5 outline outline-gray-200 rounded-lg px-2' + type="number" + /> +
gradeCustomFC && gradeCustomFC(customGrade)} + className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2 whitespace-nowrap'> + +

Grade

+
} diff --git a/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx b/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx index c9eb6d53..5f22f415 100644 --- a/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx +++ b/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx @@ -18,38 +18,39 @@ function AssignmentStudentActivity() { return ( -
-
-
-
- +
+
+
+
+

Assignment

- +
-
+
-

Due Date

-

{assignments?.assignment_object?.due_date}

+

Due Date

+

{assignments?.assignment_object?.due_date}

-
+
+ {assignments && assignments?.assignment_tasks?.sort((a: any, b: any) => a.id - b.id).map((task: any, index: number) => { return (
-
-
+
+

Task {index + 1} :

-

{task.description}

+

{task.description}

-
+
alert(task.hint)} className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'> @@ -67,9 +68,9 @@ function AssignmentStudentActivity() { )} target='_blank' download={true} - className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'> + className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-1 md:space-x-2 cursor-pointer'> -
+
{task.reference_file && ( @@ -80,7 +81,7 @@ function AssignmentStudentActivity() {
-
+
{task.assignment_type === 'QUIZ' && } {task.assignment_type === 'FILE_SUBMISSION' && }
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 diff --git a/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx index c993d4d4..2335a166 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,44 @@ const SCRIPT_BASED_EMBEDS = { // Add more platforms as needed }; +// Helper function to convert YouTube URLs to embed format +const getYouTubeEmbedUrl = (url: string): string => { + try { + // First validate that this is a proper URL + const parsedUrl = new URL(url); + + // Ensure the hostname is actually YouTube + const isYoutubeHostname = + parsedUrl.hostname === 'youtube.com' || + parsedUrl.hostname === 'www.youtube.com' || + parsedUrl.hostname === 'youtu.be' || + parsedUrl.hostname === 'www.youtu.be'; + + if (!isYoutubeHostname) { + return url; // Not a YouTube URL, return as is + } + + // Handle different YouTube URL formats with a more precise regex + const youtubeRegex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i; + const match = url.match(youtubeRegex); + + if (match && match[1]) { + // Validate the video ID format (should be exactly 11 characters) + const videoId = match[1]; + if (videoId.length === 11) { + // Return the embed URL with the video ID and secure protocol + return `https://www.youtube.com/embed/${videoId}?autoplay=0&rel=0`; + } + } + + // If no valid match found, return the original URL + return url; + } catch (e) { + // If URL parsing fails, 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 +81,26 @@ const MemoizedEmbed = React.memo(({ embedUrl, sanitizedEmbedCode, embedType }: { }, [embedType, sanitizedEmbedCode]); if (embedType === 'url' && embedUrl) { + // Process the URL if it's a YouTube URL - using proper URL validation + let isYoutubeUrl = false; + + try { + const url = new URL(embedUrl); + // Check if the hostname is exactly youtube.com or youtu.be (or www variants) + isYoutubeUrl = url.hostname === 'youtube.com' || + url.hostname === 'www.youtube.com' || + url.hostname === 'youtu.be' || + url.hostname === 'www.youtu.be'; + } catch (e) { + // Invalid URL format, not a YouTube URL + isYoutubeUrl = false; + } + + const processedUrl = isYoutubeUrl ? getYouTubeEmbedUrl(embedUrl) : embedUrl; + return (