add all movie lists page (letterboxd)

use backend urls
This commit is contained in:
Pas 2025-06-07 18:22:28 -06:00
parent 9b6fbf8aa8
commit d182ccc680
5 changed files with 230 additions and 0 deletions

View file

@ -949,6 +949,8 @@
"loading": "Loading...",
"back": "Go back"
},
"viewLists": "View All Movie Lists",
"allLists": "All Movie Lists",
"scrollToTop": "Back to top"
},
"fedapi": {

View file

@ -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<T = LetterboxdResponse>(
endpoint: string,
): Promise<T> {
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");

View file

@ -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<any>();
const [letterboxdLists, setLetterboxdLists] = useState<any[]>([]);
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 (
<SubPageLayout>
<WideContainer>
<div className="flex items-center justify-between gap-8">
<Heading1 className="text-2xl font-bold text-white">
{t("discover.allLists")}
</Heading1>
</div>
<div className="flex items-center gap-4 pb-8">
<button
type="button"
onClick={handleBack}
className="flex items-center text-white hover:text-gray-300 transition-colors"
>
<Icon className="text-xl" icon={Icons.ARROW_LEFT} />
<span className="ml-2">{t("discover.page.back")}</span>
</button>
</div>
</WideContainer>
<WideContainer ultraWide>
{/* Latest Movies */}
<MediaCarousel
content={{ type: "latest", fallback: "nowPlaying" }}
isTVShow={false}
carouselRefs={carouselRefs}
onShowDetails={handleShowDetails}
/>
{/* Latest TV Shows */}
<MediaCarousel
content={{ type: "latesttv", fallback: "onTheAir" }}
isTVShow
carouselRefs={carouselRefs}
onShowDetails={handleShowDetails}
/>
{/* Top Rated Movies */}
<MediaCarousel
content={{ type: "topRated" }}
isTVShow={false}
carouselRefs={carouselRefs}
onShowDetails={handleShowDetails}
/>
{/* Letterboxd Lists */}
{letterboxdLists.map((list) => (
<div key={list.listUrl}>
<div className="flex items-center justify-between ml-2 md:ml-8 mt-2">
<div className="flex flex-col">
<div className="flex items-center gap-4">
<h2 className="text-2xl cursor-default font-bold text-white md:text-2xl pl-5 text-balance">
{list.listName}
</h2>
</div>
</div>
</div>
<div className="relative overflow-hidden carousel-container md:pb-4">
<div
className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8"
ref={(el) => {
carouselRefs.current[list.listUrl] = el;
}}
onWheel={handleWheel}
>
<div className="md:w-12" />
{list.tmdbMovies.map((movie: TmdbMovie) => (
<div
key={movie.id}
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
>
<MediaCard
linkable
media={{
id: movie.id.toString(),
title: movie.title,
poster: movie.poster_path
? `https://image.tmdb.org/t/p/w342${movie.poster_path}`
: "/placeholder.png",
type: "movie",
year: movie.release_date
? parseInt(movie.release_date.split("-")[0], 10)
: undefined,
}}
onShowDetails={handleShowDetails}
/>
</div>
))}
<div className="md:w-12" />
</div>
</div>
</div>
))}
</WideContainer>
{detailsData && <DetailsModal id="discover-details" data={detailsData} />}
</SubPageLayout>
);
}

View file

@ -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<any>();
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() {
</LazyTabContent>
</WideContainer>
{/* View All Button */}
<div
className={classNames(
"flex justify-center mt-8 mb-12",
isMoviesTab ? "block" : "hidden",
)}
>
<Button theme="purple" onClick={() => navigate("/discover/all")}>
{t("discover.viewLists")}
</Button>
</div>
<ScrollToTopButton />
{detailsData && <DetailsModal id="discover-details" data={detailsData} />}

View file

@ -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={<MoreContent />}
/>
<Route path="/discover/more/:category" element={<MoreContent />} />
<Route path="/discover/all" element={<DiscoverMore />} />
{/* Settings page */}
<Route
path="/settings"