nonlogos
8/18/2017 - 7:08 PM

New React and React Router 4

New React and React Router 4

import styled from 'styled-components';

const Wrapper = styled.div`
  width: 32%;
  border: 2px solid #333;
  border-radius: 4px;
  margin-bottom: 25px;
  padding-right: 10px;
  overflow: hidden;
`;

const Image = Styles.img`
  width: 46%;
  float: left;
  margin-right: 10px;
`;

<Wrapper>
    <image alt={`${props.title} Show Poster`} src={`/public/img/posters/${props.poster}`} />
    <div>
      <h3>{props.title}</h3>
      <h4>({props.year})</h4>
      <p>{props.description}</p>
    </div>
  </Wrapper>
// spinner.jsx

import React from 'react';
import sytled, { keyframs } from 'styled-components';

const spin = keyframes`
  from {
    transform: rotate(0deg);
  } to {
    transform: rotate(360deg);
  }
`;

const Image = styled.img`
  animation: ${spin} 4s infinite linear;
`;

const Spinner = () => <Image src="/public/img/loading.png" alt="loading indicator" />

export default Spinner;
// ownProps is the props that's passed in from parent props
import { getAPIData } from './actionCreators';

componentDidMount() {
  if (!this.props.rating) {
    this.props.getAPIData();
  }
}

const mapStateToProps = (state, ownProps) => {
  const apiData = state.apiData[ownProps.show.imdbID] ? state.apiData[ownProps.show.imdbId]
    : {};
    
  return {
    rating: apiData.rating
  }
}

const mapDispatchToProps = (dispatch, ownProps) => ({
  getAPIData() {
    dispatch(getAPIData(ownProps.show.imdbID));
  }
});

export default connect(mapStateToProps, mapDispatchToProps);
import React from 'react';
import ShowCard from './showCard';
import preload from '../data.json';

const Search = () => (
  {preload.shows.map(show => <showCard key={show.id} {...show} />)} // with spread operator
);
export default Search;

// child nested component
import React from 'react';
import { string } from 'prop-types';

const ShowCard = props => (
  <div className ='show-card'>
    <img alt={`${props.title} Show Poster`} src={`/public/img/posters/${props.poster}`} />
    <div>
      <h3>{props.title}</h3>
      <h4>({props.year})</h4>
      <p>{props.description}</p>
    </div>
  </div>
);

ShowCard.propTypes ={
  poster: string.isRequired,
  title: string.isRequired,
  year: string.isRequired,
  description: string.isRequired,
};

export default ShowCard

// map and filter

<div>
  {preload.shows
    .filter(
      show => 
        `${show.title} ${show.description}`.toUpperCase().indexOf(this.state.searchTerm.toUpperCase()) >= 0
    )
    .map(show => <ShowCard key={show.imbdID} {...show} />)}
</div>
// types.js
export Show = {
  title: string,
  description: string,
  year: string,
  imdbID: string,
  trailer: string,
  poster: string
}

// regular component
import show from '../types.js'

props: {
  shows: Array<Show>
}
import React from 'react';

const details = props => {
  // destructuring
  const { title, description, year, poster, trailer } = props.show;
  return (
    <div className="details">
      <header>
        <h1>svideo</h1>
        <section>
          <h1>{title}</h1>
          <h2>({Year})</h2>
          <img src={`/public/img/posters/${poster}`} alt=`poster for ${title}` />
          <p>{description}</p>
        </section>
        <div>
          <iframe 
            src={`blah blah`}
            frameBorder="0"
            allowFullScreen
            title={`Trailer for ${title}`}
          />
        </div>
      </header>
    </div>
  );
}
import React from 'react';
import { connect } from 'react-redux';
import { setSearchTerm } from './actionCreators';


//if it was a class we can use decorator
@connect(mapStateToProps);
const Landing = (props) => (
  <div className="Landing">
    <input onChange={props.handleSearchTermChange} value={props.searchTerm}
      type="text" placeholder="Search" />
  </div>
);

const mapStateToProps = (state) => ({ searchTerm: state.searchTerm })
const mapDispatchToProps = (dispatch) => ({
  handleSearchTermChange(event) {
    dispatch(setSearchTerm(event.target.value));
  }
})

// if using decorator
export default Landing

export default connect(mapStateToProps, mapDispatchToProps)(Landing);
import React from 'react';

