<repo_name>codebeeper/stardb<file_sep>/src/components/app/app.js
import React from "react";

import Header from "../header";
import RandomPlanet from "../random-planet";
import SwapiService from "./../../services/swapi-service";

import "./app.css";
import ItemPage from "../item-page";

export default class App extends React.Component {
  swapiService = new SwapiService();

  render() {
    return (
      <div className="container-fluid">
        <Header />
        <RandomPlanet />
        <ItemPage
          getImage={this.swapiService.getPersonImage}
          getData={this.swapiService.getAllPersons}
          details={[["gender", "Gender"], ["eyeColor", "Eye color"]]}
        />
        <ItemPage
          getImage={this.swapiService.getStarshipImage}
          getData={this.swapiService.getAllStarships}
          details={[
            ["model", "Model"],
            ["length", "Length"],
            ["costInCredits", "Cost"]
          ]}
        />
        <ItemPage
          getImage={this.swapiService.getPlanetImage}
          getData={this.swapiService.getAllPlanets}
          details={[["population", "Population"], ["diameter", "Diameter"]]}
        />
      </div>
    );
  }
}
<file_sep>/src/components/item-page/item-page.js
import React, { Component } from "react";
import ItemList from "../item-list";
import ItemDetails from "../item-details";
import Row from "../row";
import ErrorBoundary from "../ErrorBoundary";

const Record = ({ item, field, label }) => {
  return (
    <li className="list-group-item">
      <span className="term">{label}</span>
      <span>{item[field]}</span>
    </li>
  );
};

export { Record };

export default class ItemPage extends Component {
  state = {
    selectedItem: null
  };

  onItemSelected = item => {
    this.setState({ selectedItem: item });
  };

  render() {
    const { getData, getImage, details } = this.props;
    const itemList = (
      <ItemList
        getData={getData}
        onItemSelected={this.onItemSelected}
        selectedItem={this.state.selectedItem}
      >
        {/* children */}
        {(i, field) => `${i.name} (${i[field]})`}
      </ItemList>
    );

    const itemFields = details.map(([field, label]) => {
      return <Record key={field} field={field} label={label} />;
    });

    const itemDetails = (
      <ItemDetails getImage={getImage} item={this.state.selectedItem}>
        {itemFields}
      </ItemDetails>
    );

    return (
      <ErrorBoundary>
        <Row left={itemList} right={itemDetails} />
      </ErrorBoundary>
    );
  }
}
<file_sep>/src/services/swapi-service.js
export default class SwapiService {
  _apiBase = "https://swapi.co/api";

  async collectAllPages(url, callback) {
    const request = async nextPage => {
      const res = await this.getResource(nextPage || this._apiBase + url);
      let results = [];
      results = results.concat(res.results);
      if (res.next) {
        results = results.concat(await request(res.next));
      }
      return results;
    };
    return await request().then(res => res.map(callback));
  }

  async getResource(url) {
    const res = await fetch(url);

    if (!res.ok) {
      throw new Error(`Could not fetch ${url}` + `, recieved ${res.status}`);
    }
    return await res.json();
  }

  getAllPersons = async () => {
    return await this.collectAllPages(`/people/`, this._transformPerson);
  };

  async getPerson(id) {
    const person = await this.getResource(this._apiBase + `/people/${id}`);
    return this._transformPerson(person);
  }
  getAllPlanets = async () => {
    return await this.collectAllPages("/planets/", this._transformPlanet);
  };

  async getPlanet(id) {
    const planet = await this.getResource(this._apiBase + `/planets/${id}`);
    return this._transformPlanet(planet);
  }
  getAllStarships = async () => {
    return await this.collectAllPages("/starships/", this._transformStarship);
  };

  async getStarship(id) {
    const starship = await this.getResource(this._apiBase + `/starship/${id}`);
    return this._transformStarship(starship);
  }

  _extractId = url => {
    return url.match(/[^/]\d*(?=\/$)/g)[0];
  };

  getPersonImage(id) {
    return `https://starwars-visualguide.com/assets/img/characters/${id}.jpg`;
  }

