【ハンズオン】Nuxt.jsとExpressで作る予約サイトを作ろう

## はじめに
今回はDockerとNuxt.jsとTypeScriptとExpressとPrismaを使って予約サイトを作成していきます。
こんな感じのアプリです。

![スクリーンショット 2021-09-21 9.07.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/3ab14811-8345-f5ec-ba0c-f70f9a4a476a.png)

## アプリの詳細
<ul>
<li>GoogleCalendarの予定を見て、空いている時間に予約ができる</li>
<li>予約されたらDBに保存され、Googleカレンダーにも保存される</li>
</ul>

![スクリーンショット 2021-09-21 9.07.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/5012cf4a-da0d-7e5d-d8cf-9bc6e2b78132.png)

![スクリーンショット 2021-09-21 9.08.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/d54bd10a-4906-9e16-e989-793c2727daf7.png)


## 対象読者
<ul>
<li>Dockerを使ったことがない方</li>
<li>Nuxt.jsとExpressで何か作ってみたい方</li>
<li>TypeScriptを使ったことがない方</li>
<li>TypeScriptのORMであるPrismaを使ってみたい方</li>
</ul>

## 完成コード

https://github.com/ssk9597/Nuxt-TypeScript-Express-Prisma-Reservation

## それでは一緒に作っていきましょう！
### Githubからクローン
Dockerから解説となると長くなるので省略します。

https://github.com/ssk9597/Docker-Nuxt-TypeScript-Express-Prisma

上記のファイルで環境構築が可能です。

以下の記事で具体的なやり方が書いてありますのでこちらを参考にしてください。

https://qiita.com/Ryo9597/items/875a1604029e1af9ea16

### `api/prisma/schema.prisma`を修正する

今回作るテーブルは、`User`と`Reservation`の2つです。
User:Reservation = 1:Nの関係になります。

![スクリーンショット 2021-09-21 9.20.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/f96c7a87-bb1c-2f62-b295-c689c2873910.png)

```diff_html:api/prisma/schema.prisma
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

+ model User {
+   id          Int           @id @default(autoincrement())
+   firstName   String
+   lastName    String
+   email       String        @unique
+   reservation Reservation[]
+ }

+ model Reservation {
+   id     Int    @id @default(autoincrement())
+   date   String
+   time   String
+   user   User?  @relation(fields: [userId], references: [id])
+   userId Int?
+ }
```

それでは、バックエンドから作成していきます。
まずはフォルダ構成に関してです。

```
. Nuxt-TypeScript-Express-Prisma-Reservation
├── src/
│   ├── controllers ・・・ビジネスロジックとフロントエンドを繋げる
│   │        └── ReservationController.ts
│   │        └── UserController.ts
│   ├── credentials ・・・GoogleCalendarを使うための認証ファイル
│   │        └── credentials.json
│   ├── helpers ・・・ビジネスロジック
│   │        └── google
│   │        │        └── insertEvent.ts
│   │        │        └── jwtClient.ts
│   │        │        └── listEvent.ts
│   │        │        └── types
|   │        │        │        └── listEvent.type.ts
│   ├── models ・・・ビジネスロジックとDB操作をまとめる
│   │        └── Reservations.ts
│   │        └── Users.ts
│   ├── repositories ・・・DB操作
│   │        └── reservation.repository.ts
│   │        └── user.repository.ts
│   ├── app.ts
│   ├── index.ts
```

### `moment`をインストール
JavaScriptの日付操作用ライブラリである「Moment.js」を利用します。

```terminal:ターミナル
$ docker-compose exec app npm install moment --save
```

### `GoogleCalendar`を使うための準備
`GoogleCalendar`のAPIを使用するためにGCPのコンソールに入りましょう。

https://cloud.google.com/

では、プロジェクトを作成し、`GoogleCalendar API`を使えるようにしましょう。

![スクリーンショット 2021-09-21 11.09.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/89987125-01b6-0335-a8ea-72ea8c6ce2b9.png)

では次にサービスアカウントを作成しましょう。

![スクリーンショット 2021-09-21 11.11.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/1a142e8a-4896-7ee4-d1cc-8f11de50fcf3.png)

サービスアカウントが作成されたら、クレデンシャルファイルを作成しましょう。

![スクリーンショット 2021-09-21 11.12.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/d6fd3a0d-4886-b30d-d2f7-7c2f0380cb81.png)

これで完了です。

コード内でAPIが使えるようにするために、パッケージをインストールしましょう。

```terminal:ターミナル
$ docker-compose exec app npm install googleapis@39 --save
```

最後にGoogleCalendarのカレンダーIDを`.env`に入力しましょう。

![スクリーンショット 2021-09-21 11.17.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/9fe2dd2d-1f34-f8e1-1aac-be308d89a677.png)

```html:.env
CALENDAR_ID = "XXXXXXXXX@gmail.com"
```

これで準備は完了です。
では、ビジネスロジックの`helpers`から作成していきましょう。

### 【helpers】jwtClient
JWTクライアントをサービスアカウントで認証させるロジックです。

こちらは、`Firebase`の記事になりますが一番わかりやすいためこちらの記事を参考として載せておきます。

https://firebase.google.com/docs/database/rest/auth?hl=ja#generate_an_access_token

```typescript:api/src/helpers/google/jwtClient.ts
// google
import { google, Auth } from 'googleapis';

// credential
import credentials from '../.././credentials/credentials.json';

export const jwtClient = (scopes: string[]) => {
  const jwtClient: Auth.JWT = new google.auth.JWT({
    email: credentials.client_email,
    key: credentials.private_key,
    keyFile: undefined,
    keyId: undefined,
    scopes: scopes,
    subject: undefined,
  });

  return jwtClient;
};
```

### 【helpers】insertEvent
イベントをカレンダーに追加するロジックです。

パッケージのドキュメントはこちらです。

https://googleapis.dev/nodejs/googleapis/latest/calendar/classes/Resource$Events.html#insert

公式サイトのドキュメントはこちらです。

https://developers.google.com/calendar/api/guides/create-events#node.js

https://developers.google.com/calendar/api/v3/reference/events/insert


パッケージのドキュメントに関しては、認証部分がごそっと抜けている。
GCPで、サービスアカウントを作成してキーを発行してクレデンシャル情報があるので、JWTで読み込む必要があります。
なので公式サイトを参考に作成していきます。

また、公式サイトの`resource`はタイプの不一致のエラーが発生します。
こちらはGithubのissueに上がっています。
`resource`を`requestBody`に修正することを忘れないでください。

https://github.com/googleapis/google-api-nodejs-client/issues/1629


```typescript:api/src/helpers/google/insertEvent.ts
// dotenv
import dotenv from 'dotenv';
dotenv.config();

// google
import { google, Auth, calendar_v3 } from 'googleapis';

export const insertEvent = async (
  firstName: string,
  lastName: string,
  email: string,
  date: string,
  time: string,
  jwt: Auth.JWT
) => {
  // カレンダー
  const calendar: calendar_v3.Calendar = google.calendar('v3');

  // イベント情報
  const event = {
    summary: `${firstName} ${lastName}のご予約`,
    description: `【お名前】 ${firstName} ${lastName}, \n【メールアドレス】 ${email}`,
    start: {
      dateTime: `${date}T${time}:00+09:00`,
      timeZone: 'Asia/Tokyo',
    },
    end: {
      dateTime: `${date}T${time}:00+09:00`,
      timeZone: 'Asia/Tokyo',
    },
  };

  // JWTの認証を行い問題なければイベントを追加する
  await jwt.authorize((err) => {
    if (err) {
      console.log(err);
    } else {
      calendar.events.insert({
        auth: jwt,
        calendarId: process.env.CALENDAR_ID,
        requestBody: event,
      });
    }
  });
};
```

### 【helpers】insertEvent
イベントのリストを表示するロジックです。

公式サイトのドキュメントはこちらです。

https://developers.google.com/calendar/api/v3/reference/events/list

https://developers.google.com/calendar/api/quickstart/nodejs

こちらのファイルはやっていることが多いので文章でも説明します。

<ul>
<li>GoogleCalendarのイベントを取り出す</li>
<li>そのイベントから何時に予定ができないかを表示する</li>
</ul>

![スクリーンショット 2021-09-21 11.52.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/db5fb659-0840-7df5-a2a4-61037f0d1810.png)

ではコードを書いていきましょう。

```typescript:api/src/helpers/google/listEvent.ts
// dotenv
import dotenv from 'dotenv';
dotenv.config();

// google
import { google, Auth, calendar_v3 } from 'googleapis';

// moment
import moment from 'moment';

// types
import { Events } from './types/listEvent.type';
import { start } from 'repl';

export const listEvent = async (date: string, jwt: Auth.JWT) => {
  // カレンダー
  const calendar: calendar_v3.Calendar = google.calendar('v3');

  // JWTの認証を行ってイベントのリストを表示する
  const calendars = await calendar.events.list({
    auth: jwt,
    calendarId: process.env.CALENDAR_ID,
    timeMax: `${date}T23:59:59.999Z`,
    timeMin: `${date}T00:00:00.000Z`,
    timeZone: 'Asia/Tokyo',
  });

  const results: calendar_v3.Schema$Event[] | undefined = calendars?.data.items;

  const events: Events = [];

  results?.forEach((result: calendar_v3.Schema$Event | undefined) => {
    const startDateTime = moment(result?.start?.dateTime);
    const endDateTime = moment(result?.end?.dateTime);

    const startDateTimeOClock = startDateTime.format('mm');
    const endDateTimeOClock = endDateTime.format('mm');

    // 予約の開始と終了の分が0の場合
    if (startDateTimeOClock === '00' && endDateTimeOClock === '00') {
      // 予約された時間が1時間以下とそれ以外で条件分岐を行う
      const differenceTime = endDateTime.diff(startDateTime, 'hours');

      // 1時間未満
      if (differenceTime <= 1) {
        events.push({
          date: result?.start?.dateTime?.substr(0, 10),
          time: result?.start?.dateTime?.substr(11, 5),
        });
      } else {
        // 1時間以上
        for (let i = 0; i < differenceTime; i++) {
          const START_DATE_TIME = startDateTime.add(i, 'hours').format();

          events.push({
            date: START_DATE_TIME.substr(0, 10),
            time: START_DATE_TIME.substr(11, 5),
          });
        }
      }
    } else {
      // 予約の開始と終了の分が0以外の場合
      const START_DATE_TIME = startDateTime.subtract(startDateTimeOClock, 'minute');
      let END_DATE_TIME;
      if (endDateTimeOClock === '00') {
        END_DATE_TIME = endDateTime;
      } else {
        END_DATE_TIME = endDateTime.add(60 - Number(endDateTimeOClock), 'minute');
      }

      // 予約された時間が1時間以下とそれ以外で条件分岐を行う
      const differenceTime = END_DATE_TIME.diff(START_DATE_TIME, 'hours');

      // 1時間未満
      if (differenceTime <= 1) {
        events.push({
          date: START_DATE_TIME.format('YYYY-MM-DD'),
          time: START_DATE_TIME.format('HH:mm'),
        });
      } else {
        // 1時間以上
        for (let i = 0; i < differenceTime; i++) {
          const ADD_START_DATE_TIME = START_DATE_TIME.add(i, 'hours').format();

          events.push({
            date: ADD_START_DATE_TIME.substr(0, 10),
            time: ADD_START_DATE_TIME.substr(11, 5),
          });
        }
      }
    }
  });

  return events;
};
```

`TypeScript`の型定義ファイルを用意します。

```typescript:api/src/helpers/google/types/listEvent.type.ts
export type Events = {
  date: string | undefined;
  time: string | undefined;
}[];
```

これでビジネスロジックの`helpers`の作成が完了しました。
次に、DB操作を行う`repositories`の作成をしていきましょう。

### 【repositories】user.repository.ts
#### findUser
「メールアドレスが一致するユーザーを1人取り出す」DB操作ロジックを作成します。

https://www.prisma.io/docs/concepts/components/prisma-client/crud#read

