|
|
@@ -0,0 +1,128 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import { 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";
|
|
|
+
|
|
|
+type MovieRow = Database["public"]["Tables"]["movies"]["Row"];
|
|
|
+
|
|
|
+interface UserInfo {
|
|
|
+ id: string;
|
|
|
+ avatar_color: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+interface PosterGridProps {
|
|
|
+ groupId: string;
|
|
|
+ users?: UserInfo[];
|
|
|
+ onSelect: (movie: MovieRow) => void;
|
|
|
+}
|
|
|
+
|
|
|
+export function PosterGrid({ groupId, users, onSelect }: PosterGridProps) {
|
|
|
+ const [watchedOpen, setWatchedOpen] = useState(false);
|
|
|
+
|
|
|
+ const { data, hasNextPage, isFetchingNextPage, fetchNextPage, isLoading, isError } =
|
|
|
+ useGroupMovies(groupId);
|
|
|
+
|
|
|
+ const sentinelRef = useInfiniteScroll({
|
|
|
+ hasNextPage,
|
|
|
+ isFetchingNextPage,
|
|
|
+ fetchNextPage,
|
|
|
+ });
|
|
|
+
|
|
|
+ const allMovies = useMemo(() => data?.pages.flat() ?? [], [data]);
|
|
|
+
|
|
|
+ const { unwatched, watched } = useMemo(() => {
|
|
|
+ const uw: MovieRow[] = [];
|
|
|
+ const w: MovieRow[] = [];
|
|
|
+ for (const movie of allMovies) {
|
|
|
+ if (movie.watched) w.push(movie);
|
|
|
+ else uw.push(movie);
|
|
|
+ }
|
|
|
+ return { unwatched: uw, watched: w };
|
|
|
+ }, [allMovies]);
|
|
|
+
|
|
|
+ const userMap = useMemo(() => {
|
|
|
+ const map = new Map<string, string | null>();
|
|
|
+ if (users) {
|
|
|
+ for (const u of users) {
|
|
|
+ map.set(u.id, u.avatar_color);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }, [users]);
|
|
|
+
|
|
|
+ function getAvatarColor(movie: MovieRow) {
|
|
|
+ return movie.added_by ? (userMap.get(movie.added_by) ?? null) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return <p className="py-8 text-center text-gray-400">Loading movies...</p>;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isError) {
|
|
|
+ return <p className="py-8 text-center text-red-400">Failed to load movies.</p>;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (allMovies.length === 0) {
|
|
|
+ return <p className="py-8 text-center text-gray-400">No movies yet. Add one to get started!</p>;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <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}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div ref={sentinelRef} className="h-1" />
|
|
|
+
|
|
|
+ {isFetchingNextPage && (
|
|
|
+ <p className="py-4 text-center text-sm text-gray-400">Loading more...</p>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {watched.length > 0 && (
|
|
|
+ <div className="mt-6">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={() => setWatchedOpen((prev) => !prev)}
|
|
|
+ className="flex items-center gap-1.5 text-sm font-medium text-gray-400 hover:text-gray-200"
|
|
|
+ aria-expanded={watchedOpen}
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ className="inline-block transition-transform"
|
|
|
+ style={{ transform: watchedOpen ? "rotate(90deg)" : "rotate(0deg)" }}
|
|
|
+ aria-hidden="true"
|
|
|
+ >
|
|
|
+ ▶
|
|
|
+ </span>
|
|
|
+ Watched ({watched.length})
|
|
|
+ </button>
|
|
|
+
|
|
|
+ {watchedOpen && (
|
|
|
+ <div
|
|
|
+ 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}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|