ChandimaPrabath commited on
Commit
0879a03
1 Parent(s): 1a805b3

0.0.0.3 Alpha

Browse files
frontend/src/app/genres/[genre]/genres.css ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ color: #e0e0e0; /* Light text color */
3
+ }
4
+ /* Container for the genre page */
5
+ .genre-page {
6
+ max-width: 100dvw;
7
+ margin: 0 auto;
8
+ padding: 20px;
9
+ background-color: #0a0b19; /* Slightly lighter dark background */
10
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
11
+ }
12
+
13
+ /* Title styling */
14
+ .genre-title {
15
+ font-size: 2rem;
16
+ text-align: center;
17
+ color: #ffffff; /* Light color for the title */
18
+ }
19
+
20
+ .genre-title-section{
21
+ display: flex;
22
+ justify-content: center;
23
+ align-items: center;
24
+ }
25
+
26
+ /* Section styling */
27
+ .section {
28
+ margin-bottom: 40px;
29
+ }
30
+
31
+ @media (orientation: landscape) {
32
+ .section {
33
+ margin-left: 25px;
34
+ margin-right: 25px;
35
+ }
36
+ }
37
+
38
+ .section h2 {
39
+ font-size: 1.8rem;
40
+ color: #cccccc; /* Lighter gray for section titles */
41
+ }
42
+
43
+ /* Horizontal scrolling container */
44
+ .items-grid {
45
+ display: flex;
46
+ overflow-x: auto;
47
+ padding: 10px 0;
48
+ gap: 20px;
49
+ }
50
+
51
+ .items-grid::-webkit-scrollbar {
52
+ height: 8px;
53
+ }
54
+
55
+ .items-grid::-webkit-scrollbar-thumb {
56
+ background-color: #3c23cf;
57
+ border-radius: 4px;
58
+ }
59
+
60
+ .items-grid::-webkit-scrollbar-track {
61
+ background-color: #2c2c2c;
62
+ }
63
+
64
+ /* Card for each item */
65
+ .item-card {
66
+ background-color: #2c2c2c; /* Dark card background */
67
+ border-radius: 8px;
68
+ overflow: hidden;
69
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
70
+ transition: transform 0.3s, box-shadow 0.3s;
71
+ flex: 0 0 300px; /* Fixed width for horizontal scrolling */
72
+ }
73
+
74
+ .item-card:hover {
75
+ transform: scale(1.03);
76
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.7);
77
+ }
78
+
79
+ /* Styling for item link */
80
+ .item-link {
81
+ display: block;
82
+ text-decoration: none;
83
+ color: inherit;
84
+ }
85
+
86
+ .item-image {
87
+ width: 100%;
88
+ height: 120px; /* Fixed height for consistency */
89
+ object-fit: cover; /* Maintain aspect ratio and fill container */
90
+ display: block;
91
+ }
92
+
93
+ /* Item information */
94
+ .item-info {
95
+ padding: 10px;
96
+ }
97
+
98
+ .item-title {
99
+ font-size: 1.2rem;
100
+ font-weight: bold;
101
+ margin: 0;
102
+ color: #ffffff; /* Light color for item titles */
103
+ white-space: nowrap;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis; /* Handle overflow with ellipsis */
106
+ }
107
+
108
+ .item-description {
109
+ font-size: 0.9rem;
110
+ color: #b0b0b0; /* Lighter gray for descriptions */
111
+ margin: 5px 0 0;
112
+ white-space: nowrap;
113
+ overflow: hidden;
114
+ text-overflow: ellipsis; /* Handle overflow with ellipsis */
115
+ }
116
+
117
+ /* Loading and error messages */
118
+ .loading,
119
+ .error {
120
+ font-size: 1.2rem;
121
+ text-align: center;
122
+ margin-top: 50px;
123
+ }
124
+
125
+ /* No items available message */
126
+ .no-items {
127
+ font-size: 1.2rem;
128
+ color: #888888; /* Light gray for no items message */
129
+ text-align: center;
130
+ margin-top: 20px;
131
+ }
132
+
133
+ /* Load More Button container */
134
+ .load-more-container {
135
+ text-align: center;
136
+ margin: 20px 0;
137
+ }
138
+
139
+ /* Load More Button styling */
140
+ .load-more-button {
141
+ background-color: #1a1c3f; /* Accent color */
142
+ color: #ffffff;
143
+ border: 1px solid #4339ff;
144
+ border-radius: 4px;
145
+ padding: 10px 20px;
146
+ margin-left: 15px;
147
+ font-size: 1rem;
148
+ cursor: pointer;
149
+ transition: background-color 0.3s, transform 0.3s;
150
+ }
151
+
152
+ .load-more-button:hover {
153
+ background-color: #3700b3; /* Darker shade for hover */
154
+ transform: scale(1.05);
155
+ }
156
+
157
+ .load-more-button:focus {
158
+ outline: none;
159
+ }
160
+
161
+ .section-controls {
162
+ display: flex;
163
+ align-items: center;
164
+ text-align: center;
165
+ justify-content: space-between;
166
+ }
167
+
168
+ .scroll-controls button {
169
+ margin-left: 10px;
170
+ margin-right: 10px;
171
+ }
frontend/src/app/genres/[genre]/page.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import apiClient from "@/api/apiClient";
3
+ import { useEffect, useState, useRef } from "react";
4
+ import Link from "next/link";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { faCaretLeft, faCaretRight } from "@fortawesome/free-solid-svg-icons";
7
+ import "./genres.css";
8
+
9
+ export default function GenrePage({ params }) {
10
+ const [genreItems, setGenreItems] = useState(null);
11
+ const [loading, setLoading] = useState(true);
12
+ const [error, setError] = useState(null);
13
+ const [itemLimit, setItemLimit] = useState(5);
14
+
15
+ const moviesRef = useRef(null);
16
+ const seriesRef = useRef(null);
17
+
18
+ useEffect(() => {
19
+ async function fetchData() {
20
+ try {
21
+ const data = await apiClient.getGenreItems(params.genre, null, itemLimit);
22
+ if (data) {
23
+ setGenreItems(data);
24
+ } else {
25
+ setError("Genre data not found.");
26
+ }
27
+ } catch (err) {
28
+ setError("An error occurred while fetching genre data.");
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ }
33
+ fetchData();
34
+ }, [params.genre, itemLimit]);
35
+
36
+ const loadMore = () => {
37
+ setItemLimit((prevLimit) => prevLimit + 5);
38
+ };
39
+
40
+ const scroll = (ref, direction) => {
41
+ if (ref.current) {
42
+ const scrollAmount = direction === "left" ? -360 : 360;
43
+ ref.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
44
+ }
45
+ };
46
+
47
+ if (loading) {
48
+ return <div className="loading">Loading...</div>;
49
+ }
50
+
51
+ if (error) {
52
+ return <div className="error">Error: {error}</div>;
53
+ }
54
+
55
+ return (
56
+ <div className="genre-page">
57
+ <div className="genre-title-section">
58
+ <h1 className="genre-title">{params.genre}</h1>
59
+ <div className="load-more-container">
60
+ <button className="load-more-button" onClick={loadMore}>
61
+ Load More
62
+ </button>
63
+ </div>
64
+ </div>
65
+ {genreItems && (
66
+ <>
67
+ {genreItems.movies && genreItems.movies.length > 0 && (
68
+ <section className="section movies">
69
+ <div className="section-controls">
70
+ <h2>Movies</h2>
71
+ <div className="scroll-controls">
72
+ <button onClick={() => scroll(moviesRef, "left")} className="scroll-button">
73
+ <FontAwesomeIcon icon={faCaretLeft} size="2xl" />
74
+ </button>
75
+ <button onClick={() => scroll(moviesRef, "right")} className="scroll-button">
76
+ <FontAwesomeIcon icon={faCaretRight} size="2xl" />
77
+ </button>
78
+ </div>
79
+ </div>
80
+ <div className="items-grid" ref={moviesRef}>
81
+ {genreItems.movies.map((item, index) => (
82
+ <div key={index} className="item-card">
83
+ <Link href={`/movie/${item[0]}`} passHref>
84
+ <div className="item-link">
85
+ <img
86
+ src={item[3] || `https://via.placeholder.com/400x100/1a1c3f/FFF?text=${encodeURIComponent(item[0])}`}
87
+ alt={item[0]}
88
+ className="item-image"
89
+ />
90
+ <div className="item-info">
91
+ <h3 className="item-title">{item[0]}</h3>
92
+ <p className="item-description">{item[2]}</p>
93
+ </div>
94
+ </div>
95
+ </Link>
96
+ </div>
97
+ ))}
98
+ </div>
99
+ </section>
100
+ )}
101
+
102
+ {genreItems.series && genreItems.series.length > 0 && (
103
+ <section className="section series">
104
+ <div className="section-controls">
105
+ <h2>Series</h2>
106
+ <div className="scroll-controls">
107
+ <button onClick={() => scroll(seriesRef, "left")} className="scroll-button">
108
+ <FontAwesomeIcon icon={faCaretLeft} size="2xl" />
109
+ </button>
110
+ <button onClick={() => scroll(seriesRef, "right")} className="scroll-button">
111
+ <FontAwesomeIcon icon={faCaretRight} size="2xl" />
112
+ </button>
113
+ </div>
114
+ </div>
115
+ <div className="items-grid" ref={seriesRef}>
116
+ {genreItems.series.map((item, index) => (
117
+ <div key={index} className="item-card">
118
+ <Link href={`/series/${item[0]}`} passHref>
119
+ <div className="item-link">
120
+ <img
121
+ src={item[3] || `https://via.placeholder.com/150?text=${encodeURIComponent(item[0])}`}
122
+ alt={item[0]}
123
+ className="item-image"
124
+ />
125
+ <div className="item-info">
126
+ <h3 className="item-title">{item[0]}</h3>
127
+ <p className="item-description">{item[2]}</p>
128
+ </div>
129
+ </div>
130
+ </Link>
131
+ </div>
132
+ ))}
133
+ </div>
134
+ </section>
135
+ )}
136
+ </>
137
+ )}
138
+
139
+ {!genreItems ||
140
+ (!genreItems.movies && !genreItems.series && (
141
+ <p className="no-items">No items available for this genre.</p>
142
+ ))}
143
+ </div>
144
+ );
145
+ }
frontend/src/app/index.css ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .index-page {
2
+ max-width: 100dvw;
3
+ }
4
+
5
+ @media (orientation: landscape) {
6
+ .index-page-container {
7
+ max-width: 80dvw;
8
+ }
9
+ }
10
+
11
+ .index-page-container {
12
+ display: flex;
13
+ flex-direction: column;
14
+ margin: 0 auto;
15
+ flex: 1;
16
+ }
17
+
18
+ .section-title {
19
+ color: #ffffff;
20
+ font-size: 1.4rem;
21
+ font-weight: bold;
22
+ margin: 16px 0;
23
+ padding-left: 16px;
24
+ }
25
+
26
+ .grid-container {
27
+ display: grid;
28
+ grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
29
+ gap: 16px;
30
+ padding: 0 16px;
31
+ }
frontend/src/app/movie/[title]/movie.css ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Anton&display=swap');
2
+ .movie-details-page {
3
+ position: relative;
4
+ overflow: hidden;
5
+ scroll-behavior: smooth;
6
+ color: #fff;
7
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
8
+ }
9
+
10
+ /* Backdrop Styling */
11
+ .movie-details-backdrop {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ width: 100%;
16
+ height: 100%;
17
+ background-size: 250% 150%; /* Adjusted size to create a zooming effect */
18
+ background-position: center;
19
+ z-index: -1;
20
+ animation: backdrop-anim-portrait 20s ease-in-out infinite;
21
+ filter: blur(10px);
22
+ }
23
+
24
+ @keyframes backdrop-anim-portrait {
25
+ 0% {
26
+ background-position: center;
27
+ background-size: 350% 150%;
28
+ }
29
+ 25% {
30
+ background-position: top left;
31
+ background-size: 320% 120%;
32
+ }
33
+ 50% {
34
+ background-position: bottom right;
35
+ background-size: 350% 150%;
36
+ }
37
+ 75% {
38
+ background-position: top right;
39
+ background-size: 320% 120%;
40
+ }
41
+ 100% {
42
+ background-position: center;
43
+ background-size: 350% 150%;
44
+ }
45
+ }
46
+
47
+ @media (orientation:landscape){
48
+ .movie-details-backdrop {
49
+ background-size: 250% 150%; /* Adjusted size to create a zooming effect */
50
+ animation: backdrop-anim-landscape 20s ease-in-out infinite;
51
+ }
52
+ }
53
+
54
+ @keyframes backdrop-anim-landscape {
55
+ 0% {
56
+ background-position: center;
57
+ background-size: 150% 150%;
58
+ }
59
+ 25% {
60
+ background-position: top left;
61
+ background-size: 120% 120%;
62
+ }
63
+ 50% {
64
+ background-position: bottom right;
65
+ background-size: 150% 150%;
66
+ }
67
+ 75% {
68
+ background-position: top right;
69
+ background-size: 120% 120%;
70
+ }
71
+ 100% {
72
+ background-position: center;
73
+ background-size: 150% 150%;
74
+ }
75
+ }
76
+ /* Container Styling */
77
+ .movie-details-page-container {
78
+ position: relative;
79
+ max-width: 1200px;
80
+ margin: 0 auto;
81
+ padding: 20px;
82
+ }
83
+
84
+ /* Header Styling */
85
+ .movie-details-header {
86
+ display: flex;
87
+ justify-content: space-between;
88
+ align-items: center;
89
+ margin-bottom: 20px;
90
+ }
91
+
92
+ .movie-details-header h1 {
93
+ font-size: 4rem;
94
+ font-family: "Anton", sans-serif;
95
+ font-weight: 400;
96
+ font-style: normal;
97
+ margin: 0;
98
+ }
99
+
100
+ /* Action Buttons */
101
+ .movie-details-actions {
102
+ display: flex;
103
+ gap: 15px;
104
+ }
105
+
106
+ .play-button{
107
+ border: 1px solid #4339ff;
108
+ background-color: #17174a; /* Red button color */
109
+ }
110
+
111
+ .add-list-button{
112
+ background-color: #333;
113
+ }
114
+
115
+ .play-button,
116
+ .add-list-button {
117
+ color: #fff;
118
+ padding: 12px 24px;
119
+ border-radius: 5px;
120
+ cursor: pointer;
121
+ font-size: 1rem;
122
+ transition: background-color 0.3s ease, transform 0.3s ease;
123
+ }
124
+
125
+ .play-button:hover{
126
+ background-color: #27277c;
127
+ }
128
+
129
+ .add-list-button:hover {
130
+ background-color: #444;
131
+ }
132
+
133
+ .play-button:hover,
134
+ .add-list-button:hover {
135
+ transform: scale(1.05);
136
+ }
137
+
138
+ /* Poster Styling */
139
+ .movie-details-poster img {
140
+ width: 250px;
141
+ height: auto;
142
+ border-radius: 8px;
143
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
144
+ margin-bottom: 20px;
145
+ }
146
+
147
+ /* Info Section Styling */
148
+ .movie-details-info {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 20px;
152
+ }
153
+
154
+ .movie-details-metadata,
155
+ .movie-details-overview {
156
+ padding: 20px;
157
+ border-radius: 8px;
158
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
159
+ }
160
+
161
+ .movie-details-metadata p,
162
+ .movie-details-overview p {
163
+ margin: 8px 0;
164
+ line-height: 1.6;
165
+ }
166
+
167
+ .movie-details-overview h2 {
168
+ margin-bottom: 15px;
169
+ font-size: 1.5rem;
170
+ border-bottom: 2px solid #4339ff;
171
+ padding-bottom: 5px;
172
+ }
173
+
174
+ /* Responsive Design */
175
+ @media (max-width: 768px) {
176
+ .movie-details-header h1 {
177
+ font-size: 3rem;
178
+ }
179
+
180
+ .movie-details-poster img {
181
+ width: 100%;
182
+ max-width: 200px;
183
+ }
184
+
185
+ .movie-details-info {
186
+ flex-direction: column;
187
+ }
188
+ }
189
+
190
+ .geners-section{
191
+ display: flex;
192
+ text-align: center;
193
+ align-items: center;
194
+ }
195
+
196
+ .genre-list {
197
+ list-style-type: none;
198
+ padding: 0;
199
+ display: flex;
200
+ text-align: center;
201
+ justify-content: center;
202
+ align-items: center;
203
+ }
204
+
205
+ .genre-item {
206
+ margin: 5px;
207
+ }
208
+
209
+ .genre-link {
210
+ text-decoration: none;
211
+ color: #0082f3; /* Next.js blue color */
212
+ font-weight: bold;
213
+ }
214
+
215
+ .genre-link:hover {
216
+ text-decoration: underline;
217
+ }
218
+
frontend/src/app/movie/[title]/page.js ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import Link from "next/link";
4
+ import "./movie.css";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import {
7
+ faPlay,
8
+ faBookmark as faBookmarkSolid,
9
+ } from "@fortawesome/free-solid-svg-icons";
10
+ import { faBookmark as faBookmarkRegular } from "@fortawesome/free-regular-svg-icons";
11
+ import apiClient from "@/api/apiClient";
12
+
13
+ export default function MovieDetailsPage({ params }) {
14
+ const [metadata, setMetadata] = useState(null);
15
+ const [loading, setLoading] = useState(true);
16
+ const [error, setError] = useState(null);
17
+
18
+ useEffect(() => {
19
+ async function fetchData() {
20
+ try {
21
+ const decodedTitle = decodeURIComponent(params.title);
22
+ const data = await apiClient.getMovieMetadataByTitle(decodedTitle);
23
+ if (data) {
24
+ setMetadata(data);
25
+ } else {
26
+ setError("Movie metadata not found.");
27
+ }
28
+ } catch (err) {
29
+ setError("An error occurred while fetching movie metadata.");
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ }
34
+ fetchData();
35
+ }, [params.title]);
36
+
37
+ if (loading) {
38
+ return <div className="movie-details-page-container">Loading...</div>;
39
+ }
40
+
41
+ if (error) {
42
+ return <div className="movie-details-page-container">Error: {error}</div>;
43
+ }
44
+
45
+ const englishTitle =
46
+ metadata?.data?.translations?.nameTranslations?.find(
47
+ (translation) => translation.language === "eng"
48
+ )?.name ||
49
+ metadata?.data?.name ||
50
+ "Title not available";
51
+
52
+ const englishOverview =
53
+ metadata?.data?.translations?.overviewTranslations?.find(
54
+ (translation) => translation.language === "eng"
55
+ )?.overview || "Overview not available";
56
+
57
+ const backdropImage = metadata?.data?.artworks?.find(
58
+ (artwork) => artwork.type === 15
59
+ )?.image;
60
+
61
+ const genres = metadata?.data?.genres || [];
62
+
63
+ return (
64
+ <div className="movie-details-page">
65
+ <div
66
+ className="movie-details-backdrop"
67
+ style={{
68
+ backgroundImage: `
69
+ linear-gradient(to right, rgb(17 18 31 / 80%) 50%, transparent 50%),
70
+ linear-gradient(rgba(0, 0, 0, 0.1) 0%, #11121f 70%),
71
+ url("${backdropImage}")
72
+ `,
73
+ }}
74
+ ></div>
75
+
76
+ <div className="movie-details-page-container">
77
+ <div className="movie-details-header">
78
+ <h1>
79
+ {englishTitle} {"(" + metadata?.data?.year + ")" || ""}
80
+ </h1>
81
+ </div>
82
+ <div className="movie-details-poster">
83
+ <img
84
+ src={
85
+ metadata?.data?.image ||
86
+ "https://via.placeholder.com/800x450?text=No+Image+Available"
87
+ }
88
+ alt={`${englishTitle} Poster`}
89
+ />
90
+ </div>
91
+ <div className="movie-details-actions">
92
+ <Link href={"#play"}>
93
+ <button className="play-button">
94
+ <FontAwesomeIcon icon={faPlay} size="lg" /> Play
95
+ </button>
96
+ </Link>
97
+ <button className="add-list-button">
98
+ <FontAwesomeIcon icon={faBookmarkRegular} size="lg" /> MyList
99
+ </button>
100
+ </div>
101
+ <div className="movie-details-info">
102
+ <div className="movie-details-metadata">
103
+ <p className="geners-section">
104
+ <strong>Genre:</strong>{" "}
105
+ {genres.length > 0 ? (
106
+ <ul className="genre-list">
107
+ {genres.map((genre) => (
108
+ <li key={genre.id} className="genre-item">
109
+ <Link href={`/genres/${genre.name}`} passHref>
110
+ <label className="genre-link">{genre.name}</label>
111
+ </Link>
112
+ </li>
113
+ ))}
114
+ </ul>
115
+ ) : (
116
+ "Genres not available"
117
+ )}
118
+ </p>
119
+ <p>
120
+ <strong>Director / Writer:</strong> Jon Watts, Steve Ditko, Stan
121
+ Lee, Chris McKenna, Erik Sommers
122
+ </p>
123
+ <p>
124
+ <strong>Stars:</strong> Tom Holland, Angourie Rice, Samuel L.
125
+ Jackson, Zendaya, Jon Favreau, Jake Gyllenhaal, Marisa Tomei
126
+ </p>
127
+ <p>
128
+ <strong>Release Year:</strong> {metadata?.data?.year || "N/A"}
129
+ </p>
130
+ <p>
131
+ <strong>Runtime:</strong>{" "}
132
+ {metadata?.data?.runtime
133
+ ? `${metadata.data.runtime} minutes`
134
+ : "N/A"}
135
+ </p>
136
+ </div>
137
+ <div className="movie-details-overview">
138
+ <h2>Storyline</h2>
139
+ <p>{englishOverview}</p>
140
+ <p>
141
+ <strong>Content Rating:</strong>{" "}
142
+ {metadata?.data?.contentRatings?.[0]?.fullname || "Not Rated"}
143
+ </p>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ );
149
+ }
frontend/src/app/movies/page.js CHANGED
@@ -7,6 +7,7 @@ import { useFilmContext } from '@/context/FilmContext';
7
  import './filmsPage.css';
8
 
9
  import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
 
10
 
11
  const FILMS_PER_PAGE = 2;
12
 
@@ -45,7 +46,7 @@ export default function FilmsPage() {
45
  <div className="films-page-container">
46
  <div className="films-page">
47
  {currentFilms.map(title => (
48
- <MovieCard key={title} title={title} />
49
  ))}
50
  </div>
51
  <div className="pagination-controls">
 
7
  import './filmsPage.css';
8
 
9
  import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
10
+ import Link from 'next/link';
11
 
12
  const FILMS_PER_PAGE = 2;
13
 
 
46
  <div className="films-page-container">
47
  <div className="films-page">
48
  {currentFilms.map(title => (
49
+ <Link href={`/movie/${title}`}><MovieCard key={title} title={title} /></Link>
50
  ))}
51
  </div>
52
  <div className="pagination-controls">
frontend/src/app/page.js CHANGED
@@ -1,148 +1,27 @@
1
  import Head from "next/head";
2
  import HeroSection from "@/components/HeroSection";
 
3
 
4
  const HomePage = () => {
5
  return (
6
  <>
7
  <Head>
8
- <title>Home Page</title>
9
  <meta
10
  name="description"
11
  content="Home page for the streaming service"
12
  />
13
  <link rel="icon" href="/favicon.ico" />
14
  </Head>
15
- <div style={styles.container}>
16
- <HeroSection />
17
- <h2 style={styles.sectionTitle}>Popular TV Shows</h2>
18
- <div style={styles.gridContainer}>
19
- {[
20
- "https://cdn.usegalileo.ai/stability/687bdd7d-7a06-425e-b527-7e18f4fe2201.png",
21
- "https://cdn.usegalileo.ai/stability/acb62b11-0501-4e27-b6ba-83eed9b83e95.png",
22
- "https://cdn.usegalileo.ai/stability/78f55a19-d0fa-4986-a7cb-60190a3fa76f.png",
23
- "https://cdn.usegalileo.ai/stability/05bd1020-2f24-4a82-9b8f-2680fa920751.png",
24
- "https://cdn.usegalileo.ai/stability/912a274b-dd29-4b40-9140-1c1dcd76bd07.png",
25
- "https://cdn.usegalileo.ai/sdxl10/a9cf44f2-a00c-4aaa-8350-7c70cfaa4dd0.png",
26
- "https://cdn.usegalileo.ai/stability/0eee4644-0e4c-4549-8c6a-c445b3903d3c.png",
27
- "https://cdn.usegalileo.ai/sdxl10/089b78f9-0a0a-4f17-a7a1-4684c3b5efb1.png",
28
- "https://cdn.usegalileo.ai/sdxl10/f4aa01e0-cfe0-4a32-a419-9ef37149b620.png",
29
- "https://cdn.usegalileo.ai/stability/95bccea1-1d96-4395-bd11-feb0d7650b30.png",
30
- "https://cdn.usegalileo.ai/stability/4a3482f4-899f-4dc9-9806-03025979894b.png",
31
- "https://images.unsplash.com/photo-1639452127871-8c6ea78d2b11?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3w0NTI0NDl8MHwxfHNlYXJjaHw0fHxkYXJrJTIwdGhlbWV8ZW58MXx8fHwxNzI0NDkwMjIyfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080",
32
- ].map((url, index) => (
33
- <div key={index} style={styles.imageWrapper}>
34
- <div
35
- style={{ ...styles.image, backgroundImage: `url(${url})` }}
36
- />
37
- </div>
38
- ))}
39
- </div>
40
- <h2 style={styles.sectionTitle}>Trending Movies</h2>
41
- <div style={styles.gridContainer}>
42
- {[
43
- "https://cdn.usegalileo.ai/sdxl10/e410f67a-8111-44e4-8370-99ffbfeeb7f1.png",
44
- "https://cdn.usegalileo.ai/stability/953f8b85-e6b9-4906-8e47-c9d12bf6c9e6.png",
45
- "https://cdn.usegalileo.ai/stability/58480847-a20d-4ad8-8ef7-aebf592bc4fb.png",
46
- "https://cdn.usegalileo.ai/stability/e354bd6d-aa67-44be-be97-28be6bc058f0.png",
47
- "https://cdn.usegalileo.ai/sdxl10/d251f08e-3da4-4d05-937d-1cbb318ca2e4.png",
48
- "https://cdn.usegalileo.ai/stability/f870c303-f081-4ce7-b89a-5c7336dce39e.png",
49
- "https://cdn.usegalileo.ai/stability/6fad613e-3db5-436f-a4d0-e5f4651a6fe0.png",
50
- "https://cdn.usegalileo.ai/sdxl10/4b19f651-702b-4474-8800-e81184b43539.png",
51
- "https://cdn.usegalileo.ai/stability/89022a7e-44c9-42d5-b671-2d10fe48ad07.png",
52
- "https://cdn.usegalileo.ai/stability/a6873021-6f80-47de-bf2c-f4facf9a7eb7.png",
53
- "https://cdn.usegalileo.ai/stability/a49b740a-d6ae-42da-a446-5877abf5c63d.png",
54
- "https://cdn.usegalileo.ai/stability/2e6bd747-164e-4f5e-baf7-91fa3af15f25.png",
55
- ].map((url, index) => (
56
- <div key={index} style={styles.imageWrapper}>
57
- <div
58
- style={{ ...styles.image, backgroundImage: `url(${url})` }}
59
- />
60
- </div>
61
- ))}
62
  </div>
63
  </div>
64
  </>
65
  );
66
  };
67
 
68
- const styles = {
69
- container: {
70
- display: "flex",
71
- flexDirection: "column",
72
- maxWidth: "960px",
73
- margin: "0 auto",
74
- flex: 1,
75
- },
76
- heroSection: {
77
- display: "flex",
78
- minHeight: "400px",
79
- flexDirection: "column",
80
- justifyContent: "flex-end",
81
- backgroundImage:
82
- 'linear-gradient(rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.4) 100%), url("https://cdn.usegalileo.ai/sdxl10/5e86a222-949b-4c84-b77b-bec08e557e36.png")',
83
- backgroundSize: "cover",
84
- backgroundPosition: "center",
85
-
86
- padding: "0 16px 40px",
87
- borderRadius: "16px",
88
- color: "#ffffff",
89
- textAlign: "left",
90
- },
91
- heroContent: {
92
- display: "flex",
93
- flexDirection: "column",
94
- gap: "16px",
95
- },
96
- heroTitle: {
97
- fontSize: "2.5rem",
98
- fontWeight: "bold",
99
- lineHeight: "1.2",
100
- },
101
- heroSubtitle: {
102
- fontSize: "1rem",
103
- fontWeight: "normal",
104
- lineHeight: "1.4",
105
- },
106
- playButton: {
107
- display: "flex",
108
- alignItems: "center",
109
- justifyContent: "center",
110
- width: "auto",
111
- height: "40px",
112
- padding: "0 16px",
113
- borderRadius: "8px",
114
- backgroundColor: "#695ffa",
115
- color: "#ffffff",
116
- fontSize: "1rem",
117
- fontWeight: "bold",
118
- cursor: "pointer",
119
- },
120
- sectionTitle: {
121
- color: "#ffffff",
122
- fontSize: "1.4rem",
123
- fontWeight: "bold",
124
- margin: "16px 0",
125
- paddingLeft: "16px",
126
- },
127
- gridContainer: {
128
- display: "grid",
129
- gridTemplateColumns: "repeat(auto-fit, minmax(158px, 1fr))",
130
- gap: "16px",
131
- padding: "0 16px",
132
- },
133
- imageWrapper: {
134
- display: "flex",
135
- flexDirection: "column",
136
- gap: "8px",
137
- },
138
- image: {
139
- width: "100%",
140
- backgroundSize: "cover",
141
- backgroundPosition: "center",
142
- aspectRatio: "16/9",
143
- borderRadius: "16px",
144
- height: "100%",
145
- },
146
- };
147
-
148
  export default HomePage;
 
1
  import Head from "next/head";
2
  import HeroSection from "@/components/HeroSection";
3
+ import "./index.css";
4
 
5
  const HomePage = () => {
6
  return (
7
  <>
8
  <Head>
9
+ <title>Home</title>
10
  <meta
11
  name="description"
12
  content="Home page for the streaming service"
13
  />
14
  <link rel="icon" href="/favicon.ico" />
15
  </Head>
16
+ <div className="index-page">
17
+ <div className="index-page-container">
18
+ <HeroSection />
19
+ <h2 className="section-title">Popular TV Shows</h2>
20
+ <h2 className="section-title">Trending Movies</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
  </div>
23
  </>
24
  );
25
  };
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  export default HomePage;
frontend/src/app/search/page.js CHANGED
@@ -1,12 +1,16 @@
1
- "use client"; // Indicate that this file is a client component in Next.js
2
-
3
- import { useState, useEffect } from "react";
4
  import { useRouter } from "next/navigation";
5
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
- import { faSearch } from "@fortawesome/free-solid-svg-icons";
7
- import { search } from "@/api/searchApi"; // Ensure the path is correct for your API
 
 
 
 
8
  import "./searchPage.css";
9
  import MovieCard from "@/components/MovieCard";
 
10
 
11
  const SearchPage = () => {
12
  const [query, setQuery] = useState("");
@@ -14,31 +18,45 @@ const SearchPage = () => {
14
  const [loading, setLoading] = useState(false);
15
  const [error, setError] = useState(null);
16
  const [debouncedQuery, setDebouncedQuery] = useState("");
 
17
 
18
- const router = useRouter(); // Use Next.js useRouter for navigation
 
 
 
19
 
20
  useEffect(() => {
21
- const handler = setTimeout(() => {
 
 
 
 
22
  if (debouncedQuery) {
23
  handleSearch();
24
  }
25
- }, 200); // Delay of 200ms
26
 
27
  return () => {
28
- clearTimeout(handler);
 
 
29
  };
30
  }, [debouncedQuery]);
31
 
32
  const handleSearch = async () => {
33
  setLoading(true);
34
  setError(null);
 
35
  try {
36
  const data = await search(debouncedQuery);
37
  setResults(data);
38
  } catch (err) {
39
  setError("Failed to fetch search results.");
40
  } finally {
41
- setTimeout(() => setLoading(false), 500);
 
 
 
42
  }
43
  };
44
 
@@ -48,7 +66,14 @@ const SearchPage = () => {
48
  };
49
 
50
  const handleItemClick = (path) => {
51
- router.push(path); // Use router.push for navigation
 
 
 
 
 
 
 
52
  };
53
 
54
  return (
@@ -64,7 +89,7 @@ const SearchPage = () => {
64
  {loading && (
65
  <div className="loading-indicator">
66
  <div className="search-icon">
67
- <FontAwesomeIcon icon={faSearch} size="xl"/>
68
  </div>
69
  <div className="spinner"></div>
70
  </div>
@@ -74,17 +99,33 @@ const SearchPage = () => {
74
  <div className="results-container">
75
  {results.films.length > 0 && (
76
  <div className="results-section">
77
- <h2>Films</h2>
78
- <ul className="results-list">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  {results.films.map((film, index) => (
80
  <li
81
  key={index}
82
  className="result-item"
83
  onClick={() =>
84
- handleItemClick(`/film/${encodeURIComponent(film)}`)
85
  }
86
  >
87
- <MovieCard title={film}/>
88
  </li>
89
  ))}
90
  </ul>
@@ -92,8 +133,24 @@ const SearchPage = () => {
92
  )}
93
  {results.tv_series.length > 0 && (
94
  <div className="results-section">
95
- <h2>TV Series</h2>
96
- <ul className="results-list">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  {results.tv_series.map((series, index) => (
98
  <li
99
  key={index}
@@ -102,7 +159,7 @@ const SearchPage = () => {
102
  handleItemClick(`/tvshow/${encodeURIComponent(series)}`)
103
  }
104
  >
105
- {series}
106
  </li>
107
  ))}
108
  </ul>
 
1
+ "use client";
2
+ import { useState, useEffect, useRef } from "react";
 
3
  import { useRouter } from "next/navigation";
4
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import {
6
+ faSearch,
7
+ faCaretLeft,
8
+ faCaretRight,
9
+ } from "@fortawesome/free-solid-svg-icons";
10
+ import { search } from "@/api/searchApi";
11
  import "./searchPage.css";
12
  import MovieCard from "@/components/MovieCard";
13
+ import TvShowCard from "@/components/TvShowCard";
14
 
15
  const SearchPage = () => {
16
  const [query, setQuery] = useState("");
 
18
  const [loading, setLoading] = useState(false);
19
  const [error, setError] = useState(null);
20
  const [debouncedQuery, setDebouncedQuery] = useState("");
21
+ const [loadingDelay, setLoadingDelay] = useState(null);
22
 
23
+ const router = useRouter();
24
+ const timeoutRef = useRef(null);
25
+ const moviesRef = useRef(null);
26
+ const seriesRef = useRef(null);
27
 
28
  useEffect(() => {
29
+ if (timeoutRef.current) {
30
+ clearTimeout(timeoutRef.current);
31
+ }
32
+
33
+ timeoutRef.current = setTimeout(() => {
34
  if (debouncedQuery) {
35
  handleSearch();
36
  }
37
+ }, 200);
38
 
39
  return () => {
40
+ if (timeoutRef.current) {
41
+ clearTimeout(timeoutRef.current);
42
+ }
43
  };
44
  }, [debouncedQuery]);
45
 
46
  const handleSearch = async () => {
47
  setLoading(true);
48
  setError(null);
49
+
50
  try {
51
  const data = await search(debouncedQuery);
52
  setResults(data);
53
  } catch (err) {
54
  setError("Failed to fetch search results.");
55
  } finally {
56
+ if (loadingDelay) {
57
+ clearTimeout(loadingDelay);
58
+ }
59
+ setLoadingDelay(setTimeout(() => setLoading(false), 800));
60
  }
61
  };
62
 
 
66
  };
67
 
68
  const handleItemClick = (path) => {
69
+ router.push(path);
70
+ };
71
+
72
+ const scroll = (ref, direction) => {
73
+ if (ref.current) {
74
+ const scrollAmount = direction === "left" ? -400 : 400;
75
+ ref.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
76
+ }
77
  };
78
 
79
  return (
 
89
  {loading && (
90
  <div className="loading-indicator">
91
  <div className="search-icon">
92
+ <FontAwesomeIcon icon={faSearch} size="xl" />
93
  </div>
94
  <div className="spinner"></div>
95
  </div>
 
99
  <div className="results-container">
100
  {results.films.length > 0 && (
101
  <div className="results-section">
102
+ <div className="result-scroll">
103
+ <h2>Films</h2>
104
+ <div className="scroll-controls">
105
+ <button
106
+ onClick={() => scroll(moviesRef, "left")}
107
+ className="scroll-button"
108
+ >
109
+ <FontAwesomeIcon icon={faCaretLeft} size="2xl" />
110
+ </button>
111
+ <button
112
+ onClick={() => scroll(moviesRef, "right")}
113
+ className="scroll-button"
114
+ >
115
+ <FontAwesomeIcon icon={faCaretRight} size="2xl" />
116
+ </button>
117
+ </div>
118
+ </div>
119
+ <ul className="results-list" ref={moviesRef}>
120
  {results.films.map((film, index) => (
121
  <li
122
  key={index}
123
  className="result-item"
124
  onClick={() =>
125
+ handleItemClick(`/movie/${encodeURIComponent(film)}`)
126
  }
127
  >
128
+ <MovieCard title={film} />
129
  </li>
130
  ))}
131
  </ul>
 
133
  )}
134
  {results.tv_series.length > 0 && (
135
  <div className="results-section">
136
+ <div className="result-scroll">
137
+ <h2>TV Series</h2>
138
+ <div className="scroll-controls">
139
+ <button
140
+ onClick={() => scroll(seriesRef, "left")}
141
+ className="scroll-button"
142
+ >
143
+ <FontAwesomeIcon icon={faCaretLeft} size="2xl" />
144
+ </button>
145
+ <button
146
+ onClick={() => scroll(seriesRef, "right")}
147
+ className="scroll-button"
148
+ >
149
+ <FontAwesomeIcon icon={faCaretRight} size="2xl" />
150
+ </button>
151
+ </div>
152
+ </div>
153
+ <ul className="results-list" ref={seriesRef}>
154
  {results.tv_series.map((series, index) => (
155
  <li
156
  key={index}
 
159
  handleItemClick(`/tvshow/${encodeURIComponent(series)}`)
160
  }
161
  >
162
+ <TvShowCard title={series} />
163
  </li>
164
  ))}
165
  </ul>
frontend/src/app/search/searchPage.css CHANGED
@@ -1,262 +1,278 @@
1
  .search-page {
2
- background-color: #121212;
3
- color: #ffffff;
4
- min-height: 85vh;
5
- transition: background-color 0.3s ease;
6
- font-family: "Signika", sans-serif;
7
- font-optical-sizing: auto;
8
- font-style: normal;
9
- font-variation-settings: "GRAD" 0;
 
10
  }
11
 
12
- /* Search container styles */
13
  .search-container {
14
- display: flex;
15
- flex-direction: column;
16
- align-items: center;
17
- padding: 20px;
18
- transition: transform 0.3s ease;
19
  }
20
 
21
  /* Input field styling with animation */
22
  .search-input {
23
- width: 100%;
24
- max-width: 600px;
25
- padding: 10px;
26
- font-size: 16px;
27
- border: 1px solid #4339ff;
28
- border-radius: 5px;
29
- background-color: #1c2354;
30
- color: #ffffff;
31
- transition: border-color 0.3s ease, box-shadow 0.3s ease, transform .5s ease;
32
  }
33
 
34
  .search-input:focus {
35
- outline: none;
36
- box-shadow: 0 0 15px rgba(17, 0, 255, 0.6);
37
- transform: scale(1.02);
38
  }
39
 
40
  /* Loading indicator animation */
41
  .loading-indicator {
42
- margin-top: 10px;
43
- font-size: 18px;
44
- color: #4339ff;
45
- opacity: 0;
46
- animation: fadeIn .3s forwards;
47
  }
48
 
49
  @keyframes fadeIn {
50
- from {
51
- opacity: 0;
52
- }
53
- to {
54
- opacity: 1;
55
- }
56
  }
57
 
58
  /* Error message styling */
59
  .error-message {
60
- color: #ff3300;
61
- text-align: center;
62
- margin-top: 20px;
63
- animation: bounce 1s infinite;
64
  }
65
 
66
  @keyframes bounce {
67
- 0%, 20%, 50%, 80%, 100% {
68
- transform: translateY(0);
69
- }
70
- 40% {
71
- transform: translateY(-10px);
72
- }
73
- 60% {
74
- transform: translateY(-5px);
75
- }
 
 
 
 
76
  }
77
 
78
- /* Results container styles */
79
  .results-container {
80
- padding: 20px;
81
- display: flex;
82
- flex-direction: column;
83
- align-items: center;
 
84
  }
85
 
86
- /* Results section styles */
87
  .results-section {
88
- width: 100%;
89
- max-width: 800px;
90
- margin-bottom: 20px;
91
- transition: opacity 0.3s ease;
 
92
  }
93
 
94
  .results-section h2 {
95
- font-size: 24px;
96
- border-bottom: 2px solid #444;
97
- padding-bottom: 10px;
98
- margin-bottom: 15px;
99
- color: #7c81c7;
100
- animation: fadeIn 1s ease;
 
 
 
 
 
 
 
 
 
 
101
  }
102
 
103
- /* Results list styles */
104
  .results-list {
105
- list-style-type: none;
106
- padding: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
 
109
  .result-item {
110
- background-color: #1e1e1e;
111
- border: 1px solid #444;
112
- border-radius: 5px;
113
- margin-bottom: 10px;
114
- padding: 10px;
115
- transition: background-color 0.3s ease, transform 0.3s ease;
116
- animation: slideIn 0.5s ease;
 
117
  }
118
 
119
  @keyframes slideIn {
120
- from {
121
- transform: translateY(20px);
122
- opacity: 0;
123
- }
124
- to {
125
- transform: translateY(0);
126
- opacity: 1;
127
- }
128
  }
129
 
130
  .result-item:hover {
131
- background-color: #333;
132
- transform: scale(1.02);
133
  }
134
 
135
  /* Link styling */
136
  .result-link {
137
- text-decoration: none;
138
- color: #ffffff;
139
- font-size: 18px;
140
- transition: color 0.3s ease;
141
  }
142
 
143
  .result-link:hover {
144
- text-decoration: underline;
145
- color: #ff6f00;
146
- }
147
-
148
- /* Handle animations on page load */
149
- @keyframes pageLoad {
150
- from {
151
- opacity: 0;
152
- }
153
- to {
154
- opacity: 1;
155
- }
156
- }
157
- .search-page {
158
- animation: pageLoad 1s ease;
159
  }
160
 
161
  /* Loading indicator styles */
162
  .loading-indicator {
163
- display: flex;
164
- align-items: center;
165
- margin-top: 20px;
166
- font-size: 18px;
167
- position: fixed;
168
  }
169
 
170
  .search-icon {
171
- position: fixed;
172
- z-index: 100;
173
- color: #b3b7e4;
174
- animation: search 1s infinite linear;
175
  }
176
 
177
  @keyframes search {
178
- 0% {
179
- transform: translate(10px, -15px);
180
- }
181
- 5% {
182
- transform: translate(14.5px, -14.5px);
183
- }
184
- 10% {
185
- transform: translate(18.3px, -12.7px);
186
- }
187
- 15% {
188
- transform: translate(21.2px, -9.2px);
189
- }
190
- 20% {
191
- transform: translate(23px, -5px);
192
- }
193
- 25% {
194
- transform: translate(25px, 0px);
195
- }
196
- 30% {
197
- transform: translate(23px, 5px);
198
- }
199
- 35% {
200
- transform: translate(21.2px, 9.2px);
201
- }
202
- 40% {
203
- transform: translate(18.3px, 12.7px);
204
- }
205
- 45% {
206
- transform: translate(14.5px, 14.5px);
207
- }
208
- 50% {
209
- transform: translate(10px, 15px);
210
- }
211
- 55% {
212
- transform: translate(5.5px, 14.5px);
213
- }
214
- 60% {
215
- transform: translate(1.7px, 12.7px);
216
- }
217
- 65% {
218
- transform: translate(-2.2px, 9.2px);
219
- }
220
- 70% {
221
- transform: translate(-5px, 5px);
222
- }
223
- 75% {
224
- transform: translate(-5px, 0px);
225
- }
226
- 80% {
227
- transform: translate(-5px, -5px);
228
- }
229
- 85% {
230
- transform: translate(-2.2px, -9.2px);
231
- }
232
- 90% {
233
- transform: translate(1.7px, -12.7px);
234
- }
235
- 95% {
236
- transform: translate(5.5px, -14.5px);
237
- }
238
- 100% {
239
- transform: translate(10px, -15px);
240
- }
241
  }
242
 
243
  .loading-indicator .spinner {
244
- width: 40px;
245
- height: 40px;
246
- background-image: linear-gradient(#146c98, #7139ff);
247
- border: 3px solid #3939ff;
248
- border-top: 4px solid transparent;
249
- border-radius: 50%;
250
- margin-right: 10px;
251
- animation: spin 1s linear infinite;
252
  }
253
 
254
  /* Spinner animation */
255
  @keyframes spin {
256
- from {
257
- transform: rotate(0deg);
258
- }
259
- to {
260
- transform: rotate(360deg);
261
- }
262
  }
 
1
  .search-page {
2
+ background-color: #121212;
3
+ color: #ffffff;
4
+ height: 89dvh;
5
+ transition: background-color 0.3s ease;
6
+ font-family: "Signika", sans-serif;
7
+ font-optical-sizing: auto;
8
+ font-style: normal;
9
+ font-variation-settings: "GRAD" 0;
10
+ animation: pageLoad 1s ease;
11
  }
12
 
 
13
  .search-container {
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
+ padding: 20px;
18
+ transition: transform 0.3s ease;
19
  }
20
 
21
  /* Input field styling with animation */
22
  .search-input {
23
+ width: 100%;
24
+ max-width: 600px;
25
+ padding: 10px;
26
+ font-size: 16px;
27
+ border: 1px solid #4339ff;
28
+ border-radius: 5px;
29
+ background-color: #1c2354;
30
+ color: #ffffff;
31
+ transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.5s ease;
32
  }
33
 
34
  .search-input:focus {
35
+ outline: none;
36
+ box-shadow: 0 0 15px rgba(17, 0, 255, 0.6);
37
+ transform: scale(1.02);
38
  }
39
 
40
  /* Loading indicator animation */
41
  .loading-indicator {
42
+ margin-top: 10px;
43
+ font-size: 18px;
44
+ color: #4339ff;
45
+ opacity: 0;
46
+ animation: fadeIn 0.3s forwards;
47
  }
48
 
49
  @keyframes fadeIn {
50
+ from {
51
+ opacity: 0;
52
+ }
53
+ to {
54
+ opacity: 1;
55
+ }
56
  }
57
 
58
  /* Error message styling */
59
  .error-message {
60
+ color: #ff3300;
61
+ text-align: center;
62
+ margin-top: 20px;
63
+ animation: bounce 1s infinite;
64
  }
65
 
66
  @keyframes bounce {
67
+ 0%,
68
+ 20%,
69
+ 50%,
70
+ 80%,
71
+ 100% {
72
+ transform: translateY(0);
73
+ }
74
+ 40% {
75
+ transform: translateY(-10px);
76
+ }
77
+ 60% {
78
+ transform: translateY(-5px);
79
+ }
80
  }
81
 
 
82
  .results-container {
83
+ padding-left: 20px;
84
+ padding-right: 20px;
85
+ display: flex;
86
+ flex-direction: column;
87
+ align-items: center;
88
  }
89
 
 
90
  .results-section {
91
+ width: 100%;
92
+ max-width: 100%;
93
+ display: flex;
94
+ flex-direction: column;
95
+ white-space: nowrap;
96
  }
97
 
98
  .results-section h2 {
99
+ font-size: 24px;
100
+ color: #99999b;
101
+ animation: fadeIn 1s ease;
102
+ }
103
+
104
+ .result-scroll {
105
+ display: flex;
106
+ justify-content: space-between;
107
+ margin-bottom: 15px;
108
+ border-bottom: 1px solid #4a41ef;
109
+ padding-bottom: 5px;
110
+ }
111
+ .scroll-controls button {
112
+ margin-left: 10px;
113
+ margin-right: 10px;
114
+ color: #9b9a9f;
115
  }
116
 
 
117
  .results-list {
118
+ list-style-type: none;
119
+ padding: 0;
120
+ display: flex;
121
+ flex-direction: row;
122
+ margin: 0;
123
+ scroll-behavior: smooth;
124
+ overflow-x: scroll;
125
+ overflow-y: hidden;
126
+ }
127
+
128
+ .results-list::-webkit-scrollbar {
129
+ height: 8px;
130
+ }
131
+
132
+ .results-list::-webkit-scrollbar-thumb {
133
+ background-color: #3c23cf;
134
+ border-radius: 4px;
135
  }
136
 
137
  .result-item {
138
+ background-color: #1e1e1e;
139
+ border: 1px solid #585858;
140
+ border-radius: 5px;
141
+ margin-right: 10px;
142
+ transition: background-color 0.3s ease, transform 0.3s ease;
143
+ animation: slideIn 0.5s ease;
144
+ display: inline-block;
145
+ flex: 0 0 auto;
146
  }
147
 
148
  @keyframes slideIn {
149
+ from {
150
+ transform: translateY(20px);
151
+ opacity: 0;
152
+ }
153
+ to {
154
+ transform: translateY(0);
155
+ opacity: 1;
156
+ }
157
  }
158
 
159
  .result-item:hover {
160
+ background-color: #333;
161
+ transform: scale(1.02);
162
  }
163
 
164
  /* Link styling */
165
  .result-link {
166
+ text-decoration: none;
167
+ color: #ffffff;
168
+ font-size: 18px;
169
+ transition: color 0.3s ease;
170
  }
171
 
172
  .result-link:hover {
173
+ text-decoration: underline;
174
+ color: #ff6f00;
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  }
176
 
177
  /* Loading indicator styles */
178
  .loading-indicator {
179
+ display: flex;
180
+ align-items: center;
181
+ margin-top: 20px;
182
+ font-size: 18px;
183
+ position: fixed;
184
  }
185
 
186
  .search-icon {
187
+ position: fixed;
188
+ z-index: 100;
189
+ color: #b3b7e4;
190
+ animation: search 1s infinite linear;
191
  }
192
 
193
  @keyframes search {
194
+ 0% {
195
+ transform: translate(10px, -15px);
196
+ }
197
+ 5% {
198
+ transform: translate(14.5px, -14.5px);
199
+ }
200
+ 10% {
201
+ transform: translate(18.3px, -12.7px);
202
+ }
203
+ 15% {
204
+ transform: translate(21.2px, -9.2px);
205
+ }
206
+ 20% {
207
+ transform: translate(23px, -5px);
208
+ }
209
+ 25% {
210
+ transform: translate(25px, 0px);
211
+ }
212
+ 30% {
213
+ transform: translate(23px, 5px);
214
+ }
215
+ 35% {
216
+ transform: translate(21.2px, 9.2px);
217
+ }
218
+ 40% {
219
+ transform: translate(18.3px, 12.7px);
220
+ }
221
+ 45% {
222
+ transform: translate(14.5px, 14.5px);
223
+ }
224
+ 50% {
225
+ transform: translate(10px, 15px);
226
+ }
227
+ 55% {
228
+ transform: translate(5.5px, 14.5px);
229
+ }
230
+ 60% {
231
+ transform: translate(1.7px, 12.7px);
232
+ }
233
+ 65% {
234
+ transform: translate(-2.2px, 9.2px);
235
+ }
236
+ 70% {
237
+ transform: translate(-5px, 5px);
238
+ }
239
+ 75% {
240
+ transform: translate(-5px, 0px);
241
+ }
242
+ 80% {
243
+ transform: translate(-5px, -5px);
244
+ }
245
+ 85% {
246
+ transform: translate(-2.2px, -9.2px);
247
+ }
248
+ 90% {
249
+ transform: translate(1.7px, -12.7px);
250
+ }
251
+ 95% {
252
+ transform: translate(5.5px, -14.5px);
253
+ }
254
+ 100% {
255
+ transform: translate(10px, -15px);
256
+ }
257
  }
258
 
259
  .loading-indicator .spinner {
260
+ width: 40px;
261
+ height: 40px;
262
+ background-image: linear-gradient(#146c98, #7139ff);
263
+ border: 3px solid #3939ff;
264
+ border-top: 4px solid transparent;
265
+ border-radius: 50%;
266
+ margin-right: 10px;
267
+ animation: spin 1s linear infinite;
268
  }
269
 
270
  /* Spinner animation */
271
  @keyframes spin {
272
+ from {
273
+ transform: rotate(0deg);
274
+ }
275
+ to {
276
+ transform: rotate(360deg);
277
+ }
278
  }
frontend/src/components/HeroSection.css CHANGED
@@ -1,4 +1,4 @@
1
- @import url('https://fonts.googleapis.com/css2?family=New+Amsterdam&display=swap');
2
 
3
  @keyframes fadeIn {
4
  0% {
@@ -60,8 +60,8 @@
60
  .hero-title {
61
  color: white;
62
  font-size: 5rem;
63
- font-weight: 600;
64
- font-family: "New Amsterdam", sans-serif;
65
  font-style: normal;
66
  line-height: 1;
67
  transition: font-size 0.3s ease-in-out;
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Anton&display=swap');
2
 
3
  @keyframes fadeIn {
4
  0% {
 
60
  .hero-title {
61
  color: white;
62
  font-size: 5rem;
63
+ font-family: "Anton", sans-serif;
64
+ font-weight: 400;
65
  font-style: normal;
66
  line-height: 1;
67
  transition: font-size 0.3s ease-in-out;
frontend/src/components/MovieCard.css CHANGED
@@ -3,7 +3,7 @@
3
  .movie-card {
4
  position: relative;
5
  width: 150px;
6
- height: 300px;
7
  margin: 10px;
8
  border-radius: 8px;
9
  overflow: hidden;
@@ -20,22 +20,28 @@
20
  .image-container {
21
  position: relative;
22
  width: 100%;
23
- height: 78%;
24
  }
25
 
26
  .poster {
27
  object-fit: cover;
 
28
  border-radius: 8px 8px 0 0;
29
- width: 100%;
30
- height: 100%;
 
 
 
 
 
31
  }
32
 
33
  .movie-info {
34
  position: relative;
35
  width: 100%;
36
- height: 22%;
37
  padding: 10px;
38
- background: #202232;
39
  color: #fff;
40
  text-align: center;
41
  box-sizing: border-box;
 
3
  .movie-card {
4
  position: relative;
5
  width: 150px;
6
+ height: 250px;
7
  margin: 10px;
8
  border-radius: 8px;
9
  overflow: hidden;
 
20
  .image-container {
21
  position: relative;
22
  width: 100%;
23
+ height: 80%;
24
  }
25
 
26
  .poster {
27
  object-fit: cover;
28
+ object-position: top;
29
  border-radius: 8px 8px 0 0;
30
+ animation: fade-in 2s forwards;
31
+ opacity: 0;
32
+ }
33
+
34
+ @keyframes fade-in{
35
+ 0%{opacity: 0;}
36
+ 100%{opacity: 1;}
37
  }
38
 
39
  .movie-info {
40
  position: relative;
41
  width: 100%;
42
+ height: 28%;
43
  padding: 10px;
44
+ background: #1b1c24;
45
  color: #fff;
46
  text-align: center;
47
  box-sizing: border-box;
frontend/src/components/Sidebar.css CHANGED
@@ -68,7 +68,7 @@
68
  .sidebar-link {
69
  text-decoration: none;
70
  text-align: center;
71
- justify-content: center;
72
  color: #b0b0b0;
73
  display: flex;
74
  align-items: center;
@@ -115,4 +115,5 @@
115
  .sidebar-item-text {
116
  font-size: 0.875rem;
117
  font-weight: 500;
 
118
  }
 
68
  .sidebar-link {
69
  text-decoration: none;
70
  text-align: center;
71
+ justify-content: space-between;
72
  color: #b0b0b0;
73
  display: flex;
74
  align-items: center;
 
115
  .sidebar-item-text {
116
  font-size: 0.875rem;
117
  font-weight: 500;
118
+ margin-left: 10px;
119
  }
frontend/src/components/Sidebar.js CHANGED
@@ -94,7 +94,7 @@ const Sidebar = () => {
94
  onMouseEnter={handleMouseEnter}
95
  onMouseLeave={handleMouseLeave}
96
  >
97
- <SidebarItem icon={faTv} text="Series" />
98
  </Link>
99
  <Link
100
  href="/new"
 
94
  onMouseEnter={handleMouseEnter}
95
  onMouseLeave={handleMouseLeave}
96
  >
97
+ <SidebarItem icon={faTv} text="TV Shows" />
98
  </Link>
99
  <Link
100
  href="/new"
frontend/src/components/TvShowCard.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from 'react';
2
+ import Image from 'next/image';
3
+ import apiClient from '@/api/apiClient';
4
+ import SkeletonLoader from '@/skeletons/movieCard';
5
+ import './MovieCard.css';
6
+
7
+ const TvShowCard = ({ title }) => {
8
+ const [movieData, setMovieData] = useState(null);
9
+ const [loading, setLoading] = useState(true);
10
+ const [error, setError] = useState(null);
11
+
12
+ useEffect(() => {
13
+ const fetchMovieData = async () => {
14
+ try {
15
+ const data = await apiClient.getSeriesCard(title);
16
+ setMovieData(data);
17
+ } catch (err) {
18
+ setError(err.message);
19
+ } finally {
20
+ setLoading(false);
21
+ }
22
+ };
23
+
24
+ fetchMovieData();
25
+ }, [title]);
26
+
27
+ if (loading) {
28
+ return <SkeletonLoader />;
29
+ }
30
+
31
+ if (error) {
32
+ return <div className="error">Error: {error}</div>;
33
+ }
34
+
35
+ return (
36
+ <div className="movie-card">
37
+ <div className="image-container">
38
+ <Image
39
+ src={movieData.image}
40
+ alt={`${movieData.title} poster`}
41
+ fill
42
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
43
+ priority
44
+ className="poster"
45
+ />
46
+ </div>
47
+ <div className="movie-info">
48
+ <h3 className="movie-title">{movieData.title}</h3>
49
+ <p className="movie-year">{movieData.year}</p>
50
+ </div>
51
+ </div>
52
+ );
53
+ };
54
+
55
+ export default TvShowCard;
frontend/src/lib/LoadBalancer.js CHANGED
@@ -74,10 +74,13 @@ class LoadBalancerAPI {
74
  }
75
 
76
  async getGenreItems(genre, mediaType = null, limit = 5) {
77
- const queryParams = new URLSearchParams({ genre, media_type: mediaType, limit });
 
 
 
78
  return this._getRequest(`/api/get/genre?${queryParams.toString()}`);
79
  }
80
-
81
  async getDownloadProgress(url) {
82
  return this._getRequestNoBase(url);
83
  }
 
74
  }
75
 
76
  async getGenreItems(genre, mediaType = null, limit = 5) {
77
+ const queryParams = new URLSearchParams({ genre, limit });
78
+ if (mediaType) {
79
+ queryParams.append('media_type', mediaType);
80
+ }
81
  return this._getRequest(`/api/get/genre?${queryParams.toString()}`);
82
  }
83
+
84
  async getDownloadProgress(url) {
85
  return this._getRequestNoBase(url);
86
  }
frontend/src/lib/config.js CHANGED
@@ -1,7 +1,7 @@
1
  const config = {
2
  apiBaseUrl: 'https://hans-den-load-balancer.hf.space',
3
  searchUrl: 'https://unicone-studio-search.hf.space/api/search',
4
- version: "0.0.0.2 Alpha",
5
  };
6
 
7
  export default config;
 
1
  const config = {
2
  apiBaseUrl: 'https://hans-den-load-balancer.hf.space',
3
  searchUrl: 'https://unicone-studio-search.hf.space/api/search',
4
+ version: "0.0.0.3 Alpha",
5
  };
6
 
7
  export default config;
frontend/src/skeletons/movieCard.css CHANGED
@@ -1,7 +1,7 @@
1
  .skeleton-loader {
2
  position: relative;
3
  width: 150px;
4
- height: 300px;
5
  margin: 10px;
6
  border-radius: 8px;
7
  overflow: hidden;
@@ -51,13 +51,13 @@
51
 
52
  @keyframes pulse {
53
  0% {
54
- background-color: #202232;
55
  }
56
  50% {
57
- background-color: #323450;
58
  }
59
  100% {
60
- background-color: #202232;
61
  }
62
  }
63
 
 
1
  .skeleton-loader {
2
  position: relative;
3
  width: 150px;
4
+ height: 250px;
5
  margin: 10px;
6
  border-radius: 8px;
7
  overflow: hidden;
 
51
 
52
  @keyframes pulse {
53
  0% {
54
+ background-color: #1b1c24;
55
  }
56
  50% {
57
+ background-color: #323443;
58
  }
59
  100% {
60
+ background-color: #1b1c24;
61
  }
62
  }
63