+ Despite being yet another streaming site that could make money from
+ ads on every single thing you can click on here, we don't care.
+
+
+ That said, we're now ad-free.{" "}
+
+ I talk about "how" in more detail here
+
+ . So, when you click play, you'll see the new UI. It's pretty
+ intuitive
+
+
+
+ but remember fam, if it needs fixing, we're{" "}
+
+ open source
+
+ .
+
+
+
+
+ );
+}
diff --git a/apps/web/app/anime/[id]/layout.tsx b/apps/web/app/(catalog)/anime/[id]/layout.tsx
similarity index 88%
rename from apps/web/app/anime/[id]/layout.tsx
rename to apps/web/app/(catalog)/anime/[id]/layout.tsx
index 5a2ef04d..290f5d1c 100644
--- a/apps/web/app/anime/[id]/layout.tsx
+++ b/apps/web/app/(catalog)/anime/[id]/layout.tsx
@@ -1,5 +1,6 @@
import { DetailPageLoading } from "@/components/layout/page-loading/detail-page-loading";
import { resolveAnilistTvDetailRedirects } from "@/lib/server/anime-tv-detail-route";
+import { resolveKitsuAnimeDetailRedirects } from "@/lib/server/kitsu-anime-detail-route";
import { resolveMalAnimeDetailRedirects } from "@/lib/server/mal-anime-detail-route";
import { resolveTmdbAnimeDetailRedirects } from "@/lib/server/tmdb-anime-detail-route";
import { TvShowDetailLayoutContent } from "@/lib/server/tv-detail-layout-content";
@@ -17,6 +18,7 @@ export default async function AnimeDetailLayout({ children, params }: Props) {
await resolveMalAnimeDetailRedirects(id);
await resolveTmdbAnimeDetailRedirects(id);
+ await resolveKitsuAnimeDetailRedirects(id);
await resolveAnilistTvDetailRedirects(id);
return (
diff --git a/apps/web/app/anime/[id]/loading.tsx b/apps/web/app/(catalog)/anime/[id]/loading.tsx
similarity index 100%
rename from apps/web/app/anime/[id]/loading.tsx
rename to apps/web/app/(catalog)/anime/[id]/loading.tsx
diff --git a/apps/web/app/anime/[id]/opengraph-image.tsx b/apps/web/app/(catalog)/anime/[id]/opengraph-image.tsx
similarity index 100%
rename from apps/web/app/anime/[id]/opengraph-image.tsx
rename to apps/web/app/(catalog)/anime/[id]/opengraph-image.tsx
diff --git a/apps/web/app/anime/[id]/page.tsx b/apps/web/app/(catalog)/anime/[id]/page.tsx
similarity index 100%
rename from apps/web/app/anime/[id]/page.tsx
rename to apps/web/app/(catalog)/anime/[id]/page.tsx
diff --git a/apps/web/app/anime/browse/page.tsx b/apps/web/app/(catalog)/anime/browse/page.tsx
similarity index 96%
rename from apps/web/app/anime/browse/page.tsx
rename to apps/web/app/(catalog)/anime/browse/page.tsx
index 9734fe43..c57bfce2 100644
--- a/apps/web/app/anime/browse/page.tsx
+++ b/apps/web/app/(catalog)/anime/browse/page.tsx
@@ -3,7 +3,7 @@ import { AnimeResultsGrid } from "@/components/anilist/anime-results-grid";
import { CatalogPageShell } from "@/components/catalog/catalog-page-shell";
import { CatalogGridFallback } from "@/components/catalog/catalog-suspense-fallbacks";
import { Button } from "@/components/ui/button";
-import { parseAniListSearchParams } from "@/lib/anilist";
+import { parseAniListSearchParams } from "@/lib/anilist-shared";
import { normalizeRouteSearchParams } from "@/lib/utils";
import type { Metadata } from "next";
import Link from "next/link";
diff --git a/apps/web/app/anime/loading.tsx b/apps/web/app/(catalog)/anime/loading.tsx
similarity index 100%
rename from apps/web/app/anime/loading.tsx
rename to apps/web/app/(catalog)/anime/loading.tsx
diff --git a/apps/web/app/anime/page.tsx b/apps/web/app/(catalog)/anime/page.tsx
similarity index 70%
rename from apps/web/app/anime/page.tsx
rename to apps/web/app/(catalog)/anime/page.tsx
index 0a926474..2d0496b9 100644
--- a/apps/web/app/anime/page.tsx
+++ b/apps/web/app/(catalog)/anime/page.tsx
@@ -4,6 +4,7 @@ import {
AnimeHubGenreRowsPart1,
AnimeHubGenreRowsPart2,
AnimeHubGenreRowsPart3,
+ getAnimeHubAmbientBackdrop,
AnimeHubHero,
AnimeHubHentaiCarousel,
AnimeHubMoviesCarousel,
@@ -15,6 +16,7 @@ import {
import { AnimeHeroFallback } from "@/components/anilist/anime-suspense-fallbacks";
import { AnimeResultsGrid } from "@/components/anilist/anime-results-grid";
import { CatalogPageShell } from "@/components/catalog/catalog-page-shell";
+import { IndexPage } from "@/components/catalog/index-page";
import {
CatalogGridFallback,
CatalogRowFallback,
@@ -26,12 +28,12 @@ import {
ANIME_BROWSE_PATH,
isAniListResultsLayout,
parseAniListSearchParams,
-} from "@/lib/anilist";
+} from "@/lib/anilist-shared";
import { normalizeRouteSearchParams } from "@/lib/utils";
import type { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
-import { Suspense } from "react";
+import { Suspense, type ReactNode } from "react";
export const revalidate = 3600;
@@ -89,53 +91,55 @@ const redirectLegacyAnimeModeParam = (sp: Record) => {
redirect(query ? `/anime?${query}` : ANIME_BROWSE_PATH);
};
-const AnimeHubHome = () => (
+const AnimeHubHome = ({ chrome }: { chrome?: ReactNode }) => (
<>
}>
- }>
-
+ {chrome}
+
+ }>
+
- }>
+ }>
- }>
+ }>
- }>
+ }>
- }>
+ }>
- }>
+ }>
- }>
+ }>
- }>
+ }>
- }>
+ }>
- }>
+ }>
- }>
+ }>
>
@@ -149,26 +153,39 @@ export default async function AnimePage(props: PageProps) {
const params = parseAniListSearchParams(sp);
const currentPage = toPageNumber(sp.page);
const isResultsLayout = isAniListResultsLayout(sp);
+ const backdrop = isResultsLayout ? null : await getAnimeHubAmbientBackdrop();
+
+ if (!isResultsLayout) {
+ return (
+
+
+
+
+
+ }
+ />
+
+ );
+ }
return (
}
action={
}
>
- {isResultsLayout ? (
- }>
-
-
- ) : (
-
- )}
+ }>
+
+
);
}
diff --git a/apps/web/app/browse/country/[country]/browse-client.tsx b/apps/web/app/(catalog)/browse/country/[country]/browse-client.tsx
similarity index 100%
rename from apps/web/app/browse/country/[country]/browse-client.tsx
rename to apps/web/app/(catalog)/browse/country/[country]/browse-client.tsx
diff --git a/apps/web/app/browse/country/[country]/loading.tsx b/apps/web/app/(catalog)/browse/country/[country]/loading.tsx
similarity index 100%
rename from apps/web/app/browse/country/[country]/loading.tsx
rename to apps/web/app/(catalog)/browse/country/[country]/loading.tsx
diff --git a/apps/web/app/browse/country/[country]/page.tsx b/apps/web/app/(catalog)/browse/country/[country]/page.tsx
similarity index 88%
rename from apps/web/app/browse/country/[country]/page.tsx
rename to apps/web/app/(catalog)/browse/country/[country]/page.tsx
index 753bb578..606dfb7f 100644
--- a/apps/web/app/browse/country/[country]/page.tsx
+++ b/apps/web/app/(catalog)/browse/country/[country]/page.tsx
@@ -3,7 +3,6 @@ import { ContentContainer } from "@/components/layout/content-container";
import { PageContainer } from "@/components/layout/page-container";
import { catalogCardsToMediaItems } from "@/lib/cards/catalog-dto";
import { fetchCountryBrowsePage } from "@/lib/server/browse-catalog";
-import { tmdbImage } from "@/tmdb/utils";
import { getFriendlyCountryName } from "@/utils/country-helpers";
import { countries } from "country-data-list";
import BrowseCountryClient from "./browse-client";
@@ -34,21 +33,16 @@ export default async function BrowseCountryPage(props: PageProps) {
);
const initialItems = catalogCardsToMediaItems(data.results);
- const backdropImage =
- initialItems.length > 0 && initialItems[0].backdrop_path
- ? tmdbImage.backdrop(initialItems[0].backdrop_path, "w1280")
- : "/movie-banner.webp";
-
return (
-
+
diff --git a/apps/web/app/browse/genre/[id]/browse-client.tsx b/apps/web/app/(catalog)/browse/genre/[id]/browse-client.tsx
similarity index 100%
rename from apps/web/app/browse/genre/[id]/browse-client.tsx
rename to apps/web/app/(catalog)/browse/genre/[id]/browse-client.tsx
diff --git a/apps/web/app/browse/genre/[id]/loading.tsx b/apps/web/app/(catalog)/browse/genre/[id]/loading.tsx
similarity index 100%
rename from apps/web/app/browse/genre/[id]/loading.tsx
rename to apps/web/app/(catalog)/browse/genre/[id]/loading.tsx
diff --git a/apps/web/app/browse/genre/[id]/page.tsx b/apps/web/app/(catalog)/browse/genre/[id]/page.tsx
similarity index 86%
rename from apps/web/app/browse/genre/[id]/page.tsx
rename to apps/web/app/(catalog)/browse/genre/[id]/page.tsx
index eebd7bee..4b855f48 100644
--- a/apps/web/app/browse/genre/[id]/page.tsx
+++ b/apps/web/app/(catalog)/browse/genre/[id]/page.tsx
@@ -4,7 +4,6 @@ import { ContentContainer } from "@/components/layout/content-container";
import { PageContainer } from "@/components/layout/page-container";
import { catalogCardsToMediaItems } from "@/lib/cards/catalog-dto";
import { fetchGenreBrowsePage } from "@/lib/server/browse-catalog";
-import { tmdbImage } from "@/tmdb/utils";
import BrowseGenreClient from "./browse-client";
interface PageProps {
@@ -30,21 +29,16 @@ export default async function BrowseGenrePage(props: PageProps) {
const genreName = matchedGenre ? matchedGenre.name : "Unknown Genre";
const initialItems = catalogCardsToMediaItems(data.results);
- const backdropImage =
- initialItems.length > 0 && initialItems[0].backdrop_path
- ? tmdbImage.backdrop(initialItems[0].backdrop_path, "w1280")
- : "/movie-banner.webp";
-
return (
-
+
diff --git a/apps/web/app/collection/[id]/loading.tsx b/apps/web/app/(catalog)/collection/[id]/loading.tsx
similarity index 100%
rename from apps/web/app/collection/[id]/loading.tsx
rename to apps/web/app/(catalog)/collection/[id]/loading.tsx
diff --git a/apps/web/app/collection/[id]/opengraph-image.tsx b/apps/web/app/(catalog)/collection/[id]/opengraph-image.tsx
similarity index 100%
rename from apps/web/app/collection/[id]/opengraph-image.tsx
rename to apps/web/app/(catalog)/collection/[id]/opengraph-image.tsx
diff --git a/apps/web/app/collection/[id]/page.tsx b/apps/web/app/(catalog)/collection/[id]/page.tsx
similarity index 97%
rename from apps/web/app/collection/[id]/page.tsx
rename to apps/web/app/(catalog)/collection/[id]/page.tsx
index e1b06651..1bcfcf0b 100644
--- a/apps/web/app/collection/[id]/page.tsx
+++ b/apps/web/app/(catalog)/collection/[id]/page.tsx
@@ -67,7 +67,7 @@ export default async function CollectionPage(props: Props) {
hideTitle
/>
-
+
diff --git a/apps/web/app/live/loading.tsx b/apps/web/app/(catalog)/live/loading.tsx
similarity index 100%
rename from apps/web/app/live/loading.tsx
rename to apps/web/app/(catalog)/live/loading.tsx
diff --git a/apps/web/app/live/page.tsx b/apps/web/app/(catalog)/live/page.tsx
similarity index 70%
rename from apps/web/app/live/page.tsx
rename to apps/web/app/(catalog)/live/page.tsx
index adcd8ca8..d5892976 100644
--- a/apps/web/app/live/page.tsx
+++ b/apps/web/app/(catalog)/live/page.tsx
@@ -1,11 +1,11 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
-import { getSiteFlags } from "@/lib/flags/site-flags";
-import { StaticHero } from "@/components/hero/hero-static";
-import { ContentContainer } from "@/components/layout/content-container";
+import { IndexHeader } from "@/components/catalog/index-header";
+import { IndexPage } from "@/components/catalog/index-page";
import { PageContainer } from "@/components/layout/page-container";
import { LiveTvPage } from "@/components/live/live-tv-page";
+import { getSiteFlags } from "@/lib/flags/site-flags";
import { getLiveChannels } from "@/lib/live/dulo";
import { EMPTY_LIVE_GUIDE } from "@/lib/live/empty-guide";
import {
@@ -60,21 +60,19 @@ export default async function LivePage({ searchParams }: LivePageProps) {
return (
-
-
-
-
-
-
-
-
-
+
+ }
+ >
+
+
);
}
diff --git a/apps/web/app/movies/browse/page.tsx b/apps/web/app/(catalog)/movies/(compat)/browse/page.tsx
similarity index 100%
rename from apps/web/app/movies/browse/page.tsx
rename to apps/web/app/(catalog)/movies/(compat)/browse/page.tsx
diff --git a/apps/web/app/movies/now-playing/page.tsx b/apps/web/app/(catalog)/movies/(compat)/now-playing/page.tsx
similarity index 100%
rename from apps/web/app/movies/now-playing/page.tsx
rename to apps/web/app/(catalog)/movies/(compat)/now-playing/page.tsx
diff --git a/apps/web/app/movies/popular/page.tsx b/apps/web/app/(catalog)/movies/(compat)/popular/page.tsx
similarity index 100%
rename from apps/web/app/movies/popular/page.tsx
rename to apps/web/app/(catalog)/movies/(compat)/popular/page.tsx
diff --git a/apps/web/app/movies/top-rated/page.tsx b/apps/web/app/(catalog)/movies/(compat)/top-rated/page.tsx
similarity index 100%
rename from apps/web/app/movies/top-rated/page.tsx
rename to apps/web/app/(catalog)/movies/(compat)/top-rated/page.tsx
diff --git a/apps/web/app/movies/upcoming/page.tsx b/apps/web/app/(catalog)/movies/(compat)/upcoming/page.tsx
similarity index 100%
rename from apps/web/app/movies/upcoming/page.tsx
rename to apps/web/app/(catalog)/movies/(compat)/upcoming/page.tsx
diff --git a/apps/web/app/movies/[id]/layout.tsx b/apps/web/app/(catalog)/movies/[id]/layout.tsx
similarity index 82%
rename from apps/web/app/movies/[id]/layout.tsx
rename to apps/web/app/(catalog)/movies/[id]/layout.tsx
index ef3ba4dd..73c0691c 100644
--- a/apps/web/app/movies/[id]/layout.tsx
+++ b/apps/web/app/(catalog)/movies/[id]/layout.tsx
@@ -7,7 +7,7 @@ import { hydrateMovieDetailQueries } from "@/lib/prefetch-media-detail-queries";
import { getCachedMovieDetail } from "@/lib/media-detail-cache";
import { getDetailRouteSearchParams } from "@/lib/detail-search-params";
import { getAnilistIdFromFribb } from "@/lib/fribb-mapping";
-import { getAnilistIdForMedia } from "@/utils/anilist-helpers";
+import { getAnilistIdForMedia, isAnime } from "@/utils/anilist-helpers";
import { isUpcomingMovie } from "@/utils/movie-helpers";
import { dehydrate, QueryClient } from "@tanstack/react-query";
import { HydrationBoundary } from "@tanstack/react-query";
@@ -39,12 +39,16 @@ async function MovieDetailLayoutContent({ children, params }: Props) {
const requestSearchParams = await getDetailRouteSearchParams();
const queryAnilistId = parsePositiveInt(requestSearchParams.get("anilistId"));
- const mappedAnilistId = await getAnilistIdFromFribb(
- Number.parseInt(id, 10),
- "movie",
- );
+ const numericMovieId = Number.parseInt(id, 10);
+ const shouldAutoResolveAnime = queryAnilistId === null && isAnime(movie);
+ const mappedAnilistId =
+ shouldAutoResolveAnime && Number.isInteger(numericMovieId)
+ ? await getAnilistIdFromFribb(numericMovieId, "movie")
+ : null;
const anilistId =
- queryAnilistId ?? mappedAnilistId ?? (await getAnilistIdForMedia(movie));
+ queryAnilistId ??
+ mappedAnilistId ??
+ (shouldAutoResolveAnime ? await getAnilistIdForMedia(movie) : null);
const isUpcoming = isUpcomingMovie(movie);
const queryClient = new QueryClient();
diff --git a/apps/web/app/movies/[id]/loading.tsx b/apps/web/app/(catalog)/movies/[id]/loading.tsx
similarity index 100%
rename from apps/web/app/movies/[id]/loading.tsx
rename to apps/web/app/(catalog)/movies/[id]/loading.tsx
diff --git a/apps/web/app/movies/[id]/opengraph-image.tsx b/apps/web/app/(catalog)/movies/[id]/opengraph-image.tsx
similarity index 100%
rename from apps/web/app/movies/[id]/opengraph-image.tsx
rename to apps/web/app/(catalog)/movies/[id]/opengraph-image.tsx
diff --git a/apps/web/app/movies/[id]/page.tsx b/apps/web/app/(catalog)/movies/[id]/page.tsx
similarity index 100%
rename from apps/web/app/movies/[id]/page.tsx
rename to apps/web/app/(catalog)/movies/[id]/page.tsx
diff --git a/apps/web/app/movies/actions.ts b/apps/web/app/(catalog)/movies/actions.ts
similarity index 100%
rename from apps/web/app/movies/actions.ts
rename to apps/web/app/(catalog)/movies/actions.ts
diff --git a/apps/web/app/movies/client-components.tsx b/apps/web/app/(catalog)/movies/client-components.tsx
similarity index 100%
rename from apps/web/app/movies/client-components.tsx
rename to apps/web/app/(catalog)/movies/client-components.tsx
diff --git a/apps/web/app/movies/layout.tsx b/apps/web/app/(catalog)/movies/layout.tsx
similarity index 100%
rename from apps/web/app/movies/layout.tsx
rename to apps/web/app/(catalog)/movies/layout.tsx
diff --git a/apps/web/app/movies/loading.tsx b/apps/web/app/(catalog)/movies/loading.tsx
similarity index 100%
rename from apps/web/app/movies/loading.tsx
rename to apps/web/app/(catalog)/movies/loading.tsx
diff --git a/apps/web/app/(catalog)/movies/page.tsx b/apps/web/app/(catalog)/movies/page.tsx
new file mode 100644
index 00000000..b30a7ab2
--- /dev/null
+++ b/apps/web/app/(catalog)/movies/page.tsx
@@ -0,0 +1,93 @@
+import { IndexPage } from "@/components/catalog/index-page";
+import { CatalogDiscoverHubLoading } from "@/components/layout/page-loading/catalog-hub-loading";
+import { CatalogResultsLoading } from "@/components/layout/page-loading/catalog-results-loading";
+import {
+ MoviesDiscoverContent,
+ getMoviesHubFeature,
+ MoviesListCatalogSection,
+} from "@/components/movies/movies-catalog-sections";
+import { pages } from "@/config/pages";
+import { getCatalogLayoutState } from "@/lib/catalog-page-state";
+import { parseMovieView, stripCatalogUiParams } from "@/lib/catalog-query";
+import { getMovieCatalogListCopy } from "@/lib/catalog-list-copy";
+import { getDiscoverCatalogCopy } from "@/lib/discover-page-copy";
+import { normalizeRouteSearchParams } from "@/lib/utils";
+import { buildCatalogMetadata } from "@/lib/seo/metadata";
+import type { Metadata } from "next";
+import { Suspense } from "react";
+
+export const revalidate = 3600;
+
+interface PageProps {
+ searchParams: Promise>;
+}
+
+export async function generateMetadata({
+ searchParams,
+}: PageProps): Promise {
+ const raw = await searchParams;
+ const sp = normalizeRouteSearchParams(raw);
+ const view = parseMovieView(sp.view);
+ const { title, description } =
+ getDiscoverCatalogCopy(sp, "movie") ?? getMovieCatalogListCopy(view);
+
+ return buildCatalogMetadata({
+ title,
+ description: description || undefined,
+ path: pages.movie.catalog.link,
+ });
+}
+
+const toCatalogQueryParams = (
+ sp: Record,
+): Record =>
+ stripCatalogUiParams(
+ Object.fromEntries(Object.entries(sp).filter(([key]) => key !== "page")),
+ );
+
+export default async function MoviesCatalogPage(props: PageProps) {
+ const raw = await props.searchParams;
+ const sp = normalizeRouteSearchParams(raw);
+ const view = parseMovieView(sp.view);
+ const layoutState = getCatalogLayoutState(sp, view);
+ const { title, description } =
+ getDiscoverCatalogCopy(sp, "movie") ?? getMovieCatalogListCopy(view);
+ const catalogQueryParams = toCatalogQueryParams(sp);
+ const indexHref =
+ Object.keys(sp).length > 0 ? pages.movie.root.link : undefined;
+ const hubFeature = layoutState.isHubLayout
+ ? await getMoviesHubFeature()
+ : null;
+ const backdrop = hubFeature?.backdrop ?? null;
+
+ if (view === "discover") {
+ return (
+
+ }>
+
+
+
+ );
+ }
+
+ return (
+
+ }>
+
+
+
+ );
+}
diff --git a/apps/web/app/(catalog)/page.tsx b/apps/web/app/(catalog)/page.tsx
new file mode 100644
index 00000000..9334d84d
--- /dev/null
+++ b/apps/web/app/(catalog)/page.tsx
@@ -0,0 +1,155 @@
+import {
+ CatalogCollectionsFallback,
+ CatalogRowFallback,
+ RecentlyWatchedRowFallback,
+} from "@/components/catalog/catalog-suspense-fallbacks";
+import { HomeBecauseYouWatched } from "@/components/home/because-you-watched-row";
+import { HomeProviderRail } from "@/components/home/home-provider-rail";
+import {
+ HomeCollectionsSection,
+ HomePopularMoviesCarousel,
+ HomePopularTvCarousel,
+ HomeTrendingMoviesCarousel,
+ HomeTrendingTvCarousel,
+} from "@/components/home/home-sections";
+import { HomeRecentlyWatched } from "@/components/home/recently-watched-row";
+import { HomeUpNextInbox } from "@/components/home/up-next-inbox-row";
+import type { PageBackdrop } from "@/components/hero/ambient-page-backdrop";
+import {
+ IndexFeatureHero,
+ type IndexFeatureHeroItem,
+} from "@/components/catalog/index-feature-hero";
+import { IndexPage } from "@/components/catalog/index-page";
+import { PageContainer } from "@/components/layout/page-container";
+import { siteConfig } from "@/config/site";
+import { SITE_URL } from "@/lib/constants";
+import { getHomeTrendingMovies } from "@/lib/server/home-hub-data";
+import {
+ DEFAULT_OG_IMAGE,
+ DEFAULT_OG_IMAGE_TYPE,
+ OG_IMAGE_SIZE,
+} from "@/lib/seo/constants";
+import { tmdb, type WithImages } from "@/tmdb/api";
+import { tmdbImage } from "@/tmdb/utils";
+import type { Metadata } from "next";
+import { Suspense } from "react";
+
+export const revalidate = 3600;
+
+export const metadata: Metadata = {
+ title: "Home | NyumatFlix",
+ description: siteConfig.description,
+ openGraph: {
+ type: "website",
+ url: `${SITE_URL}/`,
+ title: "Home | NyumatFlix",
+ description: siteConfig.description,
+ images: [
+ {
+ url: DEFAULT_OG_IMAGE,
+ width: OG_IMAGE_SIZE.width,
+ height: OG_IMAGE_SIZE.height,
+ type: DEFAULT_OG_IMAGE_TYPE,
+ alt: "NyumatFlix",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ site: SITE_URL,
+ title: "Home | NyumatFlix",
+ description: siteConfig.description,
+ images: [DEFAULT_OG_IMAGE],
+ },
+};
+
+type HomeHeroFeature = {
+ item: IndexFeatureHeroItem;
+ backdrop: PageBackdrop | null;
+};
+
+const toHomeBackdrop = (item: IndexFeatureHeroItem): PageBackdrop | null => {
+ if (!item.backdrop_path) return null;
+
+ return {
+ imageUrl: tmdbImage.backdrop(item.backdrop_path, "w1280"),
+ alt: item.title ?? "Featured movie",
+ priority: true,
+ };
+};
+
+const getHomeHeroFeature = async (): Promise => {
+ const movies = await getHomeTrendingMovies();
+ const featured = movies.find((movie) => Boolean(movie.backdrop_path));
+
+ if (!featured) return null;
+
+ let item: IndexFeatureHeroItem = featured;
+
+ try {
+ item = await tmdb.movie.detail({
+ id: featured.id,
+ append: "images",
+ });
+ } catch {
+ item = featured;
+ }
+
+ return {
+ item,
+ backdrop: toHomeBackdrop(item) ?? toHomeBackdrop(featured),
+ };
+};
+
+export default async function Home() {
+ const heroFeature = await getHomeHeroFeature();
+ const backdrop = heroFeature?.backdrop ?? null;
+
+ return (
+
+
+ {heroFeature ? (
+
+ ) : null}
+
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }>
+
+
+
+
+ );
+}
diff --git a/apps/web/app/people/loading.tsx b/apps/web/app/(catalog)/people/loading.tsx
similarity index 100%
rename from apps/web/app/people/loading.tsx
rename to apps/web/app/(catalog)/people/loading.tsx
diff --git a/apps/web/app/people/page.tsx b/apps/web/app/(catalog)/people/page.tsx
similarity index 100%
rename from apps/web/app/people/page.tsx
rename to apps/web/app/(catalog)/people/page.tsx
diff --git a/apps/web/app/people/popular/loading.tsx b/apps/web/app/(catalog)/people/popular/loading.tsx
similarity index 100%
rename from apps/web/app/people/popular/loading.tsx
rename to apps/web/app/(catalog)/people/popular/loading.tsx
diff --git a/apps/web/app/people/popular/page.tsx b/apps/web/app/(catalog)/people/popular/page.tsx
similarity index 100%
rename from apps/web/app/people/popular/page.tsx
rename to apps/web/app/(catalog)/people/popular/page.tsx
diff --git a/apps/web/app/person/[id]/actions.ts b/apps/web/app/(catalog)/person/[id]/actions.ts
similarity index 100%
rename from apps/web/app/person/[id]/actions.ts
rename to apps/web/app/(catalog)/person/[id]/actions.ts
diff --git a/apps/web/app/person/[id]/client-filmography.tsx b/apps/web/app/(catalog)/person/[id]/client-filmography.tsx
similarity index 100%
rename from apps/web/app/person/[id]/client-filmography.tsx
rename to apps/web/app/(catalog)/person/[id]/client-filmography.tsx
diff --git a/apps/web/app/person/[id]/layout.tsx b/apps/web/app/(catalog)/person/[id]/layout.tsx
similarity index 100%
rename from apps/web/app/person/[id]/layout.tsx
rename to apps/web/app/(catalog)/person/[id]/layout.tsx
diff --git a/apps/web/app/person/[id]/loading.tsx b/apps/web/app/(catalog)/person/[id]/loading.tsx
similarity index 100%
rename from apps/web/app/person/[id]/loading.tsx
rename to apps/web/app/(catalog)/person/[id]/loading.tsx
diff --git a/apps/web/app/person/[id]/opengraph-image.tsx b/apps/web/app/(catalog)/person/[id]/opengraph-image.tsx
similarity index 100%
rename from apps/web/app/person/[id]/opengraph-image.tsx
rename to apps/web/app/(catalog)/person/[id]/opengraph-image.tsx
diff --git a/apps/web/app/person/[id]/page.tsx b/apps/web/app/(catalog)/person/[id]/page.tsx
similarity index 100%
rename from apps/web/app/person/[id]/page.tsx
rename to apps/web/app/(catalog)/person/[id]/page.tsx
diff --git a/apps/web/app/person/actions.ts b/apps/web/app/(catalog)/person/actions.ts
similarity index 100%
rename from apps/web/app/person/actions.ts
rename to apps/web/app/(catalog)/person/actions.ts
diff --git a/apps/web/app/(catalog)/providers/[id]/page.tsx b/apps/web/app/(catalog)/providers/[id]/page.tsx
new file mode 100644
index 00000000..438fe7c3
--- /dev/null
+++ b/apps/web/app/(catalog)/providers/[id]/page.tsx
@@ -0,0 +1,58 @@
+import {
+ getProviderCatalog,
+ ProviderCatalogPage,
+} from "@/components/provider/provider-catalog-page";
+import { getWatchProviderBrand } from "@/lib/watch-providers";
+import { normalizeRouteSearchParams } from "@/lib/utils";
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+
+type ProviderPageProps = {
+ params: Promise<{ id: string }>;
+ searchParams: Promise>;
+};
+
+const parseProviderId = (value: string) => {
+ const id = Number(value);
+ return Number.isInteger(id) && id > 0 ? id : null;
+};
+
+const parseMediaType = (value: string | undefined): "movie" | "tv" =>
+ value === "tv" ? "tv" : "movie";
+
+export default async function ProviderPage(props: ProviderPageProps) {
+ const [{ id: rawId }, rawSearchParams] = await Promise.all([
+ props.params,
+ props.searchParams,
+ ]);
+ const id = parseProviderId(rawId);
+ if (id === null) notFound();
+
+ const provider = await getWatchProviderBrand(id);
+ if (!provider) notFound();
+
+ const searchParams = normalizeRouteSearchParams(rawSearchParams);
+ const mediaType = parseMediaType(searchParams.type);
+ const catalog = await getProviderCatalog(id, mediaType);
+
+ return (
+
+ );
+}
+
+export async function generateMetadata(
+ props: ProviderPageProps,
+): Promise {
+ const { id: rawId } = await props.params;
+ const id = parseProviderId(rawId);
+ if (id === null) return { title: "Provider | NyumatFlix" };
+
+ const provider = await getWatchProviderBrand(id);
+ return {
+ title: provider ? `${provider.name} | NyumatFlix` : "Provider | NyumatFlix",
+ };
+}
diff --git a/apps/web/app/search/layout.tsx b/apps/web/app/(catalog)/search/layout.tsx
similarity index 100%
rename from apps/web/app/search/layout.tsx
rename to apps/web/app/(catalog)/search/layout.tsx
diff --git a/apps/web/app/search/loading.tsx b/apps/web/app/(catalog)/search/loading.tsx
similarity index 100%
rename from apps/web/app/search/loading.tsx
rename to apps/web/app/(catalog)/search/loading.tsx
diff --git a/apps/web/app/(catalog)/search/page.tsx b/apps/web/app/(catalog)/search/page.tsx
new file mode 100644
index 00000000..1bcb83d3
--- /dev/null
+++ b/apps/web/app/(catalog)/search/page.tsx
@@ -0,0 +1,37 @@
+import { StaticHero } from "@/components/hero/hero-static";
+import { ContentContainer } from "@/components/layout/content-container";
+import { SearchPageClient } from "@/components/search/search";
+import { Suspense } from "react";
+
+export default function SearchPage() {
+ return (
+
+
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+ Loading search...
+
+
+ }
+ >
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/app/trending/movie/page.tsx b/apps/web/app/(catalog)/trending/(compat)/movie/page.tsx
similarity index 100%
rename from apps/web/app/trending/movie/page.tsx
rename to apps/web/app/(catalog)/trending/(compat)/movie/page.tsx
diff --git a/apps/web/app/trending/tv/page.tsx b/apps/web/app/(catalog)/trending/(compat)/tv/page.tsx
similarity index 100%
rename from apps/web/app/trending/tv/page.tsx
rename to apps/web/app/(catalog)/trending/(compat)/tv/page.tsx
diff --git a/apps/web/app/trending/loading.tsx b/apps/web/app/(catalog)/trending/loading.tsx
similarity index 100%
rename from apps/web/app/trending/loading.tsx
rename to apps/web/app/(catalog)/trending/loading.tsx
diff --git a/apps/web/app/(catalog)/trending/page.tsx b/apps/web/app/(catalog)/trending/page.tsx
new file mode 100644
index 00000000..57dd0a38
--- /dev/null
+++ b/apps/web/app/(catalog)/trending/page.tsx
@@ -0,0 +1,66 @@
+import {
+ CatalogHeroPairFallback,
+ CatalogRowFallback,
+ TrendingSpotlightFallback,
+} from "@/components/catalog/catalog-suspense-fallbacks";
+import { IndexHeader } from "@/components/catalog/index-header";
+import { IndexPage } from "@/components/catalog/index-page";
+import {
+ TrendingMoviesSection,
+ TrendingPeopleSection,
+ TrendingTvSection,
+} from "@/components/trending/trending-hub-sections";
+import { buildCatalogMetadata } from "@/lib/seo/metadata";
+import { pages } from "@/config/pages";
+import type { Metadata } from "next";
+import { Suspense } from "react";
+
+export const revalidate = 3600;
+
+export const metadata: Metadata = buildCatalogMetadata({
+ title: pages.trending.root.title,
+ description:
+ "Explore trending movies, TV shows, and people updated throughout the day on NyumatFlix.",
+ path: pages.trending.root.link,
+});
+
+export default function TrendingHub() {
+ return (
+
+ }
+ >
+
+
+
+
+ >
+ }
+ >
+
+
+
+
+
+
+
+ >
+ }
+ >
+
+
+
+ }>
+
+
+
+ );
+}
diff --git a/apps/web/app/trending/people/loading.tsx b/apps/web/app/(catalog)/trending/people/loading.tsx
similarity index 100%
rename from apps/web/app/trending/people/loading.tsx
rename to apps/web/app/(catalog)/trending/people/loading.tsx
diff --git a/apps/web/app/trending/people/page.tsx b/apps/web/app/(catalog)/trending/people/page.tsx
similarity index 100%
rename from apps/web/app/trending/people/page.tsx
rename to apps/web/app/(catalog)/trending/people/page.tsx
diff --git a/apps/web/app/tvshows/airing-today/page.tsx b/apps/web/app/(catalog)/tvshows/(compat)/airing-today/page.tsx
similarity index 100%
rename from apps/web/app/tvshows/airing-today/page.tsx
rename to apps/web/app/(catalog)/tvshows/(compat)/airing-today/page.tsx
diff --git a/apps/web/app/tvshows/browse/page.tsx b/apps/web/app/(catalog)/tvshows/(compat)/browse/page.tsx
similarity index 100%
rename from apps/web/app/tvshows/browse/page.tsx
rename to apps/web/app/(catalog)/tvshows/(compat)/browse/page.tsx
diff --git a/apps/web/app/tvshows/on-the-air/page.tsx b/apps/web/app/(catalog)/tvshows/(compat)/on-the-air/page.tsx
similarity index 100%
rename from apps/web/app/tvshows/on-the-air/page.tsx
rename to apps/web/app/(catalog)/tvshows/(compat)/on-the-air/page.tsx
diff --git a/apps/web/app/tvshows/popular/page.tsx b/apps/web/app/(catalog)/tvshows/(compat)/popular/page.tsx
similarity index 100%
rename from apps/web/app/tvshows/popular/page.tsx
rename to apps/web/app/(catalog)/tvshows/(compat)/popular/page.tsx
diff --git a/apps/web/app/tvshows/top-rated/page.tsx b/apps/web/app/(catalog)/tvshows/(compat)/top-rated/page.tsx
similarity index 100%
rename from apps/web/app/tvshows/top-rated/page.tsx
rename to apps/web/app/(catalog)/tvshows/(compat)/top-rated/page.tsx
diff --git a/apps/web/app/tvshows/[id]/layout.tsx b/apps/web/app/(catalog)/tvshows/[id]/layout.tsx
similarity index 100%
rename from apps/web/app/tvshows/[id]/layout.tsx
rename to apps/web/app/(catalog)/tvshows/[id]/layout.tsx
diff --git a/apps/web/app/tvshows/[id]/loading.tsx b/apps/web/app/(catalog)/tvshows/[id]/loading.tsx
similarity index 100%
rename from apps/web/app/tvshows/[id]/loading.tsx
rename to apps/web/app/(catalog)/tvshows/[id]/loading.tsx
diff --git a/apps/web/app/tvshows/[id]/opengraph-image.tsx b/apps/web/app/(catalog)/tvshows/[id]/opengraph-image.tsx
similarity index 100%
rename from apps/web/app/tvshows/[id]/opengraph-image.tsx
rename to apps/web/app/(catalog)/tvshows/[id]/opengraph-image.tsx
diff --git a/apps/web/app/tvshows/[id]/page.tsx b/apps/web/app/(catalog)/tvshows/[id]/page.tsx
similarity index 100%
rename from apps/web/app/tvshows/[id]/page.tsx
rename to apps/web/app/(catalog)/tvshows/[id]/page.tsx
diff --git a/apps/web/app/tvshows/client-components.tsx b/apps/web/app/(catalog)/tvshows/client-components.tsx
similarity index 100%
rename from apps/web/app/tvshows/client-components.tsx
rename to apps/web/app/(catalog)/tvshows/client-components.tsx
diff --git a/apps/web/app/tvshows/layout.tsx b/apps/web/app/(catalog)/tvshows/layout.tsx
similarity index 100%
rename from apps/web/app/tvshows/layout.tsx
rename to apps/web/app/(catalog)/tvshows/layout.tsx
diff --git a/apps/web/app/tvshows/loading.tsx b/apps/web/app/(catalog)/tvshows/loading.tsx
similarity index 100%
rename from apps/web/app/tvshows/loading.tsx
rename to apps/web/app/(catalog)/tvshows/loading.tsx
diff --git a/apps/web/app/(catalog)/tvshows/page.tsx b/apps/web/app/(catalog)/tvshows/page.tsx
new file mode 100644
index 00000000..bb5a2229
--- /dev/null
+++ b/apps/web/app/(catalog)/tvshows/page.tsx
@@ -0,0 +1,90 @@
+import { IndexPage } from "@/components/catalog/index-page";
+import { CatalogDiscoverHubLoading } from "@/components/layout/page-loading/catalog-hub-loading";
+import { CatalogResultsLoading } from "@/components/layout/page-loading/catalog-results-loading";
+import {
+ TvDiscoverContent,
+ getTvHubFeature,
+ TvListCatalogSection,
+} from "@/components/tv/tv-catalog-sections";
+import { pages } from "@/config/pages";
+import { getCatalogLayoutState } from "@/lib/catalog-page-state";
+import { parseTvView, stripCatalogUiParams } from "@/lib/catalog-query";
+import { getTvCatalogListCopy } from "@/lib/catalog-list-copy";
+import { getDiscoverCatalogCopy } from "@/lib/discover-page-copy";
+import { normalizeRouteSearchParams } from "@/lib/utils";
+import { buildCatalogMetadata } from "@/lib/seo/metadata";
+import type { Metadata } from "next";
+import { Suspense } from "react";
+
+export const revalidate = 3600;
+
+interface PageProps {
+ searchParams: Promise>;
+}
+
+export async function generateMetadata({
+ searchParams,
+}: PageProps): Promise {
+ const raw = await searchParams;
+ const sp = normalizeRouteSearchParams(raw);
+ const view = parseTvView(sp.view);
+ const { title, description } =
+ getDiscoverCatalogCopy(sp, "tv") ?? getTvCatalogListCopy(view);
+
+ return buildCatalogMetadata({
+ title,
+ description: description || undefined,
+ path: pages.tv.catalog.link,
+ });
+}
+
+const toCatalogQueryParams = (
+ sp: Record,
+): Record =>
+ stripCatalogUiParams(
+ Object.fromEntries(Object.entries(sp).filter(([key]) => key !== "page")),
+ );
+
+export default async function TvShowsCatalogPage(props: PageProps) {
+ const raw = await props.searchParams;
+ const sp = normalizeRouteSearchParams(raw);
+ const view = parseTvView(sp.view);
+ const layoutState = getCatalogLayoutState(sp, view);
+ const { title, description } =
+ getDiscoverCatalogCopy(sp, "tv") ?? getTvCatalogListCopy(view);
+ const catalogQueryParams = toCatalogQueryParams(sp);
+ const indexHref = Object.keys(sp).length > 0 ? pages.tv.root.link : undefined;
+ const hubFeature = layoutState.isHubLayout ? await getTvHubFeature() : null;
+ const backdrop = hubFeature?.backdrop ?? null;
+
+ if (view === "discover") {
+ return (
+
+ }>
+
+
+
+ );
+ }
+
+ return (
+
+ }>
+
+
+
+ );
+}
diff --git a/apps/web/app/watch/[id]/page.tsx b/apps/web/app/(compat)/watch/[id]/page.tsx
similarity index 100%
rename from apps/web/app/watch/[id]/page.tsx
rename to apps/web/app/(compat)/watch/[id]/page.tsx
diff --git a/apps/web/app/dev/og-preview/[type]/[id]/image/route.tsx b/apps/web/app/(internal)/dev/og-preview/[type]/[id]/image/route.tsx
similarity index 100%
rename from apps/web/app/dev/og-preview/[type]/[id]/image/route.tsx
rename to apps/web/app/(internal)/dev/og-preview/[type]/[id]/image/route.tsx
diff --git a/apps/web/app/dev/og-preview/page.tsx b/apps/web/app/(internal)/dev/og-preview/page.tsx
similarity index 100%
rename from apps/web/app/dev/og-preview/page.tsx
rename to apps/web/app/(internal)/dev/og-preview/page.tsx
diff --git a/apps/web/app/dev/site-og/image/route.tsx b/apps/web/app/(internal)/dev/site-og/image/route.tsx
similarity index 100%
rename from apps/web/app/dev/site-og/image/route.tsx
rename to apps/web/app/(internal)/dev/site-og/image/route.tsx
diff --git a/apps/web/app/dev/site-og/page.tsx b/apps/web/app/(internal)/dev/site-og/page.tsx
similarity index 100%
rename from apps/web/app/dev/site-og/page.tsx
rename to apps/web/app/(internal)/dev/site-og/page.tsx
diff --git a/apps/web/app/ffs/layout.tsx b/apps/web/app/(internal)/ffs/layout.tsx
similarity index 100%
rename from apps/web/app/ffs/layout.tsx
rename to apps/web/app/(internal)/ffs/layout.tsx
diff --git a/apps/web/app/ffs/page.tsx b/apps/web/app/(internal)/ffs/page.tsx
similarity index 100%
rename from apps/web/app/ffs/page.tsx
rename to apps/web/app/(internal)/ffs/page.tsx
diff --git a/apps/web/app/cookie-policy/page.tsx b/apps/web/app/(legal)/cookie-policy/page.tsx
similarity index 100%
rename from apps/web/app/cookie-policy/page.tsx
rename to apps/web/app/(legal)/cookie-policy/page.tsx
diff --git a/apps/web/app/dmca/page.tsx b/apps/web/app/(legal)/dmca/page.tsx
similarity index 100%
rename from apps/web/app/dmca/page.tsx
rename to apps/web/app/(legal)/dmca/page.tsx
diff --git a/apps/web/app/privacy/page.tsx b/apps/web/app/(legal)/privacy/page.tsx
similarity index 100%
rename from apps/web/app/privacy/page.tsx
rename to apps/web/app/(legal)/privacy/page.tsx
diff --git a/apps/web/app/terms/page.tsx b/apps/web/app/(legal)/terms/page.tsx
similarity index 100%
rename from apps/web/app/terms/page.tsx
rename to apps/web/app/(legal)/terms/page.tsx
diff --git a/apps/web/app/history/history-client.tsx b/apps/web/app/(library)/history/history-client.tsx
similarity index 100%
rename from apps/web/app/history/history-client.tsx
rename to apps/web/app/(library)/history/history-client.tsx
diff --git a/apps/web/app/history/page.tsx b/apps/web/app/(library)/history/page.tsx
similarity index 100%
rename from apps/web/app/history/page.tsx
rename to apps/web/app/(library)/history/page.tsx
diff --git a/apps/web/app/watchlist/actions.ts b/apps/web/app/(library)/watchlist/actions.ts
similarity index 100%
rename from apps/web/app/watchlist/actions.ts
rename to apps/web/app/(library)/watchlist/actions.ts
diff --git a/apps/web/app/watchlist/episode-check-service.ts b/apps/web/app/(library)/watchlist/episode-check-service.ts
similarity index 100%
rename from apps/web/app/watchlist/episode-check-service.ts
rename to apps/web/app/(library)/watchlist/episode-check-service.ts
diff --git a/apps/web/app/watchlist/loading.tsx b/apps/web/app/(library)/watchlist/loading.tsx
similarity index 100%
rename from apps/web/app/watchlist/loading.tsx
rename to apps/web/app/(library)/watchlist/loading.tsx
diff --git a/apps/web/app/watchlist/page.tsx b/apps/web/app/(library)/watchlist/page.tsx
similarity index 100%
rename from apps/web/app/watchlist/page.tsx
rename to apps/web/app/(library)/watchlist/page.tsx
diff --git a/apps/web/app/watchlist/watchlist-client.tsx b/apps/web/app/(library)/watchlist/watchlist-client.tsx
similarity index 100%
rename from apps/web/app/watchlist/watchlist-client.tsx
rename to apps/web/app/(library)/watchlist/watchlist-client.tsx
diff --git a/apps/web/app/watchlist/watchlist-content.tsx b/apps/web/app/(library)/watchlist/watchlist-content.tsx
similarity index 100%
rename from apps/web/app/watchlist/watchlist-content.tsx
rename to apps/web/app/(library)/watchlist/watchlist-content.tsx
diff --git a/apps/web/app/ad-free/page.tsx b/apps/web/app/ad-free/page.tsx
deleted file mode 100644
index 057e376a..00000000
--- a/apps/web/app/ad-free/page.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { StaticHero } from "@/components/hero/hero-static";
-import { ContentContainer } from "@/components/layout/content-container";
-import { LegalPage } from "@/components/layout/legal-page";
-import Link from "next/link";
-
-export const metadata = {
- title: "Big News",
-};
-
-export default function AdFreePage() {
- return (
-
-
-
-
-
-
-
-
- Despite being yet another streaming site that could make money
- from ads on every single thing you can click on here, we
- don't care.
-
-
- That said, we're now ad-free.{" "}
-
- I talk about "how" in more detail here
-
- . So, when you click play, you'll see the new UI. It's pretty
- intuitive
-
-
-
- but remember fam, if it needs fixing, we're{" "}
-
- open source
-
- .
-