153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { SearchBar } from "@/components/SearchBar";
|
|
import { MovieHeader } from "@/components/MovieHeader";
|
|
import { MovieInsightPanel } from "@/components/MovieInsightPanel";
|
|
import type { TmdbMovieDetails, TmdbSearchResult } from "@/lib/tmdb";
|
|
|
|
export default function HomePage() {
|
|
const [selectedMovie, setSelectedMovie] = useState<TmdbSearchResult | null>(
|
|
null,
|
|
);
|
|
const [movieDetails, setMovieDetails] = useState<TmdbMovieDetails | null>(
|
|
null,
|
|
);
|
|
const [isLoadingMovie, setIsLoadingMovie] = useState(false);
|
|
const [movieError, setMovieError] = useState<string | null>(null);
|
|
|
|
// Preload a random interesting movie on initial page load to demonstrate the app
|
|
useEffect(() => {
|
|
const DEMO_IDS = [27205, 157336, 155, 550, 496243, 424]; // Inception, Interstellar, Dark Knight, Fight Club, Parasite, Schindler's List
|
|
const loadDemo = async () => {
|
|
const id = DEMO_IDS[Math.floor(Math.random() * DEMO_IDS.length)];
|
|
try {
|
|
const res = await fetch(`/api/movie/${id}`);
|
|
if (!res.ok) return;
|
|
const details: TmdbMovieDetails = await res.json();
|
|
const minimalSearch: TmdbSearchResult = {
|
|
id: details.id,
|
|
title: details.title,
|
|
release_date: details.release_date || "",
|
|
poster_path: details.poster_path,
|
|
overview: details.overview || "",
|
|
};
|
|
setSelectedMovie(minimalSearch);
|
|
setMovieDetails(details);
|
|
} catch {
|
|
// silently fall back to empty state if demo preload fails
|
|
}
|
|
};
|
|
if (!selectedMovie) {
|
|
void loadDemo();
|
|
}
|
|
}, []);
|
|
|
|
const handleSelect = async (movie: TmdbSearchResult) => {
|
|
setSelectedMovie(movie);
|
|
setMovieDetails(null);
|
|
setMovieError(null);
|
|
setIsLoadingMovie(true);
|
|
|
|
try {
|
|
const res = await fetch(`/api/movie/${movie.id}`);
|
|
if (!res.ok) {
|
|
throw new Error("Failed to load movie details");
|
|
}
|
|
const details: TmdbMovieDetails = await res.json();
|
|
setMovieDetails(details);
|
|
} catch (err) {
|
|
setMovieError(
|
|
err instanceof Error ? err.message : "Failed to load movie details",
|
|
);
|
|
} finally {
|
|
setIsLoadingMovie(false);
|
|
}
|
|
};
|
|
|
|
const hasFullMovie = movieDetails != null;
|
|
|
|
return (
|
|
<>
|
|
{/* Compact navbar: title + slogan on left, search on right */}
|
|
<nav className="sticky top-0 z-50 border-b border-stone-200 bg-white">
|
|
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between gap-4 px-6">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<span className="font-semibold tracking-tight text-stone-950">
|
|
Film Intel
|
|
</span>
|
|
<span className="hidden truncate text-xs text-stone-500 sm:inline">
|
|
Movie insight you won't find in one click
|
|
</span>
|
|
</div>
|
|
|
|
<div className="w-full max-w-[320px]">
|
|
<SearchBar
|
|
autoFocus={!selectedMovie}
|
|
onSelect={handleSelect}
|
|
compact
|
|
/>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="mx-auto min-h-[calc(100vh-3.5rem)] w-full max-w-6xl px-6">
|
|
{!selectedMovie && (
|
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
<p className="max-w-md text-sm text-stone-600">
|
|
Search for any film using the bar above. We'll generate
|
|
ratings explained, skip guides, sensitivity warnings, production
|
|
facts, and more.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedMovie && (
|
|
<div className="space-y-8 py-8">
|
|
{hasFullMovie ? (
|
|
<MovieHeader movie={movieDetails} showBack={false} />
|
|
) : (
|
|
<div className="rounded-2xl border border-stone-200 bg-white p-6 shadow-sm">
|
|
<div className="space-y-2">
|
|
<h2 className="text-2xl font-semibold text-stone-950">
|
|
{selectedMovie.title}
|
|
{selectedMovie.release_date ? (
|
|
<span className="ml-2 font-normal text-stone-500">
|
|
({selectedMovie.release_date.slice(0, 4)})
|
|
</span>
|
|
) : null}
|
|
</h2>
|
|
{selectedMovie.overview && (
|
|
<p className="text-sm leading-relaxed text-stone-600">
|
|
{selectedMovie.overview}
|
|
</p>
|
|
)}
|
|
{isLoadingMovie && (
|
|
<p className="text-sm text-amber-600">
|
|
Loading extended details...
|
|
</p>
|
|
)}
|
|
{movieError && (
|
|
<p className="text-sm text-red-600">
|
|
{movieError}. Card content will still load.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<MovieInsightPanel
|
|
movieId={selectedMovie.id}
|
|
genres={movieDetails?.genres.map((g) => g.name) ?? []}
|
|
/>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
<footer className="border-t border-stone-200 py-6 text-center text-xs text-stone-500">
|
|
AI-generated content for informational purposes. Verify timestamps on
|
|
your copy.
|
|
</footer>
|
|
</>
|
|
);
|
|
} |