ChandimaPrabath commited on
Commit
9f7f3ba
·
1 Parent(s): eaa9a6e

v0.1.4 beta

Browse files
frontend/TODO.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project
2
+
3
+ Project Description
4
+
5
+ <em>[TODO.md spec & Kanban Board](https://bit.ly/3fCwKfM)</em>
6
+
7
+ ### Todo
8
+
9
+
10
+ ### In Progress
11
+
12
+ ### Done ✓
13
+
14
+
frontend/app/movies/page.tsx CHANGED
@@ -5,103 +5,246 @@ 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<any[]>([]);
14
- const { loading, setLoading } = useLoading();
15
- const itemsPerPage = 16;
16
- const [totalPages, setTotalPages] = useState(1);
 
17
 
18
- useEffect(() => {
19
- async function fetchMovies() {
20
- setLoading(true);
21
- try {
22
- const data: any[] = (await getAllMovies()) || [];
23
- const totalMovies = data?.length;
24
- setTotalPages(Math.ceil(totalMovies / itemsPerPage));
25
- const startIndex = (currentPage - 1) * itemsPerPage;
26
- const paginatedMovies = data?.slice(startIndex, startIndex + itemsPerPage);
27
- setMovies(paginatedMovies);
28
- } catch (error) {
29
- console.error('Error fetching movies:', error);
30
- } finally {
31
- setLoading(false);
32
- }
33
  }
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="3ozsx6.">
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="zbazz1e"
52
- >
53
- {movies.map((title, index) => (
54
- <MovieCard title={title} key={`${currentPage}page${index}`} data-oid="mc:2.uh" />
55
- ))}
56
- </div>
57
- <div className="flex justify-center mt-8 gap-4" data-oid="noq2z2.">
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="d:8u196"
63
- >
64
- Previous
65
- </button>
66
- <span className="px-4 py-2 text-white" data-oid="v7is21m">
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="b.bxaco"
74
- >
75
- Next
76
- </button>
77
- </div>
78
- </>
79
- )}
80
- </>
81
- );
82
- }
83
 
