mvp для просмотра и загрузки фильмов
This commit is contained in:
28
src/App.js
28
src/App.js
@@ -1,25 +1,15 @@
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import './index.css';
|
||||
import MainLayout from "./layouts/MainLayout";
|
||||
import {ToastProvider} from "./contexts/ToastProvider";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<ToastProvider>
|
||||
<MainLayout/>
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
36
src/components/CardCompact.js
Normal file
36
src/components/CardCompact.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import {Card} from 'react-bootstrap';
|
||||
|
||||
const descriptionDraw = (text) => {
|
||||
return (
|
||||
<>
|
||||
{text} <br/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CardCompact = ({item}) => {
|
||||
console.log(item);
|
||||
const image = item?.images?.length > 0 ? item.images[0]?.remoteUrl : null;
|
||||
const rating = item.ratings?.imdb?.value ?? null;
|
||||
const status = (item.monitored || item.hasFile) ? (
|
||||
item.hasFile ? "Статус: Загружен" : "Статус: Загружается"
|
||||
) : null;
|
||||
return (
|
||||
<Card className="mb-3" style={{width: '18rem'}}>
|
||||
{/*<Card.Img variant="top" src={item.remotePoster}/>*/}
|
||||
<Card.Img variant="top" src={image}/>
|
||||
<Card.Body>
|
||||
<Card.Title>{item.title}</Card.Title>
|
||||
<Card.Text>
|
||||
{descriptionDraw(`Год: ${item.year}`)}
|
||||
{descriptionDraw(`Рейтинг: ${rating}`)}
|
||||
{item.sizeOnDisk ? descriptionDraw(`Размер: ${(item.sizeOnDisk / 1000000000).toFixed(2)} Gb`) : null}
|
||||
{status ? descriptionDraw(status) : null}
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardCompact;
|
||||
32
src/components/CardExtend.js
Normal file
32
src/components/CardExtend.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import {Button, Card, Col, Row} from 'react-bootstrap';
|
||||
|
||||
const CardExtended = ({item, selectHandle}) => {
|
||||
const rating = item.ratings?.imdb?.value ?? null;
|
||||
return (
|
||||
<Card className="mb-3" style={{width: '100%'}} onClick={() => selectHandle(item)}>
|
||||
<Row>
|
||||
<Col md={3}>
|
||||
<Card.Img variant="top" src={item.remotePoster} style={{marginTop: '1rem'}}/>
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<Card.Body>
|
||||
<Card.Title>{item.title}</Card.Title>
|
||||
<Card.Text>
|
||||
<strong>Год:</strong> {item.year}<br/>
|
||||
<strong>Рейтинг:</strong> {rating}<br/>
|
||||
{item.overview}
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col style={{justifyContent: 'end'}}>
|
||||
<Button variant={'outline-primary'} title={"Download"}>Загрузить</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardExtended;
|
||||
31
src/components/Navbar.js
Normal file
31
src/components/Navbar.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Navbar, Container, Nav } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const NavbarComponent = () => {
|
||||
return (
|
||||
<Navbar bg="dark" variant="dark" expand="lg" sticky="top">
|
||||
<Container>
|
||||
<Navbar.Brand as={Link} to="/">
|
||||
Media Tracker
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav">
|
||||
<Nav className="me-auto">
|
||||
<Nav.Link as={Link} to="/">
|
||||
Главная
|
||||
</Nav.Link>
|
||||
<Nav.Link as={Link} to="/recommendations">
|
||||
Рекомендации
|
||||
</Nav.Link>
|
||||
<Nav.Link as={Link} to="/search">
|
||||
Поиск
|
||||
</Nav.Link>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarComponent;
|
||||
144
src/components/RecommendationModal.js
Normal file
144
src/components/RecommendationModal.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Col, Container, FormCheck, FormSelect, Modal, Row} from 'react-bootstrap';
|
||||
import axios from "axios";
|
||||
import {useToast} from "../hooks/useToast";
|
||||
|
||||
const movieMonitor = [
|
||||
{
|
||||
id: "movieAndCollection",
|
||||
name: "Все части"
|
||||
},
|
||||
{
|
||||
id: "movieOnly",
|
||||
name: "Только этот фильм"
|
||||
}
|
||||
]
|
||||
|
||||
const RecommendationModal = ({show, handleClose, item, serial, handleSave}) => {
|
||||
const [movieQuality, setMovieQuality] = useState([])
|
||||
const [quality, setQuality] = useState(null)
|
||||
const [monitor, setMonitor] = useState(null)
|
||||
const [film, setFilm] = useState(false)
|
||||
const {addToast} = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
axios.get(`${process.env.REACT_APP_RADARR_HOST}/api/v3/qualityprofile`,
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Key': `${process.env.REACT_APP_RADARR_API_KEY}`
|
||||
}
|
||||
})
|
||||
.then((r) => {
|
||||
setMovieQuality(r.data)
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const request = !serial ? createMovieRequest : null;
|
||||
request()
|
||||
.then(res => {
|
||||
axios.post(`${process.env.REACT_APP_RADARR_HOST}/api/v3/movie`, res,
|
||||
{headers: {'X-Api-Key': `${process.env.REACT_APP_RADARR_API_KEY}`}})
|
||||
.then(() => handleSave(item, serial))
|
||||
.catch((err) => addToast(err, 'danger'));
|
||||
})
|
||||
.catch((err) => addToast(err, 'danger'));
|
||||
}
|
||||
|
||||
const createMovieRequest = async () => {
|
||||
if(!monitor) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw 'Проверьте пункт отслеживания';
|
||||
}
|
||||
if(!quality) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw 'Необходимо указать качество';
|
||||
}
|
||||
let request = item;
|
||||
request.id = 0;
|
||||
request.monitored = true;
|
||||
request.qualityProfileId = quality;
|
||||
request.minimumAvailability = "released"
|
||||
request.addOptions = {
|
||||
monitor: monitor,
|
||||
searchForMovie: true
|
||||
}
|
||||
const folders = await axios.get(`${process.env.REACT_APP_RADARR_HOST}/api/v3/rootFolder`,
|
||||
{headers: {'X-Api-Key': `${process.env.REACT_APP_RADARR_API_KEY}`}});
|
||||
request.rootFolderPath = folders.data.find((d) => d.path.includes(film ? "film" : "mult")).path;
|
||||
|
||||
const tags = await axios.get(`${process.env.REACT_APP_RADARR_HOST}/api/v3/tag`,
|
||||
{headers: {'X-Api-Key': `${process.env.REACT_APP_RADARR_API_KEY}`}});
|
||||
request.tags = tags.data.filter((t) => t.label === (film ? "film" : "mult")).map((t) => t.id)
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
if(!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={handleClose} fullscreen={true}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{item.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Container fluid='xl'>
|
||||
<Row>
|
||||
<Col sm={{span: 3}}>
|
||||
<img width="100%" className='mb-3' src={item.remotePoster} alt={item.title}/>
|
||||
</Col>
|
||||
<div className="col-9 container text-center">
|
||||
<Row>
|
||||
<div className="col">
|
||||
<p>{item.overview}</p>
|
||||
</div>
|
||||
</Row>
|
||||
<Row style={{width: '60%'}}>
|
||||
<Col>
|
||||
<p style={{marginRight: '1rem'}}>Мультик</p>
|
||||
<FormCheck type='switch' disabled={serial} style={{marginBottom: '1rem'}}
|
||||
onChange={() => setFilm(!film)}/>
|
||||
<p>Фильм</p>
|
||||
<FormCheck type='checkbox' disabled={!serial}
|
||||
style={{marginBottom: '1rem', marginLeft: '1rem'}}
|
||||
label="Сериал"/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<p style={{width: '50%', paddingTop: '1rem'}}>Что отслеживать</p>
|
||||
<FormSelect onChange={(e) => setMonitor(e.target.value)}>
|
||||
<option selected value={null}>Выберите пункт из меню</option>
|
||||
{movieMonitor.map((m) => <option value={m.id} key={m.id}>{m.name}</option>)}
|
||||
</FormSelect>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<p style={{width: '50%', paddingTop: '1rem'}}>Качество</p>
|
||||
<FormSelect onChange={(e) => setQuality(parseInt(e.target.value))}>
|
||||
<option selected value={null}>Выберете качество</option>
|
||||
{movieQuality.map((quality) => <option value={quality.id}
|
||||
key={quality.id}>{quality.name}</option>)}
|
||||
</FormSelect>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<Button variant={'primary'} onClick={() => handleSubmit()}>Скачать</Button>
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => handleClose()}>
|
||||
Закрыть
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendationModal;
|
||||
16
src/components/SceletonCompact.js
Normal file
16
src/components/SceletonCompact.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export function SceletonCompact() {
|
||||
return (
|
||||
<div className="card" aria-hidden="true" style={{width:'18rem', height: '30rem'}}>
|
||||
<div className="card-img-top" style={{background: 'grey', width: '18rem', height: '30rem'}} />
|
||||
<div className="card-body">
|
||||
<h5 className="card-title placeholder-glow">
|
||||
<span className="placeholder col-6"></span>
|
||||
</h5>
|
||||
<p className="card-text placeholder-glow">
|
||||
<span className="placeholder col-8"></span>
|
||||
<span className="placeholder col-8"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
src/contexts/ToastProvider.js
Normal file
47
src/contexts/ToastProvider.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import {Toast, ToastContainer} from "react-bootstrap";
|
||||
import {useState} from "react";
|
||||
|
||||
export const ToastContext = React.createContext(undefined);
|
||||
|
||||
export function ToastProvider({children}) {
|
||||
const [toasts, setToasts] = useState([]);
|
||||
|
||||
// Функция для добавления нового тоста
|
||||
const addToast = (message, variant = 'secondary') => {
|
||||
const newToast = {
|
||||
id: Date.now(), // Уникальный идентификатор
|
||||
message,
|
||||
variant,
|
||||
};
|
||||
|
||||
setToasts([newToast, ...toasts]);
|
||||
|
||||
// Удаляем тост через 3 секунды
|
||||
setTimeout(() => {
|
||||
setToasts(toasts.filter(t => t.id !== newToast.id));
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ addToast }}>
|
||||
{children}
|
||||
<ToastContainer position="bottom-end">
|
||||
{toasts.map(toast => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
bg={toast.variant}
|
||||
aria-atomic={true}
|
||||
className="rounded me-2 mb-1 me-1 border-0"
|
||||
autohide
|
||||
delay={3000}
|
||||
>
|
||||
<Toast.Body className="text-white">
|
||||
{toast.message}
|
||||
</Toast.Body>
|
||||
</Toast>
|
||||
))}
|
||||
</ToastContainer>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
19
src/hooks/useAxios.js
Normal file
19
src/hooks/useAxios.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const useAxios = (url) => {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
axios.get(url, )
|
||||
.then((r) => setData(r.data))
|
||||
.catch((err) => setError(err))
|
||||
.finally(() => setLoading(false));
|
||||
}, [url]);
|
||||
|
||||
return { data, loading, error };
|
||||
};
|
||||
|
||||
export default useAxios;
|
||||
12
src/hooks/useToast.js
Normal file
12
src/hooks/useToast.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {useContext} from "react";
|
||||
import {ToastContext} from "../contexts/ToastProvider";
|
||||
|
||||
export function useToast() {
|
||||
const context = useContext(ToastContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -11,3 +11,10 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
14
src/index.js
14
src/index.js
@@ -1,17 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
// import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
23
src/layouts/MainLayout.js
Normal file
23
src/layouts/MainLayout.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
|
||||
import Navbar from "../components/Navbar";
|
||||
import {Recommendation} from "../pages/RecommendationPage";
|
||||
import {Search} from "../pages/SearchPage";
|
||||
import {NotFound} from "../pages/NotFoundPage";
|
||||
import Home from "../pages/HomePage";
|
||||
|
||||
const MainLayout = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Navbar/>
|
||||
<Routes>
|
||||
<Route exact path="/" element={<Home/>}/>
|
||||
<Route path="/recommendations" element={<Recommendation/>}/>
|
||||
<Route path="/search" element={<Search/>}/>
|
||||
<Route path="*" element={<NotFound/>}/>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
64
src/pages/HomePage.js
Normal file
64
src/pages/HomePage.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Accordion, Badge, Col, Container, Row} from "react-bootstrap";
|
||||
import CardCompact from "../components/CardCompact";
|
||||
import axios from "axios";
|
||||
import {useToast} from "../hooks/useToast";
|
||||
import {SceletonCompact} from "../components/SceletonCompact";
|
||||
|
||||
const Home = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [movies, setMovies] = useState([]);
|
||||
// const [series, setSeries] = useState([]);
|
||||
const {addToast} = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
axios.get(`${process.env.REACT_APP_RADARR_HOST}/api/v3/movie`,
|
||||
{headers: {'X-Api-Key': process.env.REACT_APP_RADARR_API_KEY}})
|
||||
.then((r) => setMovies(r.data))
|
||||
.catch((err) => addToast(err, 'alert'))
|
||||
.finally(() => setLoading(false));
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Accordion defaultActiveKey={["0"]} alwaysOpen>
|
||||
<Accordion.Item eventKey="0">
|
||||
<Accordion.Header className="mt-3">
|
||||
Фильмы
|
||||
</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
<Row className='row-cols-auto'>
|
||||
{loading ?
|
||||
(
|
||||
// Отображаем 10 скелетонов
|
||||
Array.from({length: 10}, (_, index) => (
|
||||
<Col key={`skeleton-${index}`}>
|
||||
<SceletonCompact key={index} width={300} height={400}/>
|
||||
</Col>
|
||||
))
|
||||
)
|
||||
: movies.map(rec => (
|
||||
<Col key={`col-${rec.title}-${rec.year}`}>
|
||||
<CardCompact item={rec} key={`${rec.title}-${rec.year}`}/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item eventKey={"1"}>
|
||||
<Accordion.Header className="mt-5">Сериалы</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
<Row>
|
||||
<Badge bg="info">Функционал для сериалов пока не реализован</Badge>
|
||||
</Row>
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
25
src/pages/NotFoundPage.js
Normal file
25
src/pages/NotFoundPage.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
|
||||
export function NotFound () {
|
||||
return (
|
||||
<Container className="py-5">
|
||||
<Row className="justify-content-center align-items-center">
|
||||
<Col md={6} className="text-center">
|
||||
<div className="mb-4">
|
||||
<h1 className="display-4 fw-bold text-danger">404</h1>
|
||||
<h2 className="mb-4">Кажется, что-то пошло не так...</h2>
|
||||
</div>
|
||||
<p className="lead">
|
||||
Страница, которую вы ищете, не существует или была перемещена.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<a href="/" className="btn btn-primary">
|
||||
Вернуться на главную
|
||||
</a>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
67
src/pages/RecommendationPage.js
Normal file
67
src/pages/RecommendationPage.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import axios from "axios";
|
||||
import CardCompact from "../components/CardCompact";
|
||||
import {SceletonCompact} from "../components/SceletonCompact";
|
||||
import RecommendationModal from "../components/RecommendationModal";
|
||||
import {Col, Container, Row} from "react-bootstrap";
|
||||
import {useToast} from "../hooks/useToast";
|
||||
|
||||
export function Recommendation() {
|
||||
const [recommendations, setRecommendations] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalView, setModalView] = useState(false);
|
||||
const [item, setItem] = useState({});
|
||||
const {addToast} = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
axios.get(`${process.env.REACT_APP_RADARR_HOST}/api/v3/importlist/movie?includeRecommendations=true&includeTrending=true&includePopular=true`,
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Key': `${process.env.REACT_APP_RADARR_API_KEY}`
|
||||
}
|
||||
}).then((r) => setRecommendations(r.data))
|
||||
.catch((err) => console.log(err))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
function handleModal(item) {
|
||||
setModalView(true);
|
||||
setItem(item)
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalView(false);
|
||||
setItem({})
|
||||
}
|
||||
|
||||
const handleDownload = (item) => {
|
||||
const nState = recommendations.filter(r => r.title !== item.title && r.year !== item.year);
|
||||
setRecommendations(nState);
|
||||
addToast(`${item.title} добавлен в список загрузок`, 'success');
|
||||
handleCloseModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="text-center">
|
||||
<h2 className="mt-3">Рекомендации</h2>
|
||||
<Row className='row-cols-auto'>
|
||||
{loading ?
|
||||
(
|
||||
// Отображаем 10 скелетонов
|
||||
Array.from({length: 10}, (_, index) => (
|
||||
<Col key={`skeleton-${index}`}>
|
||||
<SceletonCompact key={index} width={300} height={400}/>
|
||||
</Col>
|
||||
))
|
||||
)
|
||||
: recommendations.map(rec => (
|
||||
<Col key={`col-${rec.title}-${rec.year}`} onClick={() => handleModal(rec)}>
|
||||
<CardCompact item={rec} key={`${rec.title}-${rec.year}`}/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<RecommendationModal show={modalView} handleClose={handleCloseModal} item={item} handleSave={handleDownload}/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
105
src/pages/SearchPage.js
Normal file
105
src/pages/SearchPage.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import axios from "axios";
|
||||
import CardExtend from "../components/CardExtend";
|
||||
import {Alert, Container, InputGroup, Row} from "react-bootstrap";
|
||||
import RecommendationModal from "../components/RecommendationModal";
|
||||
import {useToast} from "../hooks/useToast";
|
||||
|
||||
export function Search() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [loadMovies, setLoadMovies] = useState(false);
|
||||
// const [loadTv, setLoadTv] = useState(false);
|
||||
const [errorMovies, setErrorMovies] = useState(false);
|
||||
// const [errorTv, setErrorTv] = useState(false);
|
||||
// const [resultTv, setResultTv] = useState([]);
|
||||
const [resultMovies, setResultMovies] = useState([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [selectItem, setSelectItem] = useState({});
|
||||
const {addToast} = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (query.length === 0) {
|
||||
return;
|
||||
}
|
||||
setLoadMovies(true);
|
||||
axios.get(`${process.env.REACT_APP_RADARR_HOST}/api/v3/movie/lookup?term=${query}}`,
|
||||
{
|
||||
headers: {
|
||||
// "Content-Type": "application/json",
|
||||
'X-Api-Key': `${process.env.REACT_APP_RADARR_API_KEY}`
|
||||
}
|
||||
})
|
||||
.then((r) => setResultMovies(r.data))
|
||||
.catch((err) => setErrorMovies(err))
|
||||
.finally(() => setLoadMovies(false));
|
||||
|
||||
}, [query]);
|
||||
|
||||
const handleChange = (item) => {
|
||||
setSelectItem(item);
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
const result = useMemo(() => {
|
||||
let array = resultMovies;
|
||||
// array.concat(resultTv);
|
||||
return array.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}, [resultMovies]);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setShowModal(false);
|
||||
setSelectItem(null);
|
||||
}
|
||||
|
||||
const handleSave = (item, serial) => {
|
||||
let nState = resultMovies;
|
||||
nState = nState.filter(i => i.title !== item.title && i.year !== item.year);
|
||||
|
||||
const func = setResultMovies;
|
||||
func(nState);
|
||||
addToast(`${item.title} добавлен в список загрузок`, 'success');
|
||||
handleCloseModal();
|
||||
}
|
||||
|
||||
// Используем хук useAxios для обработки запросов
|
||||
// const {response, loading, errorTv} = useAxios('/api/search', 'get', {query});
|
||||
|
||||
return (
|
||||
<Container style={{marginTop: 5}}>
|
||||
<h2>Поиск</h2>
|
||||
<form className="mb-4">
|
||||
<InputGroup>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Введите название"
|
||||
/>
|
||||
</InputGroup>
|
||||
</form>
|
||||
|
||||
{loadMovies && <div className="text-center">Загрузка...</div>}
|
||||
|
||||
{errorMovies && (
|
||||
<Alert variant="danger">{errorMovies}</Alert>
|
||||
)}
|
||||
|
||||
{result && result.length > 0 && (
|
||||
<div>
|
||||
<h3>Результаты поиска</h3>
|
||||
<Row>
|
||||
{resultMovies.map(result => <CardExtend item={result} key={result.id} selectHandle={handleChange}/>)}
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!loadMovies && !errorMovies && (!resultMovies || !result.length)) && (
|
||||
<Alert variant='info'>
|
||||
Ничего не найдено
|
||||
</Alert>
|
||||
)}
|
||||
<RecommendationModal item={selectItem} handleClose={handleCloseModal} show={showModal} serial={false} handleSave={handleSave}/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user