Skip to content

Commit ac20b8f

Browse files
feat: add video embed page and route group restructure (#3327)
1 parent a0be7a4 commit ac20b8f

70 files changed

Lines changed: 519 additions & 252 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type ErrorPageTemplateProps = {
1111
title: string
1212
timSays?: string
1313
loading?: boolean
14+
showHomeButton?: boolean
1415
}
1516

1617
const Page = styled.div(({ theme }) => ({
@@ -84,6 +85,7 @@ const Button = styled(ButtonLink)({
8485
export const ErrorContent: React.FC<ErrorPageTemplateProps> = ({
8586
title,
8687
timSays,
88+
showHomeButton = true,
8789
}) => {
8890
return (
8991
<ErrorContainer>
@@ -99,11 +101,13 @@ export const ErrorContent: React.FC<ErrorPageTemplateProps> = ({
99101
>
100102
{title}
101103
</Typography>
102-
<Footer>
103-
<Button variant="primary" href={HOME} Component="a">
104-
Home
105-
</Button>
106-
</Footer>
104+
{showHomeButton && (
105+
<Footer>
106+
<Button variant="primary" href={HOME} Component="a">
107+
Home
108+
</Button>
109+
</Footer>
110+
)}
107111
</ErrorContainer>
108112
)
109113
}
@@ -112,6 +116,7 @@ const ErrorPageTemplate: React.FC<ErrorPageTemplateProps> = ({
112116
title,
113117
timSays,
114118
loading = false,
119+
showHomeButton = true,
115120
}) => {
116121
if (loading) {
117122
return (
@@ -136,7 +141,11 @@ const ErrorPageTemplate: React.FC<ErrorPageTemplateProps> = ({
136141
}
137142
return (
138143
<Page>
139-
<ErrorContent title={title} timSays={timSays} />
144+
<ErrorContent
145+
title={title}
146+
timSays={timSays}
147+
showHomeButton={showHomeButton}
148+
/>
140149
</Page>
141150
)
142151
}

frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@
33
import React from "react"
44
import ErrorPageTemplate from "./ErrorPageTemplate"
55

6-
const FallbackErrorPage = () => {
7-
return <ErrorPageTemplate title="Something went wrong." />
6+
type FallbackErrorPageProps = {
7+
showHomeButton?: boolean
8+
}
9+
10+
const FallbackErrorPage = ({ showHomeButton }: FallbackErrorPageProps) => {
11+
return (
12+
<ErrorPageTemplate
13+
title="Something went wrong."
14+
showHomeButton={showHomeButton}
15+
/>
16+
)
817
}
918

1019
export default FallbackErrorPage
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client"
2+
3+
import React from "react"
4+
import { ErrorContent } from "@/app-pages/ErrorPage/ErrorPageTemplate"
5+
import { styled } from "ol-components"
6+
import backgroundImage from "@/public/images/backgrounds/error_page_background.svg"
7+
8+
const Page = styled.div(({ theme }) => ({
9+
backgroundImage: `url(${backgroundImage.src})`,
10+
backgroundAttachment: "fixed",
11+
backgroundRepeat: "no-repeat",
12+
backgroundSize: "contain",
13+
flexGrow: 1,
14+
height: "100vh",
15+
display: "flex",
16+
alignItems: "center",
17+
justifyContent: "center",
18+
[theme.breakpoints.down("sm")]: {
19+
backgroundImage: "none",
20+
},
21+
}))
22+
23+
const EmbedNotFoundPage: React.FC = () => {
24+
return (
25+
<Page>
26+
<ErrorContent
27+
title="Looks like we couldn't find what you were looking for!"
28+
timSays="404"
29+
showHomeButton={false}
30+
/>
31+
</Page>
32+
)
33+
}
34+
35+
export default EmbedNotFoundPage
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from "react"
2+
import { factories } from "api/test-utils"
3+
import { renderWithProviders, screen } from "@/test-utils"
4+
import VideoEmbedPage from "./VideoEmbedPage"
5+
import type { VideoResource } from "api/v1"
6+
import { ResourceTypeEnum } from "api/v1"
7+
8+
jest.mock("next-nprogress-bar", () => ({
9+
useRouter: () => ({}),
10+
}))
11+
12+
jest.mock("@/app-pages/VideoPlaylistCollectionPage/VideoJsPlayer", () => ({
13+
__esModule: true,
14+
default: (props: { sources?: { src: string; type: string }[] }) => (
15+
<div
16+
data-testid="video-js-player"
17+
data-sources={JSON.stringify(props.sources ?? [])}
18+
/>
19+
),
20+
}))
21+
22+
const makeVideo = (overrides: Partial<VideoResource> = {}): VideoResource =>
23+
factories.learningResources.video({
24+
resource_type: ResourceTypeEnum.Video,
25+
video: {
26+
id: 1,
27+
streaming_url: "https://example.com/video.m3u8",
28+
duration: "PT10M",
29+
caption_urls: [],
30+
cover_image_url: null,
31+
},
32+
...overrides,
33+
}) as VideoResource
34+
35+
describe("VideoEmbedPage", () => {
36+
test("renders VideoJsPlayer for a video with a streaming URL", async () => {
37+
const video = makeVideo()
38+
39+
renderWithProviders(<VideoEmbedPage videoResource={video} />)
40+
41+
const player = await screen.findByTestId("video-js-player")
42+
expect(player).toBeInTheDocument()
43+
44+
const sources = JSON.parse(player.getAttribute("data-sources") ?? "[]")
45+
expect(sources[0].type).toBe("application/x-mpegURL")
46+
})
47+
48+
test("renders VideoJsPlayer with mp4 source type", async () => {
49+
const video = makeVideo({
50+
video: {
51+
id: 2,
52+
streaming_url: "https://example.com/video.mp4",
53+
duration: "PT5M",
54+
caption_urls: [],
55+
cover_image_url: null,
56+
},
57+
})
58+
59+
renderWithProviders(<VideoEmbedPage videoResource={video} />)
60+
61+
const player = await screen.findByTestId("video-js-player")
62+
const sources = JSON.parse(player.getAttribute("data-sources") ?? "[]")
63+
expect(sources[0].type).toBe("video/mp4")
64+
})
65+
66+
test("renders YouTube iframe for a video with content_files youtube_id", async () => {
67+
const video = makeVideo({
68+
video: {
69+
id: 3,
70+
streaming_url: null,
71+
duration: "PT3M",
72+
caption_urls: [],
73+
cover_image_url: null,
74+
},
75+
content_files: [
76+
factories.learningResources.contentFile({ youtube_id: "dQw4w9WgXcQ" }),
77+
],
78+
})
79+
80+
renderWithProviders(<VideoEmbedPage videoResource={video} />)
81+
82+
const iframe = await screen.findByTitle(/^Video: /)
83+
expect(iframe).toHaveAttribute(
84+
"src",
85+
expect.stringContaining("dQw4w9WgXcQ"),
86+
)
87+
})
88+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client"
2+
3+
import React from "react"
4+
import { styled } from "ol-components"
5+
import VideoResourcePlayer from "@/app-pages/VideoPlaylistCollectionPage/VideoResourcePlayer"
6+
import type { VideoResource } from "api/v1"
7+
8+
const EmbedPlayer = styled(VideoResourcePlayer)({
9+
width: "100vw",
10+
height: "100vh",
11+
aspectRatio: "unset",
12+
})
13+
14+
type VideoEmbedPageProps = {
15+
videoResource: VideoResource
16+
}
17+
18+
const VideoEmbedPage: React.FC<VideoEmbedPageProps> = ({ videoResource }) => {
19+
const videoTitleLabel = videoResource.title.trim()
20+
21+
return (
22+
<EmbedPlayer
23+
video={videoResource}
24+
videoId={videoResource.id}
25+
isLoading={false}
26+
videoTitleLabel={videoTitleLabel}
27+
videoThumbnailAlt={`Video thumbnail for ${videoTitleLabel}`}
28+
/>
29+
)
30+
}
31+
32+
export default VideoEmbedPage

frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import React, { useEffect, useRef, useState } from "react"
44
import Link from "next/link"
55
import Image from "next/image"
6-
import { useFeatureFlagEnabled } from "posthog-js/react"
76
import { Typography, styled, theme, Skeleton } from "ol-components"
87
import VideoContainer from "./VideoContainer"
98
import { RiShareForwardFill, RiPlayCircleFill } from "@remixicon/react"
@@ -16,9 +15,6 @@ import {
1615
import type { VideoResource, VideoPlaylistResource } from "api/v1"
1716
import { VideoResourceResourceTypeEnum } from "api/v1"
1817
import { formatDurationClockTime } from "ol-utilities"
19-
import { FeatureFlags } from "@/common/feature_flags"
20-
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
21-
import { notFound } from "next/navigation"
2218
import SharePopover from "@/components/SharePopover/SharePopover"
2319
import { buildVideoStructuredData } from "./videoStructuredData"
2420
import VideoResourcePlayer from "./VideoResourcePlayer"
@@ -345,11 +341,6 @@ const VideoDetailPage: React.FC<VideoDetailPageProps> = ({
345341
enabled: !!playlistId,
346342
})
347343

348-
const showVideoPlaylistPage = useFeatureFlagEnabled(
349-
FeatureFlags.VideoPlaylistPage,
350-
)
351-
const flagsLoaded = useFeatureFlagsLoaded()
352-
353344
const playlist = playlistData as VideoPlaylistResource | undefined
354345
const video = resource as VideoResource | undefined
355346

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

401-
if (!showVideoPlaylistPage) {
402-
return flagsLoaded ? notFound() : null
403-
}
404-
405392
return (
406393
<PageWrapper>
407394
{structuredData && (

frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.test.tsx

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
import React from "react"
22
import { setMockResponse, urls, factories } from "api/test-utils"
33
import { renderWithProviders, screen } from "@/test-utils"
4-
import { notFound } from "next/navigation"
5-
import { useFeatureFlagEnabled } from "posthog-js/react"
6-
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
74
import VideoPage from "./VideoPlaylistCollectionPage"
85
import { ResourceTypeEnum } from "api/v1"
96

10-
jest.mock("posthog-js/react")
11-
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)
12-
jest.mock("@/common/useFeatureFlagsLoaded")
13-
const mockedUseFeatureFlagsLoaded = jest.mocked(useFeatureFlagsLoaded)
14-
157
jest.mock("next-nprogress-bar", () => ({
168
useRouter: () => ({}),
179
}))
@@ -81,47 +73,6 @@ const setupApis = ({
8173
}
8274

8375
describe("VideoPage", () => {
84-
beforeEach(() => {
85-
jest.clearAllMocks()
86-
mockedUseFeatureFlagEnabled.mockReturnValue(true)
87-
mockedUseFeatureFlagsLoaded.mockReturnValue(true)
88-
})
89-
90-
describe("feature-flag gating", () => {
91-
test("calls notFound when the VideoPlaylistPage flag is disabled and flags are loaded", () => {
92-
mockedUseFeatureFlagEnabled.mockReturnValue(false)
93-
mockedUseFeatureFlagsLoaded.mockReturnValue(true)
94-
const playlist = makePlaylist()
95-
setupApis({ playlistId: playlist.id, videos: [], playlist })
96-
97-
renderWithProviders(<VideoPage playlistId={playlist.id} />)
98-
99-
expect(notFound).toHaveBeenCalled()
100-
})
101-
102-
test("does not call notFound when the flag is enabled", () => {
103-
mockedUseFeatureFlagEnabled.mockReturnValue(true)
104-
const playlist = makePlaylist()
105-
setupApis({ playlistId: playlist.id, videos: [], playlist })
106-
107-
renderWithProviders(<VideoPage playlistId={playlist.id} />)
108-
109-
expect(notFound).not.toHaveBeenCalled()
110-
})
111-
112-
test("does not call notFound when the flag is undefined and flags are not yet loaded", () => {
113-
// posthog returns undefined before flags are evaluated
114-
mockedUseFeatureFlagEnabled.mockReturnValue(undefined)
115-
mockedUseFeatureFlagsLoaded.mockReturnValue(false)
116-
const playlist = makePlaylist()
117-
setupApis({ playlistId: playlist.id, videos: [], playlist })
118-
119-
renderWithProviders(<VideoPage playlistId={playlist.id} />)
120-
121-
expect(notFound).not.toHaveBeenCalled()
122-
})
123-
})
124-
12576
describe("playlist header", () => {
12677
test("renders the playlist title once data is loaded", async () => {
12778
const playlist = makePlaylist()

frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import React from "react"
44
import { styled, Skeleton, Typography } from "ol-components"
55
import { Button } from "@mitodl/smoot-design"
6-
import { useFeatureFlagEnabled } from "posthog-js/react"
76
import { notFound } from "next/navigation"
87
import { useQuery } from "@tanstack/react-query"
98
import {
@@ -21,8 +20,6 @@ import FeaturedVideo from "./FeaturedVideo"
2120
import VideoCollection from "./VideoCollection"
2221
import RelatedPlaylist from "./RelatedPlaylist"
2322
import VideoContainer from "./VideoContainer"
24-
import { FeatureFlags } from "@/common/feature_flags"
25-
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
2623

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

72-
const showVideoPlaylistPage = useFeatureFlagEnabled(
73-
FeatureFlags.VideoPlaylistPage,
74-
)
75-
const flagsLoaded = useFeatureFlagsLoaded()
76-
7769
const {
7870
data: playlist,
7971
isLoading: playlistLoading,
@@ -99,10 +91,6 @@ const VideoPlaylistCollectionPage: React.FC<
9991
}),
10092
})
10193

102-
if (!showVideoPlaylistPage) {
103-
return flagsLoaded ? notFound() : null
104-
}
105-
10694
if (isError) {
10795
return notFound()
10896
}

0 commit comments

Comments
 (0)