ChandimaPrabath commited on
Commit
6cf508e
·
1 Parent(s): 2583ec0

v0.1.2 beta

Browse files
Files changed (36) hide show
  1. frontend/app/globals.css +19 -12
  2. frontend/app/layout.tsx +16 -12
  3. frontend/app/movie/[title]/{LoadingSkeleton.js → LoadingSkeleton.tsx} +19 -19
  4. frontend/app/movie/[title]/page.js +0 -171
  5. frontend/app/movie/[title]/page.tsx +261 -0
  6. frontend/app/movies/{page.js → page.tsx} +40 -18
  7. frontend/app/mylist/page.tsx +9 -0
  8. frontend/app/not-found.js +0 -37
  9. frontend/app/not-found.tsx +122 -0
  10. frontend/app/{page.js → page.tsx} +68 -22
  11. frontend/app/tvshow/[title]/{LoadingSkeleton.js → LoadingSkeleton.tsx} +19 -19
  12. frontend/app/tvshow/[title]/page.js +0 -161
  13. frontend/app/tvshow/[title]/page.tsx +436 -0
  14. frontend/app/tvshows/page.js +0 -91
  15. frontend/app/tvshows/page.tsx +253 -0
  16. frontend/app/tvshows/page.tsx.tmp +253 -0
  17. frontend/components/loading/SplashScreen.tsx +102 -0
  18. frontend/components/movie/{MovieCard.js → MovieCard.tsx} +46 -21
  19. frontend/components/movie/MovieCardTemp.js +0 -66
  20. frontend/components/navigation/DesktopMenu.tsx +17 -5
  21. frontend/components/navigation/MobileMenu.tsx +30 -7
  22. frontend/components/navigation/Navbar.tsx +18 -6
  23. frontend/components/sections/CastSection.js +0 -52
  24. frontend/components/sections/CastSection.tsx +85 -0
  25. frontend/components/sections/NewContentSection.js +0 -67
  26. frontend/components/sections/NewContentSection.tsx +45 -0
  27. frontend/components/sections/ScrollSection.tsx +110 -0
  28. frontend/components/sections/TrendingSection.js +0 -38
  29. frontend/components/tvshow/{TvShowCard.js → TvShowCard.tsx} +62 -24
  30. frontend/components/tvshow/TvShowCardTemp.js +0 -63
  31. frontend/lib/LoadbalancerAPI.js +147 -130
  32. frontend/lib/config.js +0 -0
  33. frontend/lib/config.tsx +1 -0
  34. frontend/package-lock.json +43 -0
  35. frontend/package.json +1 -0
  36. 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
- .pan-animation {
93
- animation: kenburns-pan-landscape 5s ease-in-out infinite;
94
- background-repeat: no-repeat;
95
- background-position: center;
96
- }
97
  }
98
 
