Skip to content

Instantly share code, notes, and snippets.

@blenderous
Created November 26, 2024 12:14
Show Gist options
  • Save blenderous/bf9f348ea0b3e2b13a0cd4282e79a422 to your computer and use it in GitHub Desktop.
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.
"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