diff --git a/app/(pages)/titles/[id]/_components/title-availability.tsx b/app/(pages)/titles/[id]/_components/title-availability.tsx index d3b9d24..526d8ab 100644 --- a/app/(pages)/titles/[id]/_components/title-availability.tsx +++ b/app/(pages)/titles/[id]/_components/title-availability.tsx @@ -1,6 +1,11 @@ "use client"; import Image from "next/image"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Tooltip, TooltipContent, @@ -8,6 +13,8 @@ import { } from "@/components/ui/tooltip"; import type { AvailabilityOffer } from "@/lib/types/title"; +const MAX_VISIBLE = 4; + const offerLabels: Record = { flatrate: "Stream", rent: "Rent", @@ -59,6 +66,69 @@ function ProviderBadge({ ); } +function OverflowProviderIcon({ offer }: { offer: AvailabilityOffer }) { + return ( +
+ {offer.logoPath ? ( + {offer.providerName} + ) : ( + + {offer.providerName.slice(0, 2)} + + )} +
+ ); +} + +function OverflowBadge({ offers }: { offers: AvailabilityOffer[] }) { + return ( + + + +{offers.length} + + + {offers.map((offer) => + offer.watchUrl ? ( + + + + {offer.providerName} + + + ) : ( +
+ + + {offer.providerName} + +
+ ), + )} +
+
+ ); +} + export function TitleAvailability({ availability, }: { @@ -78,23 +148,29 @@ export function TitleAvailability({ Where to Watch
- {Object.entries(availByType).map(([type, offers]) => ( -
- - {offerLabels[type] ?? type} - -
- {offers.map((offer) => ( - - ))} + {Object.entries(availByType).map(([type, offers]) => { + const visible = offers.slice(0, MAX_VISIBLE); + const overflow = offers.slice(MAX_VISIBLE); + + return ( +
+ + {offerLabels[type] ?? type} + +
+ {visible.map((offer) => ( + + ))} + {overflow.length > 0 && } +
-
- ))} + ); + })}
); diff --git a/components/command-palette.tsx b/components/command-palette.tsx index ca1de96..792edc2 100644 --- a/components/command-palette.tsx +++ b/components/command-palette.tsx @@ -13,7 +13,7 @@ import { useHotkey, useHotkeySequence } from "@tanstack/react-hotkeys"; import { useAtom } from "jotai"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useProgress } from "@/components/navigation-progress"; import { Command, @@ -86,15 +86,22 @@ export function CommandPalette() { } }, [commandPaletteOpen]); - // Save to recent searches when results arrive + // Save to recent searches after user stops typing for a while + const saveTimerRef = useRef>(null); useEffect(() => { + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); const trimmed = debouncedQuery.trim(); if (trimmed && results.length > 0) { - setRecentSearches((prev) => { - const filtered = prev.filter((q) => q !== trimmed); - return [trimmed, ...filtered].slice(0, MAX_RECENT); - }); + saveTimerRef.current = setTimeout(() => { + setRecentSearches((prev) => { + const filtered = prev.filter((q) => q !== trimmed); + return [trimmed, ...filtered].slice(0, MAX_RECENT); + }); + }, 2000); } + return () => { + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + }; }, [debouncedQuery, results, setRecentSearches]); const handleSelect = useCallback(