```typescript:api/src/repositories/user.repository.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

const userRepository = {
  // メールアドレスが一致するユーザーを 1 人取り出す
  async findUser(email: string) {
    const user = await prisma.user.findUnique({
      where: {
        email: email,
      },
    });

    return user;
  },
};

export default userRepository;
```

#### registerUserReservation
「メールアドレスが一致したユーザーがいない場合、ユーザー情報と予約情報の登録を行う」DB操作ロジックを作成します。

https://www.prisma.io/docs/concepts/components/prisma-client/crud#create

```diff_typescript:api/src/repositories/user.repository.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

const userRepository = {
  // メールアドレスが一致するユーザーを 1 人取り出す
  async findUser(email: string) {
    const user = await prisma.user.findUnique({
      where: {
        email: email,
      },
    });

    return user;
  },

+ // メールアドレスが一致したユーザーがいない場合、ユーザー情報と予約情報の登録を行う
+ async registerUserReservation(
+   firstName: string,
+   lastName: string,
+   email: string,
+   date: string,
+   time: string
+ ) {
+   await prisma.user.create({
+     data: {
+      firstName: firstName,
+       lastName: lastName,
+       email: email,
+       reservation: {
+         create: {
+           date: date,
+           time: time,
+         },
+       },
+     },
+   });
+ },
};

export default userRepository;
```

### 【repositories】reservation.repository.ts
#### registerReservation
「メールアドレスが一致したユーザーがいた場合、予約情報のみ登録を行う」DB操作ロジックを作成します。

```typescript:api/src/repositories/reservation.repository.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

const reservationRepository = {
  // メールアドレスが一致したユーザーがいた場合、予約情報のみ登録を行う
  async registerReservation(date: string, time: string, userId: number) {
    await prisma.reservation.create({
      data: {
        date: date,
        time: time,
        userId: userId,
      },
    });
  },
};

export default reservationRepository;
```

これでDB操作を行う`repositories`の作成が完了しました。
次に、ビジネスロジックとDB操作をまとめる`models`の作成をしていきましょう。

### 【models】Users.ts
```typescript:api/src/models/Users.ts
// repositories
import userRepository from '../repositories/user.repository';
import reservationRepository from '../repositories/reservation.repository';

// helpers
import { jwtClient } from '../helpers/google/jwtClient';
import { insertEvent } from '../helpers/google/insertEvent';

const users = {
  async store(firstName: string, lastName: string, email: string, date: string, time: string) {
    try {
      // メールアドレスが一致するユーザーを取り出す
      const user = await userRepository.findUser(email);

      if (user) {
        // 予約情報を登録する
        await reservationRepository.registerReservation(date, time, user.id);
      } else {
        // ユーザーと予約情報を登録する
        await userRepository.registerUserReservation(firstName, lastName, email, date, time);
      }

      // 必要なスコープの定義
      const scopes = [
        'https://www.googleapis.com/auth/calendar',
        'https://www.googleapis.com/auth/calendar.events',
      ];

      // JWTクライアントをサービスアカウントで認証する
      const jwt = await jwtClient(scopes);

      // カレンダーに登録を行う
      await insertEvent(firstName, lastName, email, date, time, jwt);
    } catch (err) {
      console.log(err);
    }
  },
};

export default users;
```

### 【models】Reservations.ts
```typescript:api/src/models/Reservations.ts
// helpers
import { jwtClient } from '../helpers/google/jwtClient';
import { listEvent } from '../helpers/google/listEvent';

const reservations = {
  async index(date: string) {
    // 必要なスコープの定義
    const scopes = [
      'https://www.googleapis.com/auth/calendar.readonly',
      'https://www.googleapis.com/auth/calendar',
      'https://www.googleapis.com/auth/calendar.events.readonly',
      'https://www.googleapis.com/auth/calendar.events',
    ];

    // JWTクライアントをサービスアカウントで認証する
    const jwt = await jwtClient(scopes);

    // イベントのリストを表示する
    const events = await listEvent(date, jwt);

    return events;
  },
};

export default reservations;
```

これでビジネスロジックとDB操作をまとめる`models`の作成が完了しました。
次に、ビジネスロジックとフロントエンドを繋げる`controllers`の作成をしていきましょう。

### 【controllers】UserController.ts
```typescript:api/src/controllers/UserController.ts
import { Router, Request, Response } from 'express';
const router = Router();

// models
import users from '../models/users';

// POST /api/users/store
router.post('/store', async (req: Request, res: Response) => {
  const firstName: string = req.body.firstName;
  const lastName: string = req.body.lastName;
  const email: string = req.body.email;
  const date: string = req.body.date;
  const time: string = req.body.time;

  await users.store(firstName, lastName, email, date, time);
});

export default router;
```

### 【controllers】ReservationController.ts
```typescript:api/src/controllers/ReservationController.ts
import { Router, Request, Response } from 'express';
const router = Router();

// models
import reservations from '../models/reservations';

// GET /api/reservations/:date
router.get('/:date', async (req: Request, res: Response) => {
  const date = req.params.date;

  const events = await reservations.index(date);
  res.json(events);
});

export default router;
```

これでバックエンドの作成が完了しました。
次に、フロントエンドの作成をしていきましょう。

### `css-wipe`のインストール
リセットCSSを採用します。

https://github.com/stackcss/css-wipe

```terminal:ターミナル
$ docker-compose exec front npm install css-wipe --save
```

これで、リセットCSSが読み込めます。

```vue:frontend/layouts/default.vue
<template>
  <Nuxt />
</template>

<style lang="scss">
@import 'css-wipe';
</style>
```

### `FontAwesome`のインストール
アイコンを使うために`FontAwesome`をインストールします。

```terminal:ターミナル
$ docker-compose exec front npm install @fortawesome/vue-fontawesome @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons --save
```

`nuxt.config.js`のファイルに追記します。

