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