mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 02:45:39 -04:00
Replace tmdb-* URL routing with resolveTitle/resolvePerson server actions
- Add `resolveTitle` and `resolvePerson` server actions that import
from TMDB and return the internal DB id
- Remove `tmdb-{id}-{type}` URL pattern and server-side redirect logic
from TitleDetailPage and PersonDetailPage
- Rename `importTitle` → `getOrFetchTitleByTmdbId`; add `getOrFetchTitle`
combining fetch + children lookup
- Update HeroBanner, TitleCard, and CommandPalette to call resolve
actions client-side before pushing to router
- Batch availability offer inserts into a single transaction
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
|
||||
import { IconPlus, IconStar } from "@tabler/icons-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
import { useProgress } from "@/components/navigation-progress";
|
||||
import { resolveTitle } from "@/lib/actions/titles";
|
||||
|
||||
interface HeroBannerProps {
|
||||
tmdbId: number;
|
||||
@@ -21,7 +24,18 @@ export function HeroBanner({
|
||||
backdropPath,
|
||||
voteAverage,
|
||||
}: HeroBannerProps) {
|
||||
const href = `/titles/tmdb-${tmdbId}-${type}`;
|
||||
const router = useRouter();
|
||||
const progress = useProgress();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
function handleNavigate() {
|
||||
if (isPending) return;
|
||||
progress.start();
|
||||
startTransition(async () => {
|
||||
const id = await resolveTitle(tmdbId, type);
|
||||
if (id) router.push(`/titles/${id}`);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative -mt-6 mr-[calc(-50vw+50%)] mb-4 ml-[calc(-50vw+50%)] animate-stagger-item overflow-hidden">
|
||||
@@ -67,21 +81,28 @@ export function HeroBanner({
|
||||
Trending today
|
||||
</span>
|
||||
</div>
|
||||
<Link href={href} className="group/title">
|
||||
<button
|
||||
type="button"
|
||||
className="group/title cursor-pointer text-left"
|
||||
onClick={handleNavigate}
|
||||
disabled={isPending}
|
||||
>
|
||||
<h2 className="text-balance font-display text-3xl tracking-tight transition-colors group-hover/title:text-primary sm:text-4xl">
|
||||
{title}
|
||||
</h2>
|
||||
</Link>
|
||||
</button>
|
||||
<p className="mt-2 line-clamp-2 max-w-2xl text-muted-foreground text-sm">
|
||||
{overview}
|
||||
</p>
|
||||
<Link
|
||||
href={href}
|
||||
className="mt-4 inline-flex h-9 items-center gap-2 rounded-lg bg-primary px-4 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-md hover:shadow-primary/20"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNavigate}
|
||||
disabled={isPending}
|
||||
className="mt-4 inline-flex h-9 cursor-pointer items-center gap-2 rounded-lg bg-primary px-4 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-md hover:shadow-primary/20 disabled:opacity-70"
|
||||
>
|
||||
<IconPlus aria-hidden={true} className="size-4" />
|
||||
Add to Library
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,25 +4,17 @@ import { notFound } from "next/navigation";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { persons } from "@/lib/db/schema";
|
||||
import {
|
||||
getLocalFilmography,
|
||||
getOrFetchPerson,
|
||||
getOrFetchPersonByTmdbId,
|
||||
} from "@/lib/services/person";
|
||||
import { getLocalFilmography, getOrFetchPerson } from "@/lib/services/person";
|
||||
import { getUserStatusesByTitleIds } from "@/lib/services/tracking";
|
||||
import { FilmographyGrid } from "./_components/filmography-grid";
|
||||
import { PersonHero } from "./_components/person-hero";
|
||||
|
||||
const TMDB_PATTERN = /^tmdb-(\d+)$/;
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
if (TMDB_PATTERN.test(id)) return { title: "Sofa" };
|
||||
|
||||
const person = db.select().from(persons).where(eq(persons.id, id)).get();
|
||||
if (!person) return { title: "Not Found — Sofa" };
|
||||
|
||||
@@ -38,13 +30,8 @@ export default async function PersonDetailPage({
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
await getSession();
|
||||
|
||||
const tmdbMatch = TMDB_PATTERN.exec(id);
|
||||
const person = tmdbMatch
|
||||
? await getOrFetchPersonByTmdbId(Number(tmdbMatch[1]))
|
||||
: await getOrFetchPerson(id);
|
||||
|
||||
const person = await getOrFetchPerson(id);
|
||||
if (!person) notFound();
|
||||
|
||||
const filmography = getLocalFilmography(person.id);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
RecommendationsSkeleton,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { titles } from "@/lib/db/schema";
|
||||
import { getTitleWithChildren, importTitle } from "@/lib/services/metadata";
|
||||
import { getOrFetchTitle } from "@/lib/services/metadata";
|
||||
import { getUserTitleInfo } from "@/lib/services/tracking";
|
||||
import { tmdbImageUrl } from "@/lib/tmdb/image";
|
||||
import { getTitleThemeStyle } from "@/lib/utils/title-theme";
|
||||
@@ -23,16 +23,12 @@ import { TitleProvider } from "./_components/title-provider";
|
||||
import { TitleRecommendations } from "./_components/title-recommendations";
|
||||
import { TitleSeasons } from "./_components/title-seasons";
|
||||
|
||||
const TMDB_ID_PATTERN = /^tmdb-(\d+)-(movie|tv)$/;
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
if (TMDB_ID_PATTERN.test(id)) return { title: "Sofa" };
|
||||
|
||||
const title = db.select().from(titles).where(eq(titles.id, id)).get();
|
||||
if (!title) return { title: "Not Found — Sofa" };
|
||||
|
||||
@@ -56,21 +52,10 @@ export default async function TitleDetailPage({
|
||||
}) {
|
||||
const { id } = await params;
|
||||
|
||||
// TMDB ID resolution: tmdb-{id}-{type} → import + redirect
|
||||
const tmdbMatch = TMDB_ID_PATTERN.exec(id);
|
||||
if (tmdbMatch) {
|
||||
const title = await importTitle(
|
||||
Number(tmdbMatch[1]),
|
||||
tmdbMatch[2] as "movie" | "tv",
|
||||
);
|
||||
if (!title) notFound();
|
||||
redirect(`/titles/${title.id}`);
|
||||
}
|
||||
|
||||
// Fetch title + user info in parallel
|
||||
const session = await getSession();
|
||||
const [result, userInfo] = await Promise.all([
|
||||
getTitleWithChildren(id),
|
||||
getOrFetchTitle(id),
|
||||
session ? getUserTitleInfo(session.user.id, id) : null,
|
||||
]);
|
||||
if (!result) notFound();
|
||||
|
||||
Reference in New Issue
Block a user