```diff_javascript:frontend/nuxt.config.js
require('dotenv').config();
const { API_URL, API_URL_BROWSER } = process.env;

export default {
  head: {
    title: 'frontend',
    htmlAttrs: {
      lang: 'ja',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },

+ css: ['@fortawesome/fontawesome-svg-core/styles.css'],

+ plugins: ['@/plugins/fontawesome'],

  components: true,

  buildModules: ['@nuxt/typescript-build', '@nuxtjs/composition-api/module'],

  generate: {
    interval: 2000,
  },

  watchers: {
    webpack: {
      poll: true,
    },
  },

  modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv'],

  env: {
    API_URL,
    API_URL_BROWSER,
  },

  proxy: {
    '/api': process.env.API_URL,
  },

  axios: {
    baseURL: process.env.API_URL,
    browserBaseURL: process.env.API_URL_BROWSER,
  },

  build: {},
};
```

次にプラグインファイルを追記する。

```diff_javascript:frontend/plugins/fontawesome.js
import Vue from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { config, library } from '@fortawesome/fontawesome-svg-core';
// solid Style
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';

// nuxt.config.jsでCSSファイルを追加
config.autoAddCss = false;

// 利用するアイコンを配列に追加
const solidIcons = [faChevronLeft, faChevronRight];

// 利用するアイコンをlibraryに追加
solidIcons.forEach(icon => library.add(icon));

// グローバルコンポーネントに登録
Vue.component('fa', FontAwesomeIcon);
```

### `moment`をインストール
JavaScriptの日付操作用ライブラリである「Moment.js」を利用します。

```terminal:ターミナル
$ docker-compose exec front npm install moment --save
```

### 【コンポーネント作成】Atoms/Heading
見出しを作成します。

```vue:frontend/components/Atoms/Heading.vue
<template>
  <div class="heading-container">
    <h2 class="heading-content">{{ content }}</h2>
  </div>
</template>

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

export default defineComponent({
  props: {
    content: {
      type: String,
      required: true,
    },
  },
});
</script>

<style lang="scss" scoped>
.heading {
  &-container {
    padding-top: 50px;
    padding-bottom: 50px;
  }
  &-content {
    margin-left: 30px;
    padding-left: 0.5em;
    font-size: 1.2rem;
    color: #333;
    border-left: 4px solid #89535a;
  }
}
</style>
```

### 【コンポーネント作成】Atoms/FormLabel
フォームのラベルを作成します。

```vue:frontend/components/Atoms/FormLabel.vue
<template>
  <div class="form-label">
    <div class="form-label-ttl">{{ title }}</div>
    <div class="form-label-op" :style="styles">{{ option }}</div>
  </div>
</template>

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

// type
import { Option, Props } from './types/FormLabel.type';

export default defineComponent({
  props: {
    title: {
      type: String,
      required: true,
    },
    option: {
      type: String as PropType<Option>,
      default: '必須',
    },
  },
  setup(props: Props) {
    const styles = computed(() => {
      if (props.option === '必須') {
        return {
          backgroundColor: '#7E954E',
          color: '#fff',
        };
      } else {
        return {
          backgroundColor: '#757171',
          color: '#e0e0e0',
        };
      }
    });

    return {
      styles,
    };
  },
});
</script>

<style lang="scss" scoped>
.form-label {
  margin-top: 10px;
  display: flex;
  align-items: center;

  &-ttl {
    font-size: 0.8rem;
    font-weight: bold;
    color: #333;
    margin-left: 10px;
  }

  &-op {
    font-size: 0.2rem;
    margin-left: 8px;
    padding: 2px 8px;
    border-radius: 6px;
  }
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Atoms/types/FormLabel.type.ts
export type Option = '必須' | '任意';

export type Props = {
  title: string;
  option: Option;
};
```

### 【コンポーネント作成】Atoms/FormInput
フォームの入力欄を作成します。

```vue:frontend/components/Atoms/FormInput.vue
<template>
  <div>
    <input
      class="form-input"
      :type="type"
      :placeholder="placeholder"
      :value="value"
      @input="$emit('input', $event.target.value)"
    />
  </div>
</template>

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

// type
import { Type } from './types/FormInput.type';

export default defineComponent({
  props: {
    type: {
      type: String as PropType<Type>,
      default: 'text',
    },
    placeholder: {
      type: String,
      required: true,
    },
    value: {
      type: String,
      default: '',
      required: true,
    },
  },
});
</script>

<style lang="scss" scoped>
.form-input {
  display: block;
  width: 100%;
  padding: 10px 10px;
  margin: 10px 0;
  color: #333;
  border: solid 1px #ccc;
  border-radius: 6px;
  outline: 0;
  transition: 0.3s;

  &:focus {
    border: 1px solid #7e954e;
  }
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Atoms/types/FormInput.type.ts
export type Type = 'text' | 'email' | 'password';
```

### 【コンポーネント作成】Atoms/FormButton
フォームのボタンを作成します。

```vue:frontend/components/Atoms/FormButton.vue
<template>
  <div class="form-button">
    <button class="button" @click="childClick">予約する</button>
  </div>
</template>

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

// type
import { Click, Props } from './types/FormButton.type';

export default defineComponent({
  props: {
    click: {
      type: Function as PropType<Click>,
      required: true,
    },
    invalid: {
      type: Boolean,
    },
  },
  setup(props: Props) {
    const childClick = () => {
      props.click();
    };

    return {
      childClick,
    };
  },
});
</script>

<style lang="scss" scoped>
.form-button {
  padding-top: 30px;
  display: flex;
  justify-content: center;
}

.button {
  cursor: pointer;
  width: 200px;
  display: inline-block;
  padding: 0.7rem 0;
  font-weight: bold;
  color: #fff;
  background: #89535a;
  text-decoration: none;
  user-select: none;
  border: 1px #89535a solid;
  border-radius: 20px;
  transition: 0.4s ease;

  &:hover {
    color: #89535a;
    background: #fff;
  }
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Atoms/types/FormButton.type.ts
export type Click = {
  (): void;
};

export type Props = {
  click: Click;
  invalid: boolean;
};
```

### 【コンポーネント作成】Atoms/CalendarHeader
カレンダーのヘッダーを作成します。

```vue:frontend/components/Atoms/CalendarHeader.vue
<template>
  <div class="calendar-header">
    <div
      class="calendar-header-left"
      v-if="compareToday.yearMonth < compareCurrentDate.yearMonth"
      @click="grandChildPrevMonth()"
    >
      <span><fa icon="chevron-left" area-hidden="true" /></span>
      <p class="calendar-header-left-text">前月</p>
    </div>
    <div class="calendar-header-left empty" v-else></div>
    <p class="calendar-header-center">{{ getYearMonthToday }}</p>
    <div class="calendar-header-right" @click="grandChildNextMonth()">
      <p class="calendar-header-right-text">翌月</p>
      <span><fa icon="chevron-right" area-hidden="true" /></span>
    </div>
  </div>
</template>

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

// types
import {
  CompareToday,
  CompareCurrentDate,
  ClickChildPrevMonth,
  ClickChildNextMonth,
  Props,
} from './types/CalendarHeader.type';

export default defineComponent({
  props: {
    compareToday: {
      type: Object as PropType<CompareToday>,
    },
    compareCurrentDate: {
      type: Object as PropType<CompareCurrentDate>,
    },
    getYearMonthToday: {
      type: String,
    },
    clickChildPrevMonth: {
      type: Function as PropType<ClickChildPrevMonth>,
    },
    clickChildNextMonth: {
      type: Function as PropType<ClickChildNextMonth>,
    },
  },
  setup(props: Props) {
    const grandChildPrevMonth = () => {
      props.clickChildPrevMonth();
    };
    const grandChildNextMonth = () => {
      props.clickChildNextMonth();
    };

    return {
      grandChildPrevMonth,
      grandChildNextMonth,
    };
  },
});
</script>

<style lang="scss" scoped>
.calendar-header {
  padding: 10px;
  height: 50px;
  width: 100%;
  max-width: 350px;
  background: #89535a;
  color: #fff;
  display: flex;
  justify-content: space-around;
  align-items: center;
  margin: 0 auto;

  &-left {
    display: flex;
    align-items: center;
    cursor: pointer;

    &.empty {
      width: 44px;
    }

    &-text {
      margin-left: 6px;
      font-size: 0.875rem;
    }
  }

  &-center {
    font-weight: bold;
  }

  &-right {
    display: flex;
    align-items: center;
    cursor: pointer;

    &-text {
      margin-right: 6px;
      font-size: 0.875rem;
    }
  }
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Atoms/types/CalendarHeader.type.ts
export type CompareToday = {
  yearMonth: number;
  yearMonthDay: number;
};

export type CompareCurrentDate = {
  yearMonth: number;
  yearMonthDay: number;
};

export type ClickChildPrevMonth = {
  (): void;
};

export type ClickChildNextMonth = {
  (): void;
};

export type Props = {
  compareToday: CompareToday;
  compareCurrentDate: CompareCurrentDate;
  getYearMonthToday: string;
  clickChildPrevMonth: ClickChildPrevMonth;
  clickChildNextMonth: ClickChildNextMonth;
};
```

### 【コンポーネント作成】Atoms/CalendarTable
カレンダーを作成します。

```vue:frontend/components/Atoms/CalendarTable.vue
<template>
  <table class="calendar-table">
    <thead class="calendar-table">
      <tr class="calendar-table-tr">
        <th class="calendar-table-tr-th">日</th>
        <th class="calendar-table-tr-th">月</th>
        <th class="calendar-table-tr-th">火</th>
        <th class="calendar-table-tr-th">水</th>
        <th class="calendar-table-tr-th">木</th>
        <th class="calendar-table-tr-th">金</th>
        <th class="calendar-table-tr-th">土</th>
      </tr>
    </thead>
    <tbody class="calendar-table">
      <tr class="calendar-table-tr" v-for="(week, index) in getCalendar" :key="index">
        <td
          class="calendar-table-tr-td"
          v-for="(day, index) in week"
          :key="index"
          :class="[
            { outside: compareCurrentDate.yearMonth !== day.yearMonth },
            { today: compareToday.yearMonthDay === day.yearMonthDay },
            { notClick: compareToday.yearMonthDay > day.yearMonthDay },
          ]"
          @click="grandChildChooseDate(day.googleCalendarDate)"
        >
          {{ day.day }}
        </td>
      </tr>
    </tbody>
  </table>
</template>

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

// types
import {
  GetCalendar,
  CompareCurrentDate,
  CompareToday,
  ClickChildChooseDate,
  Props,
} from './types/CalendarTable.type';

export default defineComponent({
  props: {
    getCalendar: {
      type: Array as PropType<GetCalendar>,
    },
    compareCurrentDate: {
      type: Object as PropType<CompareCurrentDate>,
    },
    compareToday: {
      type: Object as PropType<CompareToday>,
    },
    clickChildChooseDate: {
      type: Function as PropType<ClickChildChooseDate>,
    },
  },
  setup(props: Props) {
    const grandChildChooseDate = (date) => {
      props.clickChildChooseDate(date);
    };

    return {
      grandChildChooseDate,
    };
  },
});
</script>

<style lang="scss" scoped>
.calendar-table {
  width: 100%;
  max-width: 350px;
  margin: 0 auto;
  border: 1px solid #ddd;
  border-right-width: 0;
  text-align: center;

  &-tr {
    display: flex;
    &:last-child {
      &-th,
      &-td {
        border-bottom-width: 0;
      }
    }

    &-th {
      flex: 1;
      height: 50px;
      font-size: 0.75rem;
      background-color: #efefef;
      border-bottom: 1px solid #ddd;
      border-right: 1px solid #ddd;
      font-weight: bold;
      line-height: 50px;

      &:first-child {
        color: #ff4136;
      }
      &:last-child {
        color: #0074d9;
      }
    }

    &-td {
      flex: 1;
      height: 50px;
      font-size: 0.75rem;
      border-bottom: 1px solid #ddd;
      border-right: 1px solid #ddd;
      font-weight: bold;
      line-height: 50px;
      cursor: pointer;
    }
  }
}

.outside {
  background-color: #ddd;
  color: rgba(0, 1, 17, 0.2);
}

.today {
  background: #89535a;
  border-radius: 50%;
  color: #fff;
  font-weight: bold;
}

.notClick {
  color: rgba(0, 1, 17, 0.05);
  pointer-events: none;
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Atoms/types/CalendarTable.type.ts
export type GetCalendar = {
  day: number;
  date: string;
  month: string;
  yearMonth: number;
  yearMonthDay: number;
  googleCalendarDate: string;
}[];

export type CompareCurrentDate = {
  yearMonth: number;
  yearMonthDay: number;
};

export type CompareToday = {
  yearMonth: number;
  yearMonthDay: number;
};

export type ClickChildChooseDate = {
  (date: string): void;
};

export type Props = {
  getCalendar: GetCalendar;
  compareCurrentDate: CompareCurrentDate;
  compareToday: CompareToday;
  clickChildChooseDate: ClickChildChooseDate;
};
```

### 【コンポーネント作成】Atoms/TimeHeader
タイムピッカーのヘッダーを作成します。

```vue:frontend/components/Atoms/TimeHeader.vue
<template>
  <div class="time-header">
    <div class="time-header-left" @click="grandChildPrevSelectDate()">
      <span class="time-header-left-header-arrow">
        <fa icon="chevron-left" />
      </span>
      <p class="time-header-left-text">日付選択</p>
    </div>
    <p class="time-header-middle">{{ dateFormat }}</p>
    <div class="time-header-right"></div>
  </div>
</template>

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

// types
import { ClickChildPrevSelectDate } from './types/TimeHeader.type';

export default defineComponent({
  props: {
    dateFormat: {
      type: String,
    },
    clickChildPrevSelectDate: {
      type: Function as PropType<ClickChildPrevSelectDate>,
    },
  },
  setup(props) {
    const grandChildPrevSelectDate = () => {
      props.clickChildPrevSelectDate();
    };

    return {
      grandChildPrevSelectDate,
    };
  },
});
</script>

<style lang="scss" scoped>
.time-header {
  padding: 10px;
  height: 50px;
  background: #89535a;
  color: #fff;
  display: flex;
  justify-content: space-between;
  align-items: center;

  &-left {
    display: flex;
    align-items: center;
    cursor: pointer;
    width: 50px;

    &-text {
      margin-left: 6px;
      font-size: 0.875rem;
    }
  }

  &-middle {
    font-weight: bold;
  }

  &-right {
    width: 50px;
  }
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Atoms/types/TimeHeader.type.ts
export type ClickChildPrevSelectDate = {
  (): void;
};
```

### 【コンポーネント作成】Atoms/TimePicker
タイムピッカーを作成します。

```vue:frontend/components/Atoms/TimePicker.vue
<template>
  <div class="time-table-header">
    <table class="time-table">
      <thead class="time-table-thead">
        <tr class="time-table-tr">
          <th class="time-table-th sticky"></th>
          <th class="time-table-th" v-for="(time, index) in timeTable" :key="index">
            {{ time.clock }}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr class="time-table-tr">
          <th class="time-table-th sticky">空き状況</th>
          <td
            class="time-table-td"
            v-for="(time, index) in timeTable"
            :key="index"
            @click="grandChildChooseTime(time.clock)"
          >
            <span v-if="time.isEmpty">○</span>
            <span v-else>×</span>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

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

// types
import { TimeTable, ClickChildChooseTime, Props } from './types/TimePicker.type';

export default defineComponent({
  props: {
    timeTable: {
      type: Array as PropType<TimeTable>,
    },
    clickChildChooseTime: {
      type: Function as PropType<ClickChildChooseTime>,
    },
  },
  setup(props: Props) {
    const grandChildChooseTime = (time) => {
      props.clickChildChooseTime(time);
    };

    return {
      grandChildChooseTime,
    };
  },
});
</script>

<style lang="scss" scoped>
.time-table-header {
  border: 1px solid #ddd;
  border-right-width: 0;
  overflow-x: auto;

  & .time-table {
    width: 100%;
    text-align: center;

    &-tr {
      &:last-child {
        & th,
        & td {
          border-bottom-width: 0;
        }
      }
    }

    &-th {
      min-width: 50px;
      height: 50px;
      font-size: 0.75rem;
      background-color: #efefef;
      border-bottom: 1px solid #ddd;
      border-right: 1px solid #ddd;
      font-weight: bold;
      vertical-align: middle;
      white-space: nowrap;

      &.sticky {
        position: sticky;
        left: 0;
      }
    }

    &-td {
      border-bottom: 1px solid #ddd;
      border-right: 1px solid #ddd;
      vertical-align: middle;
      white-space: nowrap;
    }
  }
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Atoms/types/TimePicker.type.ts
export type TimeTable = {
  clock: string;
  isEmpty: boolean;
}[];

export type ClickChildChooseTime = {
  (time: string): void;
};

export type Props = {
  timeTable: TimeTable;
  clickChildChooseTime: ClickChildChooseTime;
};
```

これで、`Atoms`が完成しました。
次に、`Molecules`の作成をしていきます。

### 【コンポーネント作成】Molecules/Calendar
カレンダーを作成します。

