Udemy 10. ログイン前のレイアウト構築 #05
2020年08月20日に更新

Nuxt.js ウェルカムページをレスポンシブデザインに対応させよう(4/4)

ウェルカムページは、全4回に渡って構築します。

1/4 ウェルカムページを構築するコンポーネントファイルの作成

2/4 アイキャッチ画像の設置とツールバーの作成

3/4 スクロールイベントでツールバーの色を変える

4/4 レスポンシブデザインの対応(最終的なコード記載)(今ここ)

前回までのおさらい

前回は、スクロールによってツールバーの色を変化させる実装を行いました。

現在のウェルカムページはこのようになっています。

2020-08-05 19-38-03

今回達成すること

今回は3つを実装し、ウェルカムページを完成させます。

  1. ツールバーに会員登録、ログインリンクを追加
  2. ツールバーメニューをレスポンシブデザインに対応
  3. フッターにコピーライトを追加

最終的にHerokuにpushして本番環境での動作も確認します。

途中でエラーにつまづいた方

このページ下部に最終的なコードを置いていますのでコピペしてください。

#最終的なコード

ツールバーに会員登録、ログインリンクを追加

ツールバーに会員登録、ログインリンクを追加します。

このリンクは、

  • ウェルカムページと
  • ログイン前のツールバー(まだ未作成)

で使用するのでコンポーネントとして切り分けます。

コンポーネントファイルを作成する

まずは「beforeLogin」ディレクトリにloginLink.vuesignupLink.vueを作成しましょう。

root $ touch front/components/beforeLogin/{loginLink.vue,signupLink.vue}

それぞれにリンクボタンを作成します。

loginLink.vueはこんな感じに。

front/components/beforeLogin/loginLink.vue
<template>
  <v-btn
    text
    class="ml-2 font-weight-bold"
    color="black"
    to="/login"
  >
    {{ $t('pages.login') }}
  </v-btn>
</template>

<script>
export default {
}
</script>

signupLink.vueはこのように編集します。

front/components/beforeLogin/signupLink.vue
<template>
  <v-btn
    outlined
    dark
    color="myblue"
    class="ml-2 font-weight-bold"
    to="/signup"
  >
    {{ $t('pages.signup') }}
  </v-btn>
</template>

<script>
export default {
}
</script>

Vuetifyにオリジナルカラーを追加する

会員登録のリンクカラーはcolor="myblue" と設定しました。

これは現在のprimaryではリンクが見えにくいためです。

nuxt.config.jsvuetifyプロパティに新しい色myblueを追加しましょう。

front/nuxt.config.js
vuetify: {
    ...
    themes: {
      light: {
        primary: '4080BE',
        info: '4FC1E9',
        success: '44D69E',
        warning: 'FEB65E',
        error: 'FB8678',
        background: 'f6f6f4',
        // 追加
        myblue: '1867C0'
      }
    }
  }
},

nuxt-i18nを書き換える

nuxt-i18nの翻訳ファイルja.jsonに、それぞれのページタイトルを追加します。

front/locales/ja.json
{
  ...
  // pagesに書き換え
  "pages": {
    "signup": "会員登録",
    "login": "ログイン"
  }
}

ツールバーにコンポーネントを追加する

作成したリンクをwelAppBar.vueに追加します。

front/components/welcome/welAppBar.vue
    ...
    </v-toolbar-items>

    <!-- 2つのコンポーネントを追加 -->
    <signup-link />
    <login-link />

  </v-app-bar>
</template>
...

コンテナを起動してブラウザで確認してみましょう。

root $ docker-compose up

2020-08-05 17-21-10

きれいに表示されましたね。

リンク先ページを作成していないので、クリックしてもエラーになります。

レスポンシブデザインに対応する

レスポンシブに対応するため、ウィンドウ幅が768px未満の場合はツールバーメニューをポップアップ形式にします。

表示・非表示を切り替えるCSSクラスは、この記事で追加したhidden-ipad-and-up(down)を使用します。

その他、アプリ名と「このサイトについて」も特定のブレイクポイントで非表示にするようCSSクラスを追加します。

front/components/welcome/welAppBar.vue
    ...
    <!-- 1. class="hidden-mobile-and-down" 追加 -->
    <v-toolbar-title
      class="hidden-mobile-and-down"
    >
      {{ appName }}
    </v-toolbar-title>

    <v-spacer />

    <!-- 2. hidden-ipad-and-down 追加 -->
    <v-toolbar-items class="ml-2 hidden-ipad-and-down">
      <!-- 3. :class="{ 'hidden-sm-and-down': (menu.title === 'about') }" 追加 -->
      <v-btn
        v-for="(menu, i) in menus"
        :key="`menu-btn-${i}`"
        text
        :class="{ 'hidden-sm-and-down': (menu.title === 'about') }"
        @click="goTo(menu.title)"
      >
        {{ $t(`menus.${menu.title}`) }}
      </v-btn>
    </v-toolbar-items>

    <signup-link />
    <login-link />

    <!-- 4. 追加 -->
    <v-menu
      bottom
      nudge-left="110"
      nudge-width="100"
    >
      <template v-slot:activator="{ on }">
        <v-app-bar-nav-icon
          class="hidden-ipad-and-up"
          v-on="on"
        />
      </template>
      <v-list
        dense
        class="hidden-ipad-and-up"
      >
        <v-list-item
          v-for="(menu, i) in menus"
          :key="`menu-list-${i}`"
          exact
          @click="goTo(menu.title)"
        >
          <v-list-item-title>
            {{ $t(`menus.${menu.title}`) }}
          </v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>
    <!-- ここまで -->
  </v-app-bar>
</template>
...
  • :class="{ 'hidden-sm-and-down': (menu.title === 'about') }"

    { 'CSSクラス': Boolean } => オブジェクトの値がtrueの場合にCSSクラスが有効になる書き方。

実装画面はこのようになります。

アプリタイトル

モバイル(426px)未満で非表示。

2020-08-05 18-01-06

ツールバーメニュー

ipad(768px)未満で非表示。

ポップアップメニュー(v-menu)

ipad(768px)未満で表示。

2020-08-05 18-03-57

メニュー「このサイトについて」

sm(960px)未満で非表示。

2020-08-05 18-12-08

フッターにコピーライトを追加する

フッターにコピーライトを追加しましょう。

befLoginFooter.vueをまるっと書き換えます。

<!-- まるっと書き換え -->
<template>
  <div :style="{ marginTop: `${height}px`}">
    <v-footer
      absolute
      dark
      color="black"
      :height="height"
    >
      <v-col
        cols="12"
        class="py-0"
      >
        <div class="text-center text-body-2">
          &copy;{{ copyRightYear }}
          <strong>{{ appName }}</strong>
        </div>
      </v-col>
    </v-footer>
  </div>
</template>

<script>
export default {
  data ({ $config: { appName } }) {
    return {
      appName,
      height: 32
    }
  },
  computed: {
    copyRightYear () {
      const beginningYear = 2020
      const thisYear = new Date().getFullYear()
      return (beginningYear < thisYear)
        ? `${beginningYear} - ${thisYear}` : beginningYear
    }
  }
}
</script>
  • :style="{ marginTop: ${height}px}"v-footerabsoluteを指定するとコンテンツが被さるため、同じ高さのマージントップを指定する。
  • copyRightYear ()beginningYearより現在の年が大きければ2020 - 2021と表示され、そうでなければ2020と表示される。

これでフッターが完成です。

2020-08-05 18-21-21

Herokuにpushする

これにてウェルカムページの完成です。Herokuにpushしましょう。

各メニューのコンテンツコードを追加する場合は、このページ下部をコピペしてください。

#各メニューコンテンツのコード

ブランチ名を分かりやすい名前に変更します。

root $ cd front
front $ git branch -m 20200803_welcome_page_layout
front $ git branch

* 20200803_welcome_page_layout
  • git branch -m <新しいブランチ名> … 今いるブランチの名前を変更する。

masterブランチにマージします。

front $ git add -A
front $ git commit -m "finished_welcome_page"
front $ git checkout master
front $ git merge 20200803_welcome_page_layout # 自分のブランチ名に読み替えてください

GitHubとHerokuにpushします。

front $ git push
front $ git push heroku

Herokuに環境変数をセットします。

お好きな名前でどうぞ。

front $ heroku config:set APP_NAME=BizPlanner
front $ heroku config

APP_NAME: BizPlanner

Herokuを開いて本番環境の表示も確認しておきましょう。

front $ heroku open

本番環境の確認が取れたら「root」ディレクトリをGitHubにpushします。

front $ cd ..
root $ git commit -am "front_finished_welcome_page"
root $ git push

以上でウェルカムページの実装を終わります。

まとめ

今回はツールバーにリンクを設定し、レスポンシブに対応するようCSSクラスを追加しました。

これでどのデバイスにも対応できるようになりましたね。

このウェルカムページは全4回に渡って構築しました。

いやーここまで来たあなた、本当にお疲れ様でした。

次回は?

次回から複数回に分けて、会員登録ページ・ログインページを構築します。

どうぞお楽しみに!

最終的なコード

ウェルカムページの最終的なコード。

エラーが発生した方はここのコードをコピペしてください。

環境変数

front/.env
APP_NAME=BizPnanner

nuxt.config.js

front/nuxt.config.js
export default {
  mode: 'spa',

  // Doc: https://ja.nuxtjs.org/blog/going-full-static/
  target: 'server',

  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  loading: { color: '#fff' },

  css: [
    '~/assets/sass/main.scss'
  ],

  plugins: [
    'plugins/axios'
  ],

  // Doc: https://nuxtjs.org/api/configuration-components
  components: true,

  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
    // Doc: https://www.npmjs.com/package/@nuxtjs/vuetify
    '@nuxtjs/vuetify'
  ],

  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    // Doc: https://www.npmjs.com/package/nuxt-i18n
    'nuxt-i18n'
  ],

  // public ENV
  // Doc: https://nuxtjs.org/guide/runtime-config/
  publicRuntimeConfig: {
    appName: process.env.APP_NAME // 追加
  },

  axios: {
  },

  vuetify: {
    // 開発環境でcustomVariablesを有効にするフラグ
    // Doc: https://vuetifyjs.com/ja/customization/a-la-carte/
    treeShake: true,
    customVariables: ['~/assets/sass/variables.scss'],
    theme: {
      themes: {
        light: {
          primary: '4080BE',
          info: '4FC1E9',
          success: '44D69E',
          warning: 'FEB65E',
          error: 'FB8678',
          background: 'f6f6f4',
          myblue: '1867C0'
        }
      }
    }
  },

  // Doc: https://nuxt-community.github.io/nuxt-i18n/basic-usage.html#nuxt-link
  i18n: {
    // アプリがサポートしている言語
    locales: ['ja', 'en'],
    // デフォルトの言語
    defaultLocale: 'ja',
    // Doc: https://kazupon.github.io/vue-i18n/api/#properties
    vueI18n: {
      // defaultLocale(ja.json)にkeyがない場合に参照される(フォールバック)言語
      fallbackLocale: 'ja',
      // true => 翻訳に失敗した場合の警告メッセージを出力しない
      // silentTranslationWarn: true,
      // true => keyが全くない場合のみ警告が発生し、フォールバック時には警告は発生しない
      silentFallbackWarn: true,
      messages: {
        ja: require('./locales/ja.json'),
        en: require('./locales/en.json')
      }
    }
  },

  build: {
  }
}

翻訳ファイル

front/locales/ja.json
{
  "menus": {
    "about": "サイトについて",
    "products": "製品",
    "price": "価格",
    "contact": "お問合せ",
    "company": "会社情報"
  },
  "pages": {
    "signup": "会員登録",
    "login": "ログイン"
  }
}

レイアウトファイル

front/layouts/welcome.vue
<template>
  <v-app>
    <wel-app-bar
      :menus="menus"
      :img-height="imgHeight"
    />

    <v-img
      id="scroll-top"
      dark
      src="https://picsum.photos/id/20/1920/1080?blur=5"
      gradient="to top right, rgba(19,84,122,.6), rgba(128,208,199,.9)"
      :height="imgHeight"
    >
      <v-row
        align="center"
        justify="center"
        :style="{ height: `${imgHeight}px` }"
      >
        <v-col
          cols="12"
          class="text-center"
        >
          <h1 class="display-1 mb-4">
            未来を作ろう。ワクワクしよう。
          </h1>
          <h4
            class="subheading"
            :style="{ letterSpacing: '5px' }"
          >
            中小企業に特化した事業計画策定ツール
          </h4>
        </v-col>
      </v-row>
    </v-img>

    <v-sheet>
      <v-container
        fluid
        :style="{ maxWidth: '1280px' }"
      >
        <v-row
          v-for="(menu, i) in menus"
          :key="`menu-${i}`"
        >
          <v-col
            :id="menu.title"
            cols="12"
          >
            <v-card flat>
              <v-card-title class="justify-center display-1">
                {{ $t(`menus.${menu.title}`) }}
              </v-card-title>
              <v-card-text class="text-center">
                {{ menu.subtitle }}
              </v-card-text>
            </v-card>
          </v-col>
          <v-col cols="12">
            <div :is="`wel-${menu.title}`" />
          </v-col>
        </v-row>
      </v-container>
    </v-sheet>

    <bef-login-footer />
  </v-app>
</template>

<script>
import welAbout from '~/components/welcome/welAbout'
import welProducts from '~/components/welcome/welProducts'
import welPrice from '~/components/welcome/welPrice'
import welContact from '~/components/welcome/welContact'
import welCompany from '~/components/welcome/welCompany'

export default {
  components: {
    welAbout,
    welProducts,
    welPrice,
    welContact,
    welCompany
  },
  data () {
    return {
      imgHeight: 500, // 追加
      menus: [
        { title: 'about', subtitle: 'このサイトはブログ"独学プログラマ"で公開されているチュートリアルのデモアプリケーションです' },
        { title: 'products', subtitle: '他にはない優れた機能の数々' },
        { title: 'price', subtitle: '会社の成長に合わせた3つのプラン' },
        { title: 'contact', subtitle: 'お気軽にご連絡を' },
        { title: 'company', subtitle: '私たちの会社' }
      ]
    }
  }
}
</script>

ツールバー

front/components/welcome/welAppBar.vue
<template>
  <v-app-bar
    app
    :dark="!isScrollPoint"
    :height="appBarHeight"
    :color="toolbarStyle.color"
    :elevation="toolbarStyle.elevation"
  >
    <app-logo
      @click.native="goTo('scroll-top')"
    />
    <v-toolbar-title
      class="hidden-mobile-and-down"
    >
      {{ appName }}
    </v-toolbar-title>

    <v-spacer />

    <v-toolbar-items class="ml-2 hidden-ipad-and-down">
      <v-btn
        v-for="(menu, i) in menus"
        :key="`menu-btn-${i}`"
        text
        :class="{ 'hidden-sm-and-down': (menu.title === 'about') }"
        @click="goTo(menu.title)"
      >
        {{ $t(`menus.${menu.title}`) }}
      </v-btn>
    </v-toolbar-items>

    <signup-link />
    <login-link />

    <v-menu
      bottom
      nudge-left="110"
      nudge-width="100"
    >
      <template v-slot:activator="{ on }">
        <v-app-bar-nav-icon
          class="hidden-ipad-and-up"
          v-on="on"
        />
      </template>
      <v-list
        dense
        class="hidden-ipad-and-up"
      >
        <v-list-item
          v-for="(menu, i) in menus"
          :key="`menu-list-${i}`"
          exact
          @click="goTo(menu.title)"
        >
          <v-list-item-title>
            {{ $t(`menus.${menu.title}`) }}
          </v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>
  </v-app-bar>
</template>

<script>
export default {
  props: {
    menus: {
      type: Array,
      default: () => []
    },
    imgHeight: {
      type: Number,
      default: 0
    }
  },
  data ({ $config: { appName }, $store }) {
    return {
      appName,
      scrollY: 0,
      appBarHeight: $store.state.styles.beforeLogin.appBarHeight
    }
  },
  computed: {
    isScrollPoint () {
      return this.scrollY > (this.imgHeight - this.appBarHeight)
    },
    toolbarStyle () {
      const color = this.isScrollPoint ? 'white' : 'transparent'
      const elevation = this.isScrollPoint ? 4 : 0
      return { color, elevation }
    }
  },
  mounted () {
    window.addEventListener('scroll', this.onScroll)
  },
  beforeDestroy () {
    window.removeEventListener('scroll', this.onScroll)
  },
  methods: {
    onScroll () {
      this.scrollY = window.scrollY
    },
    goTo (id) {
      this.$vuetify.goTo(`#${id}`)
    }
  }
}
</script>

アプリロゴ

front/components/ui/appLogo.vue
<template>
  <v-avatar
    color="black"
    size="34"
    class="my-app-log"
  >
    <span class="white--text text-subtitle-2">
      Biz
    </span>
  </v-avatar>
</template>

<script>
export default {
}
</script>

<style lang="scss" scoped>
.my-app-log {
  margin-right: 8px;
  cursor: pointer;
}
</style>

ログインリンク

front/components/beforeLogin/loginLink.vue
<template>
  <v-btn
    text
    class="ml-2 font-weight-bold"
    color="black"
    to="/login"
  >
    {{ $t('pages.login') }}
  </v-btn>
</template>

<script>
export default {
}
</script>

会員登録リンク

front/components/beforeLogin/signupLink.vue
<template>
  <v-btn
    outlined
    dark
    color="myblue"
    class="ml-2 font-weight-bold"
    to="/signup"
  >
    {{ $t('pages.signup') }}
  </v-btn>
</template>

<script>
export default {
}
</script>

フッター

省略。上記に記載あり。

各メニューコンテンツのコード

各メニューのコンテンツはご自身の好きなように編集してください。

参考までに筆者のコードを載せておきます。

面倒な方はコピペして一部を自分用に編集してくださいね。

このサイトについて(welAbout.vue)

front/components/welcome/welAbout.vue
<template>
  <div>
    <v-card-title class="pb-8 text-subtitle-2 justify-center">
      このアプリケーションの作り方は、下記URLに公開されています。
    </v-card-title>
    <v-card-title class="text-subtitle-2 justify-center">
      「Rails6とNuxt.jsで作るユーザーJWT認証付きシングルページアプリケーション」
    </v-card-title>
    <v-card-text class="text-center">
      <a
        :href="blogUrl"
        rel="nofollow"
        target="_blank"
        class="text-decoration-none"
      >
        {{ blogUrl }}
      </a>
    </v-card-text>
    <v-card-title class="text-subtitle-2 justify-center">
      採用している技術
    </v-card-title>
    <v-container
      fluid
      :style="{ maxWidth: '960px' }"
    >
      <v-row justify="space-around">
        <div
          v-for="(tec, i) in technologies"
          :key="`tec-${i}`"
          class="text-center pa-2"
        >
          <v-avatar
            :color="tec.color"
            size="80"
          >
            <span class="white--text">
              {{ tec.name }}
            </span>
          </v-avatar>
          <div class="pt-2 text-body-2 my-grey">
            {{ tec.use }}
          </div>
          <div class="pt-2 text-body-2 my-grey">
            {{ tec.v }}
          </div>
        </div>
      </v-row>
    </v-container>
  </div>
</template>

<script>
export default {
  data () {
    return {
      blogUrl: 'https://blog.cloud-acct.com/categories/udemy',
      technologies: [
        { name: 'Docker', v: 'v19.03+', use: '開発環境', color: '#2496ED' },
        { name: 'Rails api', v: 'v6.0+', use: 'サーバーサイド', color: '#CC0000' },
        { name: 'Postgre SQL', v: '', use: 'データベース', color: '#336791' },
        { name: 'Nuxt.js spa', v: 'v2.13+', use: 'フロントエンド', color: '#00C48D' },
        { name: 'Heroku', v: '', use: 'ホスティング', color: '#6762A6' },
        { name: 'Vuetify', v: 'v2.3+', use: 'CSSフレームワーク', color: '#1867C0' }
      ]
    }
  }
}
</script>

製品(welProducts.vue)

front/components/welcome/welProducts.vue
<template>
  <v-row>
    <v-col
      cols="12"
      sm="6"
    >
      <v-list flat>
        <v-list-item
          v-for="(point, i) in points"
          :key="`point-${i}`"
        >
          <v-list-item-icon>
            <v-icon
              size="30"
              :color="point.color"
              v-text="point.icon"
            />
          </v-list-item-icon>
          <v-list-item-content>
            <div
              class="text-subtitle-1"
              v-text="point.text"
            />
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-col>
    <v-col
      cols="12"
      sm="6"
    >
      <v-sparkline
        :value="sparkline.value"
        :gradient="sparkline.gradient"
        :smooth="sparkline.radius || false"
        :padding="sparkline.padding"
        :line-width="sparkline.width"
        :stroke-linecap="sparkline.lineCap"
        :gradient-direction="sparkline.gradientDirection"
        :fill="sparkline.fill"
        :type="sparkline.type"
        :auto-line-width="sparkline.autoLineWidth"
        auto-draw
      />
    </v-col>
  </v-row>
</template>

<script>
export default {
  data () {
    const gradients = [
      ['#222'],
      ['#42b3f4'],
      ['red', 'orange', 'yellow'],
      ['purple', 'violet'],
      ['#00c6ff', '#F0F', '#FF0']
    ]
    return {
      points: [
        {
          icon: 'mdi-file-table-box-multiple-outline',
          color: 'blue',
          text: '直感的な操作で快適に事業計画書を作成'
        },
        {
          icon: 'mdi-chart-bar',
          color: 'green accent-4',
          text: 'KPIから考えた夢物語ではない経営計画値を算出'
        },
        {
          icon: 'mdi-chart-arc',
          color: 'deep-orange',
          text: 'ビジュアライズに優れたグラフツールで経営を可視化'
        }
      ],
      sparkline: {
        width: 4,
        radius: 10,
        padding: 4,
        lineCap: 'round',
        gradient: gradients[4],
        value: [0, 2, 5, 9, 5, 10, 8, 2, 9, 20],
        gradientDirection: 'right',
        gradients,
        fill: false,
        type: 'trend',
        autoLineWidth: true
      }
    }
  }
}
</script>

価格(welPrice.vue)

front/locales/ja.json
{
  "menus": {
    "about": "サイトについて",
    "products": "製品",
    "price": "価格",
    "contact": "お問合せ",
    "company": "会社情報",
    // 追加
    "payments": {
      "month": "月払い",
      "year": "年払い"
    }
  },
  ...
}
front/components/welcome/welPrice.vue
<template>
  <v-row>
    <v-col
      cols="12"
      class="py-0"
    >
      <v-card-actions class="py-0">
        <v-spacer />
        <v-radio-group
          v-model="payment"
          row
        >
          <v-radio
            v-for="(pay, i) in payments"
            :key="`pay-${i}`"
            :label="$t(`menus.payments.${pay.label}`)"
            :value="pay.label"
            :color="pay.color"
          />
        </v-radio-group>
      </v-card-actions>
    </v-col>
    <v-col
      v-for="(plan, i) in plans"
      :key="`plan-${i}`"
      cols="12"
      :sm="12 / plans.length"
    >
      <v-card
        max-width="402"
        class="mx-auto"
      >
        <v-card-title
          :class="['white--text', plan.color]"
        >
          {{ plan.name }}
        </v-card-title>

        <v-card-title class="justify-center">
          {{ plan.exp }}
        </v-card-title>

        <v-divider />

        <v-card-title class="justify-center">
          メンバー
          {{ plan.member }}
        </v-card-title>

        <v-divider />

        <v-card-actions class="justify-center align-baseline">
          月
          <span class="px-2 display-1 font-weight-bold">
            {{ yen(plan.price[payment]) }}
          </span>
          円
        </v-card-actions>
        <v-card-actions class="justify-center align-baseline">
          年間 {{ yen(plan.price[payment] * 12) }} 円
        </v-card-actions>
      </v-card>
    </v-col>
  </v-row>
</template>

<script>
export default {
  data () {
    const payments = [
      { label: 'month', color: 'indigo' },
      { label: 'year', color: 'myblue' }
    ]
    return {
      payments,
      payment: payments[1].label,
      plans: [
        {
          name: 'Only',
          color: 'info',
          exp: '経営者1人のためのプラン',
          member: '1人',
          price: {
            month: 1200,
            year: 800
          }
        },
        {
          name: 'Small',
          color: 'primary',
          exp: '小規模事業に特化した1チーム専用',
          member: '3人まで',
          price: {
            month: 2400,
            year: 1800
          }
        },
        {
          name: 'Business',
          color: 'indigo',
          exp: '大規模なチームに戦略的経営を導入',
          member: '10人まで',
          price: {
            month: 5000,
            year: 4000
          }
        }
      ]
    }
  },
  computed: {
    yen () {
      return (val) => {
        return String(val).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')
      }
    }
  }
}
</script>

