初心者向け、LaravelとNuxt.jsで作るログイン機能をゼロからいっしょに作ろう！

## はじめに
LaravelとNuxt.jsを使って開発したいと思っても最初に何を作ったらいいか思いつきませんよね。
どんなサービスを作るにしても基本となる、**ログイン機能**を作ることで基礎を定着させましょう。

### 開発環境
基本はDockerで構築しています。
フロントエンド側では、composition-apiやTypeScriptなどを導入しています。

#### Docker
・php:7.4-fpm-buster
・composer:1.10
・nginx:1.18-alpine
・mysql:5.7
・node:14.15.3-alpine

#### Laravel
・Laravel Passport

#### Nuxt.js
・TypeScript
・@nuxtjs/axios
・@nuxtjs/composition-api/module
・vee-validate

## 完成コード
https://github.com/ssk9597/Login-Form

## それでは一緒にやってみましょう！

### Githubからクローン
Dockerから解説となると長くなるので省略します。

https://github.com/ssk9597/Docker-Laravel-Nuxt-Nginx-MySQL

GitHubにやり方は書いていますが、以下を実行することで環境構築ができます。

```terminal:ターミナル
$ git clone git@github.com:ssk9597/Docker-Laravel-Nuxt-Nginx-MySQL.git
$ cd Docker-Laravel-Nuxt-Nginx-MySQL
$ make nuxt
$ make backend
$ make typescript
$ make composition-api
```

`make typescript`と`make composition-api`の2つを実行時にはファイルに追記が必要となります。

こちらは以下の記事を参考にしてください。

https://qiita.com/Ryo9597/items/ba0838e68da2a73bc55b

### まずはHTMLとCSS部分を作成
```vue:frontend/pages/register/index.vue
<template>
  <div class="register-container">
    <form class="register-wrapper">
      <h2 class="register-title">新規登録</h2>
      <!-- 姓 -->
      <label class="register-label">
        <span class="register-label-check">必須</span>
        <span class="register-label-name">姓</span>
      </label>
      <input class="register-input-area" type="text" placeholder="例）田中" />
      <!-- 名 -->
      <label class="register-label">
        <span class="register-label-check">必須</span>
        <span class="register-label-name">名</span>
      </label>
      <input class="register-input-area" type="text" placeholder="例）太郎" />
      <!-- メールアドレス -->
      <label class="register-label">
        <span class="register-label-check">必須</span>
        <span class="register-label-name">メールアドレス</span>
      </label>
      <input class="register-input-area" type="email" placeholder="例）taro@example.com" />
      <!-- パスワード -->
      <label class="register-label">
        <span class="register-label-check">必須</span>
        <span class="register-label-name">パスワード</span>
      </label>
      <input class="register-input-area" type="password" placeholder="例）taroTanaka" />
      <!-- 新規追加 -->
      <button class="register-button" type="submit">新規登録</button>
    </form>
  </div>
</template>

<style lang="scss" scoped>
.register {
  &-container {
    position: relative;
    height: 100vh;
    width: 100%;
    background-color: #eeeeee;
  }

  &-wrapper {
    position: absolute;
    top: 50%;
    right: 50%;
    transform: translateX(50%) translateY(-50%);
    max-width: 400px;
    width: 100%;
    background-color: #fff;
    border-radius: 10px;
    padding: 50px 50px;
  }

  &-title {
    text-align: center;
    padding-bottom: 20px;
  }

  &-alert-red {
    padding: 5px 10px;
    background: #ffebee;
    margin-bottom: 10px;
    font-size: 12px;
  }

  &-label {
    &-check {
      background-color: #ee4056;
      color: #fff;
      padding: 3px 10px;
      border-radius: 10%;
      font-size: 0.75rem;
      font-weight: 600;
    }
    &-name {
      font-size: 0.8rem;
      padding: 5px;
    }
  }

  &-input {
    padding-top: 10px;
    padding-bottom: 15px;
    display: block;

    &-area {
      box-sizing: border-box;
      width: 100%;
      padding: 10px;
      font-size: 14px;
      color: #303030;
      border: solid 1px #eee;
      border-radius: 6px;
      outline: 0;
      transition: 0.3s;
    }

    &-error {
      padding-top: 5px;
      font-size: 12px;
      color: #ee4056;
      font-weight: 600;
    }
  }

  &-button {
    margin-top: 20px;
    width: 100%;
    height: 40px;
    color: #fff;
    background: #40c7c1;
    text-decoration: none;
    border: none;
    outline: none;
    border-radius: 8px;
    cursor: pointer;
    appearance: none;
    transition: 0.2s;

    &:hover {
      background: #78cec8;
    }
  }
}
</style>
```

### フロントエンド側にバリデーションを追加
フロントエンドとバックエンドの両方にバリデーションをつけることが必須です。
フロントエンドにバリデーションがない場合、HTTP通信が走ってしまうのでエラーページへ遷移してしまいます。
また万が一、APIを直接叩かれたときにバックエンド側にバリデーションがないとDBに直接値が入ってしまう可能性があります。
以上の理由からフロントエンドにバリデーションを設定しましょう。

Nuxt.jsには、**VeeValidate**があるのでこちらを使って実装していきましょう。

https://vee-validate.logaretm.com/v3

#### インストール方法
##### ①VeeValidateをインストールする
```terminal:ターミナル
$ docker-compose exec front npm install vee-validate --save
```

##### ②プラグインファイルを作成する
```javascript:frontend/plugins/vee-validate.js
import Vue from 'vue';

// 使用する機能
import { ValidationProvider, ValidationObserver, localize, extend } from 'vee-validate';

// エラーメッセージの日本語
import ja from 'vee-validate/dist/locale/ja.json';

// 使用するルール
import { required, email, min, alpha_dash } from 'vee-validate/dist/rules';
extend('required', required);
extend('email', email);
extend('min', min);
extend('alpha_dash', alpha_dash);

Vue.component('ValidationProvider', ValidationProvider);
Vue.component('ValidationObserver', ValidationObserver);
localize('ja', ja);
```

##### ③nuxt.config.jsに追記
```javascript:frontend/nuxt.config.js
{
   plugins: [{ src: '@/plugins/vee-validate' }],
   build: {
    transpile: ['vee-validate/dist/rules'],
  },
}
```

これで使えるようになります。

#### 使い方

```vue:index.vue
// まずは全体をValidationObserverで囲もう。
// :disabledにinvalidが付くことでバリデーションすべてクリアしないとボタンがクリックできません。
<ValidationObserver v-slot="{ invalid }">
   <button type="submit" :disabled="invalid">新規登録</button>
</ValidationObserver>
```

```vue:index.vue
// バリデーション対象をValidationProviderで囲もう。
// nameはバリデーションエラー時のメッセージで使われます。
// rulesにどんどんルールを入れていきます。
<ValidationProvider name="パスワード" rules="required|min:8|alpha_dash" v-slot="{ errors }">
    <input type="password" placeholder="例）taroTanaka" v-model="password" />
    <p>{{ errors[0] }}</p>
</ValidationProvider>
```

ルールに関しては以下の公式サイトが網羅されているのでこちらをご確認ください。

https://vee-validate.logaretm.com/v3/guide/rules.html#rules

