Created
November 26, 2024 12:14
-
-
Save blenderous/bf9f348ea0b3e2b13a0cd4282e79a422 to your computer and use it in GitHub Desktop.
A React component that uses the date-fns library to show a calendar which is highlighted on certain days.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client"; | |
import clsx from "clsx"; | |
import { | |
eachDayOfInterval, | |
eachMonthOfInterval, | |
endOfMonth, | |
format, | |
getDay, | |
isAfter, | |
isBefore, | |
isSameDay, | |
startOfMonth, | |
} from "date-fns"; | |
import { useState } from "react"; | |
export type ShownDay = { | |
date: Date; | |
dayOfWeek: number; // 0 represents Sunday | |
habitDone: boolean; | |
validDay: boolean; // the day is not valid before createdAt day and after today | |
}; | |
export default function MonthlyHighlighterWidget({ | |
days, | |
createdAt, | |
paintColour, | |
}: { | |
days: string[]; | |
createdAt: string; | |
paintColour: string; | |
}) { | |
// today | |
const today = new Date(); | |
// preparation for loading monthly data | |
// | |
// first day among all days to be loaded | |
const firstDay = startOfMonth(createdAt); | |
// last day among all days to be loaded | |
const lastDay = endOfMonth(today); | |
// all the days that need to be shown | |
const allDays = eachDayOfInterval({ | |
start: firstDay, | |
end: lastDay, | |
}); | |
// list of first days of each month | |
const allMonths = eachMonthOfInterval({ | |
start: firstDay, | |
end: lastDay, | |
}); | |
// find the number of months shown | |
const nMonths = allMonths.length; | |
// initial value of ithMonth | |
// (find out what is the index of the current month in the allMonths array) | |
const ithMonthInitialValue = allMonths.findIndex((beginningDayOfMonth) => | |
isSameDay(beginningDayOfMonth, startOfMonth(today)) | |
); | |
// month index | |
const [ithMonth, setIthMonth] = useState(ithMonthInitialValue); | |
// first day of ith month | |
let firstDayOfMonth = allMonths[ithMonth]; | |
let firstDayOfMonthDay = getDay(firstDayOfMonth); // returns 0 for Sunday, 1 for Monday and so on | |
// index of, first day of ith month (in the allDays array) | |
let j = allDays.findIndex((dayElement) => | |
isSameDay(dayElement, firstDayOfMonth) | |
); | |
// last day of ith month | |
let lastDayOfMonth = endOfMonth(allMonths[ithMonth]); | |
// index of, last day of ith month (in the allDays array) | |
let k = allDays.findIndex((dayElement) => | |
isSameDay(dayElement, lastDayOfMonth) | |
); | |
// function to check if given day is a valid day | |
function isValidDay(day: Date, createdAt: string, today: Date) { | |
// if given day is same as createdAt day or if given day is same as today | |
if (isSameDay(day, createdAt) || isSameDay(day, today)) { | |
return true; | |
} | |
// if given day is before createdAt or if given day is after today | |
if (isBefore(day, createdAt) || isAfter(day, today)) { | |
return false; | |
} | |
// if given day is between createdAt and today | |
else { | |
return true; | |
} | |
} | |
// calculate different properties of each of the days in the allDays array | |
const allShownDays: ShownDay[] = allDays.map((day) => ({ | |
date: day, | |
habitDone: days.includes(format(day, "yyyy-MM-dd")), | |
dayOfWeek: getDay(day), | |
validDay: isValidDay(day, createdAt, today), | |
})); | |
// on clicking the "Previous" button | |
const handlePreviousMonth = () => { | |
setIthMonth((prevIthMonth) => | |
prevIthMonth > 0 ? prevIthMonth - 1 : prevIthMonth | |
); | |
}; | |
// on clicking the "Next" button | |
const handleNextMonth = () => { | |
setIthMonth((prevIthMonth) => | |
prevIthMonth < nMonths - 1 ? prevIthMonth + 1 : prevIthMonth | |
); | |
}; | |
return ( | |
<> | |
<div className="bg-mildGreen-100 rounded-xl p-4 mt-4"> | |
<p className="text-themeBrown-800 mb-2"> | |
{/* ithMonth (initially current month) in the format MMMM yyyy */} | |
{format(allMonths[ithMonth], "MMMM yyyy")} | |
</p> | |
{/* List of days (Sunday to Saturday) */} | |
<ul className="list-none grid grid-cols-7 grid-flow-row gap-2"> | |
<li className="text-inactiveGreen-300 text-center">Su</li> | |
<li className="text-inactiveGreen-300 text-center">Mo</li> | |
<li className="text-inactiveGreen-300 text-center">Tu</li> | |
<li className="text-inactiveGreen-300 text-center">We</li> | |
<li className="text-inactiveGreen-300 text-center">Th</li> | |
<li className="text-inactiveGreen-300 text-center">Fr</li> | |
<li className="text-inactiveGreen-300 text-center">Sa</li> | |
{/* slice of ith month among the array allShownDays */} | |
{allShownDays.slice(j, k + 1).map((shownDay, index) => ( | |
<li | |
key={format(shownDay.date, "yyyy-MM-dd")} | |
className={clsx("rounded-md p-[6px] flex place-content-center", { | |
"bg-inactiveGreen-200": shownDay.validDay, | |
"bg-inactiveGreen-100 text-inactiveGreen-300": | |
!shownDay.validDay, | |
"border-2 border-themeBrown-800 p-[14px]": isSameDay( | |
shownDay.date, | |
today | |
), | |
"col-start-1": index === 0 && firstDayOfMonthDay === 0, | |
"col-start-2": index === 0 && firstDayOfMonthDay === 1, | |
"col-start-3": index === 0 && firstDayOfMonthDay === 2, | |
"col-start-4": index === 0 && firstDayOfMonthDay === 3, | |
"col-start-5": index === 0 && firstDayOfMonthDay === 4, | |
"col-start-6": index === 0 && firstDayOfMonthDay === 5, | |
"col-start-7": index === 0 && firstDayOfMonthDay === 6, | |
})} | |
style={ | |
shownDay.habitDone | |
? { backgroundColor: paintColour, color: "white" } | |
: {} | |
} | |
> | |
{/* date */} | |
<p>{format(shownDay.date, "dd")}</p> | |
</li> | |
))} | |
</ul> | |
</div> | |
<p className="mt-4 pb-4 flex justify-between gap-4"> | |
{/* Previous month button */} | |
<button | |
disabled={ithMonth === 0} | |
className={clsx("flex-grow p-4", { | |
"bg-inactiveBrown text-inactiveBrownForeground": ithMonth === 0, | |
})} | |
onClick={() => handlePreviousMonth()} | |
role="button" | |
> | |
Previous | |
</button> | |
{/* Next month button */} | |
<button | |
disabled={ithMonth === nMonths - 1} | |
className={clsx("flex-grow p-4", { | |
"bg-inactiveBrown text-inactiveBrownForeground": | |
ithMonth === nMonths - 1, | |
})} | |
onClick={() => handleNextMonth()} | |
role="button" | |
> | |
Next | |
</button> | |
</p> | |
</> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment