Created
May 29, 2021 22:24
-
-
Save edisonywh/dd8b70a2700a359c8f274368d155345a to your computer and use it in GitHub Desktop.
This file contains 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
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