Преглед изворни кода

feat: enhance PosterGrid with genre filter and inline expanded panel

Adds optional props for genre filtering (with aria-live announcements)
and inline ExpandedPanel rendering below the selected movie's grid row.
All new props are optional — existing callers work unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User пре 2 месеци
родитељ
комит
b9b6f3712c
1 измењених фајлова са 111 додато и 20 уклоњено
  1. 111 20
      src/components/movies/poster-grid.tsx

+ 111 - 20
src/components/movies/poster-grid.tsx

@@ -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>