From f276a6276ebd40e2c4b9a4fb9a0e771a2802f05b Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Wed, 23 Aug 2023 21:11:12 -0500 Subject: [PATCH] scrolling finally works --- lib/month.ts | 16 +++- src/calendar/Calendar.module.scss | 1 + src/calendar/Calendar.tsx | 146 +++++++++++++++++++++++------- src/calendar/Month.tsx | 42 ++++++++- 4 files changed, 166 insertions(+), 39 deletions(-) diff --git a/lib/month.ts b/lib/month.ts index 4c9c623..608c5ac 100644 --- a/lib/month.ts +++ b/lib/month.ts @@ -1,9 +1,10 @@ import { addDays, endOfMonth, - getWeek, isSameWeek, startOfMonth, + startOfWeek, + subDays, } from "date-fns"; /** @@ -11,14 +12,19 @@ import { * The last week is not returned if it contains the next month's start date * @param date */ -export function weekBoundsOfMonth(date: Date): [number, number] { +export function weekBoundsOfMonth(date: Date): [Date, Date] { const firstDayOfMonth = startOfMonth(date); - const startWeek = getWeek(firstDayOfMonth); + const startWeek = startOfWeek(firstDayOfMonth); const lastDayOfMonth = endOfMonth(date); const firstDayOfNextMonth = addDays(lastDayOfMonth, 1); - let endWeek = getWeek(lastDayOfMonth); - if (isSameWeek(lastDayOfMonth, firstDayOfNextMonth)) endWeek -= 1; + let endWeek = startOfWeek(lastDayOfMonth); + if (isSameWeek(lastDayOfMonth, firstDayOfNextMonth)) + endWeek = subDays(endWeek, 7); return [startWeek, endWeek]; } + +export function monthNameOf(date: Date): string { + return date.toLocaleDateString("default", { month: "long" }); +} diff --git a/src/calendar/Calendar.module.scss b/src/calendar/Calendar.module.scss index 500ebde..57cddcf 100644 --- a/src/calendar/Calendar.module.scss +++ b/src/calendar/Calendar.module.scss @@ -25,6 +25,7 @@ text-align: right; font-weight: 100; border-bottom: 1px solid gray; + padding: 4px; } } diff --git a/src/calendar/Calendar.tsx b/src/calendar/Calendar.tsx index a2bd546..cf398f4 100644 --- a/src/calendar/Calendar.tsx +++ b/src/calendar/Calendar.tsx @@ -1,18 +1,53 @@ import { forwardRef, useCallback, useEffect, useRef, useState } from "react"; import styles from "./Calendar.module.scss"; -import { addMonths, subMonths } from "date-fns"; +import { + addDays, + addMonths, + differenceInWeeks, + startOfMonth, + subMonths, +} from "date-fns"; import Month from "./Month"; -import { weekBoundsOfMonth } from "lib/month"; +import { monthNameOf, weekBoundsOfMonth } from "lib/month"; const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const NUM_MONTHS_SHOWN = 5; interface CalendarProps { onDateClick?: (_: Date) => void; } export default function Calendar({ onDateClick }: CalendarProps) { + const now = new Date(); + const [viewportHeight, setViewportHeight] = useState(0); - const [scrollyEl, setScrollyEl] = useState(null); + const [scrollyEl, setScrollyEl] = useState(null); + const [centerMonth, setCenterMonth] = useState(startOfMonth(now)); + const [resetScrollToEl, setResetScrollToEl] = useState< + [HTMLElement, number] | null + >(null); + + const range = [-2, -1, 0, 1, 2]; + const [monthsShown, setMonthsShown] = useState( + range.map((x) => addMonths(centerMonth, x)) + ); + + const [firstLoad, setFirstLoad] = useState(true); + useEffect(() => { + if (firstLoad && scrollyEl && viewportHeight > 0) { + // Identify the middle month + const el = scrollyEl.querySelector( + `[data-isodate="${centerMonth.toISOString()}"]` + ); + if (el) { + console.log("first run, scrolling into view", el); + el.scrollIntoView(); + + setFirstLoad(false); + } + } + }, [firstLoad, scrollyEl, viewportHeight]); + const scrollyRef = useCallback((node: HTMLDivElement) => { if (node) { setScrollyEl(node); @@ -20,11 +55,10 @@ export default function Calendar({ onDateClick }: CalendarProps) { } }, []); + // Add resize observer on the viewport useEffect(() => { const observer = new ResizeObserver((entries) => { - for (const entry of entries) { - setViewportHeight(scrollyEl.clientHeight); - } + if (entries && scrollyEl) setViewportHeight(scrollyEl.clientHeight); }); if (scrollyEl) observer.observe(scrollyEl); @@ -34,40 +68,90 @@ export default function Calendar({ onDateClick }: CalendarProps) { }; }, [scrollyEl]); + // Add intersection observer to tell when we scrolled too far up or down + useEffect(() => { + if (!scrollyEl) return; + + const children = [...scrollyEl.children]; + if (children.length !== NUM_MONTHS_SHOWN) throw new Error("wtf"); + const firstChild = children[0]; + const lastChild = children[NUM_MONTHS_SHOWN - 1]; + + const observer = new IntersectionObserver((entries) => { + let newMonthsShown = monthsShown; + let scrollUp = false; + + for (const entry of entries) { + if (!entry.isIntersecting) continue; + + if (entry.target === firstChild) { + console.log("intersected"); + const firstChildDate = new Date(firstChild.dataset.isodate); + const prevMonth = subMonths(firstChildDate, 1); + newMonthsShown = [prevMonth, ...monthsShown]; + newMonthsShown = newMonthsShown.slice(0, NUM_MONTHS_SHOWN); + + setResetScrollToEl([ + firstChild, + scrollyEl.scrollTop - firstChild.offsetTop, + ]); + } else if (entry.target === lastChild) { + const lastChildDate = new Date(lastChild.dataset.isodate); + const nextMonth = addMonths(lastChildDate, 1); + newMonthsShown = [...monthsShown, nextMonth]; + newMonthsShown = newMonthsShown.slice(-NUM_MONTHS_SHOWN); + } + } + + setMonthsShown(newMonthsShown); + }); + + console.log("observing", firstChild, lastChild); + observer.observe(firstChild); + observer.observe(lastChild); + + return () => { + observer.unobserve(firstChild); + observer.unobserve(lastChild); + }; + }, [monthsShown, scrollyEl]); + + useEffect(() => { + if (!resetScrollToEl) return; + if (!scrollyEl) return; + + console.log("current scroll top", scrollyEl.scrollTop); + console.log("params", resetScrollToEl); + const [el, height] = resetScrollToEl; + scrollyEl.scrollTop = el.offsetTop + height; + console.log("new scroll top", scrollyEl.scrollTop, el.offsetTop + height); + setResetScrollToEl(null); + }, [resetScrollToEl, scrollyEl]); + console.log("SCROLLY REF IS", scrollyRef); - const [middleMonthEl, setMiddleMonthEl] = useState(null); - const middleMonthRef = useCallback((node: HTMLDivElement) => { - if (node) setMiddleMonthEl(node); - }, []); // The calendar should always show 6 rows const dateCellHeight = viewportHeight / 6; - const now = new Date(); - const [currentlyShownMonth, setCurrentlyShownMonth] = useState(now); - const monthName = currentlyShownMonth.toLocaleDateString("default", { - month: "long", - }); + const monthName = monthNameOf(centerMonth); - const prevMonth = subMonths(currentlyShownMonth, 1); - const nextMonth = addMonths(currentlyShownMonth, 1); - const [monthsShown, setMonthsShown] = useState([ - prevMonth, - currentlyShownMonth, - nextMonth, - ]); - - useEffect(() => { - middleMonthEl?.scrollIntoView(); - }, [middleMonthRef]); + // useEffect(() => { + // middleMonthEl?.scrollIntoView(); + // }, [middleMonthRef]); return (
{monthName} - - {currentlyShownMonth.getFullYear()} + {centerMonth.getFullYear()} + (months shown:{" "} + {JSON.stringify( + monthsShown.map((x) => + x.toLocaleDateString("default", { month: "long" }) + ) + )} + )
@@ -83,15 +167,15 @@ export default function Calendar({ onDateClick }: CalendarProps) { {monthsShown.map((month) => { // How many rows will this month take up? const [startWeek, endWeek] = weekBoundsOfMonth(month); - const numWeeks = endWeek - startWeek + 1; + const numWeeks = differenceInWeeks(endWeek, startWeek); const dateGridHeight = dateCellHeight * numWeeks; const props = { month, dateGridHeight, }; - const ref = - (month === currentlyShownMonth && middleMonthRef) || undefined; + const ref = undefined; + // (month === currentlyShownMonth && middleMonthRef) || undefined; return ( void; } +const monthColors = [ + "#AA3939", + "#FFAAAA", + "#D46A6A", + "#801515", + "#550000", + "#AA6C39", + "#FFD1AA", + "#D49A6A", + "#804515", + "#552700", + "#226666", + "#669999", + "#407F7F", + "#0D4D4D", + "#003333", + "#2D882D", + "#88CC88", + "#55AA55", + "#116611", + "#004400", +]; + function Month({ month, dateGridHeight, onDateClick }: MonthProps, ref) { const now = new Date(); const monthName = month.toLocaleString("default", { month: "long" }); const [startWeek, endWeek] = weekBoundsOfMonth(month); - const numWeeks = endWeek - startWeek + 1; + const numWeeks = differenceInWeeks(endWeek, startWeek) + 1; const weeksArr = Array(numWeeks) .fill(0) .map((_, i) => addDays(startOfMonth(month), 7 * i)); @@ -39,17 +63,29 @@ function Month({ month, dateGridHeight, onDateClick }: MonthProps, ref) { className={styles.dateGrid} style={{ height: `${dateGridHeight}px` }} ref={ref} + data-isodate={month} > {datesArr.map((date) => { const isFirst = isSameDay(date, startOfMonth(date)); const isToday = isSameDay(date, now); return ( -
+
onDateClick?.(date)} > - + {isFirst && monthName} {date.getDate()}