記事書くほどの内容じゃないですが、アウトプットの練習です。
できあがったもの
実際の例(開発者のレート)
はじめに
最近AtCoderというサイトで競技プログラミングに取り組んでいて、アルゴリズムを勉強しています。
勉強の成果がでたのか少しずつレートが上昇しているわけですが、それをブログやポートフォリオに埋め込めたら便利だと思いました。
というわけでそのためのサイトを作りました。
開発環境
- Yarn
- Vercel
- Next.js
- React Bootstrap
- FaunaDB
Shields.ioでいいじゃん。
Shields.ioでtouristのバッジを作った例
Shields.ioにはバッジデータを動的に取得してくれる機能があります。
最新のレートを取得して表示するという点ではこれで足りるのですが、AtCoderではレートに応じて色がつきます。
大抵の人はレートという具体的な数値より色の方に興味がありますから、これもバッジに表示したほうが良いですよね!
しかしShields.ioには動的に色を変更してくれる機能はないようです。
ということでバッジ生成システムを自前で実装することになりました。
バッジの生成方法
GitHubなどでよく使われるバッジはサービス側が画像を返すURLを生成し、ユーザーはそれをMarkdownなどに埋め込んで使います。
このサイトでも同じ仕組みで使えるものを作ることにしました。
具体的には以下のようなURLにアクセスすると、ユーザー固有のバッジを取得できるようにしています。
https://atcoder-badges.now.sh/api/atcoder/[ユーザー名]
そこで問題となるのが画像をどうやって生成するかです。
レートと色はユーザーによって違うので、ユーザー毎に画像を生成する必要があります。
最初はnode-canvasを使おうと思ったのですが、ラスター画像の生成はサーバー負荷が高そうです。
そこで思いついたのがベースとなるSVGを作成し、リクエスト毎にレートや色の情報などのデータだけ書き換えて送信する方法です。
これなら毎回テキストデータをいじるだけなので、対して重くもありませんし、ライブラリも使う必要がありません。
というか、Shields.ioもSVGをつかっていますね…
先に確認しておけばよかった…
実際の実装ではFigmaで出力したSVGにデータを埋め込みました。
const colors = [
"808080", // gray
"804000", // brown
"008000", // green
"00C0C0", // cyan
"0000FF", // blue
"C0C000", // yellow
"FF8000", // orange
"FF0000", // red
]; // レートごとの色を定義しています。
const badge = (rate: number | null) => `\
<svg width="90" height="20" viewBox="0 0 90 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 3C0 1.34315 1.34315 0 3 0H51V20H3C1.34315 20 0 18.6569 0 17V3Z" fill="#343A40"/>
<path d="M51 0H87C88.6569 0 90 1.34315 90 3V17C90 18.6569 88.6569 20 87 20H51V0Z" fill="#${rate === null ? '000000' : colors[Math.floor(Math.min(2800, rate) / 400)]}"/>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Roboto" font-size="12" letter-spacing="0em"><tspan x="4" y="14.1016">AtCoder</tspan></text>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Roboto" font-size="12" letter-spacing="0em"><tspan x="57.0234" y="14.1016">${rate === null ? ' -' : rate.toString().padStart(4, ' ')}</tspan></text>
</svg>
`; // JavascriptのTemplate Stringsを使って、データをSVGに埋め込んでいます。
レートの取得
レートの取得自体は、以下のリンクでグラフ描画用データが得られるのでそれを使いました。
https://atcoder.jp/users/[ユーザー名]/history/json
しかし、バッジへアクセスがあるたびに毎回AtCoderのサーバーへデータを取得しにいくのは、あまり行儀が良いとはいえません。
そのため、取得したデータをキャッシュする仕組みが必要でした。
今回はホスティングサービスとしてVercelを使いたかったのですが、どうやらVercel自体にはデータベースを提供する仕組みはないようです。
そこで、VercelとのIntegrationが公開されていたFaunaDBをキャッシュの置き場所として使うことにしました。
FaunaDBはサーバーレス時代のためのデータベースで、自前でサーバーを用意する必要なくデータベースを利用できます。
公式のダッシュボードもデザインが洗練されており、かなり使いやすいです。
取得したデータはFaunaDBにキャッシュして、しばらくそちらのほうを読みに行くようにしています。
キャッシュの有効期限は、コンテスト後にデータがなるべく早く更新されるように毎時0分としました。
フロントエンド
単純にReact Bootstrapで実装しました。
ホスト先が変わっても動くように自分がホストされているドメインをクライアント側で毎回取得したかったんですが、Next.jsでクライアントのみで動くコードを書くのにすこし手間取りました。
最終的にはuseEffectでwindow.location.originを取得する形で落ち着きました。
まとめ
シンプルでいい感じのものができたと思います。
ただ、FaunaDBとGraphQLの習得に時間がかかって制作に一週間かかってしまったのは残念でした…
はじめてVercelを使ってみましたが、Zero configと謳っているだけあってかなり簡単にデプロイできましたね。
これからも使わせていただくかもしれません。