0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

reactで綺麗?でカスタマイズできるカレンダー

Last updated at Posted at 2025-01-08

初めに

 reactで綺麗でカスタマイズできるカレンダーを作ろうと挑戦して出来上がったもの残します。
 なにせ初心者の66歳のおじいちゃん作ですので、効率性が悪かったり、不要なコードがあると思いますので、お許しください。

構成はreactのCalendar.jsとcalendar.scssです。

日付を押すと、その日のデータが取得できます。
左上の「2025/1」のところをクリックすると、年の選択ができます。

完成品

スクリーンショット 2025-01-08 161416.png

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;
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?