前回【Next.js】Githubのような勉強記録アプリ②:LocalStorageを活用したデータ保存を実装しました。今回は、LocalStorageに保存したデータを利用して、GitHubのContributionのような「勉強記録グリッド」を作ってみます。完成イメージとしては、以下のようなシンプルな構成です。

- 1日ごとの勉強データを四角で表示
- 勉強時間によって色を変更
- ホバーすると詳細を確認
- LocalStorageからデータを取得して描画
必要な構造
今回の実装では、以下のようにコンポーネントを分けています。
GrassGrid
データ全体を並べるコンポーネントです。
- 365日分の日付生成
- 7日単位に分割
- Grid表示
- LocalStorageからデータ取得
GrassCell
1日分のデータを表示するコンポーネントです。
- 日付
- 勉強時間
- 色の変更
- hover時の表示
それでは、コンポーネント別のコードについて、解説します。
GrassGrid
GrassGridでは365日分のセルを生成します。さらに7日単位で分割し、1列に7個ずつセルを表示します。取得した勉強データを利用して、GrassCell側でセルの色を変更します。

日付を生成する
まずは、今日から過去365日分の日付を作成し、GitHubのContributionのように表示するため、日付を配列として管理します。
const getDates = (days: number) => {
const dates = [];
for (let i = days - 1; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateString = date.toISOString().split("T")[0];
dates.push(dateString);
}
return dates;
};
7日単位に分割する
次に、縦7マスになるようにデータを分割します。
const chunkDates = (dates: string[]) => {
const result = [];
for (let i = 0; i < dates.length; i += 7) {
result.push(dates.slice(i, i + 7));
}
return result;
};
このようにすることで、「1週間 = 1列」として表示できるようになります。
LocalStorageからデータを取得
保存済みの勉強データを取得します。こちらのデータはGrassCellに渡します。
const getStudyData = (): Record<string, StudyRecord[]> => {
const stored = localStorage.getItem("studyData");
const data: Record<string, StudyRecord[]> =
stored ? JSON.parse(stored) : {}; //データがなかったら空でOK
return data;
};
Gridとして表示
取得した日付データをmapで並べます。1週間ごとに列を作成し、その中に1日ごとのセルを表示しています。
return (
<div className="flex gap-1">
{weeks.map((week, i) => (
<div key={i} className="flex flex-col gap-1">
{week.map((date) => {
const records = studyData[date] || []; //勉強内容がなければ空の配列
return (
<GrassCell key={date} date={date} studyData={records} onClick={(date) => {}} />
);
})}
</div>
))}
</div>
);
全体コード
GrassGridの全体コードです。365日分のdate情報を7日ごとにまとめて描画します。

コードを見る
"use client";
import { StudyRecord } from "@/types/study";
import GrassCell from "./GrassCell";
import { getStudyData } from "@/lib/data";
const getDates = (days: number) => {
const dates = [];
for (let i = days - 1; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateString = date.toISOString().split("T")[0];
dates.push(dateString);
}
return dates;
};
const chunkDates = (dates: string[]) => {
const result = [];
for (let i = 0; i < dates.length; i += 7) {
result.push(dates.slice(i, i + 7));
}
return result;
};
export default function GrassGrid() {
const tates = getDates(365);
const weeks = chunkDates(tates);
const studyData = getStudyData();
console.log("dates:", tates);
console.log("weeks:", weeks);
return (
<div className="flex gap-1">
{weeks.map((week, i) => (
<div key={i} className="flex flex-col gap-1">
{week.map((date) => {
const records = studyData[date] || [];
return (
//GrassCellにdateと勉強データをと勉強データを渡します。(onClickはまた次回)
<GrassCell key={date} date={date} studyData={records} onClick={(date) => {}}/>
);
})}
</div>
))}
</div>
);
}
GrassCell
GrassCellでは、該当の日付の勉強データを取得し、勉強時間を計算します。勉強時間によってセルの色を変更し、視覚的に勉強量が分かるようにしています。さらにhoverをすると、該当の日付の勉強時間が表示されます。
勉強時間の計算
propsで取得したdateの勉強データを取得し、その日のトータル勉強時間を計算します。より分かりやすく表示するため、ミリ秒を時間単位へ変換しています。
const todayData = studyData.filter(record => record.date === date);//特定の日付の勉強情報
const totalDuration = todayData.reduce((sum, record) => sum + record.duration, 0);
const durationHours = totalDuration / (1000 * 60 * 60); //ms -> hourセルの色変更
勉強時間によって色を変更します。勉強時間が長いほど、色が濃くなるようにしています。もちろん色や勉強時間は変えられます。
let bgColor = "bg-gray-500";
if (durationHours > 0 && durationHours <= 1) {
bgColor = "bg-green-300";
} else if (durationHours > 1 && durationHours <= 2) {
bgColor = "bg-green-500";
} else if (durationHours > 2 && durationHours <= 3) {
bgColor = "bg-purple-700";
}
- 勉強記録なし:グレー
- 1時間未満:薄い緑
- 1時間以上〜2時間未満:緑
- 2時間以上〜3時間未満:紫
hoverで詳細表示
Tailwindのgroup-hoverを使って、詳細は普段はhiddenにして、hover時のみ表示できるようにしています。

<div className="relative group" onClick={() => onClick(date)}>
<div className={`w-4 h-4 ${bgColor} rounded-sm`} />
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 hidden group-hover:block bg-black text-white text-xs px-2 py-1 rounded">
<div>{date}</div>
<div>{formatDurationShort(totalDuration)}</div>
</div>
</div>
通常の hover では、自分自身にしかスタイルを適用できません。しかし、group-hover を使うことで、「親要素にhoverした時に子要素を表示する」という動きを実装できます。今回はこの仕組みを利用して、セルにマウスを乗せた時のみ、日付や勉強時間を表示するようにしています。
また、z-indexを使って詳細が常に見えるようにしています。
全体コード
GrassCellの全体コードです。
コードを見る
"use client";
import { formatDurationShort } from "@/lib/format";
import { StudyRecord } from "@/types/study";
export default function GrassCell({date , studyData, onClick} : {date:string, studyData : StudyRecord[], onClick: (date:string) => void}){
const todayData = studyData.filter(record => record.date === date);
const totalDuration = todayData.reduce((sum, record) => sum + record.duration, 0);
const durationHours = totalDuration / (1000 * 60 * 60);
let bgColor = "bg-gray-500";
if(durationHours > 0 && durationHours <= 1){
bgColor = "bg-green-300";
} else if(durationHours > 1 && durationHours <= 2){
bgColor = "bg-green-500";
} else if(durationHours > 2 && durationHours <= 3){
bgColor = "bg-purple-700";
}
return(
<div className="relative group" onClick={() => onClick(date)}>
<div className={`w-4 h-4 ${bgColor} rounded-sm`} />
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 hidden group-hover:block group-hover:z-40 bg-black text-white text-xs px-2 py-1 rounded">
<div>{date}</div>
<div>{formatDurationShort(totalDuration)}</div>
</div>
</div>
)
}終わりに
今回は、LocalStorageに保存したデータを利用して、GitHubのContributionのような勉強記録グリッドを作成してみました。シンプルなUIに見えますが、日付の管理やGrid表示、色による可視化など実際に作ってみると考えることが多く、とても勉強になりました。
次回はモーダル表示と詳細画面なども実装してみたいと思います!

