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