Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3bee63e
add video embed page and route group restructure
daniellefrappier18 May 11, 2026
d7b8490
restore global 404 page and add embed video page improvements
daniellefrappier18 May 11, 2026
dcbb7c0
fix formatting
daniellefrappier18 May 11, 2026
8139606
fix lint error
daniellefrappier18 May 11, 2026
b750733
put back source.length guard
daniellefrappier18 May 12, 2026
65b527d
move video detail page into (site) route group and remove misplaced d…
daniellefrappier18 May 12, 2026
fb7a0f2
use custom 404 page for video embed not-found
daniellefrappier18 May 12, 2026
41ce41f
use VideoResourcePlayer in embed page and support YouTube sources
daniellefrappier18 May 12, 2026
c81c24d
add noindex metadata and fix youtube embed for video embed page
daniellefrappier18 May 12, 2026
f934181
add (embed)/error.tsx and supporting changes for embed error handling
daniellefrappier18 May 12, 2026
c3f214d
fix formatting
daniellefrappier18 May 12, 2026
666d1aa
remove stale feature flag tests from VideoEmbedPage
daniellefrappier18 May 12, 2026
7dc22f2
standardize metadata for error/not-found pages
daniellefrappier18 May 12, 2026
7038522
add test coverage for video embed page route and YouTube source type
daniellefrappier18 May 13, 2026
0080d5d
extract SUPPORTED_TYPES to module scope in embed page
daniellefrappier18 May 13, 2026
24dd61a
Merge branch 'main' into daniellef/11123-feat-video-embed-page
daniellefrappier18 May 13, 2026
1e57704
lint fix
daniellefrappier18 May 13, 2026
22c5161
Update frontends/main/src/app/(embed)/video/[id]/embed/page.tsx
daniellefrappier18 May 15, 2026
0d97b3c
Update frontends/main/src/app/(embed)/video/[id]/embed/page.tsx
daniellefrappier18 May 15, 2026
ee773df
Update frontends/main/src/app/(embed)/video/[id]/embed/page.tsx
daniellefrappier18 May 15, 2026
2f5a08f
rename hideHomeButton to showHomeButton on error page components
daniellefrappier18 May 15, 2026
eb726d1
source-type gate from video embed page; mock API in page tests
daniellefrappier18 May 15, 2026
8ed033d
move Appzi script to (site)/layout to exclude embed pages
daniellefrappier18 May 15, 2026
0a8a9a0
pass VideoResource to VideoEmbedPage instead of refetching by id
daniellefrappier18 May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ErrorPageTemplateProps = {
title: string
timSays?: string
loading?: boolean
showHomeButton?: boolean
}

