20210802のvue.jsに関する記事は7件です。

Vue.jsでプレビュー付きの画像アップロードを作った。

はじめに おはようございます。こんにちは。こんばんは。 Watatakuです。 今回はVue.jsでプレビュー付きのよくある画像アップローダーの作り方です。 ⬇︎画像の選択後 対象者 Vue.js初学者。 環境 node.js: v15.11.0 npm: v7.6.0 @vue/cli: v4.5.13 vue: ^2.6.11 作り方 <template> <div class="imgContent"> <div class="imagePreview"> <img :src="this.uploadedImage" width="50" height="50" alt /> </div> <div class="module--spacing--largeSmall"></div> <label for="corporation_file" class="btn btn-success"> 画像を設定する <input type="file" class="file_input" style="display:none;" id="corporation_file" mulitple="multiple" @change="onDrop" /> </label> </div> </template> <script> export default { data() { return { uploadedImage: "", }; }, methods: { onDrop(event) { //画像データ取得 const fileList = event.target.files; this.previewImage(fileList[0]); }, // アップロードした画像を表示 previewImage(file) { let reader = new FileReader(); reader.onload = (e) => { this.uploadedImage = e.target.result; }; reader.readAsDataURL(file); }, }, }; </script> <style scoped> /* 略 */ </style> 解説 画像が選択されるとonDrop()が発火し、選択されたファイルの情報を取得し、 previewImage()で画像を表示させている流れ。 最後に ぜひ、参考にして見てください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueのコンポーネントを学ぶ

はじめに vue.jsをまた基礎から学びなおしているのですが、コンポーネントがなかなか理解した!と自信を持って言えないところです。 公式ドキュメントに沿って覚えたことを自分用解説つきで書いていきます。 コンポーネントとは コンポーネントシステムは Vue.js におけるもうひとつの重要な抽象概念です。「小さく、自己完結的で、(多くの場合)再利用可能なコンポーネント」を組み合わせることで、大規模アプリケーションを構築することが可能になります。アプリケーションのインターフェイスについて考えてみると、ほぼすべてのタイプのインターフェイスはコンポーネントツリーとして抽象化することができます 例えば、webページに共通のヘッダーフッターをすべてのページに出すとき、すべてのファイルにHTML分ちまちま書いたりしたくないですよね。このヘッダーフッターを再利用できるパーツ(再利用可能なコンポーネント)にしちゃおう!という感じです。 簡単なコンポーネントを作って表示してみる 文字読んでてもよくわからないので一旦作ってみます。 サイトでよく見る、会社名と選択中のタイトル(見出し)を表示するやつをコンポーネント化してみます。 ※コンポーネントの理解が主目的なので文言やデザインは超適当です。 componentsの直下にHeader.vueを作成します Header.vue <template> <div class="header"> <h1>会社名</h1> <span>見出し</span> </div> </template> <script> export default { name: 'Header', } </script> <style scoped> .header { background-color: #2fa16e; color: aliceblue; } </style> この作ったコンポーネントを使用してみます。今回はIndex.vueというファイルで使用します Index.vue <template> <div id="app"> <Header/> // 3 </div> </template> <script> import Header from './components/Header.vue' // 1 export default { name: "Index", components: { Header // 2 }, } </script> 1.Header.vueをimportします。importしたものをここではHeaderと呼ぶよ。という記述 2.Headerをコンポーネントとして扱ういますという記述 3.実際に使用 という感じです。 親:Index.vue 子:Header.vue の関係になります。 実行してIndex.vueを表示した結果 作成したHeader.vueコンポーネントが表示されました。 使用するコンポーネント名を変えたいとき 先ほどの書き方ではHeader.vueをHeaderという名前としてimportして使用しましたが、<>内での名称を変更することができます。 Index.vue <template> <div id="app"> <HeaderName/> </div> </template> <script> import Header from './components/Header.vue' export default { name: "Index", components: { "HeaderName": Header //ここを変更 }, } </script> Headerをコンポーネントとして使用するときはHeaderNameという名前とするよ、という記述です。 それ以外の関数でHeader.vue使用する場合はそのままHeaderとなります。 プロパティを受け取れるようにする【props】 先ほどの例では「会社名」「見出し」の文言が固定でしたが、文言をページごとに変えたい場合も多いと思います。 propsを使用することで親のスコープから子コンポーネントへとデータを渡せるようにできます。 試しに先ほどの例の「見出し」を変えられるようにしてみます。 子コンポーネントの書き方 Header.vue <template> <div class="header"> <h1>会社名</h1> <span>{{ title }}</span> </div> </template> <script> export default { name: 'Header', props: { title: String } } </script> ---以下略--- {{ title }}でtitleにセットされた値を受け取れるようにします。 では実際titleに何をいれるのか? 通常dataにtitleを定義しますが、今回titleに何が入るか決めるのは親でないといけません。そのため親からデータをもらえるようpropsを使用します。今回は文字列をもらうのでtitle: Stringとしています。 補足1 以下のように書いても同じ動きになります。 Header.vue <script> export default { name: 'Header', props: { title: { type: String } } } </script> 補足2 補足1のような書き方をすることで他の項目も定義できます。 Header.vue <script> export default { name: 'Header', props: { title: { type: String, default: 'デフォルト', //デフォルト値 required: true //必須 } } } </script> 親コンポーネントの書き方 Index.vue <template> <div id="app"> <HeaderName title="見出し1"/> <HeaderName title="見出し2"/> </div> </template> <script> import HeaderName from './components/Header.vue' export default { name: "Index", components: { HeaderName }, } </script> title="見出し1"と渡す値を指定することができます。 わかりやすいように2つ並べてみました。 実行結果 プロパティに変数を渡す【props + v-bind】 propsで値を受け取れるようにしましたが、固定でなく変数を渡したい場合は以下のようにv-bindを使用します。 子コンポーネントはそのままで問題ありません。親を修正します。 Index.vue <template> <div id="app"> <HeaderName title="見出し1"/> <HeaderName title="見出し2"/> <HeaderName v-bind:title="title"/> --追加 </div> </template> <script> import HeaderName from './components/Header.vue' export default { name: "Index", components: { HeaderName }, data() { return { title: "見出し3", } }, } </script> v-bind:prop名="データ名"で変数を渡せます。 省略した場合は:prop名="データ名"となります。 実行結果 v-forを使用するとき v-forでちょっと悩んだので書き方メモ。 子コンポーネントの書き方 Header.vue <template> <div> <li>{{ menu.name }}</li> </div> </template> <script> export default { name: 'Header', props: { menu: Object } } </script> 親コンポーネントの書き方 Index.vue <template> <div id="app"> <Header v-for="menu in menus" :key="menu.name" :menu="menu"/> </div> </template> <script> import Header from './components/Header.vue' export default { name: "Index", components: { Header }, data() { return { menus: [ { name: "メニュー1" }, { name: "メニュー2" }, { name: "メニュー3" }, ] } }, } </script> さいごに ドキュメントよみながら思い出しながら進めているので補足などあれば随時更新していきます。 また、なにか間違いやこうしたほうがいいよ等何かあればコメントいただけると嬉しいです。 最後まで読んでいただきありがとうございます!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者が6時間でWebサイト作成から公開まで

まずは完成品をどうぞ 渋谷のギラギラ感を出したかったのでネオンライト風CSSを使った。 スクロールするとこんな感じ。 試しにGoogleMapAPIも使ってみた。 スマホで見るとちょっとデザインが変わります。 実際にWeb公開しているURLはここです。 ちょっと重たいかも。 作り方 まずは作った動機 vue-CLIとGitHub Pagesの組み合わせで簡単にweb公開できることを知って 試しに何か作ってみたくなったというお話。 総制作時間6時間くらいで、もちろん費用はかかっていない。 結局一通り出来上がってからレスポンシブやらなんやらの調整に一番時間かかった。 割と見た目ができていればOKくらいの気持ちで作った。 Web公開の方法 まずGitHubでリポジトリを作ってクローンして、vue-CLIを使って実装をしていく。 実装する前にまずPagesの設定をして、vueのサンプルページがうまく表示されるかを確認した。 実際にvueの生成物をPagesでの公開する方法の詳細はこちらの記事から。 XDを使ってデザインをなんとなく考える XDはadobeのデザイン作成ソフト。 Web制作ではよく使われる上に、無料で使える模様。 adobe公式サイトのスターターキットダウンロードで入手できる。 トップのイメージを探す 素材に関してはおなじみのunsplashで見つけた。 無料とは思えないクオリティの素材を発掘できるので重宝している。 vueでトップページを作成 詳しい説明は割愛するが、HTMLとCSSに関しては、ドットインストールのはじめてのWeb制作とvue-cliの使い方をググればできるレベル。 ドットインストールは初心者におすすめ。 ネオンライト風のCSS CSSで美しいネオンライトのエフェクトをテキストやボタン、ボーダーに実装するテクニックのまとめを参考にした。 GoogleMapAPI ライブラリなしでGoogle MapをVue.jsで使う方法を参考にした。 レスポンシブデザイン PCで見るのとスマホで見るのとで幅が変わるのでデザインを変更した。 これも"レスポンシブデザイン"でググったら出てくるレベルで作成。(全然解説になってない) まとめ 以上が全貌。 結局何が難しいってフレームワークを決めることとデザイン自体の2つ。 この2点に関しては正解がないので常に最適解に近づけていく必要がある。 それ以外に関しては大体ググったら正解が手に入る。 あとは、デザインの勉強のためにWebサイト制作をしてみたが、 正直XDでお絵描きして終わりでもいいような気がする。 デザイン+実装するなると半日はかかるので、量産ができない。 いずれにしても、今後も架空のWebサイトデザインを作っていきたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者が6時間でWebサイト制作から公開まで

まずは完成品をどうぞ 渋谷のギラギラ感を出したかったのでネオンライト風CSSを使った。 スクロールするとこんな感じ。 試しにGoogleMapAPIも使ってみた。 スマホで見るとちょっとデザインが変わります。 実際にWeb公開しているURLはここです。 ちょっと重たいかも。 作り方 まずは作った動機 vue-CLIとGitHub Pagesの組み合わせで簡単にweb公開できることを知って 試しに何か作ってみたくなったというお話。 総制作時間6時間くらいで、もちろん費用はかかっていない。 結局一通り出来上がってからレスポンシブやらなんやらの調整に一番時間かかった。 割と見た目ができていればOKくらいの気持ちで作った。 Web公開の方法 まずGitHubでリポジトリを作ってクローンして、vue-CLIを使って実装をしていく。 実装する前にまずPagesの設定をして、vueのサンプルページがうまく表示されるかを確認した。 実際にvueの生成物をPagesでの公開する方法の詳細はこちらの記事から。 XDを使ってデザインをなんとなく考える XDはadobeのデザイン作成ソフト。 Web制作ではよく使われる上に、無料で使える模様。 adobe公式サイトのスターターキットダウンロードで入手できる。 トップのイメージを探す 素材に関してはおなじみのunsplashで見つけた。 無料とは思えないクオリティの素材を発掘できるので重宝している。 vueでトップページを作成 詳しい説明は割愛するが、HTMLとCSSに関しては、ドットインストールのはじめてのWeb制作とvue-cliの使い方をググればできるレベル。 ドットインストールは初心者におすすめ。 ネオンライト風のCSS CSSで美しいネオンライトのエフェクトをテキストやボタン、ボーダーに実装するテクニックのまとめを参考にした。 GoogleMapAPI ライブラリなしでGoogle MapをVue.jsで使う方法を参考にした。 レスポンシブデザイン PCで見るのとスマホで見るのとで幅が変わるのでデザインを変更した。 これも"レスポンシブデザイン"でググったら出てくるレベルで作成。(全然解説になってない) まとめ 以上が全貌。 結局何が難しいってフレームワークを決めることとデザイン自体の2つ。 この2点に関しては正解がないので常に最適解に近づけていく必要がある。 それ以外に関しては大体ググったら正解が手に入る。 あとは、デザインの勉強のためにWebサイト制作をしてみたが、 正直XDでお絵描きして終わりでもいいような気がする。 デザイン+実装するなると半日はかかるので、量産ができない。 いずれにしても、今後も架空のWebサイトデザインを作っていきたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueでWeb制作から公開まで【総6時間】

まずは完成品をどうぞ 渋谷のギラギラ感を出したかったのでネオンライト風CSSを使った。 スクロールするとこんな感じ。 試しにGoogleMapAPIも使ってみた。 スマホで見るとちょっとデザインが変わります。 実際にWeb公開しているURLはここです。 ちょっと重たいかも。 作り方 まずは作った動機 vue-CLIとGitHub Pagesの組み合わせで簡単にweb公開できることを知って 試しに何か作ってみたくなったというお話。 総制作時間6時間くらいで、もちろん費用はかかっていない。 結局一通り出来上がってからレスポンシブやらなんやらの調整に一番時間かかった。 割と見た目ができていればOKくらいの気持ちで作った。 Web公開の方法 まずGitHubでリポジトリを作ってクローンして、vue-CLIを使って実装をしていく。 実装する前にまずPagesの設定をして、vueのサンプルページがうまく表示されるかを確認した。 実際にvueの生成物をPagesでの公開する方法の詳細はこちらの記事から。 XDを使ってデザインをなんとなく考える XDはadobeのデザイン作成ソフト。 Web制作ではよく使われる上に、無料で使える模様。 adobe公式サイトのスターターキットダウンロードで入手できる。 トップのイメージを探す 素材に関してはおなじみのunsplashで見つけた。 無料とは思えないクオリティの素材を発掘できるので重宝している。 vueでトップページを作成 詳しい説明は割愛するが、HTMLとCSSに関しては、ドットインストールのはじめてのWeb制作とvue-cliの使い方をググればできるレベル。 ドットインストールは初心者におすすめ。 ネオンライト風のCSS CSSで美しいネオンライトのエフェクトをテキストやボタン、ボーダーに実装するテクニックのまとめを参考にした。 GoogleMapAPI ライブラリなしでGoogle MapをVue.jsで使う方法を参考にした。 レスポンシブデザイン PCで見るのとスマホで見るのとで幅が変わるのでデザインを変更した。 これも"レスポンシブデザイン"でググったら出てくるレベルで作成。(全然解説になってない) まとめ 以上が全貌。 結局何が難しいってフレームワークを決めることとデザイン自体の2つ。 この2点に関しては正解がないので常に最適解に近づけていく必要がある。 それ以外に関しては大体ググったら正解が手に入る。 あとは、デザインの勉強のためにWebサイト制作をしてみたが、 正直XDでお絵描きして終わりでもいいような気がする。 デザイン+実装するなると半日はかかるので、量産ができない。 いずれにしても、今後も架空のWebサイトデザインを作っていきたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ハンズオン】Nuxt.jsとLaravelとLINE Front-end Frameworkを使ってECサイトを作ろう!

はじめに 前回LINE Front-end Framework(LIFF)でお問い合わせフォームを作成しました。 今回は、Nuxt.jsとLaravelを使ってECサイトを作ってみます。 開発環境 今回は以下の技術を採用しています。 フロントエンド バックエンド インフラ サービス LIFF Laravel Docker HubSpot Nuxt.js Netlify Stripe Heroku MicroCMS Dockerは環境構築のために使用しています。 デプロイに関しては、フロントエンドはNetlifyを使い、バックエンドはHerokuを使用しています。 HubSpotは顧客管理、Stripeは決済、MicroCMSはECで販売する商品を登録するために使用しています。 本業でカスタマーサクセスをやっているのでやっぱり顧客管理はCRM使いたいよねってことでHubSpotを選択しています。 Stripeも最初はpayment-linksを使いたかったですが、コード書くならcheckoutでいいよねってことで使いませんでした。 payment-linksはどういうタイミングで使えばいいんでしょうね? MicroCMSは、わざわざ商品を登録するサイトを作るのがめんどくさかったので採用しました。 ECサイトを作る際にも普通に要件次第では使えるのではないかなーと感じました。 完成コード サイトURLはこちらです。 このサイトはLINEログインしないと使えないので、LINEログインをするためにLINEブラウザで開いてください。 ハンズオン! Githubからクローン GitHubにやり方は書いていますが、以下を実行することで環境構築ができます。 ターミナル $ git clone git@github.com:ssk9597/Docker-Laravel-Nuxt-Nginx-MySQL.git $ cd Docker-Laravel-Nuxt-Nginx-MySQL $ make nuxt $ make backend こちらは以下の記事を参考にしてください。 frontend/nuxt.config.jsに関してはこのままだとエラーが発生します。 なのでここだけは以下のコードをコピペしてください。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL } = 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: [], plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv'], env: { API_URL, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; NetlifyとHerokuにデプロイを行う Netlify 以下の記事で詳細まで説明されているのでこちらを見てもらえれば問題なくできるかと思います。 また、ステージング環境に関してもやっておいた方がいいです。 Heroku Herokuのデプロイは結構躓きがちなので自分が参考にした資料を載せておきます。 今回は複数のbuildpackが必要になるので以下を参考にしましょう。 ほとんどこちらの資料だけでできるはずです。 Papertrailでログも出しておきたいのでこれもやっておきましょう。 LIFFはフロント側でログを出すには、alertしか使えないですし、alertにオブジェクトなどが表示できないなど制限があります。 なので、Papertrailを使ってバックエンド側でログを出しておくのがいいかと思います。 最後に通信できているか確認しましょう。 以下の記事のAxiosの部分を追加しましょう。 これでHello World!が表示されればOKです。 サイト内で使用する画像を準備する 私は以下の2サイトから持ってきました。 まぁ正直なんでもいいです。 LINE Developersでチャネルを作成する 作成手順 ①プロバイダーの作成 ②新規チャネルの作成 ③LIFFの作成 プロバイダーを作成します。 プロバイダー名はご自身のお名前でいいかと思います。 新規チャネルを作成します。 LINEログインを選択してください。 設定を行います。 ウェブページにLINEログインを組み込むので、アプリタイプはウェブアプリにしておいてください。 ではLIFFアプリを追加しましょう! 基本情報の設定は以下のように行います。 これでLIFFアプリの作成ができました! Netlifyに.envを登録する 先ほど作成したLIFFアプリのチャネルIDとLIFF IDを.envに登録します。 それでは登録を行います。 LIFFアプリのタイトルを設定する サイトタイトルをつけますが、こちらは任意の名前でOKです。 私は、LIFF's Shopにしています。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL } = process.env; export default { target: 'static', head: { + title: "LIFF's Shop", 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' }], }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; LIFFアプリにLIFF SDKを組み込む npmパッケージもあるのですがうまくいかないことも多いのでSDKを使います。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL } = process.env; export default { target: 'static', head: { title: "LIFF's Shop", 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' }], + script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }], }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; .envのLIFF_IDなどをnuxt.config.jsで読み取れるようにする 先ほどNetlifyに登録したenvをフロントエンドで使えるようにしましょう! frontend/nuxt.config.js require('dotenv').config(); + const { API_URL, LIFF_ID, LIFF_CHANNEL_ID, MICROCMS_API_KEY } = process.env; export default { target: 'static', head: { title: "LIFF's Shop", 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' }], script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }], }, css: [{ src: '@/assets/styles/style.scss', lang: 'scss' }], styleResources: { scss: ['@/assets/styles/style.scss'], }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, LIFF_ID, + LIFF_CHANNEL_ID, + MICROCMS_API_KEY, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; リセットCSSを導入する いろいろ選択肢がありますが私は、css-wipeを使っています。 html5doctorのHTML5 Resetをベースに作られたスタイルシートです。 「box-sizing: border-box;」が定義されており、すべてのmargin, padding, borderが消されています。 導入方法はめちゃくちゃ簡単です。 ターミナル $ npm install css-wipe --save layoutのCSSに1行入れるだけでOKです。 frontend/layouts/default.vue <style lang="scss"> + @import 'css-wipe'; </style> エラー発生 ここまでをコミットした場合以下のエラーが発生するはずです。 発生しなかった人はそのままスルーで! Node Sass version 6.0.1 is incompatible with ^4.0.0 || ^5.0.0. 「node-sassのバージョン6はたけーぞ、4か5に下げろや」と怒られるはずなので、対応しましょう。 ターミナル $ npm uninstall -D node-sass $ npm install -D node-sass@5.0.0 これでOKです。 再度コミットしてください! 多分エラーは消えるはずです。 LIFFアプリを初期化する liff.init()メソッドを実行すると、LIFFアプリが初期化され、LIFFアプリからLIFF SDKのほかのメソッドを実行できるようになります。 アクセスした際に初期化されるようにしたいので、pages/index.vue内で読み込むことにしましょう。 frontend/pages/index.vue <template> <div> </div> </template> <script> export default { + async mounted() { + await liff.init({ + liffId: process.env.LIFF_ID, + }); + }, }; </script> まずはフロントエンド側のデザインをやっていきましょう! 完成予想図は以下のような感じです。 構成としては4つのブロックになっています。 ブロック ヘッダー ヒーロー 会員登録 商品 必要なコンポーネントです。 Atoms Molecules Template FailAlert Header Cancel Heading Hero Index LinkButton Registration Success SuccessAlert それでは必要なコンポーネントを作りつつ、そのコンポーネントをなぜ使用するのかも解説していきます。 ただその前に今回は変数定義用のscssファイルを作りましょう。 今回は使用する色に限りがあるのでこちらをscssで管理していきます。 frontend/assets/styles/utility/_color.scss $color_black: #2a2b2f; $color_white: #fff; $color_deep_gray: #979797; $color_gray: #bfbfbe; $color_yellow: #f5c500; $color_success: #4caf51; $color_fail: #e57472; frontend/assets/styles/style.scss @import "utility/color"; では最後にnuxt.config.jsに読み込みましょう。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL, LIFF_ID, LIFF_CHANNEL_ID } = process.env; export default { target: 'static', head: { title: "LIFF's Shop", 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' }], script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }], }, + css: [{ src: '@/assets/styles/style.scss', lang: 'scss' }], + styleResources: { + scss: ['@/assets/styles/style.scss'], + }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, LIFF_ID, LIFF_CHANNEL_ID, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; これでOKです。 それではコンポーネントの作成に移ります! ⚠️注意点 完成形のコードをすべて記載しました。 API側のやり取りなど解説は後ほど行うこととします。 なのでこちらはすべてコピペしていただければOKです。 FailAlert.vue なぜ作るのか こちらは会員登録に失敗したときに表示されるアラートです。 要件としては3秒間表示されて勝手に消えるようにします。 では作っていきます frontend/components/Atoms/FailAlert.vue <template> <div class="alert"> <p class="alert-title">ログインに失敗しました</p> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .alert { position: fixed; z-index: 100; background: $color_fail; width: 100%; max-width: 390px; height: 50px; display: flex; justify-content: center; align-items: center; &-title { font-size: 12px; color: $color_white; } } </style> Heading.vue なぜ作るのか 見出しとして使用します。 では作っていきます frontend/components/Atoms/Heading.vue <template> <div class="heading"> <h2 class="heading-main">{{ main }}</h2> <p class="heading-sub">{{ sub }}</p> </div> </template> <script> export default { props: { main: { type: String, required: true, }, sub: { type: String, required: true, }, }, }; </script> <style lang="scss" scoped> .heading { margin: 60px 0 30px 0; text-align: center; &-main { font-size: 32px; margin-bottom: 10px; } &-sub { font-size: 16px; } } </style> LinkButton.vue なぜ作るのか ページ移動を伴うボタンです。 TOPページに戻るために使用します。 では作っていきます frontend/components/Atoms/LinkButton.vue <template> <div class="button"> <nuxt-link to="/" class="button-btn">トップに戻る</nuxt-link> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .button { margin: 40px auto 0 auto; width: 200px; height: 40px; background: $color_yellow; border-color: transparent; &-btn { line-height: 40px; font-size: 18px; color: $color_black; text-decoration: none; font-weight: bold; } } </style> SuccessAlert.vue なぜ作るのか こちらは会員登録に成功したときに表示されるアラートです。 要件としては3秒間表示されて勝手に消えるようにします。 では作っていきます frontend/components/Atoms/SuccessAlert.vue <template> <div class="alert"> <p class="alert-title">ログインに成功しました</p> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .alert { position: fixed; z-index: 100; background: $color_success; width: 100%; max-width: 390px; height: 50px; display: flex; justify-content: center; align-items: center; &-title { font-size: 12px; color: $color_white; } } </style> これでAtomsは完成です。 次に、Moleculesを作成していきます。 Header.vue なぜ作るのか サイトのヘッダーデザインです。 では作っていきます frontend/components/Molecules/Header.vue <template> <header class="header"> <p class="header-title">LIFF's Shop</p> </header> </template> <script> export default {}; </script> <style lang="scss" scoped> .header { position: fixed; background: $color_black; width: 100%; max-width: 390px; height: 50px; display: flex; justify-content: center; align-items: center; &-title { font-size: 20px; color: $color_white; font-weight: bold; } } </style> Hero.vue なぜ作るのか サイトのヒーローデザインです。 では作っていきます frontend/components/Molecules/Hero.vue <template> <div> <div class="blank-space"></div> <div class="hero"> <img class="hero-img" src="@/assets/images/top.png" alt="ヒーロー" /> </div> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .blank-space { padding-top: 50px; } .hero { height: 300px; &-img { width: 390px; height: 300px; } } </style> Registration.vue なぜ作るのか 会員登録のデザインです。 こちらには会員登録したユーザーむけに、データの活用方法に関して記載しています。 こちらの記述をすることで、LIFFログインでメールアドレスを取得することができます。 では作っていきます frontend/components/Molecules/Registration.vue <template> <div class="registration"> <Heading :main="'Registration'" :sub="'会員登録'" /> <div class="registration-container"> <p class="registration-container-text"> 本Webサービスでは、ログイン時の認証画面にて許可を頂いた場合のみ、あなたのLINEアカウントに登録されているメールアドレスを取得します。 </p> <p class="registration-container-text"> 取得したメールアドレスは、HubSpotにユーザー登録するIDとして利用する目的以外では使用いたしません。また、法令に定められた場合を除き、第三者への提供はいたしません。 </p> <div class="registration-container-image"> <img src="@/assets/images/line.png" alt="LINEログイン" @click="childLineLogin" /> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { lineLogin: { type: Function, required: true, }, }, methods: { childLineLogin() { this.lineLogin(); }, }, }; </script> <style lang="scss" scoped> .registration-container { width: 250px; margin: 0 auto; background: $color_white; padding: 20px; &-text { font-size: 0.5rem; line-height: 1.5; padding-bottom: 10px; } &-image { padding-top: 20px; text-align: center; } } </style> LINEログインの時に「ユーザーのメールアドレス」を取得できるようにしましょう ということでここまでをgithubに、コミット、プッシュしましょう。 そしてこの情報取り扱いに関する記述をした部分をスクショしましょう! それでは、LINEにメールアドレス取得権限を付与してもらいましょう。 結構すぐに権限がもらえるはずです。 画像認識等をしているんでしょうね〜〜 「申請済み」になったらOKなので、スコープを付与しましょう。 これでLINEログインの際にメールアドレスが取得できるようになりました! これでMoleculesは完成です。 次に、Templateを作成していきます。 Cancel.vue なぜ作るのか Stripeの決済を行う際に、キャンセルページが必要なため作成します。 では作っていきます frontend/components/Template/Cancel.vue <template> <div class="container"> <Header /> <div class="message"> <p class="message-text">商品購入に失敗しました。</p> <p class="message-text">もう一度ご確認ください。</p> <LinkButton /> </div> </div> </template> <script> // components import Header from '@/components/Molecules/Header'; import LinkButton from '@/components/Atoms/LinkButton'; export default { components: { Header, LinkButton, }, }; </script> <style lang="scss"> .container { margin: 0 auto; background: $color_gray; height: 100vh; width: 100%; max-width: 390px; padding-bottom: 60px; } .message { padding-top: 70px; &-text { padding: 0 10px; line-height: 1.5; } } </style> では、キャンセルページの作成をしましょう。 frontend/pages/cancel.vue <template> <div> <Cancel /> </div> </template> <script> // components import Cancel from '@/components/Template/Cancel'; export default { components: { Cancel, }, }; </script> Index.vue なぜ作るのか TOPページです。 こちらでは、MicroCMSに登録した商品を受け取る処理を記載する必要があります。 なので、まずはMicroCMSの商品登録を行いましょう。 MicroCMSへ商品登録を行う まずはアカウントを作成しましょう。 作成方法に関することは公式サイトのブログがわかりやすいのでこちらを参考にされてください。 下記のようなスキーマを作成していきます。 Stripeの商品料金API IDは後ほど取得しますので今は適当な値を埋めておきましょう。 ということでこんな感じで3つ程度商品登録をしましょう! APIキーも必要になるので取得してNetlifyに登録しましょう。 では登録しましょう。 では作っていきます MicroCMSのデータを取得していきましょう。 asyncDataはpagesディレクトリでないとできないのでまずはpages/index.vueを作成します。 frontend/pages/index.vue <template> <div> <Index :products="products" /> </div> </template> <script> // components import Index from '@/components/Template/Index'; export default { components: { Index, }, + async asyncData({ $axios }) { + const products = await $axios.$get('https://liff-nuxt-laravel.microcms.io/api/v1/product', { + headers: { 'X-API-KEY': process.env.MICROCMS_API_KEY }, + }); + return { products }; }, data() { return { products: '', }; }, async mounted() { await liff.init({ liffId: process.env.LIFF_ID, }); }, }; </script> では、Templateを作成します。 frontend/components/Template/Index.vue <template> <div class="container"> <div v-if="isSuccess"> <SuccessAlert /> </div> <div v-if="isFail"> <FailAlert /> </div> <Header /> <Hero /> <Registration :lineLogin="lineLogin" /> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; import SuccessAlert from '@/components/Atoms/SuccessAlert'; import FailAlert from '@/components/Atoms/FailAlert'; import Header from '@/components/Molecules/Header'; import Hero from '@/components/Molecules/Hero'; import Registration from '@/components/Molecules/Registration'; export default { components: { Heading, SuccessAlert, FailAlert, Header, Hero, Registration, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', isSuccess: false, isFail: false, }; }, updated() { setTimeout(() => { this.isSuccess = false; }, 3000); setTimeout(() => { this.isFail = false; }, 3000); }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); if (this.idToken) { await this.$axios.$post('/hubspot/store', { idToken: this.idToken, }); this.isSuccess = true; } else { this.isFail = true; } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, async checkout(product) { const url = await this.$axios.$post('/stripe/store', { productStripePriceApi: product.stripe_price_api, idToken: this.idToken, }); if (url) { window.location.href = url; } }, }, }; </script> <style lang="scss"> .container { margin: 0 auto; background: $color_gray; width: 100%; max-width: 390px; padding-bottom: 60px; } .card { &-container { border-radius: 10px; background: $color_white; width: 250px; height: 300px; margin: 0 auto 30px auto; } &-product { &-img { border-radius: 10px 10px 0 0; width: 250px; height: 150px; } &-name { padding: 20px 0 0 20px; } &-price { padding: 20px 0 20px 20px; font-weight: bold; } } } .button { text-align: center; &-btn { display: inline-block; width: 200px; height: 40px; font-size: 18px; font-weight: bold; background: $color_yellow; border-color: transparent; &.disabled { color: $color_deep_gray; background: $color_gray; } } } </style> 本当はCardの部分もコンポーネント化してあげる方がいいのですが今回はデータのやり取りがめんどくさかったので諦めましたw Success.vue なぜ作るのか Stripeの決済を行う際に、サクセスページが必要なため作成します。 では作っていきます frontend/components/Template/Success.vue <template> <div class="container"> <Header /> <div class="message"> <p class="message-text">ご購入ありがとうございます!</p> <p class="message-text">ご不明点がありましたらお気軽にご連絡ください。</p> <LinkButton /> </div> </div> </template> <script> // components import Header from '@/components/Molecules/Header'; import LinkButton from '@/components/Atoms/LinkButton'; export default { components: { Header, LinkButton, }, }; </script> <style lang="scss"> .container { margin: 0 auto; background: $color_gray; height: 100vh; width: 100%; max-width: 390px; padding-bottom: 60px; } .message { padding-top: 70px; &-text { padding: 0 10px; line-height: 1.5; } } </style> では、サクセスページの作成をしましょう。 frontend/pages/success.vue <template> <div> <Success /> </div> </template> <script> // components import Success from '@/components/Template/Success'; export default { components: { Success, }, }; </script> これでフロントエンドの開発は終了です。 ではこの後はLINEログインをやっていきましょう。 LINEログインでプロフィールを取得する LIFFでのログインは、liff.getDecodedIDToken()が一番簡単です。 これで名前もメールアドレスもすべて取得することができます。 しかし、ユーザー情報をサーバーに送信しないでくださいとあるようにセキュリティの問題があります。 ということで、以下のように行います。 フロントエンド バックエンド ①IDトークンを取得 ②IDトークンをサーバーに渡す ③IDトークンを検証して、ユーザーのプロフィールを取得する ということでやっていきましょう。 ①IDトークンを取得 IDトークンの取得方法は簡単です。 frontend/components/Template/Index.vue <template> <div class="container"> <Registration :lineLogin="lineLogin" /> </div> </template> <script> // components import Registration from '@/components/Molecules/Registration'; export default { components: { Registration, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン + this.idToken = liff.getIDToken(); } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, }, }; </script> frontend/components/Molecules/Registration.vue <template> <div class="registration"> <Heading :main="'Registration'" :sub="'会員登録'" /> <div class="registration-container"> <p class="registration-container-text"> 本Webサービスでは、ログイン時の認証画面にて許可を頂いた場合のみ、あなたのLINEアカウントに登録されているメールアドレスを取得します。 </p> <p class="registration-container-text"> 取得したメールアドレスは、HubSpotにユーザー登録するIDとして利用する目的以外では使用いたしません。また、法令に定められた場合を除き、第三者への提供はいたしません。 </p> <div class="registration-container-image"> <img src="@/assets/images/line.png" alt="LINEログイン" @click="childLineLogin" /> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { lineLogin: { type: Function, required: true, }, }, methods: { childLineLogin() { this.lineLogin(); }, }, }; </script> ②IDトークンをサーバーに渡す 取得したIDトークンをLaravelに渡してあげます。 frontend/components/Template/Index.vue <template> <div class="container"> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); + if (this.idToken) { + await this.$axios.$post('/hubspot/store', { + idToken: this.idToken, + }); + } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, }, }; </script> ③IDトークンを検証して、ユーザーのプロフィールを取得する プロフィールを取得するには、ルーティングとコントローラーの2つが必要になります。 ルーティング api/routes/api.php <?php use Illuminate\Http\Request; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- */ Route::get("/", function () { return "Hello World!"; }); // HubSpotにユーザーを登録する + Route::post("/hubspot/store", "HubSpotController@store"); コントローラー コントローラーを作成しましょう。 ターミナル $ php artisan make:controller HubSpotController --resource また、Guzzleを使用するのでGuzzleをインストールしましょう。 ターミナル $ composer require guzzlehttp/guzzle 次に、.envを使うのでconfigからアクセスさせましょう。 env()を使ってはいけない理由などは以下の記事を見てみてください。 api/config/env.php <?php return [ "line_client_id" => env("LINE_CLIENT_ID"), ]; また、Herokuの.envの値も確認しておきましょう。 .envの値をコントローラー内で取得しましょう。 api/app/Http/Controllers/HubSpotController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use GuzzleHttp\Client; // Log use Illuminate\Support\Facades\Log; class HubSpotController extends Controller { public function store(Request $request) { try { // request $idToken = $request->input("idToken"); Log::info($idToken); // env + $clientID = config("env.line_client_id"); Log::info($clientID); } catch (\GuzzleHttp\Exception\BadResponseException $e) { Log::info($e); return $e->getResponse()->getBody()->getContents(); } } } では、IDトークンを検証してユーザーのプロフィールとメールアドレスを取得しましょう! api/app/Http/Controllers/HubSpotController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use GuzzleHttp\Client; // Log use Illuminate\Support\Facades\Log; class HubSpotController extends Controller { public function store(Request $request) { try { // request $idToken = $request->input("idToken"); Log::info($idToken); // env $clientID = config("env.line_client_id"); Log::info($clientID); // guzzle + $client = new Client(); + $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ + "form_params" => [ + "id_token" => $idToken, + "client_id" => $clientID + ] + ]); } catch (\GuzzleHttp\Exception\BadResponseException $e) { Log::info($e); return $e->getResponse()->getBody()->getContents(); } } } では次にこの取得した名前やメールアドレスをHubSpotに登録させましょう。 まずはHubSpotのAPIを取得しましょう。 このAPIをHerokuの.envに入力しましょう。 次に、.envを使うのでconfigからアクセスさせましょう。 api/config/env.php <?php return [ "line_client_id" => env("LINE_CLIENT_ID"), + "hubspot_api_key" => env("HUBSPOT_API_KEY"), ]; 最後にコントローラーを作成します。 LINEログイン情報を使用してHubSpotのコンタクトを作成するので、POST /crm/v3/objects/contactsを使います。 今回は、cURLコマンドで作成します。 api/app/Http/Controllers/HubSpotController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use GuzzleHttp\Client; // Log use Illuminate\Support\Facades\Log; class HubSpotController extends Controller { public function store(Request $request) { try { // request $idToken = $request->input("idToken"); // env $clientID = config("env.line_client_id"); + $HubSpotApiKey = config("env.hubspot_api_key"); // guzzle $client = new Client(); $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ "form_params" => [ "id_token" => $idToken, "client_id" => $clientID ] ]); // HubSpot Value + $profile = json_decode($response->getBody()->getContents(), true); + $profileName = $profile["name"]; + $profileEmail = $profile["email"]; // Log::info($profile["name"]); // Log::info($profile["email"]); // Register HubSpot + $data = array( + "properties" => [ + "email" => $profileEmail, + "firstname" => $profileName + ] + ); + $formParams = json_encode($data, JSON_UNESCAPED_UNICODE); // Log::info($formParams); // cURL + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.hubapi.com/crm/v3/objects/contacts?hapikey=" . $HubSpotApiKey, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => $formParams, + CURLOPT_HTTPHEADER => array( + "accept: application/json", + "content-type: application/json" + ), + )); + $response = curl_exec($curl); + $err = curl_error($curl); + curl_close($curl); + if ($err) { + Log::info($err); + } else { + Log::info($response); + } } catch (\GuzzleHttp\Exception\BadResponseException $e) { Log::info($e); return $e->getResponse()->getBody()->getContents(); } } } これでHubSpotにユーザー登録が可能になりました。 それでは最後にStripeを使って決済を行いましょう。 Stripeで決済を作成する Stripeで決済を作成するには以下のように進めていきます。 フロントエンド バックエンド サービス ①Stripeに商品登録を行う ②StripeのAPIを取得する ③IDトークンとStripeの商品IDをバックエンドに渡す ④IDトークンを検証して、ユーザーのプロフィールを取得する ⑤Stripeで決済処理を行う ⑥決済画面をフロントエンドに渡す ⑦決済画面へページ移動させる ①Stripeに商品登録を行う 「商品を追加」から商品を登録します。 商品詳細ページにAPI IDがあるのでこちらをMicro CMSに入力しましょう。 ②StripeのAPIを取得する APIが取得できたらこれをHerokuに登録します。 これでOKです! ③IDトークンとStripeの商品IDをバックエンドに渡す frontend/components/Template/Index.vue <template> <div class="container"> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); if (this.idToken) { await this.$axios.$post('/hubspot/store', { idToken: this.idToken, }); } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, + async checkout(product) { + const url = await this.$axios.$post('/stripe/store', { + productStripePriceApi: product.stripe_price_api, + idToken: this.idToken, + }); + }, }, }; </script> ④IDトークンを検証して、ユーザーのプロフィールを取得する これは先ほどやったことと全く同じですね。 まずはルーティングから作成しましょう。 api/routes/api.php <?php use Illuminate\Http\Request; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- */ Route::get("/", function () { return "Hello World!"; }); // HubSpotにユーザーを登録する Route::post("/hubspot/store", "HubSpotController@store"); // Stripeを使って決済を行う + Route::post("/stripe/store", "PayloadController@store"); では次にコントローラーの作成をしましょう。 ターミナル $ php artisan make:controller PayloadController --resource コントローラーの記述をしていきます。 api/app/Http/Controllers/PayloadController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; // guzzle use GuzzleHttp\Client; class PayloadController extends Controller { public function store(Request $request) { // line + $idToken = $request->input("idToken"); // env + $clientID = config("env.line_client_id"); // guzzle + $client = new Client(); + $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ + "form_params" => [ + "id_token" => $idToken, + "client_id" => $clientID + ] + ]); + $profile = json_decode($response->getBody()->getContents(), true); + $profileEmail = $profile["email"]; } } ⑤Stripeで決済処理を行う Stripeで決済処理を行うには、checkoutを使用します。 公式サイトに具体的なやり方が書いてあるのでこちらに倣ってやっていきます。 まずはライブラリをインストールします ターミナル $ composer require stripe/stripe-php checkoutセッションを作成する api/app/Http/Controllers/PayloadController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; // stripe use Stripe\Stripe; use Stripe\Checkout\Session; // guzzle use GuzzleHttp\Client; class PayloadController extends Controller { public function store(Request $request) { // $request // line $idToken = $request->input("idToken"); // product + $productStripePriceApi = $request->input("productStripePriceApi"); // env $clientID = config("env.line_client_id"); + $stripeApiKey = config("env.stripe_api_key"); // guzzle $client = new Client(); $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ "form_params" => [ "id_token" => $idToken, "client_id" => $clientID ] ]); $profile = json_decode($response->getBody()->getContents(), true); $profileEmail = $profile["email"]; // API + Stripe::setApiKey($stripeApiKey); // header + header('Content-Type: application/json'); // domain + $domain = "https://liff-nuxt-laravel-ec.netlify.app"; // checkout + $checkout_session = Session::create([ + 'customer_email' => $profileEmail, + 'payment_method_types' => ['card'], + 'line_items' => [[ + 'price' => $productStripePriceApi, + 'quantity' => 1, + ]], + 'mode' => 'payment', + 'success_url' => $domain . '/success', + 'cancel_url' => $domain . '/cancel', + ]); } } ⑥決済画面をフロントエンドに渡す 公式サイトではリダイレクト処理で決済画面へ移動させていますが、 ページ移動などはフロントエンドで処理したいのでURLをフロントエンド側に渡しましょう。 api/app/Http/Controllers/PayloadController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; // stripe use Stripe\Stripe; use Stripe\Checkout\Session; // guzzle use GuzzleHttp\Client; class PayloadController extends Controller { public function store(Request $request) { // $request // line $idToken = $request->input("idToken"); // product $productStripePriceApi = $request->input("productStripePriceApi"); // env $clientID = config("env.line_client_id"); $stripeApiKey = config("env.stripe_api_key"); // guzzle $client = new Client(); $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ "form_params" => [ "id_token" => $idToken, "client_id" => $clientID ] ]); $profile = json_decode($response->getBody()->getContents(), true); $profileEmail = $profile["email"]; // API Stripe::setApiKey($stripeApiKey); // header header('Content-Type: application/json'); // domain $domain = "https://liff-nuxt-laravel-ec.netlify.app"; // checkout $checkout_session = Session::create([ 'customer_email' => $profileEmail, 'payment_method_types' => ['card'], 'line_items' => [[ 'price' => $productStripePriceApi, 'quantity' => 1, ]], 'mode' => 'payment', 'success_url' => $domain . '/success', 'cancel_url' => $domain . '/cancel', ]); // return redirect($checkout_session->url, 303, [], true); + return $checkout_session->url; } } ⑦決済画面へページ移動させる これで決済画面ページへ移動されます。 frontend/components/Template/Index.vue <template> <div class="container"> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); if (this.idToken) { await this.$axios.$post('/hubspot/store', { idToken: this.idToken, }); } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, async checkout(product) { const url = await this.$axios.$post('/stripe/store', { productStripePriceApi: product.stripe_price_api, idToken: this.idToken, }); + if (url) { + window.location.href = url; + } }, }, }; </script> これで完成です! お疲れ様でした^ ^ 終わりに せっかくCRMのHubSpotを入れたので、商品購入のログなども残せるようにしたいなーと思います。 この辺も今後チャレンジしていきたいと思います。 また、HubSpotにはLINE接続するアプリもあるので同様の機能が自分自身で準備できるかも試してみたいです。 ここまで読んでいただきありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel + Vue.js講座】いいね機能を作ろう!!をlaravel8+vue-router+xamppで作ってみた♡

