Vue.js + Vuetify + Project-ShangriLa でアニメ情報を取得するアプリ作成

# 目的
面白いAPIを見つけたのでせっかくだからWebアプリ作ってみよう

# 環境
Windows Subsystem for Linux
OS: Ubuntu

# 使用技術
Vue.js（SPAフレームワーク）
Vuetify（UIコンポーネントライブラリ）
axios（Ajax通信ライブラリ）
materialdesignicons（マテリアルデザインアイコン）

サーバーレス＋小規模なアプリなのでいかに低コストで導入できるかで選びました
（ちょっとVue使ってみたかった感もある）

# やったこと

### プロジェクト作成

```
$ vue create animecheck
```

### Vuetify追加
オプション指定などできるけどデフォルトを指定する

```
$ vue add vuetify
```

### マテリアルデザインアイコン用のパッケージインストール

```
$ npm install @mdi/font --save
$ npm install material-design-iconfont --save
```

### 選択用コンボボックス作成
2014年～現在までの西暦とクールを選択する用のコンボボックスを作成する

```search.vue
<template src="../view/search.html">
</template>

<script src="../js/search.js">
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped src="../assets/css/search.css">
</style>
```

html, js, css それぞれ別々に書いたほうがわかりやすそうだったので、vueファイルはテンプレートのみ記載する

```search.html
    <v-layout row wrap>
        <v-flex xs12 sm6>
            <v-overflow-btn v-model="year" label='年' :items="selectYear" @change="getAnime"></v-overflow-btn>
        </v-flex>
        <v-flex xs12 sm6>
            <v-overflow-btn v-model="season" label='クール（季節）' :items="selectSeason" @change="getAnime"></v-overflow-btn>
        </v-flex>
    </v-layout>
```

:items でコンボボックスに入れる選択値を設定する
選択された値は v-model で指定している year, season にそれぞれ入る

```search.js
import axios from 'axios';
import 'vuetify/dist/vuetify.min.css'
import '@mdi/font/css/materialdesignicons.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'

export default {
    data() {
        return {
            year: '',
            season: '',
            animes: [],
            selectYear: this.getYear(),
            selectSeason: [
                {value: '1', text: '冬 (1 ～ 3 月)'},
                {value: '2', text: '春 (4 ～ 6 月)'},
                {value: '3', text: '夏 (7 ～ 9 月)'},
                {value: '4', text: '秋 (10 ～ 12 月)'},
            ]
        }
    },
    methods: {
        /**
         * 2014 ～ 現在までの年数を取得する関数
         */
        getYear: function() {
            let currentYear = (new Date()).getFullYear();
            const baseYear = 2014;
            let years = []
            while(currentYear >= baseYear) {
                years.push({value: currentYear, text: currentYear + '年'})
                currentYear -= 1;
            }
            return years
        },
    }
...
}
```

クールだけは面倒なので固定で書いてしまった
（固定値として別の場所に書きたい）

### アニメ取得APIとの連携
西暦とクールを選択したら、APIにリクエストを送りレスポンスをいい感じに加工して画面に表示する

```search.html
    <v-layout v-if="animes.length" row wrap>
        <v-flex lg4 sm12 xs12 v-for="anime in animes">
            <v-card raised tile class="grey lighten-3 ma-2">
                <v-card-title primary-title>
                    <div>
                        <div class="headline">
                            {{ anime.title }}
                        </div>
                    </div>
                </v-card-title>
                <v-divider light></v-divider>
                <v-card-actions class="pa-3">
                    <v-btn flat>
                        <a v-bind:href="anime.public_url">
                            <v-icon>
                                mdi-laptop
                            </v-icon>
                        </a>
                    </v-btn>
                    <v-btn flat>
                        <a v-bind:href="'https://twitter.com/' + anime.twitter_account">
                            <v-icon>
                                mdi-twitter
                            </v-icon>
                        </a>
                    </v-btn>
                    <v-btn flat>
                        <a v-bind:href="'https://twitter.com/search?q=%23' + anime.twitter_hash_tag + '&src=typd'">
                            <v-icon>
                                mdi-pound-box
                            </v-icon>
                        </a>
                    </v-btn>
                </v-card-actions>
            </v-card>
        </v-flex>
    </v-layout>
```

双方向データバインディングでanimesという変数に値が入っていない場合は、結果部分は何も表示されないようにする

```search.js
        /**
         * 年 + クールから対象のアニメリストを取得する関数
         */
        getAnime: function() {
            this.getYear();
            if (this.year && this.season) {
                // API: ShangriLa Anime API V1
                let api_url = "https://api.moemoe.tokyo/anime/v1/master/"
                this.animes = []
                axios.get(api_url + this.year + '/' + this.season, {
                }).then(response => {
                    for (let i = 0; i < Object.keys(response.data).length; i++) {
                        this.animes.push(response.data[i])
                    }
                })
            }
        }
```

コンボボックス側で指定されている関数
選択するたびに走るけど、年とクールが選択されていない限りは情報取得しないようにする

外部APIからjsonでデータ取得するためにはaxiosを使用する

アニメ情報取得には以下のAPIを使用しています
> GET /anime/v1/master/:year/:n
> :yearで指定されたYYYY年アニメの:nで指定されたクールの情報をすべて返します。

※参考
[放映中のアニメ作品の情報を提供するAnime RESTful API サーバー作りました](https://qiita.com/AKB428/items/64938febfd4dcf6ea698)
[axios を利用した API の使用](https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html)

### Vuetifyコンポーネントについて
[Vuetify Component API Overview](https://vuetifyjs.com/ja/components/api-explorer)

公式のドキュメントに各コンポーネントの例とソースが記載されている
すごくわかりやすい

### アイコンについて

[Icon Component](https://vuetifyjs.com/ja/components/icons#)
[Material Design Icons](https://materialdesignicons.com/)

> Material Design Iconsのアイコンの利用をサポートしています。アイコン名にmdi-プレフィックスを付与するだけで使用できます。あなたのプロジェクトにMaterial Design Iconsを導入しておく必要があるので注意して下さい。

```search.html
    <v-icon>
        mdi-twitter
    </v-icon>
```

これでTwitterアイコンなどが使用できる

### デプロイ
サーバーレス＋SPAなので、Github Pagesで簡単に公開したい
そのためビルド先のフォルダをdistからdocsに変更する

プロジェクトフォルダ直下に以下のファイルを作成する

```vue.config.js
module.exports = {
    publicPath: "./",
    assetsDir: "",
    outputDir: "docs"
}
```

ビルドする

```
$ npm run build
```

ビルド完了後にgitに上げてGithub Pagesの設定をしてあげれば出来上がり

### 完成品
[アニメ検索くん](https://www.animecheck.site/)

[ソースはこちら](https://github.com/h4n24w4/animecheck)

とりあえずお手軽に作れました


# 最後に
こちらのAPIを使用させていただきました！素晴らしいAPIに感謝です！
https://github.com/Project-ShangriLa/sora-playframework-scala

Vueについて少ししか勉強してないのでもっと最適化できる部分あるかもって感じです
よろしければコメントなどでご指摘いただければと思います