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:wght@300..700&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;
|