くじ引き読書法を実践するWebページをnext.jsで作る
くじ引き読書法とは
技術書の読書術という書籍で紹介されている、適当に選んだ本棚の前に立ち、目をつぶった状態で手に触れた本を読破するという読書法です。
図書館や書店などで、普段読まないようなジャンルの本棚の前に立って行うことで、
思いもよらない世界との出会いがあるという面ですごく魅力的な読書法だと思います。
その一方で、ジャンルを決めていることや、
本棚の一番上や一番下など、物理的に取りにくい位置にある本に手が届きにくいなどの問題で、
完全な意味でランダムに本を選ぶのは難しいという問題もあります。
(この点は本の中でも言及があります。)
そこで、最近触り始めたnext.jsを使って簡単なWebページを自作してみました。
Webページの挙動
上記がトップページで、「新しい本に出会う」ボタンを押すと、
本の検索が始まり、、、、
選ばれた本が表示されます。
「この本を購入」ボタンを押すと、別タブで版元ドットコムの書籍のページが開きます。
また、以下のような挙動を今後できれば追加できればと考えています。
私自身の願望として、くじ引き読書法になるべくお金をかけたくないと思っているのと、
荷物が嵩張るのが嫌い&安いと言う理由で、ほとんどの本をKindleで読むからです。
- Xにくじ引きの結果選ばれた本についての情報をポストできる機能
- Amazonに売っている本の中から選べるようにする機能
- Kindle UnimitedやPrime Readingの対象の本に絞って検索する機能
※このWebページはVercelを使ってデプロイしていますが、商用利用が禁止されているはずなので、
ここまでやろうと思うとインフラを自分で構築する必要性が生じそうですね。
(AmazonのAPIを使うのに、Amazonアソシエイトプログラムの登録が必要になるはずで、
この辺りのことが商用利用と判断されそうで怖いので、、、)
プログラムについて
ひとまず、最低限の動くものをさっさと作ることを優先して作ったので、
プログラムは汚いです。
先述した追加機能の実装の前に、主に以下の点を改善したいと考えています。
- コンポーネント化する
- テストコードがないので、書く
- TypeScriptの良さである型をうまく利用できてないので、改善する
- せっかくnext.jsを使ったのだから、SSRを使って実装する
"use client";
import { useState } from "react";
import axios from "axios";
const RandomBookSelector = () => {
const [isLoading, setIsLoading] = useState(false);
const [bookInfo, setBookInfo] = useState<any>(null);
// 無作為にISBNを生成する処理
const generateRandomISBN = () => {
let isbn = "";
const digits = "0123456789";
// 最初の3桁は978or979
const first3Digits = Math.random() < 0.5 ? "978" : "979";
// 978or979の次は4で固定(日本語の書籍に絞る)
isbn += first3Digits + "4";
// 出版社を表す4桁のコードをランダムで生成
for (let i = 0; i < 4; i++) {
isbn += digits.charAt(Math.floor(Math.random() * digits.length));
}
// 本そのものを表す4桁のコードをランダムで生成
for (let i = 0; i < 4; i++) {
isbn += digits.charAt(Math.floor(Math.random() * digits.length));
}
/* 最後の1桁はチェックデジット。
※チェックデジットとは、バーコードの読み間違いがないかを検査するための数値。
以下のような手順で作る。
・「左から奇数桁の数字の合計」に「偶数桁の数字の合計」を3倍した値を加える
・↑の下1桁を特定し、10から引く
*/
let sum = 0;
for (let i = 0; i < isbn.length - 1; i++) {
const digit = parseInt(isbn.charAt(i));
if (i % 2 === 0) {
sum += digit;
} else {
sum += digit * 3;
}
}
const checkDigit = (10 - (sum % 10)) % 10;
isbn += checkDigit;
return isbn;
};
/* openBD APIにランダムで生成したISBNを渡して検索することを、
書籍がヒットするまでやり続ける。
※ランダムで生成したISBNに紐づく書籍が必ず実在する保証がないため。
*/
const fetchBookInfo = async () => {
setIsLoading(true);
let isbn;
do {
isbn = generateRandomISBN();
const response = await axios.get(
`https://api.openbd.jp/v1/get?isbn=${isbn}`
);
if (response.data && response.data[0] && response.data[0].summary) {
setBookInfo(response.data[0].summary);
setIsLoading(false);
break;
}
} while (true);
};
// 版元ドットコムに遷移するためのリンクを生成
const handleBuyButton = () => {
const hanmotoLink = `https://www.hanmoto.com/bd/isbn/${bookInfo.isbn}`;
window.open(hanmotoLink);
};
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-3xl font-bold mb-4">Random Book Selector</h1>
{isLoading && (
<p style={{ fontWeight: "bold", fontSize: "20px" }}>
<span role="img" aria-label="glass" className="mr-2">
🔎
</span>
本を探しています...
</p>
)}
{!isLoading && bookInfo && (
<div className="text-center">
<h2 className="text-2xl mb-3">以下の本が選ばれました!</h2>
<p style={{ fontWeight: "bold", fontSize: "20px" }}>
{bookInfo.title}
</p>
<p style={{ color: "grey", fontSize: "16px" }}>
{bookInfo.author}, {bookInfo.publisher},{" "}
{bookInfo.pubdate.slice(0, 4) + "/" + bookInfo.pubdate.slice(4)}
</p>
<div className="flex justify-center items-center">
<button
className="bg-blue-500 text-white px-4 py-2 m-5 rounded-md flex items-center"
onClick={handleBuyButton}
>
<span role="img" aria-label="wallet" className="mr-2">
👛
</span>
この本を購入
</button>
</div>
</div>
)}
{!isLoading && (
<button
className="bg-blue-500 text-white px-4 py-2 rounded-md flex items-center justify-center mb-2"
onClick={fetchBookInfo}
disabled={isLoading}
>
<span role="img" aria-label="book" className="mr-2">
📓
</span>
新しい本に出会う
</button>
)}
</div>
);
};
export default RandomBookSelector;
プログラムでやろうとしていることは、無作為に作成したISBNを用いてopenBDにリクエストを投げ、そこから得た情報をもとに画面を表示するというシンプルなものです。
※無作為に作成したISBNに該当する書籍が存在しないこともあるので、
存在することが確認できるまでリクエストを投げ続けています。
デプロイの方法
Vercelを用いました。驚くほど簡単にできます。
サバイバルTypeScriptのこちらの記事が参考になりました。
主な技術要素
- next.js(14)
- Tailwind CSS
- TypeScript
- Vercel
参考にさせていただいたサイト
ありがとうございます。
裏側でやろうとしている処理の内容はほぼ同じである認識です。
(設計をする上で参考にさせていただきました。ありがとうございます。)
ありがとうございます。
Discussion