Calender context moved into redux.

This commit is contained in:
Lucid Kobold
2022-06-12 23:27:04 -05:00
parent 1f596f8f1c
commit ad6b35012d
11 changed files with 274 additions and 323 deletions

View File

@@ -1,262 +0,0 @@
import React, { createContext, useState, ReactNode } from "react";
import {
format,
startOfMonth,
endOfMonth,
getDate,
add,
sub,
set,
isAfter,
isBefore,
compareAsc
} from "date-fns";
const CalenderContext = createContext({} as CalenderContextState);
const CalenderContextProvider = ({
children
}: {
children: ReactNode;
}): JSX.Element => {
const weekDays: WeekDays = {
sunday: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
monday: [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
]
};
/**
* Using date-fns, this function checks if currDate is within the month of selectedDate or not.
* @param {Date} selectedDate The current month.
* @param {Date} currDate The date to be compared to the selected month.
* @returns True if currDate is outside of the month of selectedDate, false if otherwise.
*/
const isOverflow = (
selectedDate: Date,
currDate: Date
): {
isOverflow: boolean;
overflowDirection: "prev" | "next" | null;
} => {
let flag = false;
let direction: "next" | "prev" | null = null;
const start = startOfMonth(selectedDate);
const end = endOfMonth(selectedDate);
if (isBefore(currDate, start)) {
flag = true;
direction = "prev";
}
if (isAfter(currDate, end)) {
flag = true;
direction = "next";
}
return { isOverflow: flag, overflowDirection: direction };
};
/**
* A function that will return a month layout when given a date. It produces
* an object with 6 weeks that include overflow from the previous and next month
* with all dates aligned with the day of the week.
* @param selectedDate The date of the month to generate a month layout for.
*/
const populateMonth = (selectedDate: Date): MonthLayout => {
const endLastMonth = getDate(endOfMonth(sub(selectedDate, { months: 1 })));
const startOfSelectedMonth = format(startOfMonth(selectedDate), "iii");
const ISOToIndex = {
sunday: {
Sun: 0,
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5,
Sat: 6
},
monday: {
Mon: -1,
Tue: 0,
Wed: 1,
Thu: 2,
Fri: 3,
Sat: 4,
Sun: 5
}
};
const sundays = {
week1: new Array(7).fill(null),
week2: new Array(7).fill(null),
week3: new Array(7).fill(null),
week4: new Array(7).fill(null),
week5: new Array(7).fill(null),
week6: new Array(7).fill(null)
};
const sunStartDay =
endLastMonth - (ISOToIndex.sunday[startOfSelectedMonth] - 1);
let sunCurrDate = set(sub(selectedDate, { months: 1 }), {
date: sunStartDay
});
for (const week in sundays) {
const thisWeek = sundays[week];
thisWeek.forEach((e, i) => {
const overflowInfo = isOverflow(selectedDate, sunCurrDate);
const day: MonthDay = {
...overflowInfo,
date: sunCurrDate
};
sunCurrDate = add(sunCurrDate, {
days: 1
});
sundays[week][i] = day;
});
}
const mondays = {
week1: new Array(7).fill(null),
week2: new Array(7).fill(null),
week3: new Array(7).fill(null),
week4: new Array(7).fill(null),
week5: new Array(7).fill(null),
week6: new Array(7).fill(null)
};
const monStartDay = endLastMonth - ISOToIndex.monday[startOfSelectedMonth];
let monCurrDate = set(sub(selectedDate, { months: 1 }), {
date: monStartDay
});
for (const week in mondays) {
const thisWeek = mondays[week];
thisWeek.forEach((e, i) => {
const overflowInfo = isOverflow(selectedDate, monCurrDate);
const day: MonthDay = {
...overflowInfo,
date: monCurrDate
};
monCurrDate = add(monCurrDate, {
days: 1
});
mondays[week][i] = day;
});
}
const output = {
sunday: {
weekdays: weekDays.sunday,
month: sundays
},
monday: {
weekdays: weekDays.monday,
month: mondays
}
};
return output;
};
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [selectedDateInfo, setSelectedMonthInfo] = useState<MonthContext>({
date: selectedDate,
title: format(selectedDate, "LLLL uuuu"),
layout: populateMonth(selectedDate)
});
/**
* Updates the selectedDateInfo state when given a date.
* @param {Date} newDate The date to set the selectedDateInfo state to.
*/
const updateDateInfo = (newDate: Date) => {
const output = { ...selectedDateInfo };
output.date = newDate;
output.title = format(newDate, "LLLL uuuu");
output.layout = populateMonth(newDate);
setSelectedMonthInfo(output);
};
// TODO: Add a function that validated if a date has at least one sticker in it. Use that within the nav function (when filter is enabled).
// TODO: Add a function that will give the closest date, if available, when the nav func detects an empty month.
// Use the chart creation date to aid with this. (When filter is enabled)
/**
* TODO: Add logic that prevents navigation to the future and too far in the past. (Use chart creation date)
* Update to use a promise and return appropriate errors. Display those errors on the front end.
* Update the use of this function on the front to handle the fails of the promise.
*/
// TODO: (When filter is enabled) Update the calender update function that will take in a direction so that the the navigation buttons will take the user to the next month with stickers. Assuming there was a gap with empty months.
/**
* Updated the selectedDate state when given the appropriate object.
* @param {UpdateCalendarProps} input An object with year, month,
* and day keys that the selectedDate state will be updated to.
*/
const updateDate = (input: UpdateCalendarProps) => {
const { year, month: inputMonth, day } = input;
if (!year || !inputMonth || day < 0 || day > 31) {
return false;
} else {
const month = inputMonth - 1;
const customDate: Date = new Date(year, month, day);
if (compareAsc(customDate, selectedDate) !== 0) {
setSelectedDate(customDate);
updateDateInfo(customDate);
}
}
};
// * Attempting to fix an issue with static generation where the date does not appear to be updating after initial generation.
const [currDate, setCurrDate] = useState<Date>(new Date());
const calenderContextValues: CalenderContextState = {
currDate,
setCurrDate,
selectedDate,
title: selectedDateInfo.title,
layout: selectedDateInfo.layout,
updateDate
};
return (
<CalenderContext.Provider value={calenderContextValues}>
{children}
</CalenderContext.Provider>
);
};
export { CalenderContextProvider, CalenderContext };

