diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 78146bfd..447d2f51 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -403,7 +403,14 @@ "episode": "Episode", "airs": "Airs", "endsAt": "Ends at {{time}}", - "trailer": "Trailer" + "trailer": "Trailer", + "collection": { + "movies": "Movies", + "movie": "Movie", + "sortBy": "Sort by", + "releaseDate": "Release Date", + "rating": "Rating" + } }, "migration": { "loginRequired": "You must be logged in to migrate your data! Please go back and login to continue.", diff --git a/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx b/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx index 083d5d30..f6c8c3fe 100644 --- a/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx +++ b/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx @@ -1,23 +1,83 @@ import classNames from "classnames"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { ThemeProvider } from "@/stores/theme"; - -import { - getCollectionDetails, - getMediaBackdrop, - getMediaPoster, - mediaItemToId, -} from "@/backend/metadata/tmdb"; +import { getCollectionDetails, getMediaPoster } from "@/backend/metadata/tmdb"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icon, Icons } from "@/components/Icon"; import { MediaCard } from "@/components/media/MediaCard"; -import { DetailsModal } from "@/components/overlays/detailsModal"; +import { Flare } from "@/components/utils/Flare"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons"; import { MediaItem } from "@/utils/mediaTypes"; +// Simple carousel component for collection overlay +interface SimpleCarouselProps { + mediaItems: MediaItem[]; + onShowDetails: (movieId: number) => void; + categorySlug?: string; +} + +function SimpleCarousel({ + mediaItems, + onShowDetails, + categorySlug = "collection", +}: SimpleCarouselProps) { + const { isMobile } = useIsMobile(); + const carouselRef = useRef(null); + const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({ + [categorySlug]: null, + }); + + useEffect(() => { + if (carouselRef.current) { + carouselRefs.current[categorySlug] = carouselRef.current; + } + }, [categorySlug]); + + if (mediaItems.length === 0) return null; + + return ( +
+ {/* Carousel Container */} +
+
+ + {mediaItems.map((media) => ( +
+ onShowDetails(Number(media.id))} + linkable + /> +
+ ))} + +
+
+ + {/* Navigation Buttons */} + {!isMobile && ( + + )} +
+ ); +} + interface CollectionMovie { id: number; title: string; @@ -51,14 +111,10 @@ export function CollectionOverlay({ onMovieClick, }: CollectionOverlayProps) { const { t } = useTranslation(); - const navigate = useNavigate(); const [collection, setCollection] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [selectedMovie, setSelectedMovie] = useState(null); - const [isClosing, setIsClosing] = useState(false); const [sortOrder, setSortOrder] = useState<"release" | "rating">("release"); - const overlayRef = useRef(null); useEffect(() => { const fetchCollection = async () => { @@ -107,99 +163,52 @@ export function CollectionOverlay({ }; }; - const handleClose = useCallback(() => { - setIsClosing(true); - setTimeout(() => { - onClose(); - }, 200); - }, [onClose]); - - const handleMovieClick = useCallback( - (media: MediaItem) => { - if (onMovieClick) { - onMovieClick(Number(media.id)); - } else { - setSelectedMovie(media); - handleClose(); - - setTimeout(() => { - const mediaId = mediaItemToId(media); - navigate(`/media/${encodeURIComponent(mediaId)}`); - }, 250); - } - }, - [handleClose, navigate, onMovieClick], - ); - - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") { - handleClose(); - } - }; - window.addEventListener("keydown", handleEscape); - return () => window.removeEventListener("keydown", handleEscape); - }, [handleClose]); - - return createPortal( - -
+ - {/* Blur detail modal while collection overlay is open */} -
- -
e.stopPropagation()} - > -
-
-
+
+ + +
e.stopPropagation()} + > + {/* Header */} +
-

+

{collectionName}

{collection && ( -

- +

+ {collection.parts.length} {" "} - {t( - `media.types.movie${ - collection.parts.length !== 1 ? "s" : "" - }`, - )} + {collection.parts.length > 1 + ? t("details.collection.movies") + : t("details.collection.movie")}

)} - - {/* Sort controls */} {!loading && !error && sortedMovies.length > 1 && (
- - {t("media.sortBy")}: + + {t("details.collection.sortBy")}:
)}
- +
{/* Collection Overview */} {collection?.overview && ( -

+

{collection.overview}

)}
-
- {loading && ( -
-
-
-
+ {/* Content */} +
+
+ {loading && ( +
+
+ {Array(8) + .fill(null) + .map((_, index) => ( +
+ +
+ ))} +
-

- {t("media.loading")} -

-
- )} + )} - {/* Error State */} - {error && ( -
-
- -
-

- {t("media.errors.errorLoading")} -

-

{error}

-
- )} - - {!loading && !error && sortedMovies.length === 0 && ( -
-
- -
-

- {t("media.noMoviesInCollection")} -

-
- )} - - {!loading && !error && sortedMovies.length > 0 && ( -
- {sortedMovies.map((movie) => { - const mediaItem = movieToMediaItem(movie); - - return ( - +
+ - ); - })} -
- )} +
+

+ {t("media.errors.errorLoading")} +

+

{error}

+
+ )} + + {!loading && !error && sortedMovies.length === 0 && ( +
+
+ +
+

+ {t("media.noMoviesInCollection")} +

+
+ )} + + {!loading && !error && sortedMovies.length > 0 && ( + + )} +
-
+
-
- - {selectedMovie && ( - - )} - - - , - document.body, + +
); }