99
  @media screen and (orientation: portrait) {
100
- .pan-animation {
101
- animation: kenburns-pan-portrait 5s ease-in-out infinite;
102
- background-repeat: no-repeat;
103
- background-position: top;
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
- <div className="min-h-screen bg-gray-900 text-white">
14
- <Navbar />
15
- {children}
16
- <ProgressBar
17
- height="5px"
18
- color="#8927e9"
19
- options={{ showSpinner: false }}
20
- shallowRouting
21
- />
22
- </div>
 
 
 
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="bxlim:7">
4
  {/* Hero Section Skeleton */}
5
- <div className="relative w-full h-[60vh] bg-gray-800" data-oid="15s-275">
6
  <div
7
  className="absolute inset-0 flex items-center justify-center"
8
- data-oid="-7z9u4-"
9
  >
10
- <div className="w-full max-w-7xl px-6 space-y-4" data-oid="qjq7:4p">
11
  <div
12
  className="h-12 bg-gray-700 rounded-lg w-3/4 max-w-2xl"
13
- data-oid=".crlged"
14
  ></div>
15
  <div
16
  className="h-6 bg-gray-700 rounded-lg w-1/4 max-w-xs"
17
- data-oid="m2f8k02"
18
  ></div>
19
- <div className="flex gap-4" data-oid="99ud:0g">
20
  <div
21
  className="h-12 bg-gray-700 rounded-3xl w-32"
22
- data-oid="djnshw1"
23
  ></div>
24
  <div
25
  className="h-12 bg-gray-700 rounded-3xl w-32"
26
- data-oid="3cmt5:k"
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="g-n.rse">
35
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8" data-oid="q1ayxmh">
36
- <div className="md:col-span-2 space-y-4" data-oid="lq:eldw">
37
- <div className="h-8 bg-gray-800 rounded-lg w-1/2" data-oid="ufp7lyx"></div>
38
  <div
39
  className="h-32 bg-gray-800 rounded-lg w-full"
40
- data-oid="tgmoxx:"
41
  ></div>
42
- <div className="h-8 bg-gray-800 rounded-lg w-1/3" data-oid="-t-uslw"></div>
43
  <div
44
  className="h-24 bg-gray-800 rounded-lg w-full"
45
- data-oid="wkup7cw"
46
  ></div>
47
  </div>
48
- <div className="space-y-4" data-oid="760f:2.">
49
- <div className="h-8 bg-gray-800 rounded-lg w-full" data-oid="zk.9vl4"></div>
50
  <div
51
  className="h-40 bg-gray-800 rounded-lg w-full"
52
- data-oid="p5io6zx"
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 [loading, setLoading] = useState(true);
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">Loading...</p>
 
 
44
  ) : (
45
  <>
46
- <div className="grid grid-cols-2 md:grid-cols-8 gap-4 md:gap-6">
 
 
 
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">Page {currentPage} of {totalPages}</span>
59
- <button
60
- onClick={() => handlePageChange(currentPage + 1)}
 
 
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 className="text-2xl font-bold mb-8 bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
 
 
 
77
  Discover Movies
78
  </h2>
79
- <Suspense fallback={<p className="text-center">Loading...</p>}>
80
- <MoviesContent />
 
 
 
 
 
 
 
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 [loading, setLoading] = useState(true);
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-screen">
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 className="w-full h-full flex flex-col justify-end bg-gray-800 rounded-lg animate-pulse">
50
- <div className="w-2/4 h-5 landscape:w-1/4 ml-10 landscape:ml-20 mb-4 bg-gray-700 rounded-lg"></div>
51
- <div className="w-3/4 h-14 landscape:w-2/4 ml-10 landscape:ml-20 mb-4 bg-gray-700 rounded-lg"></div>
52
- <div className="w-3/4 h-1/4 landscape:w-2/4 ml-10 landscape:ml-20 mb-20 bg-gray-700 rounded-lg"></div>
 
 
 
 
 
 
 
 
 
 
 
 
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 className="absolute bottom-0 left-0 right-0 z-20 p-10 md:p-20 bg-gradient-to-t from-gray-900">
75
- <div className="container mx-auto">
76
- <span className="text-purple-400 text-base font-semibold">
 
 
 
 
 
 
 
 
 
 
 
 
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:underline"
 
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:underline"
 
93
  >
94
  {slide.genre}
95
  </Link>
96
  )}
97
  </span>
98
 
99
- <h1 className="text-4xl md:text-5xl font-bold mt-2 mb-4">
100
- {slide.title}
101
- </h1>
102
- <p className="text-gray-300 text-lg max-w-xl overflow-hidden line-clamp-5">
103
  {slide.description}
104
  </p>
105
- <div className="flex justify-start landscape:gap-4 mt-8">
 
 
 
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="bxlim:7">
4
  {/* Hero Section Skeleton */}
5
- <div className="relative w-full h-[60vh] bg-gray-800" data-oid="15s-275">
6
  <div
7
  className="absolute inset-0 flex items-center justify-center"
8
- data-oid="-7z9u4-"
9
  >
10
- <div className="w-full max-w-7xl px-6 space-y-4" data-oid="qjq7:4p">
11
  <div
12
  className="h-12 bg-gray-700 rounded-lg w-3/4 max-w-2xl"
13
- data-oid=".crlged"
14
  ></div>
15
  <div
16
  className="h-6 bg-gray-700 rounded-lg w-1/4 max-w-xs"
17
- data-oid="m2f8k02"
18
  ></div>
19
- <div className="flex gap-4" data-oid="99ud:0g">
20
  <div
21
  className="h-12 bg-gray-700 rounded-3xl w-32"
22
- data-oid="djnshw1"
23
  ></div>
24
  <div
25
  className="h-12 bg-gray-700 rounded-3xl w-32"
26
- data-oid="3cmt5:k"
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="g-n.rse">
35
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8" data-oid="q1ayxmh">
36
- <div className="md:col-span-2 space-y-4" data-oid="lq:eldw">
37
- <div className="h-8 bg-gray-800 rounded-lg w-1/2" data-oid="ufp7lyx"></div>
38
  <div
39
  className="h-32 bg-gray-800 rounded-lg w-full"
