From 4f06bc6ab2e30701c43292f7a875bc8c2d3e0855 Mon Sep 17 00:00:00 2001 From: dotcomnerd <70242273+dotcomnerd@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:59:38 -0700 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=97=82=EF=B8=8F=20group=20app=20rou?= =?UTF-8?q?tes=20by=20domain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/{ => (account)}/login/auth-shell.tsx | 0 .../app/{ => (account)}/login/error/page.tsx | 0 .../login/neural-network-client.tsx | 0 apps/web/app/{ => (account)}/login/page.tsx | 0 .../app/{ => (account)}/login/verify/page.tsx | 0 .../web/app/{ => (account)}/settings/page.tsx | 0 .../settings/settings-client.tsx | 0 apps/web/app/(catalog)/ad-free/page.tsx | 49 ++++++ .../app/{ => (catalog)}/anime/[id]/layout.tsx | 2 + .../{ => (catalog)}/anime/[id]/loading.tsx | 0 .../anime/[id]/opengraph-image.tsx | 0 .../app/{ => (catalog)}/anime/[id]/page.tsx | 0 .../app/{ => (catalog)}/anime/browse/page.tsx | 2 +- .../web/app/{ => (catalog)}/anime/loading.tsx | 0 apps/web/app/{ => (catalog)}/anime/page.tsx | 67 +++++--- .../country/[country]/browse-client.tsx | 0 .../browse/country/[country]/loading.tsx | 0 .../browse/country/[country]/page.tsx | 10 +- .../browse/genre/[id]/browse-client.tsx | 0 .../browse/genre/[id]/loading.tsx | 0 .../browse/genre/[id]/page.tsx | 10 +- .../collection/[id]/loading.tsx | 0 .../collection/[id]/opengraph-image.tsx | 0 .../{ => (catalog)}/collection/[id]/page.tsx | 2 +- apps/web/app/{ => (catalog)}/live/loading.tsx | 0 apps/web/app/{ => (catalog)}/live/page.tsx | 34 ++-- .../movies/(compat)}/browse/page.tsx | 0 .../movies/(compat)}/now-playing/page.tsx | 0 .../movies/(compat)}/popular/page.tsx | 0 .../movies/(compat)}/top-rated/page.tsx | 0 .../movies/(compat)}/upcoming/page.tsx | 0 .../{ => (catalog)}/movies/[id]/layout.tsx | 16 +- .../{ => (catalog)}/movies/[id]/loading.tsx | 0 .../movies/[id]/opengraph-image.tsx | 0 .../app/{ => (catalog)}/movies/[id]/page.tsx | 0 .../web/app/{ => (catalog)}/movies/actions.ts | 0 .../movies/client-components.tsx | 0 .../web/app/{ => (catalog)}/movies/layout.tsx | 0 .../app/{ => (catalog)}/movies/loading.tsx | 0 apps/web/app/(catalog)/movies/page.tsx | 93 +++++++++++ apps/web/app/(catalog)/page.tsx | 155 ++++++++++++++++++ .../app/{ => (catalog)}/people/loading.tsx | 0 apps/web/app/{ => (catalog)}/people/page.tsx | 0 .../people/popular/loading.tsx | 0 .../{ => (catalog)}/people/popular/page.tsx | 0 .../{ => (catalog)}/person/[id]/actions.ts | 0 .../person/[id]/client-filmography.tsx | 0 .../{ => (catalog)}/person/[id]/layout.tsx | 0 .../{ => (catalog)}/person/[id]/loading.tsx | 0 .../person/[id]/opengraph-image.tsx | 0 .../app/{ => (catalog)}/person/[id]/page.tsx | 0 .../web/app/{ => (catalog)}/person/actions.ts | 0 .../web/app/(catalog)/providers/[id]/page.tsx | 58 +++++++ .../web/app/{ => (catalog)}/search/layout.tsx | 0 .../app/{ => (catalog)}/search/loading.tsx | 0 apps/web/app/(catalog)/search/page.tsx | 37 +++++ .../trending/(compat)}/movie/page.tsx | 0 .../trending/(compat)}/tv/page.tsx | 0 .../app/{ => (catalog)}/trending/loading.tsx | 0 apps/web/app/(catalog)/trending/page.tsx | 66 ++++++++ .../trending/people/loading.tsx | 0 .../{ => (catalog)}/trending/people/page.tsx | 0 .../tvshows/(compat)}/airing-today/page.tsx | 0 .../tvshows/(compat)}/browse/page.tsx | 0 .../tvshows/(compat)}/on-the-air/page.tsx | 0 .../tvshows/(compat)}/popular/page.tsx | 0 .../tvshows/(compat)}/top-rated/page.tsx | 0 .../{ => (catalog)}/tvshows/[id]/layout.tsx | 0 .../{ => (catalog)}/tvshows/[id]/loading.tsx | 0 .../tvshows/[id]/opengraph-image.tsx | 0 .../app/{ => (catalog)}/tvshows/[id]/page.tsx | 0 .../tvshows/client-components.tsx | 0 .../app/{ => (catalog)}/tvshows/layout.tsx | 0 .../app/{ => (catalog)}/tvshows/loading.tsx | 0 apps/web/app/(catalog)/tvshows/page.tsx | 90 ++++++++++ .../app/{ => (compat)}/watch/[id]/page.tsx | 0 .../og-preview/[type]/[id]/image/route.tsx | 0 .../{ => (internal)}/dev/og-preview/page.tsx | 0 .../dev/site-og/image/route.tsx | 0 .../app/{ => (internal)}/dev/site-og/page.tsx | 0 apps/web/app/{ => (internal)}/ffs/layout.tsx | 0 apps/web/app/{ => (internal)}/ffs/page.tsx | 0 .../app/{ => (legal)}/cookie-policy/page.tsx | 0 apps/web/app/{ => (legal)}/dmca/page.tsx | 0 apps/web/app/{ => (legal)}/privacy/page.tsx | 0 apps/web/app/{ => (legal)}/terms/page.tsx | 0 .../history/history-client.tsx | 0 apps/web/app/{ => (library)}/history/page.tsx | 0 .../app/{ => (library)}/watchlist/actions.ts | 0 .../watchlist/episode-check-service.ts | 0 .../app/{ => (library)}/watchlist/loading.tsx | 0 .../app/{ => (library)}/watchlist/page.tsx | 0 .../watchlist/watchlist-client.tsx | 0 .../watchlist/watchlist-content.tsx | 0 apps/web/app/ad-free/page.tsx | 56 ------- apps/web/app/globals.css | 93 +++++++++-- apps/web/app/movies/page.tsx | 126 -------------- apps/web/app/page.tsx | 131 --------------- apps/web/app/search/page.tsx | 35 ---- apps/web/app/trending/page.tsx | 73 --------- apps/web/app/tvshows/page.tsx | 125 -------------- .../components/layout/route-scroll-reset.tsx | 62 +------ apps/web/next.config.mjs | 8 + 103 files changed, 715 insertions(+), 685 deletions(-) rename apps/web/app/{ => (account)}/login/auth-shell.tsx (100%) rename apps/web/app/{ => (account)}/login/error/page.tsx (100%) rename apps/web/app/{ => (account)}/login/neural-network-client.tsx (100%) rename apps/web/app/{ => (account)}/login/page.tsx (100%) rename apps/web/app/{ => (account)}/login/verify/page.tsx (100%) rename apps/web/app/{ => (account)}/settings/page.tsx (100%) rename apps/web/app/{ => (account)}/settings/settings-client.tsx (100%) create mode 100644 apps/web/app/(catalog)/ad-free/page.tsx rename apps/web/app/{ => (catalog)}/anime/[id]/layout.tsx (88%) rename apps/web/app/{ => (catalog)}/anime/[id]/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/anime/[id]/opengraph-image.tsx (100%) rename apps/web/app/{ => (catalog)}/anime/[id]/page.tsx (100%) rename apps/web/app/{ => (catalog)}/anime/browse/page.tsx (96%) rename apps/web/app/{ => (catalog)}/anime/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/anime/page.tsx (70%) rename apps/web/app/{ => (catalog)}/browse/country/[country]/browse-client.tsx (100%) rename apps/web/app/{ => (catalog)}/browse/country/[country]/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/browse/country/[country]/page.tsx (88%) rename apps/web/app/{ => (catalog)}/browse/genre/[id]/browse-client.tsx (100%) rename apps/web/app/{ => (catalog)}/browse/genre/[id]/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/browse/genre/[id]/page.tsx (86%) rename apps/web/app/{ => (catalog)}/collection/[id]/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/collection/[id]/opengraph-image.tsx (100%) rename apps/web/app/{ => (catalog)}/collection/[id]/page.tsx (97%) rename apps/web/app/{ => (catalog)}/live/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/live/page.tsx (70%) rename apps/web/app/{movies => (catalog)/movies/(compat)}/browse/page.tsx (100%) rename apps/web/app/{movies => (catalog)/movies/(compat)}/now-playing/page.tsx (100%) rename apps/web/app/{movies => (catalog)/movies/(compat)}/popular/page.tsx (100%) rename apps/web/app/{movies => (catalog)/movies/(compat)}/top-rated/page.tsx (100%) rename apps/web/app/{movies => (catalog)/movies/(compat)}/upcoming/page.tsx (100%) rename apps/web/app/{ => (catalog)}/movies/[id]/layout.tsx (82%) rename apps/web/app/{ => (catalog)}/movies/[id]/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/movies/[id]/opengraph-image.tsx (100%) rename apps/web/app/{ => (catalog)}/movies/[id]/page.tsx (100%) rename apps/web/app/{ => (catalog)}/movies/actions.ts (100%) rename apps/web/app/{ => (catalog)}/movies/client-components.tsx (100%) rename apps/web/app/{ => (catalog)}/movies/layout.tsx (100%) rename apps/web/app/{ => (catalog)}/movies/loading.tsx (100%) create mode 100644 apps/web/app/(catalog)/movies/page.tsx create mode 100644 apps/web/app/(catalog)/page.tsx rename apps/web/app/{ => (catalog)}/people/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/people/page.tsx (100%) rename apps/web/app/{ => (catalog)}/people/popular/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/people/popular/page.tsx (100%) rename apps/web/app/{ => (catalog)}/person/[id]/actions.ts (100%) rename apps/web/app/{ => (catalog)}/person/[id]/client-filmography.tsx (100%) rename apps/web/app/{ => (catalog)}/person/[id]/layout.tsx (100%) rename apps/web/app/{ => (catalog)}/person/[id]/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/person/[id]/opengraph-image.tsx (100%) rename apps/web/app/{ => (catalog)}/person/[id]/page.tsx (100%) rename apps/web/app/{ => (catalog)}/person/actions.ts (100%) create mode 100644 apps/web/app/(catalog)/providers/[id]/page.tsx rename apps/web/app/{ => (catalog)}/search/layout.tsx (100%) rename apps/web/app/{ => (catalog)}/search/loading.tsx (100%) create mode 100644 apps/web/app/(catalog)/search/page.tsx rename apps/web/app/{trending => (catalog)/trending/(compat)}/movie/page.tsx (100%) rename apps/web/app/{trending => (catalog)/trending/(compat)}/tv/page.tsx (100%) rename apps/web/app/{ => (catalog)}/trending/loading.tsx (100%) create mode 100644 apps/web/app/(catalog)/trending/page.tsx rename apps/web/app/{ => (catalog)}/trending/people/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/trending/people/page.tsx (100%) rename apps/web/app/{tvshows => (catalog)/tvshows/(compat)}/airing-today/page.tsx (100%) rename apps/web/app/{tvshows => (catalog)/tvshows/(compat)}/browse/page.tsx (100%) rename apps/web/app/{tvshows => (catalog)/tvshows/(compat)}/on-the-air/page.tsx (100%) rename apps/web/app/{tvshows => (catalog)/tvshows/(compat)}/popular/page.tsx (100%) rename apps/web/app/{tvshows => (catalog)/tvshows/(compat)}/top-rated/page.tsx (100%) rename apps/web/app/{ => (catalog)}/tvshows/[id]/layout.tsx (100%) rename apps/web/app/{ => (catalog)}/tvshows/[id]/loading.tsx (100%) rename apps/web/app/{ => (catalog)}/tvshows/[id]/opengraph-image.tsx (100%) rename apps/web/app/{ => (catalog)}/tvshows/[id]/page.tsx (100%) rename apps/web/app/{ => (catalog)}/tvshows/client-components.tsx (100%) rename apps/web/app/{ => (catalog)}/tvshows/layout.tsx (100%) rename apps/web/app/{ => (catalog)}/tvshows/loading.tsx (100%) create mode 100644 apps/web/app/(catalog)/tvshows/page.tsx rename apps/web/app/{ => (compat)}/watch/[id]/page.tsx (100%) rename apps/web/app/{ => (internal)}/dev/og-preview/[type]/[id]/image/route.tsx (100%) rename apps/web/app/{ => (internal)}/dev/og-preview/page.tsx (100%) rename apps/web/app/{ => (internal)}/dev/site-og/image/route.tsx (100%) rename apps/web/app/{ => (internal)}/dev/site-og/page.tsx (100%) rename apps/web/app/{ => (internal)}/ffs/layout.tsx (100%) rename apps/web/app/{ => (internal)}/ffs/page.tsx (100%) rename apps/web/app/{ => (legal)}/cookie-policy/page.tsx (100%) rename apps/web/app/{ => (legal)}/dmca/page.tsx (100%) rename apps/web/app/{ => (legal)}/privacy/page.tsx (100%) rename apps/web/app/{ => (legal)}/terms/page.tsx (100%) rename apps/web/app/{ => (library)}/history/history-client.tsx (100%) rename apps/web/app/{ => (library)}/history/page.tsx (100%) rename apps/web/app/{ => (library)}/watchlist/actions.ts (100%) rename apps/web/app/{ => (library)}/watchlist/episode-check-service.ts (100%) rename apps/web/app/{ => (library)}/watchlist/loading.tsx (100%) rename apps/web/app/{ => (library)}/watchlist/page.tsx (100%) rename apps/web/app/{ => (library)}/watchlist/watchlist-client.tsx (100%) rename apps/web/app/{ => (library)}/watchlist/watchlist-content.tsx (100%) delete mode 100644 apps/web/app/ad-free/page.tsx delete mode 100644 apps/web/app/movies/page.tsx delete mode 100644 apps/web/app/page.tsx delete mode 100644 apps/web/app/search/page.tsx delete mode 100644 apps/web/app/trending/page.tsx delete mode 100644 apps/web/app/tvshows/page.tsx diff --git a/apps/web/app/login/auth-shell.tsx b/apps/web/app/(account)/login/auth-shell.tsx similarity index 100% rename from apps/web/app/login/auth-shell.tsx rename to apps/web/app/(account)/login/auth-shell.tsx diff --git a/apps/web/app/login/error/page.tsx b/apps/web/app/(account)/login/error/page.tsx similarity index 100% rename from apps/web/app/login/error/page.tsx rename to apps/web/app/(account)/login/error/page.tsx diff --git a/apps/web/app/login/neural-network-client.tsx b/apps/web/app/(account)/login/neural-network-client.tsx similarity index 100% rename from apps/web/app/login/neural-network-client.tsx rename to apps/web/app/(account)/login/neural-network-client.tsx diff --git a/apps/web/app/login/page.tsx b/apps/web/app/(account)/login/page.tsx similarity index 100% rename from apps/web/app/login/page.tsx rename to apps/web/app/(account)/login/page.tsx diff --git a/apps/web/app/login/verify/page.tsx b/apps/web/app/(account)/login/verify/page.tsx similarity index 100% rename from apps/web/app/login/verify/page.tsx rename to apps/web/app/(account)/login/verify/page.tsx diff --git a/apps/web/app/settings/page.tsx b/apps/web/app/(account)/settings/page.tsx similarity index 100% rename from apps/web/app/settings/page.tsx rename to apps/web/app/(account)/settings/page.tsx diff --git a/apps/web/app/settings/settings-client.tsx b/apps/web/app/(account)/settings/settings-client.tsx similarity index 100% rename from apps/web/app/settings/settings-client.tsx rename to apps/web/app/(account)/settings/settings-client.tsx diff --git a/apps/web/app/(catalog)/ad-free/page.tsx b/apps/web/app/(catalog)/ad-free/page.tsx new file mode 100644 index 00000000..d3606953 --- /dev/null +++ b/apps/web/app/(catalog)/ad-free/page.tsx @@ -0,0 +1,49 @@ +import { IndexHeader } from "@/components/catalog/index-header"; +import { IndexPage } from "@/components/catalog/index-page"; +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 + + . +

