初めに
reactで綺麗でカスタマイズできるカレンダーを作ろうと挑戦して出来上がったもの残します。
なにせ初心者の66歳のおじいちゃん作ですので、効率性が悪かったり、不要なコードがあると思いますので、お許しください。
構成はreactのCalendar.jsとcalendar.scssです。
日付を押すと、その日のデータが取得できます。
左上の「2025/1」のところをクリックすると、年の選択ができます。
完成品
calendar.js
import React, { useEffect, useState } from "react";
import "./Calendar.scss";
const Calendar = () => {
const [year, setYear] = useState(new Date().getFullYear());
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
const [month, setMonth] = useState(new Date().getMonth());
const [holidays, setHolidays] = useState({});
const [showModal, setShowModal] = useState(false);
const [selectDay, setSelectDay] = useState("");
const getDaysInMonth = (year, month) => {
return new Date(year, month + 1, 0).getDate();
};
const getFirstDayOfMonth = (year, month) => {
return new Date(year, month, 1).getDay();
};
const getPrevMonthDays = (year, month) => {
const prevMonth = month === 0 ? 11 : month - 1;
const prevMonthYear = month === 0 ? year - 1 : year;
const prevMonthDays = getDaysInMonth(prevMonthYear, prevMonth);
const prevMonthFirstDay = getFirstDayOfMonth(prevMonthYear, prevMonth);
return { prevMonthDays, prevMonthFirstDay };
};
const getNextMonthDays = (year, month) => {
const nextMonth = month === 11 ? 0 : month + 1;
const nextMonthYear = month === 11 ? year + 1 : year;
const nextMonthDays = getDaysInMonth(nextMonthYear, nextMonth);
const nextMonthFirstDay = getFirstDayOfMonth(nextMonthYear, nextMonth);
return { nextMonthDays, nextMonthFirstDay };
};
const daysInMonth = getDaysInMonth(year, month);
const firstDayOfMonth = getFirstDayOfMonth(year, month);
const { prevMonthDays, prevMonthFirstDay } = getPrevMonthDays(year, month);
const { nextMonthDays, nextMonthFirstDay } = getNextMonthDays(year, month);
const prevRemainDays = firstDayOfMonth;
const days = [];
let l = 0;
if (firstDayOfMonth !== 6) {
for (let i = prevMonthDays - firstDayOfMonth; i < prevMonthDays + 1; i++) {
l += 1;
// 日付の形式を整える
const date = i;
const targetmonth = "prevMonth";
const formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(
date
).padStart(2, "0")}`;
// その日が祝日かどうかを確認
const isHoliday = holidays[formattedDate] || null;
// 配列にオブジェクトを追加
days.push({
day: date, // 日付
isHoliday: !!isHoliday, // 祝日かどうか (真偽値)
holidayName: isHoliday || null, // 祝日名または種別
targetmonth: targetmonth, // 月の分類 (prevMonth, nowMonth, nextMonth)
formattedDate: formattedDate, // 日付の文字列形式
});
}
}
for (let i = 1; i <= daysInMonth; i++) {
const date = i;
const targetmonth = "nowMonth";
const formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(
date
).padStart(2, "0")}`;
const formattedDateForHoliday = `${year + Math.floor(month / 12)}-${String(
(month % 12) + 1
).padStart(2, "0")}-${String(date).padStart(2, "0")}`;
// その日が祝日かどうかを確認
const isHoliday = holidays[formattedDateForHoliday] || null;
// 配列にオブジェクトを追加
days.push({
day: date, // 日付
isHoliday: !!isHoliday, // 祝日かどうか (真偽値)
holidayName: isHoliday || null, // 祝日名または種別
targetmonth: targetmonth, // 月の分類 (prevMonth, nowMonth, nextMonth)
formattedDate: formattedDate, // 日付の文字列形式
});
}
for (let i = 1; i <= 6 - nextMonthFirstDay; i++) {
let nextmonth = 0;
let nextmonthyear = 0;
if (daysInMonth + l === 35 || daysInMonth + l === 28) {
let l = null;
} else {
const date = i;
const targetmonth = "nextMonth";
if (month === 11) {
nextmonth = 0;
nextmonthyear = year + 1;
} else {
nextmonth = month + 2;
nextmonthyear = year;
}
const formattedDate = `${nextmonthyear}-${String(nextmonth).padStart(
2,
"0"
)}-${String(date).padStart(2, "0")}`;
console.log("月だよ" + nextmonth);
// その日が祝日かどうかを確認
const isHoliday = holidays[formattedDate] || null;
// 配列にオブジェクトを追加
days.push({
day: date, // 日付
isHoliday: !!isHoliday, // 祝日かどうか (真偽値)
holidayName: isHoliday || null, // 祝日名または種別
targetmonth: targetmonth, // 月の分類 (prevMonth, nowMonth, nextMonth)
formattedDate: formattedDate, // 日付の文字列形式
});
}
}
const handleYearChange = (direction) => {
setYear((prev) => prev + direction);
};
const handleMonthChange = (direction) => {
setMonth((prev) => {
const newMonth = prev + direction;
if (newMonth < 0) {
setYear((y) => y - 1);
return 11;
} else if (newMonth > 11) {
setYear((y) => y + 1);
return 0;
}
return newMonth;
});
};
useEffect(() => {
const fetchHolidays = async (year) => {
const response = await fetch(
`https://holidays-jp.github.io/api/v1/date.json`
);
if (response.ok) {
const data = await response.json();
const filteredHolidays = {};
for (const [date, name] of Object.entries(data)) {
if (date.startsWith(year) || date.startsWith(year + 1)) {
filteredHolidays[date] = name;
}
}
setHolidays(filteredHolidays);
} else {
console.error("祝日データの取得に失敗しました。");
}
};
fetchHolidays(year);
}, [year]);
const openModal = () => {
setShowModal(true);
};
const closeModal = () => {
setShowModal(false);
};
const years = [];
const yearSelect = () => {
for (let year = 2100; year >= 1924; year--) {
years.push(year);
}
};
yearSelect();
const handleChange = (event) => {
setSelectedYear(event.target.value);
};
const changeSelectYear = () => {
setYear(selectedYear);
setShowModal(false);
};
const changeSelectDay = (day) => {
if (day.targetmonth === "prevMonth") {
let premonth = "";
let preyear = "";
if (month === 0) {
premonth = 12;
preyear = year - 1;
} else {
premonth = month;
preyear = year;
}
const newSelectDay = `${preyear}-${String(premonth).padStart(
2,
"0"
)}-${String(day.day).padStart(2, "0")}`;
setSelectDay(newSelectDay);
} else if (day.targetmonth === "nextMonth") {
let nextmonth = "";
let nextyear = "";
if (month === 11) {
nextmonth = 1;
nextyear = year + 1;
} else {
nextmonth = month + 2;
nextyear = year;
}
const newSelectDay = `${nextyear}-${String(nextmonth).padStart(
2,
"0"
)}-${String(day.day).padStart(2, "0")}`;
setSelectDay(newSelectDay);
} else {
const newSelectDay = `${year}-${String(month + 1).padStart(
2,
"0"
)}-${String(day.day).padStart(2, "0")}`;
setSelectDay(newSelectDay);
}
};
// selectDayが変更された時にアラートを表示
useEffect(() => {
if (selectDay) {
alert(selectDay);
}
}, [selectDay]);
return (
<>
<div className="calendar-container">
<div className="calendar-content">
<button id="year_button" onClick={openModal}>
{year} / {month + 1 > 12 ? 1 : month + 1}
</button>
<button
className="calendar-year-prev"
onClick={() => handleYearChange(-1)}
>
▲
</button>
<button
className="calendar-year-next"
onClick={() => handleYearChange(1)}
>
▼
</button>
<button
className="calendar-prev"
onClick={() => handleMonthChange(-1)}
>
◀◀
</button>
<button
className="calendar-next"
onClick={() => handleMonthChange(1)}
>
▶▶
</button>
<div className="calendar-field">
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map(
(day, index) => (
<div
className={`calendar-label ${
index === 0 ? "sunday" : index === 6 ? "saturday" : ""
}`}
key={day}
>
{day}
</div>
)
)}
{days.map((day, index) => {
const dayOfWeek = index % 7;
const dateKey = `${year}-${String(month + 1).padStart(
2,
"0"
)}-${String(day.date).padStart(2, "0")}`;
return (
<div
className={`calendar-cell ${
day.day && dayOfWeek === 0
? "sunday"
: day.day && dayOfWeek === 6
? "saturday"
: "weekday"
} ${day.isHoliday ? "holiday" : ""} ${
day.targetmonth === "prevMonth" ? "prevmonth" : ""
} ${day.targetmonth === "nextMonth" ? "nextmonth" : ""}`}
key={index}
onClick={() => day.day && changeSelectDay(day)}
>
{day.day || ""}
<div className="holiday-name">{day.holidayName || ""}</div>
</div>
);
})}
</div>
<div id="modal" style={{ display: showModal ? "" : "none" }}>
<div className="modal-container">
<select
id="year-select"
value={selectedYear}
onChange={handleChange}
>
{years.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
<button id="modal-ok" onClick={changeSelectYear}>
OK
</button>
<button id="modal-cancel" onClick={closeModal}>
Cancel
</button>
</div>
</div>
<div className="selected-day">
{selectDay && <p>選択した日: {selectDay}</p>}
</div>
</div>
</div>
</>
);
};
export default Calendar;
calendar.scss
/* 全体のコンテナ */
.calendar-container {
width: 100%;
}
/* カレンダーの内容 */
.calendar-content {
position: relative;
width: 700px;
height: 660px;
padding: 10px;
font-size: 18px;
font-style: italic;
margin: 10px auto;
justify-content: center;
text-align: center;
@media (max-width: 576px) {
margin: 5px auto;
width: 98%;
height: 400px;
}
}
/* 年変更ボタン */
#year_button {
position: absolute;
top: 28px;
left: 12px;
font-size: 30px;
border: none;
padding: 1px 15px 0;
border-radius: 5px;
background-color: #0074d0;
color: white;
font-style: italic;
box-shadow: 3px 3px 3px -3px black;
transition: background-color 0.3s ease, color 0.3s ease;
@media (max-width: 576px) {
top: 12px;
left: 18px;
font-size: 20px;
}
}
#year_button:hover {
background-color: #6df049;
}
/* 年移動ボタン */
.calendar-year-prev,
.calendar-year-next {
border: none;
font-size: 12px;
font-style: italic;
position: absolute;
transform: scale(5, 1);
color: #0074d0;
background-color: transparent;
@media (max-width: 576px) {
font-size: 10px;
}
}
.calendar-year-prev {
top: 15px;
left: 82px;
@media (max-width: 576px) {
top: 0;
left: 65px;
}
}
.calendar-year-next {
top: 64px;
left: 82px;
@media (max-width: 576px) {
top: 36.6px;
left: 65px;
}
}
.calendar-year-prev:hover,
.calendar-year-next:hover {
color: #6df049;
}
/* 月移動ボタン */
.calendar-prev,
.calendar-next {
font-size: 20px;
font-style: italic;
position: absolute;
color: #0074d0;
background-color: transparent;
border: none;
top: 54px;
@media (max-width: 576px) {
font-size: 14px;
top: 28px;
}
}
.calendar-prev {
right: 62px;
@media (max-width: 576px) {
right: 44px;
}
}
.calendar-next {
right: 10px;
}
.calendar-prev:hover,
.calendar-next:hover {
color: #6df049;
}
/* カレンダーのフィールド */
.calendar-field {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin: 80px auto !important;
width: 100%;
border-bottom: 3px solid #0074d0;
border-left: 3px solid #0074d0;
@media (max-width: 576px) {
margin: 44px auto !important;
}
}
/* 曜日ラベル */
.calendar-label {
display: flex;
align-items: center;
justify-content: center;
height: 50px !important;
color: white;
text-align: center;
border-right: 3px solid white;
border-top: 3px solid #0074d0;
margin: 0 !important;
font-size: 22px;
font-weight: 400;
background-color: #0074d0;
@media (max-width: 576px) {
font-size: 14px;
}
}
.calendar-label.sunday {
color: red;
}
.calendar-label.saturday {
color: blue;
border-right: 3px solid #0074d0;
}
/* カレンダーのセル */
.calendar-cell {
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
height: 59px;
background-color: white;
border-right: 3px solid #0074d0;
border-top: 3px solid #0074d0;
margin: 0;
@media (max-width: 576px) {
font-size: 18px;
height: 54px;
}
}
.calendar-cell.sunday {
color: red;
}
.calendar-cell.sunday.prevmonth,
.calendar-cell.sunday.nextmonth {
color: rgb(200, 36, 36);
font-size: 14px;
background-color: rgb(228, 231, 230);
}
.calendar-cell.saturday {
color: blue;
}
.calendar-cell.saturday.prevmonth,
.calendar-cell.saturday.nextmonth {
color: rgb(29, 29, 208);
font-size: 14px;
background-color: rgb(228, 231, 230);
}
.calendar-cell.weekday {
color: black;
}
.calendar-cell.weekday.prevmonth,
.calendar-cell.weekday.nextmonth {
color: rgb(108, 105, 105);
font-size: 14px;
background-color: rgb(228, 231, 230);
}
.calendar-cell.weekday.holiday,
.calendar-cell.saturday.holiday {
color: red;
position: relative;
}
/* 祝日名 */
div.holiday-name {
position: absolute;
font-size: 8px !important;
color: red;
text-align: center;
top: 40px;
@media (max-width: 576px) {
font-size: 6px !important;
top: 40px;
}
}
/* モーダルスタイル */
#modal {
position: absolute;
width: 250px;
height: 59px;
margin-top: 0;
font-size: 24px;
background-color: white;
border: solid 4px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
top: 16px;
z-index: 2;
}
.modal-container {
display: flex;
padding: 0 !important;
}
/* モーダルのボタンとセレクトボックス */
select#year-select {
font-size: 21px;
font-style: italic;
margin: 0 5px;
padding: 0 10px;
border: none;
background-color: transparent;
}
button#modal-ok,
button#modal-cancel {
border: none;
background-color: #0074d0;
color: white;
font-size: 19px;
margin: 0 5px;
padding: 1px 7px 1px 3px !important;
font-style: italic;
border-radius: 5px;
}
button#modal-cancel {
background-color: red;
}