#### それでは実装していきましょう
```vue:frontend/pages/register/index.vue
<template>
  <div class="register-container">
    <ValidationObserver v-slot="{ invalid }">
      <form class="register-wrapper">
        <h2 class="register-title">新規登録</h2>
        <!-- 姓 -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">姓</span>
        </label>
        <ValidationProvider class="register-input" name="姓" rules="required" v-slot="{ errors }">
          <input class="register-input-area" type="text" placeholder="例）田中" />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- 名 -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">名</span>
        </label>
        <ValidationProvider class="register-input" name="名" rules="required" v-slot="{ errors }">
          <input class="register-input-area" type="text" placeholder="例）太郎" />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- メールアドレス -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">メールアドレス</span>
        </label>
        <ValidationProvider
          class="register-input"
          name="メールアドレス"
          rules="required|email"
          v-slot="{ errors }"
        >
          <input class="register-input-area" type="email" placeholder="例）taro@example.com" />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- パスワード -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">パスワード</span>
        </label>
        <ValidationProvider
          class="register-input"
          name="パスワード"
          rules="required|min:8|alpha_dash"
          v-slot="{ errors }"
        >
          <input class="register-input-area" type="password" placeholder="例）taroTanaka" />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- 新規追加 -->
        <button class="register-button" type="submit" :disabled="invalid">新規登録</button>
      </form>
    </ValidationObserver>
  </div>
</template>
```

### スクリプトを追加
それでは次に動作を担うスクリプトの部分を追加していきましょう。
分けて考えた方が分かりやすいと思いますので今回は、`data`, `computed`, `methods`の3つのパートに分けて説明していきます。

#### `data`
特に難しいところはないですね。
`lastName`、`firstName`、`email`、`password`の4つは、DBへ登録するために使う値です。
`errors`はエラー文を表示する際に使用します。

```vue:frontend/pages/register/index.vue
<template>
  <div class="register-container">
    <ValidationObserver v-slot="{ invalid }">
      <form class="register-wrapper">
        <h2 class="register-title">新規登録</h2>
        <!-- 姓 -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">姓</span>
        </label>
        <ValidationProvider class="register-input" name="姓" rules="required" v-slot="{ errors }">
          <input
            class="register-input-area"
            type="text"
            placeholder="例）田中"
            v-model="lastName"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- 名 -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">名</span>
        </label>
        <ValidationProvider class="register-input" name="名" rules="required" v-slot="{ errors }">
          <input
            class="register-input-area"
            type="text"
            placeholder="例）太郎"
            v-model="firstName"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- メールアドレス -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">メールアドレス</span>
        </label>
        <ValidationProvider
          class="register-input"
          name="メールアドレス"
          rules="required|email"
          v-slot="{ errors }"
        >
          <input
            class="register-input-area"
            type="email"
            placeholder="例）taro@example.com"
            v-model="email"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- パスワード -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">パスワード</span>
        </label>
        <ValidationProvider
          class="register-input"
          name="パスワード"
          rules="required|min:8|alpha_dash"
          v-slot="{ errors }"
        >
          <input
            class="register-input-area"
            type="password"
            placeholder="例）taroTanaka"
            v-model="password"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- 新規追加 -->
        <button class="register-button" type="submit" :disabled="invalid">新規登録</button>
      </form>
    </ValidationObserver>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@nuxtjs/composition-api';

export default defineComponent({
  setup() {
    // data
    const lastName = ref<string>('');
    const firstName = ref<string>('');
    const email = ref<string>('');
    const password = ref<string>('');
    const errors = ref<string>('');

    return {
      // data
      lastName,
      firstName,
      email,
      password,
      errors,
    };
  },
});
</script>
```

#### `computed`
こちらはHTMLの部分が何も変わらないのでスクリプト部分のみ記載します。
ポイントは2つです。
**computedは、@nuxtjs/composition-api内で読み込みが必要**
**refはvalueで値を取得するため、`lastName.value`, `firstName.value`となる**

```vue:frontend/pages/register/index.vue
<script lang="ts">
import { defineComponent, ref, computed } from '@nuxtjs/composition-api';

export default defineComponent({
  setup(props, context) {
    // data
    const lastName = ref<string>('');
    const firstName = ref<string>('');

    // computed
    const name = computed(() => {
      return `${lastName.value} ${firstName.value}`;
    });

    return {
      // data
      lastName,
      firstName,
      // computed
      name,
    };
  },
});
</script>
```

#### `methods`
こちらは重要なポイントがオンパレードです。
なので順に説明していきます。

```vue:frontend/pages/register/index.vue
<template>
  <div class="register-container">
    <ValidationObserver v-slot="{ invalid }">
      <form class="register-wrapper" @submit.prevent="registerUser()">
        <h2 class="register-title">新規登録</h2>
        <div v-if="errors" class="register-alert-red">
          <p v-if="errors.name">{{ errors.name[0] }}</p>
          <p v-if="errors.email">{{ errors.email[0] }}</p>
          <p v-if="errors.password">{{ errors.password[0] }}</p>
        </div>
        <!-- 姓 -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">姓</span>
        </label>
        <ValidationProvider class="register-input" name="姓" rules="required" v-slot="{ errors }">
          <input
            class="register-input-area"
            type="text"
            placeholder="例）田中"
            v-model="lastName"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- 名 -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">名</span>
        </label>
        <ValidationProvider class="register-input" name="名" rules="required" v-slot="{ errors }">
          <input
            class="register-input-area"
            type="text"
            placeholder="例）太郎"
            v-model="firstName"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- メールアドレス -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">メールアドレス</span>
        </label>
        <ValidationProvider
          class="register-input"
          name="メールアドレス"
          rules="required|email"
          v-slot="{ errors }"
        >
          <input
            class="register-input-area"
            type="email"
            placeholder="例）taro@example.com"
            v-model="email"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- パスワード -->
        <label class="register-label">
          <span class="register-label-check">必須</span>
          <span class="register-label-name">パスワード</span>
        </label>
        <ValidationProvider
          class="register-input"
          name="パスワード"
          rules="required|min:8|alpha_dash"
          v-slot="{ errors }"
        >
          <input
            class="register-input-area"
            type="password"
            placeholder="例）taroTanaka"
            v-model="password"
          />
          <p class="register-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- 新規追加 -->
        <button class="register-button" type="submit" :disabled="invalid">新規登録</button>
      </form>
    </ValidationObserver>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed, useContext } from '@nuxtjs/composition-api';

export default defineComponent({
  setup(props, context) {
    // axios
    const { $axios } = useContext();
    // router
    const router = context.root.$router;
    // data
    const lastName = ref<string>('');
    const firstName = ref<string>('');
    const email = ref<string>('');
    const password = ref<string>('');
    const errors = ref<string>('');

    // computed
    const name = computed(() => {
      return `${lastName.value} ${firstName.value}`;
    });

    // methods
    const registerUser = async () => {
      try {
        // 新規登録
        await $axios.post('/users/register', {
          name: name.value,
          email: email.value,
          password: password.value,
        });

        // ログイン
        const res = await $axios.post('/users/login', {
          email: email.value,
          password: password.value,
        });

        // token
        await $axios.setToken(res.data.token, 'Bearer');

        router.push('/');
      } catch (err) {
        errors.value = err.response.data.errors;
      }
    };

    return {
      // data
      lastName,
      firstName,
      email,
      password,
      errors,
      // computed
      name,
      // methods
      registerUser,
    };
  },
});
</script>
```

##### ①axiosの使い方
```vue:frontend/pages/register/index.vue
// axios
const { $axios } = useContext();
```

https://composition-api.nuxtjs.org/helpers/useContext

##### ②routerの使い方
```vue:frontend/pages/register/index.vue
// router
const router = context.root.$router;
```

##### ③バックエンドからのエラーメッセージの取得方法
`err.response.data.errors`で取得できます。

```vue:frontend/pages/register/index.vue
<script lang="ts">
export default defineComponent({
  setup(props, context) {
    const registerUser = async () => {
      try {} 
      catch (err) {
        errors.value = err.response.data.errors;
      }
    };
  },
});
</script>
```

では次にバックエンド側を作っていきましょう！

### まずはLaravel Passportを導入しよう
LaravelにはデフォルトでトークンベースのシンプルなAPI認証の機能が備わっています。
しかし公式マニュアルにある通り、Laravel Passportを使用することを強く推奨されています。
なので今回も準拠した形で作っていきましょう。

https://laravel.com/docs/6.x/passport

#### インストール方法
##### ①パッケージをインストール
```terminal:ターミナル
$ docker-compose exec app composer require laravel/passport
```

