javascriptでおしゃれでカスタム可能なカレンダー

# 初めに
 javascriptでおしゃれなカレンダーを作ろう取り組んだコードを残します。

## 完成品
![スクリーンショット 2025-01-07 201555.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3344967/30cde54a-8c0f-c1ba-0c34-42de745dabc7.png)

## 構成
構成は一般的なjavascriptの構成ですので、index.html、style.css、calendar.jsです。
カスタマイズしやすいようにclass名等を細かく決めています。
色々挑戦したので不要なコードも残っているかと思いますがお許しください。

```:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>カレンダー</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="container">
      <div class="content">
        <button id="year_button"></button>
        <button id="year_prev">▲</button>
        <button id="year_next">▼</button>
        <button id="prev">◀◀</button>
        <button id="next">▶▶</button>
        <div id="calendar"></div>
        <div id="modal" style="display: none">
          <div class="modal-container">
            <select id="year-select"></select>
            <button id="modal-ok">OK</button>
            <button id="modal-cancel">Cancel</button>
          </div>
        </div>
      </div>
    </div>
    <script src="calendar.js"></script>
  </body>
</html>
```
```:style.css
/* カレンダーのテーブル全体 */
.container {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  padding: 10px;
}
.content {
  position: relative;
  width: 400px;
  height: 360px;
  padding: 10px;
  font-size: 18px;
  font-style: italic;
  margin: 10px auto;
  justify-content: center;
}

#calendar {
  margin: 0 auto 10px;
  padding: 0;
}

button#year_button {
  position: absolute;
  font-size: 30px;
  border: none;
  padding: 1px 15px 0;
  border-radius: 5px;
  background-color: #0074d0;
  color: white;
  font-style: italic;
  top: 30px;
  left: 12px;
  box-shadow: 3px 3px 3px -3px black;
  transition: background-color 0.3s ease, color 0.3s ease;
}
button#year_button:hover {
  background-color: #6df049;
}
button#prev,
button#next {
  font-size: 20px;
  font-style: italic;
  position: absolute;
  color: #0074d0;
  background-color: transparent;
  border: none;
  top: 54px;
  right: 64px;
}
button#next {
  right: 8px;
}
button#prev:hover,
button#next:hover {
  color: #6df049;
}
button#year_prev,
button#year_next {
  font-size: 12px;
  font-style: italic;
  position: absolute;
  transform: scale(5, 1);
  color: #0074d0;
  background-color: transparent;
  border: none;
  top: 15.2px;
  left: 70px;
}
button#year_next {
  top: 65.5px;
}
button#year_prev:hover,
button#year_next:hover {
  color: #6df049;
}

table {
  border-collapse: collapse;
  background-color: rgb(255, 255, 255);
  width: 100%;
  margin: 80px auto;
  position: relative;
  padding-top: 50px;
}

table tr:first-child td {
  background-color: #0074d0;
  font-size: 18px;
  color: white !important;
  font-style: italic;
}

table .buttons {
  display: flex;
  gap: 10px;
}

td {
  text-align: center;
  padding: 4px;
  font-size: 18px;
  height: 30px;
  border: 2px solid #b9c7da;
  width: 14.28%;
  position: relative;
}

td.holiday {
  color: #e41414;
}

td.clickable {
  cursor: pointer;
  transition: background-color 0.3s;
}

td.clickable:hover {
  background-color: rgb(92, 238, 131);
}

table td:first-child {
  color: #e41414;
}

table td:nth-child(7) {
  color: #1152c1;
}
/* モーダルのスタイル */
#modal {
  position: absolute;
  width: 200px;
  height: 51px;
  margin-top: 0;
  font-size: 24px;
  background-color: white;
  border: solid 4px;
  border-radius: 10px;
  display: flex;
  justify-content: center;
  align-items: center;
  top: 18.5px;
  z-index: 2;
}

select#year-select {
  font-size: 21px;
  font-style: italic;
  background-color: transparent;
  border: none;
}

button#modal-ok,
button#modal-cancel {
  border: none;
  background-color: #0074d0;
  color: white;
  font-size: 19px;
  padding: 1px 7px 1px 3px !important;
  font-style: italic;
  border-radius: 5px;
}
button#modal-cancel {
  background-color: red;
}
```
```:calendar.js
let holidays = {}; // 祝日データを格納するオブジェクト

// 国民の祝日 API から祝日データを取得
async function fetchHolidays(year) {
  const response = await fetch(
    `https://holidays-jp.github.io/api/v1/date.json`
  );
  if (response.ok) {
    const data = await response.json();
    // 指定された年の祝日を抽出
    for (const [date, name] of Object.entries(data)) {
      if (date.startsWith(year)) {
        holidays[date] = name;
      }
    }
  } else {
    console.error("祝日データの取得に失敗しました。");
  }
}

