Commit
·
6cf508e
1
Parent(s):
2583ec0
v0.1.2 beta
Browse files- frontend/app/globals.css +19 -12
- frontend/app/layout.tsx +16 -12
- frontend/app/movie/[title]/{LoadingSkeleton.js → LoadingSkeleton.tsx} +19 -19
- frontend/app/movie/[title]/page.js +0 -171
- frontend/app/movie/[title]/page.tsx +261 -0
- frontend/app/movies/{page.js → page.tsx} +40 -18
- frontend/app/mylist/page.tsx +9 -0
- frontend/app/not-found.js +0 -37
- frontend/app/not-found.tsx +122 -0
- frontend/app/{page.js → page.tsx} +68 -22
- frontend/app/tvshow/[title]/{LoadingSkeleton.js → LoadingSkeleton.tsx} +19 -19
- frontend/app/tvshow/[title]/page.js +0 -161
- frontend/app/tvshow/[title]/page.tsx +436 -0
- frontend/app/tvshows/page.js +0 -91
- frontend/app/tvshows/page.tsx +253 -0
- frontend/app/tvshows/page.tsx.tmp +253 -0
- frontend/components/loading/SplashScreen.tsx +102 -0
- frontend/components/movie/{MovieCard.js → MovieCard.tsx} +46 -21
- frontend/components/movie/MovieCardTemp.js +0 -66
- frontend/components/navigation/DesktopMenu.tsx +17 -5
- frontend/components/navigation/MobileMenu.tsx +30 -7
- frontend/components/navigation/Navbar.tsx +18 -6
- frontend/components/sections/CastSection.js +0 -52
- frontend/components/sections/CastSection.tsx +85 -0
- frontend/components/sections/NewContentSection.js +0 -67
- frontend/components/sections/NewContentSection.tsx +45 -0
- frontend/components/sections/ScrollSection.tsx +110 -0
- frontend/components/sections/TrendingSection.js +0 -38
- frontend/components/tvshow/{TvShowCard.js → TvShowCard.tsx} +62 -24
- frontend/components/tvshow/TvShowCardTemp.js +0 -63
- frontend/lib/LoadbalancerAPI.js +147 -130
- frontend/lib/config.js +0 -0
- frontend/lib/config.tsx +1 -0
- frontend/package-lock.json +43 -0
- frontend/package.json +1 -0
- frontend/yarn.lock +23 -2
frontend/app/globals.css
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
@tailwind base;
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
|
@@ -87,24 +96,22 @@
|
|
| 87 |
}
|
| 88 |
}
|
| 89 |
|
| 90 |
-
|
| 91 |
@media screen and (orientation: landscape) {
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
}
|
| 97 |
}
|
| 98 |
|
| 99 |
@media screen and (orientation: portrait) {
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
}
|
| 105 |
}
|
| 106 |
|
| 107 |
-
|
| 108 |
@keyframes kenburns-pan-landscape {
|
| 109 |
0% {
|
| 110 |
background-position: 0% 50%;
|
|
|
|
| 1 |
+
/* Add this to your existing CSS */
|
| 2 |
+
.scrollbar-hide {
|
| 3 |
+
-ms-overflow-style: none; /* IE and Edge */
|
| 4 |
+
scrollbar-width: none; /* Firefox */
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
.scrollbar-hide::-webkit-scrollbar {
|
| 8 |
+
display: none; /* Chrome, Safari and Opera */
|
| 9 |
+
}
|
| 10 |
@tailwind base;
|
| 11 |
@tailwind components;
|
| 12 |
@tailwind utilities;
|
|
|
|
| 96 |
}
|
| 97 |
}
|
| 98 |
|
|
|
|
| 99 |
@media screen and (orientation: landscape) {
|
| 100 |
+
.pan-animation {
|
| 101 |
+
animation: kenburns-pan-landscape 5s ease-in-out infinite;
|
| 102 |
+
background-repeat: no-repeat;
|
| 103 |
+
background-position: center;
|
| 104 |
+
}
|
| 105 |
}
|
| 106 |
|
| 107 |
@media screen and (orientation: portrait) {
|
| 108 |
+
.pan-animation {
|
| 109 |
+
animation: kenburns-pan-portrait 5s ease-in-out infinite;
|
| 110 |
+
background-repeat: no-repeat;
|
| 111 |
+
background-position: top;
|
| 112 |
+
}
|
| 113 |
}
|
| 114 |
|
|
|
|
| 115 |
@keyframes kenburns-pan-landscape {
|
| 116 |
0% {
|
| 117 |
background-position: 0% 50%;
|
frontend/app/layout.tsx
CHANGED
|
@@ -3,23 +3,27 @@ import { Inter } from 'next/font/google';
|
|
| 3 |
import './globals.css';
|
| 4 |
import { Navbar } from '@components/navigation/Navbar';
|
| 5 |
import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
|
|
|
|
| 6 |
|
| 7 |
const inter = Inter({ subsets: ['latin'] });
|
| 8 |
|
| 9 |
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
| 10 |
return (
|
| 11 |
-
<html lang="en">
|
| 12 |
-
<body className={inter.className}>
|
| 13 |
-
<
|
| 14 |
-
<
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
</body>
|
| 24 |
</html>
|
| 25 |
);
|
|
|
|
| 3 |
import './globals.css';
|
| 4 |
import { Navbar } from '@components/navigation/Navbar';
|
| 5 |
import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
|
| 6 |
+
import { LoadingProvider } from '@/components/loading/SplashScreen';
|
| 7 |
|
| 8 |
const inter = Inter({ subsets: ['latin'] });
|
| 9 |
|
| 10 |
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
| 11 |
return (
|
| 12 |
+
<html lang="en" data-oid="dkam3:s">
|
| 13 |
+
<body className={inter.className} data-oid="-:k4nci">
|
| 14 |
+
<LoadingProvider data-oid="mu:2qb-">
|
| 15 |
+
<div className="min-h-screen bg-gray-900 text-white" data-oid="e2xig-g">
|
| 16 |
+
<Navbar data-oid="x1wtrkz" />
|
| 17 |
+
{children}
|
| 18 |
+
<ProgressBar
|
| 19 |
+
height="5px"
|
| 20 |
+
color="#8927e9"
|
| 21 |
+
options={{ showSpinner: false }}
|
| 22 |
+
shallowRouting
|
| 23 |
+
data-oid="ymwuf1d"
|
| 24 |
+
/>
|
| 25 |
+
</div>
|
| 26 |
+
</LoadingProvider>
|
| 27 |
</body>
|
| 28 |
</html>
|
| 29 |
);
|
frontend/app/movie/[title]/{LoadingSkeleton.js → LoadingSkeleton.tsx}
RENAMED
|
@@ -1,29 +1,29 @@
|
|
| 1 |
export function LoadingSkeleton() {
|
| 2 |
return (
|
| 3 |
-
<div className="w-full min-h-screen bg-gray-900 animate-pulse" data-oid="
|
| 4 |
{/* Hero Section Skeleton */}
|
| 5 |
-
<div className="relative w-full h-[60vh] bg-gray-800" data-oid="
|
| 6 |
<div
|
| 7 |
className="absolute inset-0 flex items-center justify-center"
|
| 8 |
-
data-oid="
|
| 9 |
>
|
| 10 |
-
<div className="w-full max-w-7xl px-6 space-y-4" data-oid="
|
| 11 |
<div
|
| 12 |
className="h-12 bg-gray-700 rounded-lg w-3/4 max-w-2xl"
|
| 13 |
-
data-oid=".
|
| 14 |
></div>
|
| 15 |
<div
|
| 16 |
className="h-6 bg-gray-700 rounded-lg w-1/4 max-w-xs"
|
| 17 |
-
data-oid="
|
| 18 |
></div>
|
| 19 |
-
<div className="flex gap-4" data-oid="
|
| 20 |
<div
|
| 21 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 22 |
-
data-oid="
|
| 23 |
></div>
|
| 24 |
<div
|
| 25 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 26 |
-
data-oid="
|
| 27 |
></div>
|
| 28 |
</div>
|
| 29 |
</div>
|
|
@@ -31,25 +31,25 @@ export function LoadingSkeleton() {
|
|
| 31 |
</div>
|
| 32 |
|
| 33 |
{/* Details Section Skeleton */}
|
| 34 |
-
<div className="max-w-7xl mx-auto px-6 py-12 space-y-8" data-oid="
|
| 35 |
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-8" data-oid="
|
| 36 |
-
<div className="md:col-span-2 space-y-4" data-oid="
|
| 37 |
-
<div className="h-8 bg-gray-800 rounded-lg w-1/2" data-oid="
|
| 38 |
<div
|
| 39 |
className="h-32 bg-gray-800 rounded-lg w-full"
|
| 40 |
-
data-oid="
|
| 41 |
></div>
|
| 42 |
-
<div className="h-8 bg-gray-800 rounded-lg w-1/3" data-oid="
|
| 43 |
<div
|
| 44 |
className="h-24 bg-gray-800 rounded-lg w-full"
|
| 45 |
-
data-oid="
|
| 46 |
></div>
|
| 47 |
</div>
|
| 48 |
-
<div className="space-y-4" data-oid="
|
| 49 |
-
<div className="h-8 bg-gray-800 rounded-lg w-full" data-oid="
|
| 50 |
<div
|
| 51 |
className="h-40 bg-gray-800 rounded-lg w-full"
|
| 52 |
-
data-oid="
|
| 53 |
></div>
|
| 54 |
</div>
|
| 55 |
</div>
|
|
|
|
| 1 |
export function LoadingSkeleton() {
|
| 2 |
return (
|
| 3 |
+
<div className="w-full min-h-screen bg-gray-900 animate-pulse" data-oid="55fubag">
|
| 4 |
{/* Hero Section Skeleton */}
|
| 5 |
+
<div className="relative w-full h-[60vh] bg-gray-800" data-oid="x6dgbus">
|
| 6 |
<div
|
| 7 |
className="absolute inset-0 flex items-center justify-center"
|
| 8 |
+
data-oid="6cr1frz"
|
| 9 |
>
|
| 10 |
+
<div className="w-full max-w-7xl px-6 space-y-4" data-oid="xnr2lak">
|
| 11 |
<div
|
| 12 |
className="h-12 bg-gray-700 rounded-lg w-3/4 max-w-2xl"
|
| 13 |
+
data-oid="e.v3sso"
|
| 14 |
></div>
|
| 15 |
<div
|
| 16 |
className="h-6 bg-gray-700 rounded-lg w-1/4 max-w-xs"
|
| 17 |
+
data-oid="u3.2iri"
|
| 18 |
></div>
|
| 19 |
+
<div className="flex gap-4" data-oid="ghrtycm">
|
| 20 |
<div
|
| 21 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 22 |
+
data-oid="5h6-d3i"
|
| 23 |
></div>
|
| 24 |
<div
|
| 25 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 26 |
+
data-oid="8dfdf5j"
|
| 27 |
></div>
|
| 28 |
</div>
|
| 29 |
</div>
|
|
|
|
| 31 |
</div>
|
| 32 |
|
| 33 |
{/* Details Section Skeleton */}
|
| 34 |
+
<div className="max-w-7xl mx-auto px-6 py-12 space-y-8" data-oid="1fd0cj5">
|
| 35 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8" data-oid="uev..vj">
|
| 36 |
+
<div className="md:col-span-2 space-y-4" data-oid="7q8.12o">
|
| 37 |
+
<div className="h-8 bg-gray-800 rounded-lg w-1/2" data-oid="1ibmxaw"></div>
|
| 38 |
<div
|
| 39 |
className="h-32 bg-gray-800 rounded-lg w-full"
|
| 40 |
+
data-oid="7rt8ae9"
|
| 41 |
></div>
|
| 42 |
+
<div className="h-8 bg-gray-800 rounded-lg w-1/3" data-oid="__ka3gb"></div>
|
| 43 |
<div
|
| 44 |
className="h-24 bg-gray-800 rounded-lg w-full"
|
| 45 |
+
data-oid="2b-ajsb"
|
| 46 |
></div>
|
| 47 |
</div>
|
| 48 |
+
<div className="space-y-4" data-oid="s73q162">
|
| 49 |
+
<div className="h-8 bg-gray-800 rounded-lg w-full" data-oid="425zelo"></div>
|
| 50 |
<div
|
| 51 |
className="h-40 bg-gray-800 rounded-lg w-full"
|
| 52 |
+
data-oid="kjwqwuc"
|
| 53 |
></div>
|
| 54 |
</div>
|
| 55 |
</div>
|
frontend/app/movie/[title]/page.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from 'react';
|
| 4 |
-
import { useParams } from 'next/navigation';
|
| 5 |
-
import { getMovieMetadata } from '@/lib/lb';
|
| 6 |
-
import { LoadingSkeleton } from './LoadingSkeleton';
|
| 7 |
-
import { convertMinutesToHM } from '@lib/utils';
|
| 8 |
-
import Link from 'next/link';
|
| 9 |
-
import CastSection from '@/components/sections/CastSection';
|
| 10 |
-
|
| 11 |
-
export default function MovieTitlePage() {
|
| 12 |
-
const { title } = useParams();
|
| 13 |
-
const decodedTitle = decodeURIComponent(title);
|
| 14 |
-
const [movie, setMovie] = useState(null);
|
| 15 |
-
|
| 16 |
-
useEffect(() => {
|
| 17 |
-
async function fetchMovie() {
|
| 18 |
-
try {
|
| 19 |
-
const data = await getMovieMetadata(decodedTitle);
|
| 20 |
-
setMovie(data.data);
|
| 21 |
-
} catch (error) {
|
| 22 |
-
console.error('Failed to fetch movie details', error);
|
| 23 |
-
}
|
| 24 |
-
}
|
| 25 |
-
fetchMovie();
|
| 26 |
-
}, [decodedTitle]);
|
| 27 |
-
|
| 28 |
-
if (!movie) {
|
| 29 |
-
return <LoadingSkeleton />;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
return (
|
| 33 |
-
<div
|
| 34 |
-
className="relative w-full min-h-screen h-full flex items-start landscape:pt-36 pt-40 text-white"
|
| 35 |
-
style={{
|
| 36 |
-
backgroundImage: `url(${movie.image})`,
|
| 37 |
-
backgroundSize: 'cover',
|
| 38 |
-
backgroundPosition: 'center',
|
| 39 |
-
}}
|
| 40 |
-
>
|
| 41 |
-
<div className="h-screen fixed inset-0 bg-gray-900 bg-opacity-50">
|
| 42 |
-
<div className="h-2/6"></div>
|
| 43 |
-
<div className="h-4/6 bg-gradient-to-t from-gray-900 to-transparent"></div>
|
| 44 |
-
</div>
|
| 45 |
-
<div className="relative z-10 max-w-1/3 landscape:mx-40 mx-6 min-h-screen">
|
| 46 |
-
<div className="relative z-10 max-w-2/6 w-5/6 landscape:w-4/6 flex-col items-start justify-start bg-gray-800 bg-opacity-50 backdrop-blur-md p-4 rounded-xl">
|
| 47 |
-
<h1 className="landscape:text-3xl text-2xl font-bold mb-2 text-gray-200">
|
| 48 |
-
{movie.translations?.nameTranslations?.find((t) => t.language === 'eng')
|
| 49 |
-
?.name ||
|
| 50 |
-
movie.translations?.nameTranslations?.find(
|
| 51 |
-
(t) => t.isAlias && t.language === 'eng',
|
| 52 |
-
)?.name ||
|
| 53 |
-
'Unknown Title'}
|
| 54 |
-
({movie.year})
|
| 55 |
-
</h1>
|
| 56 |
-
|
| 57 |
-
{/* Genres */}
|
| 58 |
-
<span className="text-purple-400 text-base font-semibold">
|
| 59 |
-
{Array.isArray(movie.genres)
|
| 60 |
-
? movie.genres.map((genre, index) => (
|
| 61 |
-
<span key={genre.id}>
|
| 62 |
-
<Link
|
| 63 |
-
href={`/genre/${encodeURIComponent(genre.slug)}`}
|
| 64 |
-
className="hover:underline"
|
| 65 |
-
>
|
| 66 |
-
{genre.name}
|
| 67 |
-
</Link>
|
| 68 |
-
{index < movie.genres.length - 1 && ', '}
|
| 69 |
-
</span>
|
| 70 |
-
))
|
| 71 |
-
: null}
|
| 72 |
-
</span>
|
| 73 |
-
{/* Improved Score Display */}
|
| 74 |
-
<div className="mb-2 mt-2 flex items-center">
|
| 75 |
-
<span className="bg-gradient-to-r from-violet-500 to-purple-400 text-gray-700 px-3 py-1 rounded-md text-sm font-semibold">
|
| 76 |
-
⭐ {movie.score}
|
| 77 |
-
</span>
|
| 78 |
-
</div>
|
| 79 |
-
{/* Improved Content Ratings Display */}
|
| 80 |
-
{movie.contentRatings?.length > 0 ? (
|
| 81 |
-
<div className="mt-2 text-gray-300">
|
| 82 |
-
<p className="text-sm font-semibold mb-1">Content Ratings:</p>
|
| 83 |
-
<ul className="flex flex-wrap gap-2">
|
| 84 |
-
{movie.contentRatings.map((rating, index) => {
|
| 85 |
-
// Map country codes to corresponding flags
|
| 86 |
-
const countryFlags = {
|
| 87 |
-
AUS: '🇦🇺',
|
| 88 |
-
USA: '🇺🇸',
|
| 89 |
-
GBR: '🇬🇧',
|
| 90 |
-
EU: '🇪🇺',
|
| 91 |
-
JPN: '🇯🇵',
|
| 92 |
-
KOR: '🇰🇷',
|
| 93 |
-
CAN: '🇨🇦',
|
| 94 |
-
DEU: '🇩🇪',
|
| 95 |
-
FRA: '🇫🇷',
|
| 96 |
-
ESP: '🇪🇸',
|
| 97 |
-
ITA: '🇮🇹',
|
| 98 |
-
};
|
| 99 |
-
|
| 100 |
-
const flag = countryFlags[rating.country.toUpperCase()] || '🌍'; // Default to globe if no match
|
| 101 |
-
|
| 102 |
-
return (
|
| 103 |
-
<li
|
| 104 |
-
key={index}
|
| 105 |
-
className="flex items-center bg-gray-800/50 px-2 py-1 rounded-md text-xs"
|
| 106 |
-
>
|
| 107 |
-
<span className="mr-2">{flag}</span>
|
| 108 |
-
<span className="text-white font-semibold">
|
| 109 |
-
{rating.name}
|
| 110 |
-
</span>
|
| 111 |
-
<span className="text-gray-400 ml-1">
|
| 112 |
-
{' '}
|
| 113 |
-
- {rating.description || 'N/A'}
|
| 114 |
-
</span>
|
| 115 |
-
</li>
|
| 116 |
-
);
|
| 117 |
-
})}
|
| 118 |
-
</ul>
|
| 119 |
-
</div>
|
| 120 |
-
) : (
|
| 121 |
-
<p className="text-gray-400 text-sm">No content ratings available.</p>
|
| 122 |
-
)}
|
| 123 |
-
|
| 124 |
-
<span className="text-gray-300 text-sm font-semibold">
|
| 125 |
-
{movie.spoken_languages?.map((lang, index) => (
|
| 126 |
-
<span key={index}>
|
| 127 |
-
{lang.toUpperCase()}
|
| 128 |
-
|
| 129 |
-
{index < movie.spoken_languages.length - 1 && ', '}
|
| 130 |
-
</span>
|
| 131 |
-
))}
|
| 132 |
-
</span>
|
| 133 |
-
|
| 134 |
-
<div className="flex justify-start landscape:gap-2 mt-4">
|
| 135 |
-
<Link
|
| 136 |
-
href={'#play'}
|
| 137 |
-
className="bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-s-2xl flex items-center transition-colors text-sm md:text-base"
|
| 138 |
-
>
|
| 139 |
-
<svg
|
| 140 |
-
className="w-4 h-4 md:w-5 md:h-5 mr-2"
|
| 141 |
-
fill="currentColor"
|
| 142 |
-
viewBox="0 0 20 20"
|
| 143 |
-
>
|
| 144 |
-
<path d="M4 4l12 6-12 6V4z" />
|
| 145 |
-
</svg>
|
| 146 |
-
Play Now
|
| 147 |
-
</Link>
|
| 148 |
-
<Link
|
| 149 |
-
href={'#'}
|
| 150 |
-
className="bg-gray-800/80 hover:bg-gray-700/80 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-e-2xl transition-colors text-sm md:text-base"
|
| 151 |
-
>
|
| 152 |
-
Add to My List
|
| 153 |
-
</Link>
|
| 154 |
-
</div>
|
| 155 |
-
</div>
|
| 156 |
-
<div className="flex flex-col landscape:flex-row gap-6 mt-4">
|
| 157 |
-
{/* Overview Section */}
|
| 158 |
-
<div className="flex-1 bg-gray-800 bg-opacity-50 backdrop-blur-md p-4 rounded-xl">
|
| 159 |
-
<p className="text-gray-400">
|
| 160 |
-
{movie.translations?.overviewTranslations?.find(
|
| 161 |
-
(t) => t.language === 'eng',
|
| 162 |
-
)?.overview || 'No overview available.'}
|
| 163 |
-
</p>
|
| 164 |
-
</div>
|
| 165 |
-
|
| 166 |
-
<CastSection movie={movie} />
|
| 167 |
-
</div>
|
| 168 |
-
</div>
|
| 169 |
-
</div>
|
| 170 |
-
);
|
| 171 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/movie/[title]/page.tsx
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react';
|
| 4 |
+
import { useParams } from 'next/navigation';
|
| 5 |
+
import { getMovieMetadata } from '@/lib/lb';
|
| 6 |
+
import { LoadingSkeleton } from './LoadingSkeleton';
|
| 7 |
+
import { convertMinutesToHM } from '@lib/utils';
|
| 8 |
+
import Link from 'next/link';
|
| 9 |
+
import CastSection from '@/components/sections/CastSection';
|
| 10 |
+
|
| 11 |
+
export default function MovieTitlePage() {
|
| 12 |
+
const { title } = useParams();
|
| 13 |
+
const decodedTitle = decodeURIComponent(Array.isArray(title) ? title[0] : title);
|
| 14 |
+
|
| 15 |
+
interface Movie {
|
| 16 |
+
name: string;
|
| 17 |
+
image: string;
|
| 18 |
+
translations?: any;
|
| 19 |
+
year?: number;
|
| 20 |
+
genres?: any[];
|
| 21 |
+
score?: number;
|
| 22 |
+
contentRatings?: any[];
|
| 23 |
+
characters?: any;
|
| 24 |
+
artworks?: any[];
|
| 25 |
+
file_structure?: any;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const [movie, setMovie] = useState<Movie | null>(null);
|
| 29 |
+
|
| 30 |
+
useEffect(() => {
|
| 31 |
+
async function fetchMovie() {
|
| 32 |
+
try {
|
| 33 |
+
const data = await getMovieMetadata(decodedTitle);
|
| 34 |
+
setMovie(data.data as Movie);
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error('Failed to fetch movie details', error);
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
fetchMovie();
|
| 40 |
+
}, [decodedTitle]);
|
| 41 |
+
|
| 42 |
+
if (!movie) {
|
| 43 |
+
return <LoadingSkeleton data-oid="4z94._3" />;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
return (
|
| 47 |
+
<div
|
| 48 |
+
className="relative w-full min-h-screen h-full flex items-start landscape:pt-24 pt-28 text-white"
|
| 49 |
+
style={{
|
| 50 |
+
backgroundImage: `url(${movie.artworks?.[0]?.image || movie.image})`,
|
| 51 |
+
backgroundSize: 'cover',
|
| 52 |
+
backgroundPosition: 'top',
|
| 53 |
+
backgroundAttachment: 'fixed',
|
| 54 |
+
}}
|
| 55 |
+
data-oid="revv9kh"
|
| 56 |
+
>
|
| 57 |
+
{/* Gradient Overlays */}
|
| 58 |
+
<div className="h-screen fixed inset-0" data-oid="9cjzadn">
|
| 59 |
+
<div
|
| 60 |
+
className="h-full bg-gradient-to-b from-gray-900/90 via-gray-900/50 to-gray-900/90"
|
| 61 |
+
data-oid=":j124gq"
|
| 62 |
+
></div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
{/* Main Content Container */}
|
| 66 |
+
<div
|
| 67 |
+
className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
|
| 68 |
+
data-oid="jn:xenk"
|
| 69 |
+
>
|
| 70 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8" data-oid="7qn5zd.">
|
| 71 |
+
{/* Left Column - Main Info */}
|
| 72 |
+
<div className="lg:col-span-2 space-y-6" data-oid="9soi9yh">
|
| 73 |
+
<div
|
| 74 |
+
className="bg-gray-800/60 backdrop-blur-md rounded-2xl p-6 border border-gray-700/50"
|
| 75 |
+
data-oid="i3s6s8."
|
| 76 |
+
>
|
| 77 |
+
{/* Title and Year */}
|
| 78 |
+
<div className="space-y-2" data-oid="etbp9oi">
|
| 79 |
+
<h1 className="text-4xl font-bold text-white" data-oid="dsob41d">
|
| 80 |
+
{movie.translations?.nameTranslations?.find(
|
| 81 |
+
(t: { language: string; name: string }) =>
|
| 82 |
+
t.language === 'eng',
|
| 83 |
+
)?.name ||
|
| 84 |
+
movie.translations?.nameTranslations?.find(
|
| 85 |
+
(t: {
|
| 86 |
+
isAlias: boolean;
|
| 87 |
+
language: string;
|
| 88 |
+
name: string;
|
| 89 |
+
}) => t.isAlias && t.language === 'eng',
|
| 90 |
+
)?.name ||
|
| 91 |
+
movie?.name ||
|
| 92 |
+
'Unknown Title'}
|
| 93 |
+
</h1>
|
| 94 |
+
<div
|
| 95 |
+
className="flex items-center gap-3 text-gray-300"
|
| 96 |
+
data-oid="df5cjt:"
|
| 97 |
+
>
|
| 98 |
+
<span className="text-lg" data-oid="sfvh8h.">
|
| 99 |
+
{movie.year}
|
| 100 |
+
</span>
|
| 101 |
+
<span data-oid="8s72a-.">•</span>
|
| 102 |
+
<span
|
| 103 |
+
className="bg-purple-500/20 text-purple-300 px-2 py-0.5 rounded text-sm"
|
| 104 |
+
data-oid="xel06np"
|
| 105 |
+
>
|
| 106 |
+
Movie
|
| 107 |
+
</span>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
{/* Genres and Score */}
|
| 111 |
+
<div
|
| 112 |
+
className="flex flex-wrap items-center gap-4 mt-4"
|
| 113 |
+
data-oid="u8ozk4w"
|
| 114 |
+
>
|
| 115 |
+
<div className="flex items-center gap-2" data-oid="he6yrlk">
|
| 116 |
+
<span
|
| 117 |
+
className="bg-purple-500 text-white px-3 py-1 rounded-full text-sm font-medium"
|
| 118 |
+
data-oid="rnhvohx"
|
| 119 |
+
>
|
| 120 |
+
⭐ {movie.score ? (movie.score / 1000).toFixed(1) : 'N/A'}
|
| 121 |
+
</span>
|
| 122 |
+
</div>
|
| 123 |
+
<div className="flex flex-wrap gap-2" data-oid="dp4ku:9">
|
| 124 |
+
{Array.isArray(movie.genres)
|
| 125 |
+
? movie.genres.map((genre) => (
|
| 126 |
+
<Link
|
| 127 |
+
key={genre.id}
|
| 128 |
+
href={`/genre/${encodeURIComponent(genre.slug)}`}
|
| 129 |
+
className="bg-gray-700/50 hover:bg-gray-600/50 text-gray-200 px-3 py-1 rounded-full text-sm transition-colors"
|
| 130 |
+
data-oid="jhs32ty"
|
| 131 |
+
>
|
| 132 |
+
{genre.name}
|
| 133 |
+
</Link>
|
| 134 |
+
))
|
| 135 |
+
: null}
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
{/* Content Ratings */}
|
| 139 |
+
{Array.isArray(movie.contentRatings) &&
|
| 140 |
+
movie.contentRatings.length > 0 ? (
|
| 141 |
+
<div className="mt-6" data-oid="1yk1r-p">
|
| 142 |
+
<h3
|
| 143 |
+
className="text-lg font-semibold text-gray-200 mb-3"
|
| 144 |
+
data-oid="dni4581"
|
| 145 |
+
>
|
| 146 |
+
Content Ratings
|
| 147 |
+
</h3>
|
| 148 |
+
<ul className="flex flex-wrap gap-3" data-oid="el5h0bp">
|
| 149 |
+
{movie.contentRatings.map((rating, index) => {
|
| 150 |
+
// Map country codes to corresponding flags
|
| 151 |
+
const countryFlags = {
|
| 152 |
+
AUS: '🇦🇺',
|
| 153 |
+
USA: '🇺🇸',
|
| 154 |
+
GBR: '🇬🇧',
|
| 155 |
+
EU: '🇪🇺',
|
| 156 |
+
JPN: '🇯🇵',
|
| 157 |
+
KOR: '🇰🇷',
|
| 158 |
+
CAN: '🇨🇦',
|
| 159 |
+
DEU: '🇩🇪',
|
| 160 |
+
FRA: '🇫🇷',
|
| 161 |
+
ESP: '🇪🇸',
|
| 162 |
+
ITA: '🇮🇹',
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
const flag =
|
| 166 |
+
countryFlags[
|
| 167 |
+
rating.country.toUpperCase() as keyof typeof countryFlags
|
| 168 |
+
] || '🌍'; // Default to globe if no match
|
| 169 |
+
|
| 170 |
+
return (
|
| 171 |
+
<li
|
| 172 |
+
key={index}
|
| 173 |
+
className="flex items-center bg-gray-800/50 px-2 py-1 rounded-md text-xs"
|
| 174 |
+
data-oid="jyj-mez"
|
| 175 |
+
>
|
| 176 |
+
<span className="mr-2" data-oid="e-5n.a3">
|
| 177 |
+
{flag}
|
| 178 |
+
</span>
|
| 179 |
+
<span
|
| 180 |
+
className="text-white font-semibold"
|
| 181 |
+
data-oid="9:ua6ec"
|
| 182 |
+
>
|
| 183 |
+
{rating.name}
|
| 184 |
+
</span>
|
| 185 |
+
<span
|
| 186 |
+
className="text-gray-400 ml-1"
|
| 187 |
+
data-oid="iq0nf4s"
|
| 188 |
+
>
|
| 189 |
+
{' '}
|
| 190 |
+
- {rating.description || 'N/A'}
|
| 191 |
+
</span>
|
| 192 |
+
</li>
|
| 193 |
+
);
|
| 194 |
+
})}
|
| 195 |
+
</ul>
|
| 196 |
+
</div>
|
| 197 |
+
) : (
|
| 198 |
+
<p className="text-gray-400 text-sm" data-oid="z2khxq1">
|
| 199 |
+
No content ratings available.
|
| 200 |
+
</p>
|
| 201 |
+
)}
|
| 202 |
+
{/* Action Buttons */}{' '}
|
| 203 |
+
<div
|
| 204 |
+
className="flex justify-start landscape:gap-2 mt-4"
|
| 205 |
+
data-oid="xaulnx5"
|
| 206 |
+
>
|
| 207 |
+
<Link
|
| 208 |
+
href={'#play'}
|
| 209 |
+
className="bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-s-2xl flex items-center transition-colors text-sm md:text-base"
|
| 210 |
+
data-oid="nt4wfms"
|
| 211 |
+
>
|
| 212 |
+
<svg
|
| 213 |
+
className="w-4 h-4 md:w-5 md:h-5 mr-2"
|
| 214 |
+
fill="currentColor"
|
| 215 |
+
viewBox="0 0 20 20"
|
| 216 |
+
data-oid="bji9c:e"
|
| 217 |
+
>
|
| 218 |
+
<path d="M4 4l12 6-12 6V4z" data-oid="f9vgah4" />
|
| 219 |
+
</svg>
|
| 220 |
+
Play Now
|
| 221 |
+
</Link>
|
| 222 |
+
<Link
|
| 223 |
+
href={'#'}
|
| 224 |
+
className="bg-gray-800/80 hover:bg-gray-700/80 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-e-2xl transition-colors text-sm md:text-base"
|
| 225 |
+
data-oid=":33g7ew"
|
| 226 |
+
>
|
| 227 |
+
Add to My List
|
| 228 |
+
</Link>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
{/* Overview Section */}
|
| 232 |
+
<div
|
| 233 |
+
className="bg-gray-800/60 backdrop-blur-md rounded-2xl p-6 border border-gray-700/50"
|
| 234 |
+
data-oid="_:x12te"
|
| 235 |
+
>
|
| 236 |
+
<h3
|
| 237 |
+
className="text-lg font-semibold text-gray-200 mb-3"
|
| 238 |
+
data-oid="3wu.i__"
|
| 239 |
+
>
|
| 240 |
+
Overview
|
| 241 |
+
</h3>
|
| 242 |
+
<p className="text-gray-300 leading-relaxed" data-oid="bvgsgiv">
|
| 243 |
+
{movie.translations?.overviewTranslations?.find(
|
| 244 |
+
(t: { language: string; overview: string }) =>
|
| 245 |
+
t.language === 'eng',
|
| 246 |
+
)?.overview ||
|
| 247 |
+
movie.translations?.overviewTranslations?.[0]?.overview ||
|
| 248 |
+
'No overview available.'}
|
| 249 |
+
</p>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
+
{/* Right Column - Cast */}
|
| 254 |
+
<div className="lg:col-span-1" data-oid="5-5evlt">
|
| 255 |
+
<CastSection movie={movie} data-oid="8d3b-36" />
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
);
|
| 261 |
+
}
|
frontend/app/movies/{page.js → page.tsx}
RENAMED
|
@@ -4,13 +4,14 @@ import { useEffect, useState, Suspense } from 'react';
|
|
| 4 |
import { useSearchParams, useRouter } from 'next/navigation';
|
| 5 |
import { getAllMovies } from '@/lib/lb';
|
| 6 |
import { MovieCard } from '@/components/movie/MovieCard';
|
|
|
|
| 7 |
|
| 8 |
function MoviesContent() {
|
| 9 |
const searchParams = useSearchParams();
|
| 10 |
const router = useRouter();
|
| 11 |
const currentPage = parseInt(searchParams.get('page') || '1', 10);
|
| 12 |
const [movies, setMovies] = useState([]);
|
| 13 |
-
const
|
| 14 |
const itemsPerPage = 16;
|
| 15 |
const [totalPages, setTotalPages] = useState(1);
|
| 16 |
|
|
@@ -33,33 +34,44 @@ function MoviesContent() {
|
|
| 33 |
fetchMovies();
|
| 34 |
}, [currentPage]);
|
| 35 |
|
| 36 |
-
const handlePageChange = (newPage) => {
|
| 37 |
router.push(`/movies?page=${newPage}`);
|
| 38 |
};
|
| 39 |
|
| 40 |
return (
|
| 41 |
<>
|
| 42 |
{loading ? (
|
| 43 |
-
<p className="text-center">
|
|
|
|
|
|
|
| 44 |
) : (
|
| 45 |
<>
|
| 46 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 47 |
{movies.map((title, index) => (
|
| 48 |
-
<MovieCard title={title} key={index} />
|
| 49 |
))}
|
| 50 |
</div>
|
| 51 |
-
<div className="flex justify-center mt-8 gap-4">
|
| 52 |
-
<button
|
| 53 |
-
onClick={() => handlePageChange(currentPage - 1)}
|
| 54 |
disabled={currentPage <= 1}
|
| 55 |
-
className="px-4 py-2 bg-gray-700 text-white rounded disabled:opacity-50"
|
|
|
|
|
|
|
| 56 |
Previous
|
| 57 |
</button>
|
| 58 |
-
<span className="px-4 py-2 text-white"
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
disabled={currentPage >= totalPages}
|
| 62 |
-
className="px-4 py-2 bg-gray-700 text-white rounded disabled:opacity-50"
|
|
|
|
|
|
|
| 63 |
Next
|
| 64 |
</button>
|
| 65 |
</div>
|
|
@@ -71,13 +83,23 @@ function MoviesContent() {
|
|
| 71 |
|
| 72 |
export default function MoviesPage() {
|
| 73 |
return (
|
| 74 |
-
<div className="py-16">
|
| 75 |
-
<div className="container mx-auto px-6">
|
| 76 |
-
<h2
|
|
|
|
|
|
|
|
|
|
| 77 |
Discover Movies
|
| 78 |
</h2>
|
| 79 |
-
<Suspense
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</Suspense>
|
| 82 |
</div>
|
| 83 |
</div>
|
|
|
|
| 4 |
import { useSearchParams, useRouter } from 'next/navigation';
|
| 5 |
import { getAllMovies } from '@/lib/lb';
|
| 6 |
import { MovieCard } from '@/components/movie/MovieCard';
|
| 7 |
+
import { useLoading } from '@/components/loading/SplashScreen';
|
| 8 |
|
| 9 |
function MoviesContent() {
|
| 10 |
const searchParams = useSearchParams();
|
| 11 |
const router = useRouter();
|
| 12 |
const currentPage = parseInt(searchParams.get('page') || '1', 10);
|
| 13 |
const [movies, setMovies] = useState([]);
|
| 14 |
+
const { loading, setLoading } = useLoading();
|
| 15 |
const itemsPerPage = 16;
|
| 16 |
const [totalPages, setTotalPages] = useState(1);
|
| 17 |
|
|
|
|
| 34 |
fetchMovies();
|
| 35 |
}, [currentPage]);
|
| 36 |
|
| 37 |
+
const handlePageChange = (newPage: number) => {
|
| 38 |
router.push(`/movies?page=${newPage}`);
|
| 39 |
};
|
| 40 |
|
| 41 |
return (
|
| 42 |
<>
|
| 43 |
{loading ? (
|
| 44 |
+
<p className="text-center" data-oid="wt363:4">
|
| 45 |
+
Loading...
|
| 46 |
+
</p>
|
| 47 |
) : (
|
| 48 |
<>
|
| 49 |
+
<div
|
| 50 |
+
className="grid grid-cols-2 md:grid-cols-6 gap-4 md:gap-6"
|
| 51 |
+
data-oid="nnyjyh_"
|
| 52 |
+
>
|
| 53 |
{movies.map((title, index) => (
|
| 54 |
+
<MovieCard title={title} key={index} data-oid="qbq--rn" />
|
| 55 |
))}
|
| 56 |
</div>
|
| 57 |
+
<div className="flex justify-center mt-8 gap-4" data-oid="o8hwe0o">
|
| 58 |
+
<button
|
| 59 |
+
onClick={() => handlePageChange(currentPage - 1)}
|
| 60 |
disabled={currentPage <= 1}
|
| 61 |
+
className="px-4 py-2 bg-gray-700 text-white rounded disabled:opacity-50"
|
| 62 |
+
data-oid="in9w58i"
|
| 63 |
+
>
|
| 64 |
Previous
|
| 65 |
</button>
|
| 66 |
+
<span className="px-4 py-2 text-white" data-oid=":f4i:-h">
|
| 67 |
+
Page {currentPage} of {totalPages}
|
| 68 |
+
</span>
|
| 69 |
+
<button
|
| 70 |
+
onClick={() => handlePageChange(currentPage + 1)}
|
| 71 |
disabled={currentPage >= totalPages}
|
| 72 |
+
className="px-4 py-2 bg-gray-700 text-white rounded disabled:opacity-50"
|
| 73 |
+
data-oid="fn1v5nv"
|
| 74 |
+
>
|
| 75 |
Next
|
| 76 |
</button>
|
| 77 |
</div>
|
|
|
|
| 83 |
|
| 84 |
export default function MoviesPage() {
|
| 85 |
return (
|
| 86 |
+
<div className="py-16" data-oid="7-jgkqc">
|
| 87 |
+
<div className="container mx-auto px-6" data-oid="ww2qum_">
|
| 88 |
+
<h2
|
| 89 |
+
className="text-2xl font-bold mb-8 bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
|
| 90 |
+
data-oid="5d7mzrh"
|
| 91 |
+
>
|
| 92 |
Discover Movies
|
| 93 |
</h2>
|
| 94 |
+
<Suspense
|
| 95 |
+
fallback={
|
| 96 |
+
<p className="text-center" data-oid="7lprvmz">
|
| 97 |
+
Loading...
|
| 98 |
+
</p>
|
| 99 |
+
}
|
| 100 |
+
data-oid="5dnkf7b"
|
| 101 |
+
>
|
| 102 |
+
<MoviesContent data-oid="ln0l91t" />
|
| 103 |
</Suspense>
|
| 104 |
</div>
|
| 105 |
</div>
|
frontend/app/mylist/page.tsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useLoading } from '@/components/loading/SplashScreen';
|
| 4 |
+
|
| 5 |
+
export default function MyListPage() {
|
| 6 |
+
const { loading, setLoading } = useLoading();
|
| 7 |
+
setLoading(true);
|
| 8 |
+
return <div data-oid="3bux562"></div>;
|
| 9 |
+
}
|
frontend/app/not-found.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import Link from 'next/link';
|
| 4 |
-
import { useEffect, useState } from 'react';
|
| 5 |
-
|
| 6 |
-
export default function NotFound() {
|
| 7 |
-
const [floating, setFloating] = useState(false);
|
| 8 |
-
|
| 9 |
-
useEffect(() => {
|
| 10 |
-
const interval = setInterval(() => {
|
| 11 |
-
setFloating((prev) => !prev);
|
| 12 |
-
}, 2000);
|
| 13 |
-
return () => clearInterval(interval);
|
| 14 |
-
}, []);
|
| 15 |
-
|
| 16 |
-
return (
|
| 17 |
-
<div className="flex flex-col items-center justify-center min-h-screen text-white">
|
| 18 |
-
<h1
|
| 19 |
-
className={`text-6xl font-bold text-gray-300 transition-transform duration-500 ${
|
| 20 |
-
floating ? 'translate-y-1' : '-translate-y-1'
|
| 21 |
-
}`}
|
| 22 |
-
>
|
| 23 |
-
<svg
|
| 24 |
-
fill="#b8b8b8" height="200px" width="200px" version="1.1" id="Layer_1" viewBox="0 0 512 512" stroke="#b8b8b8"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <g> <path d="M170.667,320.007V320c0-11.782-9.551-21.333-21.333-21.333C137.552,298.667,128,308.218,128,320v-56.89 c0-11.782-9.551-21.333-21.333-21.333s-21.333,9.551-21.333,21.333v78.229c-0.001,11.783,9.551,21.334,21.333,21.334H128v42.66 c0,11.782,9.551,21.333,21.333,21.333s21.333-9.551,21.333-21.333v-42.66c11.782,0,21.333-9.551,21.333-21.333 C192,329.558,182.449,320.007,170.667,320.007z"></path> </g> </g> <g> <g> <path d="M426.667,320.007V320c0-11.782-9.551-21.333-21.333-21.333C393.552,298.667,384,308.218,384,320v-56.89 c0-11.782-9.551-21.333-21.333-21.333c-11.782,0-21.333,9.551-21.333,21.333v78.229c-0.001,11.783,9.551,21.334,21.333,21.334H384 v42.66c0,11.782,9.551,21.333,21.333,21.333c11.782,0,21.333-9.551,21.333-21.333v-42.66c11.782,0,21.333-9.551,21.333-21.333 C448,329.558,438.449,320.007,426.667,320.007z"></path> </g> </g> <g> <g> <path d="M266.667,256c-29.446,0-53.333,23.887-53.333,53.333v64c-0.001,29.446,23.887,53.334,53.333,53.334 S320,402.78,320,373.334v-64C320,279.887,296.114,256,266.667,256z M277.334,373.333c0,5.882-4.785,10.667-10.667,10.667 c-5.882,0-10.667-4.785-10.667-10.667v-64c0-5.882,4.785-10.667,10.667-10.667c5.882,0,10.667,4.785,10.667,10.667V373.333z"></path> </g> </g> <g> <g> <path d="M490.667,0H21.333C9.552,0,0,9.551,0,21.333V192v298.667C0,502.449,9.552,512,21.333,512h469.333 c11.782,0,21.333-9.551,21.333-21.333V192V21.333C512,9.551,502.45,0,490.667,0z M469.334,469.333L469.334,469.333H42.667v-256 h426.667V469.333z M469.334,170.667H42.667v-128h426.667V170.667z"></path> </g> </g> <g> <g> <path d="M435.505,106.666l6.248-6.248c8.331-8.331,8.331-21.838,0-30.17c-8.331-8.331-21.839-8.331-30.17,0l-6.248,6.248 l-6.248-6.248c-8.331-8.331-21.839-8.331-30.17,0c-8.331,8.331-8.331,21.839,0,30.17l6.248,6.248l-6.248,6.248 c-8.331,8.331-8.331,21.839,0,30.17s21.839,8.331,30.17,0l6.248-6.248l6.248,6.248c8.331,8.331,21.839,8.331,30.17,0 s8.331-21.839,0-30.17L435.505,106.666z"></path> </g> </g> <g> <g> <path d="M256,85.333H85.333C73.552,85.333,64,94.885,64,106.667S73.552,128,85.333,128H256c11.782,0,21.333-9.551,21.333-21.333 S267.783,85.333,256,85.333z"></path> </g> </g> </g>
|
| 25 |
-
</svg>
|
| 26 |
-
</h1>
|
| 27 |
-
|
| 28 |
-
<p className="text-xl text-gray-400 mt-4">Oops! The page you’re looking for doesn’t exist.</p>
|
| 29 |
-
<Link
|
| 30 |
-
href="/"
|
| 31 |
-
className="mt-5 px-6 py-2 rounded-3xl bg-purple-600 hover:bg-purple-500 transition-all text-white text-lg font-semibold shadow-lg shadow-purple-500/30"
|
| 32 |
-
>
|
| 33 |
-
Go Home
|
| 34 |
-
</Link>
|
| 35 |
-
</div>
|
| 36 |
-
);
|
| 37 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/not-found.tsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import Link from 'next/link';
|
| 4 |
+
import { useEffect, useState } from 'react';
|
| 5 |
+
|
| 6 |
+
export default function NotFound() {
|
| 7 |
+
const [floating, setFloating] = useState(false);
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
const interval = setInterval(() => {
|
| 11 |
+
setFloating((prev) => !prev);
|
| 12 |
+
}, 2000);
|
| 13 |
+
return () => clearInterval(interval);
|
| 14 |
+
}, []);
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<div
|
| 18 |
+
className="flex flex-col items-center justify-center min-h-screen text-white"
|
| 19 |
+
data-oid="pou15yo"
|
| 20 |
+
>
|
| 21 |
+
<h1
|
| 22 |
+
className={`text-6xl font-bold text-gray-300 transition-transform duration-500 ${
|
| 23 |
+
floating ? 'translate-y-1' : '-translate-y-1'
|
| 24 |
+
}`}
|
| 25 |
+
data-oid="6f5x09d"
|
| 26 |
+
>
|
| 27 |
+
<svg
|
| 28 |
+
fill="#b8b8b8"
|
| 29 |
+
height="200px"
|
| 30 |
+
width="200px"
|
| 31 |
+
version="1.1"
|
| 32 |
+
id="Layer_1"
|
| 33 |
+
viewBox="0 0 512 512"
|
| 34 |
+
stroke="#b8b8b8"
|
| 35 |
+
data-oid="z4xly:t"
|
| 36 |
+
>
|
| 37 |
+
<g id="SVGRepo_bgCarrier" stroke-width="0" data-oid="codyezp"></g>
|
| 38 |
+
<g
|
| 39 |
+
id="SVGRepo_tracerCarrier"
|
| 40 |
+
stroke-linecap="round"
|
| 41 |
+
stroke-linejoin="round"
|
| 42 |
+
data-oid="t1:xb-5"
|
| 43 |
+
></g>
|
| 44 |
+
<g id="SVGRepo_iconCarrier" data-oid="ghtfh9h">
|
| 45 |
+
{' '}
|
| 46 |
+
<g data-oid=":f10y:v">
|
| 47 |
+
{' '}
|
| 48 |
+
<g data-oid="7zcg76n">
|
| 49 |
+
{' '}
|
| 50 |
+
<path
|
| 51 |
+
d="M170.667,320.007V320c0-11.782-9.551-21.333-21.333-21.333C137.552,298.667,128,308.218,128,320v-56.89 c0-11.782-9.551-21.333-21.333-21.333s-21.333,9.551-21.333,21.333v78.229c-0.001,11.783,9.551,21.334,21.333,21.334H128v42.66 c0,11.782,9.551,21.333,21.333,21.333s21.333-9.551,21.333-21.333v-42.66c11.782,0,21.333-9.551,21.333-21.333 C192,329.558,182.449,320.007,170.667,320.007z"
|
| 52 |
+
data-oid="lt7rll5"
|
| 53 |
+
></path>{' '}
|
| 54 |
+
</g>{' '}
|
| 55 |
+
</g>{' '}
|
| 56 |
+
<g data-oid="so2w9ck">
|
| 57 |
+
{' '}
|
| 58 |
+
<g data-oid="0kbzm.2">
|
| 59 |
+
{' '}
|
| 60 |
+
<path
|
| 61 |
+
d="M426.667,320.007V320c0-11.782-9.551-21.333-21.333-21.333C393.552,298.667,384,308.218,384,320v-56.89 c0-11.782-9.551-21.333-21.333-21.333c-11.782,0-21.333,9.551-21.333,21.333v78.229c-0.001,11.783,9.551,21.334,21.333,21.334H384 v42.66c0,11.782,9.551,21.333,21.333,21.333c11.782,0,21.333-9.551,21.333-21.333v-42.66c11.782,0,21.333-9.551,21.333-21.333 C448,329.558,438.449,320.007,426.667,320.007z"
|
| 62 |
+
data-oid="y4oji:m"
|
| 63 |
+
></path>{' '}
|
| 64 |
+
</g>{' '}
|
| 65 |
+
</g>{' '}
|
| 66 |
+
<g data-oid="pf3sr3b">
|
| 67 |
+
{' '}
|
| 68 |
+
<g data-oid="4ukzqi5">
|
| 69 |
+
{' '}
|
| 70 |
+
<path
|
| 71 |
+
d="M266.667,256c-29.446,0-53.333,23.887-53.333,53.333v64c-0.001,29.446,23.887,53.334,53.333,53.334 S320,402.78,320,373.334v-64C320,279.887,296.114,256,266.667,256z M277.334,373.333c0,5.882-4.785,10.667-10.667,10.667 c-5.882,0-10.667-4.785-10.667-10.667v-64c0-5.882,4.785-10.667,10.667-10.667c5.882,0,10.667,4.785,10.667,10.667V373.333z"
|
| 72 |
+
data-oid="pfto:60"
|
| 73 |
+
></path>{' '}
|
| 74 |
+
</g>{' '}
|
| 75 |
+
</g>{' '}
|
| 76 |
+
<g data-oid="j0k-zf7">
|
| 77 |
+
{' '}
|
| 78 |
+
<g data-oid="tuf9-xo">
|
| 79 |
+
{' '}
|
| 80 |
+
<path
|
| 81 |
+
d="M490.667,0H21.333C9.552,0,0,9.551,0,21.333V192v298.667C0,502.449,9.552,512,21.333,512h469.333 c11.782,0,21.333-9.551,21.333-21.333V192V21.333C512,9.551,502.45,0,490.667,0z M469.334,469.333L469.334,469.333H42.667v-256 h426.667V469.333z M469.334,170.667H42.667v-128h426.667V170.667z"
|
| 82 |
+
data-oid=".-1dcb."
|
| 83 |
+
></path>{' '}
|
| 84 |
+
</g>{' '}
|
| 85 |
+
</g>{' '}
|
| 86 |
+
<g data-oid="nnpjyjq">
|
| 87 |
+
{' '}
|
| 88 |
+
<g data-oid="1j1fsc5">
|
| 89 |
+
{' '}
|
| 90 |
+
<path
|
| 91 |
+
d="M435.505,106.666l6.248-6.248c8.331-8.331,8.331-21.838,0-30.17c-8.331-8.331-21.839-8.331-30.17,0l-6.248,6.248 l-6.248-6.248c-8.331-8.331-21.839-8.331-30.17,0c-8.331,8.331-8.331,21.839,0,30.17l6.248,6.248l-6.248,6.248 c-8.331,8.331-8.331,21.839,0,30.17s21.839,8.331,30.17,0l6.248-6.248l6.248,6.248c8.331,8.331,21.839,8.331,30.17,0 s8.331-21.839,0-30.17L435.505,106.666z"
|
| 92 |
+
data-oid="4a3u-1b"
|
| 93 |
+
></path>{' '}
|
| 94 |
+
</g>{' '}
|
| 95 |
+
</g>{' '}
|
| 96 |
+
<g data-oid="fvjii.6">
|
| 97 |
+
{' '}
|
| 98 |
+
<g data-oid="o153rqo">
|
| 99 |
+
{' '}
|
| 100 |
+
<path
|
| 101 |
+
d="M256,85.333H85.333C73.552,85.333,64,94.885,64,106.667S73.552,128,85.333,128H256c11.782,0,21.333-9.551,21.333-21.333 S267.783,85.333,256,85.333z"
|
| 102 |
+
data-oid="zr-f0k2"
|
| 103 |
+
></path>{' '}
|
| 104 |
+
</g>{' '}
|
| 105 |
+
</g>{' '}
|
| 106 |
+
</g>
|
| 107 |
+
</svg>
|
| 108 |
+
</h1>
|
| 109 |
+
|
| 110 |
+
<p className="text-xl text-gray-400 mt-4" data-oid="b0x5vhh">
|
| 111 |
+
Oops! The page you’re looking for doesn’t exist.
|
| 112 |
+
</p>
|
| 113 |
+
<Link
|
| 114 |
+
href="/"
|
| 115 |
+
className="mt-5 px-6 py-2 rounded-3xl bg-purple-600 hover:bg-purple-500 transition-all text-white text-lg font-semibold shadow-lg shadow-purple-500/30"
|
| 116 |
+
data-oid="jb2a6hd"
|
| 117 |
+
>
|
| 118 |
+
Go Home
|
| 119 |
+
</Link>
|
| 120 |
+
</div>
|
| 121 |
+
);
|
| 122 |
+
}
|
frontend/app/{page.js → page.tsx}
RENAMED
|
@@ -4,16 +4,27 @@ import { useState, useEffect } from 'react';
|
|
| 4 |
import NewContentSection from '@components/sections/NewContentSection';
|
| 5 |
import { getRecentItems } from '@lib/lb';
|
| 6 |
import Link from 'next/link';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
export default function Page() {
|
| 9 |
-
const [slides, setSlides] = useState([]);
|
| 10 |
const [currentSlide, setCurrentSlide] = useState(0);
|
| 11 |
-
const
|
| 12 |
const [loaded, setLoaded] = useState(false);
|
| 13 |
|
| 14 |
useEffect(() => {
|
| 15 |
async function fetchSlides() {
|
| 16 |
try {
|
|
|
|
| 17 |
const slidesData = await getRecentItems();
|
| 18 |
setSlides(slidesData);
|
| 19 |
} catch (error) {
|
|
@@ -39,17 +50,30 @@ export default function Page() {
|
|
| 39 |
return (
|
| 40 |
<>
|
| 41 |
{/* Hero Slideshow */}
|
| 42 |
-
<div className="relative h-
|
| 43 |
{/* Loading Skeleton */}
|
| 44 |
<div
|
| 45 |
className={`absolute inset-0 flex items-center justify-center bg-gray-900 transition-opacity duration-1000 ${
|
| 46 |
loading ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
| 47 |
}`}
|
|
|
|
| 48 |
>
|
| 49 |
-
<div
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
</div>
|
| 54 |
</div>
|
| 55 |
|
|
@@ -58,6 +82,7 @@ export default function Page() {
|
|
| 58 |
className={`absolute inset-0 transition-opacity duration-1000 ${
|
| 59 |
loaded ? 'opacity-100' : 'opacity-0'
|
| 60 |
}`}
|
|
|
|
| 61 |
>
|
| 62 |
{slides.map((slide, index) => (
|
| 63 |
<div
|
|
@@ -65,54 +90,74 @@ export default function Page() {
|
|
| 65 |
className={`absolute inset-0 transition-opacity duration-1000 ${
|
| 66 |
index === currentSlide ? 'opacity-100' : 'opacity-0'
|
| 67 |
}`}
|
|
|
|
| 68 |
>
|
| 69 |
-
<div className="absolute inset-0 bg-black/50 z-10" />
|
| 70 |
<div
|
| 71 |
className="w-full h-full bg-center pan-animation transition-transform duration-1000 transform-gpu"
|
| 72 |
style={{ backgroundImage: `url(${slide.image})` }}
|
|
|
|
| 73 |
></div>
|
| 74 |
-
<div
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
{Array.isArray(slide.genre) ? (
|
| 78 |
slide.genre.map((genre, index) => (
|
| 79 |
-
<span key={index}>
|
| 80 |
<Link
|
| 81 |
href={`/genre/${encodeURIComponent(genre)}`}
|
| 82 |
-
className="hover:
|
|
|
|
| 83 |
>
|
| 84 |
{genre}
|
| 85 |
</Link>
|
| 86 |
-
{index < slide.genre.length - 1 && ', '}
|
| 87 |
</span>
|
| 88 |
))
|
| 89 |
) : (
|
| 90 |
<Link
|
| 91 |
href={`/genre/${encodeURIComponent(slide.genre)}`}
|
| 92 |
-
className="hover:
|
|
|
|
| 93 |
>
|
| 94 |
{slide.genre}
|
| 95 |
</Link>
|
| 96 |
)}
|
| 97 |
</span>
|
| 98 |
|
| 99 |
-
<
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
{slide.description}
|
| 104 |
</p>
|
| 105 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 106 |
<Link
|
| 107 |
href={'#play'}
|
| 108 |
className="bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-4 landscape:py-2 py-2 md:px-8 md:py-3 landscape:rounded-3xl rounded-s-2xl flex items-center transition-colors text-sm md:text-base"
|
|
|
|
| 109 |
>
|
| 110 |
<svg
|
| 111 |
className="w-4 h-4 md:w-5 md:h-5 mr-2"
|
| 112 |
fill="currentColor"
|
| 113 |
viewBox="0 0 20 20"
|
|
|
|
| 114 |
>
|
| 115 |
-
<path d="M4 4l12 6-12 6V4z" />
|
| 116 |
</svg>
|
| 117 |
Play Now
|
| 118 |
</Link>
|
|
@@ -123,6 +168,7 @@ export default function Page() {
|
|
| 123 |
: `/tvshow/${slide.title}`
|
| 124 |
}
|
| 125 |
className="bg-gray-800/80 hover:bg-gray-700/80 px-4 landscape:py-2 py-2 md:px-8 md:py-3 landscape:rounded-3xl rounded-e-2xl transition-colors text-sm md:text-base"
|
|
|
|
| 126 |
>
|
| 127 |
More Info
|
| 128 |
</Link>
|
|
@@ -134,7 +180,7 @@ export default function Page() {
|
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
|
| 137 |
-
<NewContentSection />
|
| 138 |
</>
|
| 139 |
);
|
| 140 |
}
|
|
|
|
| 4 |
import NewContentSection from '@components/sections/NewContentSection';
|
| 5 |
import { getRecentItems } from '@lib/lb';
|
| 6 |
import Link from 'next/link';
|
| 7 |
+
import { useLoading } from '@/components/loading/SplashScreen';
|
| 8 |
+
import { tr } from 'framer-motion/client';
|
| 9 |
+
|
| 10 |
+
interface Slide {
|
| 11 |
+
image: string;
|
| 12 |
+
genre: string | string[];
|
| 13 |
+
title: string;
|
| 14 |
+
description: string;
|
| 15 |
+
type: 'movie' | 'tvshow';
|
| 16 |
+
}
|
| 17 |
|
| 18 |
export default function Page() {
|
| 19 |
+
const [slides, setSlides] = useState<Slide[]>([]);
|
| 20 |
const [currentSlide, setCurrentSlide] = useState(0);
|
| 21 |
+
const { loading, setLoading } = useLoading();
|
| 22 |
const [loaded, setLoaded] = useState(false);
|
| 23 |
|
| 24 |
useEffect(() => {
|
| 25 |
async function fetchSlides() {
|
| 26 |
try {
|
| 27 |
+
setLoading(true);
|
| 28 |
const slidesData = await getRecentItems();
|
| 29 |
setSlides(slidesData);
|
| 30 |
} catch (error) {
|
|
|
|
| 50 |
return (
|
| 51 |
<>
|
| 52 |
{/* Hero Slideshow */}
|
| 53 |
+
<div className="relative landscape:h-[80vh] portrait:h-[75vh]" data-oid="g9z.yar">
|
| 54 |
{/* Loading Skeleton */}
|
| 55 |
<div
|
| 56 |
className={`absolute inset-0 flex items-center justify-center bg-gray-900 transition-opacity duration-1000 ${
|
| 57 |
loading ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
| 58 |
}`}
|
| 59 |
+
data-oid="qzwq8le"
|
| 60 |
>
|
| 61 |
+
<div
|
| 62 |
+
className="w-full h-full flex flex-col justify-end bg-gray-800 rounded-lg animate-pulse"
|
| 63 |
+
data-oid="8.2oef."
|
| 64 |
+
>
|
| 65 |
+
<div
|
| 66 |
+
className="w-2/4 h-5 landscape:w-1/4 ml-10 landscape:ml-20 mb-4 bg-gray-700 rounded-lg"
|
| 67 |
+
data-oid="sji8cjl"
|
| 68 |
+
></div>
|
| 69 |
+
<div
|
| 70 |
+
className="w-3/4 h-14 landscape:w-2/4 ml-10 landscape:ml-20 mb-4 bg-gray-700 rounded-lg"
|
| 71 |
+
data-oid="wv3i._3"
|
| 72 |
+
></div>
|
| 73 |
+
<div
|
| 74 |
+
className="w-3/4 h-1/4 landscape:w-2/4 ml-10 landscape:ml-20 mb-20 bg-gray-700 rounded-lg"
|
| 75 |
+
data-oid="324b.67"
|
| 76 |
+
></div>
|
| 77 |
</div>
|
| 78 |
</div>
|
| 79 |
|
|
|
|
| 82 |
className={`absolute inset-0 transition-opacity duration-1000 ${
|
| 83 |
loaded ? 'opacity-100' : 'opacity-0'
|
| 84 |
}`}
|
| 85 |
+
data-oid=".1jufir"
|
| 86 |
>
|
| 87 |
{slides.map((slide, index) => (
|
| 88 |
<div
|
|
|
|
| 90 |
className={`absolute inset-0 transition-opacity duration-1000 ${
|
| 91 |
index === currentSlide ? 'opacity-100' : 'opacity-0'
|
| 92 |
}`}
|
| 93 |
+
data-oid="zwb7.j-"
|
| 94 |
>
|
| 95 |
+
<div className="absolute inset-0 bg-black/50 z-10" data-oid="ydvf.3z" />
|
| 96 |
<div
|
| 97 |
className="w-full h-full bg-center pan-animation transition-transform duration-1000 transform-gpu"
|
| 98 |
style={{ backgroundImage: `url(${slide.image})` }}
|
| 99 |
+
data-oid="b12pu__"
|
| 100 |
></div>
|
| 101 |
+
<div
|
| 102 |
+
className="absolute bottom-0 left-0 right-0 z-20 p-10 md:p-20 bg-gradient-to-t from-gray-900"
|
| 103 |
+
data-oid="4tmdrv."
|
| 104 |
+
>
|
| 105 |
+
<div className="container mx-auto" data-oid="tvybbc2">
|
| 106 |
+
<h1
|
| 107 |
+
className="text-4xl md:text-5xl font-sans font-medium mt-2 mb-4"
|
| 108 |
+
data-oid="au.xh0_"
|
| 109 |
+
>
|
| 110 |
+
{slide.title}
|
| 111 |
+
</h1>
|
| 112 |
+
<span
|
| 113 |
+
className="text-purple-400 text-base font-semibold flex flex-wrap gap-2"
|
| 114 |
+
data-oid="hnb32u."
|
| 115 |
+
>
|
| 116 |
{Array.isArray(slide.genre) ? (
|
| 117 |
slide.genre.map((genre, index) => (
|
| 118 |
+
<span key={index} data-oid="ka.icr8">
|
| 119 |
<Link
|
| 120 |
href={`/genre/${encodeURIComponent(genre)}`}
|
| 121 |
+
className="bg-purple-700/50 hover:bg-gray-600/50 text-gray-200 px-3 py-1 rounded-full text-sm transition-colors"
|
| 122 |
+
data-oid="7usaujq"
|
| 123 |
>
|
| 124 |
{genre}
|
| 125 |
</Link>
|
|
|
|
| 126 |
</span>
|
| 127 |
))
|
| 128 |
) : (
|
| 129 |
<Link
|
| 130 |
href={`/genre/${encodeURIComponent(slide.genre)}`}
|
| 131 |
+
className="bg-purple-700/50 hover:bg-gray-600/50 text-gray-200 px-3 py-1 rounded-full text-sm transition-colors"
|
| 132 |
+
data-oid=".16-s0w"
|
| 133 |
>
|
| 134 |
{slide.genre}
|
| 135 |
</Link>
|
| 136 |
)}
|
| 137 |
</span>
|
| 138 |
|
| 139 |
+
<p
|
| 140 |
+
className="text-gray-300 text-lg font-sans max-w-xl overflow-hidden line-clamp-5"
|
| 141 |
+
data-oid="u2nu3zd"
|
| 142 |
+
>
|
| 143 |
{slide.description}
|
| 144 |
</p>
|
| 145 |
+
<div
|
| 146 |
+
className="flex justify-start landscape:gap-4 mt-8"
|
| 147 |
+
data-oid="5-s.c8t"
|
| 148 |
+
>
|
| 149 |
<Link
|
| 150 |
href={'#play'}
|
| 151 |
className="bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-4 landscape:py-2 py-2 md:px-8 md:py-3 landscape:rounded-3xl rounded-s-2xl flex items-center transition-colors text-sm md:text-base"
|
| 152 |
+
data-oid="1defff:"
|
| 153 |
>
|
| 154 |
<svg
|
| 155 |
className="w-4 h-4 md:w-5 md:h-5 mr-2"
|
| 156 |
fill="currentColor"
|
| 157 |
viewBox="0 0 20 20"
|
| 158 |
+
data-oid="o7fg:-l"
|
| 159 |
>
|
| 160 |
+
<path d="M4 4l12 6-12 6V4z" data-oid="f3i:9y." />
|
| 161 |
</svg>
|
| 162 |
Play Now
|
| 163 |
</Link>
|
|
|
|
| 168 |
: `/tvshow/${slide.title}`
|
| 169 |
}
|
| 170 |
className="bg-gray-800/80 hover:bg-gray-700/80 px-4 landscape:py-2 py-2 md:px-8 md:py-3 landscape:rounded-3xl rounded-e-2xl transition-colors text-sm md:text-base"
|
| 171 |
+
data-oid="si84l0u"
|
| 172 |
>
|
| 173 |
More Info
|
| 174 |
</Link>
|
|
|
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
|
| 183 |
+
<NewContentSection data-oid="u50ba33" />
|
| 184 |
</>
|
| 185 |
);
|
| 186 |
}
|
frontend/app/tvshow/[title]/{LoadingSkeleton.js → LoadingSkeleton.tsx}
RENAMED
|
@@ -1,29 +1,29 @@
|
|
| 1 |
export function LoadingSkeleton() {
|
| 2 |
return (
|
| 3 |
-
<div className="w-full min-h-screen bg-gray-900 animate-pulse" data-oid="
|
| 4 |
{/* Hero Section Skeleton */}
|
| 5 |
-
<div className="relative w-full h-[60vh] bg-gray-800" data-oid="
|
| 6 |
<div
|
| 7 |
className="absolute inset-0 flex items-center justify-center"
|
| 8 |
-
data-oid="
|
| 9 |
>
|
| 10 |
-
<div className="w-full max-w-7xl px-6 space-y-4" data-oid="
|
| 11 |
<div
|
| 12 |
className="h-12 bg-gray-700 rounded-lg w-3/4 max-w-2xl"
|
| 13 |
-
data-oid="
|
| 14 |
></div>
|
| 15 |
<div
|
| 16 |
className="h-6 bg-gray-700 rounded-lg w-1/4 max-w-xs"
|
| 17 |
-
data-oid="
|
| 18 |
></div>
|
| 19 |
-
<div className="flex gap-4" data-oid="
|
| 20 |
<div
|
| 21 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 22 |
-
data-oid="
|
| 23 |
></div>
|
| 24 |
<div
|
| 25 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 26 |
-
data-oid="
|
| 27 |
></div>
|
| 28 |
</div>
|
| 29 |
</div>
|
|
@@ -31,25 +31,25 @@ export function LoadingSkeleton() {
|
|
| 31 |
</div>
|
| 32 |
|
| 33 |
{/* Details Section Skeleton */}
|
| 34 |
-
<div className="max-w-7xl mx-auto px-6 py-12 space-y-8" data-oid="
|
| 35 |
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-8" data-oid="
|
| 36 |
-
<div className="md:col-span-2 space-y-4" data-oid="
|
| 37 |
-
<div className="h-8 bg-gray-800 rounded-lg w-1/2" data-oid="
|
| 38 |
<div
|
| 39 |
className="h-32 bg-gray-800 rounded-lg w-full"
|
| 40 |
-
data-oid="
|
| 41 |
></div>
|
| 42 |
-
<div className="h-8 bg-gray-800 rounded-lg w-1/3" data-oid="
|
| 43 |
<div
|
| 44 |
className="h-24 bg-gray-800 rounded-lg w-full"
|
| 45 |
-
data-oid="
|
| 46 |
></div>
|
| 47 |
</div>
|
| 48 |
-
<div className="space-y-4" data-oid="
|
| 49 |
-
<div className="h-8 bg-gray-800 rounded-lg w-full" data-oid="
|
| 50 |
<div
|
| 51 |
className="h-40 bg-gray-800 rounded-lg w-full"
|
| 52 |
-
data-oid="
|
| 53 |
></div>
|
| 54 |
</div>
|
| 55 |
</div>
|
|
|
|
| 1 |
export function LoadingSkeleton() {
|
| 2 |
return (
|
| 3 |
+
<div className="w-full min-h-screen bg-gray-900 animate-pulse" data-oid="9y-8h.e">
|
| 4 |
{/* Hero Section Skeleton */}
|
| 5 |
+
<div className="relative w-full h-[60vh] bg-gray-800" data-oid="r83jchn">
|
| 6 |
<div
|
| 7 |
className="absolute inset-0 flex items-center justify-center"
|
| 8 |
+
data-oid="qvnro3o"
|
| 9 |
>
|
| 10 |
+
<div className="w-full max-w-7xl px-6 space-y-4" data-oid="n4sx.uk">
|
| 11 |
<div
|
| 12 |
className="h-12 bg-gray-700 rounded-lg w-3/4 max-w-2xl"
|
| 13 |
+
data-oid="wl80sa8"
|
| 14 |
></div>
|
| 15 |
<div
|
| 16 |
className="h-6 bg-gray-700 rounded-lg w-1/4 max-w-xs"
|
| 17 |
+
data-oid="rs621yh"
|
| 18 |
></div>
|
| 19 |
+
<div className="flex gap-4" data-oid="ahdwte6">
|
| 20 |
<div
|
| 21 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 22 |
+
data-oid="wuzu2mf"
|
| 23 |
></div>
|
| 24 |
<div
|
| 25 |
className="h-12 bg-gray-700 rounded-3xl w-32"
|
| 26 |
+
data-oid="4b32gv."
|
| 27 |
></div>
|
| 28 |
</div>
|
| 29 |
</div>
|
|
|
|
| 31 |
</div>
|
| 32 |
|
| 33 |
{/* Details Section Skeleton */}
|
| 34 |
+
<div className="max-w-7xl mx-auto px-6 py-12 space-y-8" data-oid="w1k.gzv">
|
| 35 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8" data-oid="itpb4ce">
|
| 36 |
+
<div className="md:col-span-2 space-y-4" data-oid="0m8hnwd">
|
| 37 |
+
<div className="h-8 bg-gray-800 rounded-lg w-1/2" data-oid="voa7_l_"></div>
|
| 38 |
<div
|
| 39 |
className="h-32 bg-gray-800 rounded-lg w-full"
|
| 40 |
+
data-oid="by65z9d"
|
| 41 |
></div>
|
| 42 |
+
<div className="h-8 bg-gray-800 rounded-lg w-1/3" data-oid="odbfq.m"></div>
|
| 43 |
<div
|
| 44 |
className="h-24 bg-gray-800 rounded-lg w-full"
|
| 45 |
+
data-oid="iv43qmh"
|
| 46 |
></div>
|
| 47 |
</div>
|
| 48 |
+
<div className="space-y-4" data-oid="hx3cr-7">
|
| 49 |
+
<div className="h-8 bg-gray-800 rounded-lg w-full" data-oid="aolxfq6"></div>
|
| 50 |
<div
|
| 51 |
className="h-40 bg-gray-800 rounded-lg w-full"
|
| 52 |
+
data-oid="vv.ju.h"
|
| 53 |
></div>
|
| 54 |
</div>
|
| 55 |
</div>
|
frontend/app/tvshow/[title]/page.js
DELETED
|
@@ -1,161 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from 'react';
|
| 4 |
-
import { useParams } from 'next/navigation';
|
| 5 |
-
import { getTvShowMetadata } from '@/lib/lb';
|
| 6 |
-
import { LoadingSkeleton } from './LoadingSkeleton';
|
| 7 |
-
import { convertMinutesToHM } from '@lib/utils';
|
| 8 |
-
import Link from 'next/link';
|
| 9 |
-
import CastSection from '@components/sections/CastSection';
|
| 10 |
-
|
| 11 |
-
export default function TvShowTitlePage() {
|
| 12 |
-
const { title } = useParams();
|
| 13 |
-
const decodedTitle = decodeURIComponent(title);
|
| 14 |
-
const [tvshow, setTvShow] = useState(null);
|
| 15 |
-
|
| 16 |
-
useEffect(() => {
|
| 17 |
-
async function fetchMovie() {
|
| 18 |
-
try {
|
| 19 |
-
const data = await getTvShowMetadata(decodedTitle);
|
| 20 |
-
setTvShow(data.data);
|
| 21 |
-
} catch (error) {
|
| 22 |
-
console.error('Failed to fetch movie details', error);
|
| 23 |
-
}
|
| 24 |
-
}
|
| 25 |
-
fetchMovie();
|
| 26 |
-
}, [decodedTitle]);
|
| 27 |
-
|
| 28 |
-
if (!tvshow) {
|
| 29 |
-
return <LoadingSkeleton />;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
return (
|
| 33 |
-
<div
|
| 34 |
-
className="relative w-full min-h-screen h-full flex items-start landscape:pt-36 pt-40 text-white"
|
| 35 |
-
style={{
|
| 36 |
-
backgroundImage: `url(${tvshow.image})`,
|
| 37 |
-
backgroundSize: 'cover',
|
| 38 |
-
backgroundPosition: 'center',
|
| 39 |
-
}}
|
| 40 |
-
>
|
| 41 |
-
<div className="h-screen fixed inset-0 bg-gray-900 bg-opacity-50">
|
| 42 |
-
<div className="h-2/6"></div>
|
| 43 |
-
<div className="h-4/6 bg-gradient-to-t from-gray-900 to-transparent"></div>
|
| 44 |
-
</div>
|
| 45 |
-
<div className="relative z-10 max-w-1/3 landscape:mx-40 mx-6 min-h-screen">
|
| 46 |
-
<div className="relative z-10 max-w-2/6 w-5/6 landscape:w-4/6 flex-col items-start justify-start bg-gray-800 bg-opacity-50 backdrop-blur-md p-4 rounded-xl">
|
| 47 |
-
<h1 className="landscape:text-3xl text-2xl font-bold mb-2 text-gray-200">
|
| 48 |
-
{tvshow.translations?.nameTranslations?.find((t) => t.language === 'eng')
|
| 49 |
-
?.name ||
|
| 50 |
-
tvshow.translations?.nameTranslations?.find(
|
| 51 |
-
(t) => t.isAlias && t.language === 'eng',
|
| 52 |
-
)?.name ||
|
| 53 |
-
'Unknown Title'}
|
| 54 |
-
({tvshow.year})
|
| 55 |
-
</h1>
|
| 56 |
-
|
| 57 |
-
{/* Genres */}
|
| 58 |
-
<span className="text-purple-400 text-base font-semibold">
|
| 59 |
-
{Array.isArray(tvshow.genres)
|
| 60 |
-
? tvshow.genres.map((genre, index) => (
|
| 61 |
-
<span key={genre.id}>
|
| 62 |
-
<Link
|
| 63 |
-
href={`/genre/${encodeURIComponent(genre.slug)}`}
|
| 64 |
-
className="hover:underline"
|
| 65 |
-
>
|
| 66 |
-
{genre.name}
|
| 67 |
-
</Link>
|
| 68 |
-
{index < tvshow.genres.length - 1 && ', '}
|
| 69 |
-
</span>
|
| 70 |
-
))
|
| 71 |
-
: null}
|
| 72 |
-
</span>
|
| 73 |
-
{/* Improved Score Display */}
|
| 74 |
-
<div className="mb-2 mt-2 flex items-center">
|
| 75 |
-
<span className="bg-gradient-to-r from-violet-500 to-purple-400 text-gray-700 px-3 py-1 rounded-md text-sm font-semibold">
|
| 76 |
-
⭐ {tvshow.score}
|
| 77 |
-
</span>
|
| 78 |
-
</div>
|
| 79 |
-
{/* Improved Content Ratings Display */}
|
| 80 |
-
{tvshow.contentRatings?.length > 0 ? (
|
| 81 |
-
<div className="mt-2 text-gray-300">
|
| 82 |
-
<p className="text-sm font-semibold mb-1">Content Ratings:</p>
|
| 83 |
-
<ul className="flex flex-wrap gap-2">
|
| 84 |
-
{tvshow.contentRatings.map((rating, index) => {
|
| 85 |
-
// Map country codes to corresponding flags
|
| 86 |
-
const countryFlags = {
|
| 87 |
-
AUS: '🇦🇺',
|
| 88 |
-
USA: '🇺🇸',
|
| 89 |
-
GBR: '🇬🇧',
|
| 90 |
-
EU: '🇪🇺',
|
| 91 |
-
JPN: '🇯🇵',
|
| 92 |
-
KOR: '🇰🇷',
|
| 93 |
-
CAN: '🇨🇦',
|
| 94 |
-
DEU: '🇩🇪',
|
| 95 |
-
FRA: '🇫🇷',
|
| 96 |
-
ESP: '🇪🇸',
|
| 97 |
-
ITA: '🇮🇹',
|
| 98 |
-
};
|
| 99 |
-
|
| 100 |
-
const flag = countryFlags[rating.country.toUpperCase()] || '🌍'; // Default to globe if no match
|
| 101 |
-
|
| 102 |
-
return (
|
| 103 |
-
<li
|
| 104 |
-
key={index}
|
| 105 |
-
className="flex items-center bg-gray-800/50 px-2 py-1 rounded-md text-xs"
|
| 106 |
-
>
|
| 107 |
-
<span className="mr-2">{flag}</span>
|
| 108 |
-
<span className="text-white font-semibold">
|
| 109 |
-
{rating.name}
|
| 110 |
-
</span>
|
| 111 |
-
<span className="text-gray-400 ml-1">
|
| 112 |
-
{' '}
|
| 113 |
-
- {rating.description || 'N/A'}
|
| 114 |
-
</span>
|
| 115 |
-
</li>
|
| 116 |
-
);
|
| 117 |
-
})}
|
| 118 |
-
</ul>
|
| 119 |
-
</div>
|
| 120 |
-
) : (
|
| 121 |
-
<p className="text-gray-400 text-sm">No content ratings available.</p>
|
| 122 |
-
)}
|
| 123 |
-
|
| 124 |
-
<div className="flex justify-start landscape:gap-2 mt-4">
|
| 125 |
-
<Link
|
| 126 |
-
href={'#play'}
|
| 127 |
-
className="bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-s-2xl flex items-center transition-colors text-sm md:text-base"
|
| 128 |
-
>
|
| 129 |
-
<svg
|
| 130 |
-
className="w-4 h-4 md:w-5 md:h-5 mr-2"
|
| 131 |
-
fill="currentColor"
|
| 132 |
-
viewBox="0 0 20 20"
|
| 133 |
-
>
|
| 134 |
-
<path d="M4 4l12 6-12 6V4z" />
|
| 135 |
-
</svg>
|
| 136 |
-
Play Now
|
| 137 |
-
</Link>
|
| 138 |
-
<Link
|
| 139 |
-
href={'#'}
|
| 140 |
-
className="bg-gray-800/80 hover:bg-gray-700/80 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-e-2xl transition-colors text-sm md:text-base"
|
| 141 |
-
>
|
| 142 |
-
Add to My List
|
| 143 |
-
</Link>
|
| 144 |
-
</div>
|
| 145 |
-
</div>
|
| 146 |
-
<div className="flex flex-col landscape:flex-row gap-6 mt-4">
|
| 147 |
-
{/* Overview Section */}
|
| 148 |
-
<div className="flex-1 bg-gray-800 bg-opacity-50 backdrop-blur-md p-4 rounded-xl">
|
| 149 |
-
<p className="text-gray-400">
|
| 150 |
-
{tvshow.translations?.overviewTranslations?.find(
|
| 151 |
-
(t) => t.language === 'eng',
|
| 152 |
-
)?.overview || 'No overview available.'}
|
| 153 |
-
</p>
|
| 154 |
-
</div>
|
| 155 |
-
|
| 156 |
-
<CastSection movie={tvshow} />
|
| 157 |
-
</div>
|
| 158 |
-
</div>
|
| 159 |
-
</div>
|
| 160 |
-
);
|
| 161 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/tvshow/[title]/page.tsx
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react';
|
| 4 |
+
import { useParams } from 'next/navigation';
|
| 5 |
+
import { getTvShowMetadata } from '@/lib/lb';
|
| 6 |
+
import { LoadingSkeleton } from './LoadingSkeleton';
|
| 7 |
+
import { convertMinutesToHM } from '@lib/utils';
|
| 8 |
+
import Link from 'next/link';
|
| 9 |
+
import CastSection from '@components/sections/CastSection';
|
| 10 |
+
|
| 11 |
+
interface FileStructure {
|
| 12 |
+
contents?: any[];
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const EpisodesSection = ({ fileStructure }: { fileStructure: FileStructure }) => {
|
| 16 |
+
const [activeSeason, setActiveSeason] = useState(0);
|
| 17 |
+
|
| 18 |
+
return (
|
| 19 |
+
<div
|
| 20 |
+
className="bg-gray-800/60 backdrop-blur-md rounded-2xl p-6 border border-gray-700/50"
|
| 21 |
+
data-oid="569dula"
|
| 22 |
+
>
|
| 23 |
+
<div className="flex flex-col space-y-4" data-oid="j.zuj5c">
|
| 24 |
+
<div className="flex items-center justify-between" data-oid="znwvqvv">
|
| 25 |
+
<h3 className="text-lg font-semibold text-gray-200" data-oid=".t5i_kn">
|
| 26 |
+
Episodes
|
| 27 |
+
</h3>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
{/* Season Buttons */}
|
| 31 |
+
<div
|
| 32 |
+
className="overflow-x-auto whitespace-nowrap flex gap-2 scrollbar-hide snap-x snap-mandatory pb-2"
|
| 33 |
+
data-oid="wvu-yod"
|
| 34 |
+
>
|
| 35 |
+
{fileStructure.contents?.map((season, idx) => {
|
| 36 |
+
const seasonName = season.path.split('/').pop();
|
| 37 |
+
const isSpecials = seasonName === 'Specials';
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<button
|
| 41 |
+
key={idx}
|
| 42 |
+
onClick={() => setActiveSeason(idx)}
|
| 43 |
+
className={`px-4 py-2 min-w-fit rounded-full text-sm font-medium transition-colors snap-start
|
| 44 |
+
${
|
| 45 |
+
activeSeason === idx
|
| 46 |
+
? 'bg-purple-500 text-white'
|
| 47 |
+
: isSpecials
|
| 48 |
+
? 'bg-amber-500/20 text-amber-300 hover:bg-amber-500/30'
|
| 49 |
+
: 'bg-purple-500/20 text-purple-300 hover:bg-purple-500/30'
|
| 50 |
+
}`}
|
| 51 |
+
data-oid=":2:ubya"
|
| 52 |
+
>
|
| 53 |
+
{seasonName}
|
| 54 |
+
</button>
|
| 55 |
+
);
|
| 56 |
+
})}
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
{/* Episodes List */}
|
| 60 |
+
<div className="space-y-8 mt-4" data-oid="2vjyfsk">
|
| 61 |
+
{fileStructure.contents?.[activeSeason] && (
|
| 62 |
+
<div key={activeSeason} className="space-y-4" data-oid="1rzvyvm">
|
| 63 |
+
<h4
|
| 64 |
+
className={`text-base font-medium ${
|
| 65 |
+
fileStructure.contents[activeSeason].path.includes('Specials')
|
| 66 |
+
? 'text-amber-300'
|
| 67 |
+
: 'text-purple-300'
|
| 68 |
+
}`}
|
| 69 |
+
data-oid="b1h.lah"
|
| 70 |
+
>
|
| 71 |
+
{fileStructure.contents[activeSeason].path.split('/').pop()}
|
| 72 |
+
</h4>
|
| 73 |
+
<div className="space-y-2" data-oid="zxw.0:j">
|
| 74 |
+
{fileStructure.contents[activeSeason].contents?.map(
|
| 75 |
+
(episode: any, episodeIdx: number) => {
|
| 76 |
+
const match = episode.path.match(
|
| 77 |
+
/[S](\d+)[E](\d+) - (.+?)\./,
|
| 78 |
+
);
|
| 79 |
+
if (!match) return null;
|
| 80 |
+
|
| 81 |
+
const [, , episodeNum, episodeTitle] = match;
|
| 82 |
+
const isSpecials =
|
| 83 |
+
fileStructure.contents &&
|
| 84 |
+
fileStructure.contents[activeSeason]?.path.includes(
|
| 85 |
+
'Specials',
|
| 86 |
+
);
|
| 87 |
+
|
| 88 |
+
return (
|
| 89 |
+
<div
|
| 90 |
+
key={episodeIdx}
|
| 91 |
+
className="group flex items-center gap-4 p-3 rounded-xl transition-colors hover:bg-gray-700/50 cursor-pointer"
|
| 92 |
+
data-oid="w8y34vl"
|
| 93 |
+
>
|
| 94 |
+
{/* Episode Number */}
|
| 95 |
+
<div
|
| 96 |
+
className={`flex-shrink-0 w-12 h-12 flex items-center justify-center rounded-xl bg-gray-700/50
|
| 97 |
+
${
|
| 98 |
+
isSpecials
|
| 99 |
+
? 'group-hover:bg-amber-500/20'
|
| 100 |
+
: 'group-hover:bg-purple-500/20'
|
| 101 |
+
}`}
|
| 102 |
+
data-oid="gg4owr7"
|
| 103 |
+
>
|
| 104 |
+
<span
|
| 105 |
+
className={`text-lg font-semibold text-gray-300
|
| 106 |
+
${
|
| 107 |
+
isSpecials
|
| 108 |
+
? 'group-hover:text-amber-300'
|
| 109 |
+
: 'group-hover:text-purple-300'
|
| 110 |
+
}`}
|
| 111 |
+
data-oid="4.djlcp"
|
| 112 |
+
>
|
| 113 |
+
{episodeNum}
|
| 114 |
+
</span>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
{/* Episode Info */}
|
| 118 |
+
<div className="flex-grow" data-oid="1.f11w6">
|
| 119 |
+
<h4
|
| 120 |
+
className="text-gray-200 font-medium"
|
| 121 |
+
data-oid="_6lmv4j"
|
| 122 |
+
>
|
| 123 |
+
{episodeTitle.replace(/_/g, ' ')}
|
| 124 |
+
</h4>
|
| 125 |
+
<div
|
| 126 |
+
className="flex items-center gap-3 text-sm text-gray-400"
|
| 127 |
+
data-oid="ahjmc12"
|
| 128 |
+
>
|
| 129 |
+
<span data-oid="a0trzby">
|
| 130 |
+
{Math.round(episode.size / 1024 / 1024)}{' '}
|
| 131 |
+
MB
|
| 132 |
+
</span>
|
| 133 |
+
<span data-oid="1hyz3i4">•</span>
|
| 134 |
+
<span data-oid="ib0kbnn">
|
| 135 |
+
{episode.path.includes('720p')
|
| 136 |
+
? 'HD'
|
| 137 |
+
: 'SD'}
|
| 138 |
+
</span>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
{/* Play Button */}
|
| 143 |
+
<button
|
| 144 |
+
className={`flex-shrink-0 p-2 rounded-full text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity
|
| 145 |
+
${
|
| 146 |
+
isSpecials
|
| 147 |
+
? 'bg-amber-500/20 hover:bg-amber-500/30'
|
| 148 |
+
: 'bg-purple-500/20 hover:bg-purple-500/30'
|
| 149 |
+
}`}
|
| 150 |
+
data-oid="xw0-ovl"
|
| 151 |
+
>
|
| 152 |
+
<svg
|
| 153 |
+
className="w-5 h-5"
|
| 154 |
+
fill="currentColor"
|
| 155 |
+
viewBox="0 0 20 20"
|
| 156 |
+
data-oid="sk9m.8v"
|
| 157 |
+
>
|
| 158 |
+
<path
|
| 159 |
+
d="M4 4l12 6-12 6V4z"
|
| 160 |
+
data-oid="y1gqh_1"
|
| 161 |
+
/>
|
| 162 |
+
</svg>
|
| 163 |
+
</button>
|
| 164 |
+
</div>
|
| 165 |
+
);
|
| 166 |
+
},
|
| 167 |
+
)}
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
)}
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
);
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
export default function TvShowTitlePage() {
|
| 178 |
+
const { title } = useParams();
|
| 179 |
+
const decodedTitle = decodeURIComponent(Array.isArray(title) ? title[0] : title);
|
| 180 |
+
|
| 181 |
+
interface TvShow {
|
| 182 |
+
name: string;
|
| 183 |
+
image: string;
|
| 184 |
+
translations?: any;
|
| 185 |
+
year?: number;
|
| 186 |
+
genres?: any[];
|
| 187 |
+
score?: number;
|
| 188 |
+
contentRatings?: any[];
|
| 189 |
+
characters?: any;
|
| 190 |
+
artworks?: any[];
|
| 191 |
+
file_structure?: any;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
interface FileStructure {
|
| 195 |
+
contents?: any[];
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
const [tvshow, setTvShow] = useState<TvShow | null>(null);
|
| 199 |
+
const [fileStructure, setFileStructure] = useState<FileStructure>({});
|
| 200 |
+
|
| 201 |
+
useEffect(() => {
|
| 202 |
+
async function fetchMovie() {
|
| 203 |
+
try {
|
| 204 |
+
const data = await getTvShowMetadata(decodedTitle);
|
| 205 |
+
setTvShow(data.data);
|
| 206 |
+
setFileStructure(data.file_structure);
|
| 207 |
+
} catch (error) {
|
| 208 |
+
console.error('Failed to fetch movie details', error);
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
fetchMovie();
|
| 212 |
+
}, [decodedTitle]);
|
| 213 |
+
|
| 214 |
+
if (!tvshow) {
|
| 215 |
+
return <LoadingSkeleton data-oid="1w.hc1q" />;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
return (
|
| 219 |
+
<div
|
| 220 |
+
className="relative w-full min-h-screen h-full flex items-start landscape:pt-24 pt-28 text-white"
|
| 221 |
+
style={{
|
| 222 |
+
backgroundImage: `url(${tvshow.artworks?.[0]?.image || tvshow.image})`,
|
| 223 |
+
backgroundSize: 'cover',
|
| 224 |
+
backgroundPosition: 'top',
|
| 225 |
+
backgroundAttachment: 'fixed',
|
| 226 |
+
}}
|
| 227 |
+
data-oid="0t-8gd6"
|
| 228 |
+
>
|
| 229 |
+
{/* Gradient Overlays */}
|
| 230 |
+
<div className="h-screen fixed inset-0" data-oid="5-ul0qs">
|
| 231 |
+
<div
|
| 232 |
+
className="h-full bg-gradient-to-b from-gray-900/90 via-gray-900/50 to-gray-900/90"
|
| 233 |
+
data-oid="uwf2xrs"
|
| 234 |
+
></div>
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
{/* Main Content Container */}
|
| 238 |
+
<div
|
| 239 |
+
className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
|
| 240 |
+
data-oid="cagjlpk"
|
| 241 |
+
>
|
| 242 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8" data-oid="vic82wp">
|
| 243 |
+
{/* Left Column - Main Info */}
|
| 244 |
+
<div className="lg:col-span-2 space-y-6" data-oid="yxtbieb">
|
| 245 |
+
<div
|
| 246 |
+
className="bg-gray-800/60 backdrop-blur-md rounded-2xl p-6 border border-gray-700/50"
|
| 247 |
+
data-oid="v03u3f8"
|
| 248 |
+
>
|
| 249 |
+
{/* Title and Year */}
|
| 250 |
+
<div className="space-y-2" data-oid="prpjazr">
|
| 251 |
+
<h1 className="text-4xl font-bold text-white" data-oid="w3tnf--">
|
| 252 |
+
{tvshow.translations?.nameTranslations?.find(
|
| 253 |
+
(t: { language: string; name: string }) =>
|
| 254 |
+
t.language === 'eng',
|
| 255 |
+
)?.name ||
|
| 256 |
+
tvshow.translations?.nameTranslations?.find(
|
| 257 |
+
(t: {
|
| 258 |
+
isAlias: boolean;
|
| 259 |
+
language: string;
|
| 260 |
+
name: string;
|
| 261 |
+
}) => t.isAlias && t.language === 'eng',
|
| 262 |
+
)?.name ||
|
| 263 |
+
tvshow?.name ||
|
| 264 |
+
'Unknown Title'}
|
| 265 |
+
</h1>
|
| 266 |
+
<div
|
| 267 |
+
className="flex items-center gap-3 text-gray-300"
|
| 268 |
+
data-oid="clwsp-l"
|
| 269 |
+
>
|
| 270 |
+
<span className="text-lg" data-oid="v66_mp8">
|
| 271 |
+
{tvshow.year}
|
| 272 |
+
</span>
|
| 273 |
+
<span data-oid="mcuiami">•</span>
|
| 274 |
+
<span
|
| 275 |
+
className="bg-purple-500/20 text-purple-300 px-2 py-0.5 rounded text-sm"
|
| 276 |
+
data-oid="bqk3z3j"
|
| 277 |
+
>
|
| 278 |
+
TV Series
|
| 279 |
+
</span>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
{/* Genres and Score */}
|
| 283 |
+
<div
|
| 284 |
+
className="flex flex-wrap items-center gap-4 mt-4"
|
| 285 |
+
data-oid="-brwied"
|
| 286 |
+
>
|
| 287 |
+
<div className="flex items-center gap-2" data-oid="-u.ed9k">
|
| 288 |
+
<span
|
| 289 |
+
className="bg-purple-500 text-white px-3 py-1 rounded-full text-sm font-medium"
|
| 290 |
+
data-oid="h188t58"
|
| 291 |
+
>
|
| 292 |
+
⭐ {tvshow.score ? (tvshow.score / 1000).toFixed(1) : 'N/A'}
|
| 293 |
+
</span>
|
| 294 |
+
</div>
|
| 295 |
+
<div className="flex flex-wrap gap-2" data-oid="pz.v6zj">
|
| 296 |
+
{Array.isArray(tvshow.genres)
|
| 297 |
+
? tvshow.genres.map((genre) => (
|
| 298 |
+
<Link
|
| 299 |
+
key={genre.id}
|
| 300 |
+
href={`/genre/${encodeURIComponent(genre.slug)}`}
|
| 301 |
+
className="bg-gray-700/50 hover:bg-gray-600/50 text-gray-200 px-3 py-1 rounded-full text-sm transition-colors"
|
| 302 |
+
data-oid=".jalfq6"
|
| 303 |
+
>
|
| 304 |
+
{genre.name}
|
| 305 |
+
</Link>
|
| 306 |
+
))
|
| 307 |
+
: null}
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
{/* Content Ratings */}
|
| 311 |
+
{Array.isArray(tvshow.contentRatings) &&
|
| 312 |
+
tvshow.contentRatings.length > 0 ? (
|
| 313 |
+
<div className="mt-6" data-oid="jeyectu">
|
| 314 |
+
<h3
|
| 315 |
+
className="text-lg font-semibold text-gray-200 mb-3"
|
| 316 |
+
data-oid="1c5r65f"
|
| 317 |
+
>
|
| 318 |
+
Content Ratings
|
| 319 |
+
</h3>
|
| 320 |
+
<ul className="flex flex-wrap gap-3" data-oid="dbshej9">
|
| 321 |
+
{tvshow.contentRatings.map((rating, index) => {
|
| 322 |
+
// Map country codes to corresponding flags
|
| 323 |
+
const countryFlags = {
|
| 324 |
+
AUS: '🇦🇺',
|
| 325 |
+
USA: '🇺🇸',
|
| 326 |
+
GBR: '🇬🇧',
|
| 327 |
+
EU: '🇪🇺',
|
| 328 |
+
JPN: '🇯🇵',
|
| 329 |
+
KOR: '🇰🇷',
|
| 330 |
+
CAN: '🇨🇦',
|
| 331 |
+
DEU: '🇩🇪',
|
| 332 |
+
FRA: '🇫🇷',
|
| 333 |
+
ESP: '🇪🇸',
|
| 334 |
+
ITA: '🇮🇹',
|
| 335 |
+
};
|
| 336 |
+
|
| 337 |
+
const flag =
|
| 338 |
+
countryFlags[
|
| 339 |
+
rating.country.toUpperCase() as keyof typeof countryFlags
|
| 340 |
+
] || '🌍'; // Default to globe if no match
|
| 341 |
+
|
| 342 |
+
return (
|
| 343 |
+
<li
|
| 344 |
+
key={index}
|
| 345 |
+
className="flex items-center bg-gray-800/50 px-2 py-1 rounded-md text-xs"
|
| 346 |
+
data-oid="i78owye"
|
| 347 |
+
>
|
| 348 |
+
<span className="mr-2" data-oid="7_r:-jk">
|
| 349 |
+
{flag}
|
| 350 |
+
</span>
|
| 351 |
+
<span
|
| 352 |
+
className="text-white font-semibold"
|
| 353 |
+
data-oid="8q9px9."
|
| 354 |
+
>
|
| 355 |
+
{rating.name}
|
| 356 |
+
</span>
|
| 357 |
+
<span
|
| 358 |
+
className="text-gray-400 ml-1"
|
| 359 |
+
data-oid="zf08c.5"
|
| 360 |
+
>
|
| 361 |
+
{' '}
|
| 362 |
+
- {rating.description || 'N/A'}
|
| 363 |
+
</span>
|
| 364 |
+
</li>
|
| 365 |
+
);
|
| 366 |
+
})}
|
| 367 |
+
</ul>
|
| 368 |
+
</div>
|
| 369 |
+
) : (
|
| 370 |
+
<p className="text-gray-400 text-sm" data-oid="x_m8c:m">
|
| 371 |
+
No content ratings available.
|
| 372 |
+
</p>
|
| 373 |
+
)}
|
| 374 |
+
{/* Action Buttons */}{' '}
|
| 375 |
+
<div
|
| 376 |
+
className="flex justify-start landscape:gap-2 mt-4"
|
| 377 |
+
data-oid="1w2v9n0"
|
| 378 |
+
>
|
| 379 |
+
<Link
|
| 380 |
+
href={'#play'}
|
| 381 |
+
className="bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-s-2xl flex items-center transition-colors text-sm md:text-base"
|
| 382 |
+
data-oid="mxsr0t."
|
| 383 |
+
>
|
| 384 |
+
<svg
|
| 385 |
+
className="w-4 h-4 md:w-5 md:h-5 mr-2"
|
| 386 |
+
fill="currentColor"
|
| 387 |
+
viewBox="0 0 20 20"
|
| 388 |
+
data-oid="wb166p6"
|
| 389 |
+
>
|
| 390 |
+
<path d="M4 4l12 6-12 6V4z" data-oid="oacqels" />
|
| 391 |
+
</svg>
|
| 392 |
+
Play Now
|
| 393 |
+
</Link>
|
| 394 |
+
<Link
|
| 395 |
+
href={'#'}
|
| 396 |
+
className="bg-gray-800/80 hover:bg-gray-700/80 px-4 py-2 md:px-8 landscape:rounded-3xl rounded-e-2xl transition-colors text-sm md:text-base"
|
| 397 |
+
data-oid="1qwh9q3"
|
| 398 |
+
>
|
| 399 |
+
Add to My List
|
| 400 |
+
</Link>
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
{/* Overview Section */}
|
| 404 |
+
<div
|
| 405 |
+
className="bg-gray-800/60 backdrop-blur-md rounded-2xl p-6 border border-gray-700/50"
|
| 406 |
+
data-oid="49xalb5"
|
| 407 |
+
>
|
| 408 |
+
<h3
|
| 409 |
+
className="text-lg font-semibold text-gray-200 mb-3"
|
| 410 |
+
data-oid="j0l9.dk"
|
| 411 |
+
>
|
| 412 |
+
Overview
|
| 413 |
+
</h3>
|
| 414 |
+
<p className="text-gray-300 leading-relaxed" data-oid="f4smlv3">
|
| 415 |
+
{tvshow.translations?.overviewTranslations?.find(
|
| 416 |
+
(t: { language: string; overview: string }) =>
|
| 417 |
+
t.language === 'eng',
|
| 418 |
+
)?.overview ||
|
| 419 |
+
tvshow.translations?.overviewTranslations?.[0]?.overview ||
|
| 420 |
+
'No overview available.'}
|
| 421 |
+
</p>
|
| 422 |
+
</div>
|
| 423 |
+
{/* Episodes Section */}
|
| 424 |
+
<EpisodesSection fileStructure={fileStructure} data-oid="wcxi:s8" />
|
| 425 |
+
{/* end of episodes section*/}
|
| 426 |
+
</div>
|
| 427 |
+
|
| 428 |
+
{/* Right Column - Cast */}
|
| 429 |
+
<div className="lg:col-span-1" data-oid="-ar8nfz">
|
| 430 |
+
<CastSection movie={tvshow} data-oid="zvjum9x" />
|
| 431 |
+
</div>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
);
|
| 436 |
+
}
|
frontend/app/tvshows/page.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState, Suspense } from 'react';
|
| 4 |
-
import { useSearchParams, useRouter } from 'next/navigation';
|
| 5 |
-
import { getAllTvShows } from '@/lib/lb';
|
| 6 |
-
import { TvShowCardTemp } from '@/components/tvshow/TvShowCardTemp';
|
| 7 |
-
|
| 8 |
-
function TvShowsContent() {
|
| 9 |
-
const searchParams = useSearchParams();
|
| 10 |
-
const router = useRouter();
|
| 11 |
-
const currentPage = parseInt(searchParams.get('page') || '1', 10);
|
| 12 |
-
const [tvshows, setTvShows] = useState([]);
|
| 13 |
-
const [loading, setLoading] = useState(true);
|
| 14 |
-
const itemsPerPage = 16;
|
| 15 |
-
const [totalPages, setTotalPages] = useState(1);
|
| 16 |
-
|
| 17 |
-
useEffect(() => {
|
| 18 |
-
async function fetchTvShows() {
|
| 19 |
-
setLoading(true);
|
| 20 |
-
try {
|
| 21 |
-
const data = await getAllTvShows();
|
| 22 |
-
if (data.length) {
|
| 23 |
-
setTotalPages(Math.ceil(data.length / itemsPerPage));
|
| 24 |
-
const startIndex = (currentPage - 1) * itemsPerPage;
|
| 25 |
-
setTvShows(data.slice(startIndex, startIndex + itemsPerPage));
|
| 26 |
-
}
|
| 27 |
-
} catch (error) {
|
| 28 |
-
console.error('Error fetching TV shows:', error);
|
| 29 |
-
} finally {
|
| 30 |
-
setLoading(false);
|
| 31 |
-
}
|
| 32 |
-
}
|
| 33 |
-
fetchTvShows();
|
| 34 |
-
}, [currentPage]);
|
| 35 |
-
|
| 36 |
-
const handlePageChange = (newPage) => {
|
| 37 |
-
if (newPage >= 1 && newPage <= totalPages) {
|
| 38 |
-
router.push(`/tvshows?page=${newPage}`);
|
| 39 |
-
}
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
return (
|
| 43 |
-
<>
|
| 44 |
-
{loading ? (
|
| 45 |
-
<p className="text-center">Loading...</p>
|
| 46 |
-
) : (
|
| 47 |
-
<>
|
| 48 |
-
<div className="grid grid-cols-2 md:grid-cols-8 gap-4 md:gap-6">
|
| 49 |
-
{tvshows.map((show, index) => (
|
| 50 |
-
<TvShowCardTemp
|
| 51 |
-
key={index}
|
| 52 |
-
title={show.title}
|
| 53 |
-
episodesCount={show.episodeCount}
|
| 54 |
-
/>
|
| 55 |
-
))}
|
| 56 |
-
</div>
|
| 57 |
-
<div className="flex justify-center mt-8 gap-4">
|
| 58 |
-
<button
|
| 59 |
-
onClick={() => handlePageChange(currentPage - 1)}
|
| 60 |
-
disabled={currentPage <= 1}
|
| 61 |
-
className="px-4 py-2 bg-gray-700 text-white rounded disabled:opacity-50">
|
| 62 |
-
Previous
|
| 63 |
-
</button>
|
| 64 |
-
<span className="px-4 py-2 text-white">Page {currentPage} of {totalPages}</span>
|
| 65 |
-
<button
|
| 66 |
-
onClick={() => handlePageChange(currentPage + 1)}
|
| 67 |
-
disabled={currentPage >= totalPages}
|
| 68 |
-
className="px-4 py-2 bg-gray-700 text-white rounded disabled:opacity-50">
|
| 69 |
-
Next
|
| 70 |
-
</button>
|
| 71 |
-
</div>
|
| 72 |
-
</>
|
| 73 |
-
)}
|
| 74 |
-
</>
|
| 75 |
-
);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
export default function TvShowsPage() {
|
| 79 |
-
return (
|
| 80 |
-
<div className="py-16">
|
| 81 |
-
<div className="container mx-auto px-6">
|
| 82 |
-
<h2 className="text-2xl font-bold mb-8 bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
|
| 83 |
-
Discover TV Shows
|
| 84 |
-
</h2>
|
| 85 |
-
<Suspense fallback={<p className="text-center">Loading...</p>}>
|
| 86 |
-
<TvShowsContent />
|
| 87 |
-
</Suspense>
|
| 88 |
-
</div>
|
| 89 |
-
</div>
|
| 90 |
-
);
|
| 91 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/tvshows/page.tsx
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState, Suspense } from 'react';
|
| 4 |
+
import { useSearchParams, useRouter } from 'next/navigation';
|
| 5 |
+
import { getAllTvShows } from '@/lib/lb';
|
| 6 |
+
import { TvShowCard } from '@/components/tvshow/TvShowCard';
|
| 7 |
+
import { useLoading } from '@/components/loading/SplashScreen';
|
| 8 |
+
|
| 9 |
+
function TvShowsContent() {
|
| 10 |
+
const searchParams = useSearchParams();
|
| 11 |
+
const router = useRouter();
|
| 12 |
+
const currentPage = parseInt(searchParams.get('page') || '1', 10);
|
| 13 |
+
const [tvshows, setTvShows] = useState<{ title: string; episodeCount: any }[]>([]);
|
| 14 |
+
const { loading, setLoading } = useLoading();
|
| 15 |
+
const itemsPerPage = 16;
|
| 16 |
+
const [totalPages, setTotalPages] = useState(1);
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
async function fetchTvShows() {
|
| 19 |
+
setLoading(true);
|
| 20 |
+
try {
|
| 21 |
+
const data = await getAllTvShows();
|
| 22 |
+
if (data.length) {
|
| 23 |
+
setTotalPages(Math.ceil(data.length / itemsPerPage));
|
| 24 |
+
const startIndex = (currentPage - 1) * itemsPerPage;
|
| 25 |
+
setTvShows(data.slice(startIndex, startIndex + itemsPerPage));
|
| 26 |
+
}
|
| 27 |
+
} catch (error) {
|
| 28 |
+
console.error('Error fetching TV shows:', error);
|
| 29 |
+
} finally {
|
| 30 |
+
setLoading(false);
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
fetchTvShows();
|
| 34 |
+
}, [currentPage]);
|
| 35 |
+
|
| 36 |
+
const handlePageChange = (newPage: number) => {
|
| 37 |
+
if (newPage >= 1 && newPage <= totalPages) {
|
| 38 |
+
router.push(`/tvshows?page=${newPage}`);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="p-6" data-oid="mfagx7.">
|
| 44 |
+
{loading ? (
|
| 45 |
+
<div className="flex items-center justify-center min-h-[50vh]" data-oid="uooinde">
|
| 46 |
+
<div
|
| 47 |
+
className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
|
| 48 |
+
data-oid="eb5e30i"
|
| 49 |
+
></div>
|
| 50 |
+
</div>
|
| 51 |
+
) : (
|
| 52 |
+
<>
|
| 53 |
+
{/* Dynamic Grid */}
|
| 54 |
+
<div
|
| 55 |
+
className="grid portrait:gap-2 gap-14"
|
| 56 |
+
style={{
|
| 57 |
+
gridTemplateColumns: 'repeat(auto-fill, minmax(min(150px, 100%), 1fr))',
|
| 58 |
+
'@media (min-width: 640px)': {
|
| 59 |
+
gridTemplateColumns: 'repeat(auto-fill, minmax(225px, 1fr))',
|
| 60 |
+
},
|
| 61 |
+
'@media (min-width: 1024px)': {
|
| 62 |
+
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
| 63 |
+
},
|
| 64 |
+
}}
|
| 65 |
+
data-oid="jom5n2c"
|
| 66 |
+
>
|
| 67 |
+
{tvshows.map((show, index) => (
|
| 68 |
+
<div
|
| 69 |
+
key={index}
|
| 70 |
+
className="transform transition-transform duration-300 hover:scale-105 w-[fit-content]"
|
| 71 |
+
data-oid="kwv3cwu"
|
| 72 |
+
>
|
| 73 |
+
<TvShowCard
|
| 74 |
+
title={show.title}
|
| 75 |
+
episodesCount={show.episodeCount}
|
| 76 |
+
data-oid="b3ud42f"
|
| 77 |
+
/>
|
| 78 |
+
</div>
|
| 79 |
+
))}
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
{/* Improved Pagination */}
|
| 83 |
+
<div
|
| 84 |
+
className="mt-12 flex flex-col sm:flex-row items-center justify-center gap-4"
|
| 85 |
+
data-oid="4z66hfe"
|
| 86 |
+
>
|
| 87 |
+
<div className="flex items-center gap-2" data-oid="u0wozr_">
|
| 88 |
+
<button
|
| 89 |
+
onClick={() => handlePageChange(1)}
|
| 90 |
+
disabled={currentPage <= 1}
|
| 91 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 92 |
+
data-oid="s9brfdx"
|
| 93 |
+
>
|
| 94 |
+
<svg
|
| 95 |
+
className="w-5 h-5"
|
| 96 |
+
fill="none"
|
| 97 |
+
stroke="currentColor"
|
| 98 |
+
viewBox="0 0 24 24"
|
| 99 |
+
data-oid="csf:grn"
|
| 100 |
+
>
|
| 101 |
+
<path
|
| 102 |
+
strokeLinecap="round"
|
| 103 |
+
strokeLinejoin="round"
|
| 104 |
+
strokeWidth={2}
|
| 105 |
+
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
|
| 106 |
+
data-oid=".d2eccm"
|
| 107 |
+
/>
|
| 108 |
+
</svg>
|
| 109 |
+
</button>
|
| 110 |
+
<button
|
| 111 |
+
onClick={() => handlePageChange(currentPage - 1)}
|
| 112 |
+
disabled={currentPage <= 1}
|
| 113 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 114 |
+
data-oid="bqrtm80"
|
| 115 |
+
>
|
| 116 |
+
<svg
|
| 117 |
+
className="w-5 h-5"
|
| 118 |
+
fill="none"
|
| 119 |
+
stroke="currentColor"
|
| 120 |
+
viewBox="0 0 24 24"
|
| 121 |
+
data-oid="6a0yg4_"
|
| 122 |
+
>
|
| 123 |
+
<path
|
| 124 |
+
strokeLinecap="round"
|
| 125 |
+
strokeLinejoin="round"
|
| 126 |
+
strokeWidth={2}
|
| 127 |
+
d="M15 19l-7-7 7-7"
|
| 128 |
+
data-oid="1pnx7a0"
|
| 129 |
+
/>
|
| 130 |
+
</svg>
|
| 131 |
+
</button>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div className="flex items-center gap-2" data-oid="ag92fzo">
|
| 135 |
+
<span
|
| 136 |
+
className="px-4 py-2 rounded-lg bg-gray-800 text-white font-medium"
|
| 137 |
+
data-oid="vu6z6:z"
|
| 138 |
+
>
|
| 139 |
+
Page {currentPage} of {totalPages}
|
| 140 |
+
</span>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<div className="flex items-center gap-2" data-oid="nckg2v7">
|
| 144 |
+
<button
|
| 145 |
+
onClick={() => handlePageChange(currentPage + 1)}
|
| 146 |
+
disabled={currentPage >= totalPages}
|
| 147 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 148 |
+
data-oid="psvhnlh"
|
| 149 |
+
>
|
| 150 |
+
<svg
|
| 151 |
+
className="w-5 h-5"
|
| 152 |
+
fill="none"
|
| 153 |
+
stroke="currentColor"
|
| 154 |
+
viewBox="0 0 24 24"
|
| 155 |
+
data-oid="7_wyhov"
|
| 156 |
+
>
|
| 157 |
+
<path
|
| 158 |
+
strokeLinecap="round"
|
| 159 |
+
strokeLinejoin="round"
|
| 160 |
+
strokeWidth={2}
|
| 161 |
+
d="M9 5l7 7-7 7"
|
| 162 |
+
data-oid="dr2p7r0"
|
| 163 |
+
/>
|
| 164 |
+
</svg>
|
| 165 |
+
</button>
|
| 166 |
+
<button
|
| 167 |
+
onClick={() => handlePageChange(totalPages)}
|
| 168 |
+
disabled={currentPage >= totalPages}
|
| 169 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 170 |
+
data-oid="jrzhg0."
|
| 171 |
+
>
|
| 172 |
+
<svg
|
| 173 |
+
className="w-5 h-5"
|
| 174 |
+
fill="none"
|
| 175 |
+
stroke="currentColor"
|
| 176 |
+
viewBox="0 0 24 24"
|
| 177 |
+
data-oid="os2n_ha"
|
| 178 |
+
>
|
| 179 |
+
<path
|
| 180 |
+
strokeLinecap="round"
|
| 181 |
+
strokeLinejoin="round"
|
| 182 |
+
strokeWidth={2}
|
| 183 |
+
d="M13 5l7 7-7 7M5 5l7 7-7 7"
|
| 184 |
+
data-oid="g0m_z54"
|
| 185 |
+
/>
|
| 186 |
+
</svg>
|
| 187 |
+
</button>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</>
|
| 191 |
+
)}
|
| 192 |
+
</div>
|
| 193 |
+
);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
export default function TvShowsPage() {
|
| 197 |
+
return (
|
| 198 |
+
<div className="min-h-screen pt-24 pb-16" data-oid="vmkunj1">
|
| 199 |
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8" data-oid="e8nsmst">
|
| 200 |
+
{/* Header Section */}
|
| 201 |
+
<div className="mb-8 space-y-4" data-oid="d36d5sh">
|
| 202 |
+
<h2 className="text-4xl font-bold text-white" data-oid=".2j2af-">
|
| 203 |
+
TV Shows
|
| 204 |
+
</h2>
|
| 205 |
+
<p className="text-gray-400 max-w-3xl" data-oid="d0g1zry">
|
| 206 |
+
Explore our collection of TV series from various genres. From drama to
|
| 207 |
+
comedy, find your next binge-worthy show here.
|
| 208 |
+
</p>
|
| 209 |
+
{/* Genre Filter Chips - You can make these functional later */}
|
| 210 |
+
<div className="flex flex-wrap gap-2" data-oid="t-cl4a6">
|
| 211 |
+
{['All', 'Action', 'Drama', 'Comedy', 'Anime', 'Sci-Fi'].map((genre) => (
|
| 212 |
+
<button
|
| 213 |
+
key={genre}
|
| 214 |
+
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors
|
| 215 |
+
${
|
| 216 |
+
genre === 'All'
|
| 217 |
+
? 'bg-purple-600 text-white'
|
| 218 |
+
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
|
| 219 |
+
}`}
|
| 220 |
+
data-oid="wb9w_xa"
|
| 221 |
+
>
|
| 222 |
+
{genre}
|
| 223 |
+
</button>
|
| 224 |
+
))}
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
{/* Content Section */}
|
| 229 |
+
<div
|
| 230 |
+
className="bg-gray-800/30 rounded-3xl backdrop-blur-sm border border-gray-700/50"
|
| 231 |
+
data-oid="gxvywvg"
|
| 232 |
+
>
|
| 233 |
+
<Suspense
|
| 234 |
+
fallback={
|
| 235 |
+
<div
|
| 236 |
+
className="flex items-center justify-center min-h-[50vh]"
|
| 237 |
+
data-oid="7t-_ota"
|
| 238 |
+
>
|
| 239 |
+
<div
|
| 240 |
+
className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
|
| 241 |
+
data-oid="-4.z3yi"
|
| 242 |
+
></div>
|
| 243 |
+
</div>
|
| 244 |
+
}
|
| 245 |
+
data-oid="cgb8xnf"
|
| 246 |
+
>
|
| 247 |
+
<TvShowsContent data-oid="731rgcy" />
|
| 248 |
+
</Suspense>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
);
|
| 253 |
+
}
|
frontend/app/tvshows/page.tsx.tmp
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState, Suspense } from 'react';
|
| 4 |
+
import { useSearchParams, useRouter } from 'next/navigation';
|
| 5 |
+
import { getAllTvShows } from '@/lib/lb';
|
| 6 |
+
import { TvShowCard } from '@/components/tvshow/TvShowCard';
|
| 7 |
+
import { useLoading } from '@/components/loading/SplashScreen';
|
| 8 |
+
|
| 9 |
+
function TvShowsContent() {
|
| 10 |
+
const searchParams = useSearchParams();
|
| 11 |
+
const router = useRouter();
|
| 12 |
+
const currentPage = parseInt(searchParams.get('page') || '1', 10);
|
| 13 |
+
const [tvshows, setTvShows] = useState<{ title: string; episodeCount: any }[]>([]);
|
| 14 |
+
const { loading, setLoading } = useLoading();
|
| 15 |
+
const itemsPerPage = 16;
|
| 16 |
+
const [totalPages, setTotalPages] = useState(1);
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
async function fetchTvShows() {
|
| 19 |
+
setLoading(true);
|
| 20 |
+
try {
|
| 21 |
+
const data = await getAllTvShows();
|
| 22 |
+
if (data.length) {
|
| 23 |
+
setTotalPages(Math.ceil(data.length / itemsPerPage));
|
| 24 |
+
const startIndex = (currentPage - 1) * itemsPerPage;
|
| 25 |
+
setTvShows(data.slice(startIndex, startIndex + itemsPerPage));
|
| 26 |
+
}
|
| 27 |
+
} catch (error) {
|
| 28 |
+
console.error('Error fetching TV shows:', error);
|
| 29 |
+
} finally {
|
| 30 |
+
setLoading(false);
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
fetchTvShows();
|
| 34 |
+
}, [currentPage]);
|
| 35 |
+
|
| 36 |
+
const handlePageChange = (newPage: number) => {
|
| 37 |
+
if (newPage >= 1 && newPage <= totalPages) {
|
| 38 |
+
router.push(`/tvshows?page=${newPage}`);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="p-6" data-oid="mfagx7.">
|
| 44 |
+
{loading ? (
|
| 45 |
+
<div className="flex items-center justify-center min-h-[50vh]" data-oid="uooinde">
|
| 46 |
+
<div
|
| 47 |
+
className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
|
| 48 |
+
data-oid="eb5e30i"
|
| 49 |
+
></div>
|
| 50 |
+
</div>
|
| 51 |
+
) : (
|
| 52 |
+
<>
|
| 53 |
+
{/* Dynamic Grid */}
|
| 54 |
+
<div
|
| 55 |
+
className="grid portrait:gap-2 gap-14"
|
| 56 |
+
style={{
|
| 57 |
+
gridTemplateColumns: 'repeat(auto-fill, minmax(min(150px, 100%), 1fr))',
|
| 58 |
+
'@media (min-width: 640px)': {
|
| 59 |
+
gridTemplateColumns: 'repeat(auto-fill, minmax(225px, 1fr))',
|
| 60 |
+
},
|
| 61 |
+
'@media (min-width: 1024px)': {
|
| 62 |
+
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
| 63 |
+
},
|
| 64 |
+
}}
|
| 65 |
+
data-oid="jom5n2c"
|
| 66 |
+
>
|
| 67 |
+
{tvshows.map((show, index) => (
|
| 68 |
+
<div
|
| 69 |
+
key={index}
|
| 70 |
+
className="transform transition-transform duration-300 hover:scale-105 w-[fit-content]"
|
| 71 |
+
data-oid="kwv3cwu"
|
| 72 |
+
>
|
| 73 |
+
<TvShowCard
|
| 74 |
+
title={show.title}
|
| 75 |
+
episodesCount={show.episodeCount}
|
| 76 |
+
data-oid="b3ud42f"
|
| 77 |
+
/>
|
| 78 |
+
</div>
|
| 79 |
+
))}
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
{/* Improved Pagination */}
|
| 83 |
+
<div
|
| 84 |
+
className="mt-12 flex flex-col sm:flex-row items-center justify-center gap-4"
|
| 85 |
+
data-oid="4z66hfe"
|
| 86 |
+
>
|
| 87 |
+
<div className="flex items-center gap-2" data-oid="u0wozr_">
|
| 88 |
+
<button
|
| 89 |
+
onClick={() => handlePageChange(1)}
|
| 90 |
+
disabled={currentPage <= 1}
|
| 91 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 92 |
+
data-oid="s9brfdx"
|
| 93 |
+
>
|
| 94 |
+
<svg
|
| 95 |
+
className="w-5 h-5"
|
| 96 |
+
fill="none"
|
| 97 |
+
stroke="currentColor"
|
| 98 |
+
viewBox="0 0 24 24"
|
| 99 |
+
data-oid="csf:grn"
|
| 100 |
+
>
|
| 101 |
+
<path
|
| 102 |
+
strokeLinecap="round"
|
| 103 |
+
strokeLinejoin="round"
|
| 104 |
+
strokeWidth={2}
|
| 105 |
+
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
|
| 106 |
+
data-oid=".d2eccm"
|
| 107 |
+
/>
|
| 108 |
+
</svg>
|
| 109 |
+
</button>
|
| 110 |
+
<button
|
| 111 |
+
onClick={() => handlePageChange(currentPage - 1)}
|
| 112 |
+
disabled={currentPage <= 1}
|
| 113 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 114 |
+
data-oid="bqrtm80"
|
| 115 |
+
>
|
| 116 |
+
<svg
|
| 117 |
+
className="w-5 h-5"
|
| 118 |
+
fill="none"
|
| 119 |
+
stroke="currentColor"
|
| 120 |
+
viewBox="0 0 24 24"
|
| 121 |
+
data-oid="6a0yg4_"
|
| 122 |
+
>
|
| 123 |
+
<path
|
| 124 |
+
strokeLinecap="round"
|
| 125 |
+
strokeLinejoin="round"
|
| 126 |
+
strokeWidth={2}
|
| 127 |
+
d="M15 19l-7-7 7-7"
|
| 128 |
+
data-oid="1pnx7a0"
|
| 129 |
+
/>
|
| 130 |
+
</svg>
|
| 131 |
+
</button>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div className="flex items-center gap-2" data-oid="ag92fzo">
|
| 135 |
+
<span
|
| 136 |
+
className="px-4 py-2 rounded-lg bg-gray-800 text-white font-medium"
|
| 137 |
+
data-oid="vu6z6:z"
|
| 138 |
+
>
|
| 139 |
+
Page {currentPage} of {totalPages}
|
| 140 |
+
</span>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<div className="flex items-center gap-2" data-oid="nckg2v7">
|
| 144 |
+
<button
|
| 145 |
+
onClick={() => handlePageChange(currentPage + 1)}
|
| 146 |
+
disabled={currentPage >= totalPages}
|
| 147 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 148 |
+
data-oid="psvhnlh"
|
| 149 |
+
>
|
| 150 |
+
<svg
|
| 151 |
+
className="w-5 h-5"
|
| 152 |
+
fill="none"
|
| 153 |
+
stroke="currentColor"
|
| 154 |
+
viewBox="0 0 24 24"
|
| 155 |
+
data-oid="7_wyhov"
|
| 156 |
+
>
|
| 157 |
+
<path
|
| 158 |
+
strokeLinecap="round"
|
| 159 |
+
strokeLinejoin="round"
|
| 160 |
+
strokeWidth={2}
|
| 161 |
+
d="M9 5l7 7-7 7"
|
| 162 |
+
data-oid="dr2p7r0"
|
| 163 |
+
/>
|
| 164 |
+
</svg>
|
| 165 |
+
</button>
|
| 166 |
+
<button
|
| 167 |
+
onClick={() => handlePageChange(totalPages)}
|
| 168 |
+
disabled={currentPage >= totalPages}
|
| 169 |
+
className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 disabled:hover:bg-gray-800 disabled:hover:text-gray-400 transition-colors"
|
| 170 |
+
data-oid="jrzhg0."
|
| 171 |
+
>
|
| 172 |
+
<svg
|
| 173 |
+
className="w-5 h-5"
|
| 174 |
+
fill="none"
|
| 175 |
+
stroke="currentColor"
|
| 176 |
+
viewBox="0 0 24 24"
|
| 177 |
+
data-oid="os2n_ha"
|
| 178 |
+
>
|
| 179 |
+
<path
|
| 180 |
+
strokeLinecap="round"
|
| 181 |
+
strokeLinejoin="round"
|
| 182 |
+
strokeWidth={2}
|
| 183 |
+
d="M13 5l7 7-7 7M5 5l7 7-7 7"
|
| 184 |
+
data-oid="g0m_z54"
|
| 185 |
+
/>
|
| 186 |
+
</svg>
|
| 187 |
+
</button>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</>
|
| 191 |
+
)}
|
| 192 |
+
</div>
|
| 193 |
+
);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
export default function TvShowsPage() {
|
| 197 |
+
return (
|
| 198 |
+
<div className="min-h-screen pt-24 pb-16" data-oid="vmkunj1">
|
| 199 |
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8" data-oid="e8nsmst">
|
| 200 |
+
{/* Header Section */}
|
| 201 |
+
<div className="mb-8 space-y-4" data-oid="d36d5sh">
|
| 202 |
+
<h2 className="text-4xl font-bold text-white" data-oid=".2j2af-">
|
| 203 |
+
TV Shows
|
| 204 |
+
</h2>
|
| 205 |
+
<p className="text-gray-400 max-w-3xl" data-oid="d0g1zry">
|
| 206 |
+
Explore our collection of TV series from various genres. From drama to
|
| 207 |
+
comedy, find your next binge-worthy show here.
|
| 208 |
+
</p>
|
| 209 |
+
{/* Genre Filter Chips - You can make these functional later */}
|
| 210 |
+
<div className="flex flex-wrap gap-2" data-oid="t-cl4a6">
|
| 211 |
+
{['All', 'Action', 'Drama', 'Comedy', 'Anime', 'Sci-Fi'].map((genre) => (
|
| 212 |
+
<button
|
| 213 |
+
key={genre}
|
| 214 |
+
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors
|
| 215 |
+
${
|
| 216 |
+
genre === 'All'
|
| 217 |
+
? 'bg-purple-600 text-white'
|
| 218 |
+
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
|
| 219 |
+
}`}
|
| 220 |
+
data-oid="wb9w_xa"
|
| 221 |
+
>
|
| 222 |
+
{genre}
|
| 223 |
+
</button>
|
| 224 |
+
))}
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
{/* Content Section */}
|
| 229 |
+
<div
|
| 230 |
+
className="bg-gray-800/30 rounded-3xl backdrop-blur-sm border border-gray-700/50"
|
| 231 |
+
data-oid="gxvywvg"
|
| 232 |
+
>
|
| 233 |
+
<Suspense
|
| 234 |
+
fallback={
|
| 235 |
+
<div
|
| 236 |
+
className="flex items-center justify-center min-h-[50vh]"
|
| 237 |
+
data-oid="7t-_ota"
|
| 238 |
+
>
|
| 239 |
+
<div
|
| 240 |
+
className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
|
| 241 |
+
data-oid="-4.z3yi"
|
| 242 |
+
></div>
|
| 243 |
+
</div>
|
| 244 |
+
}
|
| 245 |
+
data-oid="cgb8xnf"
|
| 246 |
+
>
|
| 247 |
+
<TvShowsContent data-oid="731rgcy" />
|
| 248 |
+
</Suspense>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
);
|
| 253 |
+
}
|
frontend/components/loading/SplashScreen.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { createContext, useContext, useState, ReactNode } from 'react';
|
| 4 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
+
import { WEB_VERSION } from '@/lib/config';
|
| 6 |
+
|
| 7 |
+
// Create Context
|
| 8 |
+
const LoadingContext = createContext<{
|
| 9 |
+
loading: boolean;
|
| 10 |
+
setLoading: (state: boolean) => void;
|
| 11 |
+
} | null>(null);
|
| 12 |
+
|
| 13 |
+
// Custom Hook
|
| 14 |
+
export function useLoading() {
|
| 15 |
+
const context = useContext(LoadingContext);
|
| 16 |
+
if (!context) {
|
| 17 |
+
throw new Error('useLoading must be used within a LoadingProvider');
|
| 18 |
+
}
|
| 19 |
+
return context;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// Provider Component
|
| 23 |
+
export function LoadingProvider({ children }: { children: ReactNode }) {
|
| 24 |
+
const [loading, setLoading] = useState(true);
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<LoadingContext.Provider value={{ loading, setLoading }} data-oid="ehjch.t">
|
| 28 |
+
<AnimatePresence mode="wait" data-oid="4vxhfjc">
|
| 29 |
+
{loading && (
|
| 30 |
+
<motion.div
|
| 31 |
+
initial={{ opacity: 1 }}
|
| 32 |
+
exit={{ opacity: 0 }}
|
| 33 |
+
transition={{ duration: 0.6, ease: 'easeInOut' }}
|
| 34 |
+
className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900"
|
| 35 |
+
data-oid="rqdqh4a"
|
| 36 |
+
>
|
| 37 |
+
<div className="relative flex flex-col items-center" data-oid="2c2vljb">
|
| 38 |
+
{/* Logo Animation */}
|
| 39 |
+
<motion.div
|
| 40 |
+
initial={{ scale: 0.5, opacity: 0 }}
|
| 41 |
+
animate={{ scale: 1, opacity: 1 }}
|
| 42 |
+
transition={{ duration: 0.6, ease: 'easeOut' }}
|
| 43 |
+
className="mb-8"
|
| 44 |
+
data-oid="wey21fv"
|
| 45 |
+
>
|
| 46 |
+
<div className="w-44 h-44 relative " data-oid="-fvshbu">
|
| 47 |
+
<div
|
| 48 |
+
className="animate-pulse absolute inset-0 bg-gradient-to-r from-purple-600 to-pink-600 rounded-2xl"
|
| 49 |
+
data-oid="4a95xnj"
|
| 50 |
+
/>
|
| 51 |
+
|
| 52 |
+
{/* Inner Background */}
|
| 53 |
+
<div
|
| 54 |
+
className="animate-pulse absolute inset-1 bg-gray-800 rounded-xl"
|
| 55 |
+
data-oid="pjmzzb8"
|
| 56 |
+
/>
|
| 57 |
+
|
| 58 |
+
{/* Text */}
|
| 59 |
+
<div
|
| 60 |
+
className="absolute inset-0 flex-col items-center text-center flex justify-items-center justify-center"
|
| 61 |
+
data-oid="2xa07_8"
|
| 62 |
+
>
|
| 63 |
+
<span
|
| 64 |
+
className="text-4xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent drop-shadow-lg"
|
| 65 |
+
data-oid="fyxc:w6"
|
| 66 |
+
>
|
| 67 |
+
NEXORA
|
| 68 |
+
</span>
|
| 69 |
+
<p className="text-gray-300" data-oid=":x61lfc">
|
| 70 |
+
{WEB_VERSION}
|
| 71 |
+
</p>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</motion.div>
|
| 75 |
+
|
| 76 |
+
{/* Loading Bar */}
|
| 77 |
+
<motion.div
|
| 78 |
+
initial={{ width: 0 }}
|
| 79 |
+
animate={{ width: '150px' }}
|
| 80 |
+
transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
|
| 81 |
+
className="animate-pulse h-1 bg-gradient-to-r m from-purple-500 to-pink-500 rounded-full"
|
| 82 |
+
data-oid="bv8_lkk"
|
| 83 |
+
/>
|
| 84 |
+
|
| 85 |
+
{/* Loading Text */}
|
| 86 |
+
<motion.p
|
| 87 |
+
initial={{ opacity: 0, y: 10 }}
|
| 88 |
+
animate={{ opacity: 1, y: 0 }}
|
| 89 |
+
transition={{ delay: 0.5, ease: 'easeOut' }}
|
| 90 |
+
className="mt-6 text-gray-300 text-sm tracking-wide"
|
| 91 |
+
data-oid="h3btqc-"
|
| 92 |
+
>
|
| 93 |
+
Getting things ready for you...
|
| 94 |
+
</motion.p>
|
| 95 |
+
</div>
|
| 96 |
+
</motion.div>
|
| 97 |
+
)}
|
| 98 |
+
</AnimatePresence>
|
| 99 |
+
{children}
|
| 100 |
+
</LoadingContext.Provider>
|
| 101 |
+
);
|
| 102 |
+
}
|
frontend/components/movie/{MovieCard.js → MovieCard.tsx}
RENAMED
|
@@ -4,58 +4,83 @@ import { useEffect, useState } from 'react';
|
|
| 4 |
import { getMovieCard } from '@/lib/lb';
|
| 5 |
import Link from 'next/link';
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
const [imageLoaded, setImageLoaded] = useState(false);
|
| 10 |
-
|
|
|
|
| 11 |
|
| 12 |
useEffect(() => {
|
| 13 |
async function fetchMovieCard() {
|
| 14 |
try {
|
| 15 |
-
const cardData = await getMovieCard(formattedTitle
|
| 16 |
setCard(cardData);
|
| 17 |
} catch (error) {
|
| 18 |
console.error('Error fetching movie card:', error);
|
| 19 |
}
|
| 20 |
}
|
| 21 |
fetchMovieCard();
|
| 22 |
-
}, []);
|
| 23 |
|
| 24 |
return (
|
| 25 |
-
<Link
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
{/* Skeleton Loader */}
|
| 28 |
{!imageLoaded && (
|
| 29 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 30 |
)}
|
| 31 |
-
|
| 32 |
{/* Image */}
|
| 33 |
{card?.image && (
|
| 34 |
<img
|
| 35 |
src={card.image}
|
| 36 |
alt={card.title}
|
| 37 |
-
className={`w-full h-full object-cover transform group-hover:scale-110 transition-
|
| 38 |
-
|
| 39 |
-
}`}
|
| 40 |
onLoad={() => setImageLoaded(true)}
|
| 41 |
-
data-oid="
|
| 42 |
/>
|
| 43 |
)}
|
| 44 |
|
| 45 |
-
{/* Overlay */}
|
| 46 |
<div
|
| 47 |
-
className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent
|
| 48 |
-
data-oid="
|
| 49 |
>
|
| 50 |
-
<div className="absolute bottom-0 p-4 w-full" data-oid="
|
| 51 |
-
<h3
|
|
|
|
|
|
|
|
|
|
| 52 |
{card?.title || 'Loading...'}
|
| 53 |
</h3>
|
| 54 |
-
<div
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
★ {card?.rating || '0'}
|
| 57 |
</span>
|
| 58 |
-
<span className="text-gray-300" data-oid="
|
| 59 |
• {card?.year || '----'}
|
| 60 |
</span>
|
| 61 |
</div>
|
|
|
|
| 4 |
import { getMovieCard } from '@/lib/lb';
|
| 5 |
import Link from 'next/link';
|
| 6 |
|
| 7 |
+
interface Card {
|
| 8 |
+
image: string;
|
| 9 |
+
title: string;
|
| 10 |
+
rating: string;
|
| 11 |
+
year: string;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const MovieCard = ({ title }: { title: string }) => {
|
| 15 |
+
const [card, setCard] = useState<Card | null>(null);
|
| 16 |
const [imageLoaded, setImageLoaded] = useState(false);
|
| 17 |
+
|
| 18 |
+
const formattedTitle = title.includes('/') ? title.split('/')[1] : title;
|
| 19 |
|
| 20 |
useEffect(() => {
|
| 21 |
async function fetchMovieCard() {
|
| 22 |
try {
|
| 23 |
+
const cardData = await getMovieCard(formattedTitle);
|
| 24 |
setCard(cardData);
|
| 25 |
} catch (error) {
|
| 26 |
console.error('Error fetching movie card:', error);
|
| 27 |
}
|
| 28 |
}
|
| 29 |
fetchMovieCard();
|
| 30 |
+
}, [formattedTitle]);
|
| 31 |
|
| 32 |
return (
|
| 33 |
+
<Link
|
| 34 |
+
href={`/movie/${title}`}
|
| 35 |
+
className="relative block w-[fit-content] h-[fit-content]"
|
| 36 |
+
data-oid="7arzaxn"
|
| 37 |
+
>
|
| 38 |
+
<div
|
| 39 |
+
className="rounded-lg overflow-hidden relative transition-transform duration-300
|
| 40 |
+
w-[150px] sm:w-[150px] md:w-[150px] lg:w-[200px] xl:w-[200px]
|
| 41 |
+
h-[225px] sm:h-[220px] md:h-[250px] lg:h-[310px] xl:h-[325px]"
|
| 42 |
+
data-oid="jn0s4au"
|
| 43 |
+
>
|
| 44 |
{/* Skeleton Loader */}
|
| 45 |
{!imageLoaded && (
|
| 46 |
+
<div
|
| 47 |
+
className="absolute inset-0 bg-gray-700 animate-pulse rounded-lg"
|
| 48 |
+
data-oid="aqy:mkn"
|
| 49 |
+
/>
|
| 50 |
)}
|
| 51 |
+
|
| 52 |
{/* Image */}
|
| 53 |
{card?.image && (
|
| 54 |
<img
|
| 55 |
src={card.image}
|
| 56 |
alt={card.title}
|
| 57 |
+
className={`w-full h-full object-cover transform group-hover:scale-110 transition-all opacity-0 ease-in-out duration-500
|
| 58 |
+
${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
|
|
|
| 59 |
onLoad={() => setImageLoaded(true)}
|
| 60 |
+
data-oid="0i69h4s"
|
| 61 |
/>
|
| 62 |
)}
|
| 63 |
|
| 64 |
+
{/* Overlay (always visible on mobile) */}
|
| 65 |
<div
|
| 66 |
+
className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent"
|
| 67 |
+
data-oid=":aitd01"
|
| 68 |
>
|
| 69 |
+
<div className="absolute bottom-0 p-4 w-full" data-oid="qy.l4pa">
|
| 70 |
+
<h3
|
| 71 |
+
className="text-sm sm:text-base md:text-lg font-semibold text-white"
|
| 72 |
+
data-oid="ceh2z0f"
|
| 73 |
+
>
|
| 74 |
{card?.title || 'Loading...'}
|
| 75 |
</h3>
|
| 76 |
+
<div
|
| 77 |
+
className="flex items-center space-x-2 mt-1 text-xs sm:text-sm"
|
| 78 |
+
data-oid="qnl0hwx"
|
| 79 |
+
>
|
| 80 |
+
<span className="text-yellow-400" data-oid="bet08uk">
|
| 81 |
★ {card?.rating || '0'}
|
| 82 |
</span>
|
| 83 |
+
<span className="text-gray-300" data-oid="ei2vuum">
|
| 84 |
• {card?.year || '----'}
|
| 85 |
</span>
|
| 86 |
</div>
|
frontend/components/movie/MovieCardTemp.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from 'react';
|
| 4 |
-
import { getMovieCard } from '@/lib/lb';
|
| 5 |
-
import Link from 'next/link';
|
| 6 |
-
|
| 7 |
-
export const MovieCardTemp = ({ title }) => {
|
| 8 |
-
const [card, setCard] = useState(null);
|
| 9 |
-
const [imageLoaded, setImageLoaded] = useState(false);
|
| 10 |
-
|
| 11 |
-
useEffect(() => {
|
| 12 |
-
async function fetchMovieCard() {
|
| 13 |
-
try {
|
| 14 |
-
const cardData = await getMovieCard(title);
|
| 15 |
-
setCard(cardData);
|
| 16 |
-
} catch (error) {
|
| 17 |
-
console.error('Error fetching movie card:', error);
|
| 18 |
-
}
|
| 19 |
-
}
|
| 20 |
-
fetchMovieCard();
|
| 21 |
-
}, []);
|
| 22 |
-
|
| 23 |
-
return (
|
| 24 |
-
<Link href={`/movie/${title}`} className="relative group" data-oid="_h1ze:e">
|
| 25 |
-
<div className="aspect-[2/3] rounded-lg overflow-hidden relative" data-oid="tf3burv">
|
| 26 |
-
{/* Skeleton Loader */}
|
| 27 |
-
{!imageLoaded && (
|
| 28 |
-
<div className="absolute inset-0 bg-gray-700 animate-pulse rounded-lg" />
|
| 29 |
-
)}
|
| 30 |
-
|
| 31 |
-
{/* Image */}
|
| 32 |
-
{card?.image && (
|
| 33 |
-
<img
|
| 34 |
-
src={card.image}
|
| 35 |
-
alt={card.title}
|
| 36 |
-
className={`w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-300 opacity-0 transition-opacity ease-in-out duration-500 ${
|
| 37 |
-
imageLoaded ? 'opacity-100' : 'opacity-0'
|
| 38 |
-
}`}
|
| 39 |
-
onLoad={() => setImageLoaded(true)}
|
| 40 |
-
data-oid="aqx14q7"
|
| 41 |
-
/>
|
| 42 |
-
)}
|
| 43 |
-
|
| 44 |
-
{/* Overlay */}
|
| 45 |
-
<div
|
| 46 |
-
className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
| 47 |
-
data-oid="c5z5p9j"
|
| 48 |
-
>
|
| 49 |
-
<div className="absolute bottom-0 p-4 w-full" data-oid="-3a50qd">
|
| 50 |
-
<h3 className="text-lg font-semibold text-white" data-oid="vw8.q_r">
|
| 51 |
-
{card?.title || 'Loading...'}
|
| 52 |
-
</h3>
|
| 53 |
-
<div className="flex items-center space-x-2 mt-1" data-oid="382afz5">
|
| 54 |
-
<span className="text-yellow-400" data-oid="ol91ry.">
|
| 55 |
-
★ {card?.rating || '0'}
|
| 56 |
-
</span>
|
| 57 |
-
<span className="text-gray-300" data-oid="s8mo0:q">
|
| 58 |
-
• {card?.year || '----'}
|
| 59 |
-
</span>
|
| 60 |
-
</div>
|
| 61 |
-
</div>
|
| 62 |
-
</div>
|
| 63 |
-
</div>
|
| 64 |
-
</Link>
|
| 65 |
-
);
|
| 66 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/navigation/DesktopMenu.tsx
CHANGED
|
@@ -2,17 +2,29 @@ import Link from 'next/link';
|
|
| 2 |
|
| 3 |
export const DesktopMenu = () => {
|
| 4 |
return (
|
| 5 |
-
<div className="hidden md:flex space-x-8">
|
| 6 |
-
<Link href="/" className="hover:text-purple-400 transition-colors">
|
| 7 |
Home
|
| 8 |
</Link>
|
| 9 |
-
<Link
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
Movies
|
| 11 |
</Link>
|
| 12 |
-
<Link
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
TV Shows
|
| 14 |
</Link>
|
| 15 |
-
<Link
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
My List
|
| 17 |
</Link>
|
| 18 |
</div>
|
|
|
|
| 2 |
|
| 3 |
export const DesktopMenu = () => {
|
| 4 |
return (
|
| 5 |
+
<div className="hidden md:flex space-x-8" data-oid="x14h_he">
|
| 6 |
+
<Link href="/" className="hover:text-purple-400 transition-colors" data-oid="dboxzpn">
|
| 7 |
Home
|
| 8 |
</Link>
|
| 9 |
+
<Link
|
| 10 |
+
href="/movies"
|
| 11 |
+
className="hover:text-purple-400 transition-colors"
|
| 12 |
+
data-oid="7n83ezu"
|
| 13 |
+
>
|
| 14 |
Movies
|
| 15 |
</Link>
|
| 16 |
+
<Link
|
| 17 |
+
href="/tvshows"
|
| 18 |
+
className="hover:text-purple-400 transition-colors"
|
| 19 |
+
data-oid="3xu:-t3"
|
| 20 |
+
>
|
| 21 |
TV Shows
|
| 22 |
</Link>
|
| 23 |
+
<Link
|
| 24 |
+
href="/mylist"
|
| 25 |
+
className="hover:text-purple-400 transition-colors"
|
| 26 |
+
data-oid="lhuwt77"
|
| 27 |
+
>
|
| 28 |
My List
|
| 29 |
</Link>
|
| 30 |
</div>
|
frontend/components/navigation/MobileMenu.tsx
CHANGED
|
@@ -17,22 +17,45 @@ export const MobileMenu = ({ isOpen }: MobileMenuProps) => {
|
|
| 17 |
leave="transition ease-in duration-200"
|
| 18 |
leaveFrom="opacity-100 transform scale-100"
|
| 19 |
leaveTo="opacity-0 transform scale-95"
|
|
|
|
| 20 |
>
|
| 21 |
-
<div
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
Home
|
| 25 |
</Link>
|
| 26 |
-
<Link
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
Movies
|
| 28 |
</Link>
|
| 29 |
-
<Link
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
TV Shows
|
| 31 |
</Link>
|
| 32 |
-
<Link
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
My List
|
| 34 |
</Link>
|
| 35 |
-
<button
|
|
|
|
|
|
|
|
|
|
| 36 |
Sign In
|
| 37 |
</button>
|
| 38 |
</div>
|
|
|
|
| 17 |
leave="transition ease-in duration-200"
|
| 18 |
leaveFrom="opacity-100 transform scale-100"
|
| 19 |
leaveTo="opacity-0 transform scale-95"
|
| 20 |
+
data-oid="iko9y9c"
|
| 21 |
>
|
| 22 |
+
<div
|
| 23 |
+
className="md:hidden bg-gray-900 border-t border-gray-900 bg-opacity-50 backdrop-blur-lg"
|
| 24 |
+
data-oid="m9tvm_2"
|
| 25 |
+
>
|
| 26 |
+
<div className="px-6 py-4 space-y-4" data-oid="th_ve6p">
|
| 27 |
+
<Link
|
| 28 |
+
href="/"
|
| 29 |
+
className="block hover:text-purple-400 transition-colors"
|
| 30 |
+
data-oid="losouyn"
|
| 31 |
+
>
|
| 32 |
Home
|
| 33 |
</Link>
|
| 34 |
+
<Link
|
| 35 |
+
href="/movies"
|
| 36 |
+
className="block hover:text-purple-400 transition-colors"
|
| 37 |
+
data-oid="dc-_o_e"
|
| 38 |
+
>
|
| 39 |
Movies
|
| 40 |
</Link>
|
| 41 |
+
<Link
|
| 42 |
+
href="/tvshows"
|
| 43 |
+
className="block hover:text-purple-400 transition-colors"
|
| 44 |
+
data-oid="7tr9q01"
|
| 45 |
+
>
|
| 46 |
TV Shows
|
| 47 |
</Link>
|
| 48 |
+
<Link
|
| 49 |
+
href="/mylist"
|
| 50 |
+
className="block hover:text-purple-400 transition-colors"
|
| 51 |
+
data-oid=".-9li1m"
|
| 52 |
+
>
|
| 53 |
My List
|
| 54 |
</Link>
|
| 55 |
+
<button
|
| 56 |
+
className="w-full bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-6 py-2 rounded-full transition-colors"
|
| 57 |
+
data-oid="7se69t7"
|
| 58 |
+
>
|
| 59 |
Sign In
|
| 60 |
</button>
|
| 61 |
</div>
|
frontend/components/navigation/Navbar.tsx
CHANGED
|
@@ -9,19 +9,24 @@ export const Navbar = () => {
|
|
| 9 |
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
| 10 |
|
| 11 |
return (
|
| 12 |
-
<nav
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
<Link
|
| 16 |
href={'/'}
|
| 17 |
className="text-3xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
|
|
|
|
| 18 |
>
|
| 19 |
NEXORA
|
| 20 |
</Link>
|
| 21 |
-
<DesktopMenu />
|
| 22 |
<button
|
| 23 |
className="md:hidden text-white"
|
| 24 |
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
|
|
| 25 |
>
|
| 26 |
{isMobileMenuOpen ? (
|
| 27 |
<svg
|
|
@@ -29,12 +34,14 @@ export const Navbar = () => {
|
|
| 29 |
fill="none"
|
| 30 |
stroke="currentColor"
|
| 31 |
viewBox="0 0 24 24"
|
|
|
|
| 32 |
>
|
| 33 |
<path
|
| 34 |
strokeLinecap="round"
|
| 35 |
strokeLinejoin="round"
|
| 36 |
strokeWidth={2}
|
| 37 |
d="M6 18L18 6M6 6l12 12"
|
|
|
|
| 38 |
/>
|
| 39 |
</svg>
|
| 40 |
) : (
|
|
@@ -43,22 +50,27 @@ export const Navbar = () => {
|
|
| 43 |
fill="none"
|
| 44 |
stroke="currentColor"
|
| 45 |
viewBox="0 0 24 24"
|
|
|
|
| 46 |
>
|
| 47 |
<path
|
| 48 |
strokeLinecap="round"
|
| 49 |
strokeLinejoin="round"
|
| 50 |
strokeWidth={2}
|
| 51 |
d="M4 6h16M4 12h16M4 18h16"
|
|
|
|
| 52 |
/>
|
| 53 |
</svg>
|
| 54 |
)}
|
| 55 |
</button>
|
| 56 |
-
<button
|
|
|
|
|
|
|
|
|
|
| 57 |
Sign In
|
| 58 |
</button>
|
| 59 |
</div>
|
| 60 |
</div>
|
| 61 |
-
<MobileMenu isOpen={isMobileMenuOpen} />
|
| 62 |
</nav>
|
| 63 |
);
|
| 64 |
};
|
|
|
|
| 9 |
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
| 10 |
|
| 11 |
return (
|
| 12 |
+
<nav
|
| 13 |
+
className="fixed w-full z-50 bg-gradient-to-b from-gray-900 to-transparent backdrop-blur-lg"
|
| 14 |
+
data-oid="6mitiyb"
|
| 15 |
+
>
|
| 16 |
+
<div className="container mx-auto px-6 py-4" data-oid="23ltc1.">
|
| 17 |
+
<div className="flex items-center justify-between" data-oid="50-cuwq">
|
| 18 |
<Link
|
| 19 |
href={'/'}
|
| 20 |
className="text-3xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
|
| 21 |
+
data-oid="f-hlmdo"
|
| 22 |
>
|
| 23 |
NEXORA
|
| 24 |
</Link>
|
| 25 |
+
<DesktopMenu data-oid="jpxa-wq" />
|
| 26 |
<button
|
| 27 |
className="md:hidden text-white"
|
| 28 |
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
| 29 |
+
data-oid="dljai9s"
|
| 30 |
>
|
| 31 |
{isMobileMenuOpen ? (
|
| 32 |
<svg
|
|
|
|
| 34 |
fill="none"
|
| 35 |
stroke="currentColor"
|
| 36 |
viewBox="0 0 24 24"
|
| 37 |
+
data-oid="29ld4tb"
|
| 38 |
>
|
| 39 |
<path
|
| 40 |
strokeLinecap="round"
|
| 41 |
strokeLinejoin="round"
|
| 42 |
strokeWidth={2}
|
| 43 |
d="M6 18L18 6M6 6l12 12"
|
| 44 |
+
data-oid="_znir0_"
|
| 45 |
/>
|
| 46 |
</svg>
|
| 47 |
) : (
|
|
|
|
| 50 |
fill="none"
|
| 51 |
stroke="currentColor"
|
| 52 |
viewBox="0 0 24 24"
|
| 53 |
+
data-oid="rnc:m-t"
|
| 54 |
>
|
| 55 |
<path
|
| 56 |
strokeLinecap="round"
|
| 57 |
strokeLinejoin="round"
|
| 58 |
strokeWidth={2}
|
| 59 |
d="M4 6h16M4 12h16M4 18h16"
|
| 60 |
+
data-oid="n3lg_d8"
|
| 61 |
/>
|
| 62 |
</svg>
|
| 63 |
)}
|
| 64 |
</button>
|
| 65 |
+
<button
|
| 66 |
+
className="hidden md:block bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 px-6 py-2 rounded-full"
|
| 67 |
+
data-oid="6wedudj"
|
| 68 |
+
>
|
| 69 |
Sign In
|
| 70 |
</button>
|
| 71 |
</div>
|
| 72 |
</div>
|
| 73 |
+
<MobileMenu isOpen={isMobileMenuOpen} data-oid="lxv.-c_" />
|
| 74 |
</nav>
|
| 75 |
);
|
| 76 |
};
|
frontend/components/sections/CastSection.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
import { useState } from 'react';
|
| 2 |
-
|
| 3 |
-
import { ChevronDown, ChevronUp } from 'lucide-react';
|
| 4 |
-
|
| 5 |
-
export default function CastSection ({ movie }) {
|
| 6 |
-
const [expanded, setExpanded] = useState(false);
|
| 7 |
-
|
| 8 |
-
return (
|
| 9 |
-
<div className="flex-1 bg-gray-800 bg-opacity-50 backdrop-blur-md p-4 rounded-xl">
|
| 10 |
-
<div className="flex justify-between items-center mb-2">
|
| 11 |
-
<h2 className="text-xl font-semibold text-white">Cast</h2>
|
| 12 |
-
<button
|
| 13 |
-
onClick={() => setExpanded(!expanded)}
|
| 14 |
-
className="text-white hover:text-gray-300 flex items-center"
|
| 15 |
-
>
|
| 16 |
-
{expanded ? 'Hide' : 'Show'}{' '}
|
| 17 |
-
{expanded ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
|
| 18 |
-
</button>
|
| 19 |
-
</div>
|
| 20 |
-
|
| 21 |
-
{expanded && (
|
| 22 |
-
<div className="flex flex-wrap gap-4">
|
| 23 |
-
{movie.characters?.map((character) => (
|
| 24 |
-
<div key={character.id} className="flex items-center space-x-3">
|
| 25 |
-
<img
|
| 26 |
-
src={
|
| 27 |
-
character.personImgURL ||
|
| 28 |
-
character.image ||
|
| 29 |
-
`https://eu.ui-avatars.com/api/?name=${character.personName}&size=250`
|
| 30 |
-
}
|
| 31 |
-
alt={character.personName}
|
| 32 |
-
className="w-12 h-12 rounded-full object-cover"
|
| 33 |
-
/>
|
| 34 |
-
<div>
|
| 35 |
-
<a
|
| 36 |
-
target="_blank"
|
| 37 |
-
href={`https://thetvdb.com/people/${character.url}`}
|
| 38 |
-
className="text-white font-semibold hover:underline"
|
| 39 |
-
>
|
| 40 |
-
{character.personName}
|
| 41 |
-
</a>
|
| 42 |
-
{character.name && (
|
| 43 |
-
<p className="text-sm text-gray-400">as {character.name}</p>
|
| 44 |
-
)}
|
| 45 |
-
</div>
|
| 46 |
-
</div>
|
| 47 |
-
))}
|
| 48 |
-
</div>
|
| 49 |
-
)}
|
| 50 |
-
</div>
|
| 51 |
-
);
|
| 52 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/sections/CastSection.tsx
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
import { ChevronDown, ChevronUp } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
interface CastSectionProps {
|
| 6 |
+
movie: {
|
| 7 |
+
characters?: {
|
| 8 |
+
id: string;
|
| 9 |
+
personImgURL?: string;
|
| 10 |
+
image?: string;
|
| 11 |
+
personName: string;
|
| 12 |
+
url?: string;
|
| 13 |
+
name?: string;
|
| 14 |
+
}[];
|
| 15 |
+
};
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export default function CastSection({ movie }: CastSectionProps) {
|
| 19 |
+
const [expanded, setExpanded] = useState(false);
|
| 20 |
+
const visibleCharacters = expanded ? movie.characters : movie.characters?.slice(0, 5);
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div
|
| 24 |
+
className="flex-1 bg-gray-800/60 backdrop-blur-md rounded-2xl p-6 border border-gray-700/50"
|
| 25 |
+
data-oid="ffawpf_"
|
| 26 |
+
>
|
| 27 |
+
<div className="flex justify-between items-center mb-2" data-oid=".muu25o">
|
| 28 |
+
<h2 className="text-xl font-semibold text-white" data-oid="7lng_mo">
|
| 29 |
+
Cast
|
| 30 |
+
</h2>
|
| 31 |
+
{movie.characters && movie.characters.length > 5 && (
|
| 32 |
+
<button
|
| 33 |
+
onClick={() => setExpanded(!expanded)}
|
| 34 |
+
className="text-white hover:text-gray-300 flex items-center"
|
| 35 |
+
data-oid="lh9t.zb"
|
| 36 |
+
>
|
| 37 |
+
{expanded ? 'Hide' : 'Show'}{' '}
|
| 38 |
+
{expanded ? (
|
| 39 |
+
<ChevronUp size={18} data-oid="y8.f7ty" />
|
| 40 |
+
) : (
|
| 41 |
+
<ChevronDown size={18} data-oid=".bovf._" />
|
| 42 |
+
)}
|
| 43 |
+
</button>
|
| 44 |
+
)}
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<div className="flex flex-wrap gap-4" data-oid="e149i:n">
|
| 48 |
+
{visibleCharacters?.map((character) => (
|
| 49 |
+
<div
|
| 50 |
+
key={character.id}
|
| 51 |
+
className="flex items-center space-x-3"
|
| 52 |
+
data-oid="7u:62hb"
|
| 53 |
+
>
|
| 54 |
+
<img
|
| 55 |
+
src={
|
| 56 |
+
character.personImgURL ||
|
| 57 |
+
character.image ||
|
| 58 |
+
`https://eu.ui-avatars.com/api/?name=${character.personName}&size=250`
|
| 59 |
+
}
|
| 60 |
+
alt={character.personName}
|
| 61 |
+
className="w-12 h-12 rounded-full object-cover"
|
| 62 |
+
data-oid="gck7k98"
|
| 63 |
+
/>
|
| 64 |
+
|
| 65 |
+
<div data-oid="vldv6x2">
|
| 66 |
+
<a
|
| 67 |
+
target="_blank"
|
| 68 |
+
href={`https://thetvdb.com/people/${character.url}`}
|
| 69 |
+
className="text-white font-semibold hover:underline"
|
| 70 |
+
data-oid="o1rg6pp"
|
| 71 |
+
>
|
| 72 |
+
{character.personName}
|
| 73 |
+
</a>
|
| 74 |
+
{character.name && (
|
| 75 |
+
<p className="text-sm text-gray-400" data-oid="q9kg8ee">
|
| 76 |
+
as {character.name}
|
| 77 |
+
</p>
|
| 78 |
+
)}
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
))}
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
);
|
| 85 |
+
}
|
frontend/components/sections/NewContentSection.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from 'react';
|
| 4 |
-
import { getNewContents } from '@/lib/lb';
|
| 5 |
-
import { MovieCardTemp } from '@/components/movie/MovieCardTemp';
|
| 6 |
-
import { MovieCard } from '../movie/MovieCard';
|
| 7 |
-
import {TvShowCardTemp} from '@components/tvshow/TvShowCardTemp';
|
| 8 |
-
|
| 9 |
-
export default function NewContentSection() {
|
| 10 |
-
const [movies, setMovies] = useState([]);
|
| 11 |
-
const [tvshows, setTvShows] = useState([]);
|
| 12 |
-
useEffect(() => {
|
| 13 |
-
async function fetchMovies() {
|
| 14 |
-
try {
|
| 15 |
-
const { movies, tvshows } = await getNewContents(10); // Correctly destructure as an object
|
| 16 |
-
setMovies(movies);
|
| 17 |
-
setTvShows(tvshows);
|
| 18 |
-
} catch (error) {
|
| 19 |
-
console.error('Error fetching slides:', error);
|
| 20 |
-
}
|
| 21 |
-
}
|
| 22 |
-
fetchMovies();
|
| 23 |
-
}, []);
|
| 24 |
-
|
| 25 |
-
return (
|
| 26 |
-
<>
|
| 27 |
-
<section className="py-16" data-oid="yvz2_cl">
|
| 28 |
-
<div className="container mx-auto px-6" data-oid="_xku7wx">
|
| 29 |
-
<h2
|
| 30 |
-
className="text-2xl font-bold mb-8 bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
|
| 31 |
-
data-oid="62.dxoz"
|
| 32 |
-
>
|
| 33 |
-
Discover Movies
|
| 34 |
-
</h2>
|
| 35 |
-
<div
|
| 36 |
-
className="grid grid-cols-2 md:grid-cols-6 gap-4 md:gap-6"
|
| 37 |
-
data-oid="owphddg"
|
| 38 |
-
>
|
| 39 |
-
{movies.map((movie, index) => (
|
| 40 |
-
<MovieCardTemp title={movie.title} key={index} />
|
| 41 |
-
//<MovieCard title={movie?.title} key={index}/>
|
| 42 |
-
))}
|
| 43 |
-
</div>
|
| 44 |
-
</div>
|
| 45 |
-
</section>
|
| 46 |
-
<section className="py-16" data-oid="yvz2_cl">
|
| 47 |
-
<div className="container mx-auto px-6" data-oid="_xku7wx">
|
| 48 |
-
<h2
|
| 49 |
-
className="text-2xl font-bold mb-8 bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
|
| 50 |
-
data-oid="62.dxoz"
|
| 51 |
-
>
|
| 52 |
-
Discover Tv Shows
|
| 53 |
-
</h2>
|
| 54 |
-
<div
|
| 55 |
-
className="grid grid-cols-2 md:grid-cols-6 gap-4 md:gap-6"
|
| 56 |
-
data-oid="owphddg"
|
| 57 |
-
>
|
| 58 |
-
{tvshows.map((tvshow, index) => (
|
| 59 |
-
<TvShowCardTemp title={tvshow.title} key={index} />
|
| 60 |
-
//<MovieCard title={tvshow?.title} key={index}/>
|
| 61 |
-
))}
|
| 62 |
-
</div>
|
| 63 |
-
</div>
|
| 64 |
-
</section>
|
| 65 |
-
</>
|
| 66 |
-
);
|
| 67 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/sections/NewContentSection.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react';
|
| 4 |
+
import { getNewContents } from '@/lib/lb';
|
| 5 |
+
import { MovieCard } from '../movie/MovieCard';
|
| 6 |
+
import { TvShowCard } from '@/components/tvshow/TvShowCard';
|
| 7 |
+
import ScrollSection from './ScrollSection';
|
| 8 |
+
|
| 9 |
+
export default function NewContentSection() {
|
| 10 |
+
const [movies, setMovies] = useState<any[]>([]);
|
| 11 |
+
const [tvshows, setTvShows] = useState<any[]>([]);
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
async function fetchMovies() {
|
| 14 |
+
try {
|
| 15 |
+
const { movies, tvshows } = await getNewContents(20); // Correctly destructure as an object
|
| 16 |
+
setMovies(movies as any[]);
|
| 17 |
+
setTvShows(tvshows as any[]);
|
| 18 |
+
} catch (error) {
|
| 19 |
+
console.error('Error fetching slides:', error);
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
fetchMovies();
|
| 23 |
+
}, []);
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<div className="space-y-2" data-oid="ioedrx4">
|
| 27 |
+
<ScrollSection title="Discover Movies" data-oid="iezk49o">
|
| 28 |
+
{movies.map((movie, index) => (
|
| 29 |
+
<MovieCard key={index} title={movie.title} data-oid="6ml:-c3" />
|
| 30 |
+
))}
|
| 31 |
+
</ScrollSection>
|
| 32 |
+
|
| 33 |
+
<ScrollSection title="Discover TV Shows" data-oid="vldcu:9">
|
| 34 |
+
{tvshows.map((tvshow, index) => (
|
| 35 |
+
<TvShowCard
|
| 36 |
+
key={index}
|
| 37 |
+
title={tvshow.title}
|
| 38 |
+
episodesCount={null}
|
| 39 |
+
data-oid="6w316m-"
|
| 40 |
+
/>
|
| 41 |
+
))}
|
| 42 |
+
</ScrollSection>
|
| 43 |
+
</div>
|
| 44 |
+
);
|
| 45 |
+
}
|
frontend/components/sections/ScrollSection.tsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useRef, useState } from 'react';
|
| 4 |
+
|
| 5 |
+
interface ScrollSectionProps {
|
| 6 |
+
title: string;
|
| 7 |
+
children: React.ReactNode;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function ScrollSection({ title, children }: ScrollSectionProps) {
|
| 11 |
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
| 12 |
+
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
| 13 |
+
const [showRightArrow, setShowRightArrow] = useState(true);
|
| 14 |
+
|
| 15 |
+
const handleScroll = () => {
|
| 16 |
+
if (scrollContainerRef.current) {
|
| 17 |
+
const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
|
| 18 |
+
setShowLeftArrow(scrollLeft > 0);
|
| 19 |
+
setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 10);
|
| 20 |
+
}
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
const scroll = (direction: 'left' | 'right') => {
|
| 24 |
+
if (scrollContainerRef.current) {
|
| 25 |
+
const scrollAmount = scrollContainerRef.current.clientWidth * 0.8;
|
| 26 |
+
scrollContainerRef.current.scrollBy({
|
| 27 |
+
left: direction === 'left' ? -scrollAmount : scrollAmount,
|
| 28 |
+
behavior: 'smooth',
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<section className="py-4 relative group" data-oid="l.ccaf8">
|
| 35 |
+
<div className="container mx-auto px-6" data-oid="3t:dihz">
|
| 36 |
+
<h2
|
| 37 |
+
className="text-2xl font-bold mb-6 bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
|
| 38 |
+
data-oid="ekzzpou"
|
| 39 |
+
>
|
| 40 |
+
{title}
|
| 41 |
+
</h2>
|
| 42 |
+
|
| 43 |
+
{/* Scroll Arrows */}
|
| 44 |
+
{showLeftArrow && (
|
| 45 |
+
<button
|
| 46 |
+
onClick={() => scroll('left')}
|
| 47 |
+
className="absolute left-0 top-1/2 z-10 transform -translate-y-1/2 bg-gradient-to-r from-gray-900 to-transparent pl-2 pr-8 py-16"
|
| 48 |
+
data-oid="cd65:45"
|
| 49 |
+
>
|
| 50 |
+
<svg
|
| 51 |
+
className="w-8 h-8 text-white hover:text-purple-400 transition-colors"
|
| 52 |
+
fill="none"
|
| 53 |
+
stroke="currentColor"
|
| 54 |
+
viewBox="0 0 24 24"
|
| 55 |
+
data-oid="-dl2br6"
|
| 56 |
+
>
|
| 57 |
+
<path
|
| 58 |
+
strokeLinecap="round"
|
| 59 |
+
strokeLinejoin="round"
|
| 60 |
+
strokeWidth={2}
|
| 61 |
+
d="M15 19l-7-7 7-7"
|
| 62 |
+
data-oid="n7uuo9l"
|
| 63 |
+
/>
|
| 64 |
+
</svg>
|
| 65 |
+
</button>
|
| 66 |
+
)}
|
| 67 |
+
|
| 68 |
+
{showRightArrow && (
|
| 69 |
+
<button
|
| 70 |
+
onClick={() => scroll('right')}
|
| 71 |
+
className="absolute right-0 top-1/2 z-10 transform -translate-y-1/2 bg-gradient-to-l from-gray-900 to-transparent pr-2 pl-8 py-16"
|
| 72 |
+
data-oid="s.ixi1d"
|
| 73 |
+
>
|
| 74 |
+
<svg
|
| 75 |
+
className="w-8 h-8 text-white hover:text-purple-400 transition-colors"
|
| 76 |
+
fill="none"
|
| 77 |
+
stroke="currentColor"
|
| 78 |
+
viewBox="0 0 24 24"
|
| 79 |
+
data-oid="sklx_ai"
|
| 80 |
+
>
|
| 81 |
+
<path
|
| 82 |
+
strokeLinecap="round"
|
| 83 |
+
strokeLinejoin="round"
|
| 84 |
+
strokeWidth={2}
|
| 85 |
+
d="M9 5l7 7-7 7"
|
| 86 |
+
data-oid="5izu.kw"
|
| 87 |
+
/>
|
| 88 |
+
</svg>
|
| 89 |
+
</button>
|
| 90 |
+
)}
|
| 91 |
+
|
| 92 |
+
{/* Scrollable Content */}
|
| 93 |
+
<div
|
| 94 |
+
ref={scrollContainerRef}
|
| 95 |
+
onScroll={handleScroll}
|
| 96 |
+
className="flex landscape:gap-5 gap-4 overflow-x-auto scrollbar-hide scroll-smooth"
|
| 97 |
+
style={{
|
| 98 |
+
maskImage:
|
| 99 |
+
'linear-gradient(to right, transparent, black 2%, black 98%, transparent)',
|
| 100 |
+
WebkitMaskImage:
|
| 101 |
+
'linear-gradient(to right, transparent, black 2%, black 98%, transparent)',
|
| 102 |
+
}}
|
| 103 |
+
data-oid="9xwvre:"
|
| 104 |
+
>
|
| 105 |
+
{children}
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</section>
|
| 109 |
+
);
|
| 110 |
+
}
|
frontend/components/sections/TrendingSection.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from 'react';
|
| 4 |
-
import { getAllMovies } from '@/lib/lb';
|
| 5 |
-
import { MovieCard } from '@/components/movie/MovieCard';
|
| 6 |
-
|
| 7 |
-
export const TrendingSection = () => {
|
| 8 |
-
const [movies, setMovies] = useState([]);
|
| 9 |
-
useEffect(() => {
|
| 10 |
-
async function fetchMovies() {
|
| 11 |
-
try {
|
| 12 |
-
const data = await getAllMovies();
|
| 13 |
-
setMovies(data);
|
| 14 |
-
} catch (error) {
|
| 15 |
-
console.error('Error fetching slides:', error);
|
| 16 |
-
}
|
| 17 |
-
}
|
| 18 |
-
fetchMovies();
|
| 19 |
-
}, []);
|
| 20 |
-
|
| 21 |
-
return (
|
| 22 |
-
<section className="py-16" data-oid="yvz2_cl">
|
| 23 |
-
<div className="container mx-auto px-6" data-oid="_xku7wx">
|
| 24 |
-
<h2
|
| 25 |
-
className="text-2xl font-bold mb-8 bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent"
|
| 26 |
-
data-oid="62.dxoz"
|
| 27 |
-
>
|
| 28 |
-
Trending Now
|
| 29 |
-
</h2>
|
| 30 |
-
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 md:gap-6" data-oid="owphddg">
|
| 31 |
-
{movies.map((title, index) => (
|
| 32 |
-
<MovieCard title={title} key={index} />
|
| 33 |
-
))}
|
| 34 |
-
</div>
|
| 35 |
-
</div>
|
| 36 |
-
</section>
|
| 37 |
-
);
|
| 38 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/tvshow/{TvShowCard.js → TvShowCard.tsx}
RENAMED
|
@@ -4,58 +4,96 @@ import { useEffect, useState } from 'react';
|
|
| 4 |
import { getTvShowCard } from '@/lib/lb';
|
| 5 |
import Link from 'next/link';
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
const [imageLoaded, setImageLoaded] = useState(false);
|
| 10 |
-
const formattedTitle = title.split('/');
|
| 11 |
|
| 12 |
useEffect(() => {
|
| 13 |
-
async function
|
| 14 |
try {
|
| 15 |
-
const cardData = await getTvShowCard(
|
| 16 |
setCard(cardData);
|
| 17 |
} catch (error) {
|
| 18 |
-
console.error('Error fetching
|
| 19 |
}
|
| 20 |
}
|
| 21 |
-
|
| 22 |
-
}, []);
|
| 23 |
|
| 24 |
return (
|
| 25 |
-
<Link
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
{/* Skeleton Loader */}
|
| 28 |
{!imageLoaded && (
|
| 29 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 30 |
)}
|
| 31 |
-
|
| 32 |
{/* Image */}
|
| 33 |
{card?.image && (
|
| 34 |
<img
|
| 35 |
src={card.image}
|
| 36 |
alt={card.title}
|
| 37 |
-
className={`w-full h-full object-cover transform group-hover:scale-110 transition-
|
| 38 |
-
|
| 39 |
-
}`}
|
| 40 |
onLoad={() => setImageLoaded(true)}
|
| 41 |
-
data-oid="
|
| 42 |
/>
|
| 43 |
)}
|
| 44 |
|
| 45 |
-
{/* Overlay */}
|
| 46 |
<div
|
| 47 |
-
className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent
|
| 48 |
-
data-oid="
|
| 49 |
>
|
| 50 |
-
<div className="absolute bottom-0 p-4 w-full" data-oid="
|
| 51 |
-
<h3
|
|
|
|
|
|
|
|
|
|
| 52 |
{card?.title || 'Loading...'}
|
| 53 |
</h3>
|
| 54 |
-
<div
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
★ {card?.rating || '0'}
|
| 57 |
</span>
|
| 58 |
-
<span className="text-gray-300" data-oid="
|
| 59 |
• {card?.year || '----'}
|
| 60 |
</span>
|
| 61 |
</div>
|
|
|
|
| 4 |
import { getTvShowCard } from '@/lib/lb';
|
| 5 |
import Link from 'next/link';
|
| 6 |
|
| 7 |
+
interface Card {
|
| 8 |
+
image: string;
|
| 9 |
+
title: string;
|
| 10 |
+
rating: string;
|
| 11 |
+
year: string;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const TvShowCard = ({
|
| 15 |
+
title,
|
| 16 |
+
episodesCount = null,
|
| 17 |
+
}: {
|
| 18 |
+
title: string;
|
| 19 |
+
episodesCount: number | null;
|
| 20 |
+
}) => {
|
| 21 |
+
const [card, setCard] = useState<Card | null>(null);
|
| 22 |
const [imageLoaded, setImageLoaded] = useState(false);
|
|
|
|
| 23 |
|
| 24 |
useEffect(() => {
|
| 25 |
+
async function fetchTvShowCard() {
|
| 26 |
try {
|
| 27 |
+
const cardData = await getTvShowCard(title);
|
| 28 |
setCard(cardData);
|
| 29 |
} catch (error) {
|
| 30 |
+
console.error('Error fetching TV show card:', error);
|
| 31 |
}
|
| 32 |
}
|
| 33 |
+
fetchTvShowCard();
|
| 34 |
+
}, [title]); // Added title as dependency to prevent stale state issues
|
| 35 |
|
| 36 |
return (
|
| 37 |
+
<Link
|
| 38 |
+
href={`/tvshow/${title}`}
|
| 39 |
+
className="relative block w-[fit-content]"
|
| 40 |
+
data-oid="ov688_a"
|
| 41 |
+
>
|
| 42 |
+
<div
|
| 43 |
+
className="rounded-lg overflow-hidden relative transition-transform duration-300
|
| 44 |
+
w-[130px] sm:w-[150px] md:w-[150px] lg:w-[180px] xl:w-[200px]
|
| 45 |
+
"
|
| 46 |
+
data-oid="cvzn.gm"
|
| 47 |
+
>
|
| 48 |
+
{/* Episodes Count Bubble */}
|
| 49 |
+
{episodesCount !== null && (
|
| 50 |
+
<div
|
| 51 |
+
className="absolute top-2 right-1 z-10 bg-violet-700 text-white text-xs font-bold px-2 py-1 rounded-full shadow-lg"
|
| 52 |
+
data-oid="q32w..7"
|
| 53 |
+
>
|
| 54 |
+
{episodesCount} Ep{episodesCount > 1 ? 's' : ''}
|
| 55 |
+
</div>
|
| 56 |
+
)}
|
| 57 |
{/* Skeleton Loader */}
|
| 58 |
{!imageLoaded && (
|
| 59 |
+
<div
|
| 60 |
+
className="absolute inset-0 bg-gray-700 animate-pulse rounded-lg"
|
| 61 |
+
data-oid="z4z:q2x"
|
| 62 |
+
/>
|
| 63 |
)}
|
| 64 |
+
|
| 65 |
{/* Image */}
|
| 66 |
{card?.image && (
|
| 67 |
<img
|
| 68 |
src={card.image}
|
| 69 |
alt={card.title}
|
| 70 |
+
className={`w-full h-full object-cover transform group-hover:scale-110 transition-all opacity-0 ease-in-out duration-500
|
| 71 |
+
${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
|
|
|
| 72 |
onLoad={() => setImageLoaded(true)}
|
| 73 |
+
data-oid="4e47cca"
|
| 74 |
/>
|
| 75 |
)}
|
| 76 |
|
| 77 |
+
{/* Overlay (always visible on mobile) */}
|
| 78 |
<div
|
| 79 |
+
className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent"
|
| 80 |
+
data-oid="u:mamg5"
|
| 81 |
>
|
| 82 |
+
<div className="absolute bottom-0 p-4 w-full" data-oid="pnq95qw">
|
| 83 |
+
<h3
|
| 84 |
+
className="text-sm sm:text-base md:text-lg font-semibold text-white"
|
| 85 |
+
data-oid="dh5njzq"
|
| 86 |
+
>
|
| 87 |
{card?.title || 'Loading...'}
|
| 88 |
</h3>
|
| 89 |
+
<div
|
| 90 |
+
className="flex items-center space-x-2 mt-1 text-xs sm:text-sm"
|
| 91 |
+
data-oid="yq8hk20"
|
| 92 |
+
>
|
| 93 |
+
<span className="text-yellow-400" data-oid=".0_zsul">
|
| 94 |
★ {card?.rating || '0'}
|
| 95 |
</span>
|
| 96 |
+
<span className="text-gray-300" data-oid="pz11um6">
|
| 97 |
• {card?.year || '----'}
|
| 98 |
</span>
|
| 99 |
</div>
|
frontend/components/tvshow/TvShowCardTemp.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from 'react';
|
| 4 |
-
import { getTvShowCard } from '@/lib/lb';
|
| 5 |
-
import Link from 'next/link';
|
| 6 |
-
|
| 7 |
-
export const TvShowCardTemp = ({ title, episodesCount = null }) => {
|
| 8 |
-
const [card, setCard] = useState(null);
|
| 9 |
-
const [imageLoaded, setImageLoaded] = useState(false);
|
| 10 |
-
|
| 11 |
-
useEffect(() => {
|
| 12 |
-
async function fetchTvShowCard() {
|
| 13 |
-
try {
|
| 14 |
-
const cardData = await getTvShowCard(title);
|
| 15 |
-
setCard(cardData);
|
| 16 |
-
} catch (error) {
|
| 17 |
-
console.error('Error fetching TV show card:', error);
|
| 18 |
-
}
|
| 19 |
-
}
|
| 20 |
-
fetchTvShowCard();
|
| 21 |
-
}, [title]); // Added title as dependency to prevent stale state issues
|
| 22 |
-
|
| 23 |
-
return (
|
| 24 |
-
<Link href={`/tvshow/${title}`} className="relative group">
|
| 25 |
-
<div className="aspect-[2/3] rounded-lg overflow-hidden relative">
|
| 26 |
-
{/* Episodes Count Bubble */}
|
| 27 |
-
{episodesCount !== null && (
|
| 28 |
-
<div className="absolute top-2 right-1 z-10 bg-violet-700 text-white text-xs font-bold px-2 py-1 rounded-full shadow-lg">
|
| 29 |
-
{episodesCount} Ep{episodesCount > 1 ? 's' : ''}
|
| 30 |
-
</div>
|
| 31 |
-
)}
|
| 32 |
-
|
| 33 |
-
{/* Skeleton Loader */}
|
| 34 |
-
{!imageLoaded && (
|
| 35 |
-
<div className="absolute inset-0 bg-gray-700 animate-pulse rounded-lg" />
|
| 36 |
-
)}
|
| 37 |
-
|
| 38 |
-
{/* Image */}
|
| 39 |
-
{card?.image && (
|
| 40 |
-
<img
|
| 41 |
-
src={card.image}
|
| 42 |
-
alt={card?.title || 'TV Show Poster'}
|
| 43 |
-
className={`w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-300 ${
|
| 44 |
-
imageLoaded ? 'opacity-100' : 'opacity-0'
|
| 45 |
-
}`}
|
| 46 |
-
onLoad={() => setImageLoaded(true)}
|
| 47 |
-
/>
|
| 48 |
-
)}
|
| 49 |
-
|
| 50 |
-
{/* Overlay */}
|
| 51 |
-
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
| 52 |
-
<div className="absolute bottom-0 p-4 w-full">
|
| 53 |
-
<h3 className="text-lg font-semibold text-white">{card?.title || 'Loading...'}</h3>
|
| 54 |
-
<div className="flex items-center space-x-2 mt-1">
|
| 55 |
-
<span className="text-yellow-400">★ {card?.rating || '0'}</span>
|
| 56 |
-
<span className="text-gray-300">• {card?.year || '----'}</span>
|
| 57 |
-
</div>
|
| 58 |
-
</div>
|
| 59 |
-
</div>
|
| 60 |
-
</div>
|
| 61 |
-
</Link>
|
| 62 |
-
);
|
| 63 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/lib/LoadbalancerAPI.js
CHANGED
|
@@ -1,141 +1,158 @@
|
|
| 1 |
class LoadBalancerAPI {
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
async getMovieStore() {
|
| 36 |
-
const response = await this._get('/api/get/movie/store');
|
| 37 |
-
if (response && Object.keys(response).length > 0) {
|
| 38 |
-
this.filmCache = response;
|
| 39 |
-
}
|
| 40 |
-
return this.filmCache || {};
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
async getMovieMetadataByTitle(title) {
|
| 44 |
-
return await this._get(`/api/get/movie/metadata/${encodeURIComponent(title)}`);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
async getMovieCard(title) {
|
| 48 |
-
return await this._get(`/api/get/movie/card/${encodeURIComponent(title)}`);
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
async getSeriesMetadataByTitle(title) {
|
| 52 |
-
return await this._get(`/api/get/series/metadata/${encodeURIComponent(title)}`);
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
async getSeriesCard(title) {
|
| 56 |
-
return await this._get(`/api/get/series/card/${encodeURIComponent(title)}`);
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
async getSeasonMetadataBySeriesId(series_id, season) {
|
| 60 |
-
return await this._get(`/api/get/series/metadata/${series_id}/${season}`);
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
async getAllMovies() {
|
| 64 |
-
return await this._get('/api/get/movie/all');
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
async getAllSeriesShows() {
|
| 68 |
-
return await this._get('/api/get/series/all');
|
| 69 |
}
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
}
|
| 82 |
-
return await this._get(`/api/get/genre?${params.toString()}`);
|
| 83 |
}
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
| 98 |
}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
}
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
async _handleResponse(response) {
|
| 122 |
-
if (!response.ok) {
|
| 123 |
-
const errorDetails = await response.text();
|
| 124 |
-
throw new Error(`HTTP Error ${response.status}: ${errorDetails}`);
|
| 125 |
-
}
|
| 126 |
-
try {
|
| 127 |
-
return await response.json();
|
| 128 |
-
} catch (error) {
|
| 129 |
-
console.error('Error parsing JSON response:', error);
|
| 130 |
-
throw error;
|
| 131 |
-
}
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
// Utility method to clear caches if needed
|
| 135 |
-
clearCache() {
|
| 136 |
-
this.filmCache = null;
|
| 137 |
-
this.tvCache = null;
|
| 138 |
}
|
| 139 |
}
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
class LoadBalancerAPI {
|
| 2 |
+
constructor(baseURL) {
|
| 3 |
+
this.baseURL = baseURL;
|
| 4 |
+
this.cache = {
|
| 5 |
+
filmStore: null,
|
| 6 |
+
tvStore: null,
|
| 7 |
+
allMovies: null,
|
| 8 |
+
allSeries: null,
|
| 9 |
+
movieMetadata: new Map(),
|
| 10 |
+
seriesMetadata: new Map(),
|
| 11 |
+
};
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
async getInstances() {
|
| 15 |
+
return await this._get('/api/get/instances');
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
async getInstancesHealth() {
|
| 19 |
+
return await this._get('/api/get/instances/health');
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
async getMovieByTitle(title) {
|
| 23 |
+
return await this._get(`/api/get/movie/${encodeURIComponent(title)}`);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
async getSeriesEpisode(title, season, episode) {
|
| 27 |
+
return await this._get(`/api/get/series/${encodeURIComponent(title)}/${season}/${episode}`);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
async getSeriesStore() {
|
| 31 |
+
if (!this.cache.tvStore) {
|
| 32 |
+
this.cache.tvStore = await this._get('/api/get/series/store');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
+
return this.cache.tvStore || {};
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async getMovieStore() {
|
| 38 |
+
if (!this.cache.filmStore) {
|
| 39 |
+
this.cache.filmStore = await this._get('/api/get/movie/store');
|
| 40 |
}
|
| 41 |
+
return this.cache.filmStore || {};
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
async getMovieMetadataByTitle(title) {
|
| 45 |
+
if (!this.cache.movieMetadata.has(title)) {
|
| 46 |
+
const metadata = await this._get(`/api/get/movie/metadata/${encodeURIComponent(title)}`);
|
| 47 |
+
this.cache.movieMetadata.set(title, metadata);
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
+
return this.cache.movieMetadata.get(title);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
async getMovieCard(title) {
|
| 53 |
+
return await this._get(`/api/get/movie/card/${encodeURIComponent(title)}`);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
async getSeriesMetadataByTitle(title) {
|
| 57 |
+
if (!this.cache.seriesMetadata.has(title)) {
|
| 58 |
+
const metadata = await this._get(`/api/get/series/metadata/${encodeURIComponent(title)}`);
|
| 59 |
+
this.cache.seriesMetadata.set(title, metadata);
|
| 60 |
}
|
| 61 |
+
return this.cache.seriesMetadata.get(title);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
async getSeriesCard(title) {
|
| 65 |
+
return await this._get(`/api/get/series/card/${encodeURIComponent(title)}`);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
async getSeasonMetadataBySeriesId(series_id, season) {
|
| 69 |
+
return await this._get(`/api/get/series/metadata/${series_id}/${season}`);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
async getAllMovies() {
|
| 73 |
+
if (!this.cache.allMovies) {
|
| 74 |
+
this.cache.allMovies = await this._get('/api/get/movie/all');
|
| 75 |
}
|
| 76 |
+
return this.cache.allMovies;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
async getAllSeriesShows() {
|
| 80 |
+
if (!this.cache.allSeries) {
|
| 81 |
+
this.cache.allSeries = await this._get('/api/get/series/all');
|
| 82 |
}
|
| 83 |
+
return this.cache.allSeries;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
async getRecent(limit = 5) {
|
| 87 |
+
return await this._get(`/api/get/recent?limit=${limit}`);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
async getGenreItems(genres, mediaType = null, limit = 5) {
|
| 91 |
+
const params = new URLSearchParams();
|
| 92 |
+
genres.forEach(genre => params.append('genre', genre));
|
| 93 |
+
params.append('limit', limit);
|
| 94 |
+
if (mediaType) {
|
| 95 |
+
params.append('media_type', mediaType);
|
| 96 |
+
}
|
| 97 |
+
return await this._get(`/api/get/genre?${params.toString()}`);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
async getDownloadProgress(url) {
|
| 101 |
+
return await this._getNoBase(url);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
async _get(endpoint) {
|
| 105 |
+
return await this._request(`${this.baseURL}${endpoint}`, { method: 'GET' });
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
async _getNoBase(url) {
|
| 109 |
+
return await this._request(url, { method: 'GET' });
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
async _post(endpoint, body) {
|
| 113 |
+
return await this._request(`${this.baseURL}${endpoint}`, {
|
| 114 |
+
method: 'POST',
|
| 115 |
+
body: JSON.stringify(body)
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
async _request(url, options) {
|
| 120 |
+
try {
|
| 121 |
+
const response = await fetch(url, {
|
| 122 |
+
headers: { 'Content-Type': 'application/json' },
|
| 123 |
+
...options,
|
| 124 |
});
|
| 125 |
+
console.log(`API Request: ${url} with options: ${JSON.stringify(options)}`);
|
| 126 |
+
return await this._handleResponse(response);
|
| 127 |
+
} catch (error) {
|
| 128 |
+
console.error(`Request error for ${url}:`, error);
|
| 129 |
+
throw error;
|
| 130 |
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
async _handleResponse(response) {
|
| 134 |
+
if (!response.ok) {
|
| 135 |
+
const errorDetails = await response.text();
|
| 136 |
+
throw new Error(`HTTP Error ${response.status}: ${errorDetails}`);
|
| 137 |
+
}
|
| 138 |
+
try {
|
| 139 |
+
return await response.json();
|
| 140 |
+
} catch (error) {
|
| 141 |
+
console.error('Error parsing JSON response:', error);
|
| 142 |
+
throw error;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
}
|
| 145 |
+
|
| 146 |
+
clearCache() {
|
| 147 |
+
this.cache = {
|
| 148 |
+
filmStore: null,
|
| 149 |
+
tvStore: null,
|
| 150 |
+
allMovies: null,
|
| 151 |
+
allSeries: null,
|
| 152 |
+
movieMetadata: new Map(),
|
| 153 |
+
seriesMetadata: new Map(),
|
| 154 |
+
};
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
export { LoadBalancerAPI };
|
frontend/lib/config.js
DELETED
|
File without changes
|
frontend/lib/config.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export const WEB_VERSION = 'v0.1.2 beta';
|
frontend/package-lock.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
| 13 |
"@radix-ui/react-slot": "^1.1.0",
|
| 14 |
"class-variance-authority": "^0.7.0",
|
| 15 |
"clsx": "^2.1.1",
|
|
|
|
| 16 |
"lucide-react": "^0.438.0",
|
| 17 |
"next": "14.2.23",
|
| 18 |
"next-nprogress-bar": "^2.4.4",
|
|
@@ -2586,6 +2587,33 @@
|
|
| 2586 |
"url": "https://github.com/sponsors/isaacs"
|
| 2587 |
}
|
| 2588 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2589 |
"node_modules/fs.realpath": {
|
| 2590 |
"version": "1.0.0",
|
| 2591 |
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
|
@@ -3660,6 +3688,21 @@
|
|
| 3660 |
"node": ">=16 || 14 >=14.17"
|
| 3661 |
}
|
| 3662 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3663 |
"node_modules/ms": {
|
| 3664 |
"version": "2.1.2",
|
| 3665 |
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
|
|
| 13 |
"@radix-ui/react-slot": "^1.1.0",
|
| 14 |
"class-variance-authority": "^0.7.0",
|
| 15 |
"clsx": "^2.1.1",
|
| 16 |
+
"framer-motion": "^12.4.2",
|
| 17 |
"lucide-react": "^0.438.0",
|
| 18 |
"next": "14.2.23",
|
| 19 |
"next-nprogress-bar": "^2.4.4",
|
|
|
|
| 2587 |
"url": "https://github.com/sponsors/isaacs"
|
| 2588 |
}
|
| 2589 |
},
|
| 2590 |
+
"node_modules/framer-motion": {
|
| 2591 |
+
"version": "12.4.2",
|
| 2592 |
+
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.2.tgz",
|
| 2593 |
+
"integrity": "sha512-pW307cQKjDqEuO1flEoIFf6TkuJRfKr+c7qsHAJhDo4368N/5U8/7WU8J+xhd9+gjmOgJfgp+46evxRRFM39dA==",
|
| 2594 |
+
"license": "MIT",
|
| 2595 |
+
"dependencies": {
|
| 2596 |
+
"motion-dom": "^12.0.0",
|
| 2597 |
+
"motion-utils": "^12.0.0",
|
| 2598 |
+
"tslib": "^2.4.0"
|
| 2599 |
+
},
|
| 2600 |
+
"peerDependencies": {
|
| 2601 |
+
"@emotion/is-prop-valid": "*",
|
| 2602 |
+
"react": "^18.0.0 || ^19.0.0",
|
| 2603 |
+
"react-dom": "^18.0.0 || ^19.0.0"
|
| 2604 |
+
},
|
| 2605 |
+
"peerDependenciesMeta": {
|
| 2606 |
+
"@emotion/is-prop-valid": {
|
| 2607 |
+
"optional": true
|
| 2608 |
+
},
|
| 2609 |
+
"react": {
|
| 2610 |
+
"optional": true
|
| 2611 |
+
},
|
| 2612 |
+
"react-dom": {
|
| 2613 |
+
"optional": true
|
| 2614 |
+
}
|
| 2615 |
+
}
|
| 2616 |
+
},
|
| 2617 |
"node_modules/fs.realpath": {
|
| 2618 |
"version": "1.0.0",
|
| 2619 |
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
|
|
|
| 3688 |
"node": ">=16 || 14 >=14.17"
|
| 3689 |
}
|
| 3690 |
},
|
| 3691 |
+
"node_modules/motion-dom": {
|
| 3692 |
+
"version": "12.0.0",
|
| 3693 |
+
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz",
|
| 3694 |
+
"integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==",
|
| 3695 |
+
"license": "MIT",
|
| 3696 |
+
"dependencies": {
|
| 3697 |
+
"motion-utils": "^12.0.0"
|
| 3698 |
+
}
|
| 3699 |
+
},
|
| 3700 |
+
"node_modules/motion-utils": {
|
| 3701 |
+
"version": "12.0.0",
|
| 3702 |
+
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
|
| 3703 |
+
"integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
|
| 3704 |
+
"license": "MIT"
|
| 3705 |
+
},
|
| 3706 |
"node_modules/ms": {
|
| 3707 |
"version": "2.1.2",
|
| 3708 |
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
frontend/package.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
| 15 |
"@radix-ui/react-slot": "^1.1.0",
|
| 16 |
"class-variance-authority": "^0.7.0",
|
| 17 |
"clsx": "^2.1.1",
|
|
|
|
| 18 |
"lucide-react": "^0.438.0",
|
| 19 |
"next": "14.2.23",
|
| 20 |
"next-nprogress-bar": "^2.4.4",
|
|
|
|
| 15 |
"@radix-ui/react-slot": "^1.1.0",
|
| 16 |
"class-variance-authority": "^0.7.0",
|
| 17 |
"clsx": "^2.1.1",
|
| 18 |
+
"framer-motion": "^12.4.2",
|
| 19 |
"lucide-react": "^0.438.0",
|
| 20 |
"next": "14.2.23",
|
| 21 |
"next-nprogress-bar": "^2.4.4",
|
frontend/yarn.lock
CHANGED
|
@@ -1331,6 +1331,15 @@ foreground-child@^3.1.0:
|
|
| 1331 |
cross-spawn "^7.0.0"
|
| 1332 |
signal-exit "^4.0.1"
|
| 1333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
fs.realpath@^1.0.0:
|
| 1335 |
version "1.0.0"
|
| 1336 |
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
|
@@ -1950,6 +1959,18 @@ minimist@^1.2.0, minimist@^1.2.6:
|
|
| 1950 |
resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz"
|
| 1951 |
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
| 1952 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1953 |
ms@^2.1.1, ms@2.1.2:
|
| 1954 |
version "2.1.2"
|
| 1955 |
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
|
|
@@ -2282,7 +2303,7 @@ queue-microtask@^1.2.2:
|
|
| 2282 |
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
| 2283 |
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
| 2284 |
|
| 2285 |
-
"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", react-dom@^18, "react-dom@^18 || ^19 || ^19.0.0-rc", react-dom@^18.2.0, react-dom@>=16.8.0:
|
| 2286 |
version "18.3.1"
|
| 2287 |
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
|
| 2288 |
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
|
@@ -2295,7 +2316,7 @@ react-is@^16.13.1:
|
|
| 2295 |
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
| 2296 |
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
| 2297 |
|
| 2298 |
-
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", react@^18, "react@^18 || ^19 || ^19.0.0-rc", react@^18.2.0, react@^18.3.1, "react@>= 16 || ^19.0.0-rc", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16.8.0:
|
| 2299 |
version "18.3.1"
|
| 2300 |
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
|
| 2301 |
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
|
|
|
| 1331 |
cross-spawn "^7.0.0"
|
| 1332 |
signal-exit "^4.0.1"
|
| 1333 |
|
| 1334 |
+
framer-motion@^12.4.2:
|
| 1335 |
+
version "12.4.2"
|
| 1336 |
+
resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.2.tgz"
|
| 1337 |
+
integrity sha512-pW307cQKjDqEuO1flEoIFf6TkuJRfKr+c7qsHAJhDo4368N/5U8/7WU8J+xhd9+gjmOgJfgp+46evxRRFM39dA==
|
| 1338 |
+
dependencies:
|
| 1339 |
+
motion-dom "^12.0.0"
|
| 1340 |
+
motion-utils "^12.0.0"
|
| 1341 |
+
tslib "^2.4.0"
|
| 1342 |
+
|
| 1343 |
fs.realpath@^1.0.0:
|
| 1344 |
version "1.0.0"
|
| 1345 |
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
|
|
|
| 1959 |
resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz"
|
| 1960 |
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
| 1961 |
|
| 1962 |
+
motion-dom@^12.0.0:
|
| 1963 |
+
version "12.0.0"
|
| 1964 |
+
resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz"
|
| 1965 |
+
integrity sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==
|
| 1966 |
+
dependencies:
|
| 1967 |
+
motion-utils "^12.0.0"
|
| 1968 |
+
|
| 1969 |
+
motion-utils@^12.0.0:
|
| 1970 |
+
version "12.0.0"
|
| 1971 |
+
resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz"
|
| 1972 |
+
integrity sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==
|
| 1973 |
+
|
| 1974 |
ms@^2.1.1, ms@2.1.2:
|
| 1975 |
version "2.1.2"
|
| 1976 |
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
|
|
|
|
| 2303 |
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
| 2304 |
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
| 2305 |
|
| 2306 |
+
"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", react-dom@^18, "react-dom@^18 || ^19 || ^19.0.0-rc", "react-dom@^18.0.0 || ^19.0.0", react-dom@^18.2.0, react-dom@>=16.8.0:
|
| 2307 |
version "18.3.1"
|
| 2308 |
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
|
| 2309 |
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
|
|
|
| 2316 |
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
| 2317 |
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
| 2318 |
|
| 2319 |
+
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", react@^18, "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0", react@^18.2.0, react@^18.3.1, "react@>= 16 || ^19.0.0-rc", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16.8.0:
|
| 2320 |
version "18.3.1"
|
| 2321 |
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
|
| 2322 |
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|