30 min
Saito
  1. 1. 概要
    1. 1.1. 素材
    2. 1.2. 機能
  2. 2. アプリの作成
    1. 2.1. Frontend
    2. 2.2. Backend
  3. 3. アプリのカスタマイズ
    1. 3.1. Frontend
    2. 3.2. Backend
  4. 4. デプロイ
    1. 4.1. Backend
    2. 4.2. Frontend
  5. 5. ダウンロード
  6. 6. Version

K5 Playgroundで、Youtubeを検索して特定の動画だけを表示する動画のポータルサイトを作ります。作成したアプリケーションは、製品のポータルサイト、趣味やイベントのサイトなど様々な用途に応用することができます。

このチュートリアルでは、K5 PlaygroundでのYoutube APIの使い方やYoutube動画をSPAに表示・再生させるためのReactコンポーネントの修正方法を説明します。

概要

素材

  • テンプレート: Simple Search
  • APIロジック: YouTube Data API Search Video

機能

  • YouTubeで動画を検索する
  • 検索した動画をReactのSPAに表示する
  • SPAで再生する

アプリの作成

K5 Playgroundにアクセスします。

Frontend

  1. Simple Search テンプレートを選択します。

  2. Search Pageをクリックします。

  3. 標準のカードの右側のカード(詳細情報があるもの)を選択します。