お問合せ(welContact.vue)

front/components/welcome/welContact.vue
<template>
  <v-row
    justify="center"
  >
    <v-col
      cols="12"
      sm="10"
      md="8"
    >
      <v-form
        ref="contact"
        v-model="isValid"
      >
        <v-container>
          <v-row>
            <v-col
              cols="12"
              sm="6"
            >
              <v-text-field
                v-model="name"
                :rules="nameRules"
                :disabled="sentIt"
                label="名前(必須)"
                outlined
              />
            </v-col>
            <v-col
              cols="12"
              sm="6"
            >
              <v-text-field
                v-model="email"
                :rules="emailRules"
                :disabled="sentIt"
                label="メールアドレス(必須)"
                outlined
                validate-on-blur
              />
            </v-col>
          </v-row>

          <v-textarea
            v-model="contents"
            :rules="contentRules"
            :disabled="sentIt"
            label="お問合せの内容をお聞かせください(必須)"
            rows="5"
            outlined
            auto-grow
          />

          <v-btn
            :disabled="!isValid || loading || sentIt"
            :loading="loading"
            color="primary"
            class="mr-2"
            @click="onSend"
          >
            送信する
          </v-btn>

          <v-btn
            text
            @click="formReset"
          >
            キャンセル
          </v-btn>
          <div class="grey--text">
            <small>実際には送信されません</small>
          </div>
        </v-container>
      </v-form>
    </v-col>
    <v-snackbar
      v-model="sentIt"
      timeout="-1"
      color="primary"
    >
      お問合せ内容が送信されました。メールアドレスへ担当者よりご連絡いたします。
      <template v-slot:action="{ attrs }">
        <v-btn
          color="white"
          text
          v-bind="attrs"
          @click="formReset"
        >
          Close
        </v-btn>
      </template>
    </v-snackbar>
  </v-row>
</template>

<script>
export default {
  data () {
    return {
      isValid: false,
      name: '',
      nameRules: [
        v => !!v || '名前を入力してください'
      ],
      email: '',
      emailRules: [
        v => !!v || 'メールアドレスを入力してください',
        v => /.+@.+\..+/.test(v) || 'メールアドレスが正しくありません'
      ],
      contents: '',
      contentRules: [
        v => !!v || 'お問合せ内容を入力してください'
      ],
      loading: false,
      sentIt: false
    }
  },
  methods: {
    onSend () {
      this.loading = true
      setTimeout(() => {
        this.loading = false
        this.sentIt = true
      }, 1500)
    },
    formReset () {
      this.sentIt = false
      this.$refs.contact.reset()
    }
  }
}
</script>

会社情報(welCompany.vue)

「assets」ディレクトリ直下に「images」ディレクトリを作成し、下記3つのイメージを保存します。

内容は何でもOKですが、名前だけは一致させてください。

front/assets/images
├── member1.png
├── member2.png
└── member3.png
front/components/welcome/welCompany.vue
<template>
  <div>
    <v-card-title class="font-weight-bold justify-center">
      メンバー
    </v-card-title>
    <v-row justify="space-around">
      <v-col
        v-for="(member, i) in members"
        :key="`member-${i}`"
      >
        <v-list-item>
          <v-list-item-icon>
            <img
              :src="member.img"
              :alt="member.nickname"
              :aspect-ratio="192 / 336"
              width="50"
            >
          </v-list-item-icon>
          <v-list-item-content>
            <div>
              {{ member.name }}
            </div>
            <v-list-item-action-text>
              {{ member.position }}
            </v-list-item-action-text>
            <v-list-item-action-text>
              <v-btn
                v-if="member.twitter"
                :href="member.twitter"
                target="_blank"
                rel="noopener noreferrer"
                small
                icon
              >
                <v-icon size="18">
                  mdi-twitter
                </v-icon>
              </v-btn>
              <v-btn
                v-if="member.slack"
                :href="member.slack"
                target="_blank"
                rel="noopener noreferrer"
                small
                icon
              >
                <v-icon size="18">
                  mdi-slack
                </v-icon>
              </v-btn>
            </v-list-item-action-text>
          </v-list-item-content>
        </v-list-item>
      </v-col>
    </v-row>
    <v-card-title class="font-weight-bold justify-center">
      会社情報
    </v-card-title>
    <v-row justify="center">
      <v-col
        cols="12"
        sm="10"
        md="8"
      >
        <v-list
          flat
          dense
        >
          <v-divider />
          <template v-for="(info, i) in infomations">
            <v-list-item :key="`info-list-${i}`">
              <v-list-item-icon>
                <v-icon v-text="info.icon" />
              </v-list-item-icon>
              <v-list-item-content>
                <a
                  v-if="info.link"
                  :href="info.link"
                  rel="nofollow"
                  target="_blank"
                  class="text-decoration-none"
                >
                  {{ info.link }}
                </a>
                <div
                  v-else
                  class="text-subtitle-1"
                  v-text="info.text"
                />
              </v-list-item-content>
            </v-list-item>
            <v-divider :key="`info-divider-${i}`" />
          </template>
        </v-list>
      </v-col>
    </v-row>
  </div>
</template>

<script>
import member1 from '~/assets/images/member1.png'
import member2 from '~/assets/images/member2.png'
import member3 from '~/assets/images/member3.png'

export default {
  data () {
    const twitter = 'https://twitter.com/esegrammer'
    const slack = 'https://join.slack.com/t/dokugaku-kai/shared_invite/zt-a5j1suoh-Y0fspHbo1fb0Wj6YTpDdXA'
    const companyUrl = 'http://blog.cloud-acct.com'
    return {
      members: [
        { name: 'あんどう', position: '代表', img: member1, twitter, slack },
        { name: 'アローン', position: 'エンジニア', img: member2 },
        { name: 'カール', position: 'エンジニア', img: member3 }
      ],
      infomations: [
        { icon: 'mdi-domain', text: 'BizPlanner株式会社' },
        { icon: 'mdi-link-variant', link: companyUrl },
        { icon: 'mdi-flag', text: '2020年7月に設立' },
        { icon: 'mdi-account-multiple', text: '3人のメンバー' },
        { icon: 'mdi-map-marker', text: '東京都港区虎ノ門一丁目17番1号' },
        { icon: 'mdi-handshake', text: 'Webアプリ開発・経営コンサルティング' }
      ]
    }
  }
}
</script>
あなたの力になれること
私自身が独学でプログラミングを勉強してきたので、一人で学び続ける苦しみは痛いほど分かります。そこで、当時の私がこんなのあったら良いのにな、と思っていたサービスを立ち上げました。周りに質問できる人がいない、答えの調べ方が分からない、ここを聞きたいだけなのにスクールは高額すぎる...。そんな方に向けた単発・短期間メンターサービスを行っています。下のサービスへお進みください。
独学プログラマのサービス
次の記事はこちら
1. このカテゴリーの歩き方 #01
【お知らせ】UdemyでRails × Nuxt.jsの動画を公開することになりました
1. このカテゴリーの歩き方 #02
【随時更新】アプリケーション仕様書
1. このカテゴリーの歩き方 #03
【随時更新】このカテゴリーの歩き方
1. このカテゴリーの歩き方 #04
(Docker+Rails6+Nuxt.js+PostgreSQL)=>Heroku 環境構築~デプロイまでの手順書
2. Docker入門 #01
Docker for Macをインストールする手順
2. Docker入門 #02
分かるDocker解説。仮想環境・コンテナ・Dockerイメージ・Dockerfileとは何か?
2. Docker入門 #03
分かるDocker解説。DockerComposeとは何か?
3. Dockerを使ったRails+Nuxt.js環境構築 #01
【Docker+Rails6+Nuxt.js】今回作成するアプリの開発環境の全体像を知ろう
3. Dockerを使ったRails+Nuxt.js環境構築 #02
【MacOS】Homebrew経由でGitをインストールする方法
3. Dockerを使ったRails+Nuxt.js環境構築 #03
Rails6を動かすAlpineベースのDockerfileを作成する(AlpineLinuxとは何か)
3. Dockerを使ったRails+Nuxt.js環境構築 #04
Nuxt.jsを動かすAlpineベースのDockerfileを作成する(C.UTF-8とは何か)
3. Dockerを使ったRails+Nuxt.js環境構築 #05
.envファイルを使ったdocker-compose.ymlの環境変数設計
3. Dockerを使ったRails+Nuxt.js環境構築 #06
Rails6・Nuxt.js・PostgreSQLを動かすdocker-compose.ymlファイルを作成する
3. Dockerを使ったRails+Nuxt.js環境構築 #07
docker-compose.ymlを使ってRails6を構築する(PostgreSQLパスワード変更方法)
3. Dockerを使ったRails+Nuxt.js環境構築 #08
docker-compose.ymlを使ってNuxt.jsを構築する
4. 複数プロジェクトのGit管理 #01
複数プロジェクトで行うGit管理の全体像を理解しよう(Gitサブモジュール解説)
4. 複数プロジェクトのGit管理 #02
【Git】既存の子ディレクトリをサブモジュール管理に変更する手順
4. 複数プロジェクトのGit管理 #03
【GitHub】秘密鍵の生成・公開鍵を追加・SSH接続するまでを画像で分かりやすく
4. 複数プロジェクトのGit管理 #04
【GitHub】リモートリポジトリの追加・サブモジュールのリンク設定を行う
5. RailsAPI×Nuxt.js初めてのAPI通信 #01
【Rails6】"Hello" jsonを返すコントローラを作成する
5. RailsAPI×Nuxt.js初めてのAPI通信 #02
【Nxut.js】axiosの初期設定を行う(baseURL・browserBaseURLを解説)
5. RailsAPI×Nuxt.js初めてのAPI通信 #03
【Rails6】Gem rack-corsを導入してCORS設定を行う(オリジン・CORSとは何か)
6. Heroku.ymlを使ったDockerデプロイ #01
デプロイ準備。Herokuへ新規会員登録を行いHerokuCLIをインストールする
6. Heroku.ymlを使ったDockerデプロイ #02
heroku.yml解説編。Docker環境のRails6をHerokuにデプロイする(1/2)
6. Heroku.ymlを使ったDockerデプロイ #03
HerokuCLI-manifestのデプロイ解説編。Docker環境のRails6をHerokuにデプロイする(2/2)
6. Heroku.ymlを使ったDockerデプロイ #04
Dockerfile解説編。Docker環境のNuxt.jsをHerokuにデプロイする(1/2)
6. Heroku.ymlを使ったDockerデプロイ #05
デプロイ完結編。Docker環境のNuxt.jsをHerokuにデプロイする(2/2)
7. モデル開発事前準備 #01
【Rails6】application.rbの初期設定(タイムゾーン・I18n・Zeitwerk)
7. モデル開発事前準備 #02
【Rails6】モデル開発に必要なGemのインストールとHirb.enableの自動化
7. モデル開発事前準備 #03
【Docker+Rails】A server is already running. Check /tmp/pids/server.pidエラーの対応
7. モデル開発事前準備 #04
【Docker】<none>タグのイメージを一括削除する & Rails .gitignoreの編集
8. ユーザーモデル開発 #01
Railsユーザーモデル作成。テーブル設計・ユーザー認証設計を理解する
8. ユーザーモデル開発 #02
Railsユーザーモデルのバリデーション設定(has_secure_password解説)
8. ユーザーモデル開発 #03
Railsバリデーションエラーメッセージの日本語化(ja.yml設定方法)
8. ユーザーモデル開発 #04
EachValidatorクラスのカスタムバリデーション設定(Rails6/lib以下読込)
8. ユーザーモデル開発 #05
Rails環境ごとにSeedデータ切り替えるseeds.rbの書き方
8. ユーザーモデル開発 #06
Rails6から導入された並列テストを理解する
8. ユーザーモデル開発 #07
Railsユーザーモデルバリデーションテスティング(name/email/password)
8. ユーザーモデル開発 #08
Nuxt.jsからRailsのユーザーテーブルを取得しHerokuにデプロイする
9. Nuxt.jsフロント開発事前準備 #01
【Nuxt.js2.13超解説】バージョンアップ手順と6つの新機能+2つの変更点
9. Nuxt.jsフロント開発事前準備 #02
Docker AlpineベースのNode.js上で動くNuxt.jsにVuetifyを導入する
9. Nuxt.jsフロント開発事前準備 #03
VuetifyにカスタムCSSを導入してオリジナルブレイクポイントを作る
9. Nuxt.jsフロント開発事前準備 #04
Nuxt.jsにnuxt-i18nを導入して国際化に対応する
10. ログイン前のレイアウト構築 #01
Nuxt.jsのレイアウト・ページ・コンポーネントの役割を理解しよう
10. ログイン前のレイアウト構築 #02
Nuxt.js ウェルカムページを構成するコンポーネントファイル群を作成しよう(1/4)
10. ログイン前のレイアウト構築 #03
Nuxt.js ウェルカムページにアイキャッチ画像・アプリ名・メニューボタンを表示しよう(2/4)
10. ログイン前のレイアウト構築 #04
Nuxt.js addEventListenerでスクロールを検知しツールバーの色を変化させよう(3/4)
10. ログイン前のレイアウト構築 #05
Nuxt.js ウェルカムページをレスポンシブデザインに対応させよう(4/4)
10. ログイン前のレイアウト構築 #06
Nuxt.js 会員登録ページのレイアウトファイルを作成しよう(1/4)
10. ログイン前のレイアウト構築 #07
Nuxt.js 名前、メール、パスワードのコンポーネントファイルを作成しよう(2/4)
10. ログイン前のレイアウト構築 #08
Nuxt.js 親子コンポーネント間の双方向データバインディングを実装する(3/4)
10. ログイン前のレイアウト構築 #09
Nuxt.js Vuetifyのv-text-fieldを使った会員登録フォームのバリデーション設定(4/4)
10. ログイン前のレイアウト構築 #10
Nuxt.js ログインページ実装とHerokuデプロイまで(router. replaceとpushの違いとは)
11. ログイン後のレイアウト構築 #01
Nuxt.js ログイン後のツールバーを作成しよう(inject解説)
11. ログイン後のレイアウト構築 #02
Nuxt.js アカウントメニューページ・ログアウト機能を実装しよう(nuxt-child解説)
11. ログイン後のレイアウト構築 #03
Nuxt.js ログイン後のトップページにプロジェクト一覧を表示しよう
11. ログイン後のレイアウト構築 #04
Nuxt.js プロジェクトページにVuetifyのナビゲーションドロワーを追加しよう
11. ログイン後のレイアウト構築 #05
Nuxt.js paramsIDからプロジェクトを検索してVuexに保存しよう
SPA開発
ブログ構築
小ネタ集