【Next.js】Githubのような勉強記録アプリ③:Gridを使って勉強時間を可視化する

grid-サムネイル Programming

前回【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表示、色による可視化など実際に作ってみると考えることが多く、とても勉強になりました。

次回はモーダル表示と詳細画面なども実装してみたいと思います!

タイトルとURLをコピーしました