diff --git a/gatsby-browser.js b/gatsby-browser.js index 4fb7c8ae..ef60d670 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -10,7 +10,7 @@ export const wrapRootElement = ({ element }) => { } export const shouldUpdateScroll = ({ routerProps: { location } }) => { - const regex = /timeline\/?(\d\d\d\d-\d\d-\d\d)?$/ + const regex = /(timeline|calendar)\/?(\d\d\d\d-\d\d-\d\d)?$/ const results = location.pathname.match(regex) if (results) { diff --git a/gatsby-node.js b/gatsby-node.js index d52502b7..388e689a 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -4,6 +4,9 @@ exports.onCreatePage = async ({ page, actions }) => { if (page.path === "/timeline/") { page.matchPath = "/timeline/*" createPage(page) + } else if (page.path === "/calendar/") { + page.matchPath = "/calendar/*" + createPage(page) } else if (page.path === "/profile/") { page.matchPath = "/profile/*" createPage(page) diff --git a/src/features/timeline/Calendar/Day.js b/src/features/timeline/Calendar/Day.js new file mode 100644 index 00000000..3184f933 --- /dev/null +++ b/src/features/timeline/Calendar/Day.js @@ -0,0 +1,97 @@ +import { useSelector } from "react-redux" +import { Box, makeStyles, Typography } from "@material-ui/core" +import { + addDays, + getDay, + isFuture as dateIsFuture, + isToday as dateIsToday, + isPast as dateIsPast, +} from "date-fns" +import { + selectHasPredictionsForDate, + selectPredictedMenstruationForDate, +} from "../../cycle" +import React from "react" +import { entryIdFromDate } from "../../utils/days" +import Header from "./Header" +import Entry from "./Entry" + +const useStyles = makeStyles((theme) => ({ + list: { listStyle: "none" }, + day: { + minHeight: 100, + background: theme.palette.grey[200], + border: (props) => + props.isPeriod + ? `2px solid ${theme.palette.error.light}` + : `2px solid ${theme.palette.grey[200]}`, + }, +})) + +const Day = ({ date, ...props }) => { + const isPredictedMenstruation = useSelector((state) => + selectPredictedMenstruationForDate(state, { date }) + ) + const classes = useStyles({ isPeriod: isPredictedMenstruation }) + const hasPredictions = useSelector((state) => + selectHasPredictionsForDate(state, { date }) + ) + + const isPast = dateIsPast(date) + const isFuture = dateIsFuture(date) + const isToday = dateIsToday(date) + const itemProps = { date, isFuture, isToday, isPast, isSelected: false } + const scrollToId = `scrollTo-${entryIdFromDate(addDays(date, 1))}` + const weekDay = props.isFirstOfMonth ? getDay(date) : null + + const columnsForFirstDay = { + 0: 7, + 1: 0, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + } + + return ( + <> + +
+ + {isFuture && !hasPredictions ? ( + + {date.getDate()} + + ) : ( + <> +
+ + + )} + + + + ) +} + +export default Day diff --git a/src/features/timeline/Calendar/Entry.js b/src/features/timeline/Calendar/Entry.js new file mode 100644 index 00000000..e5d4af7f --- /dev/null +++ b/src/features/timeline/Calendar/Entry.js @@ -0,0 +1,42 @@ +import React from "react" +import PropTypes from "prop-types" +import { Link } from "gatsby" + +import { useSelector } from "react-redux" +import { ButtonBase, Tooltip } from "@material-ui/core" +import { Brightness1 as BrightnessIcon } from "@material-ui/icons" +import { useTheme } from "@material-ui/core/styles" +import { entryIdFromDate } from "../../utils/days" +import { selectEntryNote } from "../../entries" + +const Entry = ({ date, isPast, isToday, className }) => { + const theme = useTheme() + const entryId = entryIdFromDate(date) + const editPath = `/calendar/${entryId}/edit` + const entryNote = useSelector((state) => selectEntryNote(state, { date })) + const isEditable = isPast || isToday + + if (!isEditable) return null + + return ( + + {entryNote + ? entryNote.split(" ").map((note) => ( + + + + )) + : null} + + ) +} + +Entry.propTypes = { + date: PropTypes.instanceOf(Date), + isPast: PropTypes.bool.isRequired, + isToday: PropTypes.bool.isRequired, +} + +export default Entry diff --git a/src/features/timeline/Calendar/Header.js b/src/features/timeline/Calendar/Header.js new file mode 100644 index 00000000..b41b0060 --- /dev/null +++ b/src/features/timeline/Calendar/Header.js @@ -0,0 +1,53 @@ +import React from "react" +import PropTypes from "prop-types" +import { useSelector } from "react-redux" +import { Typography } from "@material-ui/core" + +import { + selectCycleDayForDate, + selectPredictedMenstruationForDate, +} from "../../cycle" + +const TimelineHeader = ({ date, isSelected, isToday, isFuture, className }) => { + const cycleDay = useSelector((state) => + selectCycleDayForDate(state, { date }) + ) + + const isPredictedMenstruation = useSelector((state) => + selectPredictedMenstruationForDate(state, { date }) + ) + + const isMenstruation = isPredictedMenstruation + + const textColor = isToday || isFuture ? "textPrimary" : "textSecondary" + + return ( +
+ + {isToday ? "Today" : date.getDate()} + + + Day {cycleDay} + +
+ ) +} + +TimelineHeader.propTypes = { + date: PropTypes.instanceOf(Date), + isSelected: PropTypes.bool.isRequired, + isToday: PropTypes.bool.isRequired, + isFuture: PropTypes.bool.isRequired, +} + +export default TimelineHeader diff --git a/src/features/timeline/Calendar/index.js b/src/features/timeline/Calendar/index.js new file mode 100644 index 00000000..e1a4ac3e --- /dev/null +++ b/src/features/timeline/Calendar/index.js @@ -0,0 +1,67 @@ +import { getYear } from "date-fns" +import React from "react" +import { Box, Typography, makeStyles } from "@material-ui/core" +import Day from "./Day" + +const useStyles = makeStyles((theme) => ({ + calendar: { + display: "grid", + gridTemplateColumns: `repeat(7, calc(14.2% - ${theme.spacing(2)}px))`, + gridGap: theme.spacing(2), + padding: theme.spacing(1), + + [theme.breakpoints.down("sm")]: { + gridTemplateColumns: `repeat(7, calc(14.2% - ${theme.spacing(1)}px))`, + gridGap: theme.spacing(1), + }, + }, + weekDays: { + display: "grid", + gridTemplateColumns: `repeat(7, calc(14.2% - ${theme.spacing(2)}px))`, + listStyle: "none", + gridGap: theme.spacing(2), + padding: theme.spacing(1), + "& li": { + textAlign: "center", + }, + }, +})) + +const Calendar = ({ dates }) => { + const classes = useStyles() + return ( + + + + {dates[0].toLocaleString("default", { month: "long" })}{" "} + {getYear(dates[0])}{" "} + + + <> + +
  • Mon
  • +
  • Tues
  • +
  • Wed
  • +
  • Thurs
  • +
  • Fri
  • +
  • Sat
  • +
  • Sun
  • +
    + + + {dates.map((date, i) => ( + + ))} + +
    + ) +} + +export default Calendar diff --git a/src/features/timeline/TimelineEditPage.js b/src/features/timeline/TimelineEditPage.js index 879d96b9..34c28a68 100644 --- a/src/features/timeline/TimelineEditPage.js +++ b/src/features/timeline/TimelineEditPage.js @@ -30,10 +30,10 @@ const useStyles = makeStyles((theme) => ({ }, })) -const CycleEditPage = ({ entryId }) => { +const CycleEditPage = ({ entryId, calendar }) => { const classes = useStyles() const date = makeDate(entryId) - + const redirectTo = calendar ? `/calendar/${entryId}` : `/${entryId}` const dispatch = useDispatch() const entryNote = useSelector((state) => selectEntryNote(state, { entryId })) const [note, setNote] = useState(entryNote) @@ -45,13 +45,13 @@ const CycleEditPage = ({ entryId }) => { const handleReset = (event) => { event.preventDefault() setNote(entryNote) - navigate(`/timeline/${entryId}`) + navigate(redirectTo) } const handleSubmit = (event) => { event.preventDefault() dispatch(upsertEntry({ date, note })) - navigate(`/timeline/${entryId}`) + navigate(redirectTo) } return ( diff --git a/src/features/timeline/TimelineIndexPage.js b/src/features/timeline/TimelineIndexPage.js index 9bc86672..5fc95c54 100644 --- a/src/features/timeline/TimelineIndexPage.js +++ b/src/features/timeline/TimelineIndexPage.js @@ -2,14 +2,16 @@ import React, { useEffect } from "react" import { useSelector } from "react-redux" import { navigate } from "gatsby" import { List, IconButton, makeStyles } from "@material-ui/core" -import { Today } from "@material-ui/icons" -import { eachDayOfInterval, addDays, isToday } from "date-fns" +import { Today, CalendarViewDay } from "@material-ui/icons" +import AppsIcon from "@material-ui/icons/Apps" +import { eachDayOfInterval, addDays, isToday, getMonth } from "date-fns" import { makeDate, entryIdFromDate } from "../utils/days" import { AppLayout, AppMainToolbar, AppPage } from "../app" import { Welcome } from "../onboarding" import { selectDaysBetween } from "../cycle" import TimelineItem from "./TimelineItem" import DatePicker from "./DatePicker" +import Calendar from "./Calendar" const useStyles = makeStyles((theme) => ({ timeline: { @@ -19,9 +21,8 @@ const useStyles = makeStyles((theme) => ({ }, })) -const CycleIndexPage = ({ entryId }) => { +const CycleIndexPage = ({ entryId, calendar: calendarView }) => { const classes = useStyles() - const selectedDate = makeDate(entryId) const calculatedDaysBetween = useSelector(selectDaysBetween) @@ -40,6 +41,16 @@ const CycleIndexPage = ({ entryId }) => { }) }, [selectedDate]) + const daysInMonths = range.reduce((acc, curr) => { + const month = getMonth(curr) + if (acc[month]) { + acc[month] = [...acc[month], curr] + } else { + acc[month] = [curr] + } + return acc + }, {}) + return ( @@ -47,21 +58,43 @@ const CycleIndexPage = ({ entryId }) => { navigate(`/timeline/${entryIdFromDate(new Date())}`)} + onClick={() => { + const link = calendarView ? "/calendar" : "/timeline" + navigate(`${link}/${entryIdFromDate(new Date())}`) + }} style={{ marginLeft: "auto" }} > + + navigate(`/${calendarView ? "timeline/" : "calendar/"}`) + } + style={{ marginLeft: "auto" }} + > + {calendarView ? : } + - - {range.map((date) => { - return ( - - {isToday(date) && } - - ) - })} - + {calendarView ? ( + Object.keys(daysInMonths).map((monthNumber) => ( + + )) + ) : ( + + {range.map((date) => { + return ( + + {isToday(date) && } + + ) + })} + + )} ) diff --git a/src/pages/calendar.js b/src/pages/calendar.js new file mode 100644 index 00000000..7a04ca70 --- /dev/null +++ b/src/pages/calendar.js @@ -0,0 +1,53 @@ +import React, { useEffect } from "react" +import { useSelector } from "react-redux" +import { navigate } from "gatsby" +import { Router } from "@reach/router" + +import { useAuth } from "../features/auth" +import { useSubscription } from "../features/user" +import { selectAreEntriesLoading } from "../features/entries" +import { useSettings } from "../features/settings" +import { TimelineIndexPage, TimelineEditPage } from "../features/timeline" +import { selectHasMensesStartDate } from "../features/cycle" +import { Seo, Loading } from "../features/app" +import { INCOMPLETE } from "../features/navigation" + +const CalendarView = () => { + const { isAuthenticated, isAuthPending } = useAuth() + const { isSubscribed } = useSubscription() + const { isLoading: settingsIsLoading } = useSettings() + + const entriesAreLoading = useSelector(selectAreEntriesLoading) + const hasMensesStartDate = useSelector(selectHasMensesStartDate) + + const dataIsLoading = entriesAreLoading || settingsIsLoading + const isIncomplete = !hasMensesStartDate || !isSubscribed + + useEffect(() => { + if (isAuthenticated && !dataIsLoading && isIncomplete) { + navigate(INCOMPLETE.to) + } + }, [isAuthenticated, dataIsLoading, isIncomplete]) + + if (isAuthPending || dataIsLoading) { + return ( + <> + + + + ) + } + + return ( + <> + + + + + + + + ) +} + +export default CalendarView