Skip to content

Instantly share code, notes, and snippets.

@edisonywh
Created May 29, 2021 22:24
Show Gist options
  • Save edisonywh/dd8b70a2700a359c8f274368d155345a to your computer and use it in GitHub Desktop.
Save edisonywh/dd8b70a2700a359c8f274368d155345a to your computer and use it in GitHub Desktop.
import React, { useMemo, useState } from "react";
import { format, addDays, startOfWeek, endOfWeek, startOfMonth, endOfMonth, isSameMonth, addMonths, subMonths, parseISO, Interval, isFuture, getTime, isToday, isSameDay } from "date-fns";
import { classNames, isoToUtc } from "../Shared/utils";
import { Habit, HabitType, Progress, Streak, Entry } from "../Shared/types";
import { isWithinInterval } from "date-fns/esm";
import CircularProgressBar from "./CircularProgressBar";
type Prop = {
habit: Habit,
onDateClick: Function,
}
export default function Calendar(prop: Prop) {
const [month, updateMonth] = useState(new Date())
const intervals: Interval[] = useMemo(() => {
return prop.habit.streaks.map((s: Streak) => {
return { start: isoToUtc(s.start), end: isoToUtc(s.end) };
})
}, [prop.habit.streaks])
const starts: string[] = useMemo(() => {
return intervals.map((i: Interval) => {
const start = i.start as Date
return start.toDateString()
})
}, [prop.habit.streaks])
const ends = useMemo(() => {
return intervals.map((i: Interval) => {
const start = i.end as Date
return start.toDateString()
})
}, [prop.habit.streaks])
const skipped = useMemo(() => {
return prop.habit.skipped.map((e: Entry) => {
return isoToUtc(e.date).toDateString();
})
}, [prop.habit.skipped])
const entries = useMemo(() => {
return prop.habit.entries.map((e: Entry) => {
return isoToUtc(e.date).toDateString();
})
}, [prop.habit.skipped])
const onDateClick = (day: Date) => {
let completed: boolean
if (prop.habit.type == HabitType.Weekly) {
completed = isEntry(day) || isInProgress(day)
} else {
completed = isBetweenStreak(day) || isStartOfStreak(day) || isEndOfStreak(day)
}
prop.onDateClick(day, completed)
};
const nextMonth = () => {
updateMonth(addMonths(month, 1))
};
const prevMonth = () => {
updateMonth(subMonths(month, 1))
};
const renderHeader = () => {
const dateFormat = "MMMM, yyyy";
return (
<div className="text-xl font-semibold flex justify-between items-center mb-4">
<div className="">
<span>{format(month, dateFormat)}</span>
</div>
<div className="flex justify-around ml-2">
<div className="cursor-pointer p-2" onClick={prevMonth}>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
</svg>
</div>
<div className="cursor-pointer rounded p-2" onClick={nextMonth}>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
);
}
const renderDays = () => {
const dateFormat = "EEEEE";
const days = [];
let startDate = startOfWeek(month, { weekStartsOn: 1 });
for (let i = 0; i < 7; i++) {
days.push(
<div className="text-center" key={i}>
{format(addDays(startDate, i), dateFormat)}
</div>
);
}
return <div className="text-sm text-gray-400 grid grid-cols-7 mb-4">{days}</div>;
}
const isSkipped = (day: Date) => {
return skipped.includes(day.toDateString())
}
const isEntry = (day: Date) => {
return entries.includes(day.toDateString())
}
const isBetweenStreak = (day: Date) => {
return intervals.some((i: Interval) => {
return isWithinInterval(day, i)
})
}
const isStartOfStreak = (day: Date) => {
return starts.includes(day.toDateString())
}
const isEndOfStreak = (day: Date) => {
return ends.includes(day.toDateString())
}
const isInProgress = (day: Date) => {
return prop.habit.progresses.some((p: Progress) => {
return isSameDay(parseISO(p.date), day)
})
}
const getDayProgress = (day: Date) => {
return prop.habit.progresses.find((p: Progress) => {
return isSameDay(parseISO(p.date), day)
})
}
const renderCells = () => {
const monthStart = startOfMonth(month);
const monthEnd = endOfMonth(monthStart);
const startDate = startOfWeek(monthStart, { weekStartsOn: 1 });
const endDate = endOfWeek(monthEnd);
const dateFormat = "d";
let days = [];
let day = startDate;
let formattedDate = "";
// TODO: Refactor date rendering
while (day <= endDate) {
for (let i = 0; i < 7; i++) {
formattedDate = format(day, dateFormat);
const cloneDay = day;
if (prop.habit.type == HabitType.Daily && isInProgress(day)) {
days.push(
<div
key={getTime(day)}
className={
classNames(
!isSameMonth(day, monthStart) || isFuture(day) ? "text-gray-300" : ""
, isToday(day) ? "font-bold" : ""
, "flex justify-center items-center h-10"
)
}
onClick={() => onDateClick(cloneDay)}
>
<CircularProgressBar initialAnimation={true} radius={18} steps={prop.habit.times} progress={getDayProgress(day)?.current as number}>
{formattedDate}
</CircularProgressBar>
</div>
)
} else if (isSkipped(day)) {
days.push(
<div
key={getTime(day)}
className={
classNames(
!isSameMonth(day, monthStart) || isFuture(day) ? "text-gray-300" : ""
, isToday(day) ? "font-bold" : ""
, isStartOfStreak(day) ? "rounded-l-full" : ""
, isEndOfStreak(day) ? "rounded-r-full" : ""
, !isBetweenStreak(day) && !isStartOfStreak(day) && !isEndOfStreak(day) ? "rounded-l-full rounded-r-full" : ""
, "bg-secondary relative flex justify-center items-center h-10"
)
}
onClick={() => onDateClick(cloneDay)}
>
<p className="text-primary text-2xl absolute -top-1.5 right-2">{'\u00b7'}</p>
{formattedDate}
</div>
)
} else if (prop.habit.type == HabitType.Weekly) {
if (isEntry(day)) {
days.push(
<div
className={
classNames(
!isSameMonth(day, monthStart) || isFuture(day) ? "text-gray-300" : ""
, isToday(day) ? "font-bold" : ""
, isBetweenStreak(day) ? "bg-primary text-white" : ""
, isStartOfStreak(day) ? "bg-primary text-white rounded-l-full" : ""
, isEndOfStreak(day) ? "bg-primary text-white rounded-r-full" : ""
, !isBetweenStreak(day) && !isStartOfStreak(day) && !isEndOfStreak(day) ? "bg-primary rounded-l-full rounded-r-full text-white" : ""
, "flex justify-center items-center h-10"
)
}
key={getTime(day)}
onClick={() => onDateClick(cloneDay)}
>
{formattedDate}
</div >
);
} else {
days.push(
<div
className={
classNames(
!isSameMonth(day, monthStart) || isFuture(day) ? "text-gray-300" : ""
, isToday(day) ? "font-bold" : ""
, isBetweenStreak(day) ? "text-white" : ""
, isBetweenStreak(day) ? "bg-secondary text-white" : ""
, isStartOfStreak(day) ? "bg-secondary text-white rounded-l-full" : ""
, isEndOfStreak(day) ? "bg-secondary text-white rounded-r-full" : ""
, "flex justify-center items-center h-10"
)
}
key={getTime(day)}
onClick={() => onDateClick(cloneDay)}
>
{formattedDate}
</div >
);
}
} else {
days.push(
<div
className={
classNames(
!isSameMonth(day, monthStart) || isFuture(day) ? "text-gray-300" : ""
, isToday(day) ? "font-bold" : ""
, isBetweenStreak(day) ? "bg-primary text-white" : ""
, isStartOfStreak(day) ? "bg-primary text-white rounded-l-full" : ""
, isEndOfStreak(day) ? "bg-primary text-white rounded-r-full" : ""
, "flex justify-center items-center h-10"
)
}
key={getTime(day)}
onClick={() => onDateClick(cloneDay)}
>
{formattedDate}
</div >
);
}
day = addDays(day, 1);
}
}
return <div className="grid grid-flow-row grid-cols-7 grid-rows-5 gap-y-1.5">{days}</div>;
}
return (
<div className="my-4">
{renderHeader()}
{renderDays()}
{renderCells()}
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment