├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json ├── 404.html └── index.html ├── assets ├── deploy-status.png ├── how-it-works.png ├── repo-settings.png ├── template-step-1.png ├── template-step-2.png ├── gh-actions-perm-1.png └── gh-actions-perm-2.png ├── jsconfig.json ├── src ├── images │ ├── default_image.png │ ├── default_person.jpg │ └── preview-movies.jpg ├── index.css ├── components │ ├── Loader │ │ └── Loader.jsx │ ├── MoviesList │ │ ├── MoviesList.styled.jsx │ │ └── MoviesList.jsx │ ├── Reviews │ │ ├── Reviews.styled.jsx │ │ └── Reviews.jsx │ ├── SearchForm │ │ ├── SearchForm.jsx │ │ └── SearchForm.styled.jsx │ ├── SharedLayout │ │ ├── SharedLayout.jsx │ │ └── SharedLayout.styled.jsx │ ├── Cast │ │ ├── Cast.styled.jsx │ │ └── Cast.jsx │ ├── App.jsx │ └── GlobalStyle.js ├── index.js ├── pages │ ├── Home │ │ ├── Home.styled.jsx │ │ └── Home.jsx │ ├── Movies │ │ └── Movies.jsx │ └── MovieDetails │ │ ├── MovieDetails.jsx │ │ └── MovieDetails.styled.jsx └── services │ └── api.js ├── uk_translation.yml ├── .editorconfig ├── .prettierrc.json ├── .gitignore ├── .github └── workflows │ └── deploy.yml ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/public/logo512.png -------------------------------------------------------------------------------- /assets/deploy-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/assets/deploy-status.png -------------------------------------------------------------------------------- /assets/how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/assets/how-it-works.png -------------------------------------------------------------------------------- /assets/repo-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/assets/repo-settings.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /assets/template-step-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/assets/template-step-1.png -------------------------------------------------------------------------------- /assets/template-step-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/assets/template-step-2.png -------------------------------------------------------------------------------- /assets/gh-actions-perm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/assets/gh-actions-perm-1.png -------------------------------------------------------------------------------- /assets/gh-actions-perm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/assets/gh-actions-perm-2.png -------------------------------------------------------------------------------- /src/images/default_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/src/images/default_image.png -------------------------------------------------------------------------------- /src/images/default_person.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/src/images/default_person.jpg -------------------------------------------------------------------------------- /src/images/preview-movies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Morifer79/react-movie-search/HEAD/src/images/preview-movies.jpg -------------------------------------------------------------------------------- /uk_translation.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /react-homework-template/blob/main/README.md 3 | translation: react-homework-template/%two_letters_code%/%original_file_name% 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | charset = utf-8 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "proseWrap": "always" 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | #Junk 4 | .vscode/ 5 | .idea/ 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.js 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import-normalize; /* bring in normalize.css styles */ 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { ThreeCircles } from 'react-loader-spinner'; 2 | 3 | export const Loader = () => { 4 | return ( 5 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v2.3.1 13 | 14 | - name: Install, lint, build 🔧 15 | run: | 16 | npm install 17 | npm run lint:js 18 | npm run build 19 | 20 | - name: Deploy 🚀 21 | uses: JamesIves/github-pages-deploy-action@4.1.0 22 | with: 23 | branch: gh-pages 24 | folder: build 25 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/MoviesList/MoviesList.styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const MovieList = styled.ul` 5 | margin-top: ${p => p.theme.spacing(7.5)}; 6 | margin-left: ${p => p.theme.spacing(20)}; 7 | `; 8 | 9 | export const MovieItem = styled.li` 10 | &:not(:last-child) { 11 | margin-bottom: ${p => p.theme.spacing(3)}; 12 | } 13 | `; 14 | 15 | export const MovieLink = styled(Link)` 16 | font-family: 'Bad Script'; 17 | font-weight: 500; 18 | font-size: 16px; 19 | color: ${p => p.theme.colors.white}; 20 | transition: color 300ms linear; 21 | 22 | &:hover { 23 | color: ${p => p.theme.colors.bernred}; 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/components/MoviesList/MoviesList.jsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | import { MovieList, MovieItem, MovieLink } from './MoviesList.styled'; 3 | import { CameraReels } from 'pages/MovieDetails/MovieDetails.styled'; 4 | 5 | const MoviesList = ({ movies }) => { 6 | const location = useLocation(); 7 | 8 | return ( 9 | 10 | {movies.map(({ id, title, name }) => { 11 | return ( 12 | 13 | 14 | 15 | {title || name} 16 | 17 | 18 | ); 19 | })} 20 | 21 | ); 22 | }; 23 | 24 | export default MoviesList; 25 | -------------------------------------------------------------------------------- /src/components/Reviews/Reviews.styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ReviewsWrapper = styled.div` 4 | padding: 35px 20px; 5 | `; 6 | 7 | export const InfoWrapper = styled.ul` 8 | display: flex; 9 | flex-direction: column; 10 | gap: ${p => p.theme.spacing(8)}; 11 | 12 | :nth-of-type(even) { 13 | border: 1px solid ${p => p.theme.colors.bernred}; 14 | } 15 | `; 16 | 17 | export const InfoBlock = styled.li` 18 | padding: ${p => p.theme.spacing(8)}; 19 | border: 1px solid ${p => p.theme.colors.white}; 20 | border-radius: ${p => p.theme.radii.lg}; 21 | 22 | :not(:last-child) { 23 | padding: ${p => p.theme.spacing(5)}; 24 | } 25 | 26 | h3 { 27 | color: ${p => p.theme.colors.bernred}; 28 | } 29 | 30 | p { 31 | text-align: justify; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from 'components/App'; 4 | import './index.css'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import { ThemeProvider } from 'styled-components'; 7 | 8 | const theme = { 9 | colors: { 10 | black: '#000000', 11 | white: '#ffffff', 12 | bernred: '#e50914', 13 | }, 14 | radii: { 15 | sm: '4px', 16 | md: '6px', 17 | lg: '25px', 18 | }, 19 | spacing: value => `${value * 2}px`, 20 | }; 21 | 22 | ReactDOM.createRoot(document.getElementById('root')).render( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/components/SearchForm/SearchForm.jsx: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from 'react-router-dom'; 2 | import { 3 | IconSearch, 4 | SearchBar, 5 | SearchFormInput, 6 | SearchBtn, 7 | } from './SearchForm.styled'; 8 | 9 | const SearchForm = () => { 10 | const [, setSearchParams] = useSearchParams(); 11 | 12 | const handleSubmit = e => { 13 | e.preventDefault(); 14 | const query = e.currentTarget.elements.query.value.trim(); 15 | if (!query) { 16 | setSearchParams({}); 17 | } 18 | setSearchParams({ query: query }); 19 | }; 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default SearchForm; 32 | -------------------------------------------------------------------------------- /src/components/SharedLayout/SharedLayout.jsx: -------------------------------------------------------------------------------- 1 | import { Loader } from 'components/Loader/Loader'; 2 | import { Suspense } from 'react'; 3 | import { Outlet } from 'react-router-dom'; 4 | import { 5 | BtnList, 6 | Header, 7 | IconHome, 8 | IconMovie, 9 | StyledLink, 10 | Wrapper, 11 | } from 'components/SharedLayout/SharedLayout.styled'; 12 | 13 | const SharedLayout = () => { 14 | return ( 15 | 16 |
17 | 32 |
33 | }> 34 | 35 | 36 |
37 | ); 38 | }; 39 | 40 | export default SharedLayout; 41 | -------------------------------------------------------------------------------- /src/components/Cast/Cast.styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CastWrapper = styled.div` 4 | padding-top: ${p => p.theme.spacing(15)}; 5 | padding-bottom: ${p => p.theme.spacing(15)}; 6 | `; 7 | 8 | export const CastList = styled.ul` 9 | display: flex; 10 | justify-content: center; 11 | flex-wrap: wrap; 12 | gap: ${p => p.theme.spacing(10)}; 13 | `; 14 | 15 | export const WikiLink = styled.a` 16 | //для перехода на Википедию и отсутствия конфликта 17 | `; 18 | 19 | export const CastItem = styled.li` 20 | width: 200px; 21 | overflow: hidden; 22 | 23 | p:not(:last-child) { 24 | margin-top: ${p => p.theme.spacing(2.5)}; 25 | margin-bottom: ${p => p.theme.spacing(2.5)}; 26 | } 27 | 28 | &:hover img { 29 | filter: grayscale(1); 30 | transition: 300ms ease-in-out; 31 | } 32 | `; 33 | 34 | export const ActorsImg = styled.img` 35 | height: 300px; 36 | border: 3px dotted ${p => p.theme.colors.bernred}; 37 | border-top-left-radius: ${p => p.theme.radii.lg}; 38 | border-bottom-right-radius: ${p => p.theme.radii.lg}; 39 | filter: grayscale(0); 40 | `; 41 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import { lazy } from 'react'; 3 | import { GlobalStyle } from './GlobalStyle'; 4 | import SharedLayout from 'components/SharedLayout/SharedLayout'; 5 | 6 | const Home = lazy(() => import('pages/Home/Home')); 7 | const Movies = lazy(() => import('pages/Movies/Movies')); 8 | const MovieDetails = lazy(() => import('pages/MovieDetails/MovieDetails')); 9 | const Cast = lazy(() => import('components/Cast/Cast')); 10 | const Reviews = lazy(() => import('components/Reviews/Reviews')); 11 | 12 | const App = () => { 13 | return ( 14 | <> 15 | 16 | }> 17 | } /> 18 | } /> 19 | }> 20 | } /> 21 | } /> 22 | 23 | } /> 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/pages/Home/Home.styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PulsarTitle = styled.h1` 4 | margin: 30px 0 0 20px; 5 | 6 | font-family: 'Bad Script'; 7 | --interval: 1000ms; 8 | display: block; 9 | text-shadow: 0 0 10px var(--color1), 0 0 20px var(--color2), 10 | 0 0 40px var(--color3), 0 0 80px var(--color4); 11 | will-change: filter, color; 12 | filter: saturate(60%); 13 | 14 | animation: flicker steps(100) var(--interval) 1000ms infinite; 15 | color: azure; 16 | --color1: azure; 17 | --color2: aqua; 18 | --color3: dodgerblue; 19 | --color4: blue; 20 | 21 | @keyframes flicker { 22 | 50% { 23 | color: ${p => p.theme.colors.white}; 24 | filter: saturate(200%) hue-rotate(20deg); 25 | } 26 | } 27 | `; 28 | 29 | export const Quotation = styled.p` 30 | display: none; 31 | @media screen and (min-width: 768px) { 32 | display: block; 33 | padding: ${p => p.theme.spacing(10)}; 34 | 35 | font-family: 'Bad Script'; 36 | font-size: 36px; 37 | text-align: center; 38 | color: ${p => p.theme.colors.bernred}; 39 | position: absolute; 40 | top: 40%; 41 | right: 10%; 42 | 43 | span { 44 | font-size: 28px; 45 | color: ${p => p.theme.colors.white}; 46 | } 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /src/components/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | html { 5 | box-sizing: border-box; 6 | width: 100vw; 7 | overflow-x: hidden; 8 | } 9 | 10 | *, 11 | *::before, 12 | *::after { 13 | box-sizing: inherit; 14 | } 15 | 16 | img { 17 | display: block; 18 | max-width: 100%; 19 | height: auto; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 25 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 26 | sans-serif; 27 | -webkit-font-smoothing: antialiased; 28 | -moz-osx-font-smoothing: grayscale; 29 | color: ${p => p.theme.colors.white}; 30 | background-color: ${p => p.theme.colors.black}; 31 | letter-spacing: 0.03em; 32 | font-weight: 400; 33 | font-size: 14px; 34 | line-height: 1.17; 35 | } 36 | 37 | code { 38 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 39 | monospace; 40 | } 41 | 42 | h1,h2,h3,h4,h5,h6,p { 43 | margin: 0; 44 | } 45 | 46 | ul { 47 | margin: 0; 48 | padding: 0; 49 | list-style: none; 50 | } 51 | 52 | a { 53 | text-decoration: none; 54 | color: inherit; 55 | } 56 | 57 | button { 58 | font-family: inherit; 59 | padding: 0; 60 | margin: 0; 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-homework-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://Morifer79.github.io/react-movie-search/", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.3", 8 | "@testing-library/react": "^12.1.4", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.5.0", 11 | "modern-normalize": "^2.0.0", 12 | "react": "^18.1.0", 13 | "react-dom": "^18.1.0", 14 | "react-icons": "^4.11.0", 15 | "react-loader-spinner": "^5.4.5", 16 | "react-router-dom": "^6.16.0", 17 | "react-scripts": "5.0.1", 18 | "react-scroll": "^1.8.9", 19 | "styled-components": "^6.0.8", 20 | "web-vitals": "^2.1.3" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject", 27 | "lint:js": "eslint src/**/*.{js,jsx}" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/SearchForm/SearchForm.styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ImSearch } from 'react-icons/im'; 3 | 4 | export const SearchBar = styled.form` 5 | width: 100%; 6 | max-width: 450px; 7 | position: relative; 8 | `; 9 | 10 | export const SearchFormInput = styled.input` 11 | margin: 20px 0 0 40px; 12 | padding: ${p => p.theme.spacing(5)}; 13 | 14 | border-radius: ${p => p.theme.radii.sm}; 15 | border: solid 1px ${p => p.theme.colors.bernred}; 16 | background-color: inherit; 17 | outline: none; 18 | color: ${p => p.theme.colors.white}; 19 | width: 100%; 20 | height: 35px; 21 | max-width: 450px; 22 | font-size: 16px; 23 | transition: box-shadow 400ms cubic-bezier(0.4, 0, 0.2, 1); 24 | 25 | &:hover, 26 | &:focus { 27 | box-shadow: 0px 0px 10px 1px ${p => p.theme.colors.bernred}; 28 | } 29 | `; 30 | 31 | export const SearchBtn = styled.button` 32 | cursor: pointer; 33 | position: absolute; 34 | top: 20px; 35 | right: -40px; 36 | background-color: inherit; 37 | border-radius: ${p => p.theme.radii.sm}; 38 | border: solid 1px ${p => p.theme.colors.bernred}; 39 | height: 35px; 40 | width: 40px; 41 | transition: box-shadow 300ms linear; 42 | 43 | &:hover { 44 | box-shadow: inset 0px 0px 10px 3px ${p => p.theme.colors.bernred}; 45 | } 46 | `; 47 | 48 | export const IconSearch = styled(ImSearch)` 49 | color: ${p => p.theme.colors.bernred}; 50 | width: 20px; 51 | height: 20px; 52 | `; 53 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | axios.defaults.baseURL = 'https://api.themoviedb.org/3/'; 4 | const API_KEY = 'c16c869f875a641f65f14ffc799280d5'; 5 | 6 | export const fetchHomePage = async () => { 7 | const { data } = await axios.get('trending/movie/day', { 8 | params: { 9 | api_key: API_KEY, 10 | language: 'en-US', 11 | }, 12 | }); 13 | 14 | return data.results; 15 | }; 16 | 17 | export const fetchSearchMovie = async query => { 18 | const response = await axios.get('search/movie', { 19 | params: { 20 | api_key: API_KEY, 21 | language: 'en-US', 22 | query, 23 | include_adult: false, 24 | page: 1, 25 | }, 26 | }); 27 | return response.data; 28 | }; 29 | 30 | export const fetchSearchDetail = async movieId => { 31 | const response = await axios.get(`movie/${movieId}`, { 32 | params: { 33 | api_key: API_KEY, 34 | language: 'en-US', 35 | }, 36 | }); 37 | return response.data; 38 | }; 39 | 40 | export const fetchSearchCast = async movieId => { 41 | const { data } = await axios.get(`movie/${movieId}/credits`, { 42 | params: { 43 | api_key: API_KEY, 44 | language: 'en-US', 45 | }, 46 | }); 47 | return data.cast; 48 | }; 49 | 50 | export const fetchSearchReviews = async movieId => { 51 | const { data } = await axios.get(`movie/${movieId}/reviews`, { 52 | params: { 53 | api_key: API_KEY, 54 | language: 'en-US', 55 | page: 1, 56 | }, 57 | }); 58 | return data.results; 59 | }; 60 | -------------------------------------------------------------------------------- /src/pages/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { fetchHomePage } from '../../services/api'; 3 | import MoviesList from 'components/MoviesList/MoviesList'; 4 | import { Quotation, PulsarTitle } from './Home.styled'; 5 | import { Loader } from 'components/Loader/Loader'; 6 | 7 | const Home = () => { 8 | const [movies, setMovies] = useState([]); 9 | const [isLoading, setIsLoading] = useState(false); 10 | const [isError, setIsError] = useState(null); 11 | 12 | useEffect(() => { 13 | const abortController = new AbortController(); 14 | const getMoviesList = async () => { 15 | try { 16 | setIsLoading(true); 17 | setIsError(null); 18 | const trendInfo = await fetchHomePage({ 19 | signal: abortController.signal, 20 | }); 21 | setMovies(trendInfo); 22 | } catch (error) { 23 | setIsError(error.message); 24 | } finally { 25 | setIsLoading(false); 26 | } 27 | }; 28 | getMoviesList(); 29 | 30 | return () => { 31 | abortController.abort(); 32 | }; 33 | }, []); 34 | 35 | return ( 36 | <> 37 | Trending today 38 | {isLoading && } 39 | {isError &&

Error loading movies. Please try again later.

} 40 | 41 | 42 | Be yourself 43 |
- everyone else is already taken. 44 |
45 |
46 | Oscar Wilde 47 |
48 | {!isLoading && movies.length > 0 && } 49 | 50 | ); 51 | }; 52 | 53 | export default Home; 54 | -------------------------------------------------------------------------------- /src/components/SharedLayout/SharedLayout.styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { IoHomeSharp } from 'react-icons/io5'; 4 | import { ImFilm } from 'react-icons/im'; 5 | 6 | export const Wrapper = styled.div` 7 | margin: 0 auto; 8 | padding-bottom: ${p => p.theme.spacing(8)}; 9 | `; 10 | 11 | export const Header = styled.header` 12 | padding: 10px 0 12px 20px; 13 | margin-bottom: ${p => p.theme.spacing(8)}; 14 | 15 | display: flex; 16 | gap: ${p => p.theme.spacing(6)}; 17 | align-items: center; 18 | height: 60px; 19 | border-bottom: 1px solid ${p => p.theme.colors.bernred}; 20 | box-shadow: 0 0 13px 3px ${p => p.theme.colors.bernred}; 21 | `; 22 | 23 | export const BtnList = styled.ul` 24 | display: flex; 25 | align-items: center; 26 | column-gap: ${p => p.theme.spacing(8)}; 27 | `; 28 | 29 | export const StyledLink = styled(NavLink)` 30 | max-width: 60px; 31 | font-weight: 600; 32 | color: ${p => p.theme.colors.white}; 33 | padding: 8px 16px; 34 | border-radius: ${p => p.theme.radii.sm}; 35 | transition: color 300ms linear, background 350ms linear; 36 | 37 | &:hover { 38 | color: ${p => p.theme.colors.bernred}; 39 | } 40 | 41 | &.active { 42 | font-weight: 900; 43 | color: ${p => p.theme.colors.white}; 44 | background-color: ${p => p.theme.colors.bernred}; 45 | } 46 | `; 47 | 48 | export const IconHome = styled(IoHomeSharp)` 49 | vertical-align: sub; 50 | margin-right: ${p => p.theme.spacing(3)}; 51 | width: 20px; 52 | height: 20px; 53 | `; 54 | 55 | export const IconMovie = styled(ImFilm)` 56 | vertical-align: sub; 57 | margin-right: ${p => p.theme.spacing(3)}; 58 | width: 20px; 59 | height: 20px; 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/Reviews/Reviews.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { fetchSearchReviews } from 'services/api'; 4 | import { ReviewsWrapper, InfoBlock, InfoWrapper } from './Reviews.styled'; 5 | import { Loader } from 'components/Loader/Loader'; 6 | import { animateScroll } from 'react-scroll'; 7 | 8 | const Reviews = () => { 9 | const { movieId } = useParams(); 10 | const [reviews, setReviews] = useState([]); 11 | const [isLoading, setIsLoading] = useState(false); 12 | const [isError, setIsError] = useState(false); 13 | 14 | useEffect(() => { 15 | const abortController = new AbortController(); 16 | const getReviews = async () => { 17 | try { 18 | setIsLoading(true); 19 | setIsError(null); 20 | const reviewInfo = await fetchSearchReviews(movieId, { 21 | signal: abortController.signal, 22 | }); 23 | setReviews(reviewInfo); 24 | } catch (error) { 25 | setIsError(error.message); 26 | } finally { 27 | setIsLoading(false); 28 | } 29 | }; 30 | getReviews(movieId); 31 | 32 | return () => { 33 | abortController.abort(); 34 | }; 35 | }, [movieId]); 36 | 37 | if (reviews) { 38 | animateScroll.scrollMore(400); 39 | } 40 | 41 | return ( 42 | <> 43 | {isLoading && } 44 | {isError &&

Not Found

} 45 | {!isLoading && reviews.length > 0 && ( 46 | 47 | 48 | {reviews.map(({ id, author, content }) => ( 49 | 50 |

Author: {author}

51 |

{content}

52 |
53 | ))} 54 |
55 |
56 | )} 57 | {!isLoading && reviews.length === 0 && ( 58 |
We dont have ani reviews for this movie.
59 | )} 60 | 61 | ); 62 | }; 63 | 64 | export default Reviews; 65 | -------------------------------------------------------------------------------- /src/pages/Movies/Movies.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Loader } from 'components/Loader/Loader'; 3 | import { fetchSearchMovie } from 'services/api'; 4 | import MoviesList from 'components/MoviesList/MoviesList'; 5 | import SearchForm from 'components/SearchForm/SearchForm'; 6 | import { Quotation } from 'pages/Home/Home.styled'; 7 | import { useSearchParams } from 'react-router-dom'; 8 | 9 | const Movies = () => { 10 | const [movies, setMovies] = useState([]); 11 | const [isLoading, setIsLoading] = useState(false); 12 | const [isError, setIsError] = useState(null); 13 | const [searchParams] = useSearchParams(); 14 | const query = searchParams.get('query') ?? ''; 15 | 16 | useEffect(() => { 17 | if (!query) { 18 | return; 19 | } 20 | const abortController = new AbortController(); 21 | const getMovie = async () => { 22 | if (abortController.current) { 23 | abortController.current.abort(); 24 | } 25 | abortController.current = new AbortController(); 26 | 27 | try { 28 | setIsLoading(true); 29 | setIsError(null); 30 | 31 | const { results } = await fetchSearchMovie(query, { 32 | signal: abortController.current.signal, 33 | }); 34 | 35 | setMovies(results); 36 | } catch (error) { 37 | setIsError(error.message); 38 | } finally { 39 | setIsLoading(false); 40 | } 41 | }; 42 | getMovie(); 43 | return () => { 44 | abortController.abort(); 45 | }; 46 | }, [query]); 47 | 48 | return ( 49 | <> 50 | {isLoading && } 51 | {isError && !isLoading &&

No one movie

} 52 | 53 | 54 | 55 | Life 56 |
- is a series of choices. 57 |
58 |
59 | Michelle Nostradamus 60 |
61 | {movies && } 62 | 63 | ); 64 | }; 65 | 66 | export default Movies; 67 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/components/Cast/Cast.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { fetchSearchCast } from 'services/api'; 4 | import { 5 | ActorsImg, 6 | CastItem, 7 | CastList, 8 | CastWrapper, 9 | WikiLink, 10 | } from './Cast.styled'; 11 | import { Loader } from 'components/Loader/Loader'; 12 | import { animateScroll } from 'react-scroll'; 13 | import default_person from '../../images/default_person.jpg'; 14 | 15 | const IMAGE_URL = 'https://image.tmdb.org/t/p/w500'; 16 | 17 | const Cast = () => { 18 | const { movieId } = useParams(); 19 | const [cast, setCast] = useState([]); 20 | const [isLoading, setIsLoading] = useState(false); 21 | const [isError, setIsError] = useState(null); 22 | 23 | useEffect(() => { 24 | const abortController = new AbortController(); 25 | const getCast = async () => { 26 | try { 27 | setIsLoading(true); 28 | setIsError(null); 29 | const castInfo = await fetchSearchCast(movieId, { 30 | signal: abortController.signal, 31 | }); 32 | setCast(castInfo); 33 | } catch (error) { 34 | setIsError(error.message); 35 | } finally { 36 | setIsLoading(false); 37 | } 38 | }; 39 | getCast(movieId); 40 | return () => { 41 | abortController.abort(); 42 | }; 43 | }, [movieId]); 44 | 45 | if (cast) { 46 | animateScroll.scrollMore(640); 47 | } 48 | 49 | return ( 50 | <> 51 | {isLoading && } 52 | {isError &&

There is no information yet.

} 53 | 54 | 55 | {cast.map(({ id, profile_path, name, character }) => { 56 | return ( 57 | 58 | 63 | 71 |

72 | Name: 73 | {name} 74 |

75 |

76 | Character: 77 | {character} 78 |

79 |
80 |
81 | ); 82 | })} 83 |
84 |
85 | 86 | ); 87 | }; 88 | 89 | export default Cast; 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ℝ𝕖𝕒𝕔𝕥 𝕄𝕠𝕧𝕚𝕖 𝕊𝕖𝕒𝕣𝕔𝕙 2 | ## ℕ𝕚𝕟𝕥𝕙 𝕣𝕖𝕒𝕔𝕥 𝕙𝕠𝕞𝕖𝕨𝕠𝕣𝕜 3 | 4 | ![Image Search](./src/images/preview-movies.jpg) 5 | 6 | This project was created using [Create React App](https://github.com/facebook/create-react-app). 7 | 8 | ## 𝔽𝕖𝕒𝕥𝕦𝕣𝕖𝕤 : 9 | 10 | ※ An movie search app. 11 | ※ Working with API (TOP category loading). 12 | ※ Clicking on the actor's map will take you to his Wikipedia page. 13 | ※ Smooth scrolling of actor cards has been implemented. 14 | ※ Themization and GlobalStyles were used . 15 | 16 | ## 𝕋𝕖𝕔𝕙𝕟𝕠𝕝𝕠𝕘𝕚𝕖𝕤 ᎓ 17 | 18 | HTML  19 | CSS  20 | JavaScript  21 | React  22 | npm  23 | Figma  24 | VSCode  25 | 26 | ※ styled-components: Styling library for React components. 27 | ※ react-icons: to use icons in React. 28 | ※ react-loader-spinner: to display standby mode. 29 | ※ react-scroll: for a smooth twist. 30 | ※ react-router-dom: to navigate between different parts of the web application. 31 | 32 | ## 𝕀𝕟𝕤𝕥𝕒𝕝𝕝𝕒𝕥𝕚𝕠𝕟 ᎓ 33 | 34 | To get started with this project, follow the installation instructions below. 35 | 36 | 1. Clone the repository: 37 | ```bash 38 | git clone https://github.com/Morifer79/react-movie-search.git 39 | cd react-movie-search-app 40 | ``` 41 | 2. Install the dependencies: 42 | ```bash 43 | npm install 44 | ``` 45 | 3. Start the development server: 46 | ```bash 47 | npm start 48 | ``` 49 | 4. Open to view it in the browser: 50 | Badge 51 | 52 | ## 𝔼𝕞𝕒𝕚𝕝 𝕞𝕖 ᎓ 53 | Questions, suggestions, help: 54 | Mail Badge 55 | 56 | 57 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 23 | 24 | 33 | React App 34 | 35 | 36 | 64 | 65 | 66 | 67 | 68 |
69 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/pages/MovieDetails/MovieDetails.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, Suspense } from 'react'; 2 | import { Outlet, useParams, useLocation } from 'react-router-dom'; 3 | import { fetchSearchDetail } from '../../services/api'; 4 | import { Loader } from 'components/Loader/Loader'; 5 | import { BsCaretLeftFill } from 'react-icons/bs'; 6 | import default_image from '../../images/default_image.png'; 7 | import { 8 | Card, 9 | Hr, 10 | IconMasks, 11 | IconScroll, 12 | Info, 13 | InfoList, 14 | Item, 15 | PageWrapper, 16 | ReturnBtn, 17 | SubInfoLink, 18 | SubInfoList, 19 | SubInfoTitle, 20 | SubInfoWrapper, 21 | Thumb, 22 | Title, 23 | } from './MovieDetails.styled'; 24 | 25 | const MovieDetails = () => { 26 | const { movieId } = useParams(); 27 | const [movie, setMovie] = useState(null); 28 | const [isLoading, setIsLoading] = useState(false); 29 | const [isError, setIsError] = useState(null); 30 | const location = useLocation(); 31 | const goBack = useRef(location?.state?.from ?? '/'); 32 | 33 | useEffect(() => { 34 | const abortController = new AbortController(); 35 | const getDetail = async () => { 36 | try { 37 | setIsLoading(true); 38 | setIsError(null); 39 | const detailsInfo = await fetchSearchDetail(movieId, { 40 | signal: abortController.signal, 41 | }); 42 | setMovie(detailsInfo); 43 | setIsError(null); 44 | } catch (error) { 45 | setIsError(error.message); 46 | } finally { 47 | setIsLoading(false); 48 | } 49 | }; 50 | getDetail(); 51 | 52 | return () => { 53 | abortController.abort(); 54 | }; 55 | }, [movieId]); 56 | 57 | const IMAGE_URL = 'https://image.tmdb.org/t/p/w500/'; 58 | const { title, release_date, overview, vote_average, poster_path } = 59 | movie || {}; 60 | const imageSrc = poster_path ? IMAGE_URL + poster_path : default_image; 61 | const userScore = Math.round((Number(vote_average) * 100) / 10); 62 | 63 | return ( 64 | <> 65 | {isLoading && } 66 | {isError &&

{isError}

} 67 | 68 | {!isLoading && movie && ( 69 | <> 70 | 71 | 72 | Go Back 73 | 74 | 75 | 76 | {title} 77 | 78 | 79 | 80 | {title} ({release_date.slice(0, 4)}) 81 | 82 | 83 | 84 |

85 | User Score: {userScore}% 86 |

87 |
88 | 89 |

Overview

90 |

{overview}

91 |
92 | 93 |

Genres

94 |

{movie.genres.map(genre => genre.name).join(' ')}

95 |
96 |
97 |
98 |
99 | 100 | Additional information 101 | 102 | 103 | Cast 104 | 105 | 106 | Reviews 107 | 108 | 109 | 110 |
111 | 112 | )} 113 | 114 | 115 | 116 |
117 | 118 | ); 119 | }; 120 | 121 | export default MovieDetails; 122 | -------------------------------------------------------------------------------- /src/pages/MovieDetails/MovieDetails.styled.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Link, NavLink } from 'react-router-dom'; 3 | import { BsCameraReels } from 'react-icons/bs'; 4 | import { FaMasksTheater, FaScroll } from 'react-icons/fa6'; 5 | 6 | export const PageWrapper = styled.section` 7 | padding: ${p => p.theme.spacing(5)}; 8 | `; 9 | 10 | export const ReturnBtn = styled(NavLink)` 11 | padding: 7px 10px; 12 | margin-left: ${p => p.theme.spacing(8)}; 13 | margin-bottom: ${p => p.theme.spacing(2)}; 14 | 15 | display: inline-flex; 16 | align-items: center; 17 | cursor: pointer; 18 | outline: none; 19 | color: ${p => p.theme.colors.white}; 20 | border: transparent; 21 | border-radius: ${p => p.theme.radii.md}; 22 | font-weight: 600; 23 | box-shadow: 0 0 13px 3px ${p => p.theme.colors.bernred}; 24 | transition: color 300ms linear, box-shadow 300ms linear; 25 | 26 | &:hover, 27 | &:focus { 28 | color: ${p => p.theme.colors.bernred}; 29 | box-shadow: 0 0 13px 7px ${p => p.theme.colors.bernred}; 30 | } 31 | `; 32 | 33 | export const Card = styled.div` 34 | display: flex; 35 | flex-direction: row; 36 | gap: ${p => p.theme.spacing(20)}; 37 | padding: ${p => p.theme.spacing(10)}; 38 | `; 39 | 40 | export const Thumb = styled.div` 41 | width: 270px; 42 | overflow: hidden; 43 | 44 | img { 45 | border: 1px solid ${p => p.theme.colors.bernred}; 46 | border-radius: ${p => p.theme.radii.md}; 47 | transition: 1s; 48 | transform: scale(1); 49 | &:hover { 50 | transform: scale(1.02); 51 | } 52 | } 53 | `; 54 | 55 | export const Info = styled.div` 56 | width: 500px; 57 | text-align: justify; 58 | `; 59 | 60 | export const Title = styled.h2` 61 | margin-bottom: ${p => p.theme.spacing(8)}; 62 | 63 | font-family: 'Bad Script'; 64 | font-size: 28px; 65 | --interval: 5000ms; 66 | display: block; 67 | text-shadow: 0 0 10px var(--color1), 0 0 20px var(--color2), 68 | 0 0 40px var(--color3), 0 0 80px var(--color4); 69 | will-change: filter, color; 70 | filter: saturate(60%); 71 | 72 | animation: flicker steps(100) var(--interval) 1000ms infinite; 73 | color: lightpink; 74 | --color1: pink; 75 | --color2: orangered; 76 | --color3: red; 77 | --color4: magenta; 78 | 79 | @keyframes flicker { 80 | 50% { 81 | color: ${p => p.theme.colors.white}; 82 | filter: saturate(200%) hue-rotate(20deg); 83 | } 84 | } 85 | `; 86 | 87 | export const InfoList = styled.ul` 88 | h3 { 89 | margin-bottom: ${p => p.theme.spacing(8)}; 90 | 91 | font-family: 'Bad Script'; 92 | font-size: 22px; 93 | --interval: 5000ms; 94 | display: block; 95 | text-shadow: 0 0 10px var(--color1), 0 0 20px var(--color2), 96 | 0 0 40px var(--color3), 0 0 80px var(--color4); 97 | will-change: filter, color; 98 | filter: saturate(60%); 99 | 100 | animation: flicker steps(100) var(--interval) 1s infinite; 101 | color: lightpink; 102 | --color1: pink; 103 | --color2: orangered; 104 | --color3: red; 105 | --color4: magenta; 106 | 107 | @keyframes flicker { 108 | 50% { 109 | color: ${p => p.theme.colors.white}; 110 | filter: saturate(200%) hue-rotate(20deg); 111 | } 112 | } 113 | } 114 | `; 115 | 116 | export const Item = styled.li` 117 | p span { 118 | padding-left: ${p => p.theme.spacing(2.5)}; 119 | padding-right: ${p => p.theme.spacing(2.5)}; 120 | margin-left: ${p => p.theme.spacing(5)}; 121 | 122 | font-weight: 900; 123 | vertical-align: middle; 124 | color: ${p => p.theme.colors.white}; 125 | width: 50px; 126 | height: 20px; 127 | border-radius: ${p => p.theme.radii.md}; 128 | background-color: ${p => p.theme.colors.black}; 129 | box-shadow: 0 0 20px 2px ${p => p.theme.colors.bernred}; 130 | } 131 | 132 | &:not(:last-child) { 133 | margin-bottom: ${p => p.theme.spacing(10)}; 134 | } 135 | `; 136 | 137 | export const CameraReels = styled(BsCameraReels)` 138 | margin-right: ${p => p.theme.spacing(8)}; 139 | `; 140 | 141 | export const SubInfoWrapper = styled.div` 142 | padding-left: ${p => p.theme.spacing(10)}; 143 | `; 144 | 145 | export const SubInfoTitle = styled.h3` 146 | padding-left: ${p => p.theme.spacing(20)}; 147 | margin-bottom: ${p => p.theme.spacing(10)}; 148 | `; 149 | 150 | export const SubInfoList = styled.ul` 151 | display: flex; 152 | gap: ${p => p.theme.spacing(10)}; 153 | margin-bottom: ${p => p.theme.spacing(15)}; 154 | `; 155 | 156 | export const SubInfoLink = styled(Link)` 157 | padding: 8px 5px; 158 | 159 | text-align: center; 160 | border: solid 1px ${p => p.theme.colors.bernred}; 161 | border-radius: ${p => p.theme.radii.md}; 162 | width: 125px; 163 | display: block; 164 | transition: color 300ms linear, box-shadow 300ms linear; 165 | 166 | &:hover { 167 | color: ${p => p.theme.colors.bernred}; 168 | box-shadow: 0 0 13px 3px ${p => p.theme.colors.bernred}; 169 | } 170 | `; 171 | 172 | export const IconMasks = styled(FaMasksTheater)` 173 | vertical-align: sub; 174 | height: 20px; 175 | width: 20px; 176 | `; 177 | 178 | export const IconScroll = styled(FaScroll)` 179 | vertical-align: sub; 180 | height: 20px; 181 | width: 20px; 182 | `; 183 | 184 | export const Hr = styled.div` 185 | border-top: 1px solid ${p => p.theme.colors.bernred}; 186 | box-shadow: 0 0 13px 3px ${p => p.theme.colors.bernred}; 187 | `; 188 | --------------------------------------------------------------------------------