mirror of
https://github.com/sussy-code/smov.git
synced 2025-01-17 01:51:24 +01:00
Adapt explore page
This commit is contained in:
parent
a67ae7bcbb
commit
1212d68941
4 changed files with 466 additions and 296 deletions
14
package.json
14
package.json
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "sudo-flix",
|
"name": "movie-web",
|
||||||
"version": "4.6.2",
|
"version": "4.6.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://sudo-flix.lol",
|
"homepage": "https://github.com/movie-web/movie-web",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
@ -29,14 +29,13 @@
|
||||||
"@formkit/auto-animate": "^0.8.1",
|
"@formkit/auto-animate": "^0.8.1",
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@ladjs/country-language": "^1.0.3",
|
"@ladjs/country-language": "^1.0.3",
|
||||||
"@movie-web/providers": "^2.2.3",
|
"@movie-web/providers": "^2.2.7",
|
||||||
"@noble/hashes": "^1.3.3",
|
"@noble/hashes": "^1.3.3",
|
||||||
"@plasmohq/messaging": "^0.6.1",
|
"@plasmohq/messaging": "^0.6.1",
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@scure/bip39": "^1.2.2",
|
"@scure/bip39": "^1.2.2",
|
||||||
"@sozialhelden/ietf-language-tags": "^5.4.2",
|
"@sozialhelden/ietf-language-tags": "^5.4.2",
|
||||||
"@types/node-forge": "^1.3.10",
|
"@types/node-forge": "^1.3.10",
|
||||||
"@vercel/analytics": "^1.2.2",
|
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"core-js": "^3.34.0",
|
"core-js": "^3.34.0",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
|
@ -63,6 +62,7 @@
|
||||||
"react-i18next": "^14.0.0",
|
"react-i18next": "^14.0.0",
|
||||||
"react-lazy-with-preload": "^2.2.1",
|
"react-lazy-with-preload": "^2.2.1",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
|
"react-lazy-load-image-component": "^1.6.0",
|
||||||
"react-sticky-el": "^2.1.0",
|
"react-sticky-el": "^2.1.0",
|
||||||
"react-turnstile": "^1.1.2",
|
"react-turnstile": "^1.1.2",
|
||||||
"react-use": "^17.4.2",
|
"react-use": "^17.4.2",
|
||||||
|
@ -91,6 +91,7 @@
|
||||||
"@types/react-router": "^5.1.20",
|
"@types/react-router": "^5.1.20",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-stickynode": "^4.0.3",
|
"@types/react-stickynode": "^4.0.3",
|
||||||
|
"@types/react-lazy-load-image-component": "^1.6.3",
|
||||||
"@types/react-transition-group": "^4.4.10",
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@types/semver": "^7.5.6",
|
"@types/semver": "^7.5.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||||
|
@ -126,7 +127,8 @@
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-plugin-pwa": "^0.17.4",
|
"vite-plugin-pwa": "^0.17.4",
|
||||||
"vite-plugin-static-copy": "^1.0.0",
|
"vite-plugin-static-copy": "^1.0.0",
|
||||||
"vitest": "^1.1.0"
|
"vitest": "^1.1.0",
|
||||||
|
"workbox-window": "^7.0.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|
|
@ -135,13 +135,15 @@ export async function getLegacyMetaFromId(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
let imdbId = data.external_ids.find((v) => v.provider === "imdb_latest")
|
let imdbId = data.external_ids.find(
|
||||||
?.external_id;
|
(v) => v.provider === "imdb_latest",
|
||||||
|
)?.external_id;
|
||||||
if (!imdbId)
|
if (!imdbId)
|
||||||
imdbId = data.external_ids.find((v) => v.provider === "imdb")?.external_id;
|
imdbId = data.external_ids.find((v) => v.provider === "imdb")?.external_id;
|
||||||
|
|
||||||
let tmdbId = data.external_ids.find((v) => v.provider === "tmdb_latest")
|
let tmdbId = data.external_ids.find(
|
||||||
?.external_id;
|
(v) => v.provider === "tmdb_latest",
|
||||||
|
)?.external_id;
|
||||||
if (!tmdbId)
|
if (!tmdbId)
|
||||||
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
|
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ const headers = {
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function get<T>(url: string, params?: object): Promise<T> {
|
export async function get<T>(url: string, params?: object): Promise<T> {
|
||||||
if (!apiKey) throw new Error("TMDB API key not set");
|
if (!apiKey) throw new Error("TMDB API key not set");
|
||||||
|
|
||||||
const res = await proxiedFetch<any>(encodeURI(url), {
|
const res = await proxiedFetch<any>(encodeURI(url), {
|
||||||
|
|
|
@ -1,13 +1,65 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom"; // Import Link from react-router-dom
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { LazyLoadImage } from "react-lazy-load-image-component";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { ThiccContainer } from "@/components/layout/ThinContainer";
|
import "react-lazy-load-image-component/src/effects/blur.css";
|
||||||
import { Divider } from "@/components/utils/Divider";
|
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||||
import { Heading1, Paragraph } from "@/components/utils/Text";
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
|
import { HomeLayout } from "@/pages/layouts/HomeLayout";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
import { get } from "../backend/metadata/tmdb";
|
||||||
import { PageTitle } from "./parts/util/PageTitle";
|
import { Icon, Icons } from "../components/Icon";
|
||||||
|
|
||||||
|
const pagesToFetch = 5;
|
||||||
|
|
||||||
|
// Define the Media type
|
||||||
|
interface Media {
|
||||||
|
id: number;
|
||||||
|
poster_path: string;
|
||||||
|
title?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the Movie and TVShow interfaces to extend the Media interface
|
||||||
|
interface Movie extends Media {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TVShow extends Media {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the Genre type
|
||||||
|
interface Genre {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the Category type
|
||||||
|
interface Category {
|
||||||
|
name: string;
|
||||||
|
endpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the categories
|
||||||
|
const categories: Category[] = [
|
||||||
|
{
|
||||||
|
name: "Now Playing",
|
||||||
|
endpoint: "/movie/now_playing?language=en-US",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Popular",
|
||||||
|
endpoint: "/movie/popular?language=en-US",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Top Rated",
|
||||||
|
endpoint: "/movie/top_rated?language=en-US",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export function Button(props: {
|
export function Button(props: {
|
||||||
className: string;
|
className: string;
|
||||||
|
@ -30,297 +82,411 @@ export function Button(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isShowOrMovie(tmdbFullId: string): "series" | "movie" | "unknown" {
|
|
||||||
if (tmdbFullId.includes("show-")) {
|
|
||||||
return "series";
|
|
||||||
}
|
|
||||||
if (tmdbFullId.includes("movie-")) {
|
|
||||||
return "movie";
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
function directLinkToContent(tmdbFullId: string) {
|
|
||||||
if (isShowOrMovie(tmdbFullId) === "series") {
|
|
||||||
return `/media/tmdb-tv-${tmdbFullId.split("-")[1]}`;
|
|
||||||
}
|
|
||||||
if (isShowOrMovie(tmdbFullId) === "movie") {
|
|
||||||
return `/media/tmdb-movie-${tmdbFullId.split("-")[1]}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConfigValue(props: {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
id: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
}) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const link = directLinkToContent(props.id);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex">
|
|
||||||
<p className="flex-1 font-bold text-white pr-5 pl-3">
|
|
||||||
{link ? (
|
|
||||||
<p
|
|
||||||
onClick={() => navigate(link)}
|
|
||||||
className="transition duration-200 hover:underline cursor-pointer"
|
|
||||||
>
|
|
||||||
{props.name}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p>{props.name}</p>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="pr-3 cursor-default">{props.children}</p>
|
|
||||||
</div>
|
|
||||||
<p className="pr-5 pl-3 cursor-default">
|
|
||||||
{props.type.charAt(0).toUpperCase() + props.type.slice(1)}
|
|
||||||
</p>
|
|
||||||
<Divider marginClass="my-3" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRecentPlayedItems() {
|
|
||||||
const response = await fetch("https://backend.sudo-flix.lol/metrics");
|
|
||||||
const text = await response.text();
|
|
||||||
|
|
||||||
const regex =
|
|
||||||
/mw_media_watch_count{tmdb_full_id="([^"]+)",provider_id="([^"]+)",title="([^"]+)",success="([^"]+)"} (\d+)/g;
|
|
||||||
let match;
|
|
||||||
const loop = true;
|
|
||||||
const items: { [key: string]: any } = {};
|
|
||||||
|
|
||||||
while (loop) {
|
|
||||||
match = regex.exec(text);
|
|
||||||
if (match === null) break;
|
|
||||||
|
|
||||||
const [_, tmdbFullId, providerId, title, success, count] = match;
|
|
||||||
if (items[tmdbFullId]) {
|
|
||||||
items[tmdbFullId].count += parseInt(count, 10);
|
|
||||||
} else {
|
|
||||||
items[tmdbFullId] = {
|
|
||||||
tmdbFullId,
|
|
||||||
providerId,
|
|
||||||
title,
|
|
||||||
success: success === "true",
|
|
||||||
count: parseInt(count, 10),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(items).length > 0) {
|
|
||||||
return Object.values(items);
|
|
||||||
}
|
|
||||||
throw new Error("RECENT_PLAYED_ITEMS not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTotalViews() {
|
|
||||||
const response = await fetch("https://backend.sudo-flix.lol/metrics");
|
|
||||||
const text = await response.text();
|
|
||||||
|
|
||||||
// Add up all mw_media_watch_count entries
|
|
||||||
const regex = /mw_media_watch_count{[^}]*} (\d+)/g;
|
|
||||||
let totalViews = 0;
|
|
||||||
let match = regex.exec(text);
|
|
||||||
|
|
||||||
while (match !== null) {
|
|
||||||
totalViews += parseInt(match[1], 10);
|
|
||||||
match = regex.exec(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalViews > 0) {
|
|
||||||
return totalViews.toString();
|
|
||||||
}
|
|
||||||
throw new Error("TOTAL_VIEWS not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProcessStartTime(): Promise<string> {
|
|
||||||
return fetch("https://backend.sudo-flix.lol/metrics")
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then((text) => {
|
|
||||||
const regex = /process_start_time_seconds (\d+)/;
|
|
||||||
const match = text.match(regex);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const parsedNum = parseInt(match[1], 10);
|
|
||||||
const date = new Date(parsedNum * 1000);
|
|
||||||
return date.toISOString();
|
|
||||||
}
|
|
||||||
throw new Error("PROCESS_START_TIME_SECONDS not found");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTimeSinceProcessStart(): Promise<string> {
|
|
||||||
const processStartTime = await getProcessStartTime();
|
|
||||||
const currentTime = new Date();
|
|
||||||
const timeDifference =
|
|
||||||
currentTime.getTime() - new Date(processStartTime).getTime();
|
|
||||||
|
|
||||||
const hours = Math.floor(timeDifference / (1000 * 60 * 60));
|
|
||||||
const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60));
|
|
||||||
const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000);
|
|
||||||
const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
if (days > 0) {
|
|
||||||
if (days === 1) {
|
|
||||||
return `${days} day`;
|
|
||||||
}
|
|
||||||
return `${days} days`;
|
|
||||||
}
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours} hours`;
|
|
||||||
}
|
|
||||||
if (minutes > 0) {
|
|
||||||
return `${minutes} minutes`;
|
|
||||||
}
|
|
||||||
return `${seconds} seconds`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TopFlix() {
|
export function TopFlix() {
|
||||||
const [recentPlayedItems, setRecentPlayedItems] = useState<any[]>([]);
|
const { t } = useTranslation();
|
||||||
const [totalViews, setTotalViews] = useState<string | null>(null);
|
const [showBg] = useState<boolean>(false);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [genres, setGenres] = useState<Genre[]>([]);
|
||||||
const itemsPerPage = 10;
|
const [randomMovie, setRandomMovie] = useState<Movie | null>(null); // Add this line
|
||||||
const maxItemsToShow = 100; // Maximum items to show
|
const [genreMovies, setGenreMovies] = useState<{
|
||||||
const maxPageCount = Math.ceil(maxItemsToShow / itemsPerPage); // Calculate max page count based on maxItemsToShow
|
[genreId: number]: Movie[];
|
||||||
const [timeSinceProcessStart, setTimeSinceProcessStart] = useState<
|
}>({});
|
||||||
string | null
|
const [countdown, setCountdown] = useState<number | null>(null);
|
||||||
>(null);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
// Add a new state variable for the category movies
|
||||||
getRecentPlayedItems()
|
const [categoryMovies, setCategoryMovies] = useState<{
|
||||||
.then((items) => {
|
[categoryName: string]: Movie[];
|
||||||
// Limit the items to the first 100 to ensure we don't exceed the max page count
|
}>({});
|
||||||
const limitedItems = items
|
|
||||||
.slice(0, maxItemsToShow)
|
|
||||||
.filter(
|
|
||||||
(item, index, self) =>
|
|
||||||
index ===
|
|
||||||
self.findIndex((t2) => t2.tmdbFullId === item.tmdbFullId),
|
|
||||||
);
|
|
||||||
|
|
||||||
setRecentPlayedItems(limitedItems);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error fetching recent played items:", error);
|
|
||||||
});
|
|
||||||
getTotalViews()
|
|
||||||
.then((views) => {
|
|
||||||
setTotalViews(parseInt(views, 10).toLocaleString());
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error fetching total views:", error);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getTimeSinceProcessStart()
|
const fetchMoviesForCategory = async (category: Category) => {
|
||||||
.then((time) => {
|
try {
|
||||||
setTimeSinceProcessStart(time);
|
const movies: any[] = [];
|
||||||
})
|
for (let page = 1; page <= pagesToFetch; page += 1) {
|
||||||
.catch((error) => {
|
const data = await get<any>(category.endpoint, {
|
||||||
console.error("Error fetching time since process start:", error);
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
|
language: "en-US",
|
||||||
|
page: page.toString(),
|
||||||
});
|
});
|
||||||
}, []);
|
|
||||||
|
|
||||||
function getItemsForCurrentPage() {
|
movies.push(...data.results);
|
||||||
const sortedItems = recentPlayedItems.sort((a, b) => b.count - a.count);
|
}
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
setCategoryMovies((prevCategoryMovies) => ({
|
||||||
const endIndex = startIndex + itemsPerPage;
|
...prevCategoryMovies,
|
||||||
|
[category.name]: movies,
|
||||||
return sortedItems.slice(startIndex, endIndex).map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
rank: startIndex + index + 1,
|
|
||||||
}));
|
}));
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
return (
|
`Error fetching movies for category ${category.name}:`,
|
||||||
<SubPageLayout>
|
error,
|
||||||
<ThiccContainer>
|
|
||||||
<PageTitle subpage k="global.pages.topFlix" />
|
|
||||||
<div className="mt-8 w-full px-8 cursor-default">
|
|
||||||
<Heading1>Top flix</Heading1>
|
|
||||||
<Paragraph className="mb-6">
|
|
||||||
The top 100 most-watched movies on sudo-flix.lol, sourced directly
|
|
||||||
from the most recent sudo-backend deployment. The backend is
|
|
||||||
redeployed frequently which may result in low numbers being shown
|
|
||||||
here.
|
|
||||||
</Paragraph>
|
|
||||||
<div className="mt-2 w-full">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="bg-buttons-secondary rounded-xl scale-95 py-3 px-5 mb-2">
|
|
||||||
<p className="font-bold text-buttons-secondaryText">
|
|
||||||
Server Lifetime: {timeSinceProcessStart}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-buttons-secondary rounded-xl scale-95 py-3 px-5 mb-2">
|
|
||||||
<p className="font-bold text-buttons-secondaryText">
|
|
||||||
Overall Views: {totalViews}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Button
|
|
||||||
className="py-px w-60 box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center inline-block"
|
|
||||||
onClick={() => navigate("/flix/sources")}
|
|
||||||
>
|
|
||||||
Most used providers
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pl-6 pr-6">
|
|
||||||
<Divider marginClass="my-3" />
|
|
||||||
{getItemsForCurrentPage().map((item) => {
|
|
||||||
return (
|
|
||||||
<ConfigValue
|
|
||||||
key={item.tmdbFullId}
|
|
||||||
type={isShowOrMovie(item.tmdbFullId)}
|
|
||||||
id={item.tmdbFullId}
|
|
||||||
name={item.title}
|
|
||||||
>
|
|
||||||
{`${
|
|
||||||
item.providerId.charAt(0).toUpperCase() +
|
|
||||||
item.providerId.slice(1)
|
|
||||||
}`}{" "}
|
|
||||||
<strong>-</strong> {`Views: `}
|
|
||||||
<strong>{parseInt(item.count, 10).toLocaleString()}</strong>
|
|
||||||
</ConfigValue>
|
|
||||||
);
|
);
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", justifyContent: "space-between" }}
|
|
||||||
className="mt-5 w-full px-8"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center"
|
|
||||||
onClick={() =>
|
|
||||||
setCurrentPage(currentPage === 1 ? maxPageCount : currentPage - 1)
|
|
||||||
}
|
}
|
||||||
>
|
};
|
||||||
Previous page
|
categories.forEach(fetchMoviesForCategory);
|
||||||
</Button>
|
}, []);
|
||||||
<div
|
|
||||||
style={{ display: "flex", alignItems: "center", cursor: "default" }}
|
// Add a new state variable for the TV show genres
|
||||||
>
|
const [tvGenres, setTVGenres] = useState<Genre[]>([]);
|
||||||
{currentPage}/{maxPageCount}
|
|
||||||
</div>
|
// Add a new state variable for the TV shows
|
||||||
<Button
|
const [tvShowGenres, setTVShowGenres] = useState<{
|
||||||
className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center"
|
[genreId: number]: TVShow[];
|
||||||
onClick={() =>
|
}>({});
|
||||||
setCurrentPage(currentPage === maxPageCount ? 1 : currentPage + 1)
|
|
||||||
|
// Fetch TV show genres
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTVGenres = async () => {
|
||||||
|
try {
|
||||||
|
const data = await get<any>("/genre/tv/list", {
|
||||||
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
|
language: "en-US",
|
||||||
|
});
|
||||||
|
|
||||||
|
setTVGenres(data.genres);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching TV show genres:", error);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTVGenres();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch TV shows for each genre
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTVShowsForGenre = async (genreId: number) => {
|
||||||
|
try {
|
||||||
|
const tvShows: any[] = [];
|
||||||
|
for (let page = 1; page <= pagesToFetch; page += 1) {
|
||||||
|
const data = await get<any>("/discover/tv", {
|
||||||
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
|
with_genres: genreId.toString(),
|
||||||
|
language: "en-US",
|
||||||
|
page: page.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
tvShows.push(...data.results);
|
||||||
|
}
|
||||||
|
setTVShowGenres((prevTVShowGenres) => ({
|
||||||
|
...prevTVShowGenres,
|
||||||
|
[genreId]: tvShows,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching TV shows for genre ${genreId}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tvGenres.forEach((genre) => fetchTVShowsForGenre(genre.id));
|
||||||
|
}, [tvGenres]);
|
||||||
|
|
||||||
|
// Move the hooks outside of the renderMovies function
|
||||||
|
const carouselRef = useRef<HTMLDivElement>(null);
|
||||||
|
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||||
|
const gradientRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Update the scrollCarousel function to use the new ref map
|
||||||
|
function scrollCarousel(categorySlug: string, direction: string) {
|
||||||
|
const carousel = carouselRefs.current[categorySlug];
|
||||||
|
if (carousel) {
|
||||||
|
const movieElements = carousel.getElementsByTagName("a");
|
||||||
|
if (movieElements.length > 0) {
|
||||||
|
const movieWidth = movieElements[0].offsetWidth;
|
||||||
|
const visibleMovies = Math.floor(carousel.offsetWidth / movieWidth);
|
||||||
|
const scrollAmount = movieWidth * visibleMovies;
|
||||||
|
if (direction === "left") {
|
||||||
|
carousel.scrollBy({ left: -scrollAmount, behavior: "smooth" });
|
||||||
|
} else {
|
||||||
|
carousel.scrollBy({ left: scrollAmount, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [movieWidth, setMovieWidth] = useState(
|
||||||
|
window.innerWidth < 600 ? "150px" : "200px",
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setMovieWidth(window.innerWidth < 600 ? "150px" : "200px");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (carouselRef.current && gradientRef.current) {
|
||||||
|
const carouselHeight = carouselRef.current.getBoundingClientRect().height;
|
||||||
|
gradientRef.current.style.top = `${carouselHeight}px`;
|
||||||
|
gradientRef.current.style.bottom = `${carouselHeight}px`;
|
||||||
|
}
|
||||||
|
}, [movieWidth]); // Added movieWidth to the dependency array
|
||||||
|
|
||||||
|
function renderMovies(medias: Media[], category: string, isTVShow = false) {
|
||||||
|
const categorySlug = category.toLowerCase().replace(/ /g, "-"); // Convert the category to a slug
|
||||||
|
const displayCategory =
|
||||||
|
category === "Now Playing"
|
||||||
|
? "In Cinemas"
|
||||||
|
: category.includes("Movie")
|
||||||
|
? `${category}s`
|
||||||
|
: isTVShow
|
||||||
|
? `${category} Programmes`
|
||||||
|
: `${category} Movies`;
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden mt-4">
|
||||||
|
<h2 className="text-2xl font-bold text-white sm:text-3xl md:text-2xl mx-auto pl-10">
|
||||||
|
{displayCategory}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
id={`carousel-${categorySlug}`}
|
||||||
|
className="flex whitespace-nowrap overflow-auto scroll-snap-x-mandatory pb-4 mt-4 pl-10"
|
||||||
|
ref={(el) => {
|
||||||
|
carouselRefs.current[categorySlug] = el;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Next page
|
{medias.slice(0, 100).map((media) => (
|
||||||
</Button>
|
<a
|
||||||
|
key={media.id}
|
||||||
|
href={`media/tmdb-${isTVShow ? "tv" : "movie"}-${media.id}-${
|
||||||
|
isTVShow ? media.name : media.title
|
||||||
|
}`}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block text-center relative overflow-hidden transition-transform transform hover:scale-105 mr-4"
|
||||||
|
style={{ flex: "0 0 auto", width: movieWidth }} // Set a fixed width for each movie
|
||||||
|
>
|
||||||
|
<LazyLoadImage
|
||||||
|
src={`https://image.tmdb.org/t/p/w500${media.poster_path}`}
|
||||||
|
alt={isTVShow ? media.name : media.title}
|
||||||
|
className="rounded-xl mb-2"
|
||||||
|
effect="blur"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "auto",
|
||||||
|
transform: "scale(1)",
|
||||||
|
transition: "opacity 0.3s, transform 0.3s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-xl flex items-center justify-center text-white font-bold opacity-0 hover:opacity-100 transition-opacity"
|
||||||
|
style={{
|
||||||
|
backdropFilter: "blur(0px)",
|
||||||
|
transition: "opacity 0.5s",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.8)", // Darkening effect
|
||||||
|
whiteSpace: "normal", // Allow the text to wrap to the next line
|
||||||
|
wordWrap: "break-word", // Break words to prevent overflow
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-sm m-4">
|
||||||
|
{isTVShow ? media.name : media.title}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ThiccContainer>
|
</a>
|
||||||
</SubPageLayout>
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-10 bottom-10 left-0 w-10 bg-gradient-to-r from-black to-transparent" />
|
||||||
|
<div className="absolute top-10 bottom-10 right-0 w-10 bg-gradient-to-l from-black to-transparent" />
|
||||||
|
<button
|
||||||
|
type="button" // Added type attribute with value "button"
|
||||||
|
className="absolute top-1/2 left-2 transform -translate-y-1/2 z-10"
|
||||||
|
onClick={() => scrollCarousel(categorySlug, "left")}
|
||||||
|
>
|
||||||
|
<Icon icon={Icons.ARROW_LEFT} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button" // Added type attribute with value "button"
|
||||||
|
className="absolute top-1/2 right-2 transform -translate-y-1/2 z-10"
|
||||||
|
onClick={() => scrollCarousel(categorySlug, "right")}
|
||||||
|
>
|
||||||
|
<Icon icon={Icons.ARROW_RIGHT} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRandomMovieClick = () => {
|
||||||
|
const allMovies = Object.values(genreMovies).flat(); // Flatten all movie arrays
|
||||||
|
const randomIndex = Math.floor(Math.random() * allMovies.length);
|
||||||
|
const selectedMovie = allMovies[randomIndex];
|
||||||
|
setRandomMovie(selectedMovie);
|
||||||
|
|
||||||
|
// Start a 5-second countdown
|
||||||
|
setCountdown(5);
|
||||||
|
|
||||||
|
// Schedule navigation after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(`/media/tmdb-movie-${selectedMovie.id}-${selectedMovie.title}`);
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch Movie genres
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchGenres = async () => {
|
||||||
|
try {
|
||||||
|
const data = await get<any>("/genre/movie/list", {
|
||||||
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
|
language: "en-US",
|
||||||
|
});
|
||||||
|
|
||||||
|
setGenres(data.genres);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching genres:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchGenres();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch movies for each genre
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMoviesForGenre = async (genreId: number) => {
|
||||||
|
try {
|
||||||
|
const movies: any[] = [];
|
||||||
|
for (let page = 1; page <= pagesToFetch; page += 1) {
|
||||||
|
const data = await get<any>("/discover/movie", {
|
||||||
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
|
with_genres: genreId.toString(),
|
||||||
|
language: "en-US",
|
||||||
|
page: page.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
movies.push(...data.results);
|
||||||
|
}
|
||||||
|
setGenreMovies((prevGenreMovies) => ({
|
||||||
|
...prevGenreMovies,
|
||||||
|
[genreId]: movies,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching movies for genre ${genreId}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
genres.forEach((genre) => fetchMoviesForGenre(genre.id));
|
||||||
|
}, [genres]);
|
||||||
|
useEffect(() => {
|
||||||
|
let countdownInterval: NodeJS.Timeout;
|
||||||
|
if (countdown !== null && countdown > 0) {
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
setCountdown((prevCountdown) =>
|
||||||
|
prevCountdown !== null ? prevCountdown - 1 : prevCountdown,
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
};
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HomeLayout showBg={showBg}>
|
||||||
|
<div className="mb-16 sm:mb-24">
|
||||||
|
<Helmet>
|
||||||
|
<title>{t("global.name")}</title>
|
||||||
|
</Helmet>
|
||||||
|
{/* Removed HeroPart component */}
|
||||||
|
<ThinContainer>
|
||||||
|
<div className="mt-44 space-y-16 text-center">
|
||||||
|
<div className="relative z-10 mb-16">
|
||||||
|
<h1 className="text-4xl font-bold text-white">Explore</h1>
|
||||||
|
<p className="mb-6">
|
||||||
|
Credits to{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/TecEash1/movie-web/tree/master"
|
||||||
|
className="text-white"
|
||||||
|
style={{
|
||||||
|
color: "#a470dc",
|
||||||
|
textShadow: "0 0 5px #76519f",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
TecEash1
|
||||||
|
</a>{" "}
|
||||||
|
and{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/pranavsatav"
|
||||||
|
className="text-white"
|
||||||
|
style={{
|
||||||
|
color: "#a470dc",
|
||||||
|
textShadow: "0 0 5px #76519f",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aqua Rose
|
||||||
|
</a>{" "}
|
||||||
|
for making this page possible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ThinContainer>
|
||||||
|
</div>
|
||||||
|
<WideContainer>
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-center mt-6 mb-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center space-x-2 rounded-full px-4 text-white py-2 bg-pill-background bg-opacity-50 hover:bg-pill-backgroundHover transition-[background,transform] duration-100 hover:scale-105"
|
||||||
|
onClick={handleRandomMovieClick}
|
||||||
|
disabled={countdown !== null && countdown > 0} // Disable the button during the countdown
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://cdn-icons-png.flaticon.com/512/4058/4058790.png"
|
||||||
|
alt="Small Image"
|
||||||
|
style={{
|
||||||
|
width: "20px", // Adjust the width as needed
|
||||||
|
height: "20px", // Adjust the height as needed
|
||||||
|
marginRight: "10px", // Add margin-right
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{countdown !== null && countdown > 0
|
||||||
|
? `Playing in ${countdown} seconds`
|
||||||
|
: "Watch a Random Movie"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{randomMovie && (
|
||||||
|
<div className="mt-4 mb-4 text-center">
|
||||||
|
<p>Now Playing {randomMovie.title}</p>
|
||||||
|
{/* You can add additional details or play functionality here */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category.name}
|
||||||
|
id={`carousel-${category.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "-")}`}
|
||||||
|
className="mt-8"
|
||||||
|
>
|
||||||
|
{renderMovies(
|
||||||
|
categoryMovies[category.name] || [],
|
||||||
|
category.name,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{genres.map((genre) => (
|
||||||
|
<div
|
||||||
|
key={genre.id}
|
||||||
|
id={`carousel-${genre.name.toLowerCase().replace(/ /g, "-")}`}
|
||||||
|
className="mt-8"
|
||||||
|
>
|
||||||
|
{renderMovies(genreMovies[genre.id] || [], genre.name)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{tvGenres.map((genre) => (
|
||||||
|
<div
|
||||||
|
key={genre.id}
|
||||||
|
id={`carousel-${genre.name.toLowerCase().replace(/ /g, "-")}`}
|
||||||
|
className="mt-8"
|
||||||
|
>
|
||||||
|
{renderMovies(tvShowGenres[genre.id] || [], genre.name, true)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</WideContainer>
|
||||||
|
</HomeLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue