目次 (クリックで展開)
これ。
特に意味はないけど、なんか可愛いし面白いから載せることにした。
前回のブログでは、ライブラリを使って草を描画したけど、いまいち使いづらかったので今回は TailwindCSS 使って自分でちゃんと表示させることにした。月とか曜日の表示はいらないので簡単にできる。
Next.js の API ルートに、GitHub との通信を行うエンドポイントを作る。実際に表示されるコンポーネントから、そのエンドポイントにリクエストして情報を取得し、表示する感じ。
GitHub の API を叩いてコントリビュートの情報を取得する。
GitHub の API は GraphQL と REST の二種類があるが、REST の方でコントリビュートの情報は取得できないっぽい?ので GraphQL を使っていく。
ほぼこの記事と同じことをやった。
GitHubの草の情報をAPIで取得する方法
zenn.dev
実際のコードはこんな感じ
/pages/api/grass.tsimport 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
の権限だけなので、そこだけにチェックを入れます。
このエンドポイントを呼び出すための専用フックを作る。
/hooks/grass.tsimport 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.tsximport { 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 が全部面倒な計算やってくれて楽。
この、一つの縦の行が一週間で、GitHub の API がちょうど週ごとにデータを返してくれる優良設計なので、そのまま map
で回して表示すると本家と同じようなレイアウトになる。
草を表示してくれるライブラリをいくつか探してみたけれども、どれも日付と草の色の指定方法がめんどくて使いづらかった。
GrassTile
は以下
/components/widget/GrassCalendar.tsxconst 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.tsximport 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} />
指定した幅の時に表示する月の数を指定する。無理矢理レスポンシブにした。
画面幅によって値を変更する正攻法がわからないので誰か教えてください。