diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index dd8c9938..0062ffcf 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -949,6 +949,8 @@ "loading": "Loading...", "back": "Go back" }, + "viewLists": "View All Movie Lists", + "allLists": "All Movie Lists", "scrollToTop": "Back to top" }, "fedapi": { diff --git a/src/backend/metadata/letterboxd.ts b/src/backend/metadata/letterboxd.ts new file mode 100644 index 00000000..4c216a52 --- /dev/null +++ b/src/backend/metadata/letterboxd.ts @@ -0,0 +1,50 @@ +import { conf } from "@/utils/setup/config"; + +export interface TmdbMovie { + adult: boolean; + backdrop_path: string | null; + genre_ids: number[]; + id: number; + original_language: string; + original_title: string; + overview: string; + popularity: number; + poster_path: string | null; + release_date: string; + title: string; + video: boolean; + vote_average: number; + vote_count: number; +} + +export interface ListMetadata { + originalFilmCount: number; + foundTmdbMovies: number; + expectedItemCount: number | null; + workingSelector: string; +} + +export interface LetterboxdList { + listName: string; + listUrl: string; + tmdbMovies: TmdbMovie[]; + metadata: ListMetadata; +} + +export interface LetterboxdResponse { + lists: LetterboxdList[]; +} + +// Base function to fetch from Letterboxd API +async function fetchFromLetterboxd( + endpoint: string, +): Promise { + const response = await fetch(`${conf().BACKEND_URL}${endpoint}`); + if (!response.ok) { + throw new Error(`Failed to fetch from ${endpoint}: ${response.statusText}`); + } + return response.json(); +} + +// Get Letterboxd lists with TMDB movie information +export const getLetterboxdLists = () => fetchFromLetterboxd("/letterboxd"); diff --git a/src/pages/discover/AllMovieLists.tsx b/src/pages/discover/AllMovieLists.tsx new file mode 100644 index 00000000..b97773c3 --- /dev/null +++ b/src/pages/discover/AllMovieLists.tsx @@ -0,0 +1,159 @@ +import { t } from "i18next"; +import { useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { TmdbMovie, getLetterboxdLists } from "@/backend/metadata/letterboxd"; +import { Icon, Icons } from "@/components/Icon"; +import { WideContainer } from "@/components/layout/WideContainer"; +import { MediaCard } from "@/components/media/MediaCard"; +import { DetailsModal } from "@/components/overlays/details/DetailsModal"; +import { useModal } from "@/components/overlays/Modal"; +import { Heading1 } from "@/components/utils/Text"; +import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; +import { useDiscoverStore } from "@/stores/discover"; +import { MediaItem } from "@/utils/mediaTypes"; + +import { MediaCarousel } from "./components/MediaCarousel"; + +export function DiscoverMore() { + const [detailsData, setDetailsData] = useState(); + const [letterboxdLists, setLetterboxdLists] = useState([]); + const detailsModal = useModal("discover-details"); + const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); + const navigate = useNavigate(); + const { lastView } = useDiscoverStore(); + + useEffect(() => { + const fetchLetterboxdLists = async () => { + try { + const response = await getLetterboxdLists(); + setLetterboxdLists(response.lists); + } catch (error) { + console.error("Failed to fetch Letterboxd lists:", error); + } + }; + + fetchLetterboxdLists(); + }, []); + + const handleShowDetails = async (media: MediaItem) => { + setDetailsData({ + id: Number(media.id), + type: media.type === "movie" ? "movie" : "show", + }); + detailsModal.show(); + }; + + const handleBack = () => { + if (lastView) { + navigate(lastView.url); + window.scrollTo(0, lastView.scrollPosition); + } else { + navigate(-1); + } + }; + + const handleWheel = (e: React.WheelEvent) => { + if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) { + e.stopPropagation(); + e.preventDefault(); + } + }; + + return ( + + +
+ + {t("discover.allLists")} + +
+
+ +
+
+ + {/* Latest Movies */} + + + {/* Latest TV Shows */} + + + {/* Top Rated Movies */} + + + {/* Letterboxd Lists */} + {letterboxdLists.map((list) => ( +
+
+
+
+

+ {list.listName} +

+
+
+
+
+
{ + carouselRefs.current[list.listUrl] = el; + }} + onWheel={handleWheel} + > +
+ {list.tmdbMovies.map((movie: TmdbMovie) => ( +
+ +
+ ))} +
+
+
+
+ ))} + + {detailsData && } + + ); +} diff --git a/src/pages/discover/discoverContent.tsx b/src/pages/discover/discoverContent.tsx index 5c76a368..2f08aa20 100644 --- a/src/pages/discover/discoverContent.tsx +++ b/src/pages/discover/discoverContent.tsx @@ -1,5 +1,9 @@ +import classNames from "classnames"; +import { t } from "i18next"; import { useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/buttons/Button"; import { WideContainer } from "@/components/layout/WideContainer"; import { DetailsModal } from "@/components/overlays/details/DetailsModal"; import { useModal } from "@/components/overlays/Modal"; @@ -16,6 +20,7 @@ import { ScrollToTopButton } from "./components/ScrollToTopButton"; export function DiscoverContent() { const { selectedCategory, setSelectedCategory } = useDiscoverStore(); const [detailsData, setDetailsData] = useState(); + const navigate = useNavigate(); const detailsModal = useModal("discover-details"); const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const progressItems = useProgressStore((state) => state.items); @@ -222,6 +227,18 @@ export function DiscoverContent() { + {/* View All Button */} +
+ +
+ {detailsData && } diff --git a/src/setup/App.tsx b/src/setup/App.tsx index f544385b..601f94b8 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -15,6 +15,7 @@ import { useOnlineListener } from "@/hooks/usePing"; import { AboutPage } from "@/pages/About"; import { AdminPage } from "@/pages/admin/AdminPage"; import VideoTesterView from "@/pages/developer/VideoTesterView"; +import { DiscoverMore } from "@/pages/discover/AllMovieLists"; import { Discover } from "@/pages/discover/Discover"; import { MoreContent } from "@/pages/discover/MoreContent"; import { DmcaPage } from "@/pages/Dmca"; @@ -178,6 +179,7 @@ function App() { element={} /> } /> + } /> {/* Settings page */}