```vue:frontend/components/Molecules/Calendar.vue
<template>
  <div>
    <!-- カレンダーヘッダー -->
    <CalendarHeader
      :compareToday="compareToday"
      :compareCurrentDate="compareCurrentDate"
      :getYearMonthToday="getYearMonthToday"
      :clickChildPrevMonth="childPrevMonth"
      @childPrevMonth="childPrevMonth"
      :clickChildNextMonth="childNextMonth"
      @childNextMonth="childNextMonth"
    />
    <!-- カレンダーテーブル -->
    <CalendarTable
      :getCalendar="getCalendar"
      :compareCurrentDate="compareCurrentDate"
      :compareToday="compareToday"
      :clickChildChooseDate="childChooseDate"
      @childChooseDate="childChooseDate"
    />
  </div>
</template>

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

// components
import CalendarHeader from '../Atoms/CalendarHeader.vue';
import CalendarTable from '../Atoms/CalendarTable.vue';

// types
import { ClickPrevMonth, ClickNextMonth, ClickChooseDate, Props } from './types/Calendar.type';

export default defineComponent({
  components: {
    CalendarHeader,
    CalendarTable,
  },
  props: {
    currentDate: {
      type: Object,
    },
    today: {
      type: Object,
    },
    clickPrevMonth: {
      type: Function as PropType<ClickPrevMonth>,
    },
    clickNextMonth: {
      type: Function as PropType<ClickNextMonth>,
    },
    clickChooseDate: {
      type: Function as PropType<ClickChooseDate>,
    },
  },
  setup(props: Props) {
    // computed
    const getStartDate = computed(() => {
      let date = moment(props.currentDate);
      date.startOf('month');
      const dayOfWeek = date.day();
      return date.subtract(dayOfWeek, 'days');
    });

    const getEndDate = computed(() => {
      let date = moment(props.currentDate);
      date.endOf('month');
      const dayOfWeek = date.day();
      return date.add(6 - dayOfWeek, 'days');
    });

    const getCalendar = computed(() => {
      let startDate = getStartDate.value;
      const endDate = getEndDate.value;
      const weekNumber = Math.ceil(endDate.diff(startDate, 'days') / 7);

      let calendars = [];

      for (let week = 0; week < weekNumber; week++) {
        let weekRow = [];
        for (let day = 0; day < 7; day++) {
          weekRow.push({
            day: startDate.get('date'),
            date: startDate.format('DD'),
            month: startDate.format('MM'),
            yearMonth: Number(startDate.format('YYYYMM')),
            yearMonthDay: Number(startDate.format('YYYYMMDD')),
            googleCalendarDate: String(startDate.format('YYYY-MM-DD')),
          });
          startDate.add(1, 'days');
        }
        calendars.push(weekRow);
      }
      return calendars;
    });

    const getYearMonthToday = computed(() => {
      return moment(props.currentDate).format('YYYY年MM月');
    });

    const compareToday = computed(() => {
      return {
        yearMonth: Number(moment(props.today).format('YYYYMM')),
        yearMonthDay: Number(moment(props.today).format('YYYYMMDD')),
      };
    });

    const compareCurrentDate = computed(() => {
      return {
        yearMonth: Number(moment(props.currentDate).format('YYYYMM')),
        yearMonthDay: Number(moment(props.currentDate).format('YYYYMMDD')),
      };
    });

    // methods
    const childPrevMonth = () => {
      props.clickPrevMonth();
    };

    const childNextMonth = () => {
      props.clickNextMonth();
    };

    const childChooseDate = (date) => {
      props.clickChooseDate(date);
    };

    return {
      // computed
      getStartDate,
      getEndDate,
      getCalendar,
      getYearMonthToday,
      compareToday,
      compareCurrentDate,
      // methods
      childPrevMonth,
      childNextMonth,
      childChooseDate,
    };
  },
});
</script>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Molecules/types/Calendar.type.ts
export type ClickPrevMonth = {
  (): void;
};
export type ClickNextMonth = {
  (): void;
};
export type ClickChooseDate = {
  (date: string): void;
};

export type Props = {
  currentDate: object;
  today: object;
  clickPrevMonth: ClickPrevMonth;
  clickNextMonth: ClickNextMonth;
  clickChooseDate: ClickChooseDate;
};
```

### 【コンポーネント作成】Molecules/Time
タイムピッカーを作成します。

```vue:frontend/components/Molecules/Time.vue
<template>
  <div class="time-wrapper">
    <!-- タイムピッカーヘッダ -->
    <TimeHeader
      :dateFormat="dateFormat"
      :clickChildPrevSelectDate="childPrevSelectDate"
      @childPrevSelectDate="childPrevSelectDate"
    />
    <!-- タイムピッカー -->
    <TimePicker
      :timeTable="timeTable"
      :clickChildChooseTime="childChooseTime"
      @childChooseTime="childChooseTime"
    />
    <p class="supplement">※横にスクロールしてください</p>
  </div>
</template>

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

// components
import TimeHeader from '../Atoms/TimeHeader.vue';
import TimePicker from '../Atoms/TimePicker.vue';

// types
import {
  Reservations,
  TimeTable,
  ClickPrevSelectDate,
  ClickChooseTime,
  Props,
} from './types/Time.type';

export default defineComponent({
  components: {
    TimeHeader,
    TimePicker,
  },
  props: {
    date: {
      type: String,
    },
    reservations: {
      type: Array as PropType<Reservations>,
    },
    today: {
      type: Object,
    },
    timeTable: {
      type: Array as PropType<TimeTable>,
    },
    clickPrevSelectDate: {
      type: Function as PropType<ClickPrevSelectDate>,
    },
    clickChooseTime: {
      type: Function as PropType<ClickChooseTime>,
    },
  },
  setup(props: Props) {
    // computed
    const dateFormat = computed(() => {
      return moment(props.date).format('YYYY年MM月DD日');
    });

    // methods
    const childPrevSelectDate = () => {
      props.clickPrevSelectDate();
    };

    const childChooseTime = (time) => {
      props.clickChooseTime(time);
    };

    return {
      // computed
      dateFormat,
      // methods
      childPrevSelectDate,
      childChooseTime,
    };
  },
});
</script>

<style lang="scss" scoped>
// wrapper
.time-wrapper {
  padding: 0 50px;
  max-width: 450px;
  margin: 0 auto;
  text-align: center;
}

.supplement {
  font-size: 0.5rem;
  text-align: left;
}
</style>
```

`TypeScript`の型定義ファイルを用意します。

```typescript:frontend/components/Molecules/types/Time.type.ts
export type Reservations = {
  date: string;
  id: number;
  time: string;
  userId: number;
}[];
export type TimeTable = {
  clock: string;
  isEmpty: boolean;
}[];
export type ClickPrevSelectDate = {
  (): void;
};
export type ClickChooseTime = {
  (time: string): void;
};

export type Props = {
  date: string;
  reservations: Reservations;
  today: object;
  timeTable: TimeTable;
  clickPrevSelectDate: ClickPrevSelectDate;
  clickChooseTime: ClickChooseTime;
};
```

これで、`Molecules`が完成しました。
次に、`Organisms`の作成をしていきます。

### 【コンポーネント作成】Organisms/CalendarTime
カレンダーとタイムピッカーを作成します。

