chore: move screens into routes folder
This commit is contained in:
314
src/routes/Day.js
Normal file
314
src/routes/Day.js
Normal file
@@ -0,0 +1,314 @@
|
||||
import React from "react"
|
||||
import { addDays, subDays, format, isAfter, startOfYesterday } from "date-fns"
|
||||
import { BeatLoader } from "react-spinners"
|
||||
import styled from "@emotion/styled"
|
||||
/** @jsx jsx */
|
||||
import { jsx, css, keyframes } from "@emotion/core"
|
||||
import { compose } from "recompose"
|
||||
import { withTheme } from "emotion-theming"
|
||||
|
||||
import { withFirebase } from "components/firebase"
|
||||
import { withAuthentication } from "components/session"
|
||||
import { OnlineContext } from "components/context/online"
|
||||
|
||||
import { SIZES } from "styles/constants"
|
||||
|
||||
import Seek from "components/Seek"
|
||||
import Icon from "components/Icon"
|
||||
|
||||
const EntryHeading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: ${SIZES.medium};
|
||||
`
|
||||
const EntryInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`
|
||||
const JournalHeading = styled.h2`
|
||||
font-weight: 700;
|
||||
font-size: ${SIZES.tiny};
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
display: block;
|
||||
`
|
||||
const SavedMessaged = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 5px;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
font-size: ${SIZES.tiny};
|
||||
user-select: none;
|
||||
`
|
||||
const OfflineNotice = styled.div`
|
||||
padding: 5px;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
border: 1px solid;
|
||||
border-color: ${props => props.theme.colors.tertiary};
|
||||
font-size: ${SIZES.tiny};
|
||||
border-radius: 3px;
|
||||
`
|
||||
const JournalEntryArea = styled.textarea`
|
||||
font-family: sans-serif;
|
||||
flex-grow: 0.85;
|
||||
color: ${props => props.theme.colors.primary};
|
||||
caret-color: ${props => props.theme.colors.secondary};
|
||||
background-color: transparent;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.5px;
|
||||
height: calc(100vh - 300px);
|
||||
width: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-size: ${SIZES.small};
|
||||
border-radius: 1px;
|
||||
margin-top: ${SIZES.tiny};
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
&::placeholder {
|
||||
color: ${props => props.theme.colors.tertiary};
|
||||
}
|
||||
&::selection {
|
||||
background: ${props => props.theme.colors.hover};
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 8px ${props => props.theme.colors.bodyBackground},
|
||||
0 0 0 10px ${props => props.theme.colors.hover};
|
||||
}
|
||||
`
|
||||
const Buttons = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 20px;
|
||||
`
|
||||
const fadeKeyFrames = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
const LoadingSpinner = styled(BeatLoader)`
|
||||
opacity: 0;
|
||||
`
|
||||
|
||||
const AUTOSAVE_DELAY = 2000
|
||||
|
||||
class Day extends React.Component {
|
||||
state = {
|
||||
text: "",
|
||||
loading: true,
|
||||
saving: false,
|
||||
lastSavedAt: new Date(),
|
||||
lastEditedAt: new Date(),
|
||||
}
|
||||
timeout = 0
|
||||
|
||||
static contextType = OnlineContext
|
||||
|
||||
componentDidMount() {
|
||||
const { year, month, day } = this.props
|
||||
this.getDocRef(year, month, day, false)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Typical usage (don't forget to compare props):
|
||||
if (this.props.uri !== prevProps.uri) {
|
||||
console.log("here we go")
|
||||
const [
|
||||
,
|
||||
,
|
||||
prevYear,
|
||||
prevMonth,
|
||||
prevDay,
|
||||
] = prevProps.location.pathname.split("/")
|
||||
const { text } = this.state
|
||||
this.saveText(text, prevYear, prevMonth, prevDay)
|
||||
const [, , year, month, day] = this.props.location.pathname.split("/")
|
||||
this.onRouteChanged(year, month, day)
|
||||
}
|
||||
}
|
||||
|
||||
onRouteChanged = (year, month, day) => {
|
||||
this.setState({ loading: true })
|
||||
this.getDocRef(year, month, day, false)
|
||||
}
|
||||
|
||||
getDocRef = (year, month, day, cacheFirst) => {
|
||||
const { firebase, authUser } = this.props
|
||||
const getOptions = {
|
||||
source: cacheFirst ? "cache" : "default",
|
||||
}
|
||||
const docRef = firebase.db
|
||||
.collection("entries")
|
||||
.doc(`${year}${month}${day}-${authUser.uid}`)
|
||||
this.getData(docRef, getOptions)
|
||||
}
|
||||
|
||||
getData = (docRef, options) => {
|
||||
docRef
|
||||
.get(options)
|
||||
.then(doc => {
|
||||
if (doc.data()) {
|
||||
this.setState({ text: doc.data().text, loading: false })
|
||||
} else {
|
||||
this.setState({ text: "", loading: false })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn("entry not found in cache")
|
||||
// no doc was found, so reset the entry area to blank
|
||||
this.setState({ loading: false, text: "" })
|
||||
// for cache first, server second fetching, dangerous with potential overwriting of data
|
||||
// docRef.get().then(doc => {
|
||||
// if (doc.data()) {
|
||||
// this.setState({ text: doc.data().text, loading: false });
|
||||
// } else {
|
||||
// this.setState({ text: "", loading: false });
|
||||
// }
|
||||
// });
|
||||
})
|
||||
}
|
||||
|
||||
onChangeText = e => {
|
||||
if (this.timeout) clearTimeout(this.timeout)
|
||||
const text = e.target.value
|
||||
const { year, month, day } = this.props
|
||||
|
||||
this.setState({ text, lastEditedAt: new Date() })
|
||||
this.timeout = setTimeout(() => {
|
||||
this.saveText(text, year, month, day)
|
||||
}, AUTOSAVE_DELAY)
|
||||
}
|
||||
|
||||
onInsertTime = () => {
|
||||
const entryTextArea = document.getElementById("entry-text-area")
|
||||
const cursorIndex = entryTextArea.selectionStart
|
||||
const { text } = this.state
|
||||
const { year, month, day } = this.props
|
||||
const insertAt = (str, sub, pos) =>
|
||||
`${str.slice(0, pos)}${sub}${str.slice(pos)}`
|
||||
const newText = insertAt(text, format(new Date(), "h:mma "), cursorIndex)
|
||||
this.setState({
|
||||
text: newText,
|
||||
})
|
||||
entryTextArea.focus()
|
||||
this.saveText(newText, year, month, day)
|
||||
}
|
||||
|
||||
saveText = (text, year, month, day) => {
|
||||
this.setState({ saving: true })
|
||||
const { firebase, authUser } = this.props
|
||||
firebase.db
|
||||
.collection("entries")
|
||||
.doc(`${year}${month}${day}-${authUser.uid}`)
|
||||
.set(
|
||||
{
|
||||
text,
|
||||
day: Number(day),
|
||||
year: Number(year),
|
||||
month: Number(month),
|
||||
userId: authUser.uid,
|
||||
},
|
||||
{
|
||||
merge: true,
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.setState({ saving: false, lastSavedAt: new Date() })
|
||||
})
|
||||
.catch(() => {
|
||||
console.warn("saving will occur when back online")
|
||||
this.setState({ saving: false })
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { year, month, day, theme } = this.props
|
||||
const online = this.context
|
||||
const { text, loading, saving, lastSavedAt, lastEditedAt } = this.state
|
||||
const currentDay = new Date(year, month - 1, day)
|
||||
if (!currentDay) return
|
||||
const hasSavedChanges = lastSavedAt >= lastEditedAt
|
||||
|
||||
return (
|
||||
<>
|
||||
<Seek
|
||||
title={format(currentDay, "YYYY MMM DD - dddd")}
|
||||
prev={format(subDays(currentDay, 1), "/YYYY/MM/DD")}
|
||||
next={format(addDays(currentDay, 1), "/YYYY/MM/DD")}
|
||||
disableNext={isAfter(currentDay, startOfYesterday())}
|
||||
/>
|
||||
<EntryHeading>
|
||||
<JournalHeading>RECORD THOUGHTS ABOUT YOUR DAY</JournalHeading>
|
||||
<EntryInfo>
|
||||
{online && (
|
||||
<SavedMessaged>
|
||||
{saving ? (
|
||||
<>
|
||||
Saving
|
||||
<LoadingSpinner
|
||||
color={theme.colors.quarternary}
|
||||
size={5}
|
||||
css={css`
|
||||
animation: 1s ease-in;
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
) : hasSavedChanges ? (
|
||||
`Last saved at ${format(lastSavedAt, "h:mma")}`
|
||||
) : (
|
||||
"Unsaved changes"
|
||||
)}
|
||||
</SavedMessaged>
|
||||
)}
|
||||
{!online && <OfflineNotice>Offline</OfflineNotice>}
|
||||
</EntryInfo>
|
||||
</EntryHeading>
|
||||
{loading ? (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<LoadingSpinner
|
||||
color={theme.colors.quarternary}
|
||||
size={10}
|
||||
margin="4px"
|
||||
css={css`
|
||||
animation: ${fadeKeyFrames} 1s ease-in;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<JournalEntryArea
|
||||
id="entry-text-area"
|
||||
autoFocus={true}
|
||||
placeholder="Start writing..."
|
||||
onChange={e => this.onChangeText(e)}
|
||||
value={text}
|
||||
css={css`
|
||||
animation: ${fadeKeyFrames} 0.2s ease-in;
|
||||
`}
|
||||
/>
|
||||
<Buttons>
|
||||
<Icon
|
||||
name="Clock"
|
||||
label="Quick Add Time"
|
||||
labelRight
|
||||
onClick={() => this.onInsertTime()}
|
||||
/>{" "}
|
||||
</Buttons>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withFirebase,
|
||||
withTheme,
|
||||
withAuthentication
|
||||
)(Day)
|
||||
89
src/routes/Month.js
Normal file
89
src/routes/Month.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { Component } from "react"
|
||||
import styled from "@emotion/styled"
|
||||
import {
|
||||
isAfter,
|
||||
isThisYear,
|
||||
isThisMonth,
|
||||
format,
|
||||
addMonths,
|
||||
subMonths,
|
||||
getDaysInMonth,
|
||||
startOfMonth,
|
||||
} from "date-fns"
|
||||
|
||||
import { AppLink as Link } from "components/elements"
|
||||
import Seek from "components/Seek"
|
||||
|
||||
const YearCardGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(75px, 1fr));
|
||||
grid-gap: 20px;
|
||||
margin-top: 20px;
|
||||
`
|
||||
const YearCard = styled.div`
|
||||
color: ${props =>
|
||||
props.disabled
|
||||
? props.theme.colors.quarternary
|
||||
: props.theme.colors.secondary};
|
||||
border: 1px solid;
|
||||
border-color: ${props => props.theme.colors.quarternary};
|
||||
padding: 25px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
border-color: ${props => !props.disabled && props.theme.colors.tertiary};
|
||||
}
|
||||
`
|
||||
|
||||
class Month extends Component {
|
||||
render() {
|
||||
const { year, month } = this.props
|
||||
const currentDay = new Date(year, month - 1)
|
||||
|
||||
// include all months unless it's this year
|
||||
let dayIndexesToInclude = 31
|
||||
if (isThisYear(currentDay)) {
|
||||
dayIndexesToInclude = new Date().getDate()
|
||||
}
|
||||
|
||||
let yearCards = []
|
||||
for (let i = 0; i < getDaysInMonth(currentDay); i++) {
|
||||
const isDisabled = dayIndexesToInclude <= i && isThisMonth(currentDay)
|
||||
if (isDisabled) {
|
||||
yearCards.push(
|
||||
<YearCard disabled={isDisabled} key={i}>
|
||||
{i + 1}
|
||||
</YearCard>
|
||||
)
|
||||
} else {
|
||||
yearCards.push(
|
||||
<Link
|
||||
key={i}
|
||||
to={format(new Date(year, month - 1, i + 1), "/YYYY/MM/DD")}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<YearCard key={i}>{i + 1}</YearCard>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Seek
|
||||
title={format(currentDay, "YYYY MMM")}
|
||||
prev={format(subMonths(currentDay, 1), "/YYYY/MM")}
|
||||
next={format(addMonths(currentDay, 1), "/YYYY/MM")}
|
||||
disableNext={isAfter(
|
||||
currentDay,
|
||||
startOfMonth(subMonths(new Date(), 1))
|
||||
)}
|
||||
/>
|
||||
<YearCardGrid>{yearCards}</YearCardGrid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Month
|
||||
178
src/routes/Search.js
Normal file
178
src/routes/Search.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Component } from "react"
|
||||
/** @jsx jsx */
|
||||
import { jsx, css, keyframes } from "@emotion/core"
|
||||
import styled from "@emotion/styled"
|
||||
import { compose } from "recompose"
|
||||
import { withTheme } from "emotion-theming"
|
||||
import { BeatLoader } from "react-spinners"
|
||||
|
||||
import { AppLink as Link } from "components/elements"
|
||||
import { Input } from "components/elements"
|
||||
import { withFirebase } from "components/firebase"
|
||||
import { withAuthentication } from "components/session"
|
||||
import { pad } from "utils/date"
|
||||
|
||||
const SearchGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 10px;
|
||||
margin-bottom: 60px;
|
||||
`
|
||||
const SearchLayout = styled.div`
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
margin-top: 20px;
|
||||
`
|
||||
|
||||
const SearchResult = styled.div`
|
||||
margin-top: 5px;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
color: ${props => props.theme.colors.primary};
|
||||
border: 1px solid;
|
||||
border-color: ${props => props.theme.colors.quarternary};
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-color: ${props => props.theme.colors.tertiary};
|
||||
}
|
||||
`
|
||||
|
||||
const fadeKeyFrames = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
const LoadingSpinner = styled(BeatLoader)`
|
||||
opacity: 0;
|
||||
`
|
||||
|
||||
class Search extends Component {
|
||||
state = {
|
||||
entries: [],
|
||||
allEntries: [],
|
||||
searchInput: "",
|
||||
loading: true,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getEntries()
|
||||
}
|
||||
|
||||
onChange = event => {
|
||||
const searchInput = event.target.value
|
||||
this.setState({ searchInput })
|
||||
this.filterEntries(searchInput)
|
||||
}
|
||||
|
||||
filterEntries = searchTerm => {
|
||||
const { allEntries } = this.state
|
||||
if (searchTerm === "") {
|
||||
this.setState({ entries: allEntries })
|
||||
} else {
|
||||
const filteredEntries = allEntries.filter(entry => {
|
||||
return entry.text.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
})
|
||||
this.setState({ entries: filteredEntries })
|
||||
}
|
||||
}
|
||||
|
||||
getEntries = async _ => {
|
||||
const { firebase, authUser } = this.props
|
||||
const entriesRef = await firebase.db
|
||||
.collection("entries")
|
||||
.where("userId", "==", authUser.uid)
|
||||
.get()
|
||||
const entries = entriesRef.docs.map(doc => doc.data()).reverse()
|
||||
// const sortedEntries = entries.sort((a, b) => {
|
||||
// return (
|
||||
// new Date(b.year, b.month - 1, b.day) -
|
||||
// new Date(a.year, a.month - 1, a.day)
|
||||
// )
|
||||
// })
|
||||
// console.log(sortedEntries)
|
||||
this.setState({ entries, allEntries: entries, loading: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { entries, searchInput, loading } = this.state
|
||||
const { theme } = this.props
|
||||
|
||||
return (
|
||||
<SearchLayout>
|
||||
<SearchGrid>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
value={searchInput}
|
||||
onChange={e => this.onChange(e)}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
colors={theme.colors}
|
||||
/>
|
||||
{loading ? (
|
||||
<div style={{ marginTop: 10, margin: "0 auto" }}>
|
||||
<LoadingSpinner
|
||||
color={theme.colors.quarternary}
|
||||
size={10}
|
||||
margin="4px"
|
||||
css={css`
|
||||
animation: ${fadeKeyFrames} 1s ease-in;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
) : entries.length > 0 ? (
|
||||
entries.map(
|
||||
(entry, index) =>
|
||||
entry.text.length > 1 && (
|
||||
<Link
|
||||
key={index}
|
||||
to={`${entry.year}/${pad(entry.month)}/${pad(entry.day)}`}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<SearchResult
|
||||
css={css`
|
||||
animation: ${fadeKeyFrames} 0.2s ease-in;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
font-style: italic;
|
||||
color: ${theme.colors.secondary};
|
||||
margin-bottom: 5px;
|
||||
`}
|
||||
>
|
||||
{entry.day}/{entry.month}/{entry.year}
|
||||
</div>
|
||||
<div>
|
||||
{entry.text.substring(0, 128)}
|
||||
{entry.text.length >= 128 && "..."}
|
||||
</div>
|
||||
</SearchResult>
|
||||
</Link>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
css={css`
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: ${theme.colors.tertiary};
|
||||
margin-top: 5px;
|
||||
`}
|
||||
>
|
||||
No entries to display
|
||||
</div>
|
||||
)}
|
||||
</SearchGrid>
|
||||
</SearchLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withFirebase,
|
||||
withTheme,
|
||||
withAuthentication
|
||||
)(Search)
|
||||
200
src/routes/Start.js
Normal file
200
src/routes/Start.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { Component } from "react"
|
||||
import { Link, StaticQuery, graphql } from "gatsby"
|
||||
import Img from "gatsby-image"
|
||||
import styled from "@emotion/styled"
|
||||
import { withTheme } from "emotion-theming"
|
||||
|
||||
import { SIZES } from "styles/constants"
|
||||
import { Button, P } from "components/elements"
|
||||
import { todayUrl } from "utils/date"
|
||||
import Icon from "components/Icon"
|
||||
import Logo from "components/Logo"
|
||||
|
||||
const StartGrid = styled.div`
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
color: ${props => props.theme.colors.primary};
|
||||
height: 100%;
|
||||
`
|
||||
const FeatureGrid = styled.div`
|
||||
display: grid;
|
||||
text-align: left;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 30px;
|
||||
`
|
||||
const FeatureRow = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 120px 3fr;
|
||||
`
|
||||
const FeatureText = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
const FeatureTitle = styled.div`
|
||||
font-size: ${SIZES["normal"]};
|
||||
font-weight: 600;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
`
|
||||
const FeatureDescription = styled.div`
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
`
|
||||
const Footer = styled.footer`
|
||||
margin-top: 120px;
|
||||
padding: 30px 0px;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
`
|
||||
const FooterLink = styled(Link)`
|
||||
cursor: pointer;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
text-decoration: none;
|
||||
margin: 10px;
|
||||
&:hover {
|
||||
color: ${props => props.theme.colors.tertiary};
|
||||
}
|
||||
`
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: "Monitor",
|
||||
title: "Cross Platform",
|
||||
desc:
|
||||
"Write from any internet connected device, with pages optimized for all screen sizes",
|
||||
},
|
||||
{
|
||||
icon: "Package",
|
||||
title: "Install as an App",
|
||||
desc:
|
||||
"Add to your home screen on iPhone or Adroid to use it like you would an app",
|
||||
},
|
||||
{
|
||||
icon: "CloudOff",
|
||||
title: "Offline Capable",
|
||||
desc:
|
||||
"Record thoughts as they come to you, whether you have internet or not, your entries are saved when you get a connection",
|
||||
},
|
||||
{
|
||||
icon: "Search",
|
||||
title: "Full Text Search",
|
||||
desc:
|
||||
"Search through your entries by text to quickly find past entries and recall what you've written",
|
||||
},
|
||||
{
|
||||
icon: "Download",
|
||||
title: "Export",
|
||||
desc:
|
||||
"Download all of your journal entries at any time for back-up or safe keeping ",
|
||||
},
|
||||
]
|
||||
|
||||
class Start extends Component {
|
||||
render() {
|
||||
const { theme } = this.props
|
||||
return (
|
||||
<StartGrid>
|
||||
<h1>Record what's on your mind, from anywhere</h1>
|
||||
<P style={{ letterSpacing: 1.1, marginBottom: 30 }}>
|
||||
Journaling can improve your health and help you take inventory of your
|
||||
day. Sol Journal works offline and from any device. Use it as a place
|
||||
to record thoughts and events from the day.
|
||||
</P>
|
||||
<Link to={`/app${todayUrl()}`} style={{ textDecoration: "none" }}>
|
||||
<Button colors={theme.colors}>Start Writing</Button>
|
||||
</Link>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
margin: "30 auto",
|
||||
maxHeight: 350,
|
||||
}}
|
||||
>
|
||||
<StaticQuery
|
||||
query={graphql`
|
||||
query {
|
||||
landingGraphicLight: file(
|
||||
relativePath: { eq: "landing-graphic-light.png" }
|
||||
) {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 320) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
landingGraphicDark: file(
|
||||
relativePath: { eq: "landing-graphic-dark.png" }
|
||||
) {
|
||||
childImageSharp {
|
||||
fluid(maxWidth: 320) {
|
||||
...GatsbyImageSharpFluid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
render={data => {
|
||||
return theme.name === "Light" ? (
|
||||
<Img
|
||||
style={{
|
||||
maxWidth: 320,
|
||||
width: "100%",
|
||||
maxHeight: 350,
|
||||
height: "100%",
|
||||
}}
|
||||
fluid={data.landingGraphicLight.childImageSharp.fluid}
|
||||
/>
|
||||
) : (
|
||||
<Img fluid={data.LandingGraphicDark.childImageSharp.fluid} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
margin: "60px 0px",
|
||||
borderTop: `1px solid ${theme.colors.quarternary}`,
|
||||
}}
|
||||
/>
|
||||
<h2>Features</h2>
|
||||
<P style={{ letterSpacing: 1.1, marginBottom: 30 }}>
|
||||
Lightweight with the functionalities you need for journaling, and none
|
||||
of the things you don't:
|
||||
</P>
|
||||
<FeatureGrid>
|
||||
{features.map(feature => (
|
||||
<FeatureRow>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Icon disabled name={feature.icon} size={60} />
|
||||
</div>
|
||||
<FeatureText>
|
||||
<FeatureTitle>{feature.title}</FeatureTitle>
|
||||
<FeatureDescription>{feature.desc}</FeatureDescription>
|
||||
</FeatureText>
|
||||
</FeatureRow>
|
||||
))}
|
||||
</FeatureGrid>
|
||||
<Footer>
|
||||
<div>
|
||||
<Logo color={theme.colors.logo} />
|
||||
</div>
|
||||
<div>
|
||||
<FooterLink>View on GitHub</FooterLink>
|
||||
<FooterLink to="terms">Terms of Service</FooterLink>
|
||||
<FooterLink to="privacy">Privacy Policy</FooterLink>
|
||||
</div>
|
||||
<div>© 2019</div>
|
||||
</Footer>
|
||||
</StartGrid>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(Start)
|
||||
181
src/routes/User.js
Normal file
181
src/routes/User.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from "react"
|
||||
import styled from "@emotion/styled"
|
||||
import { withTheme } from "emotion-theming"
|
||||
import { compose } from "recompose"
|
||||
import { format } from "date-fns"
|
||||
import { BeatLoader } from "react-spinners"
|
||||
|
||||
import { SIZES } from "styles/constants"
|
||||
import { withFirebase } from "components/firebase"
|
||||
import { withAuthentication } from "components/session"
|
||||
import SignOut from "components/SignOut"
|
||||
import { Button } from "components/elements"
|
||||
|
||||
const ProfileGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 10px;
|
||||
margin-top: 20px;
|
||||
`
|
||||
const ProfileSection = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
const ProfileSectionHeader = styled.h2`
|
||||
font-size: ${SIZES.normal};
|
||||
color: ${props => props.theme.colors.tertiary};
|
||||
`
|
||||
const ProfileSectionText = styled.span`
|
||||
font-size: ${SIZES.normal};
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
`
|
||||
|
||||
class User extends React.Component {
|
||||
state = {
|
||||
files: [],
|
||||
exporting: false,
|
||||
}
|
||||
|
||||
getEntries = async _ => {
|
||||
const { firebase, authUser } = this.props
|
||||
const entriesRef = await firebase.db
|
||||
.collection("entries")
|
||||
.where("userId", "==", authUser.uid)
|
||||
.get()
|
||||
const entries = entriesRef.docs.map(doc => doc.data())
|
||||
const editedEntries = entries.map(entry => {
|
||||
return { ...entry, userId: undefined }
|
||||
})
|
||||
return editedEntries
|
||||
}
|
||||
|
||||
clearFiles = () => {
|
||||
if (this.state.files.length) {
|
||||
this.state.files.forEach(({ data }) => {
|
||||
window.URL.revokeObjectURL(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
prepareExport = async () => {
|
||||
try {
|
||||
this.clearFiles()
|
||||
|
||||
this.setState({ exporting: true, files: [] })
|
||||
|
||||
const data = await this.getEntries()
|
||||
const blob = new Blob([JSON.stringify(data)], {
|
||||
type: "text/json;charset=utf-8",
|
||||
})
|
||||
|
||||
const file = {
|
||||
name: `journal-export-${format(new Date(), "MMDDYYYY")}.json`,
|
||||
data: window.URL.createObjectURL(blob),
|
||||
}
|
||||
this.setState({ files: [file], exporting: false })
|
||||
} catch (e) {
|
||||
window.alert(
|
||||
"Your export ran into an issue, sorry :( if you continue to have problmes you can reach out to kylerobertgill@gmail.com"
|
||||
)
|
||||
console.error(e)
|
||||
this.setState({ files: [], exporting: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { authUser, theme, firebase } = this.props
|
||||
const { exporting, files } = this.state
|
||||
return (
|
||||
<ProfileGrid>
|
||||
<ProfileSection>
|
||||
<ProfileSectionHeader>
|
||||
User: <ProfileSectionText>{authUser.email}</ProfileSectionText>
|
||||
<div>
|
||||
<ProfileSectionText style={{ fontWeight: 400 }}>
|
||||
{authUser.emailVerified
|
||||
? "Email has been verified"
|
||||
: "Email not verified"}
|
||||
</ProfileSectionText>
|
||||
</div>
|
||||
</ProfileSectionHeader>
|
||||
<SignOut />
|
||||
</ProfileSection>
|
||||
<ProfileSection>
|
||||
<ProfileSectionHeader>
|
||||
Reset Password{" "}
|
||||
<div>
|
||||
<ProfileSectionText style={{ fontWeight: 400 }}>
|
||||
send an email with reset instructions
|
||||
</ProfileSectionText>
|
||||
</div>
|
||||
</ProfileSectionHeader>
|
||||
|
||||
<Button
|
||||
fontSize="small"
|
||||
colors={theme.colors}
|
||||
onClick={() => {
|
||||
console.log("reset!")
|
||||
firebase.doPasswordReset(authUser.email)
|
||||
}}
|
||||
>
|
||||
Send Reset
|
||||
</Button>
|
||||
</ProfileSection>
|
||||
<ProfileSection>
|
||||
<ProfileSectionHeader>
|
||||
Export Journal Entries{" "}
|
||||
<div>
|
||||
<ProfileSectionText style={{ fontWeight: 400 }}>
|
||||
download all journal entries into a JSON file
|
||||
</ProfileSectionText>
|
||||
</div>
|
||||
</ProfileSectionHeader>
|
||||
{files.length ? (
|
||||
<a
|
||||
download={files[0].name}
|
||||
href={files[0].data}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<Button
|
||||
fontSize="small"
|
||||
colors={theme.colors}
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
this.clearFiles()
|
||||
this.setState({ exporting: 0, files: [] })
|
||||
}, 1500)
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
fontSize="small"
|
||||
colors={theme.colors}
|
||||
onClick={() => this.prepareExport()}
|
||||
>
|
||||
{exporting ? (
|
||||
<BeatLoader
|
||||
color={theme.colors.secondary}
|
||||
size={10}
|
||||
margin="4px"
|
||||
/>
|
||||
) : (
|
||||
"Export"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</ProfileSection>
|
||||
</ProfileGrid>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withFirebase,
|
||||
withAuthentication,
|
||||
withTheme
|
||||
)(User)
|
||||
61
src/routes/Welcome.js
Normal file
61
src/routes/Welcome.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { Component } from "react"
|
||||
import { Link } from "gatsby"
|
||||
import styled from "@emotion/styled"
|
||||
import { withTheme } from "emotion-theming"
|
||||
|
||||
import { Button, P } from "components/elements"
|
||||
import Logo from "components/Logo"
|
||||
import { todayUrl } from "utils/date"
|
||||
|
||||
const WelcomeGrid = styled.div`
|
||||
margin-top: 30px;
|
||||
line-height: 1.5;
|
||||
color: ${props => props.theme.colors.primary};
|
||||
height: 100%;
|
||||
`
|
||||
const Footer = styled.footer`
|
||||
margin-top: 120px;
|
||||
padding: 30px 0px;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
`
|
||||
const FooterLink = styled(Link)`
|
||||
cursor: pointer;
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
text-decoration: none;
|
||||
margin: 10px;
|
||||
&:hover {
|
||||
color: ${props => props.theme.colors.tertiary};
|
||||
}
|
||||
`
|
||||
|
||||
class Welcome extends Component {
|
||||
render() {
|
||||
const { theme } = this.props
|
||||
return (
|
||||
<WelcomeGrid>
|
||||
<h1>Your Space for Wandering Thoughts and Ideas</h1>
|
||||
<P style={{ letterSpacing: 1.1, marginBottom: 30 }}>
|
||||
This your space for wandering thoughts and ideas. Write about whatever
|
||||
comes to mind.
|
||||
</P>
|
||||
<Link to={`/app${todayUrl()}`} style={{ textDecoration: "none" }}>
|
||||
<Button colors={theme.colors}>Write about today</Button>
|
||||
</Link>
|
||||
<Footer>
|
||||
<div>
|
||||
<Logo color={theme.colors.logo} />
|
||||
</div>
|
||||
<div>
|
||||
<FooterLink>View on GitHub</FooterLink>
|
||||
<FooterLink to="terms">Terms of Service</FooterLink>
|
||||
<FooterLink to="privacy">Privacy Policy</FooterLink>
|
||||
</div>
|
||||
<div>© 2019</div>
|
||||
</Footer>
|
||||
</WelcomeGrid>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(Welcome)
|
||||
73
src/routes/Year.js
Normal file
73
src/routes/Year.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { Component } from "react"
|
||||
import { addYears, subYears, format, isThisYear, getMonth } from "date-fns"
|
||||
import styled from "@emotion/styled"
|
||||
import { withTheme } from "emotion-theming"
|
||||
|
||||
import { AppLink as Link } from "components/elements"
|
||||
import Seek from "components/Seek"
|
||||
import { months } from "utils/date"
|
||||
|
||||
const MonthCardGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
grid-gap: 20px;
|
||||
margin-top: 20px;
|
||||
`
|
||||
const MonthCard = styled.div`
|
||||
color: ${props =>
|
||||
props.disabled
|
||||
? props.theme.colors.quarternary
|
||||
: props.theme.colors.secondary};
|
||||
border: 1px solid;
|
||||
border-color: ${props => props.theme.colors.quarternary};
|
||||
padding: 40px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
border-color: ${props => !props.disabled && props.theme.colors.tertiary};
|
||||
}
|
||||
`
|
||||
|
||||
class Year extends Component {
|
||||
render() {
|
||||
const { year } = this.props
|
||||
const currentDate = new Date(year, 0, 1)
|
||||
|
||||
// include all months unless it's this year
|
||||
let monthIndexesToInclude = 11
|
||||
if (isThisYear(currentDate)) {
|
||||
monthIndexesToInclude = getMonth(new Date())
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Seek
|
||||
title={year}
|
||||
prev={format(subYears(currentDate, 1), "/YYYY")}
|
||||
next={format(addYears(currentDate, 1), "/YYYY")}
|
||||
disableNext={year >= new Date().getFullYear()}
|
||||
/>
|
||||
<MonthCardGrid>
|
||||
{months.long.map((month, index) => {
|
||||
const isDisabled = monthIndexesToInclude < index
|
||||
return isDisabled ? (
|
||||
<MonthCard key={index} disabled={isDisabled}>
|
||||
{month}
|
||||
</MonthCard>
|
||||
) : (
|
||||
<Link
|
||||
key={index}
|
||||
to={format(new Date(year, index), "/YYYY/MM")}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<MonthCard>{month}</MonthCard>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</MonthCardGrid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(Year)
|
||||
Reference in New Issue
Block a user