diff --git a/gatsby-browser.js b/gatsby-browser.js
index 56b4a18b..8f900768 100644
--- a/gatsby-browser.js
+++ b/gatsby-browser.js
@@ -1,5 +1,6 @@
import { withTheme } from "./src/theme"
import { withRoot } from "./src/rootElement"
+import { entryIdFromDate, makeDate } from "./src/features/utils/days"
export const wrapPageElement = ({ element }) => {
return withTheme({ element })
@@ -8,3 +9,15 @@ export const wrapPageElement = ({ element }) => {
export const wrapRootElement = ({ element }) => {
return withRoot({ element })
}
+
+export const shouldUpdateScroll = ({ routerProps: { location } }) => {
+ const regex = /timeline\/?(\d\d\d\d-\d\d-\d\d)?$/
+ const results = location.pathname.match(regex)
+
+ if (results) {
+ const entryId = entryIdFromDate(makeDate(results[1]))
+ return `scrollTo-${entryId}`
+ } else {
+ return true
+ }
+}
diff --git a/src/features/brand/BrandLayout.js b/src/features/brand/BrandLayout.js
index 07c17e98..b27e6a75 100644
--- a/src/features/brand/BrandLayout.js
+++ b/src/features/brand/BrandLayout.js
@@ -27,14 +27,10 @@ import { useSignInNavItem, useSignUpNavItem } from "../navigation"
const useStyles = makeStyles((theme) => ({
root: {
- "& header": {
- maxWidth: "55rem",
- margin: "0 auto",
- },
- "& main": {
+ "& > main": {
maxWidth: "50rem",
},
- "& footer": {
+ "& > footer": {
maxWidth: "50rem",
},
},
@@ -47,6 +43,10 @@ const useStyles = makeStyles((theme) => ({
appBar: {
borderTop: `4px solid ${theme.palette.primary.main}`,
borderBottom: `1px solid ${theme.palette.grey[200]}`,
+ "& header": {
+ maxWidth: "55rem",
+ margin: "0 auto",
+ },
},
offset: theme.mixins.toolbar,
toolbar: {
diff --git a/src/features/cycle/slice.js b/src/features/cycle/slice.js
index 84068d6d..00f3cb3d 100644
--- a/src/features/cycle/slice.js
+++ b/src/features/cycle/slice.js
@@ -5,7 +5,6 @@ import { selectEntriesSortedByDate, selectEntryTags } from "../entries"
import { selectMenstruationTag, selectInitialDaysBetween } from "../settings"
import {
- makeDate,
daysBetweenDates,
isDateBefore,
isDateAfter,
diff --git a/src/features/timeline/TimelineIndexPage.js b/src/features/timeline/TimelineIndexPage.js
index c6b43264..56834af0 100644
--- a/src/features/timeline/TimelineIndexPage.js
+++ b/src/features/timeline/TimelineIndexPage.js
@@ -1,39 +1,59 @@
-import React from "react"
+import React, { useEffect } from "react"
import { useSelector } from "react-redux"
import { List, makeStyles } from "@material-ui/core"
+import { eachDayOfInterval, addDays, isToday } from "date-fns"
-import { makeDate, intervalAfterDate } from "../utils/days"
+import { makeDate, entryIdFromDate } from "../utils/days"
import { BrandLayout } from "../brand"
import { Welcome } from "../onboarding"
import { selectDaysBetween } from "../cycle"
-import DaySummary from "./DaySummary"
-import ForecastItem from "./ForecastItem"
+import TimelineItem from "./TimelineItem"
import DatePicker from "./DatePicker"
const useStyles = makeStyles((theme) => ({
forecast: {
maxWidth: "30rem",
- margin: theme.spacing(2, 0, 4),
},
}))
const CycleIndexPage = ({ entryId }) => {
- const date = makeDate(entryId)
const classes = useStyles()
+ const selectedDate = makeDate(entryId)
const calculatedDaysBetween = useSelector(selectDaysBetween)
- const afterInterval = intervalAfterDate(date, calculatedDaysBetween + 3)
+
+ const range = eachDayOfInterval({
+ start: addDays(selectedDate, calculatedDaysBetween * -1.5),
+ end: addDays(selectedDate, calculatedDaysBetween * 1.5),
+ })
+
+ useEffect(() => {
+ const scrollToId = `scrollTo-${entryIdFromDate(selectedDate)}`
+ const node = document.getElementById(scrollToId)
+ if (!node) return
+
+ node.scrollIntoView({
+ block: "start",
+ })
+ }, [selectedDate])
return (
- }>
-
-
+ }>
- {afterInterval.map((date) => {
- return
+ {range.map((date) => {
+ return (
+ <>
+
+ {isToday(date) && }
+ >
+ )
})}
diff --git a/src/features/timeline/TimelineItem/Entry.js b/src/features/timeline/TimelineItem/Entry.js
new file mode 100644
index 00000000..0fd05a25
--- /dev/null
+++ b/src/features/timeline/TimelineItem/Entry.js
@@ -0,0 +1,44 @@
+import React from "react"
+import PropTypes from "prop-types"
+import { Link } from "gatsby"
+import { useSelector } from "react-redux"
+import { isToday, isPast } from "date-fns"
+import { Typography, ButtonBase, Paper } from "@material-ui/core"
+import { Add as AddNoteIcon } from "@material-ui/icons"
+
+import { entryIdFromDate } from "../../utils/days"
+import { selectEntryNote } from "../../entries"
+
+const Entry = ({ date, ...props }) => {
+ const entryId = entryIdFromDate(date)
+ const editPath = `/timeline/${entryId}/edit`
+ const entryNote = useSelector((state) => selectEntryNote(state, { date }))
+ const isEditable = isPast(date) || isToday(date)
+
+ if (!isEditable) return null
+
+ return (
+
+
+ {entryNote ? (
+ {entryNote}
+ ) : (
+ <>
+
+ Add note
+ >
+ )}
+
+
+ )
+}
+
+Entry.propTypes = {
+ date: PropTypes.instanceOf(Date),
+}
+
+export default Entry
diff --git a/src/features/timeline/TimelineItem/Header.js b/src/features/timeline/TimelineItem/Header.js
new file mode 100644
index 00000000..09e6012b
--- /dev/null
+++ b/src/features/timeline/TimelineItem/Header.js
@@ -0,0 +1,52 @@
+import React from "react"
+import PropTypes from "prop-types"
+import { useSelector } from "react-redux"
+import { format, isToday, isSameDay } from "date-fns"
+import { Typography } from "@material-ui/core"
+
+import { selectIsMenstruationForDate } from "../../entries"
+import {
+ selectCycleDayForDate,
+ selectPredictedMenstruationForDate,
+} from "../../cycle"
+
+const TimelineHeader = ({ date, selectedDate, ...props }) => {
+ const cycleDay = useSelector((state) =>
+ selectCycleDayForDate(state, { date })
+ )
+
+ const isLoggedMenstruation = useSelector((state) =>
+ selectIsMenstruationForDate(state, { date })
+ )
+
+ const isPredictedMenstruation = useSelector((state) =>
+ selectPredictedMenstruationForDate(state, { date })
+ )
+
+ const isMenstruation = isLoggedMenstruation || isPredictedMenstruation
+
+ return (
+
+
+ {isToday(date) ? "Today" : format(date, "EEEE, MMMM do")}
+
+
+ Day {cycleDay}
+
+
+ )
+}
+
+TimelineHeader.propTypes = {
+ date: PropTypes.instanceOf(Date),
+}
+
+export default TimelineHeader
diff --git a/src/features/timeline/TimelineItem/Info.js b/src/features/timeline/TimelineItem/Info.js
new file mode 100644
index 00000000..f0d8ff52
--- /dev/null
+++ b/src/features/timeline/TimelineItem/Info.js
@@ -0,0 +1,46 @@
+import React from "react"
+import { useSelector } from "react-redux"
+import PropTypes from "prop-types"
+import { isToday, format } from "date-fns"
+import { Typography } from "@material-ui/core"
+
+import {
+ selectDaysBetween,
+ selectIsDaysBetweenCalculated,
+ selectIsDateCurrentCycle,
+ selectNextStartDate,
+} from "../../cycle"
+import { selectMenstruationTag } from "../../settings"
+
+const Info = ({ date, ...props }) => {
+ const menstruationTag = useSelector(selectMenstruationTag)
+
+ const daysBetween = useSelector(selectDaysBetween)
+ const isDaysBetweenCalculated = useSelector(selectIsDaysBetweenCalculated)
+
+ const isCurrentCycle = useSelector((state) =>
+ selectIsDateCurrentCycle(state, { date })
+ )
+ const nextStartDate = useSelector((state) =>
+ selectNextStartDate(state, { date })
+ )
+
+ if (!isToday(date) || !nextStartDate) return null
+
+ return (
+
+
+ #{menstruationTag} {!isCurrentCycle && "was"} estimated
+ to arrive {format(nextStartDate, "EEEE, MMMM do")}{" "}
+ based on {isDaysBetweenCalculated ? "your average" : "a default"}{" "}
+ {daysBetween || "?"} day cycle.{" "}
+
+
+ )
+}
+
+Info.propTypes = {
+ date: PropTypes.instanceOf(Date),
+}
+
+export default Info
diff --git a/src/features/timeline/TimelineItem/Predictions.js b/src/features/timeline/TimelineItem/Predictions.js
new file mode 100644
index 00000000..c5724b37
--- /dev/null
+++ b/src/features/timeline/TimelineItem/Predictions.js
@@ -0,0 +1,54 @@
+import React from "react"
+import PropTypes from "prop-types"
+import classNames from "classnames"
+import { useSelector } from "react-redux"
+import { Chip, makeStyles } from "@material-ui/core"
+
+import { selectPredictedTagsForDate } from "../../cycle"
+
+const useStyles = makeStyles((theme) => ({
+ tag: {
+ borderStyle: "dotted",
+ },
+ loggedTag: {
+ borderColor: theme.palette.text.secondary,
+ backgroundColor: theme.palette.background.paper,
+ },
+}))
+
+const Predictions = ({ date, ...props }) => {
+ const classes = useStyles()
+
+ const predictedTags = useSelector((state) =>
+ selectPredictedTagsForDate(state, { date })
+ )
+
+ if (predictedTags.length === 0) return null
+
+ return (
+
+ )
+}
+
+Predictions.propTypes = {
+ date: PropTypes.instanceOf(Date),
+}
+
+export default Predictions
diff --git a/src/features/timeline/TimelineItem/index.js b/src/features/timeline/TimelineItem/index.js
new file mode 100644
index 00000000..27817af8
--- /dev/null
+++ b/src/features/timeline/TimelineItem/index.js
@@ -0,0 +1,100 @@
+import React from "react"
+import PropTypes from "prop-types"
+import { addDays } from "date-fns"
+import { makeStyles, fade } from "@material-ui/core"
+
+import { entryIdFromDate } from "../../utils/days"
+
+import Info from "./Info"
+import Header from "./Header"
+import Entry from "./Entry"
+import Predictions from "./Predictions"
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ position: "relative",
+ marginBottom: theme.spacing(3),
+ display: "flex",
+ flexDirection: "column",
+ },
+ scrollTo: {
+ position: "absolute",
+ // Indicates how much of this entry (the day before selected)
+ // should show
+ bottom: "4rem",
+ width: "100%",
+ ...theme.mixins.toolbar,
+ },
+ header: {
+ padding: theme.spacing(0, 1),
+ display: "flex",
+ justifyContent: "space-between",
+ "& strong": {
+ fontWeight: theme.typography.fontWeightBold,
+ },
+ },
+ info: {
+ padding: theme.spacing(0.5, 1),
+ // margin: theme.spacing(0, `${theme.shape.borderRadius / 2}px`),
+ marginBottom: theme.spacing(1.5),
+ // backgroundColor: theme.palette.grey[100],
+ },
+ entry: {
+ padding: theme.spacing(2),
+ width: "100%",
+ display: "flex",
+ justifyContent: "flex-start",
+ "&:hover": {
+ backgroundColor: fade(
+ theme.palette.action.active,
+ theme.palette.action.hoverOpacity
+ ),
+ // Reset on touch devices, it doesn't add specificity
+ "@media (hover: none)": {
+ backgroundColor: "transparent",
+ },
+ },
+ "& svg": {
+ marginRight: theme.spacing(1),
+ },
+ "& *": {
+ whiteSpace: "pre-line",
+ },
+ },
+ predictions: {
+ padding: theme.spacing(1.5),
+ margin: theme.spacing(0, `${theme.shape.borderRadius / 2}px`),
+ // backgroundColor: theme.palette.grey[100],
+ backgroundColor: fade(
+ theme.palette.secondary.main,
+ theme.palette.action.hoverOpacity
+ ),
+ zIndex: "-1",
+ "& > *": {
+ margin: theme.spacing(0.5),
+ },
+ },
+}))
+
+const TimelineItem = ({ date, selectedDate, ...props }) => {
+ const classes = useStyles()
+
+ const scrollToId = `scrollTo-${entryIdFromDate(addDays(date, 1))}`
+ const itemProps = { date, selectedDate }
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+TimelineItem.propTypes = {
+ date: PropTypes.instanceOf(Date).isRequired,
+}
+
+export default TimelineItem