// カレンダーを生成する関数
async function renderCalendar(year, month) {
  // 祝日データを取得（まだ未取得の場合のみ）
  if (Object.keys(holidays).length === 0) {
    await fetchHolidays(year);
  }

  const startDate = new Date(year, month - 1, 1); // 月の最初の日
  const endDate = new Date(year, month, 0); // 月の末日
  const endDayCount = endDate.getDate(); // 月の日数
  const startDay = startDate.getDay(); // 月の最初の曜日
  let dayCount = 1; // 日にちカウンター
  let calendarHtml = ""; // カレンダーHTML

  // calendarHtml += "<h1>" + year + "/" + month + "</h1>";
  calendarHtml += "<table>";
  calendarHtml += "<tr>";

  const weeks = ["Sun", "Mon", "The", "Wed", "Thu", "Fri", "Sat"];
  for (let i = 0; i < weeks.length; i++) {
    calendarHtml += "<td>" + weeks[i] + "</td>";
  }
  calendarHtml += "</tr>";

  for (let w = 0; w < 6; w++) {
    calendarHtml += "<tr>";
    for (let d = 0; d < 7; d++) {
      if (w == 0 && d < startDay) {
        calendarHtml += "<td></td>"; // 月初まで空白を埋める
      } else if (dayCount > endDayCount) {
        calendarHtml += "<td></td>"; // 月末を超えた場合
      } else {
        const currentDate = `${year}-${String(month).padStart(2, "0")}-${String(
          dayCount
        ).padStart(2, "0")}`;
        let className = "clickable";
        if (d === 0) className += " sunday"; // 日曜日
        if (d === 6) className += " saturday"; // 土曜日
        if (holidays[currentDate]) className += " holiday"; // 祝日

        calendarHtml += `<td class="${className}" data-date="${currentDate}"><span class="date">${dayCount}</span></td>`;
        dayCount++;
      }
    }
    calendarHtml += "</tr>";
  }
  calendarHtml += "</table>";

  calendarHtml += "<tr class='calendar-header'>";
  // ここでボタンに年と月を設定
  const yearButton = document.getElementById("year_button");
  yearButton.textContent = `${year} / ${month}`; // 年と月をボタンに表示
  document.querySelector("#calendar").innerHTML = calendarHtml;

  // 最終行が空であれば非表示にする
  const rows = document.querySelectorAll("#calendar table tr");
  const lastRow = rows[rows.length - 1];
  let isEmpty = true;
  lastRow.querySelectorAll("td").forEach((cell) => {
    if (cell.textContent.trim() == "") {
      isEmpty = true;
    }
  });

  if (isEmpty) {
    lastRow.style.display = "none";
  }

  const firstRow = rows[1];
  const firstCell = firstRow.querySelector("td");
  const lastRow2 = rows[rows.length - 2];
  if (firstCell.textContent.trim() !== "" && endDayCount === 28) {
    console.log(firstCell.textContent.trim());
    lastRow2.style.display = "none";
  }

  // 日付をクリックしたときのイベント
  document.querySelectorAll(".clickable").forEach((cell) => {
    cell.addEventListener("click", (e) => {
      const targetCell = e.target.closest("td");
      const selectedDate = targetCell?.dataset.date;
      if (holidays[selectedDate]) {
        alert(
          `選択された日付: ${selectedDate}\n祝日: ${holidays[selectedDate]}`
        );
      } else {
        alert(`選択された日付: ${selectedDate}`);
      }
    });
  });
}

// 初期化
document.addEventListener("DOMContentLoaded", async () => {
  const today = new Date();
  await renderCalendar(today.getFullYear(), today.getMonth() + 1);

  document.getElementById("prev").addEventListener("click", async () => {
    today.setMonth(today.getMonth() - 1);
    await renderCalendar(today.getFullYear(), today.getMonth() + 1);
  });

  document.getElementById("next").addEventListener("click", async () => {
    today.setMonth(today.getMonth() + 1);
    await renderCalendar(today.getFullYear(), today.getMonth() + 1);
  });

  //一年移動
  document.getElementById("year_prev").addEventListener("click", async () => {
    today.setFullYear(today.getFullYear() - 1);
    await renderCalendar(today.getFullYear(), today.getMonth() + 1);
  });

  document.getElementById("year_next").addEventListener("click", async () => {
    today.setFullYear(today.getFullYear() + 1);
    await renderCalendar(today.getFullYear(), today.getMonth() + 1);
  });

  // モーダル内のイベントを設定
  document.getElementById("year_button").addEventListener("click", openModal);
  console.log(document.getElementById("year_button"));

  // 年選択肢を生成する関数
  function populateYearOptions() {
    const yearSelect = document.getElementById("year-select");
    const currentYear = new Date().getFullYear();
    if (!yearSelect) {
      console.error("Year select element not found.");
      return;
    }
    console.log(yearSelect);
    yearSelect.innerHTML = ""; // 初期化
    for (let year = 2100; year >= 1924; year--) {
      const option = document.createElement("option");
      option.value = year;
      option.textContent = year;

      // 現在の年を初期値として選択
      if (year === currentYear) {
        option.selected = true;
      }
      yearSelect.appendChild(option);
    }
  }

  // ページロード時に年選択肢を生成
  document.addEventListener("DOMContentLoaded", () => {
    populateYearOptions();
  });

  populateYearOptions();

  document.getElementById("modal-ok").addEventListener("click", async () => {
    const yearSelect = document.getElementById("year-select");
    const selectedYear = parseInt(yearSelect.value, 10);

    if (!isNaN(selectedYear)) {
      today.setFullYear(selectedYear); // 選択した年に移動
      await renderCalendar(today.getFullYear(), today.getMonth() + 1);
      closeModal(); // モーダルを閉じる
    } else {
      alert("有効な年を選択してください");
    }
  });

  document.getElementById("modal-cancel").addEventListener("click", closeModal);

  // モーダルを開く関数
  function openModal() {
    document.getElementById("modal").style.display = "flex";
  }

  // モーダルを閉じる関数
  function closeModal() {
    document.getElementById("modal").style.display = "none";
  }
});
```