DEV Community

Emmanuel Okiche
Emmanuel Okiche

Posted on

React throwaway app 2: Movie Search App

In the first article, I introduced you to the aim of the series and you built a currency converter. In this one, you would be building a movie search app.

The Rules (just to remind you)

  • Your app should be completed within 60 minutes (depending on the complexity).
  • Must be pure React (no react-router or redux).
  • Must delete the project after one week. Why? These are basic apps you should be able to build anytime and not worthy of showcasing as a portfolio for a serious job interview.
  • Don't spend much time on designing. Remember, the idea is to check if you think in React. You could style to your taste after 60 minutes.
  • Don't look at my solution until you have completed yours. Else, you would be struck with 5 years of 'tutorial purgatory'

App 2 - Movie Search App

  • Build a Movie App that connects to an external api.
  • Search for movies movies and select one to display.
  • Duration should be within 1 - 2 hours (including styling).

Here is a screenshot of what i expect you to build:

movie screenshot

This app would show that you understand how:

  • components and states work
  • to request data from an api
  • component life cycle methods
  • to use events
  • to update your UI based on state change

Your time starts now! Remember not to look at my solution until you're done with yours.

My Solution

I used the OMDb API to get my movie data. You have to get an api key (it is free). I must confess, i spent above 60 minutes to complete this because i had to get familiar with the api by playing around with different requests on PostMan. As always, i used create-react-app to generate my project.

To structure my app, i had to decide what would be containers and components.

app structure

Here is my folder structure:
folder struture

MovieCard.js:

This component is used to display the selected movie. It receives its movie data via props.

import React from 'react';

import './MovieCard.css';

const MovieCard = (props) => {
    return (
        <div className="container">
            <div className="movie-card">
                <div className="movie-header" style={{ backgroundImage: `url(${props.movie.Poster})` }}>
                </div>
                <div className="movie-content">
                    <div className="movie-content-header">
                        <h3 className="movie-title">{props.movie.Title}</h3>
                    </div>
                    <div className="movie-info">
                        <div className="info-section">
                            <label>Released</label>
                            <span>{props.movie.Released}</span>
                        </div>
                        <div className="info-section">
                            <label>IMDB Rating</label>
                            <span>{props.movie.imdbRating}</span>
                        </div>
                        <div className="info-section">
                            <label>Rated</label>
                            <span>{props.movie.Rated}</span>
                        </div>
                        <div className="info-section">
                            <label>Runtime</label>
                            <span>{props.movie.Runtime}</span>
                        </div>
                    </div>
                    <div className="plot" style={{fontSize: '12px'}}>
                        <p>{props.movie.Plot}</p>
                    </div>
                </div>
            </div>
        </div>
    );
};

export default MovieCard;
Enter fullscreen mode Exit fullscreen mode

MovieCard.css:


.container {
    display: flex;
    flex-wrap: wrap;
    max-width: 100%;
    margin-left: auto;
    margin-right: auto;
    justify-content: center;
}

.movie-card {
    background: #ffffff;
    box-shadow: 0px 6px 18px rgba(0,0,0,.1);
    width: 100%;
    max-width: 290px;
    margin: 2em;
    border-radius: 10px;
    display:inline-block;
    z-index: 10;
}

.movie-header {
    padding:0;
    margin: 0;
    height: 434px;
    width: 100%;
    display: block;
    border-top-left-radius: 10px;
    border-top-right-radius:10px;
    background-size: cover;
}

.movie-content {
    padding: 18px 18px 24px 18px;
    margin: 0;
}

.movie-content-header, .movie-info {
    display: table;
    width: 100%;
}

.movie-title {
    font-size: 24px;
    margin: 0;
    display: table-cell;
    cursor: pointer;
}

.movie-title:hover {
    color:rgb(228, 194, 42);
}

.movie-info {
    margin-top: 1em;
}

.info-section {
    display: table-cell;
    text-transform: uppercase;
    text-align:center;
}

.info-section:first-of-type {
    text-align:left;
}

.info-section:last-of-type {
    text-align:right;
}

.info-section label {
    display: block;
    color: rgba(0,0,0,.5);
    margin-bottom: .5em;
    font-size: 9px;
}

.info-section span {
    font-weight: 700;
    font-size: 11px;
}

@media only screen and (max-width: 400px) {
    .movie-header {
        height: 400px;
    }
}
Enter fullscreen mode Exit fullscreen mode

Search.js

Next, we have the Search component which contains the search input and the returned list of result.
Here is the Search.js:

import React from 'react';

import './Search.css';

const Search = (props) => {
    let resultList = null

    if (props.searching && (props.defaultTitle !== '')) {
        resultList = (
            <ul className="results">
                {props.results.map(item => (
                    <li key={item.imdbID} onClick={() => props.clicked(item)}>
                        <img src={item.Poster} alt="Movie Poster"/>
                        {item.Title}
                    </li>
                ))}
            </ul>
        )
    }

    return (
        <div className="search">
            <input type="search" name="movie-search" value={props.defaultTitle} onChange={props.search} />
            {resultList}
        </div>
    );
};

export default Search;

Enter fullscreen mode Exit fullscreen mode

Search.css

.search {
    position: relative;
    margin: 0 auto;
    width: 300px;
    margin-top: 10px;
}

.search input {
    height: 26px;
    width: 100%;
    padding: 0 12px 0 25px;
    background: white;
    border: 1px solid #babdcc;
    border-radius: 13px;
    box-sizing: border-box;
    box-shadow: inset 0 1px #e5e7ed, 0 1px 0 #fcfcfc;
}

.search input:focus {
    outline: none;
    border-color: #66b1ee;
    box-shadow: 0 0 2px rgba(85, 168, 236, 0.9);
}


.search .results {
    display: block;
    position: absolute;
    top: 35px;
    left: 0;
    right: 0;
    z-index: 20;
    padding: 0;
    margin: 0;
    border-width: 1px;
    border-style: solid;
    border-color: #cbcfe2 #c8cee7 #c4c7d7;
    border-radius: 3px;
    background-color: #fdfdfd;
}

.search .results li { 
    display: flex;
    align-items: center;
    padding: 5px;
    border-bottom: 1px solid rgba(88, 85, 85, 0.3);
    text-align: left;
    height: 50px;
    cursor: pointer;
}

.search .results li img { 
    width: 30px;
    margin-right: 5px;
}

.search .results li:hover { 
    background: rgba(88, 85, 85, 0.1);
}
Enter fullscreen mode Exit fullscreen mode

MovieSearch.js

I made MovieSearch to be a stateful component because i want to manage all my states there and pass the data to other components via props. First, make sure you get your api key from omdb api.
Here is my MovieSearch.js container:

import React, { Component } from 'react';
import axios from 'axios';

import MovieCard from '../../components/MovieCard/MovieCard';
import Search from '../../components/Search/Search';

class MovieSearch extends Component {
    state = {
        movieId: 'tt1442449', // default imdb id (Spartacus)
        title: '',
        movie: {},
        searchResults: [],
        isSearching: false,
    }

    componentDidMount() {
        this.loadMovie()
    }

    componentDidUpdate(prevProps, prevState) {
        if (prevState.movieId !== this.state.movieId) {
            this.loadMovie()
        }
    }

    loadMovie() {
        axios.get(`http://www.omdbapi.com/?apikey=YOUR_API_KEY&i=${this.state.movieId}`)
            .then(response => {
                this.setState({ movie: response.data });
            })
            .catch(error => {
                console.log('Opps!', error.message);
            })
    }

    // we use a timeout to prevent the api request to fire immediately as we type
    timeout = null;

    searchMovie = (event) => {
        this.setState({ title: event.target.value, isSearching: true })

        clearTimeout(this.timeout);

        this.timeout = setTimeout(() => {
            axios.get(`http://www.omdbapi.com/?apikey=YOUR_API_KEY&s=${this.state.title}`)
                .then(response => {

                    if (response.data.Search) {
                        const movies = response.data.Search.slice(0, 5);
                        this.setState({ searchResults: movies });
                    }
                })
                .catch(error => {
                    console.log('Opps!', error.message);
                })
        }, 1000)


    }

    // event handler for a search result item that is clicked
    itemClicked = (item) => {
        this.setState(
            {
                movieId: item.imdbID,
                isSearching: false,
                title: item.Title,
            }
        )
    }


    render() {
        return (
            <div onClick={() => this.setState({ isSearching: false })}>
                <Search
                    defaultTitle={this.state.title}
                    search={this.searchMovie}
                    results={this.state.searchResults}
                    clicked={this.itemClicked}
                    searching={this.state.isSearching} />

                <MovieCard movie={this.state.movie} />
            </div>
        );
    }
}

export default MovieSearch;
Enter fullscreen mode Exit fullscreen mode

This container is used to handle the state and update changes in our application.
The code above simply loads a an initial movie when it mounts. Whenever we search and update the movieId state, it updates the content of the MovieCard via props.

Conclusion

You might think that this was a little bit rushed. Remember, this is not a tutorial but a challenge for beginners that feel they can think in React. My code was just a guide. Thanks for reading and i hope to see you in the next part.

I don't think i would throw this one away ;)

Link to Part 1: Here

Oldest comments (6)

Collapse
 
kauresss profile image
Kauress

cool i like these, I'm building my own component library so helps to see how others think and work

Collapse
 
fleepgeek profile image
Emmanuel Okiche

Thanks. I'm glad you liked it.

Collapse
 
ibibgor profile image
Oscar

I just looked at your solution and the first thing I noticed is that I would have split the infosection into a new component. I haven't looked deeper but just want to mention this.
Nonetheless I like the idea to create a simple app just to practice.

Collapse
 
fleepgeek profile image
Emmanuel Okiche • Edited

Thanks for your feedback. I really appreciate it. You make a good point.
The aim of this series is just to build basic stuffs fast (within the time limit) and throw they away. This is to test if beginners thin in React.
The final version on my computer has some improvements. Thanks once again and your comment shows that you "Think in React."

Collapse
 
github743 profile image
Kaashyap

Can I get the source code?

Collapse
 
fleepgeek profile image
Emmanuel Okiche

Unfortunately i don't have the source hosted on github since this is a throwaway app but that's the entire source in the tutorial ;)