<file_sep>import React from 'react';
import {withRouter} from 'react-router';
import {connect} from 'react-redux';
import {getMovieDetail} from '../redux/actions';
import {Movie} from '../components/Movie';
import {Content} from '../components/Content';

import { findMovieDirector, getAllDirectedMovies } from '../client/api/helpers';

export class MoviePage extends React.Component {
    constructor(props) {
        super(props);

        this.goSearch = this.goSearch.bind(this);
        this.loadMovie = this.loadMovie.bind(this);

        if (this.props.match.params.title) {
            this.loadMovie(this.props.match.params.title);
        }
    }

    loadMovie(title) {
        if (title) {
            let [id, ...junk] = title.split('-');

            this.props.getDetail(id);
        }
    }

    goSearch(e) {
        e.preventDefault();
        this.props.history.push('/search');
    }

    componentWillUpdate(nextProps) {
        if (nextProps.match.params.title !== this.props.match.params.title) {
            this.loadMovie(nextProps.match.params.title);
        }
    }

    componentDidUpdate() {
        window.scrollTo(0, 0);
    }

    render() {
        return (
            <div>
                <Movie movie={this.props.movie} goSearch={this.goSearch}/>
                <div>Films by {findMovieDirector(this.props.movie).name}</div>
                <Content movies={getAllDirectedMovies(this.props.director)}/>
            </div>
        )
    }
}

const mapStateToProps = state => ({
    movie: state.movie,
    director: state.director
});

const mapDispatchToProps = dispatch => ({
    getDetail: (id) => dispatch(getMovieDetail(id, dispatch))
});

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MoviePage));
<file_sep>import {
    MOVIES_FETCH_SUCCESS,
    MOVIES_FETCH_FAILURE,
    MOVIES_SORT_CHANGE,
    MOVIE_FETCH_SUCCESS,
    MOVIE_FETCH_FAILURE,
    DIRECTOR_INFO_FETCH_SUCCESS,
    DIRECTOR_INFO_FETCH_FAILURE,
    QUERY_SET,
    QUERY_SET_TYPE
} from './types';

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const initialState = {
    movie: {
        credits: {
            crew: []
        },
        genres: []
    },
    id: 1,
    director: {
        movie_credits: {
            crew: []
        }
    },
    movies: [],
    selectedSearchType: 'title',
    query: '',
    oldValue: ''
};

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case MOVIE_FETCH_SUCCESS:
            return {
                ...state,
                id: action.payload.id,
                movie: action.payload.result
            };
        case MOVIE_FETCH_FAILURE: {
            console.warn(action.payload);
            return {...state};
        }
        case MOVIES_FETCH_SUCCESS:
            return {
                ...state,
                query: action.payload.query,
                movies: action.payload.results,
            };
        case MOVIES_FETCH_FAILURE: {
            console.warn(action.payload);
            return {...state};
        }
        case DIRECTOR_INFO_FETCH_SUCCESS:
            return {
                ...state,
                director: action.payload.result
            };
        case DIRECTOR_INFO_FETCH_FAILURE: {
            console.warn(action.payload);
            return {...state};
        }
        case MOVIES_SORT_CHANGE:
            return {
                ...state,
                sortType: action.payload,
                movies: state.results,
            };
        case QUERY_SET:
            return {
                ...state,
                query: action.payload.query,
                oldValue: action.payload.oldQuery
            };
        case QUERY_SET_TYPE:
            return {
                ...state,
                selectedSearchType: action.payload
            };
        default:
            return state;
    }
};

function createStoreInstance(initialState) {
    let store;

    store = createStore(reducer, initialState, applyMiddleware(thunk));
    return store;
}

export {
    initialState,
    createStoreInstance
}
<file_sep>const Movies = [{
	id: 1,
	title: 'Pulp Fiction',
	picture: {
		source: 'https://cdn.miramax.com/media/assets/Pulp-Fiction1.png',
		alt: 'pulp fiction'
	},
	description: '<NAME> (<NAME>) and <NAME> (<NAME>) are two hit men who are out to retrieve a suitcase stolen from their employer, mob boss <NAME> (Ving Rhames). Wallace has also asked Vincent to take his wife Mia (<NAME>) out a few days later when Wallace himself will be out of town. <NAME> (<NAME>) is an aging boxer who is paid by Wallace to lose his fight. The lives of these seemingly unrelated people are woven together comprising of a series of funny, bizarre and uncalled-for incidents.',
	year: 1994,
	rating: 4.1,
	duration: 140,
	director: ' <NAME>',
	cast: ['<NAME>', 'Uma', '<NAME>'],
	category: 'Oscar'
},
	{
		id: 2,
		title: 'Kill Bill 1',
		picture: {
			source: 'https://i.pinimg.com/originals/64/82/ef/6482ef51ce0b437986ec477e0d46f30c.jpg',
			alt: 'pulp fiction'
		},
		description: '<NAME> (<NAME>) and <NAME> (<NAME>) are two hit men who are out to retrieve a suitcase stolen from their employer, mob boss <NAME> (Ving Rhames). Wallace has also asked Vincent to take his wife Mia (<NAME>) out a few days later when Wallace himself will be out of town. <NAME> (<NAME>) is an aging boxer who is paid by Wallace to lose his fight. The lives of these seemingly unrelated people are woven together comprising of a series of funny, bizarre and uncalled-for incidents.',
		year: 1994,
		rating: 4.1,
		duration: 140,
		director: ' <NAME>',
		cast: ['<NAME>', 'Uma', '<NAME>'],
		category: 'Oscar'
	},
	{
		id: 3,
		title: 'KIll Bill 2',
		picture: {
			source: 'https://i.pinimg.com/736x/ed/88/0e/ed880e84030cca6cc7f4d9a74b5ee825--cult-movies-quentin-tarantino.jpg',
			alt: 'pulp fiction'
		},
		description: '<NAME> (<NAME>) and <NAME> (<NAME>) are two hit men who are out to retrieve a suitcase stolen from their employer, mob boss <NAME> (Ving Rhames). Wallace has also asked Vincent to take his wife Mia (<NAME>) out a few days later when Wallace himself will be out of town. <NAME> (<NAME>) is an aging boxer who is paid by Wallace to lose his fight. The lives of these seemingly unrelated people are woven together comprising of a series of funny, bizarre and uncalled-for incidents.',
		year: 1994,
		rating: 4.1,
		duration: 140,
		director: ' <NAME>',
		cast: ['<NAME>', 'Uma', '<NAME>'],
		category: 'Oscar'
	}];

export default Movies<file_sep>import React from 'react';
import {shallow} from 'enzyme';

import {MoviePage} from '../MoviePage';

describe('<MoviePage />', () => {
	test('should render without throwing error', () => {
		const page = shallow(
			<MoviePage
				director={{movie_credits: {crew:[]}}}
				movie={{credits: {crew:[]}}}
				history={{}}
				match={{params: {}}}
			/>
		);

		expect(page.find('Movie').length).toBe(1);
		expect(page.find('Content').length).toBe(1);
	});

	test('should handle Search btn click', () => {
		let mockedPush = jest.fn();

		const page = shallow(
			<MoviePage
				director={{movie_credits: {crew:[]}}}
				movie={{credits: {crew:[]}}}
				history={{push: mockedPush}}
				match={{params: {}}}
			/>
		);

		page.instance().goSearch({preventDefault: () => {}});

		expect(mockedPush).toHaveBeenCalledWith('/search');

	});
});<file_sep>import React from 'react';
import {Route, Switch} from 'react-router-dom';

import SearchPage from './pages/SearchPage';
import MoviePage from './pages/MoviePage';
import IndexPage from './pages/IndexPage';

export default () => (
    <Switch>
        <Route exact path="/" component={IndexPage}/>
        <Route path="/search/:query" component={SearchPage}/>
        <Route path="/search/" component={SearchPage}/>
        <Route path="/movie/:title" component={MoviePage}/>
    </Switch>
);
<file_sep>import React from 'react';

import { findMovieDirector, findMovieCast, getMovieGenres } from '../client/api/helpers';

export const Movie = (props) => (
	<div className="info-wrapper">
		<button onClick={props.goSearch}>Search</button>
		<div className="row">
			<img src={'https://image.tmdb.org/t/p/w300' + props.movie.poster_path} alt={props.movie.title}/>
			<div>
				<h1>{props.movie.title}</h1>
				<div>{props.movie.vote_average}</div>
				<div>{getMovieGenres(props.movie)}</div>
				<div><span>{props.movie.release_date}</span> | <span>{props.movie.duration | 'Inf'} min</span></div>
				<p>{props.movie.overview}</p>
				<div>Director: {findMovieDirector(props.movie).name}</div>
				<div>Cast: {findMovieCast(props.movie).name}</div>
			</div>
		</div>
	</div>
);
<file_sep>import {
    MOVIES_FETCH,
    MOVIES_FETCH_FAILURE,
    MOVIES_FETCH_SUCCESS,
    MOVIE_FETCH_FAILURE,
    MOVIE_FETCH,
    MOVIE_FETCH_SUCCESS,
    QUERY_SET,
    QUERY_SET_TYPE,
    DIRECTOR_INFO_FETCH,
    DIRECTOR_INFO_FETCH_SUCCESS,
    DIRECTOR_INFO_FETCH_FAILURE
} from './types';

import axios from 'axios';

import * as moviedb from '../client/api/moviedb';
import { findMovieDirector, getAllDirectedMovies } from '../client/api/helpers';

export function doSearch(query, selectedSearchType, dispatch) {
    dispatch({type: MOVIES_FETCH});

    let isByTitle = selectedSearchType.trim() === 'title';

		if (isByTitle) {
			return () => axios
				.get(moviedb.getSearchMovieUrl(query))
				.then(res => {
					dispatch(moviesFetchSuccess(res.data.results, query));
				})
				.catch(err => {
					console.trace(err);
					dispatch(moviesFetchFailure(err.message || 'Can\'t load results'))
				});
		} else {
			return () => axios
				.get(moviedb.getSearchPersonUrl(query))
				.then(({data: {results: persons}}) => {
					axios
						.get(moviedb.getPersonDetailUrl(persons[0].id, ['movie_credits']))
						.then(({data: director}) => {
							dispatch(moviesFetchSuccess(getAllDirectedMovies(director), query));
						})
				})
				.catch(err => {
					console.trace(err);
					dispatch(moviesFetchFailure(err.message || 'Can\'t load results'))
				});
		}
}

function moviesFetchSuccess(results, query) {
    return {
        type: MOVIES_FETCH_SUCCESS,
        payload: {results, query}
    }
}

function moviesFetchFailure(message) {
    return {
        type: MOVIES_FETCH_FAILURE,
        payload: message
    }
}

export function getMovieDetail(id, dispatch) {
    dispatch({type: MOVIE_FETCH});

    return () => axios
        .get(moviedb.getMovieDetailUrl(id, ['credits']))
        .then(({data: movie}) => {
            let director = findMovieDirector(movie);

            dispatch(movieFetchSuccess(movie, id));

            if (director.id) {
                dispatch(getDirectorInfo(director.id, dispatch));
            }
        })
        .catch(err => {
            console.trace(err);
            dispatch(movieFetchFailure(err.message || 'Can\'t load results'))
        });
}

function movieFetchSuccess(result, id) {
    return {
        type: MOVIE_FETCH_SUCCESS,
        payload: {result, id}
    }
}

function movieFetchFailure(message) {
    return {
        type: MOVIE_FETCH_FAILURE,
        payload: message
    }
}

export function getDirectorInfo(id, dispatch) {
    dispatch({type: DIRECTOR_INFO_FETCH});

    return () => axios
        .get(moviedb.getPersonDetailUrl(id, ['movie_credits']))
        .then(({data: director}) => {
            dispatch(directorInfoFetchSuccess(director, id));
        })
        .catch(err => {
            console.trace(err);
            dispatch(directorInfoFetchFailure(err.message || 'Can\'t load results'))
        });
}

function directorInfoFetchSuccess(result, id) {
    return {
        type: DIRECTOR_INFO_FETCH_SUCCESS,
        payload: {result, id}
    }
}

function directorInfoFetchFailure(message) {
    return {
        type: DIRECTOR_INFO_FETCH_FAILURE,
        payload: message
    }
}

export function setQuery(query, oldQuery) {
    return {
        type: QUERY_SET,
        payload: {query, oldQuery}
    };
}

export function setSearchType(selectedSearchType) {
    return {
        type: QUERY_SET_TYPE,
        payload: selectedSearchType
    };
}
<file_sep>const BASE_URL = 'https://api.themoviedb.org';
const VERSION = '3';

const MOVIE_DETAIL_ENDPOINT = 'movie';
const MOVIE_CREDITS_PART = 'credits';

const PERSON_DETAIL_ENDPOINT = 'person';
const PERSON_MOVIE_CREDITS_PART = 'movie_credits';

const SEARCH_MOVIE_ENDPOINT = 'search/movie';
const SEARCH_PERSON_ENDPOINT = 'search/person';

const BASE_PARAMS = {
    api_key: '172283afd6dcc9211e5e1ee68cca7767',
    language: 'en-US'
};

let stringify = function stringify(params) {
	throw '=(';
};

if (process) {
	stringify = function stringify(params) {
		return require('querystring').stringify(params);
	};
} else if (window) {
	stringify = function stringify(params) {
		return new URLSearchParams(params).toString();
	};
}

const MOVIE_DETAIL_URL = `${BASE_URL}/${VERSION}/${MOVIE_DETAIL_ENDPOINT}`;
const PERSON_DETAIL_URL = `${BASE_URL}/${VERSION}/${PERSON_DETAIL_ENDPOINT}`;

const SEARCH_MOVIE_URL = `${BASE_URL}/${VERSION}/${SEARCH_MOVIE_ENDPOINT}`;
const SEARCH_PERSON_URL = `${BASE_URL}/${VERSION}/${SEARCH_PERSON_ENDPOINT}`;

function getUrlWithQuery(base, query) {
    return `${base}?${stringify({...BASE_PARAMS, query})}`;
}
function getUrlWithIdAndAdditions(base, id, additions) {
    let params = additions.length ? {...BASE_PARAMS, append_to_response: additions.join(',')} : {...BASE_PARAMS};

    return `${base}/${id}?${stringify(params)}`;
}

export function getMovieDetailUrl(id, additions = []) {
    return getUrlWithIdAndAdditions(MOVIE_DETAIL_URL, id, additions);
}

export function getPersonDetailUrl(id, additions = []) {
    return getUrlWithIdAndAdditions(PERSON_DETAIL_URL, id, additions);
}

export function getSearchMovieUrl(query) {
    return getUrlWithQuery(SEARCH_MOVIE_URL, query);
}

export function getSearchPersonUrl(query) {
    return getUrlWithQuery(SEARCH_PERSON_URL, query);
}
<file_sep>import React from 'react';

export default class Search extends React.Component {
	render() {
		return (
			<div>
				<form onSubmit={this.props.onSubmit} className="search-form">
					<label htmlFor="search-field">Find your movie</label>
					<input type="text" name="search-field" id="search-field" value={this.props.query} onChange={this.props.onChange}/>
					<span>Search by</span>
					<input type="radio"
						name="searchType"
						id="title"
						value="title"
						checked={this.props.selectedSearchType === 'title'}
						onChange={this.props.onSearchTypeChange}/>
					<label htmlFor="title">Title</label>
					<input type="radio"
						name="searchType"
						id="director"
						value="director"
						checked={this.props.selectedSearchType === 'director'}
						onChange={this.props.onSearchTypeChange}/>
					<label htmlFor="director">Director</label>
					<input type="submit" value="Submit"/>
				</form>
			</div>
		);
	}
}
<file_sep>export const QUERY_SET = 'QUERY_SET';
export const QUERY_SET_TYPE = 'QUERY_SET_TYPE';

export const MOVIES_FETCH = 'MOVIES_FETCH';
export const MOVIES_FETCH_SUCCESS = 'MOVIES_FETCH_SUCCESS';
export const MOVIES_FETCH_FAILURE = 'MOVIES_FETCH_FAILURE';
export const MOVIES_SORT_CHANGE = 'MOVIES_SORT_CHANGE';

export const MOVIE_FETCH = 'MOVIE_FETCH';
export const MOVIE_FETCH_SUCCESS = 'MOVIE_FETCH_SUCCESS';
export const MOVIE_FETCH_FAILURE = 'MOVIE_FETCH_FAILURE';

export const DIRECTOR_INFO_FETCH = 'DIRECTOR_INFO_FETCH';
export const DIRECTOR_INFO_FETCH_SUCCESS = 'DIRECTOR_INFO_FETCH_SUCCESS';
export const DIRECTOR_INFO_FETCH_FAILURE = 'DIRECTOR_INFO_FETCH_FAILURE';
<file_sep>import Express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {StaticRouter} from 'react-router-dom';

import {createStoreInstance, initialState} from '../redux/store';
import {MyApp} from '../client/app';
import routes from '../routes';

const app = Express();
const port = 4300;

//Serve static files
app.use(Express.static('./'));

// This is fired every time the server side receives a request
app.use(handleRender);

function handleRender(req, res) {
    const store = createStoreInstance(initialState);
    const context = {};
    const app = (
        <StaticRouter location={req.url} context={context}>
            <MyApp store={store}>
                {routes()}
            </MyApp>
        </StaticRouter>);

    const html = renderToString(app);

    // Send the rendered page back to the client
    res.send(renderFullPage(html, initialState))
}

function renderFullPage(html, preloadedState = {}) {
    return `
	<!DOCTYPE html>
	<html>
	 <head>
	  <title>Redux Universal Example</title>
	  <meta charset="utf-8">
		 <base href="/">
	 <link href="styles.css" rel="stylesheet"></head>
	 <body>
		 <div class="app-wrapper" id="app">${html}</div>
		 <script>
        	window.PRELOADED_STATE = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
        </script>
	 <script type="text/javascript" src="client.js"></script><script type="text/javascript" src="styles.js"></script></body>
	</html>`
}

app.listen(port);