181
lib/populateMonth.ts Normal file
View File

@@ -0,0 +1,181 @@
import {
getDate,
endOfMonth,
sub,
format,
startOfMonth,
set,
add,
isAfter,
isBefore
} from "date-fns";
const weekDays: WeekDays = {
sunday: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
monday: [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
]
};
/**
* Using date-fns, this function checks if currDate is within the month of selectedDate or not.
* @param {Date} selectedDate The current month.
* @param {Date} currDate The date to be compared to the selected month.
* @returns True if currDate is outside of the month of selectedDate, false if otherwise.
*/
const isOverflow = (
selectedDate: Date,
currDate: Date
): {
isOverflow: boolean;
overflowDirection: "prev" | "next" | null;
} => {
let flag = false;
let direction: "next" | "prev" | null = null;
const start = startOfMonth(selectedDate);
const end = endOfMonth(selectedDate);
if (isBefore(currDate, start)) {
flag = true;
direction = "prev";
}
if (isAfter(currDate, end)) {
flag = true;
direction = "next";
}
return { isOverflow: flag, overflowDirection: direction };
};
/**
* A function that will return a month layout when given a date. It produces
* an object with 6 weeks that include overflow from the previous and next month
* with all dates aligned with the day of the week.
* @param selectedDate The date of the month to generate a month layout for.
* @returns The month layout object for the provided month.
*/
const populateMonth = (selectedDate: Date): MonthLayout => {
const endLastMonth = getDate(endOfMonth(sub(selectedDate, { months: 1 })));
const startOfSelectedMonth = format(startOfMonth(selectedDate), "iii");
const ISOToIndex = {
sunday: {
Sun: 0,
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5,
Sat: 6
},
monday: {
Mon: -1,
Tue: 0,
Wed: 1,
Thu: 2,
Fri: 3,
Sat: 4,
Sun: 5
}
};
const sundays = {
week1: new Array(7).fill(null),
week2: new Array(7).fill(null),
week3: new Array(7).fill(null),
week4: new Array(7).fill(null),
week5: new Array(7).fill(null),
week6: new Array(7).fill(null)
};
const sunStartDay =
endLastMonth - (ISOToIndex.sunday[startOfSelectedMonth] - 1);
let sunCurrDate = set(sub(selectedDate, { months: 1 }), {
date: sunStartDay
});
for (const week in sundays) {
const thisWeek = sundays[week];
thisWeek.forEach((e, i) => {
const overflowInfo = isOverflow(selectedDate, sunCurrDate);
const day: MonthDay = {
...overflowInfo,
date: sunCurrDate.toJSON()
};
sunCurrDate = add(sunCurrDate, {
days: 1
});
sundays[week][i] = day;
});
}
const mondays = {
week1: new Array(7).fill(null),
week2: new Array(7).fill(null),
week3: new Array(7).fill(null),
week4: new Array(7).fill(null),
week5: new Array(7).fill(null),
week6: new Array(7).fill(null)
};
const monStartDay = endLastMonth - ISOToIndex.monday[startOfSelectedMonth];
let monCurrDate = set(sub(selectedDate, { months: 1 }), {
date: monStartDay
});
for (const week in mondays) {
const thisWeek = mondays[week];
thisWeek.forEach((e, i) => {
const overflowInfo = isOverflow(selectedDate, monCurrDate);
const day: MonthDay = {
...overflowInfo,
date: monCurrDate.toJSON()
};
monCurrDate = add(monCurrDate, {
days: 1
});
mondays[week][i] = day;
});
}
const output = {
sunday: {
weekdays: weekDays.sunday,
month: sundays
},
monday: {
weekdays: weekDays.monday,
month: mondays
}
};
return output;
};
export default populateMonth;

