ChandimaPrabath commited on
Commit
1ac95ab
·
1 Parent(s): a5cc463

added Tv Shows page

Browse files
frontend/src/App.js CHANGED
@@ -19,9 +19,11 @@ import FilmsPage from "./pages/filmsPage";
19
  import TvShowsPage from "./pages/tvshowsPage";
20
  import FilmDetailsPage from "./pages/filmDetailsPage";
21
  import { FilmProvider } from "./context/FilmContext";
 
22
  import "./App.css";
23
  import MenueModal from "./components/modals/menueModal";
24
  import FilmPlayer from "./pages/filmPlayer";
 
25
 
26
  library.add(
27
  faSquareCaretLeft,
@@ -155,6 +157,7 @@ function AppContent() {
155
  <Route path="/tvshows" element={<TvShowsPage />} />
156
  <Route path="/film/:title" element={<FilmDetailsPage />} />
157
  <Route path="/player/film/:videoUrl" element={<FilmPlayer />} />
 
158
  </Routes>
159
  </main>
160
  </div>
@@ -164,9 +167,11 @@ function AppContent() {
164
  function App() {
165
  return (
166
  <FilmProvider>
167
- <Router>
168
- <AppContent />
169
- </Router>
 
 
170
  </FilmProvider>
171
  );
172
  }
 
19
  import TvShowsPage from "./pages/tvshowsPage";
20
  import FilmDetailsPage from "./pages/filmDetailsPage";
21
  import { FilmProvider } from "./context/FilmContext";
22
+ import { TvShowsProvider } from "./context/TvShowsContext";
23
  import "./App.css";
24
  import MenueModal from "./components/modals/menueModal";
25
  import FilmPlayer from "./pages/filmPlayer";
26
+ import TvShowDetailsPage from "./pages/tvshowDetailsPage";
27
 
28
  library.add(
29
  faSquareCaretLeft,
 
157
  <Route path="/tvshows" element={<TvShowsPage />} />
158
  <Route path="/film/:title" element={<FilmDetailsPage />} />
159
  <Route path="/player/film/:videoUrl" element={<FilmPlayer />} />
160
+ <Route path="/tvshow/:title" element={<TvShowDetailsPage />} />
161
  </Routes>
162
  </main>
163
  </div>
 
167
  function App() {
168
  return (
169
  <FilmProvider>
170
+ <TvShowsProvider>
171
+ <Router>
172
+ <AppContent />
173
+ </Router>
174
+ </TvShowsProvider>
175
  </FilmProvider>
176
  );
177
  }
frontend/src/components/tv/card.css ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .tvshow-card {
2
+ box-sizing: border-box;
3
+ width: 250px;
4
+ height: 420px;
5
+ border: 1px solid #333;
6
+ background-color: #2c2c2c;
7
+ border-radius: 15px;
8
+ overflow: hidden;
9
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
10
+ transition: background-color 0.3s ease;
11
+ display: flex;
12
+ flex-direction: column;
13
+ justify-content: space-between;
14
+ margin: 10px;
15
+ position: relative;
16
+ }
17
+
18
+ .tvshow-card:hover {
19
+ background-color: #1e1e1e;
20
+ }
21
+
22
+ .tvshow-card-image {
23
+ width: 100%;
24
+ object-fit: cover;
25
+ border-bottom: 3px solid #f8b525;
26
+ transition: transform 0.5s ease;
27
+ position: relative;
28
+ z-index: 1;
29
+ }
30
+
31
+ .tvshow-card:hover .tvshow-card-image {
32
+ transform: scale(1.1);
33
+ }
34
+
35
+ .tvshow-card-overlay {
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ background: linear-gradient(to bottom, rgba(18, 36, 65, 0) 0%, rgba(2, 12, 30, 0.658) 80%);
42
+ opacity: .6;
43
+ transition: opacity 0.3s ease;
44
+ z-index: 2;
45
+ }
46
+
47
+ .tvshow-card:hover .tvshow-card-overlay {
48
+ opacity: 1;
49
+ }
50
+
51
+ .tvshow-card-info {
52
+ height: 100%;
53
+ padding: 10px;
54
+ background-color: rgba(0, 0, 0, 0.807);
55
+ border-top: 1px solid #333;
56
+ z-index: 3;
57
+ position: relative;
58
+ }
59
+
60
+ @import url('https://fonts.googleapis.com/css2?family=Calistoga&family=Pacifico&family=Rubik+Burned&family=Rubik+Marker+Hatch&family=Rubik+Maze&family=Rubik+Microbe&family=Rubik:ital,wght@0,300..900;1,300..900&family=Signika:wght@300..700&display=swap');
61
+
62
+ .tvshow-card-title {
63
+ margin: 0;
64
+ font-family: "Signika", sans-serif;
65
+ font-optical-sizing: auto;
66
+ font-style: normal;
67
+ font-variation-settings:
68
+ "GRAD" 0;
69
+ font-weight: 600;
70
+ color: #f5f5f5;
71
+ width: 100%;
72
+ box-sizing: border-box;
73
+ text-align: center;
74
+ white-space: nowrap;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+ }
78
+
79
+ .tvshow-card-year {
80
+ margin: 0;
81
+ font-size: .9em;
82
+ font-family: "Signika", sans-serif;
83
+ font-optical-sizing: auto;
84
+ font-style: normal;
85
+ font-variation-settings:
86
+ "GRAD" 0;
87
+ font-weight: 600;
88
+ color: #949494;
89
+ border: none;
90
+ width: 100%;
91
+ box-sizing: border-box;
92
+ text-align: center;
93
+ white-space: nowrap;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ }
97
+
98
+ .spinner {
99
+ display: flex;
100
+ justify-content: center;
101
+ align-items: center;
102
+ margin: auto;
103
+ }
104
+
105
+ .spinner div {
106
+ width: 15px;
107
+ height: 15px;
108
+ background-color: #ff8c00;
109
+ border-radius: 50%;
110
+ animation: spin 1s infinite ease-in-out;
111
+ margin: 0 5px;
112
+ }
113
+
114
+ .spinner div:nth-child(1) {
115
+ animation-delay: -0.32s;
116
+ }
117
+
118
+ .spinner div:nth-child(2) {
119
+ animation-delay: -0.16s;
120
+ }
121
+
122
+ @keyframes spin {
123
+ 0%, 100% {
124
+ transform: translateY(0);
125
+ }
126
+ 50% {
127
+ transform: translateY(-20px);
128
+ }
129
+ }
130
+
131
+ .tvshow-card.loading {
132
+ display: flex;
133
+ flex-direction: column;
134
+ justify-content: center;
135
+ align-items: center;
136
+ text-align: center;
137
+ background-color: #2c2c2c;
138
+ border: 1px solid #333;
139
+ border-radius: 15px;
140
+ padding: 20px;
141
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
142
+ width: 250px;
143
+ height: 420px;
144
+ position: relative;
145
+ }
146
+
147
+ .tvshow-card.loading::before {
148
+ content: '';
149
+ position: absolute;
150
+ top: 0;
151
+ left: 0;
152
+ width: 250px;
153
+ height: 420px;
154
+ background: rgba(0, 0, 0, 0.5);
155
+ border-radius: 15px;
156
+ z-index: 1;
157
+ }
158
+
159
+ .tvshow-card.loading .spinner {
160
+ margin-bottom: 20px;
161
+ }
162
+
163
+ .tvshow-card.loading .tvshow-card-title {
164
+ font-size: 1em;
165
+ color: #f5f5f5;
166
+ width: 100%;
167
+ text-align: center;
168
+ z-index: 2;
169
+ margin-top: 10px;
170
+ }
171
+
172
+ /* Media Queries for Mobile Devices */
173
+ @media (max-width: 768px) {
174
+ .tvshow-card {
175
+ width: 150px;
176
+ height: 265px;
177
+ margin: 10px 0;
178
+ }
179
+ .tvshow-card.loading{
180
+ width: 150px;
181
+ height: 265px;
182
+ margin: 10px 0;
183
+ }
184
+
185
+ .tvshow-card-info {
186
+ padding: 5px;
187
+ }
188
+
189
+ .tvshow-card-title, .tvshow-card-year {
190
+ font-size: 0.8em;
191
+
192
+ }
193
+ }
frontend/src/components/tv/card.js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import "./card.css";
4
+ import apiClient from "../../api/apiClient";
5
+
6
+ const Spinner = () => (
7
+ <div className="spinner">
8
+ <div className="spinner-bounce1"></div>
9
+ <div className="spinner-bounce2"></div>
10
+ <div className="spinner-bounce3"></div>
11
+ </div>
12
+ );
13
+
14
+ function TvShowCard({ title }) {
15
+ const [metadata, setMetadata] = useState(null);
16
+ const navigate = useNavigate();
17
+
18
+ useEffect(() => {
19
+ const fetchMetadata = async () => {
20
+ try {
21
+ const response = await apiClient.getTVMetadataByTitle(title);
22
+ const tvshowData = response.data; // Adjust based on actual API response
23
+ console.log(tvshowData);
24
+ setMetadata(tvshowData);
25
+ } catch (error) {
26
+ console.error("Failed to fetch metadata:", error);
27
+ setMetadata({ error: "Failed to load metadata" });
28
+ }
29
+ };
30
+
31
+ fetchMetadata();
32
+ }, [title]);
33
+
34
+ const handleCardClick = () => {
35
+ navigate(`/tvshow/${title}`);
36
+ };
37
+
38
+ // Function to find the English translation name
39
+ const findEnglishTranslation = (translations) => {
40
+ if (translations && Array.isArray(translations.nameTranslations)) {
41
+ // Look for the primary English translation
42
+ const primaryEngTranslation = translations.nameTranslations.find(
43
+ translation => translation.language === 'eng' && translation.isPrimary
44
+ );
45
+
46
+ if (primaryEngTranslation) {
47
+ return primaryEngTranslation.name;
48
+ }
49
+
50
+ // If no primary English translation, look for any English translation
51
+ const engTranslation = translations.nameTranslations.find(
52
+ translation => translation.language === 'eng'
53
+ );
54
+
55
+ if (engTranslation) {
56
+ return engTranslation.name;
57
+ }
58
+ }
59
+ return null;
60
+ };
61
+
62
+ // Determine the appropriate title
63
+ const eng_title = metadata?.translations ?
64
+ (findEnglishTranslation(metadata.translations) || `${metadata?.name} (${metadata?.year})`) :
65
+ `${metadata?.name} (${metadata?.year})` || title;
66
+
67
+ // Extract other relevant details from the metadata
68
+ const imageUrl = metadata?.artworks?.find(artwork => artwork.type === 7 || artwork.type === 2)?.thumbnail || "";
69
+ const year = metadata?.year;
70
+
71
+ // Display loading spinner if metadata is not available
72
+ if (metadata === null)
73
+ return (
74
+ <div className="tvshow-card loading">
75
+ <Spinner />
76
+ <div className="tvshow-card-title">Loading...</div>
77
+ </div>
78
+ );
79
+
80
+ // Display error message if metadata fetching failed
81
+ if (metadata.error)
82
+ return (
83
+ <div className="tvshow-card error">
84
+ <p className="tvshow-card-title">Error loading metadata</p>
85
+ </div>
86
+ );
87
+
88
+ return (
89
+ <div className="tvshow-card" onClick={handleCardClick}>
90
+ <img src={imageUrl} alt={eng_title} className="tvshow-card-image" />
91
+ <div className="tvshow-card-overlay"></div>
92
+ <div className="tvshow-card-info">
93
+ <p className="tvshow-card-title">{eng_title}</p>
94
+ <p className="tvshow-card-year">{year}</p>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ export default TvShowCard;
frontend/src/config.js CHANGED
@@ -1,7 +1,7 @@
1
  // src/config.js
2
  const config = {
3
  apiBaseUrl: 'https://unicone-studio-load-balancer.hf.space',
4
- version: "0.0.3 V Alpha",
5
  };
6
 
7
  export default config;
 
1
  // src/config.js
2
  const config = {
3
  apiBaseUrl: 'https://unicone-studio-load-balancer.hf.space',
4
+ version: "0.0.4 V Alpha",
5
  };
6
 
7
  export default config;
frontend/src/context/FilmContext.js CHANGED
@@ -1,4 +1,3 @@
1
- // FilmContext.js
2
  import React, { createContext, useState, useContext } from 'react';
3
 
4
  const FilmContext = createContext();
 
 
1
  import React, { createContext, useState, useContext } from 'react';
2
 
3
  const FilmContext = createContext();
frontend/src/context/TvShowsContext.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useState } from 'react';
2
+
3
+ const TvShowsContext = createContext();
4
+
5
+ export const TvShowsProvider = ({ children }) => {
6
+ const [tvshows, setTvshows] = useState([]);
7
+
8
+ return (
9
+ <TvShowsContext.Provider value={{ tvshows, setTvshows }}>
10
+ {children}
11
+ </TvShowsContext.Provider>
12
+ );
13
+ };
14
+
15
+ export const useTvShowsContext = () => {
16
+ return useContext(TvShowsContext);
17
+ };
frontend/src/pages/tvShowsPage.css ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .tvshows-page-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ padding: 20px;
6
+ max-width: 1300px;
7
+ margin: 0 auto;
8
+ }
9
+
10
+ .tvshows-page {
11
+ display: grid;
12
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
13
+ gap: 50px;
14
+ justify-items: center;
15
+ align-items: start;
16
+ width: 100%;
17
+ }
18
+
19
+ /* Media query for smaller screens */
20
+ @media (max-width: 768px) {
21
+ .tvshows-page {
22
+ grid-template-columns: repeat(auto-fit, minmax(150px, .1fr));
23
+ gap: 10px;
24
+ }
25
+ }
26
+
27
+ @media (max-width: 480px) {
28
+ .tvshows-page {
29
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
30
+ }
31
+ }
32
+
33
+ .pagination-controls {
34
+ margin-top: 20px;
35
+ display: flex;
36
+ align-items: center;
37
+ }
38
+
39
+ .pagination-button {
40
+ background-color: #2c2b2b;
41
+ color: #f5f5f5;
42
+ border: none;
43
+ border-radius: 5px;
44
+ padding: 5px;
45
+ width: 50px;
46
+ text-align: center;
47
+ margin: 0 10px;
48
+ cursor: pointer;
49
+ transition: background-color 0.3s ease, transform 0.3s ease;
50
+ }
51
+
52
+ .pagination-button:hover {
53
+ background-color: #555;
54
+ transform: scale(1.05);
55
+ }
56
+
57
+ .pagination-button:disabled {
58
+ background-color: #555;
59
+ cursor: not-allowed;
60
+ }
61
+
62
+ .page-info {
63
+ font-size: 1.2em;
64
+ color: #f5f5f5;
65
+ }
66
+
frontend/src/pages/tvshowDetailsPage.js CHANGED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback } from "react";
2
+ import { useParams } from "react-router-dom";
3
+ import apiClient from "../api/apiClient";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { faPlay, faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons";
6
+ import PlayModal from "../components/film/modals/playModal";
7
+ import { getCountryName } from "../utils";
8
+ import "./filmDetailsPage.css";
9
+
10
+ function TvShowDetailsPage() {
11
+ const { title } = useParams();
12
+ const [filmDetails, setFilmDetails] = useState(null);
13
+ const [loading, setLoading] = useState(true);
14
+ const [error, setError] = useState(null);
15
+ const [isModalOpen, setIsModalOpen] = useState(false);
16
+ const [videoUrl, setVideoUrl] = useState(null);
17
+ const [downloadProgress, setDownloadProgress] = useState(null);
18
+ const [isOverviewExpanded, setIsOverviewExpanded] = useState(false);
19
+ const [currentArtworkIndex, setCurrentArtworkIndex] = useState(0);
20
+
21
+ const fetchFilmDetails = useCallback(async () => {
22
+ try {
23
+ const response = await apiClient.getFilmMetadataByTitle(title);
24
+ if (response.data) {
25
+ const filmData = response.data;
26
+
27
+ let countryName = "";
28
+ if (filmData.originalCountry) {
29
+ countryName = await getCountryName(filmData.originalCountry);
30
+ }
31
+
32
+ setFilmDetails({
33
+ ...filmData,
34
+ originalCountry: countryName,
35
+ });
36
+ } else {
37
+ setError("No film details found.");
38
+ }
39
+ } catch (err) {
40
+ setError("Failed to fetch film details.");
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }, [title]);
45
+
46
+ const fetchVideoUrl = useCallback(async () => {
47
+ try {
48
+ const response = await apiClient.getMovieByTitle(title);
49
+ if (response.url) {
50
+ setVideoUrl(response.url);
51
+ } else if (response.progress_url) {
52
+ setDownloadProgress(response.progress_url);
53
+ }
54
+ } catch (err) {
55
+ setError("Failed to fetch video URL.");
56
+ }
57
+ }, [title]);
58
+
59
+ useEffect(() => {
60
+ fetchFilmDetails();
61
+ }, [fetchFilmDetails]);
62
+
63
+ useEffect(() => {
64
+ if (isModalOpen && !videoUrl) {
65
+ fetchVideoUrl();
66
+ }
67
+ }, [isModalOpen, videoUrl, fetchVideoUrl]);
68
+
69
+ const handlePlay = () => {
70
+ setIsModalOpen(true);
71
+ };
72
+
73
+ const handleModalClose = () => {
74
+ setIsModalOpen(false);
75
+ };
76
+
77
+ const handleOverviewToggle = () => {
78
+ setIsOverviewExpanded(!isOverviewExpanded);
79
+ };
80
+
81
+ const handleNextArtwork = () => {
82
+ if (filmDetails && previewArtworks.length > 1) {
83
+ setCurrentArtworkIndex((prevIndex) => (prevIndex + 1) % previewArtworks.length);
84
+ }
85
+ };
86
+
87
+ const handlePreviousArtwork = () => {
88
+ if (filmDetails && previewArtworks.length > 1) {
89
+ setCurrentArtworkIndex((prevIndex) =>
90
+ (prevIndex - 1 + previewArtworks.length) % previewArtworks.length
91
+ );
92
+ }
93
+ };
94
+ const findEnglishTitleTranslation = (translations) => {
95
+ if (translations && Array.isArray(translations.nameTranslations)) {
96
+ const primaryEngTranslation = translations.nameTranslations.find(
97
+ translation => translation.language === 'eng'
98
+ );
99
+
100
+ if (primaryEngTranslation) {
101
+ return primaryEngTranslation.name;
102
+ }
103
+ const engTranslation = translations.nameTranslations.find(
104
+ translation => translation.language === 'eng'
105
+ );
106
+ if (engTranslation) {
107
+ return engTranslation.name;
108
+ }
109
+ }
110
+ return null;
111
+ };
112
+
113
+ const findEnglishOverviewTranslation = (translations) => {
114
+ if (translations) {
115
+ // Look for the primary English translation
116
+ const primaryEngTranslation = translations.find(
117
+ translation => translation.language === 'eng'
118
+ );
119
+
120
+ if (primaryEngTranslation) {
121
+ return primaryEngTranslation.overview;
122
+ }
123
+
124
+ // If no primary English translation, look for any English translation
125
+ const engTranslation = translations.find(
126
+ translation => translation.language === 'eng'
127
+ );
128
+
129
+ if (engTranslation) {
130
+ return engTranslation.overview;
131
+ }
132
+ }
133
+ return null;
134
+ };
135
+
136
+ if (loading) {
137
+ return <div className="loading">Loading...</div>;
138
+ }
139
+
140
+ if (error) {
141
+ return <div className="error">{error}</div>;
142
+ }
143
+
144
+ const {
145
+ aliases = [],
146
+ translations = {},
147
+ genres = [],
148
+ year = "",
149
+ originalCountry = "",
150
+ remoteIds = [],
151
+ artworks = [],
152
+ characters = [],
153
+ } = filmDetails || {};
154
+
155
+ const thumbnailUrl =
156
+ artworks.find((artwork) => artwork.type === 7 || artwork.type === 2)?.thumbnail ||
157
+ "default-thumbnail.jpg";
158
+ const backdropUrl =
159
+ artworks.find((artwork) => artwork.type === 3)?.thumbnail ||
160
+ "default-backdrop.jpg";
161
+
162
+ const eng_title =(findEnglishTitleTranslation(translations));
163
+ const previewArtworks = artworks.filter((artwork) => artwork.type === 3);
164
+ const overviewText = findEnglishOverviewTranslation(translations.overviewTranslations) || "";
165
+ const overviewPreview = overviewText.slice(0, 150);
166
+ const showReadMore = overviewText.length > 150;
167
+ const isFirstArtwork = previewArtworks.length > 0 && currentArtworkIndex === 0;
168
+ const isLastArtwork =
169
+ previewArtworks.length > 0 && currentArtworkIndex === previewArtworks.length - 1;
170
+
171
+ return (
172
+ <div className="film-details-page">
173
+ {/* Hero Section */}
174
+ <div className="hero-section">
175
+ <div
176
+ className="film-backdrop"
177
+ style={{ backgroundImage: `url(${backdropUrl})` }}
178
+ >
179
+ <div className="film-overlay"></div>
180
+ </div>
181
+ <div className="hero-content">
182
+ <div className="hero-content-left">
183
+ <img src={thumbnailUrl} alt={aliases.name} className="film-thumbnail" />
184
+ </div>
185
+ <div className="hero-content-right">
186
+ <h1 className="film-title">{eng_title}</h1>
187
+ <div className="film-metadata">
188
+ <div className="movie-metadata-bar">
189
+ {genres.length > 0 && (
190
+ <p className="movie-genre">
191
+ <strong>Genres:</strong> {genres.map((genre) => genre.name).join(", ")}
192
+ </p>
193
+ )}
194
+ {year && (
195
+ <p>
196
+ <strong>Year:</strong> {year}
197
+ </p>
198
+ )}
199
+ {originalCountry && (
200
+ <p>
201
+ <strong>Country:</strong> {originalCountry}
202
+ </p>
203
+ )}
204
+ </div>
205
+ {translations.overviewTranslations && translations.overviewTranslations.length > 0 && (
206
+ <p className="movie-overview">
207
+ <strong>Overview:</strong> {isOverviewExpanded ? overviewText : overviewPreview}
208
+ {showReadMore && (
209
+ <button onClick={handleOverviewToggle} className="overview-toggle">
210
+ {isOverviewExpanded ? "Read Less" : "Read More"}
211
+ </button>
212
+ )}
213
+ </p>
214
+ )}
215
+ </div>
216
+ <button onClick={handlePlay} className="play-button">
217
+ <FontAwesomeIcon icon={faPlay} size="2x" />
218
+ <span>Play</span>
219
+ </button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ {/* Preview Section */}
224
+ {previewArtworks.length > 0 && (
225
+ <div className="artwork-preview">
226
+ <h2>Preview</h2>
227
+ <div className="artwork-container">
228
+ <button
229
+ onClick={handlePreviousArtwork}
230
+ className="nav-button left"
231
+ disabled={isFirstArtwork}
232
+ >
233
+ <FontAwesomeIcon icon={faChevronLeft} />
234
+ </button>
235
+ <img
236
+ src={previewArtworks[currentArtworkIndex]?.thumbnail || "default-thumbnail.jpg"}
237
+ alt="Artwork"
238
+ className="artwork-image"
239
+ />
240
+ <button
241
+ onClick={handleNextArtwork}
242
+ className="nav-button right"
243
+ disabled={isLastArtwork}
244
+ >
245
+ <FontAwesomeIcon icon={faChevronRight} />
246
+ </button>
247
+ </div>
248
+ </div>
249
+ )}
250
+
251
+ {characters?.length > 0 && (
252
+ <div className="film-cast-crew">
253
+ <h2>Cast</h2>
254
+ <div className="cast-list">
255
+ {characters
256
+ .filter((character) => character.type === 3)
257
+ .map((character) => (
258
+ <div className="cast-member" key={character.id}>
259
+ <img
260
+ src={
261
+ character.personImgURL ||
262
+ `https://eu.ui-avatars.com/api/?name=${encodeURIComponent(
263
+ character.personName
264
+ )}&size=250`
265
+ }
266
+ alt={character.personName}
267
+ className="cast-photo"
268
+ />
269
+ <p className="cast-name">{character.personName}</p>
270
+ </div>
271
+ ))}
272
+ </div>
273
+ </div>
274
+ )}
275
+
276
+ {/* Additional Information */}
277
+ <div className="additional-info">
278
+ {remoteIds.length > 0 && (
279
+ <div className="external-links">
280
+ <strong>External Links:</strong>
281
+ <ul>
282
+ {remoteIds.map((id, index) => (
283
+ <li key={index}>
284
+ <a
285
+ href={
286
+ id.type === 2
287
+ ? `https://www.imdb.com/title/${id.id}`
288
+ : `https://www.themoviedb.org/movie/${id.id}`
289
+ }
290
+ target="_blank"
291
+ rel="noopener noreferrer"
292
+ >
293
+ {id.sourceName}
294
+ </a>
295
+ </li>
296
+ ))}
297
+ </ul>
298
+ </div>
299
+ )}
300
+ </div>
301
+
302
+ {/* Modal for Play */}
303
+ <PlayModal
304
+ isOpen={isModalOpen}
305
+ onRequestClose={handleModalClose}
306
+ videoUrl={videoUrl}
307
+ setVideoUrl={setVideoUrl}
308
+ downloadProgress={downloadProgress}
309
+ title={title}
310
+ />
311
+ </div>
312
+ );
313
+ }
314
+
315
+ export default TvShowDetailsPage;
frontend/src/pages/tvshowsPage.js CHANGED
@@ -1,12 +1,87 @@
1
- import React from 'react';
 
 
 
 
 
 
 
2
 
3
  function TvShowsPage() {
4
- return (
5
- <div>
6
- <h1>Tv Shows Page</h1>
7
- <p>Welcome to the Tv Shows Page</p>
8
- </div>
9
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
 
12
  export default TvShowsPage;
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
+ import apiClient from '../api/apiClient';
4
+ import { useTvShowsContext } from '../context/TvShowsContext';
5
+ import './tvShowsPage.css';
6
+ import TvShowCard from '../components/tv/card';
7
+
8
+ const TVSHOWS_PER_PAGE = 10;
9
 
10
  function TvShowsPage() {
11
+ const { tvshows, setTvshows } = useTvShowsContext();
12
+ const [currentPage, setCurrentPage] = useState(1);
13
+
14
+ useEffect(() => {
15
+ if (tvshows.length === 0) {
16
+ apiClient.getAllTVShows()
17
+ .then(response => {
18
+ console.log('All tvshows:', response);
19
+
20
+ // Transform the response data to get unique show titles
21
+ const uniqueTvShows = Object.keys(response);
22
+ console.log(uniqueTvShows);
23
+ setTvshows(uniqueTvShows);
24
+ })
25
+ .catch(error => {
26
+ console.error('Failed to get all tvshows:', error);
27
+ });
28
+ }
29
+ }, [tvshows, setTvshows]);
30
+
31
+ const startIndex = (currentPage - 1) * TVSHOWS_PER_PAGE;
32
+ const currentTvShows = tvshows.slice(startIndex, startIndex + TVSHOWS_PER_PAGE);
33
+
34
+ const handleNextPage = () => {
35
+ setCurrentPage(prevPage => prevPage + 1);
36
+ };
37
+
38
+ const handlePrevPage = () => {
39
+ setCurrentPage(prevPage => Math.max(prevPage - 1, 1));
40
+ };
41
+
42
+ const totalPages = Math.ceil(tvshows.length / TVSHOWS_PER_PAGE);
43
+
44
+ const isPrevButtonEnabled = currentPage > 1;
45
+ const isNextButtonEnabled = currentPage < totalPages;
46
+
47
+ return (
48
+ <div className="tvshows-page-container">
49
+ <div className="pagination-controls">
50
+ <button
51
+ onClick={handlePrevPage}
52
+ disabled={!isPrevButtonEnabled}
53
+ className="pagination-button"
54
+ >
55
+ <FontAwesomeIcon
56
+ icon="fa-regular fa-square-caret-left"
57
+ size="2xl"
58
+ color='#e88f36'
59
+ bounce={isPrevButtonEnabled}
60
+ />
61
+ </button>
62
+ <span className="page-info">
63
+ {currentPage} - {totalPages}
64
+ </span>
65
+ <button
66
+ onClick={handleNextPage}
67
+ disabled={!isNextButtonEnabled}
68
+ className="pagination-button"
69
+ >
70
+ <FontAwesomeIcon
71
+ icon="fa-regular fa-square-caret-right"
72
+ size="2xl"
73
+ color='#e88f36'
74
+ bounce={isNextButtonEnabled}
75
+ />
76
+ </button>
77
+ </div>
78
+ <div className="tvshows-page">
79
+ {currentTvShows.map(showTitle => (
80
+ <TvShowCard key={showTitle} title={showTitle} />
81
+ ))}
82
+ </div>
83
+ </div>
84
+ );
85
  }
86
 
87
  export default TvShowsPage;