  getStarshipImage(id) {
    return `https://starwars-visualguide.com/assets/img/starships/${id}.jpg`;
  }

  getPlanetImage(id) {
    return `https://starwars-visualguide.com/assets/img/planets/${id}.jpg`;
  }

  _transformPlanet = planet => {
    return {
      id: this._extractId(planet.url),
      name: planet.name,
      population: planet.population,
      rotationPeriod: planet.rotation_period,
      diameter: planet.diameter
    };
  };

  _transformStarship = starship => {
    return {
      id: this._extractId(starship.url),
      name: starship.name,
      model: starship.model,
      manufacturer: starship.manufacturer,
      costInCredits: starship.cost_in_credits,
      length: starship.length,
      crew: starship.crew,
      passengers: starship.passengers,
      cargoCapacity: starship.cargo_capacity
    };
  };

  _transformPerson = person => {
    console.log(person);
    return {
      id: this._extractId(person.url),
      name: person.name,
      gender: person.gender,
      birthYear: person.birth_year,
      eyeColor: person.eye_color
    };
  };
}
<file_sep>/src/components/item-details/item-details.js
import React, { Component } from "react";
import Spinner from "../spinner";
import "./item-details.css";

const ItemDetails = props => {
  const { item } = props;

  const itemInfo = item && (
    <React.Fragment>
      <img
        className="item-image"
        src={props.getImage(item.id)}
        alt={item.name}
      />
      <div className="card-body">
        <h4>{item.name}</h4>
        <ul className="list-group list-group-flush">
          {React.Children.map(props.children, child => {
            return React.cloneElement(child, { item });
          })}
        </ul>
      </div>
    </React.Fragment>
  );

  return (
    <div className="item-details card">{item ? itemInfo : <Spinner />}</div>
  );
};

export default ItemDetails;
<file_sep>/src/components/item-list/item-list.js
import React, { Component } from "react";
import "./item-list.css";
import Pagination from "../pagination/pagination";
import withData from "../../hoc-helpers/with-data";

class ItemList extends Component {
  _perPage = 7;

  state = {
    active: 1,
    pages: null,
    itemsList: null,
    currentVisible: null,
    isLoading: true
  };

  componentDidUpdate(prevProps) {
    const { itemsList } = this.props;
    if (prevProps.itemsList !== itemsList) {
      this.setState(
        {
          itemsList,
          isLoading: false,
          pages: Math.ceil(itemsList.length / this._perPage)
        },
        () => this.paginationClick()
      );
    }
  }

  paginationClick = (id = 1) => {
    this.setState(({ currentVisible, itemsList }) => {
      const peopleToShow = itemsList.slice(
        (id - 1) * this._perPage,
        id * this._perPage
      );
      if (currentVisible !== peopleToShow) {
        // set the first item active each navigation
        this.props.onItemSelected(peopleToShow[0]);
        // set visible items (pagination chunk)
        return { currentVisible: peopleToShow, active: id };
      }
      return;
    });
  };

  renderItems(arr) {
    const { selectedItem, children, onItemSelected } = this.props;
    return arr.map(item => {
      // use props function to set label
      const label = children(item, Object.keys(item)[3]);
      const classes =
        selectedItem && item.id === selectedItem.id
          ? "list-group-item active"
          : "list-group-item";
      return (
        <li
          onClick={() => onItemSelected(item)}
          key={item.name}
          className={classes}
        >
          {label}
        </li>
      );
    });
  }

  render() {
    const { pages, currentVisible, active, itemsList } = this.state;
    const elementsList = currentVisible && this.renderItems(currentVisible);
    if (this.state.isLoading) {
      return <ul className="item-list list-group">{`Loading...`}</ul>;
    }
    return (
      <React.Fragment>
        <ul className="item-list list-group">{elementsList}</ul>
        <Pagination
          total={itemsList && itemsList.length}
          step={currentVisible && currentVisible.length}
          perPage={this._perPage}
          pages={pages}
          active={active}
          navigate={this.paginationClick}
        />
      </React.Fragment>
    );
  }
}

export default withData(ItemList);
