Youtube Data API を使って動画を取得する
作ったもの
フォームに入力したキーワードに一致するYoutube動画を表示します。
APIキーの取得
Google Cloud PlatformからYoutube Data API v3 のキーを取得します。
参考記事
APIキーを登録
.envファイルに取得したAPIキーを記述します。 今回はNext.jsを使っています。
# youtube api key NEXT_PUBLIC_YOUTUBE_API_KEY=○○○○○○○○○○○
フォーム作成
検索キーワードを入力するフォームを作成します
<form onSubmit={handleSubmit}> <input type="text" name="keyword" value={keyword} onChange={handleInputKeyword} /> </form>
動画取得
APIを実行して動画のデータを取得します。今回は再生回数が多い順に30件取得しています。その他のプロパティは下記ドキュメントを参考にしてください。
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() await axios .get( `https://www.googleapis.com/youtube/v3/search?type=video&part=snippet&q=${keyword}&maxResults=30&order=viewCount&key=${process.env.NEXT_PUBLIC_YOUTUBE_API_KEY}` ) .then((res) => { setVideoItems(res.data.items) }) .catch((err) => { console.log(err) }) }
レスポンス内容は下記のようになります。
{ "kind": "youtube#searchResult", "etag": "kDTRucohFNe7n0uBO5psgi89I0U", "id": { "kind": "youtube#video", "videoId": "YZia5Z-oycQ" }, "snippet": { "publishedAt": "2014-04-16T00:43:46Z", "channelId": "UCS_dOTlDQhzGR2VLASzVo4g", "title": "お腹が空いたと鳴くマンチカン子猫! The Munchkin cat meowing.So cute kitten!", "description": "baby #猫 #meow 食事の支度を始めると、キッチンを金網越しに覗いて、ニャーニャーとご飯の要求をし続けている白い子猫と、一緒に鳴いてる他のねこたち。5匹とも ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/YZia5Z-oycQ/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/YZia5Z-oycQ/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/YZia5Z-oycQ/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Cynthia Moon短足だってイイじゃん", "liveBroadcastContent": "none", "publishTime": "2014-04-16T00:43:46Z" } }
動画を表示する
取得した動画データを元にUIを構築します。
<table> <tbody> {videoItems.map((video, i) => ( <tr key={i}> <td> <Image src={video.snippet.thumbnails.medium.url} alt="thumbnailsUrl" height={video.snippet.thumbnails.medium.height} width={video.snippet.thumbnails.medium.width} /> </td> <td> <strong> <a href={`https://www.youtube.com/watch?v=${video.id.videoId}`} target="_blank" rel="noopener noreferrer" > {video.snippet.title} </a> </strong> {video.snippet.channelTitle} <br /> <span> {video.snippet.description} </span> </td> </tr> ))} </tbody> </table>
おわりに
Youtube Data API を使って動画のデータを取得しました。 検索プロパティがまだたくさんあるので他にも色々できそうです。
Vercel にホスティングしているサイトに Basic 認証をつける
はじめに
Vercelにホスティングしているサイト(create-react-app)にBasic認証をつける時に、 調べた事をメモしていきます。
下記サイトを参考にさせていただきました。
Vercel で Basic 認証付きのプレビュー環境を作る - Webdelog
Next.js + Vercel で Basic 認証をかける
manifest.jsonはBasic認証がかかっていると通らない | 怪しい物を開発するブログ
実装
static-auth と safe-compare をインストールする
npm i static-auth
npm i safe-compare
server.js を作成
こちらは参考にさせていただいたサイトのコードを使用させていただきました。
const protect = require('static-auth') const safeCompare = require('safe-compare') const USER_NAME = process.env.USER_NAME || 'admin' const PASSWORD = process.env.PASSWORD || 'admin' const server = protect( '/', (username, password) => safeCompare(username, USER_NAME) && safeCompare(password, PASSWORD), { directory: `${__dirname}/build`, onAuthFailed: (res) => { res.end('Authentication failed') }, } ) module.exports = server
vercel.json を作成
{ "builds": [ { "src": "server.js", "use": "@vercel/node" } ], "routes": [ { "src": "/.*", "dest": "server.js" } ] }
デプロイ
下記実行してサイトを確認する
vercel --prod
エラー
Basic 認証をつけるとmanifest.json でエラーで出ていたので、 public / html のmanifest 部分に crossorigin="use-credentials" を追加する
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" crossorigin="use-credentials"/>
位置情報からテイクアウトやってるお店を教えてくれるLINEbot
作ったもの
位置情報を送るとそこから半径500m以内のテイクアウトやっているお店を最大10件、近い順にカルーセル表示するLINEのbotです。
主に使用した技術
やったこと
オウム返しbotを作成
- LINEbotは初めてだったので、ますは下記を参考にしてオウム返しをするbotを作成しました。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest - Qiita
yelpに登録
- yelpに登録してAPI Keyを取得します。
- .envファイルにyelpのAPI Keyを追加します。 .envファイルをまだ作ってない場合は下記コマンドで作成
npm install dotenv touch .env
# LINEのチャネルシークレットとチャネルアクセストークン CHANNEL_SECRET = "XXXXXXXXXXXX" CHANNEL_ACCESS_TOKEN = "XXXXXXXXXXXX" # 今回取得したyelpのAPI Keyを追記 YELP_API_KEY = "XXXXXXXXXXXX"
テイクアウトbot作成
- .envから値を読み込むためオウム返しbotのserver.jsに下記を追加
require('dotenv').config();
axiosをインストール
yelp APIを用いて通信を行うためaxiosをインストールします。
npm install axios
「位置情報を送信してね!」と返す
今回はユーザから位置情報が欲しいので、位置情報以外が送られた場合は「位置情報を送信してね!」と返します。
if (event.type !== 'message' || event.message.type !== 'location') { return client.replyMessage(event.replyToken, { type: 'text', text: '位置情報を送信してね!' }) }
緯度・経度を取得
送られたきた位置情報から緯度・経度を取得します。
// 緯度 const lat = event.message.latitude // 経度 const lng = event.message.longitude
店舗情報を取得
axiosを使って通信を行い店舗情報を取得します。
let yelpREST = axios.create({ baseURL: "https://api.yelp.com/v3/", headers: { Authorization: `Bearer ${process.env.YELP_API_KEY}`, "Content-type": "application/json", }, }) await yelpREST.get("/businesses/search", { params: { latitude: lat, // 取得した緯度 longitude: lng, // 取得した経度 radius: 500, // 今回は半径500m term: "takeout", // テイクアウト sort_by: "distance", // 距離でソート limit: 10, // 最大10件 }, }) (略)
その他パラメータはyelpのサイトを参照してください。
Business Search Endpoint - Yelp Fusion
レスポンスは下記のようになりました。(例)
{ businesses: [ { id: '12-34xx56xx78xx90xxxxxx', alias: 'XXXXXX-渋谷区', name: 'Shibuya XXXXXX', image_url: 'https://sample.jpg', is_closed: false, url: 'https://www.yelp.com/biz/xxxxxxxxxxxxxx', review_count: 5, categories: [Array], rating: 4.5, coordinates: [Object], transactions: [], price: '¥¥¥', location: [Object], phone: '+0123456789', display_phone: '+81 0-1234-5678', distance: 30.987654321 }, (略) }
店舗情報をカルーセル表示する
上記通信成功後レスポンスから欲しいものを選択して、カルーセル表示します。詳しくは公式ドキュメントを参照ください。
Messaging APIリファレンス | LINE Developers
(略) .then(function (response) { // handle success // データがない場合は「近くにお店はありません!」と表示する if(response.data.total === 0) { return client.replyMessage(event.replyToken, { type: 'text', text: '近くにお店はありません!' }) } // carouselは最大10 // colums配列にデータを入れていく let columns = []; for (let item of response.data.businesses) { columns.push({ "thumbnailImageUrl": item.image_url, "title": item.alias, "text": '⭐️' + item.rating, "actions": [{ "type": "uri", "label": "yelpでみる", "uri": item.url }] }); } // replyMessageの第二引数を配列にすることで複数メッセージを送信できる return client.replyMessage(event.replyToken, [ { type: 'text', text: 'おいしそうだね!' }, { type: 'template', altText: '店舗情報を送信しました!', template: { type: 'carousel', columns: columns } }]); })
デプロイ
下記記事を参考にさせていただき、vercelでデプロイを行いました。
VercelでLINE BOTを動かす 2020年5月版 - Qiita
vercel.json "version": 2 は現在はなくても大丈夫そうでした。
{ "routes": [ { "src": "/", "dest": "api/server.js" }, { "src": "/webhook", "dest": "api/server.js" } ] }
まとめ
yelpのAPIを使ってテイクアウトできるお店を教えてくれるLINEbotを作成しました。
今回作成したLINEbotは下記を読み込む事でお友達に追加できますのでよかったら追加してみてください。
Next.js + TypeScript + Tailwind CSS でブログサイトを作成
はじめに
タイトルにもある通り、Next.js + TypeScript + Tailwind CSS を用いてブログサイトを作成した時のメモ的な感じでここに記させていただきます。
下記のバージョンで操作確認しております。
"dependencies": { "next": "^10.0.0", "react": "16.13.1", "react-dom": "16.13.1", "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.1" },
Next.jsでブログサイトを作成
ブログのベースはNext.jsの公式ドキュメントにあるチュートリアル通りに作成しました。
Qiitaに日本語訳の記事もあり参考になりました。
TypeScript化
TS化を行いました。
- tsconfig.jsonを作成
touch tsconfig.json
- typescriptをインストール
npm install --save-dev typescript @types/react @types/node
.jsファイルを.tsxに変更して、型をつけていきました。 GetStaticProps、GetStaticPaths、GetServerSidePropsといった Next.js固有の型もあります。
各API (getStaticProps、getStaticPaths、getServerSideProps) については下記記事を参考にさせていただきました。
Tailwind CSSに置き換える
Tailwind CSSの導入の方法は下記の記事を参考にさせていただきました。
また、tailwind.config.js をカスタマイズしてクラス名を追加しました。
module.exports = { purge: ['./pages/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'], darkMode: false, // or 'media' or 'class' theme: { extend: { margin: { auto: 'auto' }, maxWidth: { 180: '180px' }, }, }, variants: { extend: {}, }, plugins: [], }
つまづいた点
- Tailwind CSSを導入した際に下記エラーが出ました。
Error: PostCSS plugin tailwindcss requires PostCSS 8.
こちらはTailwind CSSのサイトにて下記のように記載がありました。
If you run into the error mentioned above, uninstall Tailwind and re-install using the compatibility build instead:
上記のエラーが発生した場合は、Tailwindをアンインストールし、互換性のあるビルドを使用して再インストールしてください。
npm uninstall tailwindcss postcss autoprefixer npm install tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
Tailwind CSS v2.0以降PostCSS8に依存しており、PostCSS8はまだ数か月しか経っていないため、エコシステム内の他の多くのツールはまだ更新されていません。 よって、Tailwind CSSのインストール後にターミナルでこのようなエラーが表示される場合があるそうです。
編集・追加
ブログ記事をカードのように表示できるようにコンポーネントを追加しました。また、カードに表示する画像をmarkdownファイル(/posts)に設定している場合はその画像を表示し、それ以外はデフォルトの画像を表示します。
import React from 'react' import Image from 'next/image' import Link from 'next/link' import { FaRegCalendarAlt } from 'react-icons/fa' import { Date } from '../components/date' export const CardList = ({ allPostsData }) => { return ( <> <ul className="list-none"> {allPostsData.map(({ id, date, title, image }) => ( <div className="inline-flex mr-5 max-w-180" key={id}> <li className="mb-5 rounded-lg overflow-hidden shadow-xl p-3 w-44 bg-white"> {image ? ( <Image src={image} width="200" height="200" className="w-24 h-24" alt="thumbnail" /> ) : ( <Image src="/images/no_image.png" width="200" height="200" className="w-24 h-24" alt="no-image" /> )} <br /> <Link href={`/posts/${id}`}> <a>{title}</a> </Link> <br /> <small className="text-gray-400"> <span className="mr-1"> <FaRegCalendarAlt /> </span> <Date dateString={date} /> </small> </li> </div> ))} </ul> </> ) }
参考: Tailwind CSS入門 - フロントエンドで素晴らしい開発体験を得るために - パンダのプログラミングブログ
おわりに
今回は、Next.jsのチュートリアルで作成したブログサイトに、TypeScriptとTailWind CSSを導入しました。 今後はogpを設定したり、記事を投稿してさらに改善していきたいです。
react-axeでアクセシビリティの向上を目指す
はじめに
前回の記事で紹介させていただいたサイトのアクセシビリティをチェックしたら71点でした。
本記事では、下記の記事を参考にさせていただき、react-axeを用いてアクセシビリティの向上を目指します。
本記事で記述しているのはこのWebサイトのトップページのみの修正箇所です。
やったこと
react-axeを導入
react-axeを参考記事と同様に導入します
- react-axeと@types/react-axeをインストール
npm i react-axe npm i @types/react-axe
- 開発環境で結果を確認したいので dynamic import でライブラリをインポート
if (process.env.NODE_ENV !== 'production') { import('react-axe').then((axe) => { axe.default(React, ReactDOM, 1000) ReactDOM.render(<App />, document.getElementById('root')) }) } else { ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ) }
こうする事でコンソールに New aXe issuesというのが出力されています。
さらにリンクをクリックすると詳しい詳細を確認する事ができました。(日本語に対応していました)
あとは New aXe issuesの通りに修正していきます。
New aXe issues
以下に実際に出力されたものを記します。
critical: Buttons must have discernible text
ボタンには認識可能なテキストが存在しなければなりません
ボタン要素にaria-label属性を追加しました。
<Button aria-label="キーワードで検索" size="large" variant="contained" color="default" className={classes.button} onClick={() => history.push('/search')} > キーワードで探す </Button>
serious: Elements must have sufficient color contrast
テキスト要素は背景に対して十分な色のコントラストがなければなりません
ヘッダーの背景色と文字の色のコントラストをつけました。
moderate: Document must have one main landmark
ドキュメントにはmainランドマークが1つ含まれていなければなりません
mainタグを記述していなかったのでmainタグを記述しました。
serious: Links must have discernible text
リンクには認識可能なテキストが存在しなければなりません
リンク要素にaria-label属性を追加しました。
moderate: Page must contain a level-one heading
ページにはレベル1の見出しが含まれていなければなりません
h2タグで記述していた部分をh1タグに修正しました。
moderate: All page content must be contained by landmarks
ページのすべてのコンテンツはlandmarkに含まれていなければなりません
メインとなる部分のコンテンツをmainタグで囲んでいなかったので、mainタグの中に記述しました。
おわりに
トップページ以外も全て同じようにreact-axeの New aXe issuesに従い修正を行いました。
その結果アクセシビリティのスコアも100点にする事ができました。
修正後のソースコードはこちらにあります。
参考サイト
最近作ったレシピ投稿サイトとそれに用いた技術
はじめに
STAY HOME週間 で家にいる事が多くなり、初めてまともな自炊を始めました。
そこで、自分が作った料理のレシピを投稿するサイトを作りました。
作った物
レシピ投稿サイトです。
キーワードで検索、レシピを全て一覧表示、レシピ投稿の3つの機能があります。
- キーワード検索
レシピをCloud Firestoreに保存する時にタグを1つ以上登録してもらって、それをもとに検索します。検索結果ページより画像をクリックする事でレシピの詳細ページに遷移します。
- 一覧表示
Cloud Firestoreから全て表示します。画像をクリックする事でレシピの詳細ページに遷移します。
- 投稿
画像をCloud Storageに保存して、ダウンロードURLを取得し、それと共に、料理名、材料などの情報をCloud Firestoreに保存します。
環境
node
v12.11.1
create-react-appをインストールしてある
npm install -g create-react-app
React
Reactは公式サイトでは以下のように言われています。 チュートリアルもあるので詳しくは公式サイトを見てみてください。
ユーザインターフェース構築のためのJavaScriptライブラリ
(公式サイトより)
Reactはcreate-react-appというReactアプリの雛形を簡単に作成できる便利な物があります。
以下のコマンドを実行します。
npx create-react-app プロジェクト名 --typescript
--typescript
はオプションでこれをつけるとReact + TypeScriptの環境を構築できます。(つけなくてもReactの環境は構築できます)
作成されたフォルダに移動
cd フォルダ名
実行して動作を確認
npm start
Firebase
FirebaseはmBaasと言われいます。 Googleが提供している包括的なアプリ開発プラットフォームで様々な機能がありますが、以下の3つを利用しました。
Cloud Storage
Cloud Storageはファイルを保存・配信する事ができます。 今回は、ユーザからアップロードされた画像を保存するために用いました。
const storageRef = firebase.storage().ref('images').child(`${postIndex}.jpg`) const snapshot = storageRef.put(image) 以下略
Cloud Firestore
Cloud FirestoreはNoSQLデータベースです。 Cloud Storageにアップロードされた画像のダウンロードURL、ユーザが入力した材料などの情報を 保存するために使用しました。
const downloadURL = await storageRef.getDownloadURL() const db = firebase.firestore() db.collection('tileData').add({ image: downloadURL, title: data.title, 以下略 })
Firebase Hosting
Firebase Hostingはホスティングに用いました。
Material-UI
Material-UIはマテリアルデザインをReactに導入する事ができるUIコンポーネントです。 アプリのデザインを手軽にいい感じにする事ができます。
React Hook Form
フォームバリデーションライブラリです。 ユーザがレシピを入力するフォームにバリデーションをかけました。 公式サイトでは、他のバリデーションライブラリとコードの記述量やレンダリング数を比較してありました。
余談ですが、React Hook FormさんはTwitterでお話しやいいねをしてくれます😄
聞いてすごい!
— 📋 React Hook Form (@HookForm) May 12, 2020
useForm
React Hook FormのAPIです。
useFormを呼び出す事で様々なメソッドを受け取る事ができます。詳しくは公式サイトをご覧ください。
今回は、register
とhandleSubmit
を使いました。
const { register, handleSubmit } = useForm()
register
このメソッドを使用して、フォームにバリデーションルールを登録することができます。
※ Material-UIと併用
<TextField inputRef={register({ required: true })} />
handleSubmit
handleSubmitは、フォームバリデーションに成功するとフォームデータを渡し事ができます。
const onSubmit = (data) => { ....略.... } <form onSubmit={handleSubmit(onSubmit)}> <TextField inputRef={register({ required: true })} /> <button type="submit">Submit</button> </form>
React Router
ルーティングするために用いました。
useHistory
React RouterのHooks APIです。 ページ遷移を行う時にhistoryを取得します。
※ Material-UIと併用
const history = useHistory() <Button onClick={() => history.push('/home'}>
useParams
URLのパスの中で動的に変化する部分の値を取得します。 レシピの詳細ページを1つ用意して、URLに応じて中身を変えています。
おわりに
まだ完全にできた訳ではないので、これからも改良していきたいと思います。
あと、まともに自炊する時間も無くなってきました😅
react-testing-libraryでテストをやってみて
はじめに
以前にcreate-react-appで作成したReactアプリをテストしてみるというJestとEnzymeを使ってテストを行う記事を書きましたが、
今回は、以下の記事を参考にしてReact Testing Libraryを使ってみました。
本記事は実際に手を動かしてみた感想ですので、詳しくは参考記事をご覧ください。
では手を動かしていきます。
テスト実行時のエラー
記事通りに手を動かしていくと、2. Testing DOM elements (DOM要素のテスト)の所で以下のようなエラーが出ました
- TestElements.test.js
import React from 'react' import { render, cleanup } from '@testing-library/react' import { TestElements } from './TestElements' afterEach(cleanup) // カウンターが0に等しいか it('should equal to 0', () => { const { getByTestId } = render(<TestElements />) expect(getByTestId('counter')).toHaveTextContent(0) }) // ボタンが有効か無効か it('should be disabled', () => { const { getByTestId } = render(<TestElements />) expect(getByTestId('button-down')).toBeDisabled() })
TypeError: expect(...).toHaveTextContent is not a function TypeError: expect(...).toBeDisabled is not a function
上記のissuesにある通りにimport "@testing-library/jest-dom/extend-expect";
を追加する事で解決しました。以降同じようにやっていきます。
- TestElements.test.js (修正後)
import React from 'react' import { render, cleanup } from '@testing-library/react' import { TestElements } from './TestElements' import '@testing-library/jest-dom/extend-expect' afterEach(cleanup) // カウンターが0に等しいか it('should equal to 0', () => { const { getByTestId } = render(<TestElements />) expect(getByTestId('counter')).toHaveTextContent(0) }) // ボタンが有効か無効か it('should be disabled', () => { const { getByTestId } = render(<TestElements />) expect(getByTestId('button-down')).toBeDisabled() })
yarn test
を実行してテストが通りました🎉
これまで、getByTestId
をなんとなく使ってきましたが以下の記事にTipsとしてgetByTestId
はなるべく避けるという知見を得る事ができました。
これは、クエリというもので公式ドキュメントのガイドにも優先順位が書いてありました。
おわりに
react-testing-library
はcreate-react-app
の場合最初から導入されているので、すぐに始める事ができます。
テストを書いてみるのは2回目でしたが、独自のヘルパー関数を作る部分の理解が足りずもう少し勉強が必要だと感じました。
自分が実際にやった物はこちらのリポジトリにあげていこうと思います。