|
|
@@ -1,13 +1,17 @@
|
|
|
"use client";
|
|
|
|
|
|
-import { useMemo, useState } from "react";
|
|
|
+import { useEffect, useMemo, useState } from "react";
|
|
|
import type { Database } from "@/types/database";
|
|
|
import { useGroupMovies } from "@/hooks/use-group-movies";
|
|
|
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
|
|
|
import { PosterCard } from "./poster-card";
|
|
|
+import { ExpandedPanel } from "./expanded-panel";
|
|
|
|
|
|
type MovieRow = Database["public"]["Tables"]["movies"]["Row"];
|
|
|
|
|
|
+const EMPTY_USER_NAMES = new Map<string, string>();
|
|
|
+const noop = () => {};
|
|
|
+
|
|
|
interface UserInfo {
|
|
|
id: string;
|
|
|
avatar_color: string | null;
|
|
|
@@ -17,9 +21,25 @@ interface PosterGridProps {
|
|
|
groupId: string;
|
|
|
users?: UserInfo[];
|
|
|
onSelect: (movie: MovieRow) => void;
|
|
|
+ selectedGenre?: string | null;
|
|
|
+ onGenreSelect?: (genre: string) => void;
|
|
|
+ onClearGenre?: () => void;
|
|
|
+ selectedMovieId?: string | null;
|
|
|
+ onClosePanel?: () => void;
|
|
|
+ userNames?: Map<string, string>;
|
|
|
}
|
|
|
|
|
|
-export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
+export function PosterGrid({
|
|
|
+ groupId,
|
|
|
+ users,
|
|
|
+ onSelect,
|
|
|
+ selectedGenre = null,
|
|
|
+ onGenreSelect,
|
|
|
+ onClearGenre,
|
|
|
+ selectedMovieId = null,
|
|
|
+ onClosePanel,
|
|
|
+ userNames = EMPTY_USER_NAMES,
|
|
|
+}: PosterGridProps) {
|
|
|
const [watchedOpen, setWatchedOpen] = useState(false);
|
|
|
|
|
|
const { data, hasNextPage, isFetchingNextPage, fetchNextPage, isLoading, isError } =
|
|
|
@@ -43,6 +63,33 @@ export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
return { unwatched: uw, watched: w };
|
|
|
}, [allMovies]);
|
|
|
|
|
|
+ const filteredUnwatched = useMemo(
|
|
|
+ () =>
|
|
|
+ selectedGenre
|
|
|
+ ? unwatched.filter((m) => m.genres.includes(selectedGenre))
|
|
|
+ : unwatched,
|
|
|
+ [unwatched, selectedGenre],
|
|
|
+ );
|
|
|
+
|
|
|
+ const filteredWatched = useMemo(
|
|
|
+ () =>
|
|
|
+ selectedGenre
|
|
|
+ ? watched.filter((m) => m.genres.includes(selectedGenre))
|
|
|
+ : watched,
|
|
|
+ [watched, selectedGenre],
|
|
|
+ );
|
|
|
+
|
|
|
+ // Auto-close panel when selected movie is no longer in filtered results
|
|
|
+ useEffect(() => {
|
|
|
+ if (!selectedMovieId) return;
|
|
|
+ const inFiltered =
|
|
|
+ filteredUnwatched.some((m) => m.id === selectedMovieId) ||
|
|
|
+ filteredWatched.some((m) => m.id === selectedMovieId);
|
|
|
+ if (!inFiltered) {
|
|
|
+ onClosePanel?.();
|
|
|
+ }
|
|
|
+ }, [selectedMovieId, filteredUnwatched, filteredWatched, onClosePanel]);
|
|
|
+
|
|
|
const userMap = useMemo(() => {
|
|
|
const map = new Map<string, string | null>();
|
|
|
if (users) {
|
|
|
@@ -57,6 +104,18 @@ export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
return movie.added_by ? (userMap.get(movie.added_by) ?? null) : null;
|
|
|
}
|
|
|
|
|
|
+ function getAddedByName(movie: MovieRow): string | null {
|
|
|
+ return movie.added_by ? (userNames.get(movie.added_by) ?? null) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectedMovie = useMemo(
|
|
|
+ () =>
|
|
|
+ selectedMovieId
|
|
|
+ ? allMovies.find((m) => m.id === selectedMovieId) ?? null
|
|
|
+ : null,
|
|
|
+ [allMovies, selectedMovieId],
|
|
|
+ );
|
|
|
+
|
|
|
if (isLoading) {
|
|
|
return <p className="py-8 text-center text-gray-400">Loading movies...</p>;
|
|
|
}
|
|
|
@@ -69,17 +128,56 @@ export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
return <p className="py-8 text-center text-gray-400">No movies yet. Add one to get started!</p>;
|
|
|
}
|
|
|
|
|
|
+ function renderMoviesWithPanel(movies: MovieRow[]) {
|
|
|
+ const items: React.ReactNode[] = [];
|
|
|
+ for (const movie of movies) {
|
|
|
+ items.push(
|
|
|
+ <PosterCard
|
|
|
+ key={movie.id}
|
|
|
+ movie={movie}
|
|
|
+ avatarColor={getAvatarColor(movie)}
|
|
|
+ onSelect={onSelect}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+ if (selectedMovie && movie.id === selectedMovieId) {
|
|
|
+ items.push(
|
|
|
+ <ExpandedPanel
|
|
|
+ key={`panel-${movie.id}`}
|
|
|
+ movie={selectedMovie}
|
|
|
+ addedByName={getAddedByName(selectedMovie)}
|
|
|
+ selectedGenre={selectedGenre}
|
|
|
+ onGenreSelect={onGenreSelect ?? noop}
|
|
|
+ onClose={onClosePanel ?? noop}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+ }
|
|
|
+
|
|
|
return (
|
|
|
<div>
|
|
|
+ {selectedGenre && (
|
|
|
+ <div className="mb-3 flex items-center gap-2">
|
|
|
+ <span className="text-sm text-gray-300">
|
|
|
+ Filtered by <strong>{selectedGenre}</strong>
|
|
|
+ </span>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={onClearGenre}
|
|
|
+ className="min-h-[44px] min-w-[44px] rounded-lg px-2 py-1 text-sm text-gray-400 hover:bg-gray-700 hover:text-gray-200"
|
|
|
+ aria-label={`Clear ${selectedGenre} filter`}
|
|
|
+ >
|
|
|
+ Clear
|
|
|
+ </button>
|
|
|
+ <span aria-live="polite" className="sr-only">
|
|
|
+ Showing {filteredUnwatched.length} unwatched and {filteredWatched.length} watched movies filtered by {selectedGenre}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4">
|
|
|
- {unwatched.map((movie) => (
|
|
|
- <PosterCard
|
|
|
- key={movie.id}
|
|
|
- movie={movie}
|
|
|
- avatarColor={getAvatarColor(movie)}
|
|
|
- onSelect={onSelect}
|
|
|
- />
|
|
|
- ))}
|
|
|
+ {renderMoviesWithPanel(filteredUnwatched)}
|
|
|
</div>
|
|
|
|
|
|
<div ref={sentinelRef} className="h-1" />
|
|
|
@@ -88,7 +186,7 @@ export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
<p className="py-4 text-center text-sm text-gray-400">Loading more...</p>
|
|
|
)}
|
|
|
|
|
|
- {watched.length > 0 && (
|
|
|
+ {filteredWatched.length > 0 && (
|
|
|
<div className="mt-6">
|
|
|
<button
|
|
|
type="button"
|
|
|
@@ -103,7 +201,7 @@ export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
>
|
|
|
▶
|
|
|
</span>
|
|
|
- Watched ({watched.length})
|
|
|
+ Watched ({filteredWatched.length})
|
|
|
</button>
|
|
|
|
|
|
{watchedOpen && (
|
|
|
@@ -111,14 +209,7 @@ export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
className="mt-3 grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4"
|
|
|
aria-live="polite"
|
|
|
>
|
|
|
- {watched.map((movie) => (
|
|
|
- <PosterCard
|
|
|
- key={movie.id}
|
|
|
- movie={movie}
|
|
|
- avatarColor={getAvatarColor(movie)}
|
|
|
- onSelect={onSelect}
|
|
|
- />
|
|
|
- ))}
|
|
|
+ {renderMoviesWithPanel(filteredWatched)}
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|