40
- data-oid="tgmoxx:"
41
  ></div>
42
- <div className="h-8 bg-gray-800 rounded-lg w-1/3" data-oid="-t-uslw"></div>
43
  <div
44
  className="h-24 bg-gray-800 rounded-lg w-full"
45
- data-oid="wkup7cw"
46
  ></div>
47
  </div>
48
- <div className="space-y-4" data-oid="760f:2.">
49
- <div className="h-8 bg-gray-800 rounded-lg w-full" data-oid="zk.9vl4"></div>
50
  <div
51
  className="h-40 bg-gray-800 rounded-lg w-full"
52
- data-oid="p5io6zx"
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
- export const MovieCard = ({ title }) => {
8
- const [card, setCard] = useState(null);
 
 
 
 
 
 
 
9
  const [imageLoaded, setImageLoaded] = useState(false);
10
- const formattedTitle = title.split('/');
 
11
 
12
  useEffect(() => {
13
  async function fetchMovieCard() {
14
  try {
15
- const cardData = await getMovieCard(formattedTitle[1]);
16
  setCard(cardData);
17
  } catch (error) {
18
  console.error('Error fetching movie card:', error);
19
  }
20
  }
21
  fetchMovieCard();
22
- }, []);
23
 
24
  return (
25
- <Link href={`/movie/${formattedTitle[1]}`} className="relative group" data-oid="_h1ze:e">
26
- <div className="aspect-[2/3] rounded-lg overflow-hidden relative" data-oid="tf3burv">
 
 
 
 
 
 
 
 
 
27
  {/* Skeleton Loader */}
28
  {!imageLoaded && (
29
- <div className="absolute inset-0 bg-gray-700 animate-pulse rounded-lg" />
 
 
 
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-transform duration-300 opacity-0 transition-opacity ease-in-out duration-500 ${
38
- imageLoaded ? 'opacity-100' : 'opacity-0'
39
- }`}
40
  onLoad={() => setImageLoaded(true)}
41
- data-oid="aqx14q7"
42
  />
43
  )}
44
 
45
- {/* Overlay */}
46
  <div
47
- className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
48
- data-oid="c5z5p9j"
49
  >
50
- <div className="absolute bottom-0 p-4 w-full" data-oid="-3a50qd">
51
- <h3 className="text-lg font-semibold text-white" data-oid="vw8.q_r">
 
 
 
52
  {card?.title || 'Loading...'}
53
  </h3>
54
- <div className="flex items-center space-x-2 mt-1" data-oid="382afz5">
55
- <span className="text-yellow-400" data-oid="ol91ry.">
 
 
 
56
  ★ {card?.rating || '0'}
57
  </span>
58
- <span className="text-gray-300" data-oid="s8mo0:q">
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 href="/movies" className="hover:text-purple-400 transition-colors">
 
 
 
 
10
  Movies
11
  </Link>
12
- <Link href="/tvshows" className="hover:text-purple-400 transition-colors">
 
 
 
 
13
  TV Shows
14
  </Link>
15
- <Link href="#" className="hover:text-purple-400 transition-colors">
 
 
 
 
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 className="md:hidden bg-gray-900 border-t border-gray-900 bg-opacity-50 backdrop-blur-lg">
22
- <div className="px-6 py-4 space-y-4">
23
- <Link href="/" className="block hover:text-purple-400 transition-colors">
 
 
 
 
 
 
 
24
  Home
25
  </Link>
26
- <Link href="/movies" className="block hover:text-purple-400 transition-colors">
 
 
 
 
27
  Movies
28
  </Link>
29
- <Link href="/tvshows" className="block hover:text-purple-400 transition-colors">
 
 
 
 
30
  TV Shows
31
  </Link>
32
- <Link href="#" className="block hover:text-purple-400 transition-colors">
 
 
 
 
33
  My List
34
  </Link>
35
- <button 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">
 
 
 
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 className="fixed w-full z-50 bg-gradient-to-b from-gray-900 to-transparent backdrop-blur-lg">
13
- <div className="container mx-auto px-6 py-4">
14
- <div className="flex items-center justify-between">
 
 
 
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 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">
 
 
 
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
- export const TvShowCard = ({ title }) => {
8
- const [card, setCard] = useState(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  const [imageLoaded, setImageLoaded] = useState(false);
10
- const formattedTitle = title.split('/');
11
 
12
  useEffect(() => {
13
- async function fetchMovieCard() {
14
  try {
15
- const cardData = await getTvShowCard(formattedTitle[1]);
16
  setCard(cardData);
17
  } catch (error) {
18
- console.error('Error fetching movie card:', error);
19
  }
20
  }
21
- fetchMovieCard();
22
- }, []);
23
 
24
  return (
25
- <Link href={`/tvshow/${formattedTitle[1]}`} className="relative group" data-oid="_h1ze:e">
26
- <div className="aspect-[2/3] rounded-lg overflow-hidden relative" data-oid="tf3burv">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  {/* Skeleton Loader */}
28
  {!imageLoaded && (
29
- <div className="absolute inset-0 bg-gray-700 animate-pulse rounded-lg" />
 
 
 
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-transform duration-300 opacity-0 transition-opacity ease-in-out duration-500 ${
38
- imageLoaded ? 'opacity-100' : 'opacity-0'
39
- }`}
40
  onLoad={() => setImageLoaded(true)}
41
- data-oid="aqx14q7"
42
  />
43
  )}
44
 
45
- {/* Overlay */}
46
  <div
47
- className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
48
- data-oid="c5z5p9j"
49
  >
50
- <div className="absolute bottom-0 p-4 w-full" data-oid="-3a50qd">
51
- <h3 className="text-lg font-semibold text-white" data-oid="vw8.q_r">
 
 
 
52
  {card?.title || 'Loading...'}
53
  </h3>
54
- <div className="flex items-center space-x-2 mt-1" data-oid="382afz5">
55
- <span className="text-yellow-400" data-oid="ol91ry.">
 
 
 
56
  ★ {card?.rating || '0'}
57
  </span>
58
- <span className="text-gray-300" data-oid="s8mo0:q">
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
- constructor(baseURL) {
3
- this.baseURL = baseURL;
4
- this.filmCache = null; // Cache for movie store
5
- this.tvCache = null; // Cache for series store
6
- }
7
-
8
- // -----------------------------
9
- // Public API Methods
10
- // -----------------------------
11
- async getInstances() {
12
- return await this._get('/api/get/instances');
13
- }
14
-
15
- async getInstancesHealth() {
16
- return await this._get('/api/get/instances/health');
17
- }
18
-
19
- async getMovieByTitle(title) {
20
- return await this._get(`/api/get/movie/${encodeURIComponent(title)}`);
21
- }
22
-
23
- async getSeriesEpisode(title, season, episode) {
24
- return await this._get(`/api/get/series/${encodeURIComponent(title)}/${season}/${episode}`);
25
- }
26
-
27
- async getSeriesStore() {
28
- const response = await this._get('/api/get/series/store');
29
- if (response && Object.keys(response).length > 0) {
30
- this.tvCache = response;
31
- }
32
- return this.tvCache || {};
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
- async getRecent(limit = 5) {
72
- return await this._get(`/api/get/recent?limit=${limit}`);
 
 
 
73
  }
74
-
75
- async getGenreItems(genres, mediaType = null, limit = 5) {
76
- const params = new URLSearchParams();
77
- genres.forEach(genre => params.append('genre', genre));
78
- params.append('limit', limit);
79
- if (mediaType) {
80
- params.append('media_type', mediaType);
81
- }
82
- return await this._get(`/api/get/genre?${params.toString()}`);
83
  }
84
-
85
- async getDownloadProgress(url) {
86
- return await this._getNoBase(url);
 
 
 
 
 
 
 
 
87
  }
88
-
89
- // -----------------------------
90
- // Helper Methods
91
- // -----------------------------
92
- async _get(endpoint) {
93
- return await this._request(`${this.baseURL}${endpoint}`, { method: 'GET' });
 
 
 
 
 
 
 
 
94
  }
95
-
96
- async _getNoBase(url) {
97
- return await this._request(url, { method: 'GET' });
 
 
 
98
  }
99
-
100
- async _post(endpoint, body) {
101
- return await this._request(`${this.baseURL}${endpoint}`, {
102
- method: 'POST',
103
- body: JSON.stringify(body)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  });
 
 
 
 
 
105
  }
106
-
107
- async _request(url, options) {
108
- try {
109
- const response = await fetch(url, {
110
- headers: { 'Content-Type': 'application/json' },
111
- ...options,
112
- });
113
- console.log(`API Request: ${url} with options: ${JSON.stringify(options)}`);
114
- return await this._handleResponse(response);
115
- } catch (error) {
116
- console.error(`Request error for ${url}:`, error);
117
- throw error;
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
- export { LoadBalancerAPI };
 
 
 
 
 
 
 
 
 
 
 
 
 
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==