diff --git a/index.html b/index.html index 44fcd2a..2c761f8 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,15 @@ - - - - Hello World - - -
- - - + + + + + OMDb Movie search + + + +
+ + + + \ No newline at end of file diff --git a/src/App.js b/src/App.js index 9520f77..1a34df5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,14 +1,142 @@ import React from 'react'; +import Header from './Header'; +import Search from './Search'; +import Movies from './Movies'; +import Loading from './elements/Loading'; +import BackToTop from './elements/BackToTop'; + + class App extends React.Component { - constructor(){ + constructor() { super(); + this.state = { + search: [], + searchTerm: "", + page: 2, + watchList: localStorage.getItem("watchList") ? localStorage.getItem("watchList") : [], + watchListSection: false + } + this.receiverSearch = this.receiverSearch.bind(this); + this.receiverWatchList = this.receiverWatchList.bind(this); + this.receiverDisplayWatchlist = this.receiverDisplayWatchlist.bind(this); + this.handleScroll = this.handleScroll.bind(this); + } + + handleScroll() { + const windowHeight = "innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight; + const body = document.body; + const html = document.documentElement; + const docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); + const windowBottom = windowHeight + window.pageYOffset; + const backToTop = body.querySelector("#back-to-top"); + + const loading = document.querySelector('#search-results__loading'); + const OMBbAPIKey = "aba7af8e"; + const query = 'http://www.omdbapi.com/?s=' + this.state.searchTerm + '&apikey=' + OMBbAPIKey + '&page=' + this.state.page; + + // Back to top button + if (body.scrollTop > 300 || html.scrollTop > 300) { + backToTop.classList.add('visible'); + } else { + backToTop.classList.remove('visible'); + } + + // Infinite scroll + if (windowBottom >= docHeight) { + fetch(query) + .then(response => { + loading.textContent = "Loading..."; + loading.classList.add('show'); + return response.json(); + } + ) + .then(movies => { + if (movies.Search) { + const self = this; + setTimeout(function () { + self.setState({ + search: [...self.state.search, ...movies.Search], + page: self.state.page += 1 + }); + loading.classList.remove('show'); + }, 500) + } else { + loading.textContent = "No more results"; + loading.classList.add('show'); + } + }) + .catch(error => { + console.log(error); + loading.textContent = "There's been a problem fetching the results. Try again later.."; + loading.classList.add('show'); + }); + + } + } + + componentDidMount() { + window.addEventListener("scroll", this.handleScroll); + } + + receiverSearch(movies, searchTerm) { + this.setState({ + search: movies, + searchTerm: searchTerm, + watchListSection: false + }) + } + + receiverDisplayWatchlist() { + this.setState({ + watchListSection: true + }) + } + + receiverWatchList(articleTitle, action) { + if (action === "addToWatchList") { + this.state.search.forEach(articleObj => { + return Object.keys(articleObj).find(articleKey => { + if (articleObj[articleKey] === articleTitle) { + this.setState({ + watchList: [...this.state.watchList, articleObj] + }) + + localStorage.getItem("watchList") + ? localStorage.setItem("watchList", JSON.stringify([articleObj, ...JSON.parse(localStorage.getItem("watchList"))])) + : localStorage.setItem("watchList", JSON.stringify([...this.state.watchList, articleObj])); + } + }); + }); + } else { + const index = JSON.parse(this.state.watchList).forEach((item, index) => { + if (item.Title === articleTitle) { + // console.log("Aquis", this.state.watchList); + this.setState({ + watchList: JSON.parse(this.state.watchList).splice(index, 1) + }) + localStorage.setItem("watchList", this.state.watchList); + // console.log("Aquis", localStorage.getItem("watchList")); + } + }); + } } - render(){ + render() { + const watchList = this.state.watchListSection; + let MoviesList; + if (watchList) { + MoviesList = ; + } else { + MoviesList = ; + } return ( -
- React cinema app +
+
+ + {MoviesList} + +
) } diff --git a/src/Header.js b/src/Header.js new file mode 100644 index 0000000..d89bca8 --- /dev/null +++ b/src/Header.js @@ -0,0 +1,21 @@ +import React from 'react'; +class Header extends React.Component { + constructor(props) { + super(props); + this.handleWatchList = this.handleWatchList.bind(this); + } + handleWatchList() { + document.querySelectorAll('.movie-info__moreinfo').forEach(item => item.classList.remove("disabled")); + this.props.receiver(); + } + render() { + return ( + +
+

{this.props.title}

+ +
+ ) + } +} +export default Header; \ No newline at end of file diff --git a/src/Movie.js b/src/Movie.js new file mode 100644 index 0000000..1839d99 --- /dev/null +++ b/src/Movie.js @@ -0,0 +1,91 @@ +import React from 'react'; + +class Movie extends React.Component { + constructor(props) { + super(props); + this.movieFetch = this.movieFetch.bind(this); + this.handleWatchList = this.handleWatchList.bind(this); + }; + movieFetch(e) { + const evtTarget = e.target; + const movieParentNode = evtTarget.parentNode; + const OMBbAPIKey = "aba7af8e"; + fetch('http://www.omdbapi.com/?t=' + this.props.title + '&apikey=' + OMBbAPIKey) + .then(response => { + return response.json(); + }) + .then(data => { + // Show/hide the list + if (movieParentNode.parentNode.querySelector(".movie-info__moreinfo-details")) { + // Remove list + movieParentNode.parentNode.querySelector(".movie-info__moreinfo-details").remove(); + // Update button text + evtTarget.textContent = "Info"; + } else { + // Create list with desired movie details + const infoToDisplay = [ + "Genre", + "Plot", + "Runtime", + "Awards", + "Language" + ]; + const html = Object.keys(data) + .map(function (key) { + if (infoToDisplay.indexOf(key) !== -1) { + return `
  • ${key}: ${data[key]}
  • `; + } + }) + .join(""); + + // Update button text + evtTarget.textContent = "Close"; + + // Append list + const moreInfoList = document.createElement("ul"); + moreInfoList.setAttribute("class", "movie-info__moreinfo-details"); + moreInfoList.innerHTML = html; + movieParentNode.parentNode.append(moreInfoList); + } + }) + .catch(error => { + console.error(error); + }); + } + handleWatchList(e, action) { + const evtTarget = e.target; + if (!evtTarget.classList.contains('disabled')) { + evtTarget.classList.add("disabled"); + const articleTitle = evtTarget.getAttribute("data-title"); + this.props.receiver(articleTitle, action); + } + } + render() { + const watchlistbutton = this.props.watchlistbutton; + return ( +
    +
    + {this.props.title} +
    +
    +

    {this.props.title}

    +
    Year: { + this.props.year + }
    + IMDb ↗ + + Watch trailer ↗ + + {watchlistbutton + ? + : } + +
    +
    + ); + }; +} + +export default Movie; \ No newline at end of file diff --git a/src/Movies.js b/src/Movies.js new file mode 100644 index 0000000..bf520b2 --- /dev/null +++ b/src/Movies.js @@ -0,0 +1,42 @@ +import React from 'react'; +import Movie from './Movie'; + + +class Movies extends React.Component { + constructor() { + super(); + this.watchListReceiver = this.watchListReceiver.bind(this); + } + + watchListReceiver(title, action) { + this.props.receiver(title, action); + } + + render() { + + console.log(this.props.movies.length); + console.log(this.props.movies); + return ( + this.props.movies.length > 0 + ? +
    +
    + {this.props.movies.map((movie) => { + return ; + })} +
    +
    + : + "" + ); + } +} + +export default Movies; \ No newline at end of file diff --git a/src/Search.js b/src/Search.js new file mode 100644 index 0000000..e172254 --- /dev/null +++ b/src/Search.js @@ -0,0 +1,85 @@ +import React from 'react'; + +class Search extends React.Component { + constructor() { + super(); + this.state = { + search: "", + searchTerm: "" + } + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + this.fetchMovies = this.fetchMovies.bind(this); + }; + + fetchMovies() { + const OMBbAPIKey = "aba7af8e"; + const loading = document.querySelector('#search-results__loading'); + fetch('http://www.omdbapi.com/?s=' + this.state.search + '&apikey=' + OMBbAPIKey) + .then(response => { + loading.textContent = "Loading.."; + loading.classList.add('show'); + return response.json(); + }) + .then(result => { + loading.classList.remove('show'); + this.props.receiver(result.Search, this.state.searchTerm); + }) + .catch(error => { + loading.textContent = "There's been a problem fetching the results. Try again later..."; + loading.classList.add('show'); + console.log(error); + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.fetchMovies(event.target.value); + } + + handleChange(event) { + event.preventDefault(); + + this.setState({ + search: event.target.value, + searchTerm: event.target.value + }); + } + + render() { + return ( +
    +
    + + {/*
      +
    */} + + {/* +
    + + + + + +
    */} +
    +
    + ); + } +} + +export default Search; \ No newline at end of file diff --git a/src/css/style.css b/src/css/style.css new file mode 100644 index 0000000..48cff9d --- /dev/null +++ b/src/css/style.css @@ -0,0 +1,327 @@ +/* font-family: 'Source Sans Pro', sans-serif; */ +@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro"); + +:root { + --primary-color: #f1f1f1; + --secondary-color: #fdb813; +} + +body { + font-family: "Source Sans Pro", sans-serif; + margin: 0; + padding: 0; + font-size: 16px; +} + +input[type=text]:focus { + box-shadow: 0 0 5px rgba(253, 184, 19, 1); + border: 1px solid rgba(253, 184, 19, 1); +} + +.container { + width: 100%; + max-width: 100%; +} + +header.main-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +header.main-header .watch-list__button{ + max-height: 40px; + background-color: #000; + color: var(--secondary-color); + margin-right: 30px; +} + +@media(min-width: 768px){ + header.main-header .watch-list__button{ + margin-right: 0; + } +} + +h1.main-heading { + padding-left: 30px; +} + +@media (min-width: 960px) { + h1.main-heading { + padding-left: 0; + } +} + +/* Search form */ +.search-form { + /* margin-bottom: 60px; */ + background-color: var(--primary-color); + padding: 30px; + margin-bottom: 50px; +} + +@media (min-width: 960px) { + .container { + width: 80%; + max-width: 1200px; + margin: 0 auto; + } +} + +.movie-request__form { + display: block; + position: relative; +} +@media (min-width: 768px) { + .movie-request__form { + display: flex; + flex-wrap: wrap; + } +} + +.search-form input.movie-request__input { + width: calc(100% - 36px); + padding: 15px; + font-size: 1.1rem; + color: var(--secondary-color); +} + +.movie-request__submit { + width: 100%; + padding: 20px; + border: none; + background-color: var(--secondary-color); + /* color: #fff; */ + font-size: 0.875rem; +} + +.movie-request__submit:hover { + cursor: pointer; + opacity: .8; +} + +@media (min-width: 768px) { + .movie-request__submit { + width: 100px; + } + .search-form input.movie-request__input { + width: calc(100% - 140px); + } +} + +/* Autocomplete */ +.autocomplete { + width: calc(100% - 25px); + position: absolute; + top: 58px; + background-color: #fff; + margin: 0; + left: 1px; + list-style-type: none; + padding-left: 20px; +} +.autocomplete li { + line-height: 30px; +} +.autocomplete li:hover { + color: #666; + cursor: pointer; +} + +/* Search filters */ +.movie-request__filters-list { + width: 100%; + margin-top: 10px; + padding-left: 9px; +} +.movie-request__filters-list.hidden { + display: none; +} +.movie-request__filters-list select { + margin-right: 10px; +} +.movie-request__filters-list label { + font-size: 0.875rem; +} + +.movie-request__filters-toggle { + border: none; + background-color: transparent; + margin-top: 10px; + font-size: 0.875rem; +} + +.movie-request__filters-toggle:hover { + cursor: pointer; + opacity: 0.7; +} + +/* Movies list */ +.movie-list-wrapper { + display: grid; + grid-template-columns: 100%; + padding: 30px; + margin-bottom: 50px; + background-color: var(--primary-color); +} + +.movie-details { + display: grid; + grid-template-columns: 100%; + + margin-bottom: 20px; + padding-bottom: 30px; + border-bottom: 1px solid rgb(0, 0, 0, 0.1); + animation: fade-in 4s 1; +} + +.movie-info { + grid-column: 1/4; +} + +@media (min-width: 480px) { + .movie-details { + grid-template-columns: 200px auto; + margin-bottom: 40px; + } + .movie-info { + grid-column: 2/4; + } +} +@media (min-width: 768px) { + .movie-details { + padding-right: 35px; + } + .movie-list-wrapper { + grid-template-columns: 50% 50%; + } + + .movie-details { + grid-template-columns: 170px auto; + margin-bottom: 40px; + } +} + +.movie-info__moreinfo { + display: block; + margin-top: 10px; + margin-bottom: 10px; + padding: 7px 16px; + border: none; + background-color: var(--secondary-color); + /* color: #fff; */ + font-size: 0.875rem; +} + +.movie-info__moreinfo:hover { + cursor: pointer; + opacity: .8; +} + +.movie-info__moreinfo.disabled { + cursor: default; + opacity: .2; +} + +.movie-info__title { + border-bottom: 2px solid var(--secondary-color); +} +.movie-info__imdb-link { + display: block; + color: #000; +} + +.movie-poster img { + width: 100%; + max-width: 100%; +} + +@media (min-width: 480px) { + .movie-poster img { + width: 100%; + max-width: 150px; + } +} + +/* Movie more info details */ +ul.movie-info__moreinfo-details { + grid-column: 1/4; + + list-style-type: none; + padding-left: 0; +} + +/* Infnitie scroll loading */ +.search-results__loading { + display: none; + text-align: center; + margin-bottom: 50px; +} +.search-results__loading.show { + display: block; +} + +/* Back To Top */ +.back-to-top { + display: none; +} + +.back-to-top.visible { + display: flex; + align-items: center; + justify-content: space-around; + + background-color: var(--secondary-color); + border-radius: 50%; + border: none; + width: 40px; + height: 40px; + position: fixed; + bottom: 20px; + right: 20px; + animation: fade-in 1s 1; +} + +.back-to-top:hover { + cursor: pointer; + opacity: .8; +} + +/* Errors */ +.error-box { + display: none; + width: calc(100% - 44px); + color: #fff; + background-color: tomato; + padding: 10px 20px; +} + +/* Pagination */ +.pagination { + list-style-type: none; + visibility: hidden; + display: flex; + justify-content: flex-end; + width: 100%; + padding-left: 0; +} + +.pagination-index { + margin: 0 20px; +} + +/* .pagination li { +} */ + +.pagination li:hover { + cursor: pointer; + opacity: 0.7; +} + + +/* ANIMATIONS */ + +@keyframes fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } +} \ No newline at end of file diff --git a/src/elements/BackToTop.js b/src/elements/BackToTop.js new file mode 100644 index 0000000..fc9acf0 --- /dev/null +++ b/src/elements/BackToTop.js @@ -0,0 +1,16 @@ +import React from 'react'; + +function BackToTop() { + function handleBackToTop() { + window.scrollTo(0, 0); + }; + return ( + + ) +} + +export default BackToTop; \ No newline at end of file diff --git a/src/elements/Loading.js b/src/elements/Loading.js new file mode 100644 index 0000000..c7f7f7d --- /dev/null +++ b/src/elements/Loading.js @@ -0,0 +1,9 @@ +import React from 'react'; + +function Loading(props) { + return ( +
    Loading...
    + ) +} + +export default Loading; \ No newline at end of file