chore: add comments, setup instructions

This commit is contained in:
Kyle Gill
2019-05-29 20:50:22 -06:00
parent 477dc9e22c
commit 4dbe7ea49d
17 changed files with 91 additions and 20 deletions

View File

@@ -8,7 +8,10 @@
Sol Journal is a simple, minimal, journaling platform that works offline and across all devices. It can be self-hosted through Firebase and then installed as a PWA, on mobile devices for easy access on a phone, or on Desktops. Sol Journal is a simple, minimal, journaling platform that works offline and across all devices. It can be self-hosted through Firebase and then installed as a PWA, on mobile devices for easy access on a phone, or on Desktops.
<p align="center"> <p align="center">
<img alt="preview of page" src="https://raw.githubusercontent.com/gillkyle/images/master/hero-mixed.png" /> <img
alt="preview of page"
src="https://raw.githubusercontent.com/gillkyle/images/master/hero-mixed.png"
/>
</p> </p>
## Introduction ## Introduction
@@ -31,13 +34,25 @@ Sol Journal uses firebase to support offline functionality and authentication, m
In the spirit of minimalism, key features are what are in place for a quick, lightweight journaling experience that can work across devices, including: In the spirit of minimalism, key features are what are in place for a quick, lightweight journaling experience that can work across devices, including:
- 🔥 Authentication: Cloud firestore persists registered users to a users document and saved journal entries to an entries document - 🔥 Authentication: Cloud firestore persists registered users to a users document and saved journal entries to an entries document
- 🎨 Dark Theme: the `src/styles/theme.js` file contains a set of colors and default styles that are applied to components with Emotion. A default light and dark theme are already in the file - 🎨 Theming: the `src/styles/theme.js` file contains a set of colors and default styles that are applied to components with Emotion. A default light and dark theme are already in the file
- 🔍 Search: full-text search of a user's entries stored in Firestore for quick access to past entries - 🔍 Search: full-text search of a user's entries stored in Firestore for quick access to past entries
- 🖥 Mobile Friendly: designed to look great on mobile as well as desktop, with easy navigation on both - 🖥 Mobile Friendly: designed to look great on mobile as well as desktop, with easy navigation on both
- 💡 PWA: being a progressive web app makes it installable from Chrome/Safari on desktop, or be added to the homescreen on iOS/Android - 💡 PWA: being a progressive web app makes it installable from Chrome/Safari on desktop, or be added to the homescreen on iOS/Android
- 🔌 Offline Support: read/write when you're offline and let the updates happen when your connection is restored - 🔌 Offline Support: read/write when you're offline and let the updates happen when your connection is restored
- 🗄 Export: backup all of your entries at any time to save your data - 🗄 Export: backup all of your entries at any time to save your data
## Project setup
Files are organized into these folders:
`/components`: user interface pieces to construct the design and layout of the site
`/data`: local data transformed by gatsby to become queryable by Gatsby's GraphQL data layer
`/img`: images used by places like landing pages that are optimized by gatsby-image and then queryable in the GraphQL layer
`/pages`: public pages that can be seen by unauthenticated users and are completely server side rendered by Gatsby during `gatsby build`
`/routes`: private, client only routes only visible to authenticated users that are used by the app section of the journal
`/styles`: role based design tokens and theme definitions
`/util`: simple utility functions, for things like formatting dates
## Developing ## Developing
Clone the project: Clone the project:
@@ -74,10 +89,10 @@ GATSBY_CONFIRMATION_EMAIL_REDIRECT=https://<name>.firebaseapp.com
Navigate into the project directory, and then launch the site with this command: Navigate into the project directory, and then launch the site with this command:
```bash ```bash
yarn develop gatsby develop
``` ```
The site will be opened up in your default browser on http://localhost:3000 The site will be opened up in your default browser on http://localhost:8000
Edit code in the `/src`, save your changes, and they'll reload instantly in the browser. Edit code in the `/src`, save your changes, and they'll reload instantly in the browser.
@@ -86,10 +101,12 @@ Edit code in the `/src`, save your changes, and they'll reload instantly in the
To create an optimized build of the site run this command: To create an optimized build of the site run this command:
```bash ```bash
yarn build gatsby build
``` ```
A `/build` folder will be assembled that can be deployed with this command: A `/build` folder will be assembled that can be deployed to any static file hosting service like Netlify or surge.
It can be deployed to firebase with this command:
```bash ```bash
firebase deploy firebase deploy

View File

@@ -2,7 +2,6 @@
Setting up Sol Journal requires a free Firebase account since many of the features rely on built in Firebase capabilities to allow functionality offline. The free plan gives you 50,000 reads and 20,000 writes per day, meaning you can easily run a personal version for yourself. Setting up Sol Journal requires a free Firebase account since many of the features rely on built in Firebase capabilities to allow functionality offline. The free plan gives you 50,000 reads and 20,000 writes per day, meaning you can easily run a personal version for yourself.
## Setting up Firebase ## Setting up Firebase
You will need a Google account to create a Firebase project, then navigate to the Firebase Console: https://console.firebase.google.com/ You will need a Google account to create a Firebase project, then navigate to the Firebase Console: https://console.firebase.google.com/
@@ -71,7 +70,7 @@ In order to prevent would be hackers or nefarious folks from messing with data i
##### Configuration ##### Configuration
## Code Setup ## Setting up Code
Having node installed is a prerequistie, you can follow instructions to get it setup yourself. Having node installed is a prerequistie, you can follow instructions to get it setup yourself.
@@ -88,3 +87,30 @@ firebase login
``` ```
Follow the prompted instructions to connect your account. Follow the prompted instructions to connect your account.
Clone the repository:
```bash
git clone https://github.com/gillkyle/sol-journal
```
Then change the name of the `.env.sample` file to `.env` (or `.env.production`/`.env.development` if you would like to run the app in different environments) and fill it in, mapping your own keys from your Firebase project to the corresponding names in the file.
```env
GATSBY_FIREBASE_API_KEY=<BUNCHofRandomNumbersAndChars>
GATSBY_DEV_AUTH_DOMAIN=<name>.firebaseapp.com
GATSBY_DEV_DATABASE_URL=https://<name>.firebaseio.com
GATSBY_DEV_PROJECT_ID=<name>
GATSBY_DEV_STORAGE_BUCKET=<name>.appspot.com
GATSBY_DEV_MESSAGING_SENDER_ID=############
GATSBY_CONFIRMATION_EMAIL_REDIRECT=https://<name>.firebaseapp.com
```
Then run the build command to generate an optimized build in the public directory.
```bash
gatsby build
```
You can upload your `/public` folder to S3, or run the surge command inside the directory to deploy it to a free subdomain.

View File

@@ -1,9 +1,7 @@
module.exports = { module.exports = {
plugins: [ plugins: [
`gatsby-plugin-react-helmet`, `gatsby-plugin-react-helmet`,
{ `gatsby-plugin-emotion`,
resolve: `gatsby-plugin-emotion`,
},
// create routes for client side routing // create routes for client side routing
{ {
resolve: `gatsby-plugin-create-client-paths`, resolve: `gatsby-plugin-create-client-paths`,
@@ -53,7 +51,8 @@ module.exports = {
path: `./src/data/`, path: `./src/data/`,
}, },
}, },
// easier imports and exports // easier imports and exports by defining aliases
// for commonly used folders
{ {
resolve: "gatsby-plugin-module-resolver", resolve: "gatsby-plugin-module-resolver",
options: { options: {

View File

@@ -8,6 +8,7 @@ import { withTheme } from "emotion-theming"
const Layout = ({ children, theme }) => ( const Layout = ({ children, theme }) => (
<> <>
<Helmet title="Sol Journal" /> <Helmet title="Sol Journal" />
{/* some styles should applied globally via the layout */}
<Global <Global
styles={css` styles={css`
* { * {

View File

@@ -111,6 +111,7 @@ const Navbar = ({ authUser, theme, toggleTheme }) => (
</Header> </Header>
) )
// on langing page and simple pages outside the app, simplify link options
const SimpleNav = ({ authUser, theme }) => ( const SimpleNav = ({ authUser, theme }) => (
<Header> <Header>
<Nav> <Nav>

View File

@@ -1,6 +1,7 @@
import React from "react" import React from "react"
import { Redirect, Location } from "@reach/router" import { Redirect, Location } from "@reach/router"
// when a user isn't logged in, force a redirect
const PrivateRoute = ({ component: Component, authed, ...rest }) => { const PrivateRoute = ({ component: Component, authed, ...rest }) => {
return ( return (
<Location> <Location>

View File

@@ -1,5 +1,6 @@
import React from "react" import React from "react"
// when navigator is available outside of the build phase, provide it through Context
export const OnlineContext = React.createContext({ export const OnlineContext = React.createContext({
online: typeof window !== "undefined" && navigator && navigator.onLine, online: typeof window !== "undefined" && navigator && navigator.onLine,
}) })

View File

@@ -2,6 +2,8 @@ import React from "react"
import { Helmet } from "react-helmet" import { Helmet } from "react-helmet"
import theme from "styles/theme" import theme from "styles/theme"
// create an app-wide context for the theme being used as
// well as a function to toggle it back and forth
const ThemeTogglerContext = React.createContext({ const ThemeTogglerContext = React.createContext({
themeName: "LIGHT", themeName: "LIGHT",
toggle: () => {}, toggle: () => {},

View File

@@ -71,6 +71,7 @@ export const P = styled.p`
color: ${props => props.theme.colors.secondary}; color: ${props => props.theme.colors.secondary};
` `
// prepend links used within the app section with app
export const AppLink = props => <Link {...props} to={"/app" + props.to} /> export const AppLink = props => <Link {...props} to={"/app" + props.to} />
export const StyledLink = withTheme(styled(AppLink)` export const StyledLink = withTheme(styled(AppLink)`

View File

@@ -1,5 +1,6 @@
import React from "react" import React from "react"
// create context of firebase instance
const FirebaseContext = React.createContext(null) const FirebaseContext = React.createContext(null)
export const withFirebase = Component => props => ( export const withFirebase = Component => props => (

View File

@@ -2,6 +2,8 @@ import app from "firebase/app"
import "firebase/auth" import "firebase/auth"
import "firebase/firestore" import "firebase/firestore"
// store private keys in .env file
// the prefix GATSBY_ is necessary here
const config = { const config = {
apiKey: process.env.GATSBY_FIREBASE_API_KEY, apiKey: process.env.GATSBY_FIREBASE_API_KEY,
authDomain: process.env.GATSBY_DEV_AUTH_DOMAIN, authDomain: process.env.GATSBY_DEV_AUTH_DOMAIN,
@@ -13,6 +15,8 @@ const config = {
class Firebase { class Firebase {
constructor() { constructor() {
// protect with conditional so gatsby build doesn't have
// issues trying to access the window object
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
app.initializeApp(config) app.initializeApp(config)
this.auth = app.auth() this.auth = app.auth()
@@ -34,20 +38,25 @@ class Firebase {
} }
} }
// Auth // authentication
// create user in the database
doCreateUserWithEmailAndPassword = (email, password) => doCreateUserWithEmailAndPassword = (email, password) =>
this.auth.createUserWithEmailAndPassword(email, password) this.auth.createUserWithEmailAndPassword(email, password)
// login already existing user
doSignInWithEmailAndPassword = (email, password) => doSignInWithEmailAndPassword = (email, password) =>
this.auth.signInWithEmailAndPassword(email, password) this.auth.signInWithEmailAndPassword(email, password)
// sign out user
doSignOut = () => { doSignOut = () => {
this.auth.signOut() this.auth.signOut()
window.location.replace("/login") window.location.replace("/login")
} }
// send email reset to email provided
doPasswordReset = email => this.auth.sendPasswordResetEmail(email) doPasswordReset = email => this.auth.sendPasswordResetEmail(email)
// change password to password provided
doPasswordUpdate = password => this.auth.currentUser.updatePassword(password) doPasswordUpdate = password => this.auth.currentUser.updatePassword(password)
} }

View File

@@ -3,11 +3,14 @@ import React from "react"
import AuthUserContext from "./context" import AuthUserContext from "./context"
import { withFirebase } from "components/firebase" import { withFirebase } from "components/firebase"
// use context to provide all app components with information about
// the authUser if it's been put in localStorage
const withAuthentication = Component => { const withAuthentication = Component => {
class WithAuthentication extends React.Component { class WithAuthentication extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
// protect browser API's like localStorage so Gatsby builds don't fail
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
this.state = { this.state = {
authUser: JSON.parse(localStorage.getItem("authUser")), authUser: JSON.parse(localStorage.getItem("authUser")),
@@ -20,6 +23,7 @@ const withAuthentication = Component => {
componentDidMount() { componentDidMount() {
this.listener = this.props.firebase.auth.onAuthStateChanged( this.listener = this.props.firebase.auth.onAuthStateChanged(
authUser => { authUser => {
// accessing localStorage in componentDidMount is fine in Gatsby
localStorage.setItem("authUser", JSON.stringify(authUser)) localStorage.setItem("authUser", JSON.stringify(authUser))
if (authUser) { if (authUser) {
this.setState({ this.setState({

View File

@@ -3,6 +3,11 @@ import React from "react"
import App from "../App" import App from "../App"
import Layout from "components/Layout" import Layout from "components/Layout"
/* similar to create-react-app, the App.js is like the
entrypoint to the protected/client only content,
Context providers are moved to gatsby-browser.js
to wrap the root element with necessary providers
and context */
export default () => ( export default () => (
<Layout> <Layout>
<App /> <App />

View File

@@ -4,6 +4,9 @@ import Index from "routes/Start"
import Layout from "components/Layout" import Layout from "components/Layout"
import Container from "components/container" import Container from "components/container"
// the landing page is generated like other Gatsby pages
// other unprotected routes like /login, /privacy, etc
// are completely server side rendered by gatsby build
export default () => ( export default () => (
<Layout> <Layout>
<SimpleNavbar /> <SimpleNavbar />

View File

@@ -116,6 +116,7 @@ class Day extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
// check if a new route was hit, to save the entry and load the next one
if (this.props.uri !== prevProps.uri) { if (this.props.uri !== prevProps.uri) {
const [ const [
, ,

View File

@@ -79,6 +79,8 @@ class Search extends Component {
} }
} }
// all entries for a user are fetched this scales poorly but is much
// simpler than an API key and setting up an index for Algolia
getEntries = async _ => { getEntries = async _ => {
const { firebase, authUser } = this.props const { firebase, authUser } = this.props
const entriesRef = await firebase.db const entriesRef = await firebase.db
@@ -86,13 +88,7 @@ class Search extends Component {
.where("userId", "==", authUser.uid) .where("userId", "==", authUser.uid)
.get() .get()
const entries = entriesRef.docs.map(doc => doc.data()).reverse() 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 }) this.setState({ entries, allEntries: entries, loading: false })
} }

View File

@@ -1,3 +1,6 @@
// standardized role-based design tokens used throughout the whole app
// a name like lightGray doesn't make sense with themese when light
// and dark are possibile
const theme = { const theme = {
LIGHT: { LIGHT: {
name: "LIGHT", name: "LIGHT",