【Nuxt.js・Firebase】献立アプリをつくりました！

![ファーストビュー](https://user-images.githubusercontent.com/87516579/142719845-c357bd78-1824-44e7-a1c4-d90b84f8752c.png)

# はじめに
こんにちは。
こちらの記事では、Nuxt.jsとFirebaseを用いて開発したポートフォリオについて記しています。
機能の追加・修正は随時対応中（2021年12月8日現在）

# アプリの概要
筋トレを始めた人のためにランダムでメニュー生成してくれる「献立アプリ」です。

##想定ユーザー
筋トレを始めたばかりの人、これから始める人を想定しています。

##ポートフォリオの制作背景
仕事やトレーニングを両立させながら毎日タンパク質量を気にするのは手間がかかるので、ランダムで献立を生成するサービスを制作しました。

解決したい課題として、「筋トレ初心者が挫折する機会を減らしたい」という想いでつくりました。筋トレ初心者向けに食事に関するワンポイントアドバイスを記載し、献立を考える手間を減らすことでトレーニングに専念することができます。

##機能一覧

|     | 機　能                              |
| :-: | :---------------------------------- |
|  1  | アカウント登録機能                  |
|  2  | メールアドレスログイン機能          |
|  3  | Google ログイン機能                 |
|  4  | 匿名ログイン機能                    |
|  5  | パスワードリセット機能              |
|  6  | ログアウト機能                      |
|  7  | お問い合わせ機能                    |
|  8  | メニューのランダム表示機能（axios） |
|  9  | メニューの一覧表示機能（追加）      |
| 10  | メニューの質問選択機能（追加）      |


## 使用技術

- nuxt 2.15.7
- tailwindcss
- JavaScript
- CSS
- microCMS API
- SendGrid
- Firebase(Functions、Authentication、Hosting)

## インフラ構成図

![インフラ構成図](https://user-images.githubusercontent.com/87516579/142752772-81f71fe6-fbe6-455b-bdfd-caccd355b11d.png)

## ログイン方法
- ゲストログインボタンで簡単にログインできます。

![ゲストログイン](https://user-images.githubusercontent.com/87516579/142746105-c5206bb6-b146-45d4-9e42-c7bb6c6caf67.png)

## このアプリについて

### 1.トップページ

![ファーストビュー](https://user-images.githubusercontent.com/87516579/142719845-c357bd78-1824-44e7-a1c4-d90b84f8752c.png)

- トップページにアクセスするとこの画面が描画されます。
- ヘッダーにログイン・新規登録を配置して、nuxt-link でフォームを描画しています。

<br>

### 2.アカウント登録

![アカウント登録](https://i.gyazo.com/3ac73578d845dae92021fbb722c830e3.gif)

- メールアドレス、パスワードを入力して登録します。
- 入力完了後、ボタンの色が変更されます。
- アカウント登録と同時にアカウント情報を Firebase Authentication に保存しています。
- アラートが表示されてページ遷移します。（ページ遷移は pages ディレクトリ内で処理を実行しています）

<br>

### 3. アカウント認証

![アカウント認証](https://i.gyazo.com/f684a1d6a1f90c20acda7f3ad3cf3f08.gif)

- アカウント登録済みの場合はフォームにメールアドレス、パスワードを入力してログイン。
- アラートが表示されてページ遷移します。
- store ディレクトリで認証状態を管理。
- v-if で認証状態を判別し、ヘッダーに表示させるリンクを変更しています。

<br>

```javascript
<script>
export default {
  data: function () {
    return {
      email: "",
      password: "",
    };
  },
  methods: {
    login() {
      //this.$authは、firebase.auth()と同義
      this.$auth
        .signInWithEmailAndPassword(this.email, this.password)
        .then((user) => {
          alert("ログインに成功しました。");
          this.$store.dispatch("checkLogin");
          //ログイン成功でメニュー選択画面に遷移
          this.$router.push("/random");
        })
        .catch((error) => {
          console.log(error);
          alert("メールアドレスもしくはパスワードが違う可能性があります。");
        });
    },
  },
};
</script>

```

<br>

### 4.パスワード再設定

![パスワード再設定](https://i.gyazo.com/601358e5be80e2821af1182fba2c3d33.gif)

![再設定メール](https://i.gyazo.com/fd44710848f1f564c110e959df2592d3.png)

![パスワード変更通知](https://i.gyazo.com/611f163ac8061bbbe8920d45c0fc2fd6.png)

- 入力したメールアドレス宛に再設定のメールが送信されます。
- メールアドレスに記載された指示に従い、パスワードを変更することが出来ます。

<br>

### 5. メニューの選択

#### 5-1. ランダム表示機能

![メニューランダム](https://i.gyazo.com/d991d64f2795d21c1e265e8a12565ef4.gif)

- 「ランダムに選ぶ」ボタンをクリック。
- microCMS の API を叩いて ID を取得し、メニューページにランダムで遷移します。

```javascript
<template>
  <div class="mt-12 py-48 md:mt-20 md:py-64">
    <h2 class="text-center text-2xl font-bold md:text-4xl">
      いずれかのボタンから選択
    </h2>
    <AppButton
      text="ランダムに選ぶ"
      :link="`/menu/${randomId}`"
      class="mt-12 md:mt-20"
    />
    <AppButton text="一覧から選ぶ" link="/menu/list" />
    <AppButton text="質問から選ぶ" link="/question" />
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      ids: [],
      randomId: "",
    };
  },
  asyncData: async function ({ $microcms }) {
    const menuData = await $microcms.get({
      endpoint: "menu",
    });
    const contents = menuData.contents;
    const ids = contents.map((e) => {
      return e.id;
    });
    return { ids };
  },
  mounted: function () {
    this.randomId = this.ids[Math.floor(Math.random() * this.ids.length)];
  },
};
</script>
```

<br>

#### 5-2. 一覧表示機能

![メニュー一覧](https://i.gyazo.com/b2135fb98b8a1544141a1287c5600c2e.gif)

- 「一覧から選ぶ」ボタンをクリック。
- 取得したデータをもとに v-for で繰り返し描画し、選択したメニューページに遷移します。

<br>

#### 5-3. 質問選択機能

![メニュー質問](https://i.gyazo.com/0459c618994eef50fb3061833e2f8b51.gif)

- 「質問から選ぶ」ボタンをクリック。
- 質問に回答することで、気分に合ったメニューページに遷移します。

<br>

### 6. お問い合わせ機能

![お問い合わせGIF](https://i.gyazo.com/55a7e09fe2dec3ed3e61cb79b98cf7a9.gif)

**【ユーザー側】**

![お問い合わせ画像１](https://i.gyazo.com/e58e22c42a3b726f9f22a9e7be4bb57b.png)

**【管理者側】**

![お問い合わせ画像２](https://i.gyazo.com/12afea8532f8f9e64effb8964fc4650a.png)

- お問い合わせフォームから送信が可能。
- Firebase Functions で SendGrid のメール送信 API を実行する。
- 入力したユーザーのメールアドレス宛に確認メール、管理者宛に受付メールが送信されます。

```javascript
<script>
export default {
  data: function () {
    return {
      form: {
        name: "",
        email: "",
        content: "",
      },
    };
  },
  methods: {
    async sendMail() {
      const sendContents = await this.$firebase
        .app()
        .functions("asia-northeast1")
        .httpsCallable("sendMail");
      sendContents({
        name: this.form.name,
        email: this.form.email,
        content: this.form.content,
      })
    }
  }
}
</script>
```

<br>

### 7. レスポンシブ対応

![レスポンシブ](https://i.gyazo.com/d79087990b2511ced810f2762eea2cdd.gif)

- スマートフォンからでも使用可能。
- tailwindcss を用いて、ハンバーガーメニューを実装。

<br>

### 8. バリデーション

![バリデーション1](https://i.gyazo.com/5b3ac1dbd250fc22cfe130aadc0597b8.png)
![バリデーション2](https://i.gyazo.com/a62292d6da392a495137930e2c8676b4.png)

- フォームの入力欄は必須項目に。
- 正規表現で、メールアドレスは `@` を含める、パスワードは半角英数字を含んだ 8-20 文字の範囲で入力。
- 登録済みのメールアドレスの場合も、メッセージを表示。

```javascript
<template>
  <form @submit.prevent class="px-8 md:px-40" novalidate>
    <div>
      <div class="mt-12 w-full">
        <input
          type="email"
          name="email"
          required="required"
          placeholder="メールアドレス"
          v-model="email"
          @focus="isInput"
          @blur="isValidEmail"
          class="
            pl-4
            w-full
            h-14
            text-base
            border-2 border-solid border-black border-opacity-25
            hover:border-opacity-50
            rounded-md
            transition
            duration-300
          "
          autofocus
        />
        <p class="text-red-400">{{ emailErrorMassage }}</p>
      </div>
      <div class="mt-12 w-full">
        <input
          type="password"
          name="password"
          required="required"
          placeholder="パスワード"
          v-model="password"
          @focus="isInput"
          @blur="isValidEmail"
          class="
            pl-4
            w-full
            h-14
            text-base
            border-2 border-solid border-black border-opacity-25
            hover:border-opacity-50
            rounded-md
            transition
            duration-300
          "
        />
        <p class="text-red-400">
          {{ passwordErrorMassage }}
        </p>
      </div>
      <button
        type="submit"
        @click="register"
        class="
          block
          mt-8
          mx-auto
          w-2/3
          h-12
          text-white text-xl
          rounded-md
          cursor-pointer
          transition
          duration-300
        "
        :class="btnColor"
      >
        登録
      </button>
    </div>
  </form>
</template>

<script>
export default {
  data: function () {
    return {
      email: "",
      password: "",
      emailErrorMassage: "",
      passwordErrorMassage: "",
      emailRegexp: /^[a-z\d][\w.-]*@[\w.-]+\.[a-z\d]+$/i,
      passwordRegexp: /^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,20}$/i,
    };
  },
  computed: {
    btnColor: function () {
      return {
        "bg-red-500": this.email !== "" && this.password !== "",
        "bg-gray-200": this.email === "" || this.password === "",
      };
    },
  },
  methods: {
    googleLogin() {
      this.$auth
        .signInWithPopup(new this.$firebase.auth.GoogleAuthProvider())
        .then(() => {
          alert("登録が完了しました。");
          this.$store.dispatch("checkLogin");
          this.$router.push("/random");
        })
        .catch((error) => {
          console.log(error);
          alert(
            "エラーにより登録ができませんでした。恐れ入りますが再度お試しください。"
          );
        });
    },
    register() {
      this.$auth
        .createUserWithEmailAndPassword(this.email, this.password)
        .then((user) => {
          alert("登録が完了しました。");
          this.$store.dispatch("checkLogin");
          this.$router.push("/random");
        })
        .catch((error) => {
          console.log({ code: error.code, message: error.message });
          if (error.code === "auth/invalid-email") {
            this.emailErrorMassage = "このメールアドレスは無効です。";
          } else if (error.code === "auth/email-already-in-use") {
            this.emailErrorMassage =
              "このメールアドレスは既に使用されています。";
          } else {
            alert(
              "エラーにより登録ができませんでした。恐れ入りますが再度お試しください。"
            );
          }
        });
    },
    isInput() {
      this.emailErrorMassage = "";
      this.passwordErrorMassage = "";
    },
    isValidEmail() {
      if (!this.emailRegexp.test(this.email)) {
        this.emailErrorMassage =
          "このメールアドレスは無効です。正しく入力してください。";
      }
      if (this.email === "") {
        this.emailErrorMassage = "メールアドレスを入力してください。";
      } else {
        return;
      }
    },
    isValidPassword() {
      if (!this.passwordRegexp.test(this.password)) {
        this.passwordErrorMassage =
          "このパスワードは無効です。半角英数字を含んで8-20文字の範囲内で入力してください。";
      }
      if (this.password === "") {
        this.passwordErrorMassage = "パスワードを入力してください。";
      } else {
        return;
      }
    },
  },
};
</script>
```

<br>

### 9. ボタンのコンポーネント化

![ボタン](https://i.gyazo.com/cfa7bf2096cdd794ee4e04a3157c96e6.png)

- ボタンを components で作成。
- props でデータを受け渡しているので、表示するテキストやリンク、ボタンの色を変更可能。

```javascript
<template>
  <div class="md:py-22 container mx-auto py-8 w-64">
    <div
      class="flex justify-center mx-auto border-2 rounded-full cursor-pointer"
      :class="[btnBorder, btnColor, `hover:${colorOnHover}`]"
    >
      <p class="w-full text-center">
        <nuxt-link :to="link" class="block py-4 w-full text-lg md:text-2xl">{{
          text
        }}</nuxt-link>
      </p>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    text: {
      type: String,
      default: "",
    },
    link: {
      type: String,
      default: "/",
    },
    btnBorder: {
      type: String,
      default: "border-black",
    },
    btnColor: {
      type: String,
      default: "",
    },
    colorOnHover: {
      type: String,
      default: "bg-gray-100",
    },
  },
};
</script>
```

<br>

### 10. ユーザーへのヒアリング

![ヒアリング1](https://i.gyazo.com/2571859dc597d12f0806c86155b4e02f.png)
![ヒアリング2](https://i.gyazo.com/f020b46358d55f89a527a680b0d63c6f.png)
![ヒアリング3](https://i.gyazo.com/e43a7ddd2fdfab8c2d215d21893d122d.png)

- アプリを使用したユーザーへヒアリングを行い、お問い合わせページから投稿していただきました。
  - ~~送信完了後にページ遷移~~ (2021 年 11 月 22 日対応済み)
  - ~~メニュー数の増加~~ (2021 年 11 月 28 日対応済み)
  - ~~スクロールストップ~~ （2021 年 11 月 27 日対応済み）
- 機能の追加実装
  - メニューの一覧表示機能（2021 年 11 月 30 日実装）
  - メニューの質問選択機能（2021 年 12 月 7 日実装）

<br>

### 11. 工夫したところ（UI/UX）

- ファーストビューで、どんな人の課題を解決するアプリなのか伝わる画像を配置しました。（デザイナーさんに外注）
- ログイン → ボタンクリック → メニュー表示 のわかりやすい設計にしました。
- 「筋トレ初心者」をターゲットにしているため、明るく親しみやすい配色を採用しております。

<br>

### 12. 工夫したところ（実装面）

####ログイン状態を取得して判別し、リダイレクト処理を実行

```javascript
export default function ({ store, route, redirect }) {
  const userLogin = store.state.user.login;
  if (
    !userLogin &&
    route.name !== "login" &&
    route.name !== "register" &&
    route.name !== "passReset" &&
    route.name !== "contact" &&
    route.name !== "index"
  ) {
    return redirect("/login");
  }
  if (
    (userLogin && route.name === "login") ||
    (userLogin && route.name === "register")
  ) {
    return redirect("/random");
  }
}
```

<br>

####microCMS の API から取得した HTML を、 v-html で描画

```html
<div
  v-html="menu.material"
  class="menu-material pt-8 px-8 md:pt-16 md:text-xl md:leading-relaxed"
></div>
```

<br>

####plugins ディレクトリ内で inject を使用して関数を共通化。`this.$-`で呼び出し可能に

```javascript
const auth = firebase.auth();
const functions = firebase.functions();

export default function (context, inject) {
  inject("auth", auth); // this.$auth
  inject("functions", functions); // this.$functions
  inject("firebase", firebase); // this.$firebase
}
```

<br>

### 13. 工夫したところ（学習面）

![学習面2](https://i.gyazo.com/d9c4fbbd14114281c5bde523f4624d3d.png)

- Twitter や Qiita、note などで学習内容をアウトプットを継続。
- どうしても理解できないことに関しては、起きている問題・問題解決のために試したこと・聞きたいことなど、整理してからメンターさんに質問。常にコミュニケーションコストを最小限にすることを意識。

<br>

### 14. 工夫したところ（開発面）


- 理解できていないロジックは、紙に書き出して整理しながら進めました。
- 現役エンジニアからヒアリングした情報をもとに、イシューの作成・機能ごとにブランチを作成・コミットなど、開発現場を想定した工程で開発を実施。
- イシュー・コミット・プルリクエストを連携させ、後から変更内容を確認しやすくなることを意識。

### 15. 苦労したところ

#### microCMSのAPIから取得したメニューをランダムで表示

取得したデータをランダムで表示させるにはなにが最善なのか、を試行錯誤するのが最初の関門でした。

microCMSとNuxt.jsのAPI関連の記事で情報収集を行い、asyncData内で処理を実行しました。`mapメソッド`や`Math.random関数`など、JavaScriptの基礎学習をした際の内容も復習しながら活かすことができました。

<br>

#### Firebase Authentication を使用したログインまわりの実装

アカウント登録やログイン、ログアウトの実装にFirebaseを使用しました。
storeディレクトリでの認証状態の管理、midllewareディレクトリでのリダイレクト処理など、新しく実装する機能も多かったのでひとつひとつ理解できるよう、紙にロジックを書き出したりQiitaでアウトプットを行なっていきました。

Firebaseの公式サイトをもとに、ユーザーが起こしたアクションに対してわかりやすくアラートでメッセージを表示するようにしました。

<br>

#### Firebase Functions と SendGrid を連携しお問い合わせ機能の実装

お問い合わせ内容を送受信するのに、SendGrid のメール送信APIを使用しました。
APIキーを環境変数に設定して、外部からのアクセスを防止しております。

ユーザーからお問い合わせ
→　フロント側で情報を取得
→　サーバー側に渡してメールを送信
→　ユーザーへレスポンス

この流れがスムーズにいかずに何度もエラーに遭遇しましたが、その経験からエラーに耐性がついて、「解決できれば成功の確率が上げられる！」「成長できるチャンス！」とポジティブに捉えることができるようになりました。


<br>

# おわりに
ここまで、約２ヶ月ほどかけて開発したポートフォリオについてまとめました。

新しい機能や概念に多く触れる機会となり、この期間で何度も壁にぶち当たっていたので、正直しんどくなることもありました。ですが、それ以上にできなかったことができるようになる成功体験がほんっっっっっとうに嬉しくて、毎日が刺激的でした笑

意識していたこととして、処理が実行できたら「なぜ動いたのか」「どこを間違えるとエラーが起きるのか」に着目して開発を進めていきました。実際にエンジニアとして働く際にも、コピペしかできないエンジニアではなく、処理内容を把握してわかりやすい設計を意識した開発のできるエンジニアが求められていると考えています。

このアプリ自体も、公開はしていますがまだまだ未完成です。
ユーザーにヒアリングした内容や実装したい機能が多くあるので、転職活動と並行して改善を繰り返していきたいと思います！

これからもアウトプットを意識してスキル向上につとめていきます！

以上、最後まで読んでいただきありがとうございました！
誤っている点や改善点などがあればコメントいただけると助かります！
よければLGTMもよろしくお願いします！