【Laravel + Vue.js講座】いいね機能を作ろう!!を手本にして作りました。 講座ではlaravel5で作成していましたので私はlaravel8とxamppとvue-routerで作ってみた。 作成する機能は講座とほとんど同じです。v-routerでの投稿機能(CRUD)は下記のリンクを参考にしています。 認証機能 投稿機能(CRUD) イイね機能(1つの投稿にユーザーは1回(toggle)いいねが可能) 目的は laravel8+vue+vue-route+xamppの備忘記録 ◎プロジェクトの作成 laravel new riku_larabel_vue_good_btn composer require laravel/ui php artisan ui vue --auth npm install && npm run dev 再度 npm run dev 確認 localhostにアクセスして、loginとregister画面の確認 vue-routeのインストール npm install vue-router npm run dev ◎asset関数からmix関数へ変更する そうすることで効率的に作成できるみたい。 ①.resources\views\layouts\app.blade.phpファイルを編集 resources\views\layouts\app.blade.php <!-- Scripts --> {{-- コメントアウト<script src="{{ asset('js/app.js') }}" defer></script> --}} <script src="{{ mix('js/app.js') }}" defer></script> <!-- Styles --> {{-- コメントアウト<link href="{{ asset('css/app.css') }}" rel="stylesheet"> --}} <link href="{{ mix('css/app.css') }}" rel="stylesheet"> 再度確認するとスタイル等が崩れるている localhostにアクセスして、loginとregister画面の確認してみると崩れているのを確認。 ②.mix_urlを追記する。config/app.phpに追加(asset_urlの直後がいいのでは) config\app.php 'mix_url' => env('MIX_ASSET_URL', 'http://localhost/riku_larabel_vue_good_btn/public/'), 確認するとスタイル等があたっている。 localhost ◎webpack.mix.jsファイルの編集 .sourceMaps().version()を追加する。 webpack.mix.js mix.js('resources/js/app.js', 'public/js') .vue() .sass('resources/sass/app.scss', 'public/css') .sourceMaps() .version(); npm run dev .sourceMaps()で下記の警報が消える DevTools failed to load source map: Could not load content for http://localhost/riku_larabel_vue_good_btn/public/js/popper.js.map: HTTP error: status code 404, net::ERR_HTTP_RESPONSE_CODE_FAILURE .version() でmixの変更が反映されないのを防ぐ!!これがためにmix関数を使っている。 ◎データーベースの作成   http://localhost/phpmyadmin/index.php 1.データーベース名はプロジェクト名と同じ   riku_larabel_vue_good_btn 2.文字コードは   utf8mb4_general_ci envの設定 変更なし。 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=riku_larabel_vue_good_btn DB_USERNAME=root DB_PASSWORD= ◎モデルとコントローラーとテーブルの作成 ①PostとLikeモデルを作成する。 php artisan make:model Post -a php artisan make:model Like -a ②postsとlikesマイグレーションファイルの編集 postsテーブルのマイグレーションの編集 database\migrations\2021_xx_xx_xxxxx_create_posts_table.php public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained(); $table->string('content')->nullable(); $table->timestamps(); }); } likesテーブルのマイグレーションの編集 database\migrations\2021_xx_xx_xxxxx_create_likes_table.php public function up() { Schema::create('likes', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onUpdate('cascade')->onDelete('cascade'); $table->foreignId('post_id')->constrained()->onUpdate('cascade')->onDelete('cascade'); $table->unique(['user_id', 'post_id']); $table->timestamps(); }); } 独り言:メモ likesテーブルはpostsとusersテーブルの多対多の関係にあるので、中間テーブルとしてpost_userテーブルでも良いと思ったがとんだ勘違いだった。 postにuserタグを貼り付けるとも言えるが、postにはすでにuserの外部キーがあるし・・余計複雑怪奇になるわ。 usersとpostsとの関係はあくまでも1対多の関係であるため、likesテーブルがusersとpostsの多対多の関係にならないし、なんの関係もない。と思う。 ③モデルの編集 app\Models\Post.php protected $fillable = [ 'user_id','content', ]; public function likes() { return $this->hasMany(Like::class); } app\Models\Like.php protected $fillable = [ 'user_id','post_id', ]; ④migrateの実行 php artisan migrate ◎ルート構成 ・認証機能等はlaravelのデフォルト ・ポストのCRUDはvue-router(index,create,edit) ・postsテーブルとの通信はaxiosを使用 ・likesテーブルとの通信もaxiosで使用。 routes\web.php use App\Http\Controllers\PostController;//忘れずに Auth::routes(); Route::get('/', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); //laravel用のルート Route::get('/posts', function () { return view('posts.index'); })->name('posts.index')->middleware('auth'); //vue用のルート Route::get('posts/{any}', function () { return view('posts.index'); })->where('any', '.*')->middleware('auth'); //axiosでの通信用のルート 外部通信は不要なのでweb.phpに記述 Route::prefix('api')->as('api')->group(function () { Route::resource('posts', PostController::class); Route::post('posts/{post}/like', [PostController::class, 'like']); Route::post('posts/{post}/unlike', [PostController::class, 'unlike']); }); ログイン遷移先レジスター遷移先を変更する。 app\Providers\RouteServiceProvider.phpの編集 RouteServiceProvider.php public const HOME = '/'; ナビにポスト画面へのリンクを作成する。 resources\views\layouts\app.blade.phpの編集 resources\views\layouts\app.blade.php <ul class="navbar-nav ml-auto"> <!-- Authentication Links --> @guest //省略 @if (Route::has('register')) -------ここから------ <li class="nav-item"> <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a> </li> ------ここまでコピー--@endifのすぐ上------ @endif @else -------@elseのすぐ下に貼り付けて編集------    <li class="nav-item">     <a class="nav-link" href="{{ route('posts.index') }}">Post</a>    </li> //省略 @endguest ◎簡単にvueの動作確認をしておく。 ①PostのIndexファイルの作成 resources\views\posts\index.blade.phpを作成する。 home.blade.phpをコピーして編集。 resources\views\posts\index.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Post Index</div> <div class="card-body"> {{-- デフォルトのvueコンポネントで動作確認 --}} <example-component></example-component> </div> </div> </div> </div> </div> @endsection vueコンポーネントが描写されているか確認 localhost 描写されているのが確認できたらとりあえず npm run watch して、 resources\js\components\ExampleComponent.vueを適当に編集して 画面が変更されるかを再度確認する。 vueコンポーネントの編集が反映されていることを確認 ◎xamppでのvue-routerの動作確認もしておく。 ①index.blade.phpの編集 resources\views\posts\index.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Post Index</div> <div class="card-body"> {{-- デフォルトのvueコンポネントで動作確認 --}} {{-- コメントアウト<example-component></example-component> --}} <router-view /> {{-- 追加--}} </div> </div> </div> </div> </div> @endsection ②resources\js\app.jsの編集 resources\js\app.js //ルーティングは別ファイルに記述する方法でやる。 import router from "./router"; //<---追加 require('./bootstrap'); window.Vue = require('vue').default; Vue.component('example-component', require('./components/ExampleComponent.vue').default); const app = new Vue({ el: '#app', router: router, //<---追加 }); ③resources\js\router.jsファイルを作成する resources\js\router.js import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); import index from "./components/index.vue"; import create from "./components/create.vue"; const router = new VueRouter({ mode: "history", //baseの設定を忘れずに、xampp対応。 base:'/riku_larabel_vue_good_btn/public/posts/', routes: [ { path: "", name: "index", component: index }, { path: "/create", name: "create", component: create }, ] }); export default router; ④Vueコンポーネントファイルの作成 1.resources\js\components\ index.vueの作成 resources\js\components\index.vue <template> <div> <h1>POST INDEX</h1> </div> </template> 2.resources\js\components\ create.vueの作成 resources\js\components\create.vue <template> <div> <h1>POST CREATE</h1> </div> </template> ⑤webpack.mix.jsの編集 webpack.mix.js mix.js('resources/js/app.js', 'public/js') .vue() .js("resources/js/router.js", "public/js") //追加 .sass('resources/sass/app.scss', 'public/css') .sourceMaps() .version(); npm run dev npm run watch vue-routerの動作確認 描写とルーティングの確認 post.index post.create ◎PostのCRUD機能の実装(取得と登録編) ①index.vueを編集 ・新規投稿画面へのリンク ・postsテーブルを取得してテーブルで表示 ・詳細画面へのリンク ・編集画面へのリンクボタン ・削除ボタン ・投稿ごとのlikes数の取得(これはCRUDの後で実装) resources\js\components\index.vue <template> <div> <router-link :to="{ name: 'create' }">新規追加</router-link> <table class="table table-bordered text-center table-sm" style="table-layout: fixed"> <thead> <tr> <th class="col-1" scope="col">ID</th> <th class="col-7" scope="col">本文</th> <th class="col-1" scope="col">?</th> <th class="col-2" scope="col">action</th> </tr> </thead> <tbody> <tr v-for="post in posts" :key="post.id"> <th scope="row"> <router-link :to="{ name: 'show', params: { id: post.id } }">{{ post.id }}</router-link> </th> <td class="text-truncate">{{ post.content }}</td> <td class="text-truncate">{{ post.likes_count }}</td> <td class="d-flex justify-content-around"> <router-link class="btn btn-secondary btn-sm" :to="{ name: 'edit', params: { id: post.id } }" >編集</router-link > <button class="btn btn-danger btn-sm" @click="postDstroy(post.id)"> 削除 </button> </td> </tr> </tbody> </table> </div> </template> <script> export default { data() { return { message: "", posts: {}, }; }, created: function () { this.getPost(); }, methods: { getPost() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/") .then((response) => { console.log(response.data.posts); this.posts = response.data.posts; }) .catch((error) => { this.message = error; }); }, postDstroy(id) { axios .delete("/riku_larabel_vue_good_btn/public/api/posts/" + id) .then((response) => { this.getPost(); this.message = ""; }) .catch((error) => { this.message = error; }); }, }, }; </script> ②create.vueを編集 エラー確認のためformにnovalidateを貼っている。 resources\js\components\create.vue <template> <div> <router-link :to="{ name: 'index' }">トップ</router-link> <form @submit.prevent="postStore" novalidate> <div class="form-group row"> <label for="content" class="col-12 col-form-label"> Exsample Post </label> <div class="col-12"> <textarea id="content" type="text" class="form-control" v-bind:class="{ 'is-invalid': isInvalid }" name="content" v-model="content" required rows="5" ></textarea> <p class="text-danger" v-if="message">{{ message }}</p> </div> </div> <div class="form-group row mb-0"> <div class="col-md-8 offset-md-4 text-right"> <button type="submit" class="btn btn-primary">投稿</button> </div> </div> </form> </div> </template> <script> export default { props: ["userId"], data() { return { message: "", posts: {}, content: "", isInvalid:false, }; }, methods: { postStore() { axios .post("/riku_larabel_vue_good_btn/public/api/posts", { content: this.content, user_id: this.userId, }) .then((response) => { console.log(response.data); this.$router.push({ name: "index" }); }) .catch(({ response }) => { console.log(response.data.errors.content); this.isInvalid = true; this.message = response.data.errors.content[0]; }); }, }, }; </script> ③ポストコントローラーの編集 app\Http\Controllers\PostController.phpの編集 indexメソッドとstoreメソッドの作成 app\Http\Controllers\PostController.php public function index(Request $request) { //if ($request->ajax())の判定には // headers: { "X-Requested-With": "XMLHttpRequest" },が必 if ($request->ajax()) { $posts = Post::all(); return response()->json(['posts' => $posts]); } else { return view('posts.index'); } } public function store(PostRequest $request) { Post::create([ 'content'=>$request->content, 'user_id'=>$request->user_id, ]); } ④バリデーションファイルの作成 php artisan make:request PostRequest app\Http\Requests\PostRequest.phpの編集 app\Http\Requests\PostRequest.php <?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class PostRequest extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'content' => 'required|max:255', 'user_id' => 'required|integer', ]; } } PostControllerに追記 app\Http\Controllers\PostController.php use App\Http\Requests\PostRequest; ⑤index.blade.phpの編集 vue-routerへAuth::id()データーを渡してやる resources\views\posts\index.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Post Index</div> <div class="card-body"> {{-- デフォルトのvueコンポネントで動作確認 --}} {{-- コメントアウト<example-component></example-component> --}} <router-view :user-id="{{ json_encode(Auth::id()) }}"/>{{-- 編集--}} </div> </div> </div> </div> </div> @endsection ⑥axios通信のための設定を追加するためresources\js\app.jsの編集 resources\js\app.jsに追記 resources\js\app.js //ルーティングは別ファイルに記述する方法でやる。 import router from "./router"; //<↓axios通信のための設定> window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' const token = document.head.querySelector('meta[name="csrf-token"]') if (token) { window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content } //<↑/axios通信のための設定> require('./bootstrap'); window.Vue = require('vue').default; Vue.component('example-component', require('./components/ExampleComponent.vue').default); const app = new Vue({ el: '#app', router: router, }); index画面、create画面での表示と登録機能の確認 create画面で投稿してindex画面での表示を確認 また、バリデーションが機能しているか確認 posts.create ◎PostのCRUD機能の実装(削除機能と詳細表示と編集機能 編) ①詳細表示の作成 resources\js\components\show.vueの作成 resources\js\components\show.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td>{{ post.content }}</td> </tr> </table> </div> </template> <script> export default { data() { return { message: "", post: {}, }; }, created() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id) .then(response => (this.post = response.data)) .catch(erorr => console.log(error)); }, methods: { }, }; </script> ②編集画面の作成 resources\js\components\edit.vueの作成 resources\js\components\edit.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td><textarea class="w-100" rows="5" v-model="post.content">{{ post.content }}</textarea></td> </tr> </table> <div class="text-right mt-3"> <button class="btn btn-success" v-on:click="PostUpdate">更新</button> <button class="btn btn-danger" v-on:click="updateCancel">キャンセル</button> </div> </div> </template> <script> export default { props:['userId'], data() { return { message: "", post: {}, }; }, created() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id) .then(response => (this.post = response.data)) .catch(erorr => console.log(error)); }, methods: { updateCancel() { this.$router.push({ name: "index" }); }, PostUpdate() { axios .put("/riku_larabel_vue_good_btn/public/api/posts/" + this.post.id, { content: this.post.content, user_id: this.userId, }) .then(response => { this.$router.push({ name: "index" }); }) .catch(erorr => { this.message = erorr; }); } }, }; </script> ③Vue-routerの追加 resources\js\router.jsの編集 resources\js\router.js import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); import index from "./components/index.vue"; import create from "./components/create.vue"; //<↓追加> import show from "./components/show.vue"; import edit from "./components/edit.vue"; //<↑/追加> const router = new VueRouter({ mode: "history", //baseの設定を忘れずに、xampp対応です。 base:'/riku_larabel_vue_good_btn/public/posts/', routes: [ { path: "", name: "index", component: index }, { path: "/create", name: "create", props: true, component: create }, //<↓追加> { path: "/:id", name: "show", component: show }, { path: "/:id/edit", name: "edit", component: edit } //<↑/追加> ] }); export default router; ④destroyメソッドとshowメソッドとupdateメソッドの追加 app\Http\Controllers\PostController.php public function show($id) { return Post::find($id); } public function update(Request $request,$id) { $update = [ 'content'=>$request->content, 'user_id'=>$request->user_id, ]; Post::where('id', $id)->update($update); } public function destroy($id) { Post::where('id', $id)->delete(); } PostのCRUDの最終確認 ①ポストして posts.create ②インデックス画面で一覧表示の確認 posts.index ③詳細画面で詳細確認 posts.show ④編集画面で編集投稿 posts.edit ⑤インデックス画面で削除を実行 posts.index ◎いいね機能を実装する ①イイねボタンのコンポーネントを作成する resources\js\components\like.vueの作成 resources\js\components\like.vue <template> <div class=""> <button type="button" class="btn" :class="liked?'btn-danger':'btn-outline-danger'" :disabled="processing" @click="like" @mouseleave="mouseLeaveAction" > いいね♡{{ likeCount }} </button> </div> </template> <script> export default { props: ["postId", "userId", "defaultLiked", "defaultCount"], data() { return { changed: false, processing: false, liked: false, likeCount: 0, }; }, watch: { defaultLiked: { handler() { if (!this.defaultLiked) return; this.liked = this.defaultLiked; }, }, defaultCount: { handler() { if (!this.defaultCount) return; this.likeCount = this.defaultCount; }, }, }, methods: { like() { this.changed = !this.changed; if ((this.liked = !this.liked)) { this.likeCount += 1; } else { this.likeCount -= 1; } this.$emit('love', this.likeCount) }, mouseLeaveAction() { if (!this.changed) return; if (this.processing) return; this.processing = true; this.changed = false; let url = this.liked ? `/riku_larabel_vue_good_btn/public/api/posts/${this.postId}/like` : `/riku_larabel_vue_good_btn/public/api/posts/${this.postId}/unlike`; axios .post(url, { user_id: this.userId, }) .then((response) => { this.likeCount = response.data.likeCount; }) .catch((error) => { console.log(error); }) .finally(() => { this.processing = false; }); }, }, }; </script> ②app.jsにlikeコンポーネントを登録する resources\js\app.jsに追記 resources\js\app.js import router from "./router"; require('./bootstrap'); window.Vue = require('vue').default; //<↓axios通信のための設定> window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' const token = document.head.querySelector('meta[name="csrf-token"]') if (token) { window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content } //<↑/axios通信のための設定> Vue.component('example-component', require('./components/ExampleComponent.vue').default); Vue.component('like', require('./components/like.vue').default);//追加 const app = new Vue({ el: '#app', router: router, }); ②showコンポーネントで表示させるため、show.vueに追記。 resources\js\components\show.vueの編集 resources\js\components\show.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td>{{ post.content }}</td> </tr> </table> <!-- ↓追加 --> <div class="text-right mt-3"> <like @love="updateLikeCount" :post-id="post.id" :user-id="userId" :defaultLiked="defaultLiked" :defaultCount="post.likes_count" > </like> </div> <!-- /↑追加 --> </div> </template> <script> export default { props: ["userId"], //追加 data() { return { message: "", defaultLiked: "", //追加 post: {}, }; }, created() { const queries = { user_id: this.userId }; //追加 axios //変更 .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id, { params: queries, }) //変更 .then((response) => { console.log(response.data.defaultLiked); this.post = response.data.post; this.defaultLiked = response.data.defaultLiked; }) .catch(erorr => console.log(error)); }, methods: { //追加 updateLikeCount(likeCount) { this.post.likes_count = likeCount; }, }, }; </script> ③ポストコントローラーを編集する indexメソッドとshowメソッドの再修正 likeメソッドとunlikeメソッドの追加 PostControllerファイルの全記述 app\Http\Controllers\PostController.php <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; use App\Http\Requests\PostRequest; use App\Models\Like; class PostController extends Controller { public function __construct() { } public function index(Request $request) { //if ($request->ajax())の判定には // headers: { "X-Requested-With": "XMLHttpRequest" },が必 if ($request->ajax()) { $posts = Post::withCount('likes') ->orderBy('likes_count','desc')->get(); return response()->json(['posts' => $posts]); } else { return view('posts.index'); } } // public function create() // { // // // } public function store(PostRequest $request) { Post::create([ 'content'=>$request->content, 'user_id'=>$request->user_id, ]); } public function show(Request $request ,$id) { $post = Post::find($id); $post->loadCount('likes'); $defaultLiked =Like::where('post_id',$post->id)->where('user_id',$request->user_id)->exists(); return response()->json(['post'=>$post,'defaultLiked'=>$defaultLiked]); } public function update(Request $request,$id) { $update = [ 'content'=>$request->content, 'user_id'=>$request->user_id, ]; Post::where('id', $id)->update($update); } public function destroy($id) { Post::where('id', $id)->delete(); } public function like(Post $post, Request $request) { $like = Like::create(['post_id' => $post->id, 'user_id' => $request->user_id]); $likeCount = Like::where('post_id', $post->id)->count(); return response()->json(['likeCount' => $likeCount]); } public function unlike(Post $post, Request $request) { $like = Like::where('user_id', $request->user_id)->where('post_id', $post->id)->first(); $like->delete(); $likeCount = Like::where('post_id', $post->id)->count(); return response()->json(['likeCount' => $likeCount]); } } ④edit.vueの再編集(showメッドの返り値を変更したため) resources\js\components\edit.vue resources\js\components\edit.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td><textarea class="w-100" rows="5" v-model="post.content">{{ post.content }}</textarea></td> </tr> </table> <div class="text-right mt-3"> <button class="btn btn-success" v-on:click="PostUpdate">更新</button> <button class="btn btn-danger" v-on:click="updateCancel">キャンセル</button> </div> </div> </template> <script> export default { props:['userId'], data() { return { message: "", post: {}, }; }, created() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id) .then(response => (this.post = response.data.post)) //修正 .catch(erorr => console.log(error)); }, methods: { updateCancel() { this.$router.push({ name: "index" }); }, PostUpdate() { axios .put("/riku_larabel_vue_good_btn/public/api/posts/" + this.post.id, { content: this.post.content, user_id: this.userId, }) .then(response => { this.$router.push({ name: "index" }); }) .catch(erorr => { this.message = erorr; }); } }, }; </script> ◎機能の最終確認 posts.index 以上 終わり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む