const Details = (props) => (
  <div className="details">
    <pre><code>{JSON.stringify(props, null, 4)}</code></pre>
  </div>
);
class Details extends Component {
  constructor(props) {
    super(props);
    state = {
      apiData: { rating: '' }
    };
  }
  componentDidMount() {
    axios
      .get(`http://localhost:3000/${this.props.show.imdgID}`)
      .then(response => this.setState({apiData: response.data}));
  }
  props: {
    show: Show
  };
  render() {
    const { title, description, year, poster, trailer } = this.props.show;
    let ratingComponent;
    if (this.state.apiData.rating) {
      ratingComponent = <h3>{this.state.apiData.rating}</h3>;
    } else {
      ratingComponent = <Spinner />;
    }
    return (
      <div className="details">
      <header>
        <h1>svideo</h1>
        <section>
          <h1>{title}</h1>
          <h2>({Year})</h2>
          {ratingComponent}
          <img src={`/public/img/posters/${poster}`} alt=`poster for ${title}` />
          <p>{description}</p>
        </section>
        <div>
          <iframe 
            src={`blah blah`}
            frameBorder="0"
            allowFullScreen
            title={`Trailer for ${title}`}
          />
        </div>
      </header>
    </div>
    );
  }
}
//actions
const SET_SEARCH_TERM = 'SET_SEARCH_TERM';

export function setSearchTerm(searchTerm) {
  return {
    type: SET_SEARCH_TERM,
    payload: seartchTerm
  };
}

//reducers
const setSearchTerm = (state, action) => Object.assign({}, state, { searchTerm: action.payload })

//root reducer
import {SET_SEARCH_TERM} from ...

const DEFAULT_STATE = {
  searchTerm: ''
}

const rootReducer = (state = DEFAULT_STATE, action) => {
  switch (action.type) {
    case SET_SEARCH_TERM:
      return setSearchTerm(state, action);
    default:
      return state;
  }
};

export default rootReducer;
//inside of constructor
this.state = {
  searchTerm: ''
};

this.handleSearchTermChange = this.handleSearchTermChange.bind(this);

// JSX
<input onChange={this.handleSearchTermChange} value={this.state.searchTerm} type="text" placeholder="search" />

// class methods

handleSearchTermChange(event) {
  this.setState({searchTerm: event.target.value});
}

// with the babel class property - don't need constructor
this.state = {
  searchTerm: ''
};

handleSearchTermChange = event => {
  this.setState({searchTerm: event.target.value});
}


import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Landing from './Landing';
import Search from './Search';
import Details from './Details';

const FourOhFour = () => <h1>404</h1>

const App = () => (
  <BrowserRouter>
    <div className="app">
      <Switch>
        <Route exact path="/" component={Landing} />
        <Route path="/search" component={Search} />
        <Route path="details/:id" component={props => <Details show={preload.shows.find(show => props.match.params.id === show.imdbID)} {...props} />} />
        <Route component={FourOhFour} />
      </Switch>
    </div>
  </BrowserRouter>
);
//reducers

export const SET_SEARCH_TERM = 'SET_SEARCH_TERM';
export const ADD_API_DATA = 'ADD_API_DATA';

import { combineReducers } from 'redux';
import { SET_SEARCH_TERM, ADD_API_DATA } from './actions';

const searchTerm = (state = '', action) => {
  if (action.type === SET_SEARCH_TERM) return action.payload;
  return state;
};

const apiData = (state = {}, action) => {
  if (action.type === ADD_API_DATA){
    returnn Object.assign({}, state, { [action.payload.imdbID]: action.payload })
  }
  return state;
}

const rootReducer = combineReducers({ searchTerm, apiData });

export default rootReducer;

// action creators
import axios from 'axios';
import { ADD_API_DATA } from './actions';

export function addAPIData(apiData) {
  return {type: ADD_API_DATA, payload: apiData};
};
// thunk that returns a function/deferred action 
export function getAPIDetails(imdbID) {
  return (dispatch) => {
    axios
      .get(`http://localhost:3000/${imdbID}`)
      .then(response => {
        // dispatch the action back to redux
        dispatch(addAPIData(response.data))
      })
      .catch(error => {
        console.error('axios error', error); //eslint-disable-line no-console
      });
  };
}
import React from 'react';
import { shallow } from 'enzyme';
import preload from '../../data.json';
import ShowCard from '../ShowCard';

test('search should render correct amount of shows', () => {
  const component = shallow(<Search />);
  expect(component.find(ShowCard.length)).toEqual(preload.shows.length));
});

// xtest or xit or xdescribe will stop it from running

test('Search sould render correct amount of shows based on search term', () => {
  const searchWord = 'black';
  const component = shallow(<Search />);
  component.find('input').simulate('change', {target: {value: searchWord}});
  const showCount = preload.shows.filter(
    show => `${show.title} ${show.description}`.toUpperCase().indexOf(searchWord.toUpperCase()) >= 0
  ).length;
  
  expect(component.find(ShowCard).length).toEqual(showCount);
});

// or use describe to create a test suite

describe('Search', () => {
  it('renders correctly', () => {
    const component = shallow(<Search />);
    expect(component).toMatchSnapshot();
  
  });

  it('should render correct amount of shows', () => {
    const component = shallow(<Search />);
    expect(component.find(ShowCard.length)).toEqual(preload.shows.length));
  });

});