feat: add YouTube URL input to media screen video section (NES-1369)#8799
feat: add YouTube URL input to media screen video section (NES-1369)#8799jaco-brink wants to merge 2 commits intomainfrom
Conversation
Adds a YouTube URL input below the custom file upload button in the media screen's VideosSection. Users can paste a YouTube URL, which is validated and linked to the selected card's video block via the existing VIDEO_BLOCK_UPDATE mutation. Loading states reuse the same 'updating' status as Mux uploads, preventing the custom upload button from being clicked and blocking navigation via the Next button until complete. Made-with: Cursor
WalkthroughThis pull request adds YouTube video linking support to the template customization media workflow. It includes a YouTube video ID extraction utility, a new React hook for persisting YouTube videos via GraphQL mutation, updates to the TemplateVideoUploadContext to expose YouTube linking functionality, and UI enhancements to the VideosSection component to accept and validate YouTube URLs. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant VideosSection
participant VideoIDUtils
participant TemplateVideoUploadContext
participant useYouTubeVideoLinking
participant ApolloMutation as Apollo (VIDEO_BLOCK_UPDATE)
participant Notistack as Toast Notifications
User->>VideosSection: Enter YouTube URL & click Set
VideosSection->>VideosSection: handleYouTubeSubmit
VideosSection->>VideoIDUtils: extractYouTubeVideoId(url)
VideoIDUtils-->>VideosSection: youtubeVideoId
VideosSection->>TemplateVideoUploadContext: startYouTubeLink(videoBlockId, youtubeVideoId)
TemplateVideoUploadContext->>useYouTubeVideoLinking: linkYouTubeVideo(videoBlockId, youtubeVideoId)
useYouTubeVideoLinking->>useYouTubeVideoLinking: Mark block as updating
useYouTubeVideoLinking->>ApolloMutation: Execute VIDEO_BLOCK_UPDATE
ApolloMutation-->>useYouTubeVideoLinking: Success/Error response
useYouTubeVideoLinking->>useYouTubeVideoLinking: Refetch GET_JOURNEY
alt Success
useYouTubeVideoLinking->>Notistack: Show success toast
useYouTubeVideoLinking->>useYouTubeVideoLinking: Update upload task state
else Error
useYouTubeVideoLinking->>Notistack: Show error toast
useYouTubeVideoLinking->>useYouTubeVideoLinking: Update task with error status
end
useYouTubeVideoLinking->>useYouTubeVideoLinking: Clean up active block marker
useYouTubeVideoLinking-->>VideosSection: Promise resolved
VideosSection->>VideosSection: Clear input on success
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit 1b33bbb
☁️ Nx Cloud last updated this comment at |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/VideosSection/VideosSection.tsx (1)
209-223: Use named handlers for input/button events and clear stale validation while typing.Line 209 and Line 222 use inline event lambdas. Extract named handlers (e.g.,
handleYouTubeUrlChange,handleSetYouTubeClick) and clearyoutubeUrlErrorinside the change handler so the error updates immediately as users edit.♻️ Suggested refactor
-import { KeyboardEvent, ReactElement, useState } from 'react' +import { ChangeEvent, KeyboardEvent, ReactElement, useState } from 'react' @@ + function handleYouTubeUrlChange( + event: ChangeEvent<HTMLInputElement> + ): void { + setYoutubeUrl(event.target.value) + if (youtubeUrlError != null) setYoutubeUrlError(undefined) + } + + function handleSetYouTubeClick(): void { + void handleYouTubeSubmit() + } + @@ - onChange={(e) => setYoutubeUrl(e.target.value)} + onChange={handleYouTubeUrlChange} @@ - onClick={() => void handleYouTubeSubmit()} + onClick={handleSetYouTubeClick}As per coding guidelines, "event functions should be named with a “handle” prefix, like “handleClick” for onClick and “handleKeyDown” for onKeyDown".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/VideosSection/VideosSection.tsx` around lines 209 - 223, Extract inline lambdas into named handlers: add handleYouTubeUrlChange that accepts the input event, calls setYoutubeUrl(e.target.value) and clears youtubeUrlError (e.g., setYoutubeUrlError(null)) so validation clears while typing; replace onChange={(e) => setYoutubeUrl(...)} with onChange={handleYouTubeUrlChange}. Add handleSetYouTubeClick that calls void handleYouTubeSubmit() and use it for onClick instead of the inline arrow; keep handleYouTubeKeyDown as the existing key handler. Ensure the Button disabled prop still uses youtubeUrl.trim() === '' and that the new handlers reference the existing state setters (setYoutubeUrl, setYoutubeUrlError) and submit function (handleYouTubeSubmit).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/VideosSection/VideosSection.tsx`:
- Around line 150-153: The code clears youtubeUrl (via setYoutubeUrl('')) before
awaiting the async startYouTubeLink call, which discards the user's pasted URL
if the mutation fails; change the flow in the handler that currently calls
setYoutubeUrlError(undefined); setYoutubeUrl(''); await
startYouTubeLink(videoBlock.id, extractedId) to instead clear the url only after
startYouTubeLink resolves successfully—e.g., call setYoutubeUrlError(undefined)
then await startYouTubeLink(videoBlock.id, extractedId) inside a try block and
only call setYoutubeUrl('') in the success path, and catch errors to set an
appropriate error via setYoutubeUrlError without clearing the input so the
user’s URL is preserved.
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/utils/videoSectionUtils/videoSectionUtils.ts`:
- Around line 8-17: The extractYouTubeVideoId function currently uses
YOUTUBE_ID_REGEX against any string and can return false positives from
non-YouTube hosts; update extractYouTubeVideoId to first parse the input with
the URL constructor (catching invalid URLs) and verify the hostname belongs to
an allowed YouTube host (e.g., endsWith "youtube.com", equals "youtu.be", or
includes "youtube-nocookie.com" and handle "shorts."), and only then apply
YOUTUBE_ID_REGEX (or extract from pathname/search) to get the 11-character ID;
ensure non-YouTube hosts or parse failures return null and keep YOUTUBE_ID_REGEX
as the extraction pattern reference.
---
Nitpick comments:
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/VideosSection/VideosSection.tsx`:
- Around line 209-223: Extract inline lambdas into named handlers: add
handleYouTubeUrlChange that accepts the input event, calls
setYoutubeUrl(e.target.value) and clears youtubeUrlError (e.g.,
setYoutubeUrlError(null)) so validation clears while typing; replace
onChange={(e) => setYoutubeUrl(...)} with onChange={handleYouTubeUrlChange}. Add
handleSetYouTubeClick that calls void handleYouTubeSubmit() and use it for
onClick instead of the inline arrow; keep handleYouTubeKeyDown as the existing
key handler. Ensure the Button disabled prop still uses youtubeUrl.trim() === ''
and that the new handlers reference the existing state setters (setYoutubeUrl,
setYoutubeUrlError) and submit function (handleYouTubeSubmit).
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/VideosSection/VideosSection.tsxapps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/utils/index.tsapps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/utils/videoSectionUtils/index.tsapps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/utils/videoSectionUtils/videoSectionUtils.tsapps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/TemplateVideoUploadProvider/TemplateVideoUploadProvider.tsxapps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/TemplateVideoUploadProvider/types.tsapps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/TemplateVideoUploadProvider/useYouTubeVideoLinking.ts
| setYoutubeUrlError(undefined) | ||
| setYoutubeUrl('') | ||
| await startYouTubeLink(videoBlock.id, extractedId) | ||
| } |
There was a problem hiding this comment.
Preserve the URL until linking succeeds.
Line 151 clears youtubeUrl before the async link attempt completes. If the mutation fails, users lose the pasted URL and need to re-enter it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/VideosSection/VideosSection.tsx`
around lines 150 - 153, The code clears youtubeUrl (via setYoutubeUrl(''))
before awaiting the async startYouTubeLink call, which discards the user's
pasted URL if the mutation fails; change the flow in the handler that currently
calls setYoutubeUrlError(undefined); setYoutubeUrl(''); await
startYouTubeLink(videoBlock.id, extractedId) to instead clear the url only after
startYouTubeLink resolves successfully—e.g., call setYoutubeUrlError(undefined)
then await startYouTubeLink(videoBlock.id, extractedId) inside a try block and
only call setYoutubeUrl('') in the success path, and catch errors to set an
appropriate error via setYoutubeUrlError without clearing the input so the
user’s URL is preserved.
| const YOUTUBE_ID_REGEX = /(\/|%3D|vi=|v=)([0-9A-Za-z-_]{11})([%#?&/]|$)/ | ||
|
|
||
| /** | ||
| * Extracts an 11-character YouTube video ID from a URL. | ||
| * | ||
| * Supports standard watch URLs, youtu.be short links, shorts, and embed URLs. | ||
| * Returns null when no valid ID can be found. | ||
| */ | ||
| export function extractYouTubeVideoId(url: string): string | null { | ||
| return url.match(YOUTUBE_ID_REGEX)?.[2] ?? null |
There was a problem hiding this comment.
Restrict extraction to YouTube hosts to avoid false positives.
Line 8 currently matches IDs from non-YouTube URLs (e.g., any domain containing /XXXXXXXXXXX or v=XXXXXXXXXXX). That means invalid links can pass validation and be submitted as YouTube videos.
🔧 Proposed fix
-const YOUTUBE_ID_REGEX = /(\/|%3D|vi=|v=)([0-9A-Za-z-_]{11})([%#?&/]|$)/
+const YOUTUBE_ID_REGEX = /^[0-9A-Za-z_-]{11}$/
+const YOUTUBE_HOST_REGEX = /(^|\.)youtube\.com$|(^|\.)youtu\.be$/
export function extractYouTubeVideoId(url: string): string | null {
- return url.match(YOUTUBE_ID_REGEX)?.[2] ?? null
+ try {
+ const parsed = new URL(url.trim())
+ const host = parsed.hostname.toLowerCase()
+ if (!YOUTUBE_HOST_REGEX.test(host)) return null
+
+ const candidate =
+ host.endsWith('youtu.be')
+ ? parsed.pathname.split('/').filter(Boolean)[0]
+ : parsed.searchParams.get('v') ??
+ parsed.pathname.split('/').filter(Boolean).at(-1)
+
+ return candidate != null && YOUTUBE_ID_REGEX.test(candidate)
+ ? candidate
+ : null
+ } catch {
+ return null
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/utils/videoSectionUtils/videoSectionUtils.ts`
around lines 8 - 17, The extractYouTubeVideoId function currently uses
YOUTUBE_ID_REGEX against any string and can return false positives from
non-YouTube hosts; update extractYouTubeVideoId to first parse the input with
the URL constructor (catching invalid URLs) and verify the hostname belongs to
an allowed YouTube host (e.g., endsWith "youtube.com", equals "youtu.be", or
includes "youtube-nocookie.com" and handle "shorts."), and only then apply
YOUTUBE_ID_REGEX (or extract from pathname/search) to get the 11-character ID;
ensure non-YouTube hosts or parse failures return null and keep YOUTUBE_ID_REGEX
as the extraction pattern reference.
|
The latest updates on your projects.
|
Summary
VideosSectionVIDEO_BLOCK_UPDATEwithsource: youTubeuseYouTubeVideoLinkinghook that setsstatus: 'updating'during the mutation, keepinghasActiveUploads: true— this blocks the custom upload button and the "Next" navigation button until the link is completeextractYouTubeVideoIdutility tovideoSectionUtilsand updates barrel exportsTest plan
https://www.youtube.com/watch?v=dQw4w9WgXcQ) and click Set — video player should update with the YouTube videoyoutu.beshort link and confirm it worksCloses NES-1369
Made with Cursor
Summary by CodeRabbit
New Features