insane week behavior
This commit is contained in:
parent
f375c25a48
commit
924c93271b
17 changed files with 482 additions and 72 deletions
|
@ -0,0 +1,24 @@
|
||||||
|
.scrollyEl {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: scroll; /* Add the ability to scroll the y axis */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: scroll; /* Add the ability to scroll the y axis */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the scroll bar
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,36 +1,49 @@
|
||||||
import {
|
import { Fragment, useCallback, useContext, useEffect, useState } from "react";
|
||||||
ForwardedRef,
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import styles from "./ScrollContainer.module.scss";
|
import styles from "./ScrollContainer.module.scss";
|
||||||
import { subMonths } from "date-fns";
|
import classNames from "classnames";
|
||||||
|
import { getWeek } from "date-fns";
|
||||||
|
import { CalendarContext } from "src/calendar/Calendar";
|
||||||
|
import { monthNameOf, weekString } from "./date";
|
||||||
|
|
||||||
interface ScrollContainerProps<T> {
|
interface ScrollContainerProps<T> {
|
||||||
direction: "horizontal" | "vertical";
|
direction: "horizontal" | "vertical";
|
||||||
|
className?: string;
|
||||||
|
|
||||||
/** A list of things managed */
|
/** A list of things managed */
|
||||||
list: T[];
|
list: T[];
|
||||||
|
|
||||||
generatePreviousObject: () => void;
|
renderItem: (_: T) => JSX.Element;
|
||||||
generateNextObject: () => void;
|
keyOf: (_: T) => Key;
|
||||||
|
generatePreviousObject: (_: HTMLElement) => void;
|
||||||
|
generateNextObject: (_: HTMLElement) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OldScrollState {
|
||||||
|
el: HTMLElement;
|
||||||
|
offsetLeft: number;
|
||||||
|
offsetTop: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic infinite scrolling container
|
* Generic infinite scrolling container
|
||||||
*/
|
*/
|
||||||
function ScrollContainer<T>(
|
function ScrollContainer<T>({
|
||||||
{ direction, list }: ScrollContainerProps<T>,
|
direction,
|
||||||
ref: ForwardedRef<HTMLDivElement>
|
className,
|
||||||
) {
|
list,
|
||||||
const [resetScrollToEl, setResetScrollToEl] = useState<
|
renderItem,
|
||||||
[HTMLElement, number] | null
|
generateNextObject,
|
||||||
>(null);
|
generatePreviousObject,
|
||||||
|
keyOf,
|
||||||
|
}: ScrollContainerProps<T>) {
|
||||||
|
const [resetScrollToEl, setResetScrollToEl] = useState<OldScrollState | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
// Viewport state
|
// Viewport state
|
||||||
const [viewportSize, setViewportSize] = useState<[number, number]>([0, 0]);
|
const { viewportWidth, viewportHeight, setViewportSize } = useContext(
|
||||||
const [viewportWidth, viewportHeight] = viewportSize;
|
CalendarContext
|
||||||
|
);
|
||||||
const viewportMetric =
|
const viewportMetric =
|
||||||
direction === "horizontal" ? viewportWidth : viewportHeight;
|
direction === "horizontal" ? viewportWidth : viewportHeight;
|
||||||
|
|
||||||
|
@ -43,6 +56,9 @@ function ScrollContainer<T>(
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Scroll to middle on first load
|
||||||
|
const [firstLoad, setFirstLoad] = useState(true);
|
||||||
|
|
||||||
// Add resize observer on the viewport
|
// Add resize observer on the viewport
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new ResizeObserver((entries) => {
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
@ -60,43 +76,109 @@ function ScrollContainer<T>(
|
||||||
|
|
||||||
// Add intersection observer to tell when we scrolled too far
|
// Add intersection observer to tell when we scrolled too far
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (viewportWidth === 0 || viewportHeight === 0) return;
|
||||||
if (!scrollyEl) return;
|
if (!scrollyEl) return;
|
||||||
|
|
||||||
const children: HTMLElement[] = [...scrollyEl.children];
|
if (firstLoad) {
|
||||||
|
const scrollLen = scrollyEl.children.length;
|
||||||
|
const middleIdx = (scrollLen + 1) / 2 - 1;
|
||||||
|
console.log("middle idx", middleIdx);
|
||||||
|
|
||||||
|
// Identify the middle month
|
||||||
|
const el = scrollyEl.children[middleIdx];
|
||||||
|
if (el) {
|
||||||
|
const date = new Date(el.dataset.isodate!);
|
||||||
|
console.log("first run, scrolling into view", date.getMonth());
|
||||||
|
|
||||||
|
el.scrollIntoView();
|
||||||
|
setFirstLoad(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = [...scrollyEl.children] as HTMLElement[];
|
||||||
|
|
||||||
const firstChild = children[0];
|
const firstChild = children[0];
|
||||||
const lastChild = children[children.length - 1];
|
const lastChild = children[children.length - 1];
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
if (viewportMetric === 0) return;
|
if (viewportMetric === 0) return;
|
||||||
let newList = [...list];
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isIntersecting) continue;
|
if (!entry.isIntersecting) continue;
|
||||||
|
|
||||||
|
// const intersectionArea =
|
||||||
|
// entry.intersectionRect.width * entry.intersectionRect.height;
|
||||||
|
// if (intersectionArea === 0) continue;
|
||||||
|
|
||||||
|
const date = new Date(entry.target.dataset.isodate!);
|
||||||
|
console.log("intersected with", weekString(date), entry);
|
||||||
|
|
||||||
if (entry.target === firstChild) {
|
if (entry.target === firstChild) {
|
||||||
console.log("intersected");
|
const newResetScrollToEl = {
|
||||||
const firstChildDate = new Date(firstChild.dataset.isodate!);
|
el: firstChild,
|
||||||
const prevMonth = subMonths(firstChildDate, 1);
|
offsetLeft: scrollyEl.scrollLeft - firstChild.offsetLeft,
|
||||||
newList = [prevMonth, ...monthsShown];
|
offsetTop: scrollyEl.scrollTop - firstChild.offsetTop,
|
||||||
newList = newMonthsShown.slice(0, NUM_MONTHS_SHOWN);
|
};
|
||||||
|
setResetScrollToEl(newResetScrollToEl);
|
||||||
setResetScrollToEl([
|
generatePreviousObject(firstChild);
|
||||||
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);
|
if (entry.target === lastChild) {
|
||||||
});
|
console.log("child offset left", lastChild.offsetLeft);
|
||||||
|
const newResetScrollToEl = {
|
||||||
|
el: lastChild,
|
||||||
|
offsetLeft: scrollyEl.scrollLeft - lastChild.offsetLeft,
|
||||||
|
offsetTop: scrollyEl.scrollTop - lastChild.offsetTop,
|
||||||
|
};
|
||||||
|
setResetScrollToEl(newResetScrollToEl);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
generateNextObject(lastChild);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={styles.scrollyEl} ref={scrollyRef}></div>;
|
const firstDate = new Date(firstChild.dataset.isodate!);
|
||||||
|
const lastDate = new Date(lastChild.dataset.isodate!);
|
||||||
|
console.log("observing", weekString(firstDate), weekString(lastDate));
|
||||||
|
|
||||||
|
observer.observe(firstChild);
|
||||||
|
observer.observe(lastChild);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(firstChild);
|
||||||
|
observer.unobserve(lastChild);
|
||||||
|
};
|
||||||
|
}, [firstLoad, list, scrollyEl, viewportHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resetScrollToEl) return;
|
||||||
|
if (!scrollyEl) return;
|
||||||
|
|
||||||
|
const { el, offsetLeft, offsetTop } = resetScrollToEl;
|
||||||
|
if (direction === "horizontal")
|
||||||
|
scrollyEl.scrollLeft = el.offsetLeft + offsetLeft;
|
||||||
|
else if (direction === "vertical")
|
||||||
|
scrollyEl.scrollTop = el.offsetTop + offsetTop;
|
||||||
|
|
||||||
|
setResetScrollToEl(null);
|
||||||
|
}, [resetScrollToEl, scrollyEl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{JSON.stringify([firstLoad, viewportWidth, viewportHeight])}
|
||||||
|
<div
|
||||||
|
className={classNames(styles.scrollyEl, styles[direction], className)}
|
||||||
|
ref={scrollyRef}
|
||||||
|
>
|
||||||
|
{list.map((x) => (
|
||||||
|
<Fragment key={keyOf(x)}>{renderItem(x)}</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default forwardRef(ScrollContainer);
|
export default ScrollContainer;
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import {
|
import {
|
||||||
addDays,
|
addDays,
|
||||||
endOfMonth,
|
endOfMonth,
|
||||||
|
getWeek,
|
||||||
isSameWeek,
|
isSameWeek,
|
||||||
startOfMonth,
|
startOfMonth,
|
||||||
startOfWeek,
|
startOfWeek,
|
||||||
subDays,
|
subDays,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
|
|
||||||
|
export const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return week bounds in a [closed, open) fashion
|
* Return week bounds in a [closed, open) fashion
|
||||||
* The last week is not returned if it contains the next month's start date
|
* The last week is not returned if it contains the next month's start date
|
||||||
|
@ -19,12 +22,17 @@ export function weekBoundsOfMonth(date: Date): [Date, Date] {
|
||||||
const lastDayOfMonth = endOfMonth(date);
|
const lastDayOfMonth = endOfMonth(date);
|
||||||
const firstDayOfNextMonth = addDays(lastDayOfMonth, 1);
|
const firstDayOfNextMonth = addDays(lastDayOfMonth, 1);
|
||||||
let endWeek = startOfWeek(lastDayOfMonth);
|
let endWeek = startOfWeek(lastDayOfMonth);
|
||||||
|
|
||||||
if (isSameWeek(lastDayOfMonth, firstDayOfNextMonth))
|
if (isSameWeek(lastDayOfMonth, firstDayOfNextMonth))
|
||||||
endWeek = subDays(endWeek, 7);
|
endWeek = subDays(endWeek, 7);
|
||||||
|
|
||||||
return [startWeek, endWeek];
|
return [startWeek, endWeek];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function weekString(date: Date): string {
|
||||||
|
return `${getWeek(date)}/${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function monthNameOf(date: Date): string {
|
export function monthNameOf(date: Date): string {
|
||||||
return date.toLocaleDateString("default", { month: "long" });
|
return date.toLocaleDateString("default", { month: "long" });
|
||||||
}
|
}
|
15
lib/persistentState.ts
Normal file
15
lib/persistentState.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function usePersistentState<T>(key: string, defaultValue: T) {
|
||||||
|
const [value, setValue] = useState(() => {
|
||||||
|
const value = window.localStorage.getItem(key);
|
||||||
|
if (value === "undefined") return defaultValue;
|
||||||
|
return value ? JSON.parse(value) : defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
}, [key, value]);
|
||||||
|
|
||||||
|
return [value, setValue];
|
||||||
|
}
|
|
@ -4,10 +4,28 @@ import ToggleSwitch from "src/widgets/ToggleSwitch";
|
||||||
import InputBox from "src/widgets/InputBox";
|
import InputBox from "src/widgets/InputBox";
|
||||||
import Button from "src/widgets/Button";
|
import Button from "src/widgets/Button";
|
||||||
import MonthCalendar from "./MonthCalendar";
|
import MonthCalendar from "./MonthCalendar";
|
||||||
|
import WeekCalendar from "./WeekCalendar";
|
||||||
|
import { usePersistentState } from "lib/persistentState";
|
||||||
|
import MonthCalendar2 from "./MonthCalendar2";
|
||||||
|
|
||||||
|
export enum CalendarMode {
|
||||||
|
Week = "Week",
|
||||||
|
Month = "Month",
|
||||||
|
Year = "Year",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CalendarView {
|
||||||
|
Calendar = "Calendar",
|
||||||
|
Graph = "Graph",
|
||||||
|
}
|
||||||
|
|
||||||
interface ICalendarContext {
|
interface ICalendarContext {
|
||||||
onDateClick: (_: Date) => void;
|
onDateClick: (_: Date) => void;
|
||||||
setCurrentTitle: Dispatch<SetStateAction<JSX.Element>>;
|
setCurrentTitle: Dispatch<SetStateAction<JSX.Element>>;
|
||||||
|
|
||||||
|
viewportWidth: number;
|
||||||
|
viewportHeight: number;
|
||||||
|
setViewportSize: Dispatch<SetStateAction<[number, number]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CalendarContext = createContext<ICalendarContext>(null!);
|
export const CalendarContext = createContext<ICalendarContext>(null!);
|
||||||
|
@ -19,14 +37,29 @@ interface CalendarProps {
|
||||||
export default function Calendar({ onDateClick }: CalendarProps) {
|
export default function Calendar({ onDateClick }: CalendarProps) {
|
||||||
const [currentTitle, setCurrentTitle] = useState<JSX.Element>(<></>);
|
const [currentTitle, setCurrentTitle] = useState<JSX.Element>(<></>);
|
||||||
|
|
||||||
const scrollToToday = () => {};
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
const [currentMode, setCurrentMode] = useState("Month");
|
const [currentMode, setCurrentMode] = usePersistentState(
|
||||||
const [currentView, setCurrentView] = useState("Calendar");
|
"CalendarMode",
|
||||||
|
CalendarMode.Month
|
||||||
|
);
|
||||||
|
const [currentView, setCurrentView] = usePersistentState(
|
||||||
|
"CalendarView",
|
||||||
|
CalendarView.Calendar
|
||||||
|
);
|
||||||
|
|
||||||
const contextValue = { onDateClick, setCurrentTitle };
|
const [viewportSize, setViewportSize] = useState<[number, number]>([0, 0]);
|
||||||
|
const [viewportWidth, viewportHeight] = viewportSize;
|
||||||
|
|
||||||
|
const contextValue = {
|
||||||
|
onDateClick,
|
||||||
|
setCurrentTitle,
|
||||||
|
viewportWidth,
|
||||||
|
viewportHeight,
|
||||||
|
setViewportSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToToday = () => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CalendarContext.Provider value={contextValue}>
|
<CalendarContext.Provider value={contextValue}>
|
||||||
|
@ -46,16 +79,18 @@ export default function Calendar({ onDateClick }: CalendarProps) {
|
||||||
<div className={styles.controlButtons}>
|
<div className={styles.controlButtons}>
|
||||||
<Button>Today</Button>
|
<Button>Today</Button>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch<CalendarMode>
|
||||||
options={["Week", "Month", "Year"]}
|
options={Object.values(CalendarMode) as CalendarMode[]}
|
||||||
value={currentMode}
|
value={currentMode}
|
||||||
setValue={setCurrentMode}
|
setValue={(x) => setCurrentMode(x)}
|
||||||
|
renderValue={(x) => <>{CalendarMode[x]}</>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch<CalendarView>
|
||||||
options={["Calendar", "Graph"]}
|
options={Object.values(CalendarView) as CalendarView[]}
|
||||||
value={currentView}
|
value={currentView}
|
||||||
setValue={setCurrentView}
|
setValue={setCurrentView}
|
||||||
|
renderValue={(x) => <>{CalendarView[x]}</>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,9 +102,17 @@ export default function Calendar({ onDateClick }: CalendarProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActualCalendar({ mode }) {
|
interface ActualCalendarProps {
|
||||||
|
mode: CalendarMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActualCalendar({ mode }: ActualCalendarProps) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
default:
|
case CalendarMode.Month:
|
||||||
return <MonthCalendar />;
|
return <MonthCalendar2 />;
|
||||||
|
case CalendarMode.Week:
|
||||||
|
return <WeekCalendar />;
|
||||||
|
case CalendarMode.Year:
|
||||||
|
return <MonthCalendar2 />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
.monthTitle {
|
|
||||||
}
|
|
||||||
|
|
||||||
.dateGrid {
|
.dateGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
startOfWeek,
|
startOfWeek,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { weekBoundsOfMonth } from "lib/month";
|
import { weekBoundsOfMonth } from "lib/date";
|
||||||
import { journalFmt, useJournals } from "lib/queries";
|
import { journalFmt, useJournals } from "lib/queries";
|
||||||
|
|
||||||
interface MonthProps {
|
interface MonthProps {
|
||||||
|
|
|
@ -25,3 +25,11 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monthContainer {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
|
@ -7,11 +7,10 @@ import {
|
||||||
startOfMonth,
|
startOfMonth,
|
||||||
subMonths,
|
subMonths,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { monthNameOf, weekBoundsOfMonth } from "lib/month";
|
import { daysOfWeek, monthNameOf, weekBoundsOfMonth } from "lib/date";
|
||||||
import Month from "./Month";
|
import Month from "./Month";
|
||||||
import { CalendarContext } from "./Calendar";
|
import { CalendarContext } from "./Calendar";
|
||||||
|
|
||||||
const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
||||||
const NUM_MONTHS_SHOWN = 5;
|
const NUM_MONTHS_SHOWN = 5;
|
||||||
|
|
||||||
export default function MonthCalendar() {
|
export default function MonthCalendar() {
|
||||||
|
@ -55,7 +54,7 @@ export default function MonthCalendar() {
|
||||||
);
|
);
|
||||||
if (el) {
|
if (el) {
|
||||||
console.log("first run, scrolling into view", el);
|
console.log("first run, scrolling into view", el);
|
||||||
el.scrollIntoView();
|
el.scrollIntoView(false);
|
||||||
|
|
||||||
setFirstLoad(false);
|
setFirstLoad(false);
|
||||||
}
|
}
|
||||||
|
|
101
src/calendar/MonthCalendar2.tsx
Normal file
101
src/calendar/MonthCalendar2.tsx
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { CalendarContext } from "./Calendar";
|
||||||
|
import styles from "./MonthCalendar.module.scss";
|
||||||
|
import {
|
||||||
|
addMonths,
|
||||||
|
differenceInWeeks,
|
||||||
|
isSameMonth,
|
||||||
|
startOfMonth,
|
||||||
|
subMonths,
|
||||||
|
} from "date-fns";
|
||||||
|
import { daysOfWeek, monthNameOf, weekBoundsOfMonth } from "lib/date";
|
||||||
|
import Month from "./Month";
|
||||||
|
import ScrollContainer from "lib/ScrollContainer";
|
||||||
|
|
||||||
|
const NUM_MONTHS_SHOWN = 5;
|
||||||
|
|
||||||
|
export default function MonthCalendar2() {
|
||||||
|
const now = new Date();
|
||||||
|
const { onDateClick, setCurrentTitle, viewportHeight } = useContext(
|
||||||
|
CalendarContext
|
||||||
|
);
|
||||||
|
|
||||||
|
const range = [-2, -1, 0, 1, 2];
|
||||||
|
const [monthsShown, setMonthsShown] = useState(
|
||||||
|
range.map((x) => addMonths(startOfMonth(now), x))
|
||||||
|
);
|
||||||
|
const centerMonth = monthsShown[2];
|
||||||
|
console.log(
|
||||||
|
monthNameOf(centerMonth),
|
||||||
|
monthsShown.map((x) => monthNameOf(x))
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentTitle(
|
||||||
|
<>
|
||||||
|
<b>{monthNameOf(centerMonth)}</b>
|
||||||
|
{centerMonth.getFullYear()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [centerMonth]);
|
||||||
|
|
||||||
|
const generatePreviousObject = (el: HTMLElement) => {
|
||||||
|
const date = new Date(el.dataset.isodate!);
|
||||||
|
const prevMonth = subMonths(date, 1);
|
||||||
|
const newWeeksShown = [prevMonth, ...monthsShown];
|
||||||
|
setMonthsShown(newWeeksShown.slice(0, NUM_MONTHS_SHOWN));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateNextObject = (el: HTMLElement) => {
|
||||||
|
const date = new Date(el.dataset.isodate!);
|
||||||
|
const nextMonth = addMonths(date, 1);
|
||||||
|
const newWeeksShown = [...monthsShown, nextMonth];
|
||||||
|
setMonthsShown(newWeeksShown.slice(-NUM_MONTHS_SHOWN));
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyOf = (month: Date) => month.toISOString();
|
||||||
|
|
||||||
|
// The calendar should always show 6 rows
|
||||||
|
const dateCellHeight = viewportHeight / 6;
|
||||||
|
|
||||||
|
const renderItem = (month: Date) => {
|
||||||
|
// 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),
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Month key={month.toString()} {...props} onDateClick={onDateClick} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.daysOfWeek}>
|
||||||
|
{daysOfWeek.map((day) => (
|
||||||
|
<div key={day} className={styles.dayOfWeek}>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.monthContainer}>
|
||||||
|
<ScrollContainer
|
||||||
|
className={styles.weekScroll}
|
||||||
|
direction="vertical"
|
||||||
|
list={monthsShown}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyOf={keyOf}
|
||||||
|
generateNextObject={generateNextObject}
|
||||||
|
generatePreviousObject={generatePreviousObject}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
8
src/calendar/Week.module.scss
Normal file
8
src/calendar/Week.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.week {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 100vw;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
min-width: 100vw;
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
14
src/calendar/Week.tsx
Normal file
14
src/calendar/Week.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { getWeek } from "date-fns";
|
||||||
|
import styles from "./Week.module.scss";
|
||||||
|
|
||||||
|
export interface WeekProps {
|
||||||
|
week: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Week({ week }: WeekProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.week} data-isodate={week.toISOString()}>
|
||||||
|
Week {getWeek(week)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
25
src/calendar/WeekCalendar.module.scss
Normal file
25
src/calendar/WeekCalendar.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.weekContainer {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekScroll {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
overflow-x: scroll;
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,63 @@
|
||||||
import { useContext } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { CalendarContext } from "./Calendar";
|
import { CalendarContext } from "./Calendar";
|
||||||
|
import ScrollContainer from "lib/ScrollContainer";
|
||||||
|
import { addWeeks, getWeek, startOfWeek, subWeeks } from "date-fns";
|
||||||
|
import Week from "./Week";
|
||||||
|
import styles from "./WeekCalendar.module.scss";
|
||||||
|
|
||||||
|
const NUM_WEEKS_SHOWN = 5;
|
||||||
|
|
||||||
export default function WeekCalendar() {
|
export default function WeekCalendar() {
|
||||||
|
const now = new Date();
|
||||||
const { onDateClick, setCurrentTitle } = useContext(CalendarContext);
|
const { onDateClick, setCurrentTitle } = useContext(CalendarContext);
|
||||||
|
|
||||||
return <></>;
|
const range = [-2, -1, 0, 1, 2];
|
||||||
|
const [weeksShown, setWeeksShown] = useState(
|
||||||
|
range.map((x) => addWeeks(startOfWeek(now), x))
|
||||||
|
);
|
||||||
|
const centerWeek = weeksShown[2];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentTitle(
|
||||||
|
<>
|
||||||
|
{centerWeek.getFullYear()}
|
||||||
|
|
||||||
|
<b>{JSON.stringify(weeksShown.map((x) => getWeek(x)))}</b>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [centerWeek]);
|
||||||
|
|
||||||
|
const generatePreviousObject = (el: HTMLElement) => {
|
||||||
|
const date = new Date(el.dataset.isodate!);
|
||||||
|
const prevWeek = subWeeks(date, 1);
|
||||||
|
const newWeeksShown = [prevWeek, ...weeksShown];
|
||||||
|
setWeeksShown(newWeeksShown.slice(0, NUM_WEEKS_SHOWN));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateNextObject = (el: HTMLElement) => {
|
||||||
|
const date = new Date(el.dataset.isodate!);
|
||||||
|
const nextWeek = addWeeks(date, 1);
|
||||||
|
const newWeeksShown = [...weeksShown, nextWeek];
|
||||||
|
setWeeksShown(newWeeksShown.slice(-NUM_WEEKS_SHOWN));
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyOf = (week: Date) => week.toISOString();
|
||||||
|
|
||||||
|
const renderItem = (week: Date) => {
|
||||||
|
return <Week key={week.toISOString()} week={week} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.weekContainer}>
|
||||||
|
<ScrollContainer
|
||||||
|
className={styles.weekScroll}
|
||||||
|
direction="horizontal"
|
||||||
|
list={weeksShown}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyOf={keyOf}
|
||||||
|
generateNextObject={generateNextObject}
|
||||||
|
generatePreviousObject={generatePreviousObject}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,13 @@
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
|
transition: background-color 0.1s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($color: lightgray, $amount: 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: darken($color: lightgray, $amount: 10%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.toggleSwitch {
|
.toggleSwitch {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 1px;
|
||||||
|
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
|
@ -9,13 +9,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
border-bottom-right-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: background-color 0.1s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($color: lightgray, $amount: 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: darken($color: lightgray, $amount: 10%);
|
||||||
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styles from "./ToggleSwitch.module.scss";
|
import styles from "./ToggleSwitch.module.scss";
|
||||||
|
import { Key } from "react";
|
||||||
|
|
||||||
interface ToggleSwitchProps {
|
interface ToggleSwitchProps<T> {
|
||||||
options: string[];
|
options: T[];
|
||||||
defaultOption?: string;
|
defaultOption?: string;
|
||||||
value: string;
|
value: T;
|
||||||
setValue: (_: string) => any;
|
renderValue: (_: T) => JSX.Element;
|
||||||
|
setValue: (_: T) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ToggleSwitch({
|
export default function ToggleSwitch<T extends Key>({
|
||||||
options,
|
options,
|
||||||
defaultOption,
|
defaultOption,
|
||||||
value,
|
value,
|
||||||
|
renderValue,
|
||||||
setValue,
|
setValue,
|
||||||
}: ToggleSwitchProps) {
|
}: ToggleSwitchProps<T>) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.toggleSwitch}>
|
<div className={styles.toggleSwitch}>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
|
@ -24,7 +27,7 @@ export default function ToggleSwitch({
|
||||||
className={classNames(styles.option, isSelected && styles.selected)}
|
className={classNames(styles.option, isSelected && styles.selected)}
|
||||||
onClick={() => setValue(option)}
|
onClick={() => setValue(option)}
|
||||||
>
|
>
|
||||||
{option}
|
{renderValue(option)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
Loading…
Reference in a new issue