diff --git a/lib/ScrollContainer.module.scss b/lib/ScrollContainer.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/lib/ScrollContainer.tsx b/lib/ScrollContainer.tsx new file mode 100644 index 0000000..775dfd2 --- /dev/null +++ b/lib/ScrollContainer.tsx @@ -0,0 +1,102 @@ +import { + ForwardedRef, + forwardRef, + useCallback, + useEffect, + useState, +} from "react"; +import styles from "./ScrollContainer.module.scss"; +import { subMonths } from "date-fns"; + +interface ScrollContainerProps { + direction: "horizontal" | "vertical"; + + /** A list of things managed */ + list: T[]; + + generatePreviousObject: () => void; + generateNextObject: () => void; +} +/** + * Generic infinite scrolling container + */ +function ScrollContainer( + { direction, list }: ScrollContainerProps, + ref: ForwardedRef +) { + const [resetScrollToEl, setResetScrollToEl] = useState< + [HTMLElement, number] | null + >(null); + + // Viewport state + const [viewportSize, setViewportSize] = useState<[number, number]>([0, 0]); + const [viewportWidth, viewportHeight] = viewportSize; + const viewportMetric = + direction === "horizontal" ? viewportWidth : viewportHeight; + + // Scroll element state + const [scrollyEl, setScrollyEl] = useState(null); + const scrollyRef = useCallback((node: HTMLDivElement) => { + if (node) { + setScrollyEl(node); + setViewportSize([node.clientWidth, node.clientHeight]); + } + }, []); + + // Add resize observer on the viewport + useEffect(() => { + const observer = new ResizeObserver((entries) => { + if (entries && scrollyEl) { + setViewportSize([scrollyEl.clientWidth, scrollyEl.clientHeight]); + } + }); + + if (scrollyEl) observer.observe(scrollyEl); + + return () => { + if (scrollyEl) observer.unobserve(scrollyEl); + }; + }, [scrollyEl]); + + // Add intersection observer to tell when we scrolled too far + useEffect(() => { + if (!scrollyEl) return; + + const children: HTMLElement[] = [...scrollyEl.children]; + const firstChild = children[0]; + const lastChild = children[children.length - 1]; + + const observer = new IntersectionObserver((entries) => { + if (viewportMetric === 0) return; + let newList = [...list]; + + 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); + newList = [prevMonth, ...monthsShown]; + newList = 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); + }); + }); + + return
; +} + +export default forwardRef(ScrollContainer); diff --git a/src/calendar/Calendar.module.scss b/src/calendar/Calendar.module.scss index d7f9c89..21bd450 100644 --- a/src/calendar/Calendar.module.scss +++ b/src/calendar/Calendar.module.scss @@ -22,31 +22,3 @@ gap: 12px; } } - -.daysOfWeek { - display: grid; - grid-template-columns: repeat(7, 1fr); - - div { - flex-grow: 1; - text-align: right; - font-weight: 100; - border-bottom: 1px solid gray; - padding: 4px; - } -} - -.scrollyPart { - flex-grow: 1; - - overflow-y: scroll; /* Add the ability to scroll the y axis */ - - /* Hide the scrollbar for Internet Explorer, Edge and Firefox */ - -ms-overflow-style: none; /* Internet Explorer and Edge */ - scrollbar-width: none; /* Firefox */ - - /* Hide the scrollbar for Chrome, Safari and Opera */ - &::-webkit-scrollbar { - display: none; - } -} diff --git a/src/calendar/Calendar.tsx b/src/calendar/Calendar.tsx index 29d41ab..16429e5 100644 --- a/src/calendar/Calendar.tsx +++ b/src/calendar/Calendar.tsx @@ -1,225 +1,75 @@ -import { forwardRef, useCallback, useEffect, useRef, useState } from "react"; +import { Dispatch, SetStateAction, createContext, useState } from "react"; import styles from "./Calendar.module.scss"; -import { - addDays, - addMonths, - differenceInWeeks, - isSameMonth, - startOfMonth, - subMonths, -} from "date-fns"; -import Month from "./Month"; -import { monthNameOf, weekBoundsOfMonth } from "lib/month"; -import { useJournals } from "lib/queries"; import ToggleSwitch from "src/widgets/ToggleSwitch"; import InputBox from "src/widgets/InputBox"; import Button from "src/widgets/Button"; +import MonthCalendar from "./MonthCalendar"; -const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -const NUM_MONTHS_SHOWN = 5; +interface ICalendarContext { + onDateClick: (_: Date) => void; + setCurrentTitle: Dispatch>; +} + +export const CalendarContext = createContext(null!); interface CalendarProps { - onDateClick?: (_: Date) => void; + onDateClick: (_: Date) => void; } export default function Calendar({ onDateClick }: CalendarProps) { - const now = new Date(); + const [currentTitle, setCurrentTitle] = useState(<>); - const [viewportHeight, setViewportHeight] = useState(0); - const [scrollyEl, setScrollyEl] = useState(null); - const [resetScrollToEl, setResetScrollToEl] = useState< - [HTMLElement, number] | null - >(null); - - const range = [-2, -1, 0, 1, 2]; - const [monthsShown, setMonthsShown] = useState( - range.map((x) => addMonths(startOfMonth(now), x)) - ); - const centerMonth = monthsShown[2]; - - const startDate = monthsShown[0]; - const endDate = monthsShown[NUM_MONTHS_SHOWN - 1]; - - 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); - setViewportHeight(node.clientHeight); - } - }, []); - - // Add resize observer on the viewport - useEffect(() => { - const observer = new ResizeObserver((entries) => { - if (entries && scrollyEl) setViewportHeight(scrollyEl.clientHeight); - }); - - if (scrollyEl) observer.observe(scrollyEl); - - return () => { - if (scrollyEl) observer.unobserve(scrollyEl); - }; - }, [scrollyEl]); - - // Add intersection observer to tell when we scrolled too far up or down - useEffect(() => { - if (!scrollyEl) return; - - const children: HTMLElement[] = [...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); - - // The calendar should always show 6 rows - const dateCellHeight = viewportHeight / 6; - - const monthName = monthNameOf(centerMonth); - - // useEffect(() => { - // middleMonthEl?.scrollIntoView(); - // }, [middleMonthRef]); + const scrollToToday = () => {}; const [searchQuery, setSearchQuery] = useState(""); - const [currentLayout, setCurrentLayout] = useState("Month"); + const [currentMode, setCurrentMode] = useState("Month"); const [currentView, setCurrentView] = useState("Calendar"); + const contextValue = { onDateClick, setCurrentTitle }; + return ( -
-
-
-
- {monthName} - {centerMonth.getFullYear()} -
+ +
+
+
+
{currentTitle}
-
- -
- -
- - - - - -
-
- -
- {daysOfWeek.map((day) => ( -
- {day} +
+
- ))} + +
+ + + + + +
+
-
-
- {monthsShown.map((month) => { - // How many rows will this month take up? - const [startWeek, endWeek] = weekBoundsOfMonth(month); - const numWeeks = differenceInWeeks(endWeek, startWeek); - const dateGridHeight = dateCellHeight * numWeeks; - - const props = { - month, - dateGridHeight, - dateCellHeight, - isActive: isSameMonth(centerMonth, month), - }; - const ref = undefined; - // (month === currentlyShownMonth && middleMonthRef) || undefined; - return ( - - ); - })} +
-
+
); } + +function ActualCalendar({ mode }) { + switch (mode) { + default: + return ; + } +} diff --git a/src/calendar/MonthCalendar.module.scss b/src/calendar/MonthCalendar.module.scss new file mode 100644 index 0000000..f7eeebb --- /dev/null +++ b/src/calendar/MonthCalendar.module.scss @@ -0,0 +1,27 @@ +.daysOfWeek { + display: grid; + grid-template-columns: repeat(7, 1fr); + + div { + flex-grow: 1; + text-align: right; + font-weight: 100; + border-bottom: 1px solid gray; + padding: 4px; + } +} + +.scrollyPart { + flex-grow: 1; + + overflow-y: scroll; /* Add the ability to scroll the y axis */ + + /* Hide the scrollbar for Internet Explorer, Edge and Firefox */ + -ms-overflow-style: none; /* Internet Explorer and Edge */ + scrollbar-width: none; /* Firefox */ + + /* Hide the scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } +} diff --git a/src/calendar/MonthCalendar.tsx b/src/calendar/MonthCalendar.tsx new file mode 100644 index 0000000..fa4c482 --- /dev/null +++ b/src/calendar/MonthCalendar.tsx @@ -0,0 +1,178 @@ +import { useCallback, useContext, useEffect, useState } from "react"; +import styles from "./MonthCalendar.module.scss"; +import { + addMonths, + differenceInWeeks, + isSameMonth, + startOfMonth, + subMonths, +} from "date-fns"; +import { monthNameOf, weekBoundsOfMonth } from "lib/month"; +import Month from "./Month"; +import { CalendarContext } from "./Calendar"; + +const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const NUM_MONTHS_SHOWN = 5; + +export default function MonthCalendar() { + const { onDateClick, setCurrentTitle } = useContext(CalendarContext); + const now = new Date(); + + const [resetScrollToEl, setResetScrollToEl] = useState< + [HTMLElement, number] | null + >(null); + const [viewportHeight, setViewportHeight] = useState(0); + const [scrollyEl, setScrollyEl] = useState(null); + const scrollyRef = useCallback((node: HTMLDivElement) => { + if (node) { + setScrollyEl(node); + setViewportHeight(node.clientHeight); + } + }, []); + + const range = [-2, -1, 0, 1, 2]; + const [monthsShown, setMonthsShown] = useState( + range.map((x) => addMonths(startOfMonth(now), x)) + ); + const centerMonth = monthsShown[2]; + + useEffect(() => { + setCurrentTitle( + <> + {monthNameOf(centerMonth)} + {centerMonth.getFullYear()} + + ); + }, [centerMonth]); + + // Scroll to today on first load + 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]); + + // Add resize observer on the viewport + useEffect(() => { + const observer = new ResizeObserver((entries) => { + if (entries && scrollyEl) setViewportHeight(scrollyEl.clientHeight); + }); + + if (scrollyEl) observer.observe(scrollyEl); + + return () => { + if (scrollyEl) observer.unobserve(scrollyEl); + }; + }, [scrollyEl]); + + // The calendar should always show 6 rows + const dateCellHeight = viewportHeight / 6; + + // Add intersection observer to tell when we scrolled too far up or down + useEffect(() => { + if (!scrollyEl) return; + + const children: HTMLElement[] = [...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) => { + if (viewportHeight === 0) return; + let newMonthsShown = monthsShown; + + 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]); + + return ( + <> +
+ {daysOfWeek.map((day) => ( +
+ {day} +
+ ))} +
+ +
+ {monthsShown.map((month) => { + // How many rows will this month take up? + const [startWeek, endWeek] = weekBoundsOfMonth(month); + const numWeeks = differenceInWeeks(endWeek, startWeek); + const dateGridHeight = dateCellHeight * numWeeks; + + const props = { + month, + dateGridHeight, + dateCellHeight, + isActive: isSameMonth(centerMonth, month), + }; + const ref = undefined; + // (month === currentlyShownMonth && middleMonthRef) || undefined; + return ( + + ); + })} +
+ + ); +} diff --git a/src/calendar/WeekCalendar.tsx b/src/calendar/WeekCalendar.tsx new file mode 100644 index 0000000..b58b28d --- /dev/null +++ b/src/calendar/WeekCalendar.tsx @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { CalendarContext } from "./Calendar"; + +export default function WeekCalendar() { + const { onDateClick, setCurrentTitle } = useContext(CalendarContext); + + return <>; +}