##### ②DBをマイグレーションしてキーを発行する
```terminal:ターミナル
$ docker-compose exec app php artisan migrate
$ docker-compose exec app php artisan passport:install
```

##### ③Models/User.phpを編集
```php:api/app/Models/User.php
<?php
namespace App\Models;

use Laravel\Passport\HasApiTokens; // 追記

class User extends Authenticatable
{
  use HasApiTokens, Notifiable; // 追記
}
```

##### ④Providers/AuthServiceProvider.phpを編集
```php:api/app/Providers/AuthServiceProvider.php
<?php
namespace App\Providers;

use Laravel\Passport\Passport; // 追記

class AuthServiceProvider extends ServiceProvider
{
  public function boot()
  {
    $this->registerPolicies();
    Passport::routes(); // 追記
  }
}
```

##### ⑤config/auth.phpを編集
```php:api/config/auth.php
<?php
return [
  'guards' => [
    'web' => [
      'driver' => 'session',
      'provider' => 'users',
    ],

    'api' => [
      'driver' => 'passport',
      'provider' => 'users',
      'hash' => false,
    ],
  ],
}
```

これで使えるようになりました。

### Authを使うのでCORSを修正しよう
header情報にtokenを設置するのでCORSの修正が必要になります。

```php:api/app/Http/Middleware/Cors.php
<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{
  public function handle($request, Closure $next)
  {
    return $next($request)
      ->header('Access-Control-Allow-Origin', 'http://localhost:3000')
      ->header('Access-Control-Allow-Methods', 'GET, POST')
      ->header('Access-Control-Allow-Headers', 'Content-Type, X-XSRF-TOKEN, Authorization')
      ->header('Access-Control-Allow-Credentials', 'true');
  }
}
```

### バリデーションを作成する
```terminal:ターミナル
$ docker-compose exec app php artisan make:request StoreRegister
```

```php:api/app/Http/Requests/StoreBlogPost.php
// ①trueに変更（これをしないとうまく作用しない）
public function authorize() {
    return true;
}

public function rules()
  {
    return [
      "name" => "required|string",
      "email" => "required|email|unique:users|max:255",
      "password" => "required|min:8"
    ];
  }
```

### コントローラーを作成する
```terminal:ターミナル
$ docker-compose exec app php artisan make:controller LoginController
```

```php:api/app/Http/Controllers/LoginController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

// Models
use App\Models\User;

// Hash
use Illuminate\Support\Facades\Hash;

// Auth
use Illuminate\Support\Facades\Auth;

// Validation
use App\Http\Requests\StoreRegister;

class LoginController extends Controller
{
  // 新規登録
  public function register(StoreRegister $request)
  {
    // instance
    $user = new User;

    // value_save
    $user->fill(array_merge($request->all(), ["password" => Hash::make($request->password)]))->save();
  }

  // ログイン
  public function login(Request $request)
  {
    // validation
    $login = $request->validate([
      "email" => "required|email",
      "password" => "required|string|min:8"
    ]);

    // validationエラー
    if (!Auth::attempt($login)) {
      return response(["message" => "メールアドレスもしくはパスワードが間違っています"]);
    };

    // token発行
    $token = Auth::user()->createToken('authToken')->accessToken;

    // tokenとユーザー情報を受け取る
    return response(["user" => Auth::user(), "token" => $token]);
  }
}
```

### ルーティング
```php:api/routes/web.php
Route::middleware(['cors'])->group(function () {
  Route::options('accounts', function () {
    return response()->json();
  });

  Route::post("/users/register", "LoginController@register");
  Route::post("/users/login", "LoginController@login");
});
```

これで新規登録画面が作成できました。
それでは次にログイン画面を作っていきましょう。
バックエンドの処理に関してはすべて記載したので、フロントエンド部分のみ作ります。

