import type { JellyItem } from './shared'; import { Link, useLocation, useSearchParams } from 'react-router'; import { Check, Play } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { episodeLabel, itemHref, posterUrl, progressPercent, subtitleFor, thumbUrl } from './shared'; // One tile in a shelf or a grid. // // It is a real to `?item=` — cmd-click opens the detail in a new tab, the URL says what is open, // and the back button closes it. The play button is a SIBLING anchor rather than nested inside, because an // anchor inside an anchor is not valid HTML and browsers resolve it by dropping one of them. type MediaCardProps = { item: JellyItem; wide?: boolean }; export const MediaCard = ({ item, wide = false }: MediaCardProps) => { const { token } = useClient(); const { pathname } = useLocation(); const [params] = useSearchParams(); const src = wide ? thumbUrl(item, token) : posterUrl(item, token); const progress = progressPercent(item); const watched = item.UserData?.Played === true; const unplayed = item.UserData?.UnplayedItemCount ?? 0; const subtitle = subtitleFor(item); return (
{src ? ( ) : (
{item.Name}
)} {watched && ( )} {!watched && unplayed > 0 && ( {unplayed} )} {progress > 0 && progress < 100 && ( )}

{episodeLabel(item)}

{subtitle &&

{subtitle}

}
); }; /** A horizontal shelf. Overflow scrolls rather than wrapping — a shelf that wraps is a grid with a title. */ export const MediaShelf = ({ title, items, wide }: { title: string; items: JellyItem[]; wide?: boolean }) => { if (!items.length) return null; return (

{title}

{items.map((item) => (
))}
); }; /** A wrapping grid. Column count comes from a min tile width, so it works in a panel of any size. */ export const MediaGrid = ({ items, wide }: { items: JellyItem[]; wide?: boolean }) => (
{items.map((item) => ( ))}
);