import { Link } from "react-router"; import { Check, Film, Library, Loader2, Plus, Tv } from "lucide-react"; import type { MediaRequest, RequestMediaResult } from "@/api/types"; import { cn } from "@/lib/utils"; import { formatRequestReason, formatRequestStatus, tmdbImageURL } from "@/lib/mediaRequests"; const POSTER_WIDTH = "w-[148px] sm:w-[164px] lg:w-[184px]"; type DiscoverProps = { variant: "discover"; item: RequestMediaResult; /** Called when the inline hover Request button is clicked. Omit to suppress the button. */ onRequest?: () => void; /** Displays the spinner state on the hover Request button. Ignored when onRequest is omitted. */ isSubmitting?: boolean; /** When true, fills the parent (use inside grids). Default: fixed carousel width. */ fluid?: boolean; }; type MineProps = { variant: "mine"; request: MediaRequest; fluid?: boolean; }; export type RequestPosterCardProps = DiscoverProps | MineProps; export default function RequestPosterCard(props: RequestPosterCardProps) { if (props.variant === "mine") { return ; } return ( ); } function DiscoverCard({ item, isSubmitting, onRequest, fluid, }: { item: RequestMediaResult; isSubmitting?: boolean; onRequest?: () => void; fluid?: boolean; }) { const poster = tmdbImageURL(item.poster_path); const requestable = item.request.requestable; const statusLabel = item.request.status ? formatRequestStatus(item.request.status) : null; const reasonLabel = !requestable && !item.request.status ? formatRequestReason(item.request.reason) : null; const availableInLibrary = item.availability === "available" && !item.request.status; const ribbon: { kind: RibbonKind; label: string } | null = statusLabel ? { kind: (item.request.status as RibbonKind) ?? "pending", label: statusLabel } : availableInLibrary ? { kind: "completed", label: "In library" } : reasonLabel ? { kind: "blocked", label: reasonLabel } : null; return (
{ribbon && ( )} {requestable && onRequest && (
)} {item.library_content_id ? ( ) : null}
); } function MineCard({ request, fluid }: { request: MediaRequest; fluid?: boolean }) { const poster = tmdbImageURL(request.poster_path); const isCompleted = request.status === "completed"; const isFailed = request.outcome === "failed" || request.outcome === "declined" || request.outcome === "cancelled"; const kind: RibbonKind = isFailed ? "blocked" : (request.status as RibbonKind); const label = isFailed ? formatOutcome(request.outcome) : formatRequestStatus(request.status); return (
{isCompleted && (
Ready to watch
)}
{request.last_error ? (

{request.last_error}

) : null} {request.library_content_id ? ( ) : null}
); } function LibraryCardLink({ contentID, title }: { contentID: string; title: string }) { return ( Library ); } function PosterFrame({ poster, title, mediaType, dim, children, }: { poster: string | null; title: string; mediaType: "movie" | "series"; dim?: boolean; children?: React.ReactNode; }) { return (
{poster ? ( {title ) : ( )} {/* subtle bottom vignette for legibility behind ribbons / hover overlays */}
{children}
); } function PosterFallback({ title, mediaType, dim, }: { title: string; mediaType: "movie" | "series"; dim?: boolean; }) { const hue = stringHue(title); const Icon = mediaType === "series" ? Tv : Film; return (
{mediaType === "series" ? "Series" : "Motion picture"}

{title}

); } function stringHue(input: string): number { let hash = 0; for (let i = 0; i < input.length; i++) { hash = (Math.imul(hash, 31) + input.charCodeAt(i)) | 0; } return Math.abs(hash) % 360; } function CardMeta({ title, year, rating, mediaType, }: { title: string; year?: number; rating?: number; mediaType?: "movie" | "series"; }) { const Icon = mediaType === "series" ? Tv : Film; const hasMeta = mediaType || year !== undefined || rating !== undefined; return (

{title}

{hasMeta && (
{mediaType && ( )} {year ? {year} : null} {(year || mediaType) && rating ? ( · ) : null} {rating ? ( ★ {rating.toFixed(1)} ) : null}
)}
); } type RibbonKind = "pending" | "approved" | "queued" | "downloading" | "completed" | "blocked"; const RIBBON_STYLES: Record = { pending: "bg-amber-950/75 text-amber-100 ring-amber-400/30 [&_.dot]:bg-amber-300 [&_.dot]:animate-pulse", approved: "bg-emerald-950/75 text-emerald-100 ring-emerald-400/30 [&_.dot]:bg-emerald-300", queued: "bg-sky-950/75 text-sky-100 ring-sky-400/30 [&_.dot]:bg-sky-300 [&_.dot]:animate-pulse", downloading: "bg-sky-950/80 text-sky-100 ring-sky-400/35 [&_.dot]:bg-sky-300 [&_.dot]:animate-pulse", completed: "bg-emerald-950/80 text-emerald-100 ring-emerald-400/30 [&_.dot]:bg-emerald-300", blocked: "bg-zinc-900/80 text-zinc-200 ring-white/10 [&_.dot]:bg-zinc-400", }; function StatusRibbon({ status, label, reserveLibrarySpace, }: { status: string; label: string; reserveLibrarySpace?: boolean; }) { const kind = (RIBBON_STYLES[status as RibbonKind] ? status : "blocked") as RibbonKind; return ( {label} ); } function formatOutcome(outcome: MediaRequest["outcome"]): string { switch (outcome) { case "declined": return "Declined"; case "cancelled": return "Cancelled"; case "failed": return "Failed"; default: return "Active"; } }