ChandimaPrabath commited on
Commit
03769d0
1 Parent(s): 17406e2
frontend/next.config.mjs CHANGED
@@ -1,4 +1,8 @@
1
  /** @type {import('next').NextConfig} */
2
- const nextConfig = {};
 
 
 
 
3
 
4
  export default nextConfig;
 
1
  /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ images: {
4
+ domains: ['artworks.thetvdb.com'],
5
+ },
6
+ };
7
 
8
  export default nextConfig;
frontend/src/app/films/filmsPage.css DELETED
@@ -1,78 +0,0 @@
1
- .films-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
- .films-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
- .films-page {
22
- grid-template-columns: repeat(auto-fit, minmax(150px, .1fr));
23
- gap: 10px;
24
- }
25
- }
26
-
27
- @media (max-width: 480px) {
28
- .films-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: #21264a;
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: #202a75;
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
-
67
- /* Handle animations on page load */
68
- @keyframes pageLoad {
69
- from {
70
- opacity: 0;
71
- }
72
- to {
73
- opacity: 1;
74
- }
75
- }
76
- .films-page-container {
77
- animation: pageLoad 1s ease;
78
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/app/films/page.js DELETED
@@ -1,87 +0,0 @@
1
- 'use client';
2
- import { useEffect, useState } from 'react';
3
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4
- import apiClient from '@/api/apiClient'; // Updated for Next.js absolute import
5
- import FilmCard from '@/components/film/card'; // Updated for Next.js absolute import
6
- import { useFilmContext } from '@/context/FilmContext'; // Updated for Next.js absolute import
7
- import './filmsPage.css'; // Updated for Next.js absolute import
8
-
9
- import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
10
- import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
11
-
12
- const FILMS_PER_PAGE = 2;
13
-
14
- export default function FilmsPage() {
15
- const { films, setFilms } = useFilmContext();
16
- const [currentPage, setCurrentPage] = useState(1);
17
-
18
- useEffect(() => {
19
- if (films.length === 0) {
20
- apiClient.getAllFilms()
21
- .then(response => {
22
- console.log('All films:', response);
23
- setFilms(response.map(film => film.replace('films/', '')));
24
- })
25
- .catch(error => {
26
- console.error('Failed to get all films:', error);
27
- });
28
- }
29
- }, [films, setFilms]);
30
-
31
- const startIndex = (currentPage - 1) * FILMS_PER_PAGE;
32
- const currentFilms = films.slice(startIndex, startIndex + FILMS_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
- // Calculate total number of pages
43
- const totalPages = Math.ceil(films.length / FILMS_PER_PAGE);
44
-
45
- // Determine if the previous and next buttons should be enabled
46
- const isPrevButtonEnabled = currentPage > 1;
47
- const isNextButtonEnabled = currentPage < totalPages;
48
-
49
- return (
50
- <div className="films-page-container">
51
- <div className="pagination-controls">
52
- <button
53
- onClick={handlePrevPage}
54
- disabled={!isPrevButtonEnabled}
55
- className="pagination-button"
56
- >
57
- <FontAwesomeIcon
58
- icon={faCaretLeft}
59
- size="2xl"
60
- color='#3f5fd2'
61
- bounce={isPrevButtonEnabled}
62
- />
63
- </button>
64
- <span className="page-info">
65
- {currentPage} - {totalPages}
66
- </span>
67
- <button
68
- onClick={handleNextPage}
69
- disabled={!isNextButtonEnabled}
70
- className="pagination-button"
71
- >
72
- <FontAwesomeIcon
73
- icon={faCaretRight}
74
- size="2xl"
75
- color='#3f5fd2'
76
- bounce={isNextButtonEnabled}
77
- />
78
- </button>
79
- </div>
80
- <div className="films-page">
81
- {currentFilms.map(title => (
82
- <FilmCard key={title} title={title} />
83
- ))}
84
- </div>
85
- </div>
86
- );
87
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/app/movies/filmsPage.css ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Global Styles */
2
+ body {
3
+ background-color: #121212;
4
+ color: #e0e0e0;
5
+ font-family: Arial, sans-serif;
6
+ }
7
+
8
+ /* Films Page Container */
9
+ .films-page-container {
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ padding: 20px;
14
+ max-width: 1200px;
15
+ margin: 0 auto;
16
+ }
17
+
18
+ /* Films Grid */
19
+ .films-page {
20
+ display: grid;
21
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
22
+ gap: 30px;
23
+ width: 100%;
24
+ justify-content: center;
25
+ }
26
+
27
+ /* Media Queries for Responsiveness */
28
+ @media (max-width: 768px) {
29
+ .films-page {
30
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
31
+ gap: 15px;
32
+ }
33
+ }
34
+
35
+ @media (max-width: 480px) {
36
+ .films-page {
37
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
38
+ }
39
+ }
40
+
41
+ /* Pagination Controls */
42
+ .pagination-controls {
43
+ margin-top: 20px;
44
+ display: flex;
45
+ align-items: center;
46
+ }
47
+
48
+ /* Pagination Button */
49
+ .pagination-button {
50
+ background-color: #3f0071;
51
+ color: #ffffff;
52
+ border: none;
53
+ border-radius: 50%;
54
+ padding: 10px;
55
+ width: 50px;
56
+ height: 50px;
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ cursor: pointer;
61
+ transition: background-color 0.3s, transform 0.3s;
62
+ }
63
+
64
+ .pagination-button.enabled:hover {
65
+ background-color: #5c0097;
66
+ transform: scale(1.1);
67
+ }
68
+
69
+ .pagination-button.disabled {
70
+ background-color: #333;
71
+ cursor: not-allowed;
72
+ }
73
+
74
+ /* Page Info */
75
+ .page-info {
76
+ font-size: 1.2em;
77
+ margin: 0 15px;
78
+ color: #e0e0e0;
79
+ }
80
+
81
+ /* Handle Animations on Page Load */
82
+ @keyframes pageLoad {
83
+ from {
84
+ opacity: 0;
85
+ }
86
+ to {
87
+ opacity: 1;
88
+ }
89
+ }
90
+
91
+ .films-page-container {
92
+ animation: pageLoad 1s ease;
93
+ }
frontend/src/app/movies/page.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4
+ import apiClient from '@/api/apiClient';
5
+ import MovieCard from '@/components/MovieCard';
6
+ 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 = 10;
12
+
13
+ export default function FilmsPage() {
14
+ const { films, setFilms } = useFilmContext();
15
+ const [currentPage, setCurrentPage] = useState(1);
16
+
17
+ useEffect(() => {
18
+ if (films.length === 0) {
19
+ apiClient.getAllMovies()
20
+ .then(response => {
21
+ setFilms(response.map(film => film.replace('films/', '')));
22
+ })
23
+ .catch(error => {
24
+ console.error('Failed to get all films:', error);
25
+ });
26
+ }
27
+ }, [films, setFilms]);
28
+
29
+ const startIndex = (currentPage - 1) * FILMS_PER_PAGE;
30
+ const currentFilms = films.slice(startIndex, startIndex + FILMS_PER_PAGE);
31
+
32
+ const handleNextPage = () => {
33
+ setCurrentPage(prevPage => prevPage + 1);
34
+ };
35
+
36
+ const handlePrevPage = () => {
37
+ setCurrentPage(prevPage => Math.max(prevPage - 1, 1));
38
+ };
39
+
40
+ const totalPages = Math.ceil(films.length / FILMS_PER_PAGE);
41
+ const isPrevButtonEnabled = currentPage > 1;
42
+ const isNextButtonEnabled = currentPage < totalPages;
43
+
44
+ return (
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">
52
+ <button
53
+ onClick={handlePrevPage}
54
+ disabled={!isPrevButtonEnabled}
55
+ className={`pagination-button ${isPrevButtonEnabled ? 'enabled' : 'disabled'}`}
56
+ >
57
+ <FontAwesomeIcon
58
+ icon={faChevronLeft}
59
+ size="2xl"
60
+ />
61
+ </button>
62
+ <span className="page-info">
63
+ {currentPage} of {totalPages}
64
+ </span>
65
+ <button
66
+ onClick={handleNextPage}
67
+ disabled={!isNextButtonEnabled}
68
+ className={`pagination-button ${isNextButtonEnabled ? 'enabled' : 'disabled'}`}
69
+ >
70
+ <FontAwesomeIcon
71
+ icon={faChevronRight}
72
+ size="2xl"
73
+ />
74
+ </button>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
frontend/src/components/MovieCard.css ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* styles/MovieCard.css */
2
+
3
+ .movie-card {
4
+ position: relative;
5
+ width: 150px;
6
+ height: 300px;
7
+ margin: 10px;
8
+ border-radius: 8px;
9
+ overflow: hidden;
10
+ background-color: #202232;
11
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
12
+ cursor: pointer;
13
+ transition: transform 0.3s ease;
14
+ display: flex;
15
+ flex-direction: column;
16
+ opacity: 0;
17
+ animation: fadeIn 0.5s forwards;
18
+ }
19
+
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;
42
+ display: flex;
43
+ flex-direction: column;
44
+ justify-content: center;
45
+ }
46
+
47
+ .movie-title {
48
+ margin: 0;
49
+ font-size: 16px;
50
+ font-weight: bold;
51
+ overflow: hidden;
52
+ text-overflow: ellipsis;
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .movie-year {
57
+ margin: 5px 0 0;
58
+ font-size: 14px;
59
+ overflow: hidden;
60
+ text-overflow: ellipsis;
61
+ white-space: nowrap;
62
+ }
63
+
64
+ .error {
65
+ color: #e74c3c;
66
+ }
67
+
68
+ @keyframes fadeIn {
69
+ from {
70
+ opacity: 0;
71
+ }
72
+ to {
73
+ opacity: 1;
74
+ }
75
+ }
76
+
frontend/src/components/MovieCard.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 MovieCard = ({ 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.getMovieCard(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 MovieCard;
frontend/src/components/Sidebar.js CHANGED
@@ -65,9 +65,9 @@ const Sidebar = () => {
65
  <SidebarItem icon={faHome} text="Home" />
66
  </Link>
67
  <Link
68
- href="/films"
69
  className={`sidebar-link ${
70
- pathname === "/films" ? "active" : ""
71
  }`}
72
  onMouseEnter={handleMouseEnter}
73
  onMouseLeave={handleMouseLeave}
 
65
  <SidebarItem icon={faHome} text="Home" />
66
  </Link>
67
  <Link
68
+ href="/movies"
69
  className={`sidebar-link ${
70
+ pathname === "/movies" ? "active" : ""
71
  }`}
72
  onMouseEnter={handleMouseEnter}
73
  onMouseLeave={handleMouseLeave}
frontend/src/components/film/card.css DELETED
@@ -1,193 +0,0 @@
1
- .film-card {
2
- box-sizing: border-box;
3
- width: 220px;
4
- height: 390px;
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
- .film-card:hover {
19
- background-color: #1e1e1e;
20
- }
21
-
22
- .film-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
- .film-card:hover .film-card-image {
32
- transform: scale(1.1);
33
- }
34
-
35
- .film-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
- .film-card:hover .film-card-overlay {
48
- opacity: 1;
49
- }
50
-
51
- .film-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:[email protected]&display=swap');
61
-
62
- .film-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
- .film-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
- .film-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
- .film-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
- .film-card.loading .spinner {
160
- margin-bottom: 20px;
161
- }
162
-
163
- .film-card.loading .film-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
- .film-card {
175
- width: 150px;
176
- height: 265px;
177
- margin: 10px 0;
178
- }
179
- .film-card.loading{
180
- width: 150px;
181
- height: 265px;
182
- margin: 10px 0;
183
- }
184
-
185
- .film-card-info {
186
- padding: 5px;
187
- }
188
-
189
- .film-card-title, .film-card-year {
190
- font-size: 0.8em;
191
-
192
- }
193
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/film/card.js DELETED
@@ -1,92 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { useRouter } from 'next/navigation'; // Use the correct import for Next.js 13
3
- import apiClient from "@/api/apiClient";
4
- import './card.css';
5
-
6
- // Spinner component
7
- const Spinner = () => (
8
- <div className="spinner">
9
- <div className="spinner-bounce1"></div>
10
- <div className="spinner-bounce2"></div>
11
- <div className="spinner-bounce3"></div>
12
- </div>
13
- );
14
-
15
- export default function FilmCard({ title }) {
16
- const [metadata, setMetadata] = useState(null);
17
- const router = useRouter(); // Use Next.js router
18
-
19
- useEffect(() => {
20
- const fetchMetadata = async () => {
21
- try {
22
- const response = await apiClient.getFilmMetadataByTitle(title);
23
- const filmData = response.data; // Adjust based on actual API response
24
- console.log(filmData);
25
- setMetadata(filmData);
26
- } catch (error) {
27
- console.error("Failed to fetch metadata:", error);
28
- setMetadata({ error: "Failed to load metadata" });
29
- }
30
- };
31
-
32
- fetchMetadata();
33
- }, [title]);
34
-
35
- const handleCardClick = () => {
36
- router.push(`/film/${title}`); // Use Next.js router
37
- };
38
-
39
- const findEnglishTranslation = (translations) => {
40
- if (translations && Array.isArray(translations.nameTranslations)) {
41
- const primaryEngTranslation = translations.nameTranslations.find(
42
- translation => translation.language === 'eng' && translation.isPrimary
43
- );
44
-
45
- if (primaryEngTranslation) {
46
- return primaryEngTranslation.name;
47
- }
48
-
49
- const engTranslation = translations.nameTranslations.find(
50
- translation => translation.language === 'eng'
51
- );
52
-
53
- if (engTranslation) {
54
- return engTranslation.name;
55
- }
56
- }
57
- return null;
58
- };
59
-
60
- const eng_title = metadata?.translations ?
61
- (findEnglishTranslation(metadata.translations) || `${metadata?.name} (${metadata?.year})`) :
62
- `${metadata?.name} (${metadata?.year})` || title;
63
-
64
- const imageUrl = metadata?.artworks?.find(artwork => artwork.type === 14)?.thumbnail || "";
65
- const year = metadata?.year;
66
-
67
- if (metadata === null)
68
- return (
69
- <div className="film-card loading">
70
- <Spinner />
71
- <div className="film-card-title">Loading...</div>
72
- </div>
73
- );
74
-
75
- if (metadata.error)
76
- return (
77
- <div className="film-card error">
78
- <p className="film-card-title">Error loading metadata</p>
79
- </div>
80
- );
81
-
82
- return (
83
- <div className="film-card" onClick={handleCardClick}>
84
- <img src={imageUrl} alt={eng_title} className="film-card-image" />
85
- <div className="film-card-overlay"></div>
86
- <div className="film-card-info">
87
- <p className="film-card-title">{eng_title}</p>
88
- <p className="film-card-year">{year}</p>
89
- </div>
90
- </div>
91
- );
92
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/lib/LoadBalancer.js CHANGED
@@ -1,124 +1,137 @@
1
  class LoadBalancerAPI {
2
- constructor(baseURL) {
3
- this.baseURL = baseURL;
4
- this.filmCache = null; // Cache for film store
5
- this.tvCache = null; // Cache for TV store
6
- }
7
-
8
- async getInstances() {
9
- return this._getRequest('/api/get/instances');
10
- }
11
-
12
- async getInstancesHealth() {
13
- return this._getRequest('/api/get/instances/health');
14
- }
15
-
16
- async getMovieByTitle(title) {
17
- return this._getRequest(`/api/get/movie/${encodeURIComponent(title)}`);
18
- }
19
-
20
- async getSeriesEpisode(title, season, episode) {
21
- return this._getRequest(`/api/get/series/${encodeURIComponent(title)}/${season}/${episode}`);
22
- }
23
-
24
- async getSeriesStore() {
25
- const response = await this._getRequest('/api/get/series/store');
26
-
27
- if (response && Object.keys(response).length > 0) {
28
- this.tvCache = response; // Update cache if response is not empty
29
- }
30
-
31
- return this.tvCache || {}; // Return cache if response is empty
32
- }
33
-
34
- async getMovieStore() {
35
- const response = await this._getRequest('/api/get/movie/store');
36
-
37
- if (response && Object.keys(response).length > 0) {
38
- this.filmCache = response; // Update cache if response is not empty
39
- }
40
-
41
- return this.filmCache || {}; // Return cache if response is empty
42
- }
43
-
44
- async getMovieMetadataByTitle(title) {
45
- return this._getRequest(`/api/get/movie/metadata/${encodeURIComponent(title)}`);
46
- }
47
-
48
- async getSeriesMetadataByTitle(title) {
49
- return this._getRequest(`/api/get/series/metadata/${encodeURIComponent(title)}`);
50
- }
51
 
52
- async getSeasonMetadataBySeriesId(seires_id, season) {
53
- return this._getRequest(`/api/get/series/metadata/${seires_id}/${season}`);
54
- }
55
-
56
- async getAllMovies() {
57
- return this._getRequest('/api/get/film/all');
58
- }
59
-
60
- async getAllSeriesShows() {
61
- return this._getRequest('/api/get/series/all');
62
- }
63
 
64
- async getRecent() {
65
- return this._getRequest('/api/get/recent');
66
- }
67
 
68
- async getDownloadProgress(url) {
69
- return this._getRequestNoBase(url);
70
- }
71
-
72
- // Helper methods for GET and POST requests
73
- async _getRequest(endpoint) {
74
- try {
75
- const response = await fetch(`${this.baseURL}${endpoint}`, {
76
- method: 'GET',
77
- headers: { 'Content-Type': 'application/json' },
78
- });
79
- console.log(`api endpoint: ${this.baseURL}${endpoint}`);
80
- return await this._handleResponse(response);
81
- } catch (error) {
82
- console.error(`Error during GET request to ${endpoint}:`, error);
83
- throw error;
84
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
-
87
- async _postRequest(endpoint, body) {
88
- try {
89
- const response = await fetch(`${this.baseURL}${endpoint}`, {
90
- method: 'POST',
91
- headers: { 'Content-Type': 'application/json' },
92
- body: JSON.stringify(body),
93
- });
94
- return await this._handleResponse(response);
95
- } catch (error) {
96
- console.error(`Error during POST request to ${endpoint}:`, error);
97
- throw error;
98
- }
99
  }
100
-
101
- async _handleResponse(response) {
102
- if (!response.ok) {
103
- const errorDetails = await response.text();
104
- throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
105
- }
106
- return response.json();
107
  }
108
-
109
- async _getRequestNoBase(url) {
110
- try {
111
- const response = await fetch(`${url}`, {
112
- method: 'GET',
113
- headers: { 'Content-Type': 'application/json' },
114
- });
115
- console.log(`api endpoint: ${url}`);
116
- return await this._handleResponse(response);
117
- } catch (error) {
118
- console.error(`Error during GET request to ${url}:`, error);
119
- throw error;
120
- }
 
121
  }
122
  }
123
-
124
- export { LoadBalancerAPI };
 
 
1
  class LoadBalancerAPI {
2
+ constructor(baseURL) {
3
+ this.baseURL = baseURL;
4
+ this.filmCache = null; // Cache for film store
5
+ this.tvCache = null; // Cache for TV store
6
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ async getInstances() {
9
+ return this._getRequest('/api/get/instances');
10
+ }
 
 
 
 
 
 
 
 
11
 
12
+ async getInstancesHealth() {
13
+ return this._getRequest('/api/get/instances/health');
14
+ }
15
 
16
+ async getMovieByTitle(title) {
17
+ return this._getRequest(`/api/get/movie/${encodeURIComponent(title)}`);
18
+ }
19
+
20
+ async getSeriesEpisode(title, season, episode) {
21
+ return this._getRequest(`/api/get/series/${encodeURIComponent(title)}/${season}/${episode}`);
22
+ }
23
+
24
+ async getSeriesStore() {
25
+ const response = await this._getRequest('/api/get/series/store');
26
+
27
+ if (response && Object.keys(response).length > 0) {
28
+ this.tvCache = response; // Update cache if response is not empty
29
+ }
30
+
31
+ return this.tvCache || {}; // Return cache if response is empty
32
+ }
33
+
34
+ async getMovieStore() {
35
+ const response = await this._getRequest('/api/get/movie/store');
36
+
37
+ if (response && Object.keys(response).length > 0) {
38
+ this.filmCache = response; // Update cache if response is not empty
39
+ }
40
+
41
+ return this.filmCache || {}; // Return cache if response is empty
42
+ }
43
+
44
+ async getMovieMetadataByTitle(title) {
45
+ return this._getRequest(`/api/get/movie/metadata/${encodeURIComponent(title)}`);
46
+ }
47
+
48
+ async getMovieCard(title) {
49
+ return this._getRequest(`/api/get/movie/card/${encodeURIComponent(title)}`);
50
+ }
51
+
52
+ async getSeriesMetadataByTitle(title) {
53
+ return this._getRequest(`/api/get/series/metadata/${encodeURIComponent(title)}`);
54
+ }
55
+
56
+ async getSeriesCard(title) {
57
+ return this._getRequest(`/api/get/series/card/${encodeURIComponent(title)}`);
58
+ }
59
+
60
+ async getSeasonMetadataBySeriesId(series_id, season) {
61
+ return this._getRequest(`/api/get/series/metadata/${series_id}/${season}`);
62
+ }
63
+
64
+ async getAllMovies() {
65
+ return this._getRequest('/api/get/movie/all');
66
+ }
67
+
68
+ async getAllSeriesShows() {
69
+ return this._getRequest('/api/get/series/all');
70
+ }
71
+
72
+ async getRecent() {
73
+ return this._getRequest('/api/get/recent');
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
+ }
84
+
85
+ // Helper methods for GET and POST requests
86
+ async _getRequest(endpoint) {
87
+ try {
88
+ const response = await fetch(`${this.baseURL}${endpoint}`, {
89
+ method: 'GET',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ });
92
+ console.log(`api endpoint: ${this.baseURL}${endpoint}`);
93
+ return await this._handleResponse(response);
94
+ } catch (error) {
95
+ console.error(`Error during GET request to ${endpoint}:`, error);
96
+ throw error;
97
  }
98
+ }
99
+
100
+ async _postRequest(endpoint, body) {
101
+ try {
102
+ const response = await fetch(`${this.baseURL}${endpoint}`, {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(body),
106
+ });
107
+ return await this._handleResponse(response);
108
+ } catch (error) {
109
+ console.error(`Error during POST request to ${endpoint}:`, error);
110
+ throw error;
111
  }
112
+ }
113
+
114
+ async _handleResponse(response) {
115
+ if (!response.ok) {
116
+ const errorDetails = await response.text();
117
+ throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
 
118
  }
119
+ return response.json();
120
+ }
121
+
122
+ async _getRequestNoBase(url) {
123
+ try {
124
+ const response = await fetch(`${url}`, {
125
+ method: 'GET',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ });
128
+ console.log(`api endpoint: ${url}`);
129
+ return await this._handleResponse(response);
130
+ } catch (error) {
131
+ console.error(`Error during GET request to ${url}:`, error);
132
+ throw error;
133
  }
134
  }
135
+ }
136
+
137
+ export { LoadBalancerAPI };
frontend/src/skeletons/movieCard.css ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .skeleton-loader {
2
+ position: relative;
3
+ width: 150px;
4
+ height: 300px;
5
+ margin: 10px;
6
+ border-radius: 8px;
7
+ overflow: hidden;
8
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
9
+ cursor: pointer;
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ animation: pulse 1.5s infinite ease-in-out;
14
+ }
15
+
16
+ .image-container {
17
+ width: 100%;
18
+ height: 80%;
19
+ border-radius: 8px 8px 0 0;
20
+ animation: pulse 1.5s infinite ease-in-out;
21
+ }
22
+
23
+ .movie-info {
24
+ width: 100%;
25
+ height: 20%;
26
+ padding: 10px;
27
+ color: #fff;
28
+ text-align: center;
29
+ box-sizing: border-box;
30
+ display: flex;
31
+ flex-direction: column;
32
+ justify-content: center;
33
+ }
34
+
35
+ .skeleton-title, .skeleton-year {
36
+ border-radius: 4px;
37
+ margin: 0 auto;
38
+ animation: pulse 1.5s infinite ease-in-out;
39
+ }
40
+
41
+ .skeleton-title {
42
+ height: 20px;
43
+ width: 80%;
44
+ }
45
+
46
+ .skeleton-year {
47
+ height: 16px;
48
+ width: 60%;
49
+ margin-top: 5px;
50
+ }
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
+
frontend/src/skeletons/movieCard.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import '@/skeletons/movieCard.css';
3
+
4
+ const SkeletonLoader = () => {
5
+ return (
6
+ <div className='skeleton-loader'>
7
+ <div className='image-container'>
8
+ {/* Placeholder for the image */}
9
+ </div>
10
+ <div className='movie-info'>
11
+ <div className='skeleton-title'></div>
12
+ <div className='skeleton-year'></div>
13
+ </div>
14
+ </div>
15
+ );
16
+ };
17
+
18
+ export default SkeletonLoader;