const Page = styled.div(({ theme }) => ({
Expand Down Expand Up @@ -84,6 +85,7 @@ const Button = styled(ButtonLink)({
export const ErrorContent: React.FC<ErrorPageTemplateProps> = ({
title,
timSays,
showHomeButton = true,
}) => {
return (
<ErrorContainer>
Expand All @@ -99,11 +101,13 @@ export const ErrorContent: React.FC<ErrorPageTemplateProps> = ({
>
{title}
</Typography>
<Footer>
<Button variant="primary" href={HOME} Component="a">
Home
</Button>
</Footer>
{showHomeButton && (
<Footer>
<Button variant="primary" href={HOME} Component="a">
Home
</Button>
</Footer>
)}
</ErrorContainer>
)
}
Expand All @@ -112,6 +116,7 @@ const ErrorPageTemplate: React.FC<ErrorPageTemplateProps> = ({
title,
timSays,
loading = false,
showHomeButton = true,
}) => {
if (loading) {
return (
Expand All @@ -136,7 +141,11 @@ const ErrorPageTemplate: React.FC<ErrorPageTemplateProps> = ({
}
return (
<Page>
<ErrorContent title={title} timSays={timSays} />
<ErrorContent
title={title}
timSays={timSays}
showHomeButton={showHomeButton}
/>
</Page>
)
}
Expand Down
13 changes: 11 additions & 2 deletions frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
import React from "react"
import ErrorPageTemplate from "./ErrorPageTemplate"

const FallbackErrorPage = () => {
return <ErrorPageTemplate title="Something went wrong." />
type FallbackErrorPageProps = {
showHomeButton?: boolean
}

const FallbackErrorPage = ({ showHomeButton }: FallbackErrorPageProps) => {
return (
<ErrorPageTemplate
title="Something went wrong."
showHomeButton={showHomeButton}
/>
)
}

export default FallbackErrorPage
35 changes: 35 additions & 0 deletions frontends/main/src/app-pages/VideoEmbedPage/EmbedNotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client"

import React from "react"
import { ErrorContent } from "@/app-pages/ErrorPage/ErrorPageTemplate"
import { styled } from "ol-components"
import backgroundImage from "@/public/images/backgrounds/error_page_background.svg"

const Page = styled.div(({ theme }) => ({
backgroundImage: `url(${backgroundImage.src})`,
backgroundAttachment: "fixed",
backgroundRepeat: "no-repeat",
backgroundSize: "contain",
flexGrow: 1,
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
[theme.breakpoints.down("sm")]: {
backgroundImage: "none",
},
}))

const EmbedNotFoundPage: React.FC = () => {
return (
<Page>
<ErrorContent
title="Looks like we couldn't find what you were looking for!"
timSays="404"
showHomeButton={false}
/>
</Page>
)
}

export default EmbedNotFoundPage
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from "react"
import { factories } from "api/test-utils"
import { renderWithProviders, screen } from "@/test-utils"
import VideoEmbedPage from "./VideoEmbedPage"
import type { VideoResource } from "api/v1"
import { ResourceTypeEnum } from "api/v1"

jest.mock("next-nprogress-bar", () => ({
useRouter: () => ({}),
}))

jest.mock("@/app-pages/VideoPlaylistCollectionPage/VideoJsPlayer", () => ({
__esModule: true,
default: (props: { sources?: { src: string; type: string }[] }) => (
<div
data-testid="video-js-player"
data-sources={JSON.stringify(props.sources ?? [])}
/>
),
}))

const makeVideo = (overrides: Partial<VideoResource> = {}): VideoResource =>
factories.learningResources.video({
resource_type: ResourceTypeEnum.Video,
video: {
id: 1,
streaming_url: "https://example.com/video.m3u8",
duration: "PT10M",
caption_urls: [],
cover_image_url: null,
},
...overrides,
}) as VideoResource

describe("VideoEmbedPage", () => {
test("renders VideoJsPlayer for a video with a streaming URL", async () => {
const video = makeVideo()

renderWithProviders(<VideoEmbedPage videoResource={video} />)

const player = await screen.findByTestId("video-js-player")
expect(player).toBeInTheDocument()

const sources = JSON.parse(player.getAttribute("data-sources") ?? "[]")
expect(sources[0].type).toBe("application/x-mpegURL")
})

test("renders VideoJsPlayer with mp4 source type", async () => {
const video = makeVideo({
video: {
id: 2,
streaming_url: "https://example.com/video.mp4",
duration: "PT5M",
caption_urls: [],
cover_image_url: null,
},
})

renderWithProviders(<VideoEmbedPage videoResource={video} />)

const player = await screen.findByTestId("video-js-player")
const sources = JSON.parse(player.getAttribute("data-sources") ?? "[]")
expect(sources[0].type).toBe("video/mp4")
})

test("renders YouTube iframe for a video with content_files youtube_id", async () => {
const video = makeVideo({
video: {
id: 3,
streaming_url: null,
duration: "PT3M",
caption_urls: [],
cover_image_url: null,
},
content_files: [
factories.learningResources.contentFile({ youtube_id: "dQw4w9WgXcQ" }),
],
})

renderWithProviders(<VideoEmbedPage videoResource={video} />)

const iframe = await screen.findByTitle(/^Video: /)
expect(iframe).toHaveAttribute(
"src",
expect.stringContaining("dQw4w9WgXcQ"),
)
})
})
32 changes: 32 additions & 0 deletions frontends/main/src/app-pages/VideoEmbedPage/VideoEmbedPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client"

import React from "react"
import { styled } from "ol-components"
import VideoResourcePlayer from "@/app-pages/VideoPlaylistCollectionPage/VideoResourcePlayer"
import type { VideoResource } from "api/v1"

const EmbedPlayer = styled(VideoResourcePlayer)({
width: "100vw",
height: "100vh",
aspectRatio: "unset",
})

type VideoEmbedPageProps = {
videoResource: VideoResource
}

const VideoEmbedPage: React.FC<VideoEmbedPageProps> = ({ videoResource }) => {
const videoTitleLabel = videoResource.title?.trim() || "Untitled video"

return (
<EmbedPlayer
video={videoResource}
videoId={videoResource.id}
isLoading={false}
videoTitleLabel={videoTitleLabel}
videoThumbnailAlt={`Video thumbnail for ${videoTitleLabel}`}
/>
)
}

export default VideoEmbedPage
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import React, { useEffect, useRef, useState } from "react"
import Link from "next/link"
import Image from "next/image"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { Typography, styled, theme, Skeleton } from "ol-components"
import VideoContainer from "./VideoContainer"
import { RiShareForwardFill, RiPlayCircleFill } from "@remixicon/react"
Expand All @@ -16,9 +15,6 @@ import {
import type { VideoResource, VideoPlaylistResource } from "api/v1"
import { VideoResourceResourceTypeEnum } from "api/v1"
import { formatDurationClockTime } from "ol-utilities"
import { FeatureFlags } from "@/common/feature_flags"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
import { notFound } from "next/navigation"
import SharePopover from "@/components/SharePopover/SharePopover"
import { buildVideoStructuredData } from "./videoStructuredData"
import VideoResourcePlayer from "./VideoResourcePlayer"
Expand Down Expand Up @@ -345,11 +341,6 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
enabled: !!playlistId,
})

const showVideoPlaylistPage = useFeatureFlagEnabled(
FeatureFlags.VideoPlaylistPage,
)
const flagsLoaded = useFeatureFlagsLoaded()

const playlist = playlistData as VideoPlaylistResource | undefined
const video = resource as VideoResource | undefined

Expand Down Expand Up @@ -398,10 +389,6 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
// See: https://developers.google.com/search/docs/appearance/structured-data/video
const structuredData = !isLoading ? buildVideoStructuredData(video) : null

if (!showVideoPlaylistPage) {
return flagsLoaded ? notFound() : null
}

return (
<PageWrapper>
{structuredData && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import React from "react"
import { setMockResponse, urls, factories } from "api/test-utils"
import { renderWithProviders, screen } from "@/test-utils"
import { notFound } from "next/navigation"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
import VideoPage from "./VideoPlaylistCollectionPage"
import { ResourceTypeEnum } from "api/v1"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)
jest.mock("@/common/useFeatureFlagsLoaded")
const mockedUseFeatureFlagsLoaded = jest.mocked(useFeatureFlagsLoaded)

jest.mock("next-nprogress-bar", () => ({
useRouter: () => ({}),
}))
Expand Down Expand Up @@ -81,47 +73,6 @@ const setupApis = ({
}

describe("VideoPage", () => {
beforeEach(() => {
jest.clearAllMocks()
mockedUseFeatureFlagEnabled.mockReturnValue(true)
mockedUseFeatureFlagsLoaded.mockReturnValue(true)
})

describe("feature-flag gating", () => {
test("calls notFound when the VideoPlaylistPage flag is disabled and flags are loaded", () => {
mockedUseFeatureFlagEnabled.mockReturnValue(false)
mockedUseFeatureFlagsLoaded.mockReturnValue(true)
const playlist = makePlaylist()
setupApis({ playlistId: playlist.id, videos: [], playlist })

renderWithProviders(<VideoPage playlistId={playlist.id} />)

expect(notFound).toHaveBeenCalled()
})

test("does not call notFound when the flag is enabled", () => {
mockedUseFeatureFlagEnabled.mockReturnValue(true)
const playlist = makePlaylist()
setupApis({ playlistId: playlist.id, videos: [], playlist })

renderWithProviders(<VideoPage playlistId={playlist.id} />)

expect(notFound).not.toHaveBeenCalled()
})

test("does not call notFound when the flag is undefined and flags are not yet loaded", () => {
// posthog returns undefined before flags are evaluated
mockedUseFeatureFlagEnabled.mockReturnValue(undefined)
mockedUseFeatureFlagsLoaded.mockReturnValue(false)
const playlist = makePlaylist()
setupApis({ playlistId: playlist.id, videos: [], playlist })

renderWithProviders(<VideoPage playlistId={playlist.id} />)

expect(notFound).not.toHaveBeenCalled()
})
})

describe("playlist header", () => {
test("renders the playlist title once data is loaded", async () => {
const playlist = makePlaylist()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import React from "react"
import { styled, Skeleton, Typography } from "ol-components"
import { Button } from "@mitodl/smoot-design"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { notFound } from "next/navigation"
import { useQuery } from "@tanstack/react-query"
import {
Expand All @@ -21,8 +20,6 @@ import FeaturedVideo from "./FeaturedVideo"
import VideoCollection from "./VideoCollection"
import RelatedPlaylist from "./RelatedPlaylist"
import VideoContainer from "./VideoContainer"
import { FeatureFlags } from "@/common/feature_flags"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"

const Page = styled.div(({ theme }) => ({
backgroundColor: theme.custom.colors.lightGray1,
Expand Down Expand Up @@ -69,11 +66,6 @@ const VideoPlaylistCollectionPage: React.FC<
const getVideoHref = (resource: VideoResource) =>
`/video/${resource.id}?playlist=${playlistId}`

const showVideoPlaylistPage = useFeatureFlagEnabled(
FeatureFlags.VideoPlaylistPage,
)
const flagsLoaded = useFeatureFlagsLoaded()

const {
data: playlist,
isLoading: playlistLoading,
Expand All @@ -99,10 +91,6 @@ const VideoPlaylistCollectionPage: React.FC<
}),
})

if (!showVideoPlaylistPage) {
return flagsLoaded ? notFound() : null
}

if (isError) {
return notFound()
}
Expand Down
Loading
Loading