| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- "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<number | null>(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<string, string>();
- 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 (
- <div className="space-y-6">
- <RollBar
- isLoading={isLoading}
- poolEmpty={poolEmpty}
- onRoll={handleRandomRoll}
- onGenreRoll={handleGenreRollOpen}
- />
- <div>
- <SearchBar onSearch={handleSearch} isLoading={isSearchLoading} />
- {addMovie.isError && (
- <p role="alert" aria-live="polite" className="mt-2 text-sm text-red-500">
- Couldn't add movie. Please try again.
- {addMovie.error?.message ? ` (${addMovie.error.message})` : ""}
- </p>
- )}
- {searchQuery.length >= 2 && !resultsHidden && (
- <div className="mt-2 rounded-lg border border-white/10 bg-white/5 p-3">
- <SearchResults
- tmdbResults={tmdbResults}
- groupMovies={allMovies}
- query={searchQuery}
- isAdding={addMovie.isPending}
- addingTmdbId={addingTmdbId}
- onAdd={handleAdd}
- />
- </div>
- )}
- </div>
- <RollAnnouncer state={rollState} winner={winner} />
- {noMatchesBanner && (
- <div
- role="status"
- aria-live="polite"
- className="rounded-lg border border-yellow-500/40 bg-yellow-500/10 px-3 py-2 text-sm text-yellow-200"
- data-testid="roll-no-matches-banner"
- >
- No matches — showing full list
- </div>
- )}
- {rollState !== "idle" && (
- <ListRollCarousel
- pool={allMovies}
- winner={winner}
- state={rollState}
- onWatchedToggle={handleWatchedToggle}
- />
- )}
- <section aria-label="Movie list" aria-live="polite">
- <PosterGrid groupId={groupId} users={users} userNames={userNames} />
- </section>
- {genreModalOpen && (
- <GenreRollModal
- onClose={() => setGenreModalOpen(false)}
- onRoll={handleGenreRollSubmit}
- loading={false}
- />
- )}
- </div>
- );
- }
|