View File

@@ -1,5 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from "./store";
export const useAppDiscpatch = () => useDispatch<AppDispatch>();
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -1,14 +1,19 @@
import React, { useContext } from "react";
import React from "react";
import { useRouter } from "next/router";
import { HStack, IconButton } from "@chakra-ui/react";
import { Icon } from "@iconify/react";
import { format, isSameMonth, addMonths, subMonths } from "date-fns";
import findValidDateRange from "../../../lib/findValidDateRange";
import DatePicker from "./DatePicker";
import { CalenderContext } from "../../../contexts/CalenderContext";
import { useAppSelector } from "../../app/hooks";
const CalenderNav = (): JSX.Element => {
const { selectedDate } = useContext(CalenderContext);
const selectedDate = useAppSelector(
(state) => state.calender.selectedDateInfo
);
const { date } = selectedDate;
const selectedDateObj = new Date(date);
const validDateRange = findValidDateRange();
const { start: validStart, end: validEnd } = validDateRange;
@@ -17,14 +22,14 @@ const CalenderNav = (): JSX.Element => {
const handleNavButtons = (direction: "next" | "prev") => {
if (direction === "next") {
const newMonth = addMonths(selectedDate, 1);
const newMonth = addMonths(selectedDateObj, 1);
const year = format(newMonth, "y");
const month = format(newMonth, "L");
router.push(`/calendar/${year}/${month}`);
} else if (direction === "prev") {
const newMonth = subMonths(selectedDate, 1);
const newMonth = subMonths(selectedDateObj, 1);
const year = format(newMonth, "y");
const month = format(newMonth, "L");
@@ -36,14 +41,14 @@ const CalenderNav = (): JSX.Element => {
return (
<HStack spacing={10} as="nav" w="auto" h="10vh" textAlign="center">
<IconButton
isDisabled={isSameMonth(selectedDate, validStart)}
isDisabled={isSameMonth(selectedDateObj, validStart)}
aria-label="Previous Month"
icon={<Icon icon="akar-icons:chevron-left" />}
onClick={() => handleNavButtons("prev")}
/>
<DatePicker />
<IconButton
isDisabled={isSameMonth(selectedDate, validEnd)}
isDisabled={isSameMonth(selectedDateObj, validEnd)}
aria-label="Next Month"
icon={<Icon icon="akar-icons:chevron-right" />}
onClick={() => handleNavButtons("next")}

View File

@@ -1,4 +1,4 @@
import React, { useContext, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { useRouter } from "next/router";
import {
Button,
@@ -27,10 +27,12 @@ import {
import { format } from "date-fns";
import findValidDateRange from "../../../lib/findValidDateRange";
import FormValidateEmoji from "./FormValidateEmoji";
import { CalenderContext } from "../../../contexts/CalenderContext";
import { useAppSelector } from "../../app/hooks";
const DatePicker = (): JSX.Element => {
const { title } = useContext(CalenderContext);
const selectedDate = useAppSelector(
(state) => state.calender.selectedDateInfo
);
const router = useRouter();
@@ -129,7 +131,7 @@ const DatePicker = (): JSX.Element => {
<PopoverTrigger>
<Button border="none" variant="outline">
<Heading w="100%" h="auto">
{title}
{selectedDate.title}
</Heading>
</Button>
</PopoverTrigger>

View File

@@ -19,7 +19,7 @@ interface DayProps {
overflowDirection?: "next" | "prev" | null;
sticker: StickerVal;
date: Date;
selectedDate: Date;
selectedDate: string;
currDate: Date;
isToday: boolean;
}
@@ -42,17 +42,19 @@ const Day = ({
currDate,
isToday
}: DayProps): JSX.Element => {
const selectedDateObj = new Date(selectedDate);
const handleNav = (direction: "next" | "prev") => {
if (direction === "next") {
console.log(overflowDirection);
const newMonth = add(selectedDate, { months: 1 });
const newMonth = add(selectedDateObj, { months: 1 });
const year = getYear(newMonth);
const month = getMonth(newMonth) + 1;
router.push(`/calendar/${year}/${month}`);
} else if (direction === "prev") {
const newMonth = sub(selectedDate, { months: 1 });
const newMonth = sub(selectedDateObj, { months: 1 });
const year = getYear(newMonth);
const month = getMonth(newMonth) + 1;

View File

@@ -1,41 +1,50 @@
import React, { useContext, useEffect } from "react";
import { Box, HStack, SimpleGrid, Text, VStack } from "@chakra-ui/react";
import { isSameDay, format } from "date-fns";
import { useAppDiscpatch, useAppSelector } from "../../app/hooks";
import { updateCurrDate, updateMonth } from '../../features/calender/calender';
import { CalenderContext } from "../../../contexts/CalenderContext";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { updateCurrDate, updateMonth } from "../../features/calender/calender";
import { StickersContext } from "../../../contexts/StickerContext";
import CalenderNav from "./CalenderNav";
import Day from "./Day";
const Calender = (newDate?: UpdateCalendarProps): JSX.Element => {
const currDate: Date = useAppSelector(state => state.calender.currDate);
const seletedMonth = useAppSelector(state => state.calender.selectedDateInfo);
const currDate: string = useAppSelector((state) => state.calender.currDate);
const selectedDate = useAppSelector(
(state) => state.calender.selectedDateInfo
);
const { layout } = selectedDate;
const dispatch = useAppDispatch();
const { selectedDate, layout, updateDate,/* currDate, */setCurrDate } =
useContext(CalenderContext);
const { stickersMonth } = useContext(StickersContext);
const currDateObj = new Date(currDate);
useEffect(() => {
if (newDate && newDate.year && newDate.month && newDate.day) {
const { year, month, day } = newDate;
if (year > 0 && month > 0 && day > 0) {
updateDate(newDate);
const generatedDate: Date = new Date(year, month - 1, day);
const dateString: string = generatedDate.toJSON();
dispatch(updateMonth(dateString));
} else {
console.warn("Invalid date format: ", newDate);
}
}
}, [newDate, updateDate]);
}, [dispatch, newDate]);
useEffect(() => {
console.info("Check to update date.");
if (!isSameDay(currDate, new Date())) {
const currDateObj = new Date(currDate);
if (!isSameDay(currDateObj, new Date())) {
console.info("Updated date.");
setCurrDate(new Date());
dispatch(updateCurrDate());
}
}, [currDate, setCurrDate]);
}, [currDate, dispatch]);
// Simulated user settings context
const userSettings = {
@@ -87,12 +96,14 @@ const Calender = (newDate?: UpdateCalendarProps): JSX.Element => {
return thisWeek.map((day: MonthDay) => {
const { date, isOverflow, overflowDirection } = day;
const toDateObj: Date = new Date(date);
let sticker = null;
let id = "";
stickersMonth.map((stickerDay) => {
if (isSameDay(stickerDay.date, date)) {
if (isSameDay(stickerDay.date, toDateObj)) {
sticker = stickerDay.sticker;
id = stickerDay.id;
@@ -104,14 +115,14 @@ const Calender = (newDate?: UpdateCalendarProps): JSX.Element => {
isOverflow={isOverflow}
overflowDirection={overflowDirection}
sticker={sticker}
date={date}
selectedDate={selectedDate}
currDate={currDate}
isToday={isSameDay(currDate, date)}
date={toDateObj}
selectedDate={selectedDate.date}
currDate={currDateObj}
isToday={isSameDay(currDateObj, toDateObj)}
key={
id.length
? id
: format(date, "yyyyddLL") +
: format(toDateObj, "yyyyddLL") +
`/${sticker === null ? 0 : sticker}`
}
/>

View File

@@ -1,43 +1,59 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { format } from "date-fns";
import populate from "../../../lib/populateMonth";
interface CalenderSlice {
currDate: Date;
currDate: string;
selectedDateInfo: {
selectedDate: Date;
date: string;
title: string;
layout: MonthLayout;
};
}
const getCurrDate = (): Date => new Date();
const dateParse = (date: Date) => date.toJSON();
const dateFormatter = (date: Date): string => format(date, "LLLL uuuu");
const initialState: CalenderSlice = {
currDate: getCurrDate(),
currDate: dateParse(getCurrDate()),
selectedDateInfo: {
selectedDate: getCurrDate(),
title: dateFormatter(new Date()),
layout: {} as MonthLayout
date: dateParse(getCurrDate()),
title: dateFormatter(getCurrDate()),
layout: populate(getCurrDate())
}
};
// TODO: Add a function that validated if a month has at least one sticker in it. Use that within the nav function (when filter is enabled).
// TODO: Add a function that will give the closest date, if available, when the nav func detects an empty month.
// Use the chart creation date to aid with this. (When filter is enabled)
/**
* TODO: Add logic that prevents navigation to the future and too far in the past. (Use chart creation date)
* Update to use a promise and return appropriate errors. Display those errors on the front end.
* Update the use of this function on the front to handle the fails of the promise.
*/
// TODO: (When filter is enabled) Update the calender update function that will take in a direction so that the the navigation buttons will take the user to the next month with stickers. Assuming there was a gap with empty months.
const calenderSlice = createSlice({
name: "Calender",
initialState,
reducers: {
// Populate month
// Update month info
updateMonth(state: CalenderSlice, action: PayloadAction<Date>) {
const { payload: newDate } = action;
updateMonth(state: CalenderSlice, action: PayloadAction<string>) {
const { payload } = action;
state.selectedDateInfo.selectedDate = newDate;
state.selectedDateInfo.title = dateFormatter(newDate);
// ! Add the layout formatter function
const toDateObj: Date = new Date(payload);
state.selectedDateInfo.date = payload;
state.selectedDateInfo.title = dateFormatter(toDateObj);
state.selectedDateInfo.layout = populate(toDateObj);
},
// Update current date
updateCurrDate(state: CalenderSlice) {
state.currDate = new Date();
state.currDate = dateParse(new Date());
}
}
});

View File

@@ -13,7 +13,8 @@ import {
// import findValidDateRange from "../../lib/findValidDateRange";
import ErrorPage from "next/error";
import Calender from "../../components/calender";
import { CalenderContextProvider } from "../../../contexts/CalenderContext";
import { Provider } from "react-redux";
import { store } from "../../app/store";
import { StickersContextProvider } from "../../../contexts/StickerContext";
const DateRoute: React.FC<unknown> = () => {
@@ -59,8 +60,6 @@ const DateRoute: React.FC<unknown> = () => {
} else if (!dateArr[2]) {
date.day = 1;
}
} else {
return date;
}
return date;
@@ -199,11 +198,11 @@ const DateRoute: React.FC<unknown> = () => {
<ErrorPage statusCode={404} />
) : (
<Box textAlign="center" w="100%" h="auto" pt="50px" pb="10vh">
<CalenderContextProvider>
<StickersContextProvider>
<Provider store={store}>
<Calender {...date} />
</Provider>
</StickersContextProvider>
</CalenderContextProvider>
</Box>
);
};

View File

@@ -4,7 +4,6 @@ import { format } from "date-fns";
import { Provider } from "react-redux";
import { store } from "../app/store";
import { StickersContextProvider } from "../../contexts/StickerContext";
import { CalenderContextProvider } from "../../contexts/CalenderContext";
import Calender from "../components/calender";
const IndexPage = (): JSX.Element => {
@@ -17,11 +16,9 @@ const IndexPage = (): JSX.Element => {
return (
<Box textAlign="center" w="100%" h="auto" pt="50px" pb="10vh">
<StickersContextProvider>
<CalenderContextProvider>
<Provider store={store}>
<Calender {...date.current} />
</Provider>
</CalenderContextProvider>
</StickersContextProvider>
</Box>
);

View File

@@ -15,7 +15,7 @@ interface WeekDays {
}
interface MonthDay {
date: Date;
date: string;
isOverflow: boolean;
overflowDirection: "prev" | "next" | null;
}