```vue:frontend/components/Organisms/CalendarTime.vue
<template>
  <div>
    <Calendar
      :currentDate="currentDate"
      :today="today"
      :clickPrevMonth="prevMonth"
      @prevMonth="prevMonth()"
      :clickNextMonth="nextMonth"
      @nextMonth="nextMonth()"
      :clickChooseDate="chooseDate"
      @chooseDate="chooseDate(date)"
    />
    <br />
    <Time
      :date="date"
      :reservations="reservations"
      :today="today"
      :timeTable="timeTable"
      :clickPrevSelectDate="prevSelectDate"
      @prevSelectDate="prevSelectDate()"
      :clickChooseTime="chooseTime"
      @chooseTime="chooseTime(time)"
    />
  </div>
</template>

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

// components
import Calendar from '../Molecules/Calendar.vue';
import Time from '../Molecules/Time.vue';

export default defineComponent({
  components: {
    Calendar,
    Time,
  },
  setup() {
    // axios
    const { $axios } = useContext();

    // onMounted
    onMounted(async () => {
      if (sessionStorage.date) {
        date.value = sessionStorage.date;
      }

      const sessionDate = sessionStorage.date;
      if (sessionDate) {
        reservations.value = await $axios.$get(`/api/reservations/${sessionDate}`);
      } else {
        const todayFormat = today.value.format('YYYY-MM-DD');
        reservations.value = await $axios.$get(`/api/reservations/${todayFormat}`);
      }

      reservations.value.forEach((reservation) => {
        timeTable.value.forEach((time) => {
          if (reservation.time === time.clock) {
            time.isEmpty = false;
          }
        });
      });
    });

    // data
    const currentDate = ref<any>(moment());
    const today = ref<any>(moment());
    const date = ref<string>();
    const reservations = ref<string[]>();
    const timeTable = ref<string[]>([
      { clock: '9:00', isEmpty: true },
      { clock: '10:00', isEmpty: true },
      { clock: '11:00', isEmpty: true },
      { clock: '12:00', isEmpty: true },
      { clock: '13:00', isEmpty: true },
      { clock: '14:00', isEmpty: true },
      { clock: '15:00', isEmpty: true },
      { clock: '16:00', isEmpty: true },
      { clock: '17:00', isEmpty: true },
      { clock: '18:00', isEmpty: true },
    ]);

    // methods
    const prevMonth = () => {
      currentDate.value = moment(currentDate.value).subtract(1, 'month');
    };

    const nextMonth = () => {
      currentDate.value = moment(currentDate.value).add(1, 'month');
    };

    const chooseDate = (date) => {
      window.location.href = 'http://localhost:3000';
      sessionStorage.date = date;
    };

    const prevSelectDate = () => {
      sessionStorage.date = '';
      sessionStorage.time = '';
      window.location.href = 'http://localhost:3000';
    };

    const chooseTime = (time) => {
      sessionStorage.time = time;
      window.location.href = 'http://localhost:3000';
    };

    return {
      // data
      currentDate,
      today,
      date,
      reservations,
      timeTable,
      // methods
      prevMonth,
      nextMonth,
      chooseDate,
      prevSelectDate,
      chooseTime,
    };
  },
});
</script>
```

### 【コンポーネント作成】Organisms/Form
フォームを作成します。

```vue:frontend/components/Organisms/Form.vue
<template>
  <div class="form">
    <div class="form-name-container">
      <div class="form-name-container-item">
        <FormLabel :title="'姓'" />
        <FormInput :placeholder="'例: 田中'" :value="firstName" @input="firstName = $event" />
      </div>
      <div class="form-name-container-item">
        <FormLabel :title="'名'" />
        <FormInput :placeholder="'例: 太郎'" :value="lastName" @input="lastName = $event" />
      </div>
    </div>
    <FormLabel :title="'メールアドレス'" />
    <FormInput
      :type="'email'"
      :placeholder="'例: taro-tanaka@example.com'"
      :value="email"
      @input="email = $event"
    />
    <FormButton :click="submit" @submit="submit()" />
  </div>
</template>

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

// component
import FormLabel from '../Atoms/FormLabel.vue';
import FormInput from '../Atoms/FormInput.vue';
import FormButton from '../Atoms/FormButton.vue';

export default defineComponent({
  components: {
    FormLabel,
    FormInput,
    FormButton,
  },
  setup() {
    // axios
    const { $axios } = useContext();

    // data
    const firstName = ref<string | undefined>();
    const lastName = ref<string | undefined>();
    const email = ref<string | undefined>();

    const submit = async () => {
      const date = sessionStorage.date;
      const time = sessionStorage.time;

      sessionStorage.date = '';
      sessionStorage.time = '';

      window.location.href = 'http://localhost:3000';

      await $axios.$post('/api/users/store', {
        firstName: firstName.value,
        lastName: lastName.value,
        email: email.value,
        date: date,
        time: time,
      });
    };

    return {
      // data
      firstName,
      lastName,
      email,
      // methods
      submit,
    };
  },
});
</script>

<style lang="scss" scoped>
.form {
  padding: 0px 50px 30px 50px;
  &-name-container {
    display: flex;
    align-items: center;

    &-item {
      width: 100%;
      &:nth-child(1) {
        margin-right: 5px;
      }

      &:nth-child(2) {
        margin-left: 5px;
      }
    }
  }
}
</style>
```

これで、`Organisms`が完成しました。

### `frontend/pages/index.vue`の作成
上記までで作成した`Organisms`を使って、`pages/index.vue`の修正を行いましょう。

```vue:frontend/pages/index.vue
<template>
  <div class="reservation-container">
    <Heading :content="'ご希望の日時をお選びください'" />
    <CalendarTime />
    <Heading :content="'お客様の情報を入力してください'" />
    <Form />
  </div>
</template>

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

// component
import Heading from '../components/Atoms/Heading.vue';
import Form from '../components/Organisms/Form.vue';
import CalendarTime from '../components/Organisms/CalendarTime.vue';

export default defineComponent({
  components: {
    Heading,
    Form,
    CalendarTime,
  },
});
</script>

<style lang="scss" scoped>
.reservation {
  &-container {
    width: 100%;
    background: #f7f5ee;
    max-width: 450px;
    margin: 0 auto;
  }
}
</style>
```

これでフロントエンドは完成です。