Nuxt.js✖️Firebaseで「表参道ランチマップ」作ってみた

#自己紹介
[G's Academy東京](https://gsacademy.tokyo/)　LAB8期に通っています。
６ヶ月の通いのうち、現在１ヶ月が経過しました。

#Javascriptを使った課題

Javascriptを使えば何しても良いということで、やりたいことは無限にあるので、今回はNuxt.jsを触ることにしました。
発表の日を目標にデプロイする気持ちで、めちゃめちゃ頑張りました。
とにかく「作りきる」が目標。

#作ったもの
表参道（G's Academyの最寄駅）のランチを投稿できるウェブサービス
<img width="1431" alt="スクリーンショット 2019-11-13 15.22.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447711/41936ab6-16f5-5c84-3a30-7921a24f0c85.png">


#Vue.jsは楽しい
何が楽しいかは言葉で説明できないので、触ってみればきっと楽しいと思います。

#Nuxtについての感想
自分は公式のドキュメントに従って、VueCLIしか触ったことがなかったので、大きな差異について語ると、
・Vuexをインストールする必要がない
・VueRouterをインストールする必要がない
VueCLIで始める必要ないという噂は本当だなぁと思った。

#使ったもの
Nuxt.js
Vuetify
Firestore
FireStorage
GooglemapAPI
OpenWeatherMap

#実装したこと
・ランチの投稿機能
　　→画像保存（firestorage）
　　→店の位置（GooglemapAPI）
・いいね機能
・コメント機能
・ストック機能（qiitaをリスペクト）
・猫

#苦労した点（以下、コードがメインです）
###画像ファイルをFirebaseStorageに入れて、FireStoreに画像のurlをaddする

Vuetifyの

```
<v-file-input></v-file-input>
```
は、changeでファイルの入力を受け取った時にBlob等での変換処理がいらないみたいです。
なので、

```create.vue
<template>
  <v-file-input type="file" @change="onFileChange" label="画像の添付"></v-file-input>
</template>

export default{

data(){
  return{
   blob: [],
  }
 }
methods:{
    onFileChange(e) {
      const files = e;
      this.createImage(e);
      this.img_name = e.name;
      this.blob = files;
 }
}
```
これでblobに画像のデータが入るようで、HTML＋JSより圧倒的に記述が少ない。
methodsの続き

```create.vue
filesend() {
      console.log("storage1");
      let uploadRef = firebase
        .storage()
        .ref("image/" + this.$store.getters.uid)
        .child(this.img_name);
      if (this.blob) {
        uploadRef.put(this.blob).then(snapshot => {
          snapshot.ref
            .getDownloadURL()
            .then(url => {
              this.addPost({
                url: url,
              });
            })
            .catch(error => {
              console.log(error);
            });
        });
      } else {
        this.dialog = false;
      }
    },


*addPostはVuexのactionsにfirebaseにaddする処理が書かれています。
```
もし、ファイルがアップロードされたらurlを保存するという処理です。

##Vuexの処理のサンプル
Vuexではページから送られきたPostのオブジェクトを保存して表示に反映されるように、

```create.js
import firebase from '~/plugins/firebase';

const db = firebase.firestore();
const postsRef = db.collection('posts');

export const state = () => ({
  posts: [],
  contribution: 0
});

export const mutations = {
  addPost(state, { id, post }) {
    post.id = id;
    state.posts.push(post);
  },
  deletePost(state, { id }) {
    console.log('test');
  },
  addContribution(state, number) {
    state.contribution = number;
  }
};

export const actions = {
  addPost({ commit }, post) {
    postsRef.add(post).then(doc => {
      commit('addPost', { id: doc.id, post });
    });
  },

  fetchPost({ commit }) {
    postsRef
      .orderBy('timestamp', 'desc')
      .get()
      .then(snapshot => {
        snapshot.forEach(doc =>
          commit('addPost', { id: doc.id, post: doc.data() })
        );
      });
  },

  deletePost({ commit }, id) {
    // console.log(id);
    postsRef
      .doc(id)
      .delete()
      .then();
    commit('deletePost', id);
  },

  addContribution({ commit }, number) {
    commit('addContribution', number);
  },

  test() {
    console.log('test');
  }
};

export const getters = {
  posts: state => (state.posts ? state.posts : null),
  getPostId: state => post_id => state.posts.find(post => post.id === post_id),
  getCategoryPost: state => category => {
    return state.posts.filter(post => post.category === category);
  }
};

```

こんな感じで書いています。削除の表示が反映されていないので改善点


###しゃべる猫（OpenWeatherMapを使って天気によって色々しゃべる）
クリックすると猫ちゃんが天気によってオススメのお店を提案してくれるニャ！
<img width="377" alt="スクリーンショット 2019-11-13 16.24.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447711/401f7e31-8190-f00a-6348-8e8942e1ea87.png">


```neko.vue
<template>
  <div class="neko-image">
    <div id="nekocat" v-if="neko_display" v-show="toggle" class="neko1 balloon1-right flex">
      <div class="fukidasi">
        <p>ミーをクリックすると、オススメのお店を紹介するニャ！</p>
      </div>
      <img
        @click="neko1"
        src="https://firebasestorage.googleapis.com/v0/b/lunch-app-16cce.appspot.com/o/neko1.png?alt=media&token=14b8475e-e533-4602-a354-f5be7be0c88c"
        alt
      />
    </div>
    <transition name="bounce" mode="out-in">
      <div v-show="!toggle" class="neko2 balloon2-right flex">
        <div class="fukidasi">
          <p>{{fukidasi}}</p>
          <p>
            {{shop.comment}}
            <span class="text-blue">{{shop.name}}</span>
            で、おいしい{{shop.food}}でもいかがかニャ？
          </p>
          <a :href="shop.url" target="_blank" rel="noopener">
            <v-btn small color="primary" class="font-weight-bold">お店のホームページ</v-btn>
          </a>
        </div>
        <img
          @click="neko2"
          src="https://firebasestorage.googleapis.com/v0/b/lunch-app-16cce.appspot.com/o/neko2.png?alt=media&token=070f6083-0c34-4fb7-8851-1d1c932d3af4"
        />
      </div>
    </transition>
  </div>
</template>

// <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script>
import axios from "axios";

export default {
  data() {
    return {
      neko_display: false,
      toggle: true,
      temp_max: null, //今日の最高気温
      condition: {
        main: null //天気
      },
      fukidasi: "",
      shop: [{ name: "", food: "", url: "", comment: "" }],
      clear_message: [
        "今日は気持ち良い晴れニャ。",
        "今日はミーのような透き通った晴れニャ。"
      ],
      clear_shop: [
        {
          name: "エイベックスビル前広場 ネオ屋台村",
          food: "お弁当",
          url:
            "https://www.w-tokyodo.com/neostall/space/lunch/?lunch=%E3%82%A8%E3%82%A4%E3%83%99%E3%83%83%E3%82%AF%E3%82%B9%E3%83%93%E3%83%AB%E5%89%8D%E5%BA%83%E5%A0%B4%20%E3%83%8D%E3%82%AA%E5%B1%8B%E5%8F%B0%E6%9D%91",
          comment: "ミーは鯖のお弁当が大好きニャ。"
        },
        {
          name: "讃岐うどん 愛",
          food: "うどん",
          url: "https://tabelog.com/tokyo/A1306/A130602/13001185/",
          comment: "モチモチした食感がたまらニャイ、"
        }
      ],
      clouds_message: [
        "今日はパッとしない天気になるニャ。",
        "今日はミーの嫌いな曇りニャ。"
      ],
      clouds_shop: [
        {
          name: "アジアンランチ 表参道店",
          food: "お弁当",
          url: "http://www.asianlunch.co.jp/",
          comment: "ミーのオススメのタイ料理が食べられる"
        },
        {
          name: "おまかせ亭",
          food: "ランチメニュー",
          url: "https://www.omakasety.com/",
          comment: "今日はちょっと贅沢なランチするニャ！"
        }
      ],
      rain_message: [
        "今日は雨ニャ。雨の日はなんだか楽しいニャ。",
        "雨ニャ。ミーが長靴を履いた猫のモデルなんだニャ。嘘ニャ♪"
      ],
      rain_shop: [
        {
          name: "Uver Eats",
          food: "デリバリー",
          url:
            "https://www.ubereats.com/ja-JP/feed/?pl=JTdCJTIyYWRkcmVzcyUyMiUzQSUyMiVFOCVBMSVBOCVFNSU4RiU4MiVFOSU4MSU5MyUyMiUyQyUyMnJlZmVyZW5jZSUyMiUzQSUyMkNoSUp5Y1ZIME0tTkdHQVJZcUc1dUtacjR6NCUyMiUyQyUyMnJlZmVyZW5jZVR5cGUlMjIlM0ElMjJnb29nbGVfcGxhY2VzJTIyJTJDJTIybGF0aXR1ZGUlMjIlM0EzNS42NjUyOSUyQyUyMmxvbmdpdHVkZSUyMiUzQTEzOS43MTIxOCU3RA%3D%3D",
          comment: "職場のみんなで"
        },
        {
          name: "Line デリマ",
          food: "デリバリー",
          url: "https://delima.line.me/",
          comment: "実はお得ニャ"
        }
      ],
      else_shop: [
        {
          name: "セブンイレブン",
          food: "お好み焼き",
          url:
            "https://www.sej.co.jp/i/item/300301410191.html?category=186&page=1",
          comment: "実はミーは週に１回は食べてしまうほど好きニャ！"
        }
      ]
    };
  },
  methods: {
    neko_hyouji() {
      this.neko_display = true;
    },
    neko1() {
      this.toggle = false;
      let r;
      let s;
      function rand(n) {
        return Math.ceil(Math.random() * n);
      }
      console.log(this.condition.main);
      if (this.fukidasi == false) {
        if (
          this.condition.main == "Thunderstorm" ||
          this.condition.main == "Drizzle" ||
          this.condition.main == "Rain"
        ) {
          r = rand(this.rain_message.length);
          this.fukidasi = this.rain_message[r - 1];
          s = rand(this.rain_shop.length);
          this.shop = this.rain_shop[r - 1];
        } else if (this.condition.main === "Clouds") {
          r = rand(this.clouds_message.length);
          this.fukidasi = this.clouds_message[r - 1];
          s = rand(this.clouds_shop.length);
          this.shop = this.clouds_shop[s - 1];
        } else if (this.condition.main == "Clear") {
          r = rand(this.clear_message.length);
          this.fukidasi = this.clear_message[r - 1];
          s = rand(this.clear_shop.length);
          this.shop = this.clear_shop[s - 1];
        } else {
          this.fukidasi = "今日もミーは絶好調ニャ！";
          s = rand(
            this.rain_shop.length +
              this.clouds_shop.length +
              this.clear_shop.length
          );
          if (s <= this.rain_shop.length) {
            this.shop = this.rain_shop[s - 1];
          } else if (s <= this.rain_shop.length + this.clouds_shop.length) {
            this.shop = this.clouds_shop[s - 1 - this.rain_shop.length];
          } else {
            this.shop = this.clear_shop[
              s - 1 - this.rain_shop.length - this.clouds_shop.length
            ];
          }
        }
      } else {
        this.fukidasi = "今日もミーは絶好調ニャ！";
      }
    },
    neko2() {
      this.toggle = true;
    }
  },
  created: function() {
    axios
      .get(
        "https://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp&units=metric&APIkey"
      )
      .then(
        function(response) {
          this.city = response.data.name;
          this.temp = response.data.main.temp;
          this.tenki = response.data.weather.main;
          this.condition = response.data.weather[0];
          this.temp_max = response.data.main.temp_max;
        }.bind(this)
      )
      .catch(function(error) {
        console.log(error);
      });
  },
  mounted() {
    // console.log("test");
    setTimeout(this.neko_hyouji, 10000);
  }
};
</script>

<style scoped>
.neko-image {
  position: absolute;
  z-index: 2;
  height: 100px;
  width: 100px;
  top: 200px;
  right: 0;
}

.neko-image img {
  height: 100px;
  width: 100px;
  object-fit: cover;
}

.neko-image img:hover {
  cursor: pointer;
}

.balloon1-right {
  text-align: right;
  display: inline-block;
  margin: 1.5em 15px 1.5em 0;
  padding: 20px 10px;
  min-width: 340px;
  min-height: 150px;
  max-width: 100%;
  max-height: 100%;
  color: #555;
  font-size: 16px;
  background: #fff;
  border: solid 3px #555;
  box-sizing: border-box;
  border-radius: 50%;
}

.balloon2-right {
  text-align: right;
  display: inline-block;
  margin: 1.5em 15px 1.5em 0;
  padding: 7px 10px;
  min-width: 340px;
  min-height: 250px;
  max-width: 100%;
  max-height: 100%;
  color: #555;
  font-size: 16px;
  background: #fff;
  border: solid 3px #555;
  box-sizing: border-box;
  border-radius: 15%;
}

.flex {
  display: flex;
  justify-content: space-between;
}

.text-blue {
  color: blue;
  font-weight: bold;
}

.neko1 {
  position: absolute;
  right: 0;
}

.neko2 {
  position: absolute;
  right: 0;
}

.fukidasi {
  margin-top: 20px;
  text-align: left;
}

.bounce-enter-active {
  animation: bounce-in 1s;
}
.bounce-leave-active {
  animation: bounce-out 1s;
}

@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.3) translateX(-150px);
  }
  100% {
    transform: scale(1);
  }
}

@keyframes bounce-out {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.3);
  }
  100% {
    transform: scale(0);
  }
}

.balloon2-right p {
  margin: 0;
  padding: 0;
}

@media screen and (max-width: 767px) {
  .neko-image {
    height: 50px;
    width: 50px;
  }

  .neko-image img {
    height: 50px;
    width: 50px;
    object-fit: cover;
  }

  .neko-image img:hover {
    cursor: pointer;
  }

  .balloon1-right {
    opacity: 0.8;
    text-align: right;
    display: inline-block;
    /* margin: 1.5em 15px 0em 0; */
    padding: 20px 10px;
    min-width: 170px;
    min-height: 75px;
    max-width: 100%;
    max-height: 100%;
    color: #555;
    font-size: 10px;
    background: #fff;
    border: solid 2px #555;
    box-sizing: border-box;
    border-radius: 50%;
  }

  .balloon2-right {
    text-align: right;
    display: inline-block;
    /* margin: 1.5em 15px 1.5em 0; */
    padding: 7px 10px;
    min-width: 170px;
    min-height: 125px;
    max-width: 100%;
    max-height: 100%;
    color: #555;
    font-size: 10px;
    background: #fff;
    border: solid 2px #555;
    box-sizing: border-box;
    border-radius: 15%;
  }

  .fukidasi {
    margin-top: 0;
  }
}
</style>

```
自分はナビゲーションバーにposition:fixedしているのでそこが基準になっています。
データのところやメッセージのバリエーションを増やしたい時、そこに追加すればどんどんバリエーションが増えるので、今後のアップデートをしやすくしました。

#最後に
今日、早速ランチを投稿してもらったのですが、
・投稿を押した後の反応がない
・ログインしたときの反応がなくて、ログインできたか不安
・店を追加した時にセレクトする必要がない方が良い
・ボタンの場所が悪すぎる

など色々使用感をいただけたので、改善していきたいと思います。
とにかく何か１つ作りきってみたいということがとりあえず実現できたのでよかった。
自分が作ったものを使ってもらえるとすごくテンションが上がります。みなさんもぜひ！！！

#参考文献
[Nuxt.js](https://ja.nuxtjs.org/)
[Vue.js](https://jp.vuejs.org/index.html)
[基礎から学ぶVue.js](https://cr-vue.mio3io.com/)
[Udemy講座](https://www.udemy.com/course/vuejs-firebase/learn/lecture/14204342?start=0#overview)