Backend

  1. 左メニューのWebAPIsを選択して、WebAPI名を次のように変更します。

    • sample_productsmoviesにする
  2. GET /moviesを修正します。

    1. GET /movies エンドポイントをクリックします。

    2. Fetch Document APIロジックを削除します。

    3. 右ペインからSearch Video APIロジックを追加します。

    4. Search Video APIロジックを次のように修正します。

      1. Set API Key

        1
        var apiKey =  '[API Key]';
      2. 検索ワードの設定

        1
        var q = req.swagger.params.keyword.value;
      3. part変数を次のように設定し、Youtubeから動画の詳細情報を取れるようにします。

        1
        var part = 'snippet';
      4. ページングのためにpageToken変数を作成します。

        1
        var pageToken = req.swagger.params.offset.value ? ('&pageToken=' + req.swagger.params.offset.value) : '';
      5. HTTPリクエストのoptions変数を修正します。

      ページング用にmaxResultspageToken、ソート用にorderを設定します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      var options = {
      url: url +
      "?key=" + apiKey +
      "&part=" + part +
      "&q=" + q +
      "&order=" + req.swagger.params.sort.value +
      "&maxResults=" + req.swagger.params.limit.value +
      pageToken,
      method: 'GET',
  3. アプリをダウンロードします。

アプリのカスタマイズ

Frontend

  1. actions/ProductActionCreators.jsを修正します。

    getProducts メソッドを次のように修正します。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    getProducts({ offset, limit, sort, keyword }) {
    const getProducts = api.get('/movies', {
    params: {
    keyword,
    offset,
    limit,
    sort,
    },
    });

    axios.all([getProducts])
    .then(axios.spread((productList) => {
    AppDispatcher.dispatch({
    type: ActionTypes.GET_PRODUCTS,
    data: {
    products: productList.data.items,
    count: productList.data.pageInfo.totalResults,
    pageToken: {
    prev: productList.data.prevPageToken,
    next: productList.data.nextPageToken,
    },
    },
    });
    })).catch(() => {
    // @todo handle error
    });
    },
  2. stores/ProductsStore.jsを修正します。

    pageTokenをdata formatに付加します。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    getInitialState() {
    return {
    data: Immutable.fromJS({}).toOrderedMap(),
    count: 0,
    pageToken: {
    prev: undefined,
    next: undefined,
    },
    };
    }

    reduce(state, action) {
    switch (action.type) {
    case ActionTypes.GET_PRODUCTS: {
    return {
    data: new Immutable.OrderedMap(action.data.products.map(x => [x.id.videoId, x])),
    count: action.data.count,
    pageToken: action.data.pageToken,
    };
    }
  3. components/containers/ProductListContainer.jsを修正します。

    getProductsの引数の修正と、ProductListPageのpropsを修正します。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    componentWillMount() {
    ProductActionCreators.getProducts({
    limit: Constants.PRODUCT_MAX,
    keyword: this.state.filter.data.get('keyword'),
    });
    }

    render() {
    const { products, cart, filter } = this.state;

    return (
    <ProductListPage
    router={this.context.router}
    products={products.data}
    productsDisplayCount={products.data.count()}
    productsCount={products.count}
    cartBadge={cart.data.count()}
    keyword={filter.data.get('keyword')}
    pageToken={products.pageToken}
    {...this.props}
    />
    );
    }
  4. components/pages/ProductListPage.jsを修正します。

    1. 動画ポップアップのためにReactのDialog部品を追加します。

      1
      2
      3
      4
      import { FlatButton, RaisedButton } from 'material-ui';
      import NavigationChevronLeft from 'material-ui/svg-icons/navigation/chevron-left';
      import NavigationChevronRight from 'material-ui/svg-icons/navigation/chevron-right';
      import Dialog from 'material-ui/Dialog';
    2. (任意)propTypesを修正します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      export default class ProductListPage extends Component {
      static propTypes = {
      products: PropTypes.shape({
      id: PropTypes.shape({
      videoId: PropTypes.string,
      }).isRequired,
      }).isRequired,
      productsDisplayCount: PropTypes.number.isRequired,
      productsCount: PropTypes.number.isRequired,
      cartBadge: PropTypes.number.isRequired,
      keyword: PropTypes.string.isRequired,
      pageToken: PropTypes.shape({
      prev: PropTypes.string,
      next: PropTypes.string,
      }).isRequired,
      };
    3. stateの初期値を修正します。

      1
      2
      3
      4
      5
      state = {
      currentPage: 1,
      sort: 'date',
      openCardId: '',
      }
    4. handleSortChangegetProductsメソッドの引数を修正します。

      1
      2
      3
      4
      5
      ProductActionCreators.getProducts({
      limit: Constants.PRODUCT_MAX,
      sort: value,
      keyword: this.props.keyword,
      });
    5. ページングのハンドラを修正します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      handlePreviousPage = () => {
      const { keyword, pageToken } = this.props;

      this.setState({
      currentPage: this.state.currentPage - 1,
      });

      ProductActionCreators.getProducts({
      limit: Constants.PRODUCT_MAX,
      keyword,
      offset: pageToken.prev,
      sort: this.state.sort,
      });
      }

      handleNextPage = () => {
      const { keyword, pageToken } = this.props;

      this.setState({
      currentPage: this.state.currentPage + 1,
      });

      ProductActionCreators.getProducts({
      limit: Constants.PRODUCT_MAX,
      keyword,
      offset: pageToken.next,
      sort: this.state.sort,
      });
      }

      render() {
    6. ページャを修正します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      const pager = (
      <VerticalLayout alignX={'center'} alignY={'middle'} style={styles.pager}>
      <div>
      <FlatButton
      disabled={!this.props.pageToken.prev}
      onClick={this.handlePreviousPage}
      icon={<NavigationChevronLeft />}
      />
      <FlatButton
      disabled={!this.props.pageToken.next}
      onClick={this.handleNextPage}
      icon={<NavigationChevronRight />}
      />
      </div>
      </VerticalLayout>
      );
    7. ソートのオプションを修正します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      const sort = (
      <HorizontalLayout alignX={'right'} alignY={'middle'} style={styles.sort}>
      <span style={styles.sortLabel}>Sort</span>
      <SelectField
      value={this.state.sort}
      onChange={(event, index, value) => this.handleSortChange(value)}
      style={styles.sortControl}
      >
      <MenuItem value="date" primaryText="date" />
      <MenuItem value="rating" primaryText="rating" />
      <MenuItem value="relevance" primaryText="relevance" />
      <MenuItem value="title" primaryText="title" />
      <MenuItem value="viewCount" primaryText="viewCount" />
      </SelectField>
      </HorizontalLayout>
      );
    8. カード部品に、動画や再生ボタンを追加します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      const card = value => (
      <div>
      <VerticalCard003
      key={value.id.videoId}
      imageAlt={value.snippet.title}
      imageSrc={value.snippet.thumbnails.high.url}
      title={value.snippet.title}
      description={value.snippet.description}
      contentStyle={{ paddingBottom: 12 }}
      highlighted={new Date(value.snippet.publishedAt).toLocaleString()}
      >
      <RaisedButton
      label="Play"
      onTouchTap={() => this.setState({ openCardId: value.id.videoId })}
      />
      </VerticalCard003>
      <Dialog
      actions={<RaisedButton
      label="close"
      onTouchTap={() => this.setState({ openCardId: '' })}
      />}
      contentStyle={{ textAlign: 'center' }}
      title={value.snippet.title}
      modal={false}
      open={value.id.videoId === this.state.openCardId}
      onRequestClose={() => this.setState({ openCardId: '' })}
      >
      <iframe
      height={600}
      width={600}
      src={
      open
      ? `https://www.youtube.com/embed/${value.id.videoId}?&autoplay=1`
      : `https://www.youtube.com/embed/${value.id.videoId}`
      }
      frameBorder="0"
      allowFullScreen
      />
      </Dialog>
      </div>
      );

Backend

  1. api/swagger.jsonを編集します。

    1. movies/get/parametersを修正します。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      "parameters": [
      {
      "name": "keyword",
      "in": "query",
      "required": false,
      "type": "string",
      "default": "",
      "description": ""
      },
      {
      "name": "sort",
      "in": "query",
      "required": false,
      "type": "string",
      "default": "date",
      "description": ""
      },
      {
      "name": "offset",
      "in": "query",
      "required": false,
      "type": "string"
      },
      {
      "name": "limit",
      "in": "query",
      "required": false,
      "type": "number",
      "default": 10
      }
      ],

デプロイ

Backend

1
2
cd backend
cf push [APP_NAME]

Frontend

1
2
3
4
5
6
cd frontend
npm install
set API_URL=[Backend URL] # for Windows
export API_URL=[Backend URL] # for Mac/Linux
npm run build
cf push [APP_NAME] -p public

ダウンロード

Version

  • Simple Search Template: v1.0.0