### ログイン画面
```vue:frontend/pages/login/index.vue
<template>
  <div class="login-container">
    <ValidationObserver v-slot="{ invalid }">
      <form class="login-wrapper" @submit.prevent="loginUser()">
        <h2 class="login-title">ログイン</h2>
        <div v-if="errors" class="login-alert-red">
          <p>{{ errors }}</p>
        </div>
        <!-- メールアドレス -->
        <label class="login-label">
          <span class="login-label-name">メールアドレス</span>
        </label>
        <ValidationProvider
          class="login-input"
          name="メールアドレス"
          rules="required|email"
          v-slot="{ errors }"
        >
          <input
            class="login-input-area"
            type="email"
            placeholder="例）taro@example.com"
            v-model="email"
          />
          <p class="login-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- パスワード -->
        <label class="login-label">
          <span class="login-label-name">パスワード</span>
        </label>
        <ValidationProvider
          class="login-input"
          name="パスワード"
          rules="required|min:8|alpha_dash"
          v-slot="{ errors }"
        >
          <input
            class="login-input-area"
            type="password"
            placeholder="例）taroTanaka"
            v-model="password"
          />
          <p class="login-input-error">{{ errors[0] }}</p>
        </ValidationProvider>
        <!-- 新規追加 -->
        <button class="login-button" type="submit" :disabled="invalid">ログイン</button>
      </form>
    </ValidationObserver>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, useContext } from '@nuxtjs/composition-api';

export default defineComponent({
  setup(props, context) {
    // axios
    const { $axios } = useContext();
    // router
    const router = context.root.$router;
    // data
    const email = ref<string>('');
    const password = ref<string>('');
    const errors = ref<string>('');

    // methods
    const loginUser = async () => {
      try {
        const res = await $axios.post('/users/login', {
          email: email.value,
          password: password.value,
        });
        // error_message
        errors.value = res.data.message;
        // errorがない時TOPページへ移動する
        if (!errors.value) {
          router.push('/');
        }
        // token
        await $axios.setToken(res.data.token, 'Bearer');
      } catch (err) {
        errors.value = err.response.data.errors;
      }
    };

    return {
      // data
      email,
      password,
      errors,
      // methods
      loginUser,
    };
  },
});
</script>

<style lang="scss" scoped>
.login {
  &-container {
    position: relative;
    height: 100vh;
    width: 100%;
    background-color: #eeeeee;
  }

  &-wrapper {
    position: absolute;
    top: 50%;
    right: 50%;
    transform: translateX(50%) translateY(-50%);
    max-width: 400px;
    width: 100%;
    background-color: #fff;
    border-radius: 10px;
    padding: 50px 50px;
  }

  &-title {
    text-align: center;
    padding-bottom: 20px;
  }

  &-alert-red {
    padding: 5px 10px;
    background: #ffebee;
    margin-bottom: 10px;
    font-size: 12px;
  }

  &-label {
    &-name {
      font-size: 0.8rem;
    }
  }

  &-input {
    padding-top: 10px;
    padding-bottom: 15px;
    display: block;

    &-area {
      box-sizing: border-box;
      width: 100%;
      padding: 10px;
      font-size: 14px;
      color: #303030;
      border: solid 1px #eee;
      border-radius: 6px;
      outline: 0;
      transition: 0.3s;
    }

    &-error {
      padding-top: 5px;
      font-size: 12px;
      color: #ee4056;
      font-weight: 600;
    }
  }

  &-button {
    margin-top: 20px;
    width: 100%;
    height: 40px;
    color: #fff;
    background: #40c7c1;
    text-decoration: none;
    border: none;
    outline: none;
    border-radius: 8px;
    cursor: pointer;
    appearance: none;
    transition: 0.2s;

    &:hover {
      background: #78cec8;
    }
  }
}
</style>
```

## 終わりに
特に難しいと思うポイントも少なく実装できるかと思います。
この後は掲示板サイトを作ってみても、何かしらのアプリケーションを作ってみてもいいと思います。
こちらをスタートに良質なポートフォリオをたくさん作っていってくださいね。

何かわからないところや間違っているところがありましたらご指摘よろしくお願いいたします。