NuxtとGraphQLとAWS(Amplify, appsync, dynamodb, cognito, s3....)を用いてWebアプリチュートリアルPart2

## 概要
- [NuxtとGraphQLとAWS(Amplify, appsync, dynamodb, cognito, s3....)を用いてWebアプリチュートリアルPart1](https://qiita.com/kaito_111/items/8633fd0088e07a1c8247)
- NuxtとGraphQLとAWS Amplify用いてWebアプリを作ってみたPart2
- [NuxtとGraphQLとAWS(Amplify, appsync, dynamodb, cognito, s3....)を用いてWebアプリチュートリアルPart3](https://qiita.com/kaito_111/items/1526174a206d91beee97)
- [NuxtとGraphQLとAWS(Amplify, appsync, dynamodb, cognito, s3....)を用いてWebアプリチュートリアルPart4](https://qiita.com/kaito_111/items/bbbe216a0274305adf62)


## やること
- ブログ一覧表示
- 新規登録

## フロント実装編
### ブログ一覧表示
まずはデータの取得なしでデザインのみでコードを書いていきます。

```:page/index.vue
<template>
  <display />
</template>

<script>
import Display from '../components/readBlog/index'

export default {
  components: {
    Display,
  },
}
</script>

```
コンポーネントを記述します。

```:components/readBlog/index.vue
<template>
  <v-card class="mx-auto" max-width="344">
    <v-img
      src="https://cdn.vuetifyjs.com/images/cards/sunshine.jpg"
      height="200px"
    ></v-img>

    <v-card-title> Top western road trips </v-card-title>

    <v-card-subtitle> 1,000 miles of wonder </v-card-subtitle>

    <v-card-actions>
      <v-btn color="orange lighten-2" text> Explore </v-btn>
      <v-spacer></v-spacer>
    </v-card-actions>
  </v-card>
</template>

```
コンポーネントのコードは[vuetify](https://vuetifyjs.com/ja/components/cards/)からのコピペです。

![スクリーンショット 2021-02-14 11.54.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712534/e9d50bda-0754-111c-2c41-d0c64acbef88.png)

このように表示されます。

続いて、データの取得をしたいところですが、dynamoDBにデータがないので、新規作成機能を作り、そのあと取得することにします。

### 新規作成機能
最初は、デザインからいきます。

```:pages/write/create.vue
<template>
  <CreateBlog />
</template>

<script>
import CreateBlog from '../../components/writeBlog/create'

export default {
  components: {
    CreateBlog,
  },
}
</script>
```
コンポーネントを適当に作成します

```:components/writeBlog/create.vue
<template>
  <v-container>
    <v-row>
      <v-col col="12">
        <input v-model="title" type="text" />
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">ファイル用</v-col>
    </v-row>
    <v-row>
      <v-col col="12"></v-col>
    </v-row>
  </v-container>
</template>

```
今、ブログのタイトル、画像の投稿を入れました。
続いて、ブログの内容を書くためにマークダウンに対応したエディタをインストールします。

#### 候補
- [marvonEditor](https://github.com/hinesboy/mavonEditor)
- [vue2editor](https://www.vue2editor.com/guide.html#installation)
- [quarsar FrameWork](https://quasar.dev/)

お好きなのを使ってください。
今回私が使用するプラグインは[mavonEditor](https://github.com/hinesboy/mavonEditor)です。

#### mavon-editorのインストール

```
npm install mavon-editor --save
```

```:plugins/vue-mavon-editor.js
import Vue from 'vue'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'

Vue.use(mavonEditor)
```
次に``nuxt.config.js``ファイルにプラグインパスを書きます。

```:nuxt.config.js
  plugins: [
  ...
    { src: '@/plugins/vue-mavon-editor', ssr: false }
//  { src: './plugins/vue-mavon-editor', ssr: false } ドキュメント通りやると私のほうはエラー出たのでこちらを使いました。 
  ],
```

続いて、mavonEditorの表示です。

```:components/writeBlog/create.vue
<template>
  <v-container>
    <v-row>
      <v-col col="12">
        <input v-model="title" type="text" />
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">ファイル用</v-col>
    </v-row>
    <v-row>
<!-- 追加 -->
      <v-col col="12">
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="handbook"
              :toolbars="markdownOption"
              language="en"   // デフォルトは中国語になるので英語に変更
            />
          </no-ssr>
        </div>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  data() {
    return {
      markdownOption: {
        bold: true, // 粗体
        italic: true, // 斜体
        header: true, // hello
        underline: true, // 下划线
        strikethrough: true, // 中划线
        mark: true, // 标记
        superscript: true, // 上角标
        subscript: true, // 下角标
        quote: true, // 引用
        ol: true, // 有序列表
        ul: true, // 无序列表
        link: true, // hi
        // imagelink: true, // 图片链接
        code: true, // code
        table: true, // 表格
        fullscreen: true, // 全屏编辑
        readmodel: true, // 沉浸式阅读
        htmlcode: true, // 展示html源码
        help: true, // 帮助
        /* 1.3.5 */
        undo: true, // 上一步
        redo: true, // 下一步
        trash: true, // 清空
        // save: true, // 保存（触发events中的save事件）
        /* 1.4.2 */
        navigation: true, // 导航目录
        /* 2.1.8 */
        alignleft: true, // 左对齐
        aligncenter: true, // 居中
        alignright: true, // 右对齐
        /* 2.2.1 */
        subfield: true, // 单双栏模式
        preview: true, // 预览
      },
      handbook: '#### how to use mavonEditor in nuxt.js',
    }
  },
  methods: {
    // comfirmImage() {},
  },
}
</script>

// 追加↓
<style scoped>
.mavonEditor {
  width: 100%;
  height: 100%;
}
</style>
```
追加完了です。
こちらにアクセスしてみると画像のように表示されていればOKです！！
[http://localhost:3000/write/create](http://localhost:3000/write/create)
![スクリーンショット 2021-02-16 14.54.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712534/edcd3d6e-73ac-859d-d677-d2addca0c324.png)

一度レイアウトを整えます。。
タイトル名を表示する場所が背景と同化しているので、、。

```:components/writeBlog/create.vue
// 略

        <p>Title</p>
        <input class="text" v-model="title" type="text" />
      </v-col>
    </v-row>
    <v-row>
      <!--      <v-col col="12"><input type="file" @change="confirmImage" /></v-col>-->
    </v-row>
    <v-row>
      <v-col col="12">
        <p>Contents</p>
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="handbook"
              :toolbars="markdownOption"
              language="en"
            />
          </no-ssr>

// 略
<style scoped>
// 略
.text {
  color: white;
  padding: 0px 10px;
  font-size: 30px;
  border: 1px solid #424242;
  border-radius: 20px;
  height: 40px;
  width: 100%;
}
```
以下のように書き換えましょう。。

これでスタイル調整完了です。
以下のように表示されます。
![スクリーンショット 2021-02-16 15.11.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712534/7ae4b21b-80da-c21e-76c1-a13da7170ab3.png)

次に投稿実装と画像のアップロードも一緒にやります。

まずは画像のアップロード機能を実装しましょう。(s3以外)

```:components/writeBlog/create.vue
// 略
    <v-row>
      <v-col col="12">
        <p>Title</p>
        <input class="text" v-model="title" type="text" />
      </v-col>
    </v-row>
// 画像追加用
    <v-row>
      <v-col col="6">
        <input class="image" type="file" @change="confirmImage" />
      </v-col>
      <v-col col="6">
        <span v-if="confirmedImage">
          <img class="c-image" :src="confirmedImage" alt />
        </span>
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">
        <p>Contents</p>
// 略

<script>
export default {
  data() {
    return {
      markdownOption: {
        // 略
      },
      handbook: '#### how to use mavonEditor in nuxt.js',
//追加
      file: '',
      confirmedImage: '',
      message: '',
    }
  },
  methods: {
// 追加
    confirmImage(event) {
      this.message = ''
      this.file = event.target.files[0]
      if (!this.file.type.match('image.*')) {
        this.message = '画像ファイルを選択して下さい'
        this.confirmedImage = ''
        return
      }
      this.createImage(this.file)
    },
    createImage(file) {
      const reader = new FileReader()
      reader.readAsDataURL(file)
      reader.onload = (e) => {
        this.confirmedImage = e.target.result
      }
    },
  },
}
</script>

<style scoped>
// 追加
.image {
  height: 150px;
}
.c-image {
  width: 300px;
  height: 150px;
}
</style>
```
これで画像アップロード用の機能実装完了です。残りはs3とgraphqlの実装です。

### s3の導入
ターミナルで以下のコードを実行

```
% amplify add storage
```

```
? Please select from one of the below mentioned services: Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: blog
? Please provide bucket name: blog
? Who should have access: Auth and guest users
? What kind of access do you want for Authenticated users? create/update, read, delete
? What kind of access do you want for Guest users? read
? Do you want to add a Lambda Trigger for your S3 Bucket? No
```
以下のコマンドを打ってバックエンドに反映させましょう✌️

``` 
% amplify push
```


これで導入完了です。
本当かよ！ってくらい早くないっすか？？
気になる人は簡単にチェックする方法があって、s3のページに行くのもいいですが、
aws-export.jsというファイルを見ると``s3``の項目が表示されているはずです。

```
% amplify status
```
↑こちらでも確認できます。

#### 投稿機能仕上げ

投稿機能ラストです！！
投稿しましょう。

```:components/writeBlog/create.vue
<template>
<!-- 略 -->
      <v-col col="12">
        <p>Contents</p>
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="handbook"
              :toolbars="markdownOption"
              language="en"
            />
          </no-ssr>
        </div>
      </v-col>
    </v-row>
    <v-row>
      <v-col class="btn-box" col="12">
        <input class="btn" type="button" @click="submit" value="投稿" />
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
// 追加
import { API, Storage } from 'aws-amplify'
import awsmobile from '../../aws-exports'
import { createBlog } from '../../graphql/mutations'

export default {
  data() {
    return {
// 略
      handbook: '#### how to use mavonEditor in nuxt.js',
      imageData: '',
      confirmedImage: '',
      imageUrl: '',
      message: '',
    }
  },
  methods: {
// 略
    async submit() {
      const filePath = `cuisine/${this.imageData.name}`
      const imageUrlDefault =
        'https://' +
        awsmobile.aws_user_files_s3_bucket +
        '.s3-' +
        awsmobile.aws_user_files_s3_bucket_region +
        '.amazonaws.com/public/'
      await Storage.put(filePath, this.imageData).then((result) => {
        this.imageUrl = imageUrlDefault + filePath
      })
      const createBlogInfo = {
        title: this.title,
        image_url: this.imageUrl,
        content: this.handbook,
      }
      await API.graphql({
        query: createBlog,
        variables: { input: createBlogInfo },
      })
      window.location.href = '/'
    },
  },
}
</script>

<style scoped>
/* 略 */
.btn-box {
  text-align: center;
}
.btn {
  border: 1px solid #000000;
  background-color: white;
  color: #424242;
  width: 90px;
  height: 30px;
  border-radius: 10px;
}
</style>
```

さ！これで投稿ボタンを押して投稿！！。
![スクリーンショット 2021-02-16 17.19.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712534/76cfe1c6-5294-6723-76e4-38ebac59a3a8.png)

あれ、、、。エラー？？

違います。
実はs3バケットを作成した時の権限の話で、s3に書き込みができるのは、認証済みユーザのみ！としたはずです、、。
つまり、``ログイン必須``。

```:pages/write/create.vue
<template>
  <amplify-authenticator>
    <CreateBlog />
    <amplify-sign-out></amplify-sign-out>
  </amplify-authenticator>
</template>

<script>
import CreateBlog from '../../components/writeBlog/create'

export default {
  name: 'Create',
  components: {
    CreateBlog,
  },
}
</script>
```

``amplifyのauth``タグの追加を行いました。
プラスで、``name``属性の追加を行いました。どうやらこれが無いとダメっぽいので、、、。

さ、完了です。投稿してみましょう。

投稿が完了すると,トップページへ画面遷移しますが、トップページでは、データの取得を行っていません。なのでデータの取得を行います。

```:page/index.vue
<template>
  <div>
    <Display v-for="post in posts" :key="post.id" :post="post" />
  </div>
</template>

<script>
import { API } from 'aws-amplify'
import Display from '../components/readBlog/index'
import { listBlogs } from '../graphql/queries'

export default {
  components: {
    Display,
  },
  data() {
    return {
      posts: [],
    }
  },
  created() {
    this.getBlog()
  },
  methods: {
    async getBlog() {
      const posts = await API.graphql({
        query: listBlogs,
      })
      this.posts = posts.data.listBlogs.items
    },
  },
}
</script>
```
次にコンポーネント

```:components/readBlog/index.vue
<template>
  <v-card class="mx-auto" max-width="344">
    <v-img :src="post.image_url" height="200px"></v-img>

    <v-card-title>{{ post.title }}</v-card-title>

    <v-card-subtitle>{{ post.createdAt }}</v-card-subtitle>

    <v-card-actions>
      <v-btn color="orange lighten-2" text>
        Explore
      </v-btn>
      <v-spacer></v-spacer>
    </v-card-actions>
  </v-card>
</template>

<script>
export default {
  name: 'PostCard',
  props: ['post'],
}
</script>
```

![スクリーンショット 2021-02-17 13.07.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712534/fb7c93d0-a59b-97ba-615a-0ff36a76fcfc.png)


このようにデータ取得できていたら成功です。
デザインが少しおかしいですが、ま、いいでしょう。

次に詳細画面への画面遷移です。

まずは、一覧表示の画面で、詳細へ飛べるようにリンクを貼りましょう

```:components/readBlog/index.vue
<template>
  <v-card class="mx-auto" max-width="344">
    <v-img :src="post.image_url" height="200px"></v-img>

    <v-card-title>{{ post.title }}</v-card-title>

    <v-card-subtitle>{{ post.createdAt }}</v-card-subtitle>

    <v-card-actions>
      <v-btn color="orange lighten-2" text :to="'/detail/' + post.id">
        Explore
      </v-btn>
      <v-spacer></v-spacer>
    </v-card-actions>
  </v-card>
</template>

<script>
export default {
  name: 'PostCard',
  props: ['post'],
}
</script>
```

あとは、詳細ページ用のファイルを作成して完了です。

```:pages/detail/_id.vue
<template>
  <DetailCard :key="blog.id" :blog="blog" />
</template>

<script>
import { API } from 'aws-amplify'
import { getBlog } from '../../graphql/queries'
import DetailCard from '../../components/readBlog/detail'

export default {
  components: {
    DetailCard,
  },
  data() {
    return {
      blog: [],
    }
  },
  created() {
    this.getCuisineDetail()
  },
  methods: {
    async getCuisineDetail() {
      // eslint-disable-next-line no-undef
      const detailBlog = await API.graphql({
        query: getBlog,
        variables: {
          id: this.$route.params.id,
        },
      })
      this.blog = detailBlog.data.getBlog
    },
  },
}
</script>
```
次はコンポーネント

```:components/readBlog/detail.vue
<template>
  <v-container>
    <v-row>
      <v-col col="12">
        <img class="image" :src="blog.image_url" alt="test" />
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">
        <h2>{{ blog.title }}</h2>
      </v-col>
    </v-row>
    <v-row>
      <v-col col="12">
        <div class="mavonEditor">
          <no-ssr>
            <mavon-editor
              v-model="content"
              language="ja"
              :subfield="false"
              defaultOpen="preview"
              :toolbars="false"
            />
          </no-ssr>
        </div>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  name: 'DetailCard',
  props: ['blog'],
  data() {
    return {
      content: '',
    }
  },
  created() {
    this.data()
  },
  methods: {
    data() {
      this.content = this.blog.content
    },
  },
}
</script>

<style scoped>
.image {
  width: 100%;
  height: 300px;
}
</style>
```

#### ブログ一覧と詳細
![01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712534/a0d8d135-39d9-db4f-f7ec-3b9fc7d7f323.gif)

以上でパート2終わりです。

#### ソースコード
part2の[ソースコード](https://github.com/kaitoohsiro/blog-app/tree/part2)です。

## 続きのURL 
- [Part3](https://qiita.com/kaito_111/items/1526174a206d91beee97)
- [Part4](https://qiita.com/kaito_111/items/bbbe216a0274305adf62)

## 参考
- [AWS Amplify & Nuxt 使って爆速でWebアプリ作る](https://qiita.com/TakahiRoyte/items/26dab7929e752a6aa5c6)
- [Vue.jsにマークダウンエディタを埋め込む【mavonEditor】](https://qiita.com/watatakahashi/items/097120d3a77ee90695eb)
- [Aws-amplify ドキュメント](https://docs.amplify.aws/lib/storage/getting-started/q/platform/js)
- [vuetify](https://vuetifyjs.com/ja/components/bottom-sheets/#section-4f7f304465b9)
- [aws-amplify 認証](https://feature-ai-service.dma9ecr5ksxts.amplifyapp.com/amplify/amplify-auth/)