84
- export default function MoviesPage() {
85
- return (
86
- <div className="page py-16" data-oid="_s_yu53">
87
- <div className="container mx-auto px-6" data-oid="ty5ykip">
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="_y6oahy"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  >
92
- Discover Movies
93
- </h2>
94
- <Suspense
95
- fallback={
96
- <p className="text-center" data-oid="pmn28rb">
97
- Loading...
98
- </p>
99
- }
100
- data-oid="boa955w"
 
 
 
 
 
 
 
 
 
 
 
 
101
  >
102
- <MoviesContent data-oid="j159kgb" />
103
- </Suspense>
 
 
 
 
 
 
 
104
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </div>
106
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
 
5
  import { getAllMovies } from '@/lib/lb';
6
  import { MovieCard } from '@/components/movie/MovieCard';
7
  import { useLoading } from '@/components/loading/SplashScreen';
8
+ import GenresFilter from '@/components/shared/GenresFilter';
9
 
10
  function MoviesContent() {
11
+ const searchParams = useSearchParams();
12
+ const router = useRouter();
13
+ const currentPage = parseInt(searchParams.get('page') || '1', 10);
14
+ const [movies, setMovies] = useState<any[]>([]);
15
+ const [allMovies, setAllMovies] = useState<any[]>([]);
16
+ const { loading, setLoading } = useLoading();
17
+ const itemsPerPage = 20;
18
+ const [totalPages, setTotalPages] = useState(1);
19
 
20
+ // Fetch all movies once on mount
21
+ useEffect(() => {
22
+ async function fetchMovies() {
23
+ setLoading(true);
24
+ try {
25
+ const data = await getAllMovies();
26
+ if (data.length) {
27
+ // Sort the data to ensure a consistent order (e.g., alphabetically by title)
28
+ const sortedData = [...data].sort((a, b) =>
29
+ a.title.localeCompare(b.title)
30
+ );
31
+ setAllMovies(sortedData);
32
+ setTotalPages(Math.ceil(sortedData.length / itemsPerPage));
 
 
33
  }
34
+ } catch (error) {
35
+ console.error('Error fetching TV shows:', error);
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ }
40
+ fetchMovies();
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, []);
43
 
44
+ // Update the displayed movies when the page or full list changes
45
+ useEffect(() => {
46
+ if (allMovies.length) {
47
+ const startIndex = (currentPage - 1) * itemsPerPage;
48
+ setMovies(allMovies.slice(startIndex, startIndex + itemsPerPage));
49
+ }
50
+ }, [allMovies, currentPage]);
51
 
52
+ const handlePageChange = (newPage: number) => {
53
+ if (newPage >= 1 && newPage <= totalPages) {
54
+ router.push(`/movies?page=${newPage}`);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div className="portrait:p-2 p-4" data-oid="z4j5t8d">
60
+ {loading ? (
61
+ <div
62
+ className="flex items-center justify-center min-h-[60vh]"
63
+ data-oid="jdvygx2"
64
+ >
65
+ <div
66
+ className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
67
+ data-oid="ex20n6p"
68
+ ></div>
69
+ </div>
70
+ ) : (
71
  <>
72
+ {/* Default Movies Grid */}
73
+ <div
74
+ key={currentPage}
75
+ className="flex flex-wrap justify-center items-center portrait:gap-2 gap-10"
76
+ data-oid="ke2_x_x"
77
+ >
78
+ {movies.map((movie, index) => (
79
+ <div
80
+ key={`${movie.title}-${index}`}
81
+ className="transform transition-transform duration-300 hover:scale-105 w-[fit-content]"
82
+ data-oid="x:.ceyx"
83
+ >
84
+ <MovieCard
85
+ title={movie.title}
86
+ data-oid="p.v-:hm"
87
+ />
88
+ </div>
89
+ ))}
90
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ {/* Pagination */}
93
+ <div
94
+ className="mt-12 flex flex-row items-center justify-center gap-2"
95
+ data-oid="6fx8gif"
96
+ >
97
+ <div className="flex items-center gap-2" data-oid="qq.402e">
98
+ <button
99
+ onClick={() => handlePageChange(1)}
100
+ disabled={currentPage <= 1}
101
+ 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"
102
+ data-oid="ve0w5mr"
103
+ >
104
+ <svg
105
+ className="w-5 h-5"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ viewBox="0 0 24 24"
109
+ data-oid="ya2aw1i"
110
+ >
111
+ <path
112
+ strokeLinecap="round"
113
+ strokeLinejoin="round"
114
+ strokeWidth={2}
115
+ d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
116
+ data-oid="xc60cie"
117
+ />
118
+ </svg>
119
+ </button>
120
+ <button
121
+ onClick={() => handlePageChange(currentPage - 1)}
122
+ disabled={currentPage <= 1}
123
+ 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"
124
+ data-oid="z91p4_y"
125
+ >
126
+ <svg
127
+ className="w-5 h-5"
128
+ fill="none"
129
+ stroke="currentColor"
130
+ viewBox="0 0 24 24"
131
+ data-oid="bk.mb1x"
132
+ >
133
+ <path
134
+ strokeLinecap="round"
135
+ strokeLinejoin="round"
136
+ strokeWidth={2}
137
+ d="M15 19l-7-7 7-7"
138
+ data-oid="7m5rce:"
139
+ />
140
+ </svg>
141
+ </button>
142
+ </div>
143
+
144
+ <div className="flex items-center gap-2" data-oid="hqeas1v">
145
+ <span
146
+ className="px-4 py-2 rounded-lg bg-gray-800 text-white font-medium"
147
+ data-oid="k8ve27b"
148
+ >
149
+ Page {currentPage} of {totalPages}
150
+ </span>
151
+ </div>
152
+
153
+ <div className="flex items-center gap-2" data-oid="yq:jhwz">
154
+ <button
155
+ onClick={() => handlePageChange(currentPage + 1)}
156
+ disabled={currentPage >= totalPages}
157
+ 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"
158
+ data-oid="bi1rjxh"
159
+ >
160
+ <svg
161
+ className="w-5 h-5"
162
+ fill="none"
163
+ stroke="currentColor"
164
+ viewBox="0 0 24 24"
165
+ data-oid=".1uvef3"
166
  >
167
+ <path
168
+ strokeLinecap="round"
169
+ strokeLinejoin="round"
170
+ strokeWidth={2}
171
+ d="M9 5l7 7-7 7"
172
+ data-oid="i91blmp"
173
+ />
174
+ </svg>
175
+ </button>
176
+ <button
177
+ onClick={() => handlePageChange(totalPages)}
178
+ disabled={currentPage >= totalPages}
179
+ 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"
180
+ data-oid="g9nu10p"
181
+ >
182
+ <svg
183
+ className="w-5 h-5"
184
+ fill="none"
185
+ stroke="currentColor"
186
+ viewBox="0 0 24 24"
187
+ data-oid=".g:j8vb"
188
  >
189
+ <path
190
+ strokeLinecap="round"
191
+ strokeLinejoin="round"
192
+ strokeWidth={2}
193
+ d="M13 5l7 7-7 7M5 5l7 7-7 7"
194
+ data-oid="yh5__i."
195
+ />
196
+ </svg>
197
+ </button>
198
  </div>
199
+ </div>
200
+ </>
201
+ )}
202
+ </div>
203
+ );
204
+ }
205
+
206
+ export default function MoviesPage() {
207
+ const [filterActive, setFilterActive] = useState(false);
208
+
209
+ return (
210
+ <div className="page min-h-screen pt-20 pb-4" data-oid="0-uxuak">
211
+ <div className="container mx-auto portrait:px-3 px-4" data-oid="7tvn4t1">
212
+ {/* Header Section */}
213
+ <div className="mb-8 space-y-2" data-oid="8d_p425">
214
+ <h2 className="text-4xl font-bold text-white" data-oid="y4vnny4">
215
+ Movies
216
+ </h2>
217
+ <p className="text-gray-400 max-w-3xl" data-oid="othjg4g">
218
+ Explore our collection of movies from various genres. From action to romance, find your next movie night selection here.
219
+ </p>
220
+ <GenresFilter mediaType="movie" onFilterChange={setFilterActive} />
221
  </div>
222
+
223
+ {/* Content Section */}
224
+ {!filterActive && (
225
+ <div
226
+ className="bg-gray-800/30 rounded-3xl backdrop-blur-sm border border-gray-700/50"
227
+ data-oid="zv:kb.h"
228
+ >
229
+ <Suspense
230
+ fallback={
231
+ <div
232
+ className="flex items-center justify-center min-h-[50vh]"
233
+ data-oid="uo0i4dx"
234
+ >
235
+ <div
236
+ className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
237
+ data-oid="7nesstc"
238
+ ></div>
239
+ </div>
240
+ }
241
+ data-oid="85rngk_"
242
+ >
243
+ <MoviesContent data-oid="z5e4dpy" />
244
+ </Suspense>
245
+ </div>
246
+ )}
247
+ </div>
248
+ </div>
249
+ );
250
  }
frontend/app/movies/page.tsx.bkp ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ 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
+ 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<any[]>([]);
14
+ const { loading, setLoading } = useLoading();
15
+ const itemsPerPage = 16;
16
+ const [totalPages, setTotalPages] = useState(1);
17
+
18
+ useEffect(() => {
19
+ async function fetchMovies() {
20
+ setLoading(true);
21
+ try {
22
+ const data: any[] = (await getAllMovies()) || [];
23
+ const totalMovies = data?.length;
24
+ setTotalPages(Math.ceil(totalMovies / itemsPerPage));
25
+ const startIndex = (currentPage - 1) * itemsPerPage;
26
+ const paginatedMovies = data?.slice(startIndex, startIndex + itemsPerPage);
27
+ setMovies(paginatedMovies);
28
+ } catch (error) {
29
+ console.error('Error fetching movies:', error);
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ }
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="3ozsx6.">
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="zbazz1e"
52
+ >
53
+ {movies.map((title, index) => (
54
+ <MovieCard title={title} key={`${currentPage}page${index}`} data-oid="mc:2.uh" />
55
+ ))}
56
+ </div>
57
+ <div className="flex justify-center mt-8 gap-4" data-oid="noq2z2.">
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="d:8u196"
63
+ >
64
+ Previous
65
+ </button>
66
+ <span className="px-4 py-2 text-white" data-oid="v7is21m">
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="b.bxaco"
74
+ >
75
+ Next
76
+ </button>
77
+ </div>
78
+ </>
79
+ )}
80
+ </>
81
+ );
82
+ }
83
+
84
+ export default function MoviesPage() {
85
+ return (
86
+ <div className="page py-16" data-oid="_s_yu53">
87
+ <div className="container mx-auto px-6" data-oid="ty5ykip">
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="_y6oahy"
91
+ >
92
+ Discover Movies
93
+ </h2>
94
+ <Suspense
95
+ fallback={
96
+ <p className="text-center" data-oid="pmn28rb">
97
+ Loading...
98
+ </p>
99
+ }
100
+ data-oid="boa955w"
101
+ >
102
+ <MoviesContent data-oid="j159kgb" />
103
+ </Suspense>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
frontend/app/tvshows/page.tsx CHANGED
@@ -8,224 +8,244 @@ import { useLoading } from '@/components/loading/SplashScreen';
8
  import GenresFilter from '@/components/shared/GenresFilter';
9
 
10
  function TvShowsContent() {
11
- const searchParams = useSearchParams();
12
- const router = useRouter();
13
- const currentPage = parseInt(searchParams.get('page') || '1', 10);
14
- const [tvshows, setTvShows] = useState<{ title: string; episodeCount: any }[]>([]);
15
- const { loading, setLoading } = useLoading();
16
- const itemsPerPage = 16;
17
- const [totalPages, setTotalPages] = useState(1);
18
- useEffect(() => {
19
- async function fetchTvShows() {
20
- setLoading(true);
21
- try {
22
- const data = await getAllTvShows();
23
- if (data.length) {
24
- setTotalPages(Math.ceil(data.length / itemsPerPage));
25
- const startIndex = (currentPage - 1) * itemsPerPage;
26
- setTvShows(data.slice(startIndex, startIndex + itemsPerPage));
27
- }
28
- } catch (error) {
29
- console.error('Error fetching TV shows:', error);
30
- } finally {
31
- setLoading(false);
32
- }
33
- }
34
- fetchTvShows();
35
- }, [currentPage]);
36
 
37
- const handlePageChange = (newPage: number) => {
38
- if (newPage >= 1 && newPage <= totalPages) {
39
- router.push(`/tvshows?page=${newPage}`);
 
 
 
 
 
 
 
 
 
 
40
  }
41
- };
42
-
43
- return (
44
- <div className="portrait:p-2 p-4" data-oid="z4j5t8d">
45
- {loading ? (
46
- <div className="flex items-center justify-center min-h-[60vh]" data-oid="jdvygx2">
47
- <div
48
- className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
49
- data-oid="ex20n6p"
50
- ></div>
51
- </div>
52
- ) : (
53
- <>
54
- {/* Dynamic Grid */}
55
- <div
56
- className="flex flex-wrap justify-center items-center portrait:gap-2 gap-10"
57
- data-oid="ke2_x_x"
58
- >
59
- {tvshows.map((show, index) => (
60
- <div
61
- key={`${currentPage}page${index}`}
62
- className="transform transition-transform duration-300 hover:scale-105 w-[fit-content]"
63
- data-oid="x:.ceyx"
64
- >
65
- <TvShowCard
66
- title={show.title}
67
- episodesCount={show.episodeCount}
68
- data-oid="p.v-:hm"
69
- />
70
- </div>
71
- ))}
72
- </div>
73
 
74
- {/* Improved Pagination */}
75
- <div
76
- className="mt-12 flex flex-row items-center justify-center gap-2"
77
- data-oid="6fx8gif"
78
- >
79
- <div className="flex items-center gap-2" data-oid="qq.402e">
80
- <button
81
- onClick={() => handlePageChange(1)}
82
- disabled={currentPage <= 1}
83
- 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"
84
- data-oid="ve0w5mr"
85
- >
86
- <svg
87
- className="w-5 h-5"
88
- fill="none"
89
- stroke="currentColor"
90
- viewBox="0 0 24 24"
91
- data-oid="ya2aw1i"
92
- >
93
- <path
94
- strokeLinecap="round"
95
- strokeLinejoin="round"
96
- strokeWidth={2}
97
- d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
98
- data-oid="xc60cie"
99
- />
100
- </svg>
101
- </button>
102
- <button
103
- onClick={() => handlePageChange(currentPage - 1)}
104
- disabled={currentPage <= 1}
105
- 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"
106
- data-oid="z91p4_y"
107
- >
108
- <svg
109
- className="w-5 h-5"
110
- fill="none"
111
- stroke="currentColor"
112
- viewBox="0 0 24 24"
113
- data-oid="bk.mb1x"
114
- >
115
- <path
116
- strokeLinecap="round"
117
- strokeLinejoin="round"
118
- strokeWidth={2}
119
- d="M15 19l-7-7 7-7"
120
- data-oid="7m5rce:"
121
- />
122
- </svg>
123
- </button>
124
- </div>
125
 
126
- <div className="flex items-center gap-2" data-oid="hqeas1v">
127
- <span
128
- className="px-4 py-2 rounded-lg bg-gray-800 text-white font-medium"
129
- data-oid="k8ve27b"
130
- >
131
- Page {currentPage} of {totalPages}
132
- </span>
133
- </div>
134
 
135
- <div className="flex items-center gap-2" data-oid="yq:jhwz">
136
- <button
137
- onClick={() => handlePageChange(currentPage + 1)}
138
- disabled={currentPage >= totalPages}
139
- 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"
140
- data-oid="bi1rjxh"
141
- >
142
- <svg
143
- className="w-5 h-5"
144
- fill="none"
145
- stroke="currentColor"
146
- viewBox="0 0 24 24"
147
- data-oid=".1uvef3"
148
- >
149
- <path
150
- strokeLinecap="round"
151
- strokeLinejoin="round"
152
- strokeWidth={2}
153
- d="M9 5l7 7-7 7"
154
- data-oid="i91blmp"
155
- />
156
- </svg>
157
- </button>
158
- <button
159
- onClick={() => handlePageChange(totalPages)}
160
- disabled={currentPage >= totalPages}
161
- 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"
162
- data-oid="g9nu10p"
163
- >
164
- <svg
165
- className="w-5 h-5"
166
- fill="none"
167
- stroke="currentColor"
168
- viewBox="0 0 24 24"
169
- data-oid=".g:j8vb"
170
- >
171
- <path
172
- strokeLinecap="round"
173
- strokeLinejoin="round"
174
- strokeWidth={2}
175
- d="M13 5l7 7-7 7M5 5l7 7-7 7"
176
- data-oid="yh5__i."
177
- />
178
- </svg>
179
- </button>
180
- </div>
181
- </div>
182
- </>
183
- )}
184
  </div>
185
- );
186
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  export default function TvShowsPage() {
191
- return (
192
- <div className="page min-h-screen pt-20 pb-4" data-oid="0-uxuak">
193
- <div className="container mx-auto portrait:px-3 px-4" data-oid="7tvn4t1">
194
- {/* Header Section */}
195
- <div className="mb-8 space-y-2" data-oid="8d_p425">
196
- <h2 className="text-4xl font-bold text-white" data-oid="y4vnny4">
197
- TV Shows
198
- </h2>
199
- <p className="text-gray-400 max-w-3xl" data-oid="othjg4g">
200
- Explore our collection of TV series from various genres. From drama to
201
- comedy, find your next binge-worthy show here.
202
- </p>
203
- <GenresFilter mediaType='series' />
204
- </div>
205
 
206
- {/* Content Section */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  <div
208
- className="bg-gray-800/30 rounded-3xl backdrop-blur-sm border border-gray-700/50"
209
- data-oid="zv:kb.h"
210
  >
211
- <Suspense
212
- fallback={
213
- <div
214
- className="flex items-center justify-center min-h-[50vh]"
215
- data-oid="uo0i4dx"
216
- >
217
- <div
218
- className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
219
- data-oid="7nesstc"
220
- ></div>
221
- </div>
222
- }
223
- data-oid="85rngk_"
224
- >
225
- <TvShowsContent data-oid="z5e4dpy" />
226
- </Suspense>
227
  </div>
228
- </div>
229
- </div>
230
- );
 
 
 
 
 
 
 
231
  }
 
8
  import GenresFilter from '@/components/shared/GenresFilter';
9
 
10
  function TvShowsContent() {
11
+ const searchParams = useSearchParams();
12
+ const router = useRouter();
13
+ const currentPage = parseInt(searchParams.get('page') || '1', 10);
14
+ const [allTvShows, setAllTvShows] = useState<{ title: string; episodeCount: any }[]>([]);
15
+ const [tvshows, setTvShows] = useState<{ title: string; episodeCount: any }[]>([]);
16
+ const { loading, setLoading } = useLoading();
17
+ const itemsPerPage = 20;
18
+ const [totalPages, setTotalPages] = useState(1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ // Fetch all TV shows once on mount
21
+ useEffect(() => {
22
+ async function fetchTvShows() {
23
+ setLoading(true);
24
+ try {
25
+ const data = await getAllTvShows();
26
+ if (data.length) {
27
+ // Sort the data to ensure a consistent order (e.g., alphabetically by title)
28
+ const sortedData = [...data].sort((a, b) =>
29
+ a.title.localeCompare(b.title)
30
+ );
31
+ setAllTvShows(sortedData);
32
+ setTotalPages(Math.ceil(sortedData.length / itemsPerPage));
33
  }
34
+ } catch (error) {
35
+ console.error('Error fetching TV shows:', error);
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ }
40
+ fetchTvShows();
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ // Update the displayed tv shows when the page or full list changes
45
+ useEffect(() => {
46
+ if (allTvShows.length) {
47
+ const startIndex = (currentPage - 1) * itemsPerPage;
48
+ setTvShows(allTvShows.slice(startIndex, startIndex + itemsPerPage));
49
+ }
50
+ }, [allTvShows, currentPage]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ const handlePageChange = (newPage: number) => {
53
+ if (newPage >= 1 && newPage <= totalPages) {
54
+ router.push(`/tvshows?page=${newPage}`);
55
+ }
56
+ };
 
 
 
57
 
58
+ return (
59
+ <div className="portrait:p-2 p-4" data-oid="z4j5t8d">
60
+ {loading ? (
61
+ <div
62
+ className="flex items-center justify-center min-h-[60vh]"
63
+ data-oid="jdvygx2"
64
+ >
65
+ <div
66
+ className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
67
+ data-oid="ex20n6p"
68
+ ></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  </div>
70
+ ) : (
71
+ <>
72
+ {/* Default TV Shows Grid */}
73
+ <div
74
+ key={currentPage}
75
+ className="flex flex-wrap justify-center items-center portrait:gap-2 gap-10"
76
+ data-oid="ke2_x_x"
77
+ >
78
+ {tvshows.map((show, index) => (
79
+ <div
80
+ key={`${show.title}-${index}`}
81
+ className="transform transition-transform duration-300 hover:scale-105 w-[fit-content]"
82
+ data-oid="x:.ceyx"
83
+ >
84
+ <TvShowCard
85
+ title={show.title}
86
+ episodesCount={show.episodeCount}
87
+ data-oid="p.v-:hm"
88
+ />
89
+ </div>
90
+ ))}
91
+ </div>
92
 
93
+ {/* Pagination */}
94
+ <div
95
+ className="mt-12 flex flex-row items-center justify-center gap-2"
96
+ data-oid="6fx8gif"
97
+ >
98
+ <div className="flex items-center gap-2" data-oid="qq.402e">
99
+ <button
100
+ onClick={() => handlePageChange(1)}
101
+ disabled={currentPage <= 1}
102
+ 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"
103
+ data-oid="ve0w5mr"
104
+ >
105
+ <svg
106
+ className="w-5 h-5"
107
+ fill="none"
108
+ stroke="currentColor"
109
+ viewBox="0 0 24 24"
110
+ data-oid="ya2aw1i"
111
+ >
112
+ <path
113
+ strokeLinecap="round"
114
+ strokeLinejoin="round"
115
+ strokeWidth={2}
116
+ d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
117
+ data-oid="xc60cie"
118
+ />
119
+ </svg>
120
+ </button>
121
+ <button
122
+ onClick={() => handlePageChange(currentPage - 1)}
123
+ disabled={currentPage <= 1}
124
+ 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"
125
+ data-oid="z91p4_y"
126
+ >
127
+ <svg
128
+ className="w-5 h-5"
129
+ fill="none"
130
+ stroke="currentColor"
131
+ viewBox="0 0 24 24"
132
+ data-oid="bk.mb1x"
133
+ >
134
+ <path
135
+ strokeLinecap="round"
136
+ strokeLinejoin="round"
137
+ strokeWidth={2}
138
+ d="M15 19l-7-7 7-7"
139
+ data-oid="7m5rce:"
140
+ />
141
+ </svg>
142
+ </button>
143
+ </div>
144
+
145
+ <div className="flex items-center gap-2" data-oid="hqeas1v">
146
+ <span
147
+ className="px-4 py-2 rounded-lg bg-gray-800 text-white font-medium"
148
+ data-oid="k8ve27b"
149
+ >
150
+ Page {currentPage} of {totalPages}
151
+ </span>
152
+ </div>
153
 
154
+ <div className="flex items-center gap-2" data-oid="yq:jhwz">
155
+ <button
156
+ onClick={() => handlePageChange(currentPage + 1)}
157
+ disabled={currentPage >= totalPages}
158
+ 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"
159
+ data-oid="bi1rjxh"
160
+ >
161
+ <svg
162
+ className="w-5 h-5"
163
+ fill="none"
164
+ stroke="currentColor"
165
+ viewBox="0 0 24 24"
166
+ data-oid=".1uvef3"
167
+ >
168
+ <path
169
+ strokeLinecap="round"
170
+ strokeLinejoin="round"
171
+ strokeWidth={2}
172
+ d="M9 5l7 7-7 7"
173
+ data-oid="i91blmp"
174
+ />
175
+ </svg>
176
+ </button>
177
+ <button
178
+ onClick={() => handlePageChange(totalPages)}
179
+ disabled={currentPage >= totalPages}
180
+ 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"
181
+ data-oid="g9nu10p"
182
+ >
183
+ <svg
184
+ className="w-5 h-5"
185
+ fill="none"
186
+ stroke="currentColor"
187
+ viewBox="0 0 24 24"
188
+ data-oid=".g:j8vb"
189
+ >
190
+ <path
191
+ strokeLinecap="round"
192
+ strokeLinejoin="round"
193
+ strokeWidth={2}
194
+ d="M13 5l7 7-7 7M5 5l7 7-7 7"
195
+ data-oid="yh5__i."
196
+ />
197
+ </svg>
198
+ </button>
199
+ </div>
200
+ </div>
201
+ </>
202
+ )}
203
+ </div>
204
+ );
205
+ }
206
 
207
  export default function TvShowsPage() {
208
+ const [filterActive, setFilterActive] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
+ return (
211
+ <div className="page min-h-screen pt-20 pb-4" data-oid="0-uxuak">
212
+ <div className="container mx-auto portrait:px-3 px-4" data-oid="7tvn4t1">
213
+ {/* Header Section */}
214
+ <div className="mb-8 space-y-2" data-oid="8d_p425">
215
+ <h2 className="text-4xl font-bold text-white" data-oid="y4vnny4">
216
+ TV Shows
217
+ </h2>
218
+ <p className="text-gray-400 max-w-3xl" data-oid="othjg4g">
219
+ Explore our collection of TV series from various genres. From drama to comedy, find your next binge-worthy show here.
220
+ </p>
221
+ <GenresFilter mediaType="series" onFilterChange={setFilterActive} />
222
+ </div>
223
+
224
+ {/* Content Section */}
225
+ {!filterActive && (
226
+ <div
227
+ className="bg-gray-800/30 rounded-3xl backdrop-blur-sm border border-gray-700/50"
228
+ data-oid="zv:kb.h"
229
+ >
230
+ <Suspense
231
+ fallback={
232
  <div
233
+ className="flex items-center justify-center min-h-[50vh]"
234
+ data-oid="uo0i4dx"
235
  >
236
+ <div
237
+ className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"
238
+ data-oid="7nesstc"
239
+ ></div>
 
 
 
 
 
 
 
 
 
 
 
 
240
  </div>
241
+ }
242
+ data-oid="85rngk_"
243
+ >
244
+ <TvShowsContent data-oid="z5e4dpy" />
245
+ </Suspense>
246
+ </div>
247
+ )}
248
+ </div>
249
+ </div>
250
+ );
251
  }
frontend/components/movie/MovieCard.tsx CHANGED
@@ -72,20 +72,18 @@ export const MovieCard: React.FC<MovieCardProps> = ({ title }) => {
72
  const slideshowTimer = useRef<NodeJS.Timeout | null>(null);
73
  const hoverTimer = useRef<NodeJS.Timeout | null>(null);
74
 
75
- // Extract the correct title
76
- const formattedTitle = title.includes('/') ? title.split('/').pop() : title;
77
 
78
  useEffect(() => {
79
  async function fetchMovieCard() {
80
  try {
81
- const cardData = await getMovieCard(formattedTitle);
82
  setCard(cardData);
83
  } catch (error) {
84
  console.error('Error fetching movie card:', error);
85
  }
86
  }
87
  fetchMovieCard();
88
- }, [formattedTitle]);
89
 
90
  // Set a random card image once when card loads.
91
  useEffect(() => {
@@ -281,7 +279,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ title }) => {
281
 
282
  </button>
283
  <div className="flex flex-col w-full">
284
- <h3 className="pb-2 pl-2 pt-2 bg-gray-700/60 text-md sm:text-lg md:text-xl font-semibold text-white sm:max-w-[50%] md:max-w-[70%] text-clip line-clamp-1">
285
  {card?.title || 'Loading...'}
286
  </h3>
287
 
@@ -311,7 +309,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ title }) => {
311
  alt="Banner Preview"
312
  />
313
  {/* Slideshow controls */}
314
- {card.banner.length > 0 && (
315
  <>
316
  <button
317
  onClick={handlePrev}
@@ -334,8 +332,8 @@ export const MovieCard: React.FC<MovieCardProps> = ({ title }) => {
334
  {card.overview || 'No overview available.'}
335
  </div>
336
  {/* View Details Button */}
337
- <div>
338
- <Link href={`/movie/${formattedTitle}`}>
339
  <button className="text-white 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 rounded-3xl flex items-center text-sm md:text-base transition-all duration-750 ease-in-out">
340
  {`View Details >`}
341
  </button>
@@ -357,7 +355,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ title }) => {
357
  >
358
  <div
359
  className="rounded-lg border border-gray-400/50 overflow-hidden relative transition-transform duration-300 w-[140px] sm:w-[150px] md:w-[180px] lg:w-[180px] xl:w-[200px]
360
- h-[210px] sm:h-[220px] md:h-[250px] lg:h-[270px] xl:h-[290px]"
361
  >
362
  {/* Skeleton Loader */}
363
  {!imageLoaded && (
 
72
  const slideshowTimer = useRef<NodeJS.Timeout | null>(null);
73
  const hoverTimer = useRef<NodeJS.Timeout | null>(null);
74
 
 
 
75
 
76
  useEffect(() => {
77
  async function fetchMovieCard() {
78
  try {
79
+ const cardData = await getMovieCard(title);
80
  setCard(cardData);
81
  } catch (error) {
82
  console.error('Error fetching movie card:', error);
83
  }
84
  }
85
  fetchMovieCard();
86
+ }, [title]);
87
 
88
  // Set a random card image once when card loads.
89
  useEffect(() => {
 
279
 
280
  </button>
281
  <div className="flex flex-col w-full">
282
+ <h3 className="pb-2 pl-2 pt-2 bg-gray-700/60 text-md sm:text-lg md:text-xl font-semibold text-white text-clip line-clamp-1">
283
  {card?.title || 'Loading...'}
284
  </h3>
285
 
 
309
  alt="Banner Preview"
310
  />
311
  {/* Slideshow controls */}
312
+ {card.banner.length > 1 && (
313
  <>
314
  <button
315
  onClick={handlePrev}
 
332
  {card.overview || 'No overview available.'}
333
  </div>
334
  {/* View Details Button */}
335
+ <div className='flex flex-row'>
336
+ <Link href={`/movie/${title}`}>
337
  <button className="text-white 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 rounded-3xl flex items-center text-sm md:text-base transition-all duration-750 ease-in-out">
338
  {`View Details >`}
339
  </button>
 
355
  >
356
  <div
357
  className="rounded-lg border border-gray-400/50 overflow-hidden relative transition-transform duration-300 w-[140px] sm:w-[150px] md:w-[180px] lg:w-[180px] xl:w-[200px]
358
+ h-[210px] sm:h-[220px] md:h-[250px] lg:h-[280px] xl:h-[300px]"
359
  >
360
  {/* Skeleton Loader */}
361
  {!imageLoaded && (
frontend/components/shared/GenresFilter.tsx CHANGED
@@ -2,20 +2,25 @@ import { getGenresItems, getGenreCategories } from '@/lib/lb';
2
  import { useEffect, useState } from 'react';
3
  import { useLoading } from '../loading/SplashScreen';
4
  import { FunnelIcon } from '@heroicons/react/24/outline';
5
- import { useSearchParams } from 'next/navigation';
6
- import { useRouter } from 'next/navigation';
7
  import { TvShowCard } from '../tvshow/TvShowCard';
 
8
  import { useSpinnerLoading } from '../loading/Spinner';
9
 
 
 
 
 
 
10
  interface GenreCompProps {
11
  mediaType: string;
 
12
  }
13
 
14
- export default function GenresFilter({ mediaType }: GenreCompProps) {
15
- const [genreCategories, setGenreCategories] = useState<string[]>([]);
16
  const [selectedGenreCategories, setSelectedGenreCategories] = useState<string[]>([]);
17
  const [genresItems, setGenresItems] = useState<any[]>([]);
18
- const {spinnerLoading, setSpinnerLoading} = useSpinnerLoading();
19
  const [currentPage, setCurrentPage] = useState(1);
20
  const [totalPages, setTotalPages] = useState(1);
21
  const [showAll, setShowAll] = useState(false);
@@ -25,8 +30,8 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
25
  async function fetchGenreCategories() {
26
  setSpinnerLoading(true);
27
  try {
28
- const gc = await getGenreCategories();
29
- setGenreCategories(gc);
30
  } catch (error) {
31
  console.error('Error fetching genre categories:', error);
32
  } finally {
@@ -34,13 +39,13 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
34
  }
35
  }
36
  fetchGenreCategories();
37
- }, []);
38
 
39
  const handleGenreClick = (genre: string) => {
40
  if (genre === 'All') {
41
  setSelectedGenreCategories(['All']);
42
  setGenresItems([]);
43
- setCurrentPage(1); // Reset to first page when changing genres
44
  } else {
45
  setSelectedGenreCategories((prev) => {
46
  const filtered = prev.filter((g) => g !== 'All');
@@ -48,7 +53,7 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
48
  ? filtered.filter((g) => g !== genre)
49
  : [...filtered, genre];
50
  });
51
- setCurrentPage(1); // Reset to first page when changing genres
52
  }
53
  };
54
 
@@ -63,9 +68,19 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
63
  itemsPerPage,
64
  currentPage,
65
  );
66
- setGenresItems(response?.series || []);
67
- // Assuming the API returns total count or total pages
68
- setTotalPages(Math.ceil(response?.totalCount / itemsPerPage) || 1);
 
 
 
 
 
 
 
 
 
 
69
  } catch (error) {
70
  console.error('Error fetching genre items:', error);
71
  } finally {
@@ -73,12 +88,18 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
73
  }
74
  };
75
 
76
- // Call handleFilter when page changes or when filters are updated
77
  useEffect(() => {
78
- if (selectedGenreCategories.length > 0) {
79
- handleFilter();
 
 
 
 
 
 
80
  }
81
- }, [currentPage, selectedGenreCategories]);
82
 
83
  const handlePageChange = (newPage: number) => {
84
  if (newPage >= 1 && newPage <= totalPages) {
@@ -99,6 +120,7 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
99
  <FunnelIcon className="size-5" />
100
  <p className="text-sm">Filter</p>
101
  </button>
 
102
  <button
103
  key="All"
104
  onClick={() => handleGenreClick('All')}
@@ -110,20 +132,23 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
110
  >
111
  All
112
  </button>
113
- {genreCategories.slice(0, showAll ? genreCategories.length : 5).map((genre) => (
114
- <button
115
- key={genre}
116
- onClick={() => handleGenreClick(genre)}
117
- className={`px-4 py-2 rounded-full text-sm font-medium transition-colors border border-gray-400/50 ${
118
- isSelected(genre)
119
- ? 'bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 text-white'
120
- : 'bg-gray-800 text-gray-300 hover:bg-gray-700'
121
- }`}
122
- >
123
- {genre}
124
- </button>
125
- ))}
126
- {genreCategories.length > 5 && (
 
 
 
127
  <button
128
  className="text-gray-500 hover:text-gray-400"
129
  onClick={() => setShowAll(!showAll)}
@@ -139,12 +164,12 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
139
  ) : (
140
  <>
141
  <div className="flex flex-wrap justify-center items-center portrait:gap-2 gap-10">
142
- {genresItems.map((show, index) => (
143
  <div
144
  key={index}
145
  className="transform transition-transform duration-300 hover:scale-105 w-[fit-content]"
146
- >
147
- <TvShowCard title={show.title} episodesCount={null} />
148
  </div>
149
  ))}
150
  </div>
@@ -154,7 +179,7 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
154
  <button
155
  onClick={() => handlePageChange(1)}
156
  disabled={currentPage <= 1 || spinnerLoading}
157
- 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"
158
  >
159
  <svg
160
  className="w-5 h-5"
@@ -173,7 +198,7 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
173
  <button
174
  onClick={() => handlePageChange(currentPage - 1)}
175
  disabled={currentPage <= 1 || spinnerLoading}
176
- 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"
177
  >
178
  <svg
179
  className="w-5 h-5"
@@ -199,7 +224,7 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
199
  <button
200
  onClick={() => handlePageChange(currentPage + 1)}
201
  disabled={currentPage >= totalPages || spinnerLoading}
202
- 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"
203
  >
204
  <svg
205
  className="w-5 h-5"
@@ -218,7 +243,7 @@ export default function GenresFilter({ mediaType }: GenreCompProps) {
218
  <button
219
  onClick={() => handlePageChange(totalPages)}
220
  disabled={currentPage >= totalPages || spinnerLoading}
221
- 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"
222
  >
223
  <svg
224
  className="w-5 h-5"
 
2
  import { useEffect, useState } from 'react';
3
  import { useLoading } from '../loading/SplashScreen';
4
  import { FunnelIcon } from '@heroicons/react/24/outline';
 
 
5
  import { TvShowCard } from '../tvshow/TvShowCard';
6
+ import { MovieCard } from '../movie/MovieCard';
7
  import { useSpinnerLoading } from '../loading/Spinner';
8
 
9
+ interface Genre {
10
+ name: string;
11
+ density: number;
12
+ }
13
+
14
  interface GenreCompProps {
15
  mediaType: string;
16
+ onFilterChange?: (active: boolean) => void;
17
  }
18
 
19
+ export default function GenresFilter({ mediaType, onFilterChange }: GenreCompProps) {
20
+ const [genreCategories, setGenreCategories] = useState<Genre[]>([]);
21
  const [selectedGenreCategories, setSelectedGenreCategories] = useState<string[]>([]);
22
  const [genresItems, setGenresItems] = useState<any[]>([]);
23
+ const { spinnerLoading, setSpinnerLoading } = useSpinnerLoading();
24
  const [currentPage, setCurrentPage] = useState(1);
25
  const [totalPages, setTotalPages] = useState(1);
26
  const [showAll, setShowAll] = useState(false);
 
30
  async function fetchGenreCategories() {
31
  setSpinnerLoading(true);
32
  try {
33
+ const response = await getGenreCategories(mediaType);
34
+ setGenreCategories(response);
35
  } catch (error) {
36
  console.error('Error fetching genre categories:', error);
37
  } finally {
 
39
  }
40
  }
41
  fetchGenreCategories();
42
+ }, [mediaType, setSpinnerLoading]);
43
 
44
  const handleGenreClick = (genre: string) => {
45
  if (genre === 'All') {
46
  setSelectedGenreCategories(['All']);
47
  setGenresItems([]);
48
+ setCurrentPage(1);
49
  } else {
50
  setSelectedGenreCategories((prev) => {
51
  const filtered = prev.filter((g) => g !== 'All');
 
53
  ? filtered.filter((g) => g !== genre)
54
  : [...filtered, genre];
55
  });
56
+ setCurrentPage(1);
57
  }
58
  };
59
 
 
68
  itemsPerPage,
69
  currentPage,
70
  );
71
+ if (mediaType === 'movie') {
72
+ setGenresItems(response?.movies || []);
73
+ } else {
74
+ setGenresItems(response?.series || []);
75
+ }
76
+
77
+ setTotalPages(Math.ceil(response?.total_series / itemsPerPage) || 1);
78
+ const filteringActive =
79
+ selectedGenreCategories.length > 0 &&
80
+ !(selectedGenreCategories.length === 1 && selectedGenreCategories[0] === 'All');
81
+ if (onFilterChange) {
82
+ onFilterChange(filteringActive);
83
+ }
84
  } catch (error) {
85
  console.error('Error fetching genre items:', error);
86
  } finally {
 
88
  }
89
  };
90
 
91
+ // If no filter is active, clear any filtered items and notify parent.
92
  useEffect(() => {
93
+ const filteringActive =
94
+ selectedGenreCategories.length > 0 &&
95
+ !(selectedGenreCategories.length === 1 && selectedGenreCategories[0] === 'All');
96
+ if (!filteringActive) {
97
+ setGenresItems([]);
98
+ if (onFilterChange) {
99
+ onFilterChange(false);
100
+ }
101
  }
102
+ }, [selectedGenreCategories, onFilterChange]);
103
 
104
  const handlePageChange = (newPage: number) => {
105
  if (newPage >= 1 && newPage <= totalPages) {
 
120
  <FunnelIcon className="size-5" />
121
  <p className="text-sm">Filter</p>
122
  </button>
123
+ {/* "All" Button */}
124
  <button
125
  key="All"
126
  onClick={() => handleGenreClick('All')}
 
132
  >
133
  All
134
  </button>
135
+ {/* Other Genre Buttons */}
136
+ {genreCategories?.length > 0 &&
137
+ genreCategories.slice(0, showAll ? genreCategories.length : 5).map((genre) => (
138
+ <button
139
+ key={genre.name}
140
+ onClick={() => handleGenreClick(genre.name)}
141
+ className={`px-4 py-2 rounded-full text-sm font-medium transition-colors border border-gray-400/50 ${
142
+ isSelected(genre.name)
143
+ ? 'bg-gradient-to-r from-violet-600 to-purple-500 hover:bg-gradient-to-r hover:from-violet-500 hover:to-purple-400 text-white'
144
+ : 'bg-gray-800 text-gray-300 hover:bg-gray-700'
145
+ }`}
146
+ >
147
+ {genre.name}{' '}
148
+ <span className="text-xs text-gray-500">({genre.density})</span>
149
+ </button>
150
+ ))}
151
+ {genreCategories?.length > 5 && (
152
  <button
153
  className="text-gray-500 hover:text-gray-400"
154
  onClick={() => setShowAll(!showAll)}
 
164
  ) : (
165
  <>
166
  <div className="flex flex-wrap justify-center items-center portrait:gap-2 gap-10">
167
+ {genresItems.map((item, index) => (
168
  <div
169
  key={index}
170
  className="transform transition-transform duration-300 hover:scale-105 w-[fit-content]"
171
+ >{mediaType === 'movie' ? <MovieCard title={item.title} /> : <TvShowCard title={item.title} episodesCount={null} />}
172
+
173
  </div>
174
  ))}
175
  </div>
 
179
  <button
180
  onClick={() => handlePageChange(1)}
181
  disabled={currentPage <= 1 || spinnerLoading}
182
+ className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 transition-colors"
183
  >
184
  <svg
185
  className="w-5 h-5"
 
198
  <button
199
  onClick={() => handlePageChange(currentPage - 1)}
200
  disabled={currentPage <= 1 || spinnerLoading}
201
+ className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 transition-colors"
202
  >
203
  <svg
204
  className="w-5 h-5"
 
224
  <button
225
  onClick={() => handlePageChange(currentPage + 1)}
226
  disabled={currentPage >= totalPages || spinnerLoading}
227
+ className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 transition-colors"
228
  >
229
  <svg
230
  className="w-5 h-5"
 
243
  <button
244
  onClick={() => handlePageChange(totalPages)}
245
  disabled={currentPage >= totalPages || spinnerLoading}
246
+ className="p-2 rounded-lg bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white disabled:opacity-50 transition-colors"
247
  >
248
  <svg
249
  className="w-5 h-5"
frontend/components/tvshow/TvShowCard.tsx CHANGED
@@ -288,7 +288,7 @@ export const TvShowCard: React.FC<TvShowCardProps> = ({ title, episodesCount = n
288
  )}
289
  {/* Title and Year */}
290
  <div className="flex flex-col w-full">
291
- <h3 className="pl-8 h-11 lg:pb:2 pt-2 bg-gray-700/60 text-md sm:text-lg md:text-xl font-semibold text-white sm:max-w-[50%] md:max-w-[70%] text-clip line-clamp-1">
292
  {card?.title || 'Loading...'}
293
  </h3>
294
 
@@ -318,7 +318,7 @@ export const TvShowCard: React.FC<TvShowCardProps> = ({ title, episodesCount = n
318
  alt="Banner Preview"
319
  />
320
  {/* Slideshow controls */}
321
- {card.banner.length > 0 && (
322
  <>
323
  <button
324
  onClick={handlePrev}
@@ -341,8 +341,8 @@ export const TvShowCard: React.FC<TvShowCardProps> = ({ title, episodesCount = n
341
  {card.overview}
342
  </div>
343
  {/* View Details Button */}
344
- <div>
345
- <Link href={`/tvshow/${card.title}`}>
346
  <button className="text-white 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 rounded-3xl flex items-center text-sm md:text-base transition-all duration-750 ease-in-out">
347
  {`View Details >`}
348
  </button>
@@ -364,7 +364,7 @@ export const TvShowCard: React.FC<TvShowCardProps> = ({ title, episodesCount = n
364
  >
365
  <div
366
  className="rounded-lg border border-gray-400/50 overflow-hidden relative transition-transform duration-300 w-[140px] sm:w-[150px] md:w-[180px] lg:w-[180px] xl:w-[200px]
367
- h-[210px] sm:h-[220px] md:h-[250px] lg:h-[270px] xl:h-[290px]"
368
  >
369
  {/* Episodes Count Bubble */}
370
  {episodesCount !== null && (
 
288
  )}
289
  {/* Title and Year */}
290
  <div className="flex flex-col w-full">
291
+ <h3 className="pl-8 h-11 lg:pb:2 pt-2 bg-gray-700/60 text-md sm:text-lg md:text-xl font-semibold text-white text-clip line-clamp-1">
292
  {card?.title || 'Loading...'}
293
  </h3>
294
 
 
318
  alt="Banner Preview"
319
  />
320
  {/* Slideshow controls */}
321
+ {card.banner.length > 1 && (
322
  <>
323
  <button
324
  onClick={handlePrev}
 
341
  {card.overview}
342
  </div>
343
  {/* View Details Button */}
344
+ <div className='flex flex-row'>
345
+ <Link href={`/tvshow/${title}`}>
346
  <button className="text-white 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 rounded-3xl flex items-center text-sm md:text-base transition-all duration-750 ease-in-out">
347
  {`View Details >`}
348
  </button>
 
364
  >
365
  <div
366
  className="rounded-lg border border-gray-400/50 overflow-hidden relative transition-transform duration-300 w-[140px] sm:w-[150px] md:w-[180px] lg:w-[180px] xl:w-[200px]
367
+ h-[210px] sm:h-[220px] md:h-[250px] lg:h-[280px] xl:h-[300px]"
368
  >
369
  {/* Episodes Count Bubble */}
370
  {episodesCount !== null && (
frontend/lib/LoadbalancerAPI.js CHANGED
@@ -87,9 +87,13 @@ class LoadBalancerAPI {
87
  return await this._get(`/api/get/recent?limit=${limit}`);
88
  }
89
 
90
- async getGenreCategories() {
91
- return await this._get('/api/get/genre_categories');
 
 
 
92
  }
 
93
 
94
  async getGenreItems(genres, mediaType, limit = 5, page = 1) {
95
  if (!Array.isArray(genres)) {
 
87
  return await this._get(`/api/get/recent?limit=${limit}`);
88
  }
89
 
90
+ async getGenreCategories(mediaType) {
91
+ const url = mediaType
92
+ ? `/api/get/genre_categories?media_type=${encodeURIComponent(mediaType)}`
93
+ : '/api/get/genre_categories';
94
+ return await this._get(url);
95
  }
96
+
97
 
98
  async getGenreItems(genres, mediaType, limit = 5, page = 1) {
99
  if (!Array.isArray(genres)) {
frontend/lib/config.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export const WEB_VERSION = 'v0.1.4 beta';
frontend/lib/config.tsx DELETED
@@ -1 +0,0 @@
1
- export const WEB_VERSION = 'v0.1.3 beta';
 
 
frontend/lib/lb.js CHANGED
@@ -77,9 +77,13 @@ export async function getNewContents(limit = 5) {
77
 
78
 
79
  export async function getAllMovies(){
80
- const movies = await lb.getAllMovies();
81
- console.debug(movies);
82
- return movies
 
 
 
 
83
  }
84
 
85
  export async function getAllTvShows() {
@@ -119,8 +123,8 @@ export async function getTvShowMetadata(title){
119
  return tvshow
120
  }
121
 
122
- export async function getGenreCategories(){
123
- const gc = await lb.getGenreCategories();
124
  console.debug(gc);
125
  if (gc.genres)
126
  return gc.genres
 
77
 
78
 
79
  export async function getAllMovies(){
80
+ const movies = await lb.getAllMovies();
81
+ console.debug(movies);
82
+
83
+ const formattedMovies = movies.map(title => ({
84
+ title: title.replace('films/', '')
85
+ }));
86
+ return formattedMovies
87
  }
88
 
89
  export async function getAllTvShows() {
 
123
  return tvshow
124
  }
125
 
126
+ export async function getGenreCategories(mediaType){
127
+ const gc = await lb.getGenreCategories(mediaType);
128
  console.debug(gc);
129
  if (gc.genres)
130
  return gc.genres