Files
sofa/app/(pages)/explore/page.tsx
T
jakeandClaude Opus 4.6 ee723e41d6 Add TMDB image caching pipeline and resolve image URLs server-side
Introduce a local disk cache for TMDB images served through a proxy API
route (/api/images/[...path]), eliminating direct client-side CDN
dependencies. Images are cached by category (posters, backdrops, stills,
logos) and served with immutable cache headers.

Move all tmdbImageUrl() calls from client components to API routes and
server components so clients receive ready-to-use URLs. This removes the
need to expose TMDB_IMAGE_BASE_URL and IMAGE_CACHE_ENABLED via
next.config.ts env block. The landing page is split into a server
wrapper (app/page.tsx) and client component (components/landing-page.tsx).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 13:10:17 -05:00

92 lines
2.6 KiB
TypeScript

import { IconDeviceTv, IconFlame, IconMovie } from "@tabler/icons-react";
import { getGenres, getPopular, getTrending } from "@/lib/tmdb/client";
import { tmdbImageUrl } from "@/lib/tmdb/image";
import { GenreBrowser } from "./genre-browser";
import { HeroBanner } from "./hero-banner";
import { TitleRow } from "./title-row";
function mapResults(
results: {
id: number;
media_type?: string;
title?: string;
name?: string;
poster_path: string | null;
release_date?: string;
first_air_date?: string;
vote_average: number;
}[],
fallbackType: "movie" | "tv",
) {
return results
.filter((r) => r.poster_path)
.map((r) => ({
tmdbId: r.id,
type: (r.media_type === "movie" || r.media_type === "tv"
? r.media_type
: fallbackType) as "movie" | "tv",
title: r.title ?? r.name ?? "",
posterPath: tmdbImageUrl(r.poster_path, "w500"),
releaseDate: r.release_date ?? r.first_air_date ?? null,
voteAverage: r.vote_average,
}));
}
export default async function ExplorePage() {
const [trending, popularMovies, popularTv, movieGenres, tvGenres] =
await Promise.all([
getTrending("all", "day"),
getPopular("movie"),
getPopular("tv"),
getGenres("movie"),
getGenres("tv"),
]);
const trendingItems = mapResults(trending.results, "movie");
const popularMovieItems = mapResults(popularMovies.results, "movie");
const popularTvItems = mapResults(popularTv.results, "tv");
const heroTitle = trending.results.find(
(r) =>
r.backdrop_path && (r.media_type === "movie" || r.media_type === "tv"),
);
return (
<div className="space-y-10">
{heroTitle && (
<HeroBanner
tmdbId={heroTitle.id}
type={heroTitle.media_type as "movie" | "tv"}
title={heroTitle.title ?? heroTitle.name ?? ""}
overview={heroTitle.overview}
backdropPath={tmdbImageUrl(heroTitle.backdrop_path, "w1280")}
voteAverage={heroTitle.vote_average}
/>
)}
<TitleRow
heading="Trending Today"
icon={<IconFlame size={20} className="text-primary" />}
items={trendingItems.slice(0, 20)}
/>
<TitleRow
heading="Popular Movies"
icon={<IconMovie size={20} className="text-primary" />}
items={popularMovieItems.slice(0, 20)}
/>
<TitleRow
heading="Popular TV Shows"
icon={<IconDeviceTv size={20} className="text-primary" />}
items={popularTvItems.slice(0, 20)}
/>
<GenreBrowser
movieGenres={movieGenres.genres}
tvGenres={tvGenres.genres}
/>
</div>
);
}