+
+
+
+ ); +} 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 - - . -

-
-
-
-
-
- ); -} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 161620ce..f8e8b5cd 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -260,18 +260,6 @@ html { color-scheme: dark; } -/* - * Keep wide-screen browsing intentionally dense. This recreates the clearer, - * zoomed-out composition without scaling the rendered page or affecting - * tablet/mobile layouts. Rem-based type and spacing follow this proportion, - * while browser zoom and user accessibility settings continue to work. - */ -@media (width >= 80rem) { - html { - font-size: 82.5%; - } -} - /* *=========== Default theme =========== */ @layer base { /* :root { @@ -533,6 +521,69 @@ html { } } +@utility index-container { + margin-inline: auto; + width: 100%; + max-width: 1760px; + padding-inline: 1rem; + + @media (width >= 40rem) { + padding-inline: 1.5rem; + } + + @media (width >= 64rem) { + padding-inline: 2rem; + } + + @media (width >= 80rem) { + padding-inline: 3rem; + } + + @media (width >= 96rem) { + padding-inline: 4rem; + } +} + +@utility index-bleed { + margin-inline: -1rem; + + @media (width >= 40rem) { + margin-inline: -1.5rem; + } + + @media (width >= 64rem) { + margin-inline: -2rem; + } + + @media (width >= 80rem) { + margin-inline: -3rem; + } + + @media (width >= 96rem) { + margin-inline: -4rem; + } +} + +@utility index-rail-padding { + padding-inline: 1rem; + + @media (width >= 40rem) { + padding-inline: 1.5rem; + } + + @media (width >= 64rem) { + padding-inline: 2rem; + } + + @media (width >= 80rem) { + padding-inline: 3rem; + } + + @media (width >= 96rem) { + padding-inline: 4rem; + } +} + @utility site-container { margin-inline: auto; width: 100%; @@ -550,16 +601,28 @@ html { @utility grid-list { display: grid; - grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(100%, 8.5rem), 1fr)); gap: 1rem; @media (width >= 48rem) { - grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); } @media (width >= 64rem) { - grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); } + + @media (width >= 96rem) { + grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); + } +} + +@utility h-index-spotlight { + height: clamp(28rem, 38vw, 42rem); +} + +@utility h-index-feature { + height: clamp(20rem, 26vw, 32rem); } @utility h-hero { diff --git a/apps/web/app/movies/page.tsx b/apps/web/app/movies/page.tsx deleted file mode 100644 index b92dd26c..00000000 --- a/apps/web/app/movies/page.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { CatalogDiscoverHubLoading } from "@/components/layout/page-loading/catalog-hub-loading"; -import { CatalogResultsLoading } from "@/components/layout/page-loading/catalog-results-loading"; -import { QueryPageHeader } from "@/components/catalog/query-page-header"; -import { StaticHero } from "@/components/hero/hero-static"; -import { ContentContainer } from "@/components/layout/content-container"; -import { - MoviesDiscoverContent, - MoviesDiscoverToolbarFallback, - MoviesDiscoverToolbarSection, - MoviesListCatalogSection, -} from "@/components/movies/movies-catalog-sections"; -import { ScrollToTop } from "@/components/ui/scroll-to-top"; -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; - - if (view === "discover") { - return ( -
- - - -
-
- {layoutState.isHubLayout ? ( - <> - - - }> - - - - ) : null} - - }> - - -
-
-
- - -
- ); - } - - return ( -
- - - -
-
- }> - - -
-
-
- - -
- ); -} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx deleted file mode 100644 index 250f83f7..00000000 --- a/apps/web/app/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { StaticHero } from "@/components/hero/hero-static"; -import { - CatalogCollectionsFallback, - CatalogHeroPairFallback, - CatalogRowFallback, - CatalogSpotlightFallback, - RecentlyWatchedRowFallback, -} from "@/components/catalog/catalog-suspense-fallbacks"; -import { HomeBecauseYouWatched } from "@/components/home/because-you-watched-row"; -import { - HomeCollectionsSection, - HomeFeaturedMovie, - HomePopularMovieHeroes, - HomePopularMoviesCarousel, - HomePopularTvCarousel, - HomePopularTvHeroes, - HomeTrendingMovieHeroes, - HomeTrendingMoviesCarousel, - HomeTrendingTvCarousel, - HomeTrendingTvHeroes, -} from "@/components/home/home-sections"; -import { HomeRecentlyWatched } from "@/components/home/recently-watched-row"; -import { HomeUpNextInbox } from "@/components/home/up-next-inbox-row"; -import { ContentContainer } from "@/components/layout/content-container"; -import { PageContainer } from "@/components/layout/page-container"; -import { siteConfig } from "@/config/site"; -import { SITE_URL } from "@/lib/constants"; -import { - DEFAULT_OG_IMAGE, - DEFAULT_OG_IMAGE_TYPE, - OG_IMAGE_SIZE, -} from "@/lib/seo/constants"; -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], - }, -}; - -export default function Home() { - return ( - -
- - - -
-
- }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - - - }> - - -
-
-
-
-
- ); -} diff --git a/apps/web/app/search/page.tsx b/apps/web/app/search/page.tsx deleted file mode 100644 index d6c5868c..00000000 --- a/apps/web/app/search/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -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/page.tsx b/apps/web/app/trending/page.tsx deleted file mode 100644 index 7bd19bbb..00000000 --- a/apps/web/app/trending/page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - CatalogHeroPairFallback, - CatalogRowFallback, - TrendingSpotlightFallback, -} from "@/components/catalog/catalog-suspense-fallbacks"; -import { StaticHero } from "@/components/hero/hero-static"; -import { ContentContainer } from "@/components/layout/content-container"; -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 ( -
- - - -
-
-
-

- Trending -

-
- - - - - - - } - > - - - - - - - - - } - > - - - - }> - - -
-
-
-
- ); -} diff --git a/apps/web/app/tvshows/page.tsx b/apps/web/app/tvshows/page.tsx deleted file mode 100644 index 0d37a21f..00000000 --- a/apps/web/app/tvshows/page.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { CatalogDiscoverHubLoading } from "@/components/layout/page-loading/catalog-hub-loading"; -import { CatalogResultsLoading } from "@/components/layout/page-loading/catalog-results-loading"; -import { QueryPageHeader } from "@/components/catalog/query-page-header"; -import { StaticHero } from "@/components/hero/hero-static"; -import { ContentContainer } from "@/components/layout/content-container"; -import { - TvDiscoverContent, - TvDiscoverToolbarFallback, - TvDiscoverToolbarSection, - TvListCatalogSection, -} from "@/components/tv/tv-catalog-sections"; -import { ScrollToTop } from "@/components/ui/scroll-to-top"; -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; - - if (view === "discover") { - return ( -
- - - -
-
- {layoutState.isHubLayout ? ( - <> - - - }> - - - - ) : null} - - }> - - -
-
-
- - -
- ); - } - - return ( -
- - - -
-
- }> - - -
-
-
- - -
- ); -} diff --git a/apps/web/components/layout/route-scroll-reset.tsx b/apps/web/components/layout/route-scroll-reset.tsx index 5fb8d116..923e174a 100644 --- a/apps/web/components/layout/route-scroll-reset.tsx +++ b/apps/web/components/layout/route-scroll-reset.tsx @@ -1,7 +1,7 @@ "use client"; import { usePathname, useRouter } from "next/navigation"; -import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useRef } from "react"; import { commitNavigationEntry, consumePendingRouteSnapshot, @@ -9,7 +9,6 @@ import { getRelativeUrl, readRouteSnapshot, recordNavigationOrigin, - subscribePendingRouteRestore, type RouteSnapshot, } from "@/lib/navigation/route-restoration"; import { @@ -83,17 +82,8 @@ export const RouteScrollReset = () => { const routerRef = useRef(router); routerRef.current = router; - const [isFreshNavigating, setIsFreshNavigating] = useState(false); - const [isRestoring, setIsRestoring] = useState(false); - const pendingDetailNavRef = useRef(false); const popNavigationRef = useRef(false); - useEffect(() => { - return subscribePendingRouteRestore((active) => { - if (active) setIsRestoring(true); - }); - }, []); - useEffect(() => { if ("scrollRestoration" in window.history) { window.history.scrollRestoration = "manual"; @@ -140,15 +130,10 @@ export const RouteScrollReset = () => { if (isMediaDetailPath(nextUrl.pathname)) { event.preventDefault(); - pendingDetailNavRef.current = true; - setIsFreshNavigating(true); // Keep the list page's scroll position intact for soft-back restore. // Detail pages scroll themselves to top on mount. - const href = `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`; - window.setTimeout(() => { - routerRef.current.push(href, { scroll: false }); - }, 50); + routerRef.current.push(href, { scroll: false }); return; } @@ -173,58 +158,21 @@ export const RouteScrollReset = () => { (popNavigationRef.current ? readRouteSnapshot(currentUrl) : null); popNavigationRef.current = false; - const shouldClearFreshNav = pendingDetailNavRef.current; - let cancelled = false; - const finishFreshNav = () => { - if (cancelled) return; - pendingDetailNavRef.current = false; - setIsFreshNavigating(false); - }; - if (snapshot) { - if (shouldClearFreshNav) finishFreshNav(); - setIsRestoring(true); - return restoreRouteSnapshot(snapshot, { - onLanded: () => { - if (!cancelled) setIsRestoring(false); - }, - }); + return restoreRouteSnapshot(snapshot); } - setIsRestoring(false); scrollToTop(); - // Soft-nav overlay must clear on detail arrival. Previously this returned - // early for media detail paths and left the spinner stuck forever. - if (shouldClearFreshNav) { - requestAnimationFrame(() => { - requestAnimationFrame(finishFreshNav); - }); - } - if (isMediaDetailPath(pathname)) { const stabilize = stabilizeScrollTop([0, 50, 150, 400]); return () => { - cancelled = true; stabilize.cancel(); }; } - - return () => { - cancelled = true; - }; }, [pathname]); - if (!isFreshNavigating && !isRestoring) return null; - - return ( -
-
-
- ); + return null; }; const RESTORE_ATTEMPT_MS = [ @@ -353,7 +301,7 @@ const restoreRouteSnapshot = ( if (!settled) return; - // Hold the overlay through one paint at the final position, then reveal. + // Let the browser paint once at the final restored position before cleanup. settleRafId = window.requestAnimationFrame(() => { settleRafId = window.requestAnimationFrame(() => { finishLanded(); diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 3d6bd0a4..a14f853c 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -58,6 +58,10 @@ const nextConfig = { key: "Cache-Control", value: "public, max-age=31536000, immutable", }; + const longLivedPublicAsset = { + key: "Cache-Control", + value: "public, max-age=86400, stale-while-revalidate=604800", + }; return [ { @@ -72,6 +76,10 @@ const nextConfig = { source: "/movie-banner.webp", headers: [immutablePublicAsset], }, + { + source: "/ascii-art-21st.webm", + headers: [longLivedPublicAsset], + }, { source: "/favicon.ico", headers: [immutablePublicAsset], From 23e1c612b60b32ccfdcbf048a29fb9acda085898 Mon Sep 17 00:00:00 2001 From: dotcomnerd <70242273+dotcomnerd@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:00:39 -0700 Subject: [PATCH 02/16] =?UTF-8?q?=F0=9F=93=BA=20watch-provider=20catalog?= =?UTF-8?q?=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/home/home-provider-rail.tsx | 51 ++++++ .../provider/provider-catalog-page.tsx | 173 ++++++++++++++++++ .../provider/provider-logo-mark.tsx | 46 +++++ apps/web/lib/watch-providers.ts | 151 +++++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 apps/web/components/home/home-provider-rail.tsx create mode 100644 apps/web/components/provider/provider-catalog-page.tsx create mode 100644 apps/web/components/provider/provider-logo-mark.tsx create mode 100644 apps/web/lib/watch-providers.ts diff --git a/apps/web/components/home/home-provider-rail.tsx b/apps/web/components/home/home-provider-rail.tsx new file mode 100644 index 00000000..03a201cc --- /dev/null +++ b/apps/web/components/home/home-provider-rail.tsx @@ -0,0 +1,51 @@ +import { ProviderLogoMark } from "@/components/provider/provider-logo-mark"; +import { + getWatchProviderBrands, + type WatchProviderBrand, +} from "@/lib/watch-providers"; +import Link from "next/link"; + +export async function HomeProviderRail() { + const homeProviders: WatchProviderBrand[] = await getWatchProviderBrands(); + + return ( +
+
+

+ Browse by Provider +

+
+ +
+ {homeProviders.map((provider) => ( + + + + {provider.name} + + + ))} +
+
+ ); +} diff --git a/apps/web/components/provider/provider-catalog-page.tsx b/apps/web/components/provider/provider-catalog-page.tsx new file mode 100644 index 00000000..aab75f54 --- /dev/null +++ b/apps/web/components/provider/provider-catalog-page.tsx @@ -0,0 +1,173 @@ +import { CatalogInfiniteGrid } from "@/components/catalog/catalog-infinite-grid"; +import { SilkShaderBackground } from "@/components/hero/silk-shader-background"; +import { ContentContainer } from "@/components/layout/content-container"; +import { PageContainer } from "@/components/layout/page-container"; +import { ProviderLogoMark } from "@/components/provider/provider-logo-mark"; +import { ScrollToTop } from "@/components/ui/scroll-to-top"; +import { cn } from "@/lib/utils"; +import type { WatchProviderBrand } from "@/lib/watch-providers"; +import { + getTodayIsoDateUtc, + filterReleasedMovies, + filterReleasedTvShows, +} from "@/lib/released-media"; +import { TMDB_WATCH_REGION } from "@/lib/constants"; +import { slimMediaItemsForRsc } from "@/lib/cards/catalog-dto"; +import type { MediaItem } from "@/lib/domain/typings"; +import { tmdb } from "@/tmdb/api"; +import Link from "next/link"; + +type ProviderMediaType = "movie" | "tv"; + +export type ProviderCatalogData = { + items: MediaItem[]; + currentPage: number; + totalPages: number; +}; + +export async function getProviderCatalog( + providerId: number, + mediaType: ProviderMediaType, +): Promise { + const provider = String(providerId); + const today = getTodayIsoDateUtc(); + + if (mediaType === "movie") { + const response = await tmdb.discover.movie({ + watch_region: TMDB_WATCH_REGION, + with_watch_providers: provider, + page: "1", + sort_by: "popularity.desc", + "primary_release_date.lte": today, + }); + const movies = filterReleasedMovies(response.results ?? []); + + return { + items: slimMediaItemsForRsc( + movies.map((movie) => ({ ...movie, media_type: "movie" as const })), + ), + currentPage: response.page, + totalPages: response.total_pages, + }; + } + + const response = await tmdb.discover.tv({ + watch_region: TMDB_WATCH_REGION, + with_watch_providers: provider, + page: "1", + sort_by: "popularity.desc", + "first_air_date.lte": today, + }); + const shows = filterReleasedTvShows(response.results ?? []); + + return { + items: slimMediaItemsForRsc( + shows.map((show) => ({ ...show, media_type: "tv" as const })), + ), + currentPage: response.page, + totalPages: response.total_pages, + }; +} + +const providerTypeHref = (providerId: number, mediaType: ProviderMediaType) => + `/providers/${providerId}?type=${mediaType}`; + +const switcherItemClass = (active: boolean) => + cn( + "flex h-10 min-w-24 items-center justify-center rounded-full px-5 text-sm font-semibold transition-colors sm:min-w-28", + active + ? "bg-white/95 text-black shadow-sm shadow-black/25" + : "text-white/65 hover:bg-white/10 hover:text-white", + ); + +function ProviderShaderBackdrop() { + return ( +

-
+
{collections.map((collection, index) => ( ); } -export async function HomeTrendingTvHeroes() { - const tvShows = await getHomeTrendingTv(); - - return ( - -
- -
-
- ); -} - export async function HomeTrendingTvCarousel() { const tvShows = await getHomeTrendingTv(); @@ -144,30 +91,14 @@ export async function HomeTrendingTvCarousel() { type="tv" title="Trending TV" link={pages.trending.tv.link} - items={toSlimCarouselItems(tvShows.slice(2))} + compact + bleed + items={toSlimCarouselItems(tvShows)} /> ); } -export async function HomePopularMovieHeroes() { - const popularMovies = await getHomePopularMovies(); - - return ( - -
- -
-
- ); -} - export async function HomePopularTvCarousel() { const popularTv = await getHomePopularTv(); @@ -177,26 +108,10 @@ export async function HomePopularTvCarousel() { type="tv" title="Popular TV" link={pages.tv.popular.link} - items={toSlimCarouselItems(popularTv.slice(2, 22))} + compact + bleed + items={toSlimCarouselItems(popularTv.slice(0, 22))} /> ); } - -export async function HomePopularTvHeroes() { - const popularTv = await getHomePopularTv(); - - return ( - -
- -
-
- ); -} diff --git a/apps/web/components/home/home-wide-carousel-row.tsx b/apps/web/components/home/home-wide-carousel-row.tsx index bdba788c..0f893557 100644 --- a/apps/web/components/home/home-wide-carousel-row.tsx +++ b/apps/web/components/home/home-wide-carousel-row.tsx @@ -1,19 +1,18 @@ "use client"; -import { Button, buttonVariants } from "@/components/ui/button"; import { Carousel, - CarouselApi, CarouselContent, CarouselItem, + CarouselNext, + CarouselPrevious, } from "@/components/ui/carousel"; -import { cn } from "@/lib/utils"; -import { ArrowLeft, ArrowRight } from "lucide-react"; +import { ChevronRight } from "lucide-react"; import Link from "next/link"; -import { useEffect, useState, type ReactNode } from "react"; +import { type ReactNode } from "react"; export const homeWideCarouselItemClassName = - "basis-[85%] pl-3 sm:basis-[70%] md:basis-[42%] lg:basis-[32%] xl:basis-[28%]"; + "pl-3 basis-[82%] sm:basis-[58%] md:basis-[42%] lg:pl-4 lg:basis-[26rem] xl:basis-[28rem] 2xl:basis-[30rem]"; type HomeWideCarouselRowProps = { ariaLabel: string; @@ -32,118 +31,51 @@ export function HomeWideCarouselRow({ title, description, actionHref, - actionLabel = "More", + actionLabel = "View all", action, items, getItemKey, renderItem, }: HomeWideCarouselRowProps) { - const [api, setApi] = useState(); - const [pageCount, setPageCount] = useState(0); - const [pageIndex, setPageIndex] = useState(0); - const [canScrollPrev, setCanScrollPrev] = useState(false); - const [canScrollNext, setCanScrollNext] = useState(false); - - useEffect(() => { - if (!api) return; - - const sync = () => { - const snaps = api.scrollSnapList(); - setPageCount(snaps.length); - setPageIndex(api.selectedScrollSnap()); - setCanScrollPrev(api.canScrollPrev()); - setCanScrollNext(api.canScrollNext()); - }; - - sync(); - api.on("select", sync); - api.on("reInit", sync); - - return () => { - api.off("select", sync); - api.off("reInit", sync); - }; - }, [api, items.length]); - return (
-
-
-

+
+
+

{title}

- {description ? ( -

- {description} -

- ) : null} + {description ?

{description}

: null}
{action ?? (actionHref ? ( - {actionLabel} + {actionLabel} + ) : null)} - -
- {pageCount > 0 ? ( -

- - {pageIndex + 1} - - / - {pageCount} -

- ) : null} - - - -
- + {items.map((item, index) => ( ({ ))} + + +

); diff --git a/apps/web/components/home/recently-watched-card.tsx b/apps/web/components/home/recently-watched-card.tsx index c2b788f1..45e52f9d 100644 --- a/apps/web/components/home/recently-watched-card.tsx +++ b/apps/web/components/home/recently-watched-card.tsx @@ -52,7 +52,7 @@ export function RecentlyWatchedCard({ return (
playHoverSound?.()} > diff --git a/apps/web/components/home/recently-watched-row.tsx b/apps/web/components/home/recently-watched-row.tsx index f120c45c..63856050 100644 --- a/apps/web/components/home/recently-watched-row.tsx +++ b/apps/web/components/home/recently-watched-row.tsx @@ -1,13 +1,14 @@ "use client"; import { RecentlyWatchedRowFallback } from "@/components/catalog/catalog-suspense-fallbacks"; +import { homeWideCarouselItemClassName } from "@/components/home/home-wide-carousel-row"; import { RecentlyWatchedCard } from "@/components/home/recently-watched-card"; -import { Button, buttonVariants } from "@/components/ui/button"; import { Carousel, - CarouselApi, CarouselContent, CarouselItem, + CarouselNext, + CarouselPrevious, } from "@/components/ui/carousel"; import { useContinueWatchingActions } from "@/hooks/use-continue-watching-actions"; import { usePersonalizedHome } from "@/hooks/use-personalized-home"; @@ -15,16 +16,19 @@ import { useRecentlyWatched } from "@/hooks/use-recently-watched"; import { continueWatchingTitleKey } from "@/lib/playback/continue-watching-dismiss"; import type { RecentlyWatchedScope } from "@/lib/playback/recently-watched"; import { cn } from "@/lib/utils"; -import { ArrowLeft, ArrowRight } from "lucide-react"; +import { ChevronRight } from "lucide-react"; import Link from "next/link"; import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; type RecentlyWatchedRowProps = { scope?: RecentlyWatchedScope; + bleed?: boolean; }; -export function RecentlyWatchedRow({ scope = "all" }: RecentlyWatchedRowProps) { +export function RecentlyWatchedRow({ + scope = "all", + bleed = false, +}: RecentlyWatchedRowProps) { const { status: sessionStatus } = useSession(); const personalized = usePersonalizedHome({ enabled: scope === "all" }); const continueWatchingActions = useContinueWatchingActions(); @@ -50,35 +54,9 @@ export function RecentlyWatchedRow({ scope = "all" }: RecentlyWatchedRowProps) { ? true : scopedRecentlyWatched.isLoading, }; - const [api, setApi] = useState(); - const [pageCount, setPageCount] = useState(0); - const [pageIndex, setPageIndex] = useState(0); - const [canScrollPrev, setCanScrollPrev] = useState(false); - const [canScrollNext, setCanScrollNext] = useState(false); - - useEffect(() => { - if (!api) return; - - const sync = () => { - const snaps = api.scrollSnapList(); - setPageCount(snaps.length); - setPageIndex(api.selectedScrollSnap()); - setCanScrollPrev(api.canScrollPrev()); - setCanScrollNext(api.canScrollNext()); - }; - - sync(); - api.on("select", sync); - api.on("reInit", sync); - - return () => { - api.off("select", sync); - api.off("reInit", sync); - }; - }, [api, items.length]); if (isLoading) { - return ; + return ; } if (items.length === 0) { @@ -88,19 +66,33 @@ export function RecentlyWatchedRow({ scope = "all" }: RecentlyWatchedRowProps) { return (
-
-
-

+
+
+

Continue Watching

@@ -108,66 +100,23 @@ export function RecentlyWatchedRow({ scope = "all" }: RecentlyWatchedRowProps) { {isSignedIn ? ( - Watchlist + Watchlist + ) : null} - -
- {pageCount > 0 ? ( -

- - {pageIndex + 1} - - / - {pageCount} -

- ) : null} - - - -
- + {items.map((item, index) => ( ))} + + +

); } export function HomeRecentlyWatched() { - return ; + return ; } diff --git a/apps/web/components/home/up-next-inbox-row.tsx b/apps/web/components/home/up-next-inbox-row.tsx index d09a6fed..a15d1574 100644 --- a/apps/web/components/home/up-next-inbox-row.tsx +++ b/apps/web/components/home/up-next-inbox-row.tsx @@ -14,7 +14,7 @@ export function UpNextInboxRow() { } if (isLoading) { - return ; + return ; } if (items.length === 0) { diff --git a/apps/web/components/watchlist/watchlist.tsx b/apps/web/components/watchlist/watchlist.tsx index c1d0cd3b..219bd3d0 100644 --- a/apps/web/components/watchlist/watchlist.tsx +++ b/apps/web/components/watchlist/watchlist.tsx @@ -1,6 +1,6 @@ "use client"; -import { getWatchlistItem } from "@/app/watchlist/actions"; +import { getWatchlistItem } from "@/app/(library)/watchlist/actions"; import type { EpisodeInfo } from "@/lib/domain/episodes"; import type { WatchlistItem, WatchlistStatus } from "@/lib/domain/watchlist"; import { MalListControls } from "@/components/media/mal-list-controls"; From 86a51e360e31e67a1d6a8f320c6e8cb34d4369df Mon Sep 17 00:00:00 2001 From: dotcomnerd <70242273+dotcomnerd@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:03:30 -0700 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=94=8E=20search=20trend=20peek=20an?= =?UTF-8?q?d=20detail=20surfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/components/media/media-client.tsx | 9 +- .../components/movie/movie-overview-tab.tsx | 6 - apps/web/components/movie/movie-server.tsx | 75 ++++-- apps/web/components/peek/media-peek-sheet.tsx | 2 +- apps/web/components/person/person-server.tsx | 41 ++- apps/web/components/search/search.tsx | 50 +++- .../components/trend/catalog-spotlight.tsx | 108 ++++++-- apps/web/components/trend/trend-client.tsx | 235 +++++++++++------- apps/web/components/trend/trend-server.tsx | 53 ++-- apps/web/components/tv/tv-server.tsx | 63 +++-- apps/web/lib/stores/episode-store.ts | 4 +- apps/web/lib/tv-detail-catalog.ts | 9 +- 12 files changed, 417 insertions(+), 238 deletions(-) diff --git a/apps/web/components/media/media-client.tsx b/apps/web/components/media/media-client.tsx index c18f1eae..06d0f1f3 100644 --- a/apps/web/components/media/media-client.tsx +++ b/apps/web/components/media/media-client.tsx @@ -325,7 +325,7 @@ export function CastCarousel({ cast }: CastCarouselProps) { return (
{youtubeVideos.slice(0, 10).map((video: Video) => ( - +