ChandimaPrabath
commited on
Commit
•
03769d0
1
Parent(s):
17406e2
api fix
Browse files- frontend/next.config.mjs +5 -1
- frontend/src/app/films/filmsPage.css +0 -78
- frontend/src/app/films/page.js +0 -87
- frontend/src/app/movies/filmsPage.css +93 -0
- frontend/src/app/movies/page.js +78 -0
- frontend/src/components/MovieCard.css +76 -0
- frontend/src/components/MovieCard.js +55 -0
- frontend/src/components/Sidebar.js +2 -2
- frontend/src/components/film/card.css +0 -193
- frontend/src/components/film/card.js +0 -92
- frontend/src/lib/LoadBalancer.js +128 -115
- frontend/src/skeletons/movieCard.css +63 -0
- frontend/src/skeletons/movieCard.js +18 -0
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="/
|
69 |
className={`sidebar-link ${
|
70 |
-
pathname === "/
|
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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
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 |
-
|
53 |
-
|
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 |
-
|
65 |
-
|
66 |
-
|
67 |
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
}
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
}
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
}
|
106 |
-
return response.json();
|
107 |
}
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
}
|
|
|
121 |
}
|
122 |
}
|
123 |
-
|
124 |
-
|
|
|
|
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;
|