diff --git a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx index 000e0212ff..aa75fe4935 100644 --- a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx +++ b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx @@ -11,6 +11,7 @@ type ErrorPageTemplateProps = { title: string timSays?: string loading?: boolean + showHomeButton?: boolean } const Page = styled.div(({ theme }) => ({ @@ -84,6 +85,7 @@ const Button = styled(ButtonLink)({ export const ErrorContent: React.FC = ({ title, timSays, + showHomeButton = true, }) => { return ( @@ -99,11 +101,13 @@ export const ErrorContent: React.FC = ({ > {title} -
- -
+ {showHomeButton && ( +
+ +
+ )}
) } @@ -112,6 +116,7 @@ const ErrorPageTemplate: React.FC = ({ title, timSays, loading = false, + showHomeButton = true, }) => { if (loading) { return ( @@ -136,7 +141,11 @@ const ErrorPageTemplate: React.FC = ({ } return ( - + ) } diff --git a/frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx b/frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx index bfbf85dba1..dbb18689de 100644 --- a/frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx +++ b/frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx @@ -3,8 +3,17 @@ import React from "react" import ErrorPageTemplate from "./ErrorPageTemplate" -const FallbackErrorPage = () => { - return +type FallbackErrorPageProps = { + showHomeButton?: boolean +} + +const FallbackErrorPage = ({ showHomeButton }: FallbackErrorPageProps) => { + return ( + + ) } export default FallbackErrorPage diff --git a/frontends/main/src/app-pages/VideoEmbedPage/EmbedNotFoundPage.tsx b/frontends/main/src/app-pages/VideoEmbedPage/EmbedNotFoundPage.tsx new file mode 100644 index 0000000000..d9b04ad6de --- /dev/null +++ b/frontends/main/src/app-pages/VideoEmbedPage/EmbedNotFoundPage.tsx @@ -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 ( + + + + ) +} + +export default EmbedNotFoundPage diff --git a/frontends/main/src/app-pages/VideoEmbedPage/VideoEmbedPage.test.tsx b/frontends/main/src/app-pages/VideoEmbedPage/VideoEmbedPage.test.tsx new file mode 100644 index 0000000000..3bb87fd94d --- /dev/null +++ b/frontends/main/src/app-pages/VideoEmbedPage/VideoEmbedPage.test.tsx @@ -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 }[] }) => ( +
+ ), +})) + +const makeVideo = (overrides: Partial = {}): 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() + + 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() + + 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() + + const iframe = await screen.findByTitle(/^Video: /) + expect(iframe).toHaveAttribute( + "src", + expect.stringContaining("dQw4w9WgXcQ"), + ) + }) +}) diff --git a/frontends/main/src/app-pages/VideoEmbedPage/VideoEmbedPage.tsx b/frontends/main/src/app-pages/VideoEmbedPage/VideoEmbedPage.tsx new file mode 100644 index 0000000000..8452065fa6 --- /dev/null +++ b/frontends/main/src/app-pages/VideoEmbedPage/VideoEmbedPage.tsx @@ -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 = ({ videoResource }) => { + const videoTitleLabel = videoResource.title?.trim() || "Untitled video" + + return ( + + ) +} + +export default VideoEmbedPage diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx index b03158203a..bd2f3b9084 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx @@ -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" @@ -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" @@ -345,11 +341,6 @@ const VideoDetailPage: React.FC = ({ enabled: !!playlistId, }) - const showVideoPlaylistPage = useFeatureFlagEnabled( - FeatureFlags.VideoPlaylistPage, - ) - const flagsLoaded = useFeatureFlagsLoaded() - const playlist = playlistData as VideoPlaylistResource | undefined const video = resource as VideoResource | undefined @@ -398,10 +389,6 @@ const VideoDetailPage: React.FC = ({ // See: https://developers.google.com/search/docs/appearance/structured-data/video const structuredData = !isLoading ? buildVideoStructuredData(video) : null - if (!showVideoPlaylistPage) { - return flagsLoaded ? notFound() : null - } - return ( {structuredData && ( diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.test.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.test.tsx index 0c828f232a..e4e28bde44 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.test.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.test.tsx @@ -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: () => ({}), })) @@ -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() - - 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() - - 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() - - expect(notFound).not.toHaveBeenCalled() - }) - }) - describe("playlist header", () => { test("renders the playlist title once data is loaded", async () => { const playlist = makePlaylist() diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.tsx index b0f0475255..840f789509 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoPlaylistCollectionPage.tsx @@ -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 { @@ -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, @@ -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, @@ -99,10 +91,6 @@ const VideoPlaylistCollectionPage: React.FC< }), }) - if (!showVideoPlaylistPage) { - return flagsLoaded ? notFound() : null - } - if (isError) { return notFound() } diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.test.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.test.tsx index e6ed55c0a1..1a465d87b5 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.test.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.test.tsx @@ -1,18 +1,10 @@ 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 VideoSeriesDetailPage from "./VideoSeriesDetailPage" import { ResourceTypeEnum } from "api/v1" import type { VideoResource, VideoPlaylistResource } 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: () => ({}), })) @@ -101,37 +93,6 @@ const renderPage = ({ // ── Tests ──────────────────────────────────────────────────────────────────── describe("VideoSeriesDetailPage", () => { - beforeEach(() => { - jest.clearAllMocks() - mockedUseFeatureFlagEnabled.mockReturnValue(true) - mockedUseFeatureFlagsLoaded.mockReturnValue(true) - }) - - describe("feature-flag gating", () => { - test("calls notFound when VideoPlaylistPage flag is disabled and flags are loaded", () => { - mockedUseFeatureFlagEnabled.mockReturnValue(false) - mockedUseFeatureFlagsLoaded.mockReturnValue(true) - - renderPage() - - expect(notFound).toHaveBeenCalled() - }) - - test("does not call notFound when the flag is enabled", () => { - renderPage() - expect(notFound).not.toHaveBeenCalled() - }) - - test("does not call notFound when the flag is undefined and flags are not yet loaded", () => { - mockedUseFeatureFlagEnabled.mockReturnValue(undefined) - mockedUseFeatureFlagsLoaded.mockReturnValue(false) - - renderPage() - - expect(notFound).not.toHaveBeenCalled() - }) - }) - describe("video title and institution label", () => { test("renders the video title once data is loaded", async () => { const video = makeVideo({ title: "Introduction to Machine Learning" }) diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.tsx index ebd670f006..ee3eecee99 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoSeriesDetailPage.tsx @@ -1,15 +1,11 @@ "use client" import React, { useEffect, useRef } from "react" -import { useFeatureFlagEnabled } from "posthog-js/react" import { Skeleton, styled } from "ol-components" import VideoContainer from "./VideoContainer" import { useLearningResourcesDetail } from "api/hooks/learningResources" import type { VideoResource, VideoPlaylistResource } 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 { useSeriesNavigation } from "./useSeriesNavigation" import SeriesNavBar from "./SeriesNavBar" import UpNextSection from "./UpNextSection" @@ -39,11 +35,6 @@ const VideoSeriesDetailPage: React.FC = ({ const { data: resource, isLoading: videoLoading } = useLearningResourcesDetail(videoId) - const showVideoPlaylistPage = useFeatureFlagEnabled( - FeatureFlags.VideoPlaylistPage, - ) - const flagsLoaded = useFeatureFlagsLoaded() - const playlist = playlistData as VideoPlaylistResource | undefined const video = resource as VideoResource | undefined const { @@ -84,10 +75,6 @@ const VideoSeriesDetailPage: React.FC = ({ // See: https://developers.google.com/search/docs/appearance/structured-data/video const structuredData = !isLoading ? buildVideoStructuredData(video) : null - if (!showVideoPlaylistPage) { - return flagsLoaded ? notFound() : null - } - return ( {structuredData && ( diff --git a/frontends/main/src/app/(embed)/error.tsx b/frontends/main/src/app/(embed)/error.tsx new file mode 100644 index 0000000000..121ff75b4f --- /dev/null +++ b/frontends/main/src/app/(embed)/error.tsx @@ -0,0 +1,16 @@ +"use client" + +import React, { useEffect } from "react" +import * as Sentry from "@sentry/nextjs" +import FallbackErrorPage from "@/app-pages/ErrorPage/FallbackErrorPage" + +const Error = ({ error }: { error: Error }) => { + useEffect(() => { + console.error("Error encountered in React error boundary:", error) + Sentry.captureException(error) + }, [error]) + + return +} + +export default Error diff --git a/frontends/main/src/app/(embed)/layout.tsx b/frontends/main/src/app/(embed)/layout.tsx new file mode 100644 index 0000000000..e5882afa90 --- /dev/null +++ b/frontends/main/src/app/(embed)/layout.tsx @@ -0,0 +1,14 @@ +import React from "react" +import { MITLearnGlobalStyles } from "ol-components" +export default function EmbedLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + <> + + {children} + + ) +} diff --git a/frontends/main/src/app/(embed)/not-found.tsx b/frontends/main/src/app/(embed)/not-found.tsx new file mode 100644 index 0000000000..41044fc5fc --- /dev/null +++ b/frontends/main/src/app/(embed)/not-found.tsx @@ -0,0 +1,15 @@ +import React from "react" +import EmbedNotFoundPage from "@/app-pages/VideoEmbedPage/EmbedNotFoundPage" +import { standardizeMetadata } from "@/common/metadata" +import type { Metadata } from "next" + +export const metadata: Metadata = standardizeMetadata({ + title: "Not Found", + social: false, +}) + +const NotFound: React.FC = () => { + return +} + +export default NotFound diff --git a/frontends/main/src/app/(embed)/video/[id]/embed/page.test.tsx b/frontends/main/src/app/(embed)/video/[id]/embed/page.test.tsx new file mode 100644 index 0000000000..042d50cdcc --- /dev/null +++ b/frontends/main/src/app/(embed)/video/[id]/embed/page.test.tsx @@ -0,0 +1,73 @@ +import { notFound } from "next/navigation" +import { factories, setMockResponse, urls } from "api/test-utils" +import type { VideoResource } from "api/v1" +import Page from "./page" + +jest.mock("@/app/getQueryClient", () => { + const { makeBrowserQueryClient } = jest.requireActual("@/app/getQueryClient") + return { + getQueryClient: () => makeBrowserQueryClient({ maxRetries: 0 }), + } +}) + +jest.mock("@tanstack/react-query", () => ({ + ...jest.requireActual("@tanstack/react-query"), + dehydrate: jest.fn().mockReturnValue({}), +})) + +jest.mock("@/app-pages/VideoEmbedPage/VideoEmbedPage", () => ({ + __esModule: true, + default: () => null, +})) + +const mockNotFound = jest.mocked(notFound) + +describe("video embed page.tsx server component", () => { + beforeEach(() => { + mockNotFound.mockImplementation(() => { + throw new Error("NEXT_NOT_FOUND") + }) + }) + + test("calls notFound for a non-integer id", async () => { + await expect( + Page({ params: Promise.resolve({ id: "abc" }) }), + ).rejects.toThrow("NEXT_NOT_FOUND") + expect(notFound).toHaveBeenCalled() + }) + + test("calls notFound for id of zero", async () => { + await expect( + Page({ params: Promise.resolve({ id: "0" }) }), + ).rejects.toThrow("NEXT_NOT_FOUND") + expect(notFound).toHaveBeenCalled() + }) + + test("calls notFound for a negative id", async () => { + await expect( + Page({ params: Promise.resolve({ id: "-1" }) }), + ).rejects.toThrow("NEXT_NOT_FOUND") + expect(notFound).toHaveBeenCalled() + }) + + test("calls notFound when the resource is not a video", async () => { + const course = factories.learningResources.course() + setMockResponse.get( + urls.learningResources.details({ id: course.id }), + course, + ) + + await expect( + Page({ params: Promise.resolve({ id: String(course.id) }) }), + ).rejects.toThrow("NEXT_NOT_FOUND") + expect(notFound).toHaveBeenCalled() + }) + + test("does not call notFound for a video resource", async () => { + const video = factories.learningResources.video() as VideoResource + setMockResponse.get(urls.learningResources.details({ id: video.id }), video) + + await Page({ params: Promise.resolve({ id: String(video.id) }) }) + expect(notFound).not.toHaveBeenCalled() + }) +}) diff --git a/frontends/main/src/app/(embed)/video/[id]/embed/page.tsx b/frontends/main/src/app/(embed)/video/[id]/embed/page.tsx new file mode 100644 index 0000000000..84d806ce7b --- /dev/null +++ b/frontends/main/src/app/(embed)/video/[id]/embed/page.tsx @@ -0,0 +1,66 @@ +import React from "react" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" +import { learningResourceQueries } from "api/hooks/learningResources" +import { getQueryClient } from "@/app/getQueryClient" +import { notFound } from "next/navigation" +import VideoEmbedPage from "@/app-pages/VideoEmbedPage/VideoEmbedPage" +import { VideoResourceResourceTypeEnum, type VideoResource } from "api/v1" +import { + safeGenerateMetadata, + standardizeMetadata, + MetadataNotFound, +} from "@/common/metadata" + +export const generateMetadata = ({ + params, +}: { + params: Promise<{ id: string }> +}) => + safeGenerateMetadata(async () => { + const { id } = await params + const videoId = Number(id) + if (!Number.isInteger(videoId) || videoId <= 0) { + throw new MetadataNotFound() + } + const queryClient = getQueryClient() + const resource = await queryClient + .fetchQuery(learningResourceQueries.detail(videoId)) + .catch(() => { + throw new MetadataNotFound() + }) + + if (resource?.resource_type !== VideoResourceResourceTypeEnum.Video) { + throw new MetadataNotFound() + } + + return standardizeMetadata({ + title: resource.title, + robots: "noindex, nofollow", + social: false, + }) + }) + +const Page = async ({ params }: { params: Promise<{ id: string }> }) => { + const { id } = await params + const videoId = Number(id) + if (!Number.isInteger(videoId) || videoId <= 0) { + notFound() + } + + const queryClient = getQueryClient() + const resource = await queryClient.fetchQueryOr404( + learningResourceQueries.detail(videoId), + ) + + if (resource.resource_type !== VideoResourceResourceTypeEnum.Video) { + notFound() + } + + return ( + + + + ) +} + +export default Page diff --git a/frontends/main/src/app/(products)/courses/[readable_id]/page.tsx b/frontends/main/src/app/(site)/(products)/courses/[readable_id]/page.tsx similarity index 100% rename from frontends/main/src/app/(products)/courses/[readable_id]/page.tsx rename to frontends/main/src/app/(site)/(products)/courses/[readable_id]/page.tsx diff --git a/frontends/main/src/app/(products)/courses/p/[readable_id]/page.tsx b/frontends/main/src/app/(site)/(products)/courses/p/[readable_id]/page.tsx similarity index 100% rename from frontends/main/src/app/(products)/courses/p/[readable_id]/page.tsx rename to frontends/main/src/app/(site)/(products)/courses/p/[readable_id]/page.tsx diff --git a/frontends/main/src/app/(products)/programs/[readable_id]/page.tsx b/frontends/main/src/app/(site)/(products)/programs/[readable_id]/page.tsx similarity index 100% rename from frontends/main/src/app/(products)/programs/[readable_id]/page.tsx rename to frontends/main/src/app/(site)/(products)/programs/[readable_id]/page.tsx diff --git a/frontends/main/src/app/(site)/[...unmatched]/page.tsx b/frontends/main/src/app/(site)/[...unmatched]/page.tsx new file mode 100644 index 0000000000..c3cf7b09f6 --- /dev/null +++ b/frontends/main/src/app/(site)/[...unmatched]/page.tsx @@ -0,0 +1,7 @@ +import { notFound } from "next/navigation" + +export const generateMetadata = () => notFound() + +const UnmatchedPage = () => notFound() + +export default UnmatchedPage diff --git a/frontends/main/src/app/about/page.tsx b/frontends/main/src/app/(site)/about/page.tsx similarity index 100% rename from frontends/main/src/app/about/page.tsx rename to frontends/main/src/app/(site)/about/page.tsx diff --git a/frontends/main/src/app/c/[channelType]/[name]/page.tsx b/frontends/main/src/app/(site)/c/[channelType]/[name]/page.tsx similarity index 100% rename from frontends/main/src/app/c/[channelType]/[name]/page.tsx rename to frontends/main/src/app/(site)/c/[channelType]/[name]/page.tsx diff --git a/frontends/main/src/app/cart/page.tsx b/frontends/main/src/app/(site)/cart/page.tsx similarity index 100% rename from frontends/main/src/app/cart/page.tsx rename to frontends/main/src/app/(site)/cart/page.tsx diff --git a/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/page.tsx similarity index 100% rename from frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx rename to frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/page.tsx diff --git a/frontends/main/src/app/certificate/[certificateType]/[uuid]/pdf/route.test.tsx b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.test.tsx similarity index 100% rename from frontends/main/src/app/certificate/[certificateType]/[uuid]/pdf/route.test.tsx rename to frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.test.tsx diff --git a/frontends/main/src/app/certificate/[certificateType]/[uuid]/pdf/route.tsx b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.tsx similarity index 100% rename from frontends/main/src/app/certificate/[certificateType]/[uuid]/pdf/route.tsx rename to frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.tsx diff --git a/frontends/main/src/app/certificate/[certificateType]/[uuid]/pdf/utils.ts b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/utils.ts similarity index 100% rename from frontends/main/src/app/certificate/[certificateType]/[uuid]/pdf/utils.ts rename to frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/utils.ts diff --git a/frontends/main/src/app/dashboard/layout.tsx b/frontends/main/src/app/(site)/dashboard/layout.tsx similarity index 100% rename from frontends/main/src/app/dashboard/layout.tsx rename to frontends/main/src/app/(site)/dashboard/layout.tsx diff --git a/frontends/main/src/app/dashboard/my-lists/[id]/page.tsx b/frontends/main/src/app/(site)/dashboard/my-lists/[id]/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/my-lists/[id]/page.tsx rename to frontends/main/src/app/(site)/dashboard/my-lists/[id]/page.tsx diff --git a/frontends/main/src/app/dashboard/my-lists/page.tsx b/frontends/main/src/app/(site)/dashboard/my-lists/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/my-lists/page.tsx rename to frontends/main/src/app/(site)/dashboard/my-lists/page.tsx diff --git a/frontends/main/src/app/dashboard/organization/[orgSlug]/contract/[contractSlug]/page.tsx b/frontends/main/src/app/(site)/dashboard/organization/[orgSlug]/contract/[contractSlug]/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/organization/[orgSlug]/contract/[contractSlug]/page.tsx rename to frontends/main/src/app/(site)/dashboard/organization/[orgSlug]/contract/[contractSlug]/page.tsx diff --git a/frontends/main/src/app/dashboard/organization/[orgSlug]/page.test.tsx b/frontends/main/src/app/(site)/dashboard/organization/[orgSlug]/page.test.tsx similarity index 100% rename from frontends/main/src/app/dashboard/organization/[orgSlug]/page.test.tsx rename to frontends/main/src/app/(site)/dashboard/organization/[orgSlug]/page.test.tsx diff --git a/frontends/main/src/app/dashboard/organization/[orgSlug]/page.tsx b/frontends/main/src/app/(site)/dashboard/organization/[orgSlug]/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/organization/[orgSlug]/page.tsx rename to frontends/main/src/app/(site)/dashboard/organization/[orgSlug]/page.tsx diff --git a/frontends/main/src/app/dashboard/organization/page.tsx b/frontends/main/src/app/(site)/dashboard/organization/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/organization/page.tsx rename to frontends/main/src/app/(site)/dashboard/organization/page.tsx diff --git a/frontends/main/src/app/dashboard/page.tsx b/frontends/main/src/app/(site)/dashboard/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/page.tsx rename to frontends/main/src/app/(site)/dashboard/page.tsx diff --git a/frontends/main/src/app/dashboard/profile/page.tsx b/frontends/main/src/app/(site)/dashboard/profile/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/profile/page.tsx rename to frontends/main/src/app/(site)/dashboard/profile/page.tsx diff --git a/frontends/main/src/app/dashboard/program/[id]/page.tsx b/frontends/main/src/app/(site)/dashboard/program/[id]/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/program/[id]/page.tsx rename to frontends/main/src/app/(site)/dashboard/program/[id]/page.tsx diff --git a/frontends/main/src/app/dashboard/settings/page.tsx b/frontends/main/src/app/(site)/dashboard/settings/page.tsx similarity index 100% rename from frontends/main/src/app/dashboard/settings/page.tsx rename to frontends/main/src/app/(site)/dashboard/settings/page.tsx diff --git a/frontends/main/src/app/departments/page.tsx b/frontends/main/src/app/(site)/departments/page.tsx similarity index 100% rename from frontends/main/src/app/departments/page.tsx rename to frontends/main/src/app/(site)/departments/page.tsx diff --git a/frontends/main/src/app/enrollmentcode/[code]/page.tsx b/frontends/main/src/app/(site)/enrollmentcode/[code]/page.tsx similarity index 100% rename from frontends/main/src/app/enrollmentcode/[code]/page.tsx rename to frontends/main/src/app/(site)/enrollmentcode/[code]/page.tsx diff --git a/frontends/main/src/app/error.test.tsx b/frontends/main/src/app/(site)/error.test.tsx similarity index 100% rename from frontends/main/src/app/error.test.tsx rename to frontends/main/src/app/(site)/error.test.tsx diff --git a/frontends/main/src/app/error.tsx b/frontends/main/src/app/(site)/error.tsx similarity index 94% rename from frontends/main/src/app/error.tsx rename to frontends/main/src/app/(site)/error.tsx index 970d257f88..046773bef7 100644 --- a/frontends/main/src/app/error.tsx +++ b/frontends/main/src/app/(site)/error.tsx @@ -15,10 +15,6 @@ import FallbackErrorPage from "@/app-pages/ErrorPage/FallbackErrorPage" import { ForbiddenError } from "@/common/errors" import ForbiddenPage from "@/app-pages/ErrorPage/ForbiddenPage" -export const metadata = { - title: "Error", -} - const Error = ({ error }: { error: Error }) => { useEffect(() => { console.error("Error encountered in React error boundary:", error) diff --git a/frontends/main/src/app/honor_code/page.tsx b/frontends/main/src/app/(site)/honor_code/page.tsx similarity index 100% rename from frontends/main/src/app/honor_code/page.tsx rename to frontends/main/src/app/(site)/honor_code/page.tsx diff --git a/frontends/main/src/app/(site)/layout.tsx b/frontends/main/src/app/(site)/layout.tsx new file mode 100644 index 0000000000..c99f1e21dd --- /dev/null +++ b/frontends/main/src/app/(site)/layout.tsx @@ -0,0 +1,25 @@ +import React from "react" +import Script from "next/script" +import Header from "@/page-components/Header/Header" +import Footer from "@/page-components/Footer/Footer" +import { PageWrapper, PageWrapperInner } from "@/app/styled" +import { MITLearnGlobalStyles } from "ol-components" +export default function SiteLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + <> + + +
+ {children} +