🌿

ブログにGitHubの草を載せた

目次 (クリックで展開)


GitHub の草

SCR-20220824-v6s.png

これ。

特に意味はないけど、なんか可愛いし面白いから載せることにした。

前回のブログでは、ライブラリを使って草を描画したけど、いまいち使いづらかったので今回は TailwindCSS 使って自分でちゃんと表示させることにした。月とか曜日の表示はいらないので簡単にできる。

実装

使ったライブラリ

  • SWR (キャッシュもしてくれる便利なデータ取得ライブラリ。Vercel製)
  • Recoil (ReactのProvider地獄を解消してくれるステート管理ライブラリ。Reduxはややこしくてわからん)
  • date-fns (日付の計算。ややこしいことは全部ライブラリに任せよう)
  • TailwindCSS (CSSの細かいことを考えなくて済む。嬉しい)
  • graphql-request (Prisma作ってるとこが作ったGraphQLのクライアント)

流れ

Next.js の API ルートに、GitHub との通信を行うエンドポイントを作る。実際に表示されるコンポーネントから、そのエンドポイントにリクエストして情報を取得し、表示する感じ。

GitHub API

GitHub の API を叩いてコントリビュートの情報を取得する。

GitHub の API は GraphQL と REST の二種類があるが、REST の方でコントリビュートの情報は取得できないっぽい?ので GraphQL を使っていく。

ほぼこの記事と同じことをやった。

GitHubの草の情報をAPIで取得する方法

zenn.dev

実際のコードはこんな感じ

/pages/api/grass.ts
import type { NextApiRequest as Req, NextApiResponse as Res } from "next" import { gql, GraphQLClient } from "graphql-request" import { GrassCalendarRes } from "../../types/grass" const handler = async (req: Req, res: Res) => { const endpoint = "https://api.github.com/graphql" const query = gql` query ($userName: String!) { user(login: $userName) { contributionsCollection { contributionCalendar { totalContributions weeks { contributionDays { contributionCount date } } } } } } ` const variables = { userName: "p1atdev", } const client = new GraphQLClient(endpoint, { headers: { Authorization: `Bearer ${process.env.GITHUB_API_KEY}`, }, }) try { const data: GrassCalendarRes = await client.request(query, variables) res.status(200).json({ ...data }) } catch (error) { res.status(500).json({ error }) } } export default handler

ユーザー名はハードコードしちゃってます。

GitHub の API キーは https://github.com/settings/tokens から生成できます。今回必要なのは、user:read の権限だけなので、そこだけにチェックを入れます。

SWR

このエンドポイントを呼び出すための専用フックを作る。

/hooks/grass.ts
import useSWR from "swr" import { GrassCalendarRes } from "../types/grass" export const useGrass = () => { const res = useSWR("/api/grass", (url) => fetch(url).then((res) => res.json())) const data: GrassCalendarRes = res.data const error = res.error if (error) { return { error, } } try { const grass = data.user.contributionsCollection.contributionCalendar.weeks return { grass: grass, error, } } catch (error) { return { error, } } }

useSWR を間に入れて fetch すると、レスポンスをキャッシュしていい感じになる。

ちなみに、useSWR の第一引数は識別子の役割にもなっているので、これを使って状態管理をするテクニックもある。

GrassCalendarRes は返ってくるレスポンスの型。quicktype で一瞬で型情報を生成できるので便利。

カレンダー

描画部分。レスポンシブにするのがちょっとめんどくて、結局最終的にもゴリ押し感がある。

まずはカレンダー本体

/components/widget/GrassCalendar.tsx
import { differenceInCalendarWeeks, subMonths } from "date-fns" import { ComponentProps, useEffect, useState } from "react" import { useGrass } from "../../hooks/useGrass" import { Week } from "../../types/grass" interface Props { months: number } const GrassCalendar = ({ months = 6 }: Props) => { const { grass, error } = useGrass() const [weeks, setWeeks] = useState<Week[]>([]) useEffect(() => { if (grass) { const startDate = subMonths(new Date(), months) const diffWeeks = differenceInCalendarWeeks(new Date(), startDate) setWeeks(grass.slice(grass.length - diffWeeks, grass.length)) } }, [grass, months]) return ( <div> {error && <div>植栽中...🌿🌿🌿</div>} <div className="flex gap-x-1"> {weeks && weeks.map((week, index) => { return ( <div key={`week:${index}`} className="flex flex-col gap-y-1"> {week.contributionDays.map((day) => { return <GrassTile key={day.date} count={day.contributionCount} /> })} </div> ) })} </div> </div> ) }

現在の日付から、渡された月の数の草を表示している。date-fns が全部面倒な計算やってくれて楽。

SCR-20220824-v6s.png

この、一つの縦の行が一週間で、GitHub の API がちょうど週ごとにデータを返してくれる優良設計なので、そのまま map で回して表示すると本家と同じようなレイアウトになる。

草を表示してくれるライブラリをいくつか探してみたけれども、どれも日付と草の色の指定方法がめんどくて使いづらかった。

GrassTile は以下

/components/widget/GrassCalendar.tsx
const defaultColorSet = new Map([ [0, "#e4f2e4"], [1, "#a5e68b"], [2, "#60cf64"], [4, "#37c94a"], [8, "#24904c"], ]) const GrassTile = ({ count, colorSet = defaultColorSet }: GrassTileProps) => { const [color, setColor] = useState<string>("#f5f5f5") useEffect(() => { colorSet.forEach((value, key) => { if (count >= key) { setColor(value) } }) }, [count]) return ( <div> <div className="h-3 w-3 rounded-sm" style={{ backgroundColor: color, }} ></div> </div> ) }

渡された count で色分けしている。一応色のと数の組み合わせは変えられるようにしてある。

レスポンシブ対応

デフォルトの 6ヶ月だと、画面幅によっては小さすぎたりはみ出たりしてしまう。なので、無理矢理だけども画面幅によって表示する月が変わるようにした。

/components/widget/GrassCalendarResponsive.tsx
import GrassCalendar from "./GrassCalendar" interface Props { /** * default */ def?: number sm?: number md?: number lg?: number xl?: number } const GrassCalendarResponsive = ({ def = 6, sm = 2, md = 3, lg = 4, xl = 6 }: Props) => { return ( <div> <div className="sm:hidden"> <GrassCalendar months={def} /> </div> <div className="hidden sm:block md:hidden"> <GrassCalendar months={sm} /> </div> <div className="hidden md:block lg:hidden"> <GrassCalendar months={md} /> </div> <div className="hidden lg:block xl:hidden"> <GrassCalendar months={lg} /> </div> <div className="hidden xl:block"> <GrassCalendar months={xl} /> </div> </div> ) } export default GrassCalendarResponsive

指定した画面幅にいる時だけ hidden から block にして表示させている。

正直、レスポンシブに何かの値を変える正攻法わからない。もしかしたらそれ専用のメソッドが用意されているのかもしれない...

表示

実際に草を配置する。

<GrassCalendarResponsive def={5} sm={8} md={8} lg={4} xl={5} />

指定した幅の時に表示する月の数を指定する。無理矢理レスポンシブにした。

終わり

画面幅によって値を変更する正攻法がわからないので誰か教えてください。