Convert dashboard and title pages to server components with granular Suspense

Replace fully client-side dashboard and title detail pages with server
component orchestrators that fetch data directly via service functions,
eliminating extra network round-trips through API routes. Each section
streams independently through its own Suspense boundary.

- Extract getUserStats(), getTitleWithChildren(), getRecommendationsForTitle()
  into service layer; update getNewAvailableFeed() to include tmdbId/voteAverage
- Split dashboard into server sections (stats, continue watching, library,
  recommendations) with client children for animations
- Split title page into server hero + client interaction provider with shared
  context for optimistic mutations across actions and seasons
- Add generateMetadata with OG tags, server-side TMDB ID resolution via
  redirect(), loading.tsx and not-found.tsx for both pages
- Add per-section skeleton components, fix ContinueWatchingSkeleton dimensions
- Remove 6 unused API routes (feed/*, titles/[id] GET, titles/[id]/recommendations)
- Remove components/stats-summary.tsx, add lib/types/title.ts for shared types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 15:42:42 -05:00
co-authored by Claude Opus 4.6
parent 081f53beac
commit c1867a4f23
33 changed files with 1804 additions and 1654 deletions
@@ -0,0 +1,32 @@
import { IconSparkles } from "@tabler/icons-react";
import { getRecommendationsFeed } from "@/lib/services/discovery";
import { tmdbImageUrl } from "@/lib/tmdb/image";
import { FeedSection } from "./feed-section";
import { TitleGrid } from "./title-grid";
export async function RecommendationsSection({ userId }: { userId: string }) {
const feed = await getRecommendationsFeed(userId);
if (feed.length === 0) return null;
const items = feed
.filter((item) => !!item)
.slice(0, 10)
.map((t) => ({
id: t.id,
tmdbId: t.tmdbId,
type: t.type,
title: t.title,
posterPath: tmdbImageUrl(t.posterPath, "w500"),
releaseDate: t.releaseDate ?? t.firstAirDate,
voteAverage: t.voteAverage,
}));
return (
<FeedSection
title="Recommended for You"
icon={<IconSparkles size={20} className="text-primary" />}
>
<TitleGrid items={items} />
</FeedSection>
);
}