"use client"; import { useCallback, useMemo, useState } from "react"; import { SearchBar } from "./search-bar"; import { SearchResults } from "./search-results"; import { PosterGrid } from "./poster-grid"; import { useGroupMovies } from "@/hooks/use-group-movies"; import { useMovieSearch } from "@/hooks/use-movie-search"; import { useAddMovie } from "@/hooks/use-add-movie"; import { useRealtimeMovies } from "@/hooks/use-realtime-movies"; import { useRoll } from "@/hooks/use-roll"; import { useToggleWatched } from "@/hooks/use-toggle-watched"; import { RollBar } from "@/components/dice/roll-bar"; import { RollAnnouncer } from "@/components/dice/roll-announcer"; import { ListRollCarousel } from "@/components/dice/list-roll-carousel"; import { GenreRollModal } from "@/components/dice/genre-roll-modal"; import { filterByGenresAndEmotionsStructured } from "@/lib/dice/genre-filter"; import type { Database } from "@/types/database"; type MovieRow = Database["public"]["Tables"]["movies"]["Row"]; interface MemberInfo { id: string; displayName: string; avatarColor: string | null; } interface MovieListClientProps { groupId: string; groupName: string; members: MemberInfo[]; } export function MovieListClient({ groupId, members }: MovieListClientProps) { const [searchQuery, setSearchQuery] = useState(""); const [resultsHidden, setResultsHidden] = useState(false); const [addingTmdbId, setAddingTmdbId] = useState(null); const [genreModalOpen, setGenreModalOpen] = useState(false); const [noMatchesBanner, setNoMatchesBanner] = useState(false); const { data, isLoading } = useGroupMovies(groupId); const allMovies = useMemo(() => data?.pages.flat() ?? [], [data]); const unwatchedPool = useMemo(() => allMovies.filter((m) => !m.watched), [allMovies]); const poolEmpty = unwatchedPool.length === 0; const { data: searchData, isLoading: isSearchLoading } = useMovieSearch(searchQuery); const tmdbResults = searchData?.results ?? []; const addMovie = useAddMovie(); const toggleWatched = useToggleWatched(groupId); const { result: winner, rollState, roll } = useRoll(); useRealtimeMovies(groupId); const users = useMemo( () => members.map((m) => ({ id: m.id, avatar_color: m.avatarColor })), [members], ); const userNames = useMemo(() => { const m = new Map(); for (const member of members) m.set(member.id, member.displayName); return m; }, [members]); const handleSearch = useCallback((query: string) => { setSearchQuery(query); setResultsHidden(false); }, []); const handleAdd = useCallback( (tmdbId: number) => { setAddingTmdbId(tmdbId); setResultsHidden(true); addMovie.mutate( { tmdb_id: tmdbId, group_id: groupId }, { onSettled: () => setAddingTmdbId(null), }, ); }, [addMovie, groupId], ); const handleRandomRoll = useCallback(() => { if (isLoading || poolEmpty) return; setNoMatchesBanner(false); roll(unwatchedPool); }, [isLoading, poolEmpty, unwatchedPool, roll]); const handleGenreRollOpen = useCallback(() => { if (isLoading || poolEmpty) return; setGenreModalOpen(true); }, [isLoading, poolEmpty]); const handleGenreRollSubmit = useCallback( (payload: { genreIds: number[]; moodKeys: string[] }) => { setGenreModalOpen(false); const { movies: filtered, noMatches } = filterByGenresAndEmotionsStructured( payload, unwatchedPool, ); if (noMatches) { setNoMatchesBanner(true); roll(unwatchedPool); return; } setNoMatchesBanner(false); roll(filtered); }, [unwatchedPool, roll], ); const handleWatchedToggle = useCallback( (movie: MovieRow) => { toggleWatched.mutate({ movieId: movie.id, watched: !movie.watched }); }, [toggleWatched], ); return (
{addMovie.isError && (

Couldn't add movie. Please try again. {addMovie.error?.message ? ` (${addMovie.error.message})` : ""}

)} {searchQuery.length >= 2 && !resultsHidden && (
)}
{noMatchesBanner && (
No matches — showing full list
)} {rollState !== "idle" && ( )}
{genreModalOpen && ( setGenreModalOpen(false)} onRoll={handleGenreRollSubmit} loading={false} /> )}
); }