Sfoglia il codice sorgente

Merge branch 'worktree-agent-a3887962'

User 2 mesi fa
parent
commit
cd1e440eff
2 ha cambiato i file con 108 aggiunte e 29 eliminazioni
  1. 17 29
      src/app/(app)/list/[id]/page.tsx
  2. 91 0
      src/components/movies/movie-list-client.tsx

+ 17 - 29
src/app/(app)/list/[id]/page.tsx

@@ -1,5 +1,6 @@
 import { notFound } from "next/navigation";
 import { getSupabaseServerClient } from "@/lib/supabase/server";
+import { MovieListClient } from "@/components/movies/movie-list-client";
 
 interface ListPageProps {
   params: Promise<{ id: string }>;
@@ -17,6 +18,21 @@ export default async function ListPage({ params }: ListPageProps) {
 
   const group = data as { id: string; name: string };
 
+  const { data: membersData } = await supabase
+    .from("group_members")
+    .select("user_id, users(display_name, avatar_color)")
+    .eq("group_id", id) as {
+    data:
+      | { user_id: string; users: { display_name: string; avatar_color: string | null } | null }[]
+      | null;
+  };
+
+  const members = (membersData ?? []).map((m) => ({
+    id: m.user_id,
+    displayName: m.users?.display_name ?? "Unknown",
+    avatarColor: m.users?.avatar_color ?? null,
+  }));
+
   return (
     <div className="flex min-h-screen flex-col">
       {/* Header */}
@@ -48,35 +64,7 @@ export default async function ListPage({ params }: ListPageProps) {
       </header>
 
       <main className="mx-auto w-full max-w-5xl flex-1 px-4 py-6">
-        {/* Search bar area */}
-        <div className="mb-6">
-          <div className="h-10 rounded-lg border border-white/10 bg-white/5" />
-        </div>
-
-        {/* Roll buttons area */}
-        <div className="mb-8 flex gap-3">
-          <div className="h-12 flex-1 rounded-lg bg-white/5" />
-          <div className="h-12 flex-1 rounded-lg bg-white/5" />
-        </div>
-
-        {/* Movie grid area */}
-        <section aria-label="Movie list">
-          <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
-            {/* Movie cards will be rendered here by client components */}
-          </div>
-        </section>
-
-        {/* Watched section */}
-        <section className="mt-8 border-t border-white/10 pt-6" aria-label="Watched movies">
-          <details>
-            <summary className="cursor-pointer text-sm font-medium text-foreground/60 hover:text-foreground">
-              Watched
-            </summary>
-            <div className="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
-              {/* Watched movie cards will be rendered here */}
-            </div>
-          </details>
-        </section>
+        <MovieListClient groupId={group.id} groupName={group.name} members={members} />
       </main>
 
       {/* TMDB footer */}

+ 91 - 0
src/components/movies/movie-list-client.tsx

@@ -0,0 +1,91 @@
+"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 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 [addingTmdbId, setAddingTmdbId] = useState<number | null>(null);
+
+  const { data } = useGroupMovies(groupId);
+  const allMovies = useMemo(() => data?.pages.flat() ?? [], [data]);
+
+  const { data: searchData, isLoading: isSearchLoading } = useMovieSearch(searchQuery);
+  const tmdbResults = searchData?.results ?? [];
+
+  const addMovie = useAddMovie();
+
+  useRealtimeMovies(groupId);
+
+  const users = useMemo(
+    () => members.map((m) => ({ id: m.id, avatar_color: m.avatarColor })),
+    [members],
+  );
+
+  // TODO: Wire selectedMovieId, genre filter, userNames when Unit 3 adds optional PosterGrid props
+  const handleSelect = useCallback((_movie: MovieRow) => {
+    // Will toggle selectedMovieId once PosterGrid accepts it
+  }, []);
+
+  const handleSearch = useCallback((query: string) => {
+    setSearchQuery(query);
+  }, []);
+
+  const handleAdd = useCallback(
+    (tmdbId: number) => {
+      setAddingTmdbId(tmdbId);
+      addMovie.mutate(
+        { tmdb_id: tmdbId, group_id: groupId },
+        {
+          onSettled: () => setAddingTmdbId(null),
+        },
+      );
+    },
+    [addMovie, groupId],
+  );
+
+  return (
+    <div className="space-y-6">
+      <div>
+        <SearchBar onSearch={handleSearch} isLoading={isSearchLoading} />
+        {searchQuery.length >= 2 && (
+          <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>
+
+      <section aria-label="Movie list" aria-live="polite">
+        <PosterGrid groupId={groupId} users={users} onSelect={handleSelect} />
+      </section>
+    </div>
+  );
+}