| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- "use client";
- import Link from "next/link";
- import type { Movie } from "@/types/movie";
- import { TMDB_GENRE_MAP, getTMDBImageUrl } from "@/types/tmdb";
- /**
- * <HomeRollTeaserCard /> — in-place result card for the home-page cross-list
- * roll. Renders IN PLACE per PROJECT_SCOPE.md:222-223; the only navigation
- * is the user-initiated "Open list" link.
- *
- * Uses the DB `Movie` row shape (not the TMDB landing teaser shape). Titles,
- * genres, and group names are rendered as React text children only — no
- * `dangerouslySetInnerHTML`, no unescaped `title=` attributes.
- */
- interface HomeRollTeaserCardProps {
- movie: Movie;
- groupId: string;
- groupName: string | null;
- onReroll: () => void;
- }
- function genreLabelsFromIds(ids: string[]): string[] {
- const labels: string[] = [];
- for (const raw of ids) {
- const id = Number.parseInt(raw, 10);
- if (Number.isNaN(id)) continue;
- const name = TMDB_GENRE_MAP[id];
- if (name) labels.push(name);
- }
- return labels;
- }
- export function HomeRollTeaserCard({
- movie,
- groupId,
- groupName,
- onReroll,
- }: HomeRollTeaserCardProps) {
- const posterUrl = getTMDBImageUrl(movie.poster_path, "panel");
- const genreLabels = genreLabelsFromIds(movie.genres ?? []);
- return (
- <div
- className="mt-4 rounded-xl border border-foreground/10 bg-foreground/5 p-4 sm:p-6"
- data-testid="home-roll-teaser-card"
- >
- <div className="flex flex-col gap-4 sm:flex-row">
- {posterUrl ? (
- // eslint-disable-next-line @next/next/no-img-element
- <img
- src={posterUrl}
- alt={movie.title}
- loading="lazy"
- className="w-32 h-48 rounded-lg object-cover self-center sm:self-start"
- />
- ) : (
- <div className="w-32 h-48 rounded-lg bg-foreground/10 flex items-center justify-center text-xs text-foreground/50 self-center sm:self-start">
- No poster
- </div>
- )}
- <div className="flex-1 min-w-0">
- <h2 className="text-lg sm:text-xl font-semibold text-foreground break-words">
- {movie.title}
- </h2>
- <p className="mt-1 text-sm text-foreground/60">{movie.year}</p>
- {genreLabels.length > 0 && (
- <div className="mt-2 flex flex-wrap gap-1.5">
- {genreLabels.map((label) => (
- <span
- key={label}
- className="rounded-full bg-foreground/10 px-2 py-0.5 text-xs text-foreground/70"
- >
- {label}
- </span>
- ))}
- </div>
- )}
- <p
- className="mt-3 text-xs text-foreground/60 truncate"
- style={{ maxWidth: "22rem" }}
- >
- from {groupName ?? "a list"}
- </p>
- <div className="mt-4 flex flex-wrap gap-2">
- <button
- type="button"
- onClick={onReroll}
- aria-label="Re-roll the dice"
- className="rounded-lg bg-foreground text-background px-4 py-2 text-sm font-medium hover:opacity-90 transition-opacity"
- style={{ minHeight: 44 }}
- >
- 🎲 Re-roll
- </button>
- <Link
- href={`/list/${groupId}`}
- className="rounded-lg border border-foreground/20 px-4 py-2 text-sm font-medium text-foreground hover:bg-foreground/5 transition-colors inline-flex items-center"
- style={{ minHeight: 44 }}
- >
- Open list
- </Link>
- </div>
- </div>
- </div>
- </div>
- );
- }
|