From 8d4628bb940c58f5e32be2d21298912e564c9bdf Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Tue, 3 Mar 2026 19:13:00 -0500 Subject: [PATCH] Move star-rating and status-button to title detail co-location, remove unused search components Co-locate `StarRating` and `StatusButton` under the title detail `_components/` directory since they're only used there. Delete the unused `SearchAutocomplete` and `SearchBar` components from the shared `components/` root. --- .../titles/[id]/_components}/star-rating.tsx | 0 .../[id]/_components}/status-button.tsx | 0 .../titles/[id]/_components/title-actions.tsx | 4 +- components/search-autocomplete.tsx | 208 ------------------ components/search-bar.tsx | 48 ---- 5 files changed, 2 insertions(+), 258 deletions(-) rename {components => app/(pages)/titles/[id]/_components}/star-rating.tsx (100%) rename {components => app/(pages)/titles/[id]/_components}/status-button.tsx (100%) delete mode 100644 components/search-autocomplete.tsx delete mode 100644 components/search-bar.tsx diff --git a/components/star-rating.tsx b/app/(pages)/titles/[id]/_components/star-rating.tsx similarity index 100% rename from components/star-rating.tsx rename to app/(pages)/titles/[id]/_components/star-rating.tsx diff --git a/components/status-button.tsx b/app/(pages)/titles/[id]/_components/status-button.tsx similarity index 100% rename from components/status-button.tsx rename to app/(pages)/titles/[id]/_components/status-button.tsx diff --git a/app/(pages)/titles/[id]/_components/title-actions.tsx b/app/(pages)/titles/[id]/_components/title-actions.tsx index ecdee6b..4497218 100644 --- a/app/(pages)/titles/[id]/_components/title-actions.tsx +++ b/app/(pages)/titles/[id]/_components/title-actions.tsx @@ -2,13 +2,13 @@ import { IconPlayerPlay } from "@tabler/icons-react"; import { useAtomValue } from "jotai"; -import { StarRating } from "@/components/star-rating"; -import { StatusButton } from "@/components/status-button"; import { titleTypeAtom, userRatingAtom, userStatusAtom, } from "@/lib/atoms/title"; +import { StarRating } from "./star-rating"; +import { StatusButton } from "./status-button"; import { useTitleActions } from "./use-title-actions"; export function TitleActions() { diff --git a/components/search-autocomplete.tsx b/components/search-autocomplete.tsx deleted file mode 100644 index 0795ed7..0000000 --- a/components/search-autocomplete.tsx +++ /dev/null @@ -1,208 +0,0 @@ -"use client"; - -import { - IconDeviceTv, - IconMovie, - IconSearch, - IconStarFilled, -} from "@tabler/icons-react"; -import { Command as CommandPrimitive } from "cmdk"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import { useDebounce } from "@/hooks/use-debounce"; - -interface SearchResult { - tmdbId: number; - type: "movie" | "tv"; - title: string; - overview: string; - releaseDate: string | null; - posterPath: string | null; - popularity: number; - voteAverage: number; -} - -interface SearchAutocompleteProps { - onResults?: (results: SearchResult[]) => void; - onLoading?: (loading: boolean) => void; -} - -export function SearchAutocomplete({ - onResults, - onLoading, -}: SearchAutocompleteProps) { - const router = useRouter(); - const [query, setQuery] = useState(""); - const [results, setResults] = useState([]); - const [loading, setLoading] = useState(false); - const [open, setOpen] = useState(false); - const [filter, setFilter] = useState<"all" | "movie" | "tv">("all"); - const debouncedQuery = useDebounce(query, 300); - const inputRef = useRef(null); - - useEffect(() => { - if (!debouncedQuery.trim()) { - setResults([]); - onResults?.([]); - return; - } - let cancelled = false; - setLoading(true); - onLoading?.(true); - const typeParam = filter !== "all" ? `&type=${filter}` : ""; - fetch(`/api/search?query=${encodeURIComponent(debouncedQuery)}${typeParam}`) - .then((r) => r.json()) - .then((data) => { - if (!cancelled) { - if (data.code === "TMDB_NOT_CONFIGURED") { - toast.error("TMDB API key not configured", { - description: "Visit /setup for instructions.", - action: { - label: "Setup", - onClick: () => (window.location.href = "/setup"), - }, - }); - return; - } - const res = data.results ?? []; - setResults(res); - onResults?.(res); - setOpen(res.length > 0); - } - }) - .finally(() => { - if (!cancelled) { - setLoading(false); - onLoading?.(false); - } - }); - return () => { - cancelled = true; - }; - }, [debouncedQuery, filter, onResults, onLoading]); - - const handleSelect = useCallback( - (result: SearchResult) => { - router.push(`/titles/tmdb-${result.tmdbId}-${result.type}`); - }, - [router], - ); - - return ( -
- {/* Filter tabs */} -
- {(["all", "movie", "tv"] as const).map((t) => ( - - ))} -
- -
- -
- - { - setQuery(val); - if (val.trim()) setOpen(true); - }} - onFocus={() => { - if (results.length > 0) setOpen(true); - }} - onBlur={() => { - // Delay to allow click on result - setTimeout(() => setOpen(false), 200); - }} - className="flex h-13 w-full rounded-xl border border-border/50 bg-card/50 pl-11 pr-4 text-base backdrop-blur-sm transition-all placeholder:text-muted-foreground/50 focus:border-primary/40 focus:outline-none focus:ring-1 focus:ring-primary/20" - /> - {loading && ( -
-
-
- )} -
- - {open && results.length > 0 && ( - - {results.map((r) => ( - handleSelect(r)} - className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-sm data-[selected=true]:bg-accent" - > -
- {r.posterPath ? ( - {r.title} - ) : ( -
- ? -
- )} -
-
-
-

{r.title}

- {r.type === "movie" ? ( - - ) : ( - - )} -
-
- {r.releaseDate && ( - {r.releaseDate.slice(0, 4)} - )} - {r.voteAverage > 0 && ( - - - {r.voteAverage.toFixed(1)} - - )} -
- {r.overview && ( -

- {r.overview} -

- )} -
-
- ))} -
- )} - -
-
- ); -} diff --git a/components/search-bar.tsx b/components/search-bar.tsx deleted file mode 100644 index 2033173..0000000 --- a/components/search-bar.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { IconSearch } from "@tabler/icons-react"; -import { useEffect, useRef, useState } from "react"; - -interface SearchBarProps { - onSearch: (query: string) => void; - placeholder?: string; - defaultValue?: string; -} - -export function SearchBar({ - onSearch, - placeholder = "Search movies & TV shows...", - defaultValue = "", -}: SearchBarProps) { - const [value, setValue] = useState(defaultValue); - const timerRef = useRef>(null); - - useEffect(() => { - if (timerRef.current) clearTimeout(timerRef.current); - if (!value.trim()) return; - - timerRef.current = setTimeout(() => { - onSearch(value.trim()); - }, 400); - - return () => { - if (timerRef.current) clearTimeout(timerRef.current); - }; - }, [value, onSearch]); - - return ( -
- - setValue(e.target.value)} - placeholder={placeholder} - className="flex h-13 w-full rounded-xl border border-border/50 bg-card/50 pl-11 pr-4 text-base backdrop-blur-sm transition-all placeholder:text-muted-foreground/50 focus:border-primary/40 focus:outline-none focus:ring-1 focus:ring-primary/20" - /> -
- ); -}