├── burger.png
├── .firebaserc
├── public
├── static
│ ├── offline-sprite.png
│ ├── favicons
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ └── safari-pinned-tab.svg
│ ├── print.css
│ ├── burger.svg
│ ├── addtohomescreen.css
│ └── addtohomescreen.min.js
├── manifest.json
└── index.html
├── bin
├── deploy.sh
├── appcache.sh
└── build.sh
├── src
├── index.css
├── App.test.js
├── index.js
├── heart.svg
├── Schema.js
├── Settings.js
├── App.css
├── Common.js
├── Store.js
├── Favorites.js
├── Days.js
├── Search.js
├── User.js
├── SignIn.js
├── Nav.js
├── Group.js
├── Day.js
└── App.js
├── .gitignore
├── sw-precache-config.js
├── .editorconfig
├── README.md
├── database-rules.json
├── Makefile
├── package.json
├── firebase.json
└── TODO
/burger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/burger.png
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "dev": "dinnerd-dev",
4 | "prod": "dinnerd-45b97"
5 | }
6 | }
--------------------------------------------------------------------------------
/public/static/offline-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/public/static/offline-sprite.png
--------------------------------------------------------------------------------
/public/static/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/public/static/favicons/favicon.ico
--------------------------------------------------------------------------------
/public/static/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/public/static/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/static/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/public/static/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/static/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/public/static/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/bin/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eo pipefail
3 |
4 |
5 | #firebase deploy
6 | firebase deploy --only database
7 | firebase deploy --only hosting
8 |
--------------------------------------------------------------------------------
/public/static/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/public/static/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/static/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/dinnerd/master/public/static/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | /*body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }*/
6 |
7 |
8 |
9 | body, .smooth-container { scroll-behavior: smooth; }
10 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | });
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 | firebase-debug.log
17 |
--------------------------------------------------------------------------------
/sw-precache-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stripPrefix: 'build/',
3 | staticFileGlobs: [
4 | 'build/*.html',
5 | 'build/manifest.json',
6 | 'build/static/**/!(*map|*~)',
7 | ],
8 | dontCacheBustUrlsMatching: /\.\w{8}\./,
9 | swFilePath: 'build/service-worker.js'
10 | };
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.py]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [Makefile]
16 | indent_style = tab
17 | indent_size = 4
18 |
--------------------------------------------------------------------------------
/bin/appcache.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eo pipefail
3 |
4 |
5 | # "build/**/*.png" \
6 | appcache-manifest \
7 | "build/index.{html,css,js}" \
8 | "build/**/*.{css,js}" \
9 | "build/static/*.png" \
10 | --network-star \
11 | -o build/index.appcache
12 |
13 | echo "APPCACHE BUILT:"
14 | cat build/index.appcache
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Experimental App for planning and logging what you have for dinner.
2 |
3 | ### To run the first time:
4 |
5 | yarn install
6 |
7 | ### To start:
8 |
9 | yarn start
10 |
11 |
12 | ### Artwork
13 |
14 | Burger picture comes from
15 | https://www.iconfinder.com/icons/1760342/big_mac_burger_icon#size=512
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | // if (process.env.NODE_ENV !== 'production') {
7 | // const {whyDidYouUpdate} = require('why-did-you-update')
8 | // whyDidYouUpdate(React)
9 | // }
10 |
11 | ReactDOM.render(
12 | ,
13 | document.getElementById('root')
14 | );
15 |
--------------------------------------------------------------------------------
/database-rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "groups": {
4 | ".read": "auth != null",
5 | ".write": "auth != null",
6 | "$groupId": {
7 | "days": {
8 | ".indexOn": ["datetime"]
9 | }
10 | }
11 | },
12 | "group-codes": {
13 | ".read": "auth != null",
14 | ".write": "auth != null"
15 | },
16 | "user-groups": {
17 | ".read": "auth != null",
18 | ".write": "auth != null"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build appcache deploy release
2 |
3 | help:
4 | @echo "The list of commands for local development:\n"
5 | @echo " build Build the static files"
6 | @echo " appcache Generate the appcache manifest"
7 | @echo " deploy Send build to Firebase"
8 | @echo " release All of the above\n"
9 |
10 | build:
11 | ./bin/build.sh
12 |
13 | appcache:
14 | ./bin/appcache.sh
15 |
16 | deploy:
17 | ./bin/deploy.sh
18 |
19 |
20 | release: build appcache deploy
21 |
--------------------------------------------------------------------------------
/src/heart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/print.css:
--------------------------------------------------------------------------------
1 | body, h2, h3, h4, h5 {
2 | font-family: cursive !important;
3 | }
4 |
5 | .day p.text,
6 | .day p.notes {
7 | text-align: center;
8 | }
9 |
10 |
11 | div.day,
12 | h3.week-head {
13 | margin-top: 0 !important;
14 | }
15 |
16 | .App-header,
17 | div.options,
18 | span.weekday-head-date,
19 | nav {
20 | display: none !important;
21 | }
22 |
23 | .day {
24 | padding-top: 30px !important;
25 | border-top: none !important;
26 | }
27 |
28 | .page-container {
29 | margin-top: 0 !important;
30 | }
31 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Dinnerd",
3 | "name": "Dinnerd",
4 | "icons": [
5 | {
6 | "src": "/static/favicons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/static/favicons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "start_url": "./",
19 | "display": "standalone"
20 | }
21 |
--------------------------------------------------------------------------------
/src/Schema.js:
--------------------------------------------------------------------------------
1 | import lf from 'lovefield'
2 |
3 | const getSchema = () => {
4 | let schema = lf.schema.create('dinnerd', 1)
5 |
6 | schema.createTable('Days')
7 | .addColumn('date', lf.Type.STRING)
8 | .addColumn('datetime', lf.Type.DATE_TIME)
9 | .addColumn('text', lf.Type.STRING)
10 | .addColumn('notes', lf.Type.STRING)
11 | .addColumn('starred', lf.Type.BOOLEAN)
12 | // .addColumn('modified', lf.Type.DATE_TIME)
13 | // .addColumn('userCreated', lf.Type.STRING)
14 | // .addColumn('userModified', lf.Type.STRING)
15 | .addPrimaryKey(['date'])
16 |
17 | return schema
18 | }
19 |
20 | export default getSchema
21 |
--------------------------------------------------------------------------------
/bin/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eo pipefail
3 |
4 | export REACT_APP_DEV=false
5 |
6 | export REACT_APP_FIREBASE_LOGGING=false
7 |
8 | # export REACT_APP_FIREBASE_API_KEY="AIzaSyAG0xEHwP1TrGSe6Jw0JmVWkSyhEDp-svw"
9 | # export REACT_APP_FIREBASE_AUTH_DOMAIN="dinnerd-dev.firebaseapp.com"
10 | # export REACT_APP_FIREBASE_DATABASE_URL="https://dinnerd-dev.firebaseio.com"
11 | # export REACT_APP_FIREBASE_STORAGE_BUCKET="dinnerd-dev.appspot.com"
12 | # export REACT_APP_FIREBASE_MESSAGING_SENDER_ID="378499526474"
13 |
14 | export REACT_APP_FIREBASE_API_KEY="AIzaSyAOs63MOvdKYRg7wJ6pTFxO6v96SrYHhhs"
15 | export REACT_APP_FIREBASE_AUTH_DOMAIN="dinnerd-45b97.firebaseapp.com"
16 | export REACT_APP_FIREBASE_DATABASE_URL="https://dinnerd-45b97.firebaseio.com"
17 | export REACT_APP_FIREBASE_STORAGE_BUCKET="dinnerd-45b97.appspot.com"
18 | export REACT_APP_FIREBASE_MESSAGING_SENDER_ID="501356720142"
19 |
20 | yarn run build
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dinnerd",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "appcache-manifest": "^2.1.0",
7 | "firebase-admin": "^5.0.0",
8 | "react-addons-perf": "^15.4.2",
9 | "react-scripts": "^1.0.10",
10 | "sw-precache": "^5.2.0",
11 | "why-did-you-update": "^0.0.8"
12 | },
13 | "dependencies": {
14 | "date-fns": "^2.0.0-alpha.1",
15 | "elasticlunr": "^0.9.5",
16 | "firebase": "^4.1.3",
17 | "mobx": "^3.1.16",
18 | "mobx-react": "^4.2.2",
19 | "react": "^15.6.1",
20 | "react-addons-css-transition-group": "^15.6.0",
21 | "react-dom": "^15.6.1",
22 | "react-highlight-words": "^0.8.0",
23 | "react-linkify": "^0.2.1",
24 | "recompose": "^0.23.5",
25 | "zenscroll": "^4.0.0"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build && sw-precache --config=sw-precache-config.js",
30 | "test": "react-scripts test --env=jsdom",
31 | "eject": "react-scripts eject"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database-rules.json"
4 | },
5 | "storage": {
6 | "rules": "storage.rules"
7 | },
8 | "hosting": {
9 | "public": "./build",
10 | "ignore": [
11 | "firebase.json",
12 | "database-rules.json",
13 | "storage.rules"
14 | ],
15 | "headers": [
16 | {
17 | "source": "/index.appcache",
18 | "headers": [
19 | {
20 | "key": "Cache-Control",
21 | "value": "max-age=0"
22 | },
23 | {
24 | "key": "Content-Type",
25 | "value": "text/cache-manifest"
26 | }
27 | ]
28 | },
29 | {
30 | "source": "/",
31 | "headers": [
32 | {
33 | "key": "Cache-Control",
34 | "value": "max-age=864000"
35 | }
36 | ]
37 | },
38 | {
39 | "source": "static/*.@(jpg|jpeg|gif|png|svg|css)",
40 | "headers": [
41 | {
42 | "key": "Cache-Control",
43 | "value": "max-age=8640000"
44 | }
45 | ]
46 | },
47 | {
48 | "source": "static/favicons/*.*",
49 | "headers": [
50 | {
51 | "key": "Cache-Control",
52 | "value": "max-age=8640000"
53 | }
54 | ]
55 | },
56 | {
57 | "source": "static/**/main*.@(css|js)",
58 | "headers": [
59 | {
60 | "key": "Cache-Control",
61 | "value": "max-age=31536000"
62 | }
63 | ]
64 | }
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Settings.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 |
4 | import store from './Store'
5 |
6 |
7 | const Settings = observer(class Settings extends Component {
8 |
9 | render() {
10 | return (
11 |
12 |
Settings
13 |
28 |
29 |
43 |
44 |
45 |
50 | Close Settings
51 |
52 |
53 | )
54 | }
55 | })
56 |
57 | export default Settings
58 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | TODO
2 | ====
3 |
4 | * Consider using mobx-state-tree
5 | https://codesandbox.io/s/nZ26kGMD
6 |
7 | * Import addtohomescreen as a module
8 | https://github.com/cubiq/add-to-homescreen/issues/257
9 |
10 | * Is there a way to import much less of recompose since we only need
11 | pure and onlyUpdateForKeys
12 |
13 | * Convert URLs to titles using
14 | http://tools.buzzstream.com/meta-tag-extractor
15 | or http://embed.ly/docs/explore/extract?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FPage_Visibility_API
16 |
17 |
18 | * Try https://github.com/BafS/Gutenberg
19 | for printing.
20 |
21 | * Ability to upload and store pictures.
22 | http://stackoverflow.com/a/31501426/205832
23 |
24 | * Consider using runInAction() when updating the store with multiple things.
25 |
26 | * Figure how a way
27 | 1) encourage saving to Home screen on first run
28 | 2) if someone uses IndexedDB in safari and THEN later decide to add
29 | to Home screen, all data is lost.
30 |
31 | * Ideas for how to style print.css
32 | https://www.tinyprints.com/shop/custom-menus.htm
33 |
34 | * Consider js-search since it supports incrementally adding documents
35 | apparently
36 | https://github.com/bvaughn/js-search/issues/20#issuecomment-277922936
37 |
38 | DONE
39 | ====
40 |
41 | * Upload burger logo on http://realfavicongenerator.net/
42 |
43 | * https://zengabor.github.io/zenscroll/ seems to work on iOS safari
44 |
45 | * Experiment with:
46 | appcache-manifest "build/index.{html,css,js}" "build/**/*.{css,js}"
47 | "build/burger.png" --network-star -o build/index.appcache
48 |
49 | * Install that react-perf addon and see if some components really do
50 | over render.
51 |
52 | * Experiment with https://www.npmjs.com/package/autobind-decorator
53 | to see if we can avoid having to type any
54 | `this.someMethod=this.someMethod.bind(this)`
55 |
56 | * Avoid DOM query selectors and use refs
57 | https://facebook.github.io/react/docs/refs-and-the-dom.html
58 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .navbar-brand .week-head-dates {
2 | font-size: 70%;
3 | opacity: 0.7;
4 | }
5 |
6 | .options.top {
7 | margin-top: 40px;
8 | }
9 |
10 | .options.bottom {
11 | margin-top: 40px;
12 | margin-bottom: 100px;
13 | }
14 |
15 | .day .actions .action {
16 | padding-top: 10px;
17 | }
18 |
19 | .action.buttons {
20 | text-align: right;
21 | }
22 |
23 | .day .week-head {
24 | text-align: center;
25 | margin-top: 40px;
26 | padding-top: 10px;
27 | margin-bottom: 20px;
28 | }
29 |
30 | .day {
31 | margin-top: 20px;
32 | padding-top: 10px;
33 | border-top: 1px solid #efefef;
34 | }
35 |
36 | /*.weekday-head {
37 | margin-bottom: 0.3rem;
38 | }*/
39 |
40 | .day .weekday-head-date {
41 | font-size: 70%;
42 | opacity: 0.7;
43 | }
44 | .day textarea {
45 | font-size: 1em;
46 | }
47 |
48 | .display-day p {
49 | margin: 3px;
50 | line-height: 1.3;
51 | }
52 |
53 | .search-results .search-result p.last-used {
54 | margin: 3px;
55 | font-size: 0.8em;
56 | opacity: 0.7;
57 |
58 | }
59 |
60 | .display-day p.text,
61 | textarea.text {
62 | font-weight: bold;
63 | }
64 |
65 |
66 | .search-results .search-result {
67 | margin: 20px 0;
68 | }
69 |
70 | .fadein-enter {
71 | opacity: 0.01;
72 | }
73 | .fadein-enter.fadein-enter-active {
74 | opacity: 1;
75 | transition: opacity 500ms ease-in;
76 | }
77 | .fadein-leave {
78 | opacity: 1;
79 | }
80 | .fadein-leave.fadein-leave-active {
81 | opacity: 0.01;
82 | transition: opacity 300ms ease-in;
83 | }
84 |
85 |
86 | .fadein-appear {
87 | opacity: 0.01;
88 | }
89 | .fadein-appear.fadein-appear-active {
90 | opacity: 1;
91 | transition: opacity .5s ease-in;
92 | }
93 |
94 | .btn.close-button {
95 | margin-top: 50px;
96 | margin-bottom: 50px;
97 | }
98 |
99 |
100 | .offline-indicator {
101 | margin-left: 20px;
102 | text-transform: uppercase;
103 | font-size: 0.5em;
104 | }
105 | .offline-indicator .online {
106 | color: #8BAB57;
107 | }
108 | .offline-indicator .offline {
109 | color: #CF441F;
110 | }
111 |
112 | .navbar-light .navbar-brand {
113 | font-size: 1.25em;
114 | }
115 | @media (max-width:320px) {
116 | .navbar-light .navbar-brand {
117 | font-size: 1.05em;
118 | }
119 | .offline-indicator {
120 | margin-left: 5px;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/Common.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { pure } from 'recompose'
3 | import { format } from 'date-fns/esm'
4 |
5 | const dateFns = {
6 | format: format,
7 | }
8 |
9 | export const makeWeekId = (datetime) => {
10 | return 'week-head-' + dateFns.format(datetime, 'YYYYW')
11 | }
12 |
13 | export const makeDayId = (datetime) => {
14 | return 'day-' + dateFns.format(datetime, 'YYYYMMDD')
15 | }
16 |
17 | export const ShowWeekHeaderDates = pure(
18 | ({ start, end }) => {
19 | return (
20 |
21 | { dateFns.format(start, 'D MMM') }
22 | {' '}
23 | ...
24 | {' '}
25 | { dateFns.format(end, 'D MMM') }
26 |
27 | )
28 | })
29 |
30 | export const Heart = pure(
31 | ({ filled, bubble, size = 18 }) => {
32 | return (
33 |
34 |
35 |
38 |
39 |
40 | )
41 | })
42 |
43 |
44 | export function debounce(callback, wait, context = this) {
45 | let timeout = null
46 | let callbackArgs = null
47 |
48 | const later = () => callback.apply(context, callbackArgs)
49 |
50 | return function() {
51 | callbackArgs = arguments
52 | clearTimeout(timeout)
53 | timeout = setTimeout(later, wait)
54 | }
55 | }
56 |
57 |
58 | export const ShowFirebaseError = ({ heading, error }) => {
59 | if (!error) {
60 | return null
61 | }
62 | return (
63 |
64 |
{ heading }
65 |
66 |
67 | { error.code }
68 |
69 |
70 |
71 | { error.message }
72 |
73 |
74 | )
75 | }
76 |
77 |
78 | const pagifyRegex = /\b(page |p\.?|p\.? )(\d+)$/
79 |
80 | export const pagifyScrubText = (text) => {
81 | // Any string that ends with...
82 | // page 123
83 | // p. 123
84 | // p123
85 | // p.123
86 | // gets the number part replaced with a ? character.
87 | return text.replace(pagifyRegex, 'page ?')
88 | }
89 |
90 | export const pagifyPromptText = (text) => {
91 | if (pagifyRegex.test(text)) {
92 | const defaultPage = text.match(pagifyRegex)[2]
93 | const page = prompt('What page this time?', defaultPage)
94 | if (page) {
95 | text = text.replace(pagifyRegex, `page ${page}`)
96 | }
97 | }
98 | return text
99 | }
100 |
--------------------------------------------------------------------------------
/src/Store.js:
--------------------------------------------------------------------------------
1 | import { action, extendObservable, ObservableMap } from 'mobx'
2 | // import { autorun } from 'mobx'
3 | import { isWithinInterval } from 'date-fns/esm'
4 |
5 | const dateFns = {
6 | isWithinInterval: isWithinInterval,
7 | }
8 |
9 | class Day {
10 | constructor(date, datetime, text = '', notes = '', starred = false) {
11 | this.date = date
12 | this.datetime = datetime
13 | extendObservable(this, {
14 | text: text,
15 | notes: notes,
16 | starred: starred,
17 | })
18 | }
19 | }
20 |
21 | class Store {
22 | constructor() {
23 | extendObservable(this, {
24 | noFirebase: false,
25 | offline: null, // null == "Don't really know"
26 | // wasOffline: false,
27 | currentUser: null,
28 | days: new ObservableMap(),
29 | copied: null,
30 | firstDateThisWeek: null,
31 | currentGroup: null,
32 | dateRangeStart: null,
33 | dateRangeEnd: null,
34 | extendDateRange: action((firstDate, lastDate) => {
35 | if (this.dateRangeStart) {
36 | if (firstDate < this.dateRangeStart) {
37 | this.dateRangeStart = firstDate
38 | }
39 | if (lastDate > this.dateRangeEnd) {
40 | this.dateRangeEnd = lastDate
41 | }
42 | } else {
43 | this.dateRangeStart = firstDate
44 | this.dateRangeEnd = lastDate
45 | }
46 | }),
47 | get dateRangeLength() {
48 | if (this.dateRangeStart) {
49 | // If dateRangeStart and dateRangeEnd spans two different
50 | // day-light savings events, this might not add up to a multiple
51 | // of 7 as expected.
52 | // That's why it needs to be rounded to the nearest integer.
53 | return Math.round((this.dateRangeEnd - this.dateRangeStart) / (1000 * 24 * 3600))
54 | } else {
55 | return 7
56 | }
57 | },
58 | get filteredDays() {
59 | return this.days.values().filter(day => {
60 | if (this.dateRangeStart) {
61 | const withinRange = dateFns.isWithinInterval(
62 | day.datetime,
63 | {start: this.dateRangeStart, end:this.dateRangeEnd}
64 | )
65 | if (withinRange) {
66 | return true
67 | } else {
68 | }
69 | } else {
70 | return true
71 | }
72 | return false
73 | }).sort((a, b) => a.datetime - b.datetime)
74 | },
75 | addDay: action((date, datetime, text = '', notes = '', starred = false) => {
76 | this.days.set(date, new Day(date, datetime, text, notes, starred))
77 | }),
78 | recentFavorites: null,
79 | settings: JSON.parse(localStorage.getItem('settings') || '{}'),
80 | setSetting: action((key, value) => {
81 | this.settings[key] = value
82 | localStorage.setItem('settings', JSON.stringify(this.settings))
83 | }),
84 | })
85 | }
86 | }
87 |
88 | const store = window.store = new Store()
89 |
90 | export default store
91 |
92 | // autorun(() => {
93 | // // console.log('Store changed DAYS:', store.days, store.settings);
94 | // console.log('Store changed DAYS:', store.days.length)
95 | // })
96 |
--------------------------------------------------------------------------------
/src/Favorites.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 | import {
4 | format,
5 | formatDistanceStrict,
6 | } from 'date-fns/esm'
7 |
8 | import { DisplayDay } from './Day'
9 | import store from './Store'
10 |
11 | const dateFns = {
12 | format: format,
13 | formatDistanceStrict: formatDistanceStrict,
14 | }
15 |
16 |
17 | const Favorites = observer(class Favorites extends Component {
18 |
19 | render() {
20 |
21 | // console.log('Rendering Favorites, store.recentFavorites:', store.recentFavorites);
22 |
23 | return (
24 |
25 |
26 | Favorites
27 | {' '}
28 | (most recent first)
29 |
30 | { !store.recentFavorites ? Loading... : null }
31 |
32 | {
33 | store.recentFavorites && !store.recentFavorites.length ?
34 | No favorites saved. Heart more!
35 | : null
36 | }
37 | {
38 | store.recentFavorites && store.recentFavorites.length ?
39 |
43 | : null
44 | }
45 |
46 |
51 | Close Favorites
52 |
53 |
54 | )
55 | }
56 | })
57 |
58 | export default Favorites
59 |
60 |
61 | // Consider importing this from Search instead (or Common)
62 | const ShowSearchResults = ({ results, onClosePage }) => {
63 | return (
64 |
65 | {
66 | results.map(result => {
67 | return (
68 |
70 |
71 |
76 |
77 | Last used { dateFns.format(result.datetime, 'D MMM') },
78 | {' '}
79 | { dateFns.formatDistanceStrict(
80 | new Date(), result.datetime, {addSuffix: true}
81 | ) }
82 |
83 |
84 |
86 | {
90 | store.copied = {
91 | date: result.date,
92 | text: result.text,
93 | notes: result.notes,
94 | starred: result.starred,
95 | }
96 | onClosePage()
97 | }}>
98 | Copy
99 |
100 |
101 |
102 | )
103 | })
104 | }
105 |
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/public/static/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/Days.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 | import zenscroll from 'zenscroll'
4 | import {
5 | startOfWeek,
6 | subDays,
7 | isEqual,
8 | } from 'date-fns/esm'
9 |
10 | import store from './Store'
11 | import { makeDayId } from './Common'
12 | import Day from './Day'
13 |
14 | const dateFns = {
15 | startOfWeek: startOfWeek,
16 | subDays: subDays,
17 | isEqual: isEqual,
18 | }
19 |
20 | const Days = observer(class Days extends Component {
21 |
22 | constructor(props) {
23 | super(props)
24 | this.state = {
25 | loadingPreviousWeek: false,
26 | loadingNextWeek: false,
27 | }
28 | }
29 |
30 | loadPreviousWeek = (event) => {
31 | const firstDatetime = store.dateRangeStart
32 | const firstDatePreviousWeek = dateFns.subDays(firstDatetime, 7)
33 | this.props.loadWeek(firstDatePreviousWeek)
34 | this.setState({loadingPreviousWeek: false}, () => {
35 | const id = makeDayId(firstDatePreviousWeek)
36 | const element = document.querySelector('#' + id)
37 | if (element) {
38 | zenscroll.to(element)
39 | }
40 | })
41 | }
42 |
43 | loadNextWeek = (event) => {
44 | const firstDateNextWeek = store.dateRangeEnd
45 | this.props.loadWeek(firstDateNextWeek)
46 | this.setState({loadingNextWeek: false}, () => {
47 | const id = makeDayId(firstDateNextWeek)
48 | const element = document.querySelector('#' + id)
49 | if (element) {
50 | zenscroll.to(element)
51 | }
52 | })
53 | }
54 |
55 | render() {
56 | const weekStartsOn = store.settings.weekStartsOnAMonday ? 1 : 0
57 | return (
58 |
59 | {/*
60 | By default currentUser===null, but if firebase
61 | has successfully run onAuthStateChanged at least once
62 | then it gets set to 'false'.
63 | Only then show the unauthorized warning.
64 | */}
65 | {
66 | store.currentUser === false && !store.offline ?
67 |
70 | : null
71 | }
72 |
73 | {
74 | store.days.size ?
75 |
76 | {
80 | // this.setState({loadingPreviousWeek: true})
81 | // setTimeout(() => {
82 | // this.setState({loadingPreviousWeek: false})
83 | // }, 1000)
84 | // }}
85 | onClick={this.loadPreviousWeek.bind(this)}>
86 | Previous week
87 |
88 |
89 | : null
90 | }
91 |
92 | { !store.days.size ?
Loading... : null }
93 |
94 | {
95 | store.filteredDays.map(day => {
96 | let firstDateThisWeek = dateFns.isEqual(
97 | day.datetime,
98 | dateFns.startOfWeek(day.datetime, {weekStartsOn: weekStartsOn})
99 | )
100 | return
107 | })
108 | }
109 | {
110 | store.days.size ?
111 |
112 | {
116 | // this.setState({loadingNextWeek: true})
117 | // setTimeout(() => {
118 | // this.setState({loadingNextWeek: false})
119 | // }, 1000)
120 | // }}
121 | onClick={this.loadNextWeek.bind(this)}>
122 | { this.state.loadingNextWeek ? 'Loading...' : 'Next week' }
123 |
124 |
125 | : null
126 | }
127 |
128 | )
129 | }
130 | })
131 |
132 |
133 | export default Days
134 |
135 |
136 | const ShowUnauthorizedWarning = ({ gotoSignInPage }) => {
137 | return (
138 |
139 |
Not Signed In
140 |
141 | It's OK but nothing you enter will be backed up.
142 |
143 |
144 | {
148 | gotoSignInPage()
149 | }}
150 | >
151 | Sign In
152 |
153 |
154 |
155 | )
156 | }
157 |
--------------------------------------------------------------------------------
/src/Search.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 | import {
4 | format,
5 | formatDistanceStrict,
6 | } from 'date-fns/esm'
7 |
8 | import { DisplayDay } from './Day'
9 | import { debounce } from './Common'
10 | import store from './Store'
11 |
12 | const dateFns = {
13 | format: format,
14 | formatDistanceStrict: formatDistanceStrict,
15 |
16 | }
17 |
18 | const Search = observer(class Search extends Component {
19 |
20 | constructor() {
21 | super()
22 | this.state = {
23 | search: '',
24 | searching: false,
25 | searchResults: null,
26 | }
27 | this.autoCompleteSearch = debounce(this.autoCompleteSearch.bind(this), 300)
28 | }
29 |
30 | componentDidMount = () => {
31 | this.refs.search.focus()
32 | }
33 |
34 | autoCompleteSearch = () => {
35 | if (!this.state.search.trim().length) {
36 | this.setState({
37 | searchResults: [],
38 | searching: false,
39 | })
40 | return
41 | }
42 | this.setState({searching: true})
43 | const results = this.props.searcher(this.state.search, {
44 | fields: {
45 | text: {boost: 3},
46 | notes: {boost: 1},
47 | },
48 | bool: 'OR',
49 | expand: true,
50 | })
51 | let hashes = new Set()
52 | let searchResults = []
53 | results.forEach(result => {
54 | let hash = result.text + result.notes
55 | hash = hash.toLowerCase()
56 | if (!hashes.has(hash) && searchResults.length < 50) {
57 | searchResults.push(result)
58 | hashes.add(hash)
59 | }
60 | })
61 | this.setState({
62 | searchResults: searchResults,
63 | searching: false,
64 | })
65 | }
66 |
67 | render() {
68 |
69 | return (
70 |
71 |
Search
72 |
93 |
94 | { this.state.searching ?
Searching... : null }
95 |
96 | {
97 | !this.state.searching && this.state.searchResults && !this.state.searchResults.length ?
98 |
Nothing found.
99 | : null
100 | }
101 |
102 | {
103 | !this.state.searching && this.state.searchResults && this.state.searchResults.length ?
104 |
108 | : null
109 | }
110 |
111 |
116 | Close Search
117 |
118 |
119 | )
120 | }
121 | })
122 |
123 | export default Search
124 |
125 |
126 | const ShowSearchResults = ({ results, onClosePage }) => {
127 | return (
128 |
129 | {
130 | results.map(result => {
131 | return (
132 |
134 |
135 |
141 |
142 | Last used { dateFns.format(result.datetime, 'D MMM') },
143 | {' '}
144 | { dateFns.formatDistanceStrict(
145 | new Date(), result.datetime, {addSuffix: true}
146 | ) }
147 |
148 |
149 |
151 | {
155 | store.copied = {
156 | date: result.date,
157 | text: result.text,
158 | notes: result.notes,
159 | starred: result.starred,
160 | }
161 | onClosePage()
162 | }}>
163 | Copy
164 |
165 |
166 |
167 | )
168 | })
169 | }
170 |
171 | )
172 | }
173 |
--------------------------------------------------------------------------------
/src/User.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 |
4 | import store from './Store'
5 | import { ShowFirebaseError } from './Common'
6 |
7 | const User = observer(class User extends Component {
8 |
9 | constructor(props) {
10 | super(props)
11 | this.state = {changePassword: false}
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
User
18 |
19 |
20 | {/* Name */}
21 | {/* { store.currentUser.displayName } */}
22 | Email
23 | { store.currentUser.email }
24 | Email Verified?
25 | { store.currentUser.emailVerified ? 'Yes' : 'Not yet' }
26 |
27 |
28 | {
29 | this.state.changePassword ?
30 |
{
33 | this.setState({changePassword: false})
34 | }}
35 | />
36 | : null
37 | }
38 |
39 |
44 | Sign out
45 |
46 | {
50 | this.setState({changePassword: !this.state.changePassword})
51 | }}
52 | >
53 | Change your password
54 |
55 | {
59 | store.currentUser.getToken().then(t => {
60 | console.warn('Access Token', t)
61 | prompt(t)
62 | })
63 | }}
64 | >
65 | Get your Access Token
66 |
67 |
68 |
73 | Close
74 |
75 |
76 | )
77 | }
78 | })
79 |
80 | export default User
81 |
82 |
83 | class ChangePassword extends Component {
84 |
85 | constructor(props) {
86 | super(props)
87 | this.state = {misMatched: false}
88 | }
89 |
90 | render() {
91 | return (
92 |
162 | )
163 | }
164 |
165 | }
166 |
--------------------------------------------------------------------------------
/src/SignIn.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 |
4 | import { ShowFirebaseError } from './Common'
5 |
6 | const SignIn = observer(class SignIn extends Component {
7 |
8 | constructor(props) {
9 | super(props)
10 | this.state = {
11 | resetPassword: false,
12 | }
13 | }
14 |
15 | render() {
16 |
17 | return (
18 |
19 |
Sign In
20 |
21 | {
22 | this.state.resetPassword ?
23 | {
25 | this.setState({resetPassword: false})
26 | }}
27 | auth={this.props.auth}
28 | /> :
29 |
34 | }
35 |
36 | {
37 | !this.state.resetPassword ?
38 | {
43 | this.setState({resetPassword: true})
44 | }}
45 | >
46 | Forgot your password?
47 |
48 | : null
49 | }
50 |
51 |
57 | Close
58 |
59 |
60 | )
61 | }
62 | })
63 |
64 | export default SignIn
65 |
66 |
67 | class Login extends Component {
68 |
69 | constructor(props) {
70 | super(props)
71 | this.state = {
72 | error: null,
73 | }
74 | }
75 |
76 | render() {
77 | return (
78 |
139 |
140 |
141 | )
142 | }
143 | }
144 |
145 |
146 | class ResetPassword extends Component {
147 |
148 | constructor(props) {
149 | super(props)
150 | this.state = {
151 | error: null,
152 | }
153 | }
154 |
155 | render() {
156 | return (
157 |
194 | )
195 | }
196 |
197 | }
198 |
--------------------------------------------------------------------------------
/public/static/burger.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 | import { pure } from 'recompose'
4 | import { addDays } from 'date-fns/esm'
5 |
6 | import store from './Store'
7 | import { ShowWeekHeaderDates, Heart } from './Common'
8 |
9 | // Because I love namespaces
10 | const dateFns = {addDays: addDays}
11 |
12 |
13 | const Nav = observer(class Nav extends Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | collapsed: true,
18 | collapsing: false,
19 | }
20 | }
21 | render() {
22 | let burgerClassname = 'navbar-toggler navbar-toggler-right'
23 | let navlinksClassname = 'navbar-collapse'
24 | if (this.state.collapsing) {
25 | navlinksClassname += ' collapsing'
26 | } else if (this.state.collapsed) {
27 | navlinksClassname += ' collapse '
28 | } else {
29 | navlinksClassname += ' collapse show'
30 | }
31 |
32 | return (
33 |
34 | {
43 | this.setState({collapsing: true})
44 | window.setTimeout(() => {
45 | this.setState({collapsing: false, collapsed: !this.state.collapsed})
46 | }, 200)
47 | }}>
48 |
51 |
52 | {
56 | e.preventDefault()
57 | this.props.onGotoWeek()
58 | }}>
59 | Dinnerd
60 | {' '}
61 | {
62 | store.firstDateThisWeek ?
63 | :
67 | null
68 | }
69 | {' '}
70 |
71 |
72 |
205 |
206 | )
207 | }
208 | })
209 |
210 | export default Nav
211 |
212 |
213 | export const ShowOfflineIndicator = pure(
214 | ({ offline }) => {
215 | let style = {
216 | backgroundImage: `url(${process.env.PUBLIC_URL}/static/offline-sprite.png)`,
217 | backgroundRepeat: 'no-repeat',
218 | display: 'inline-block',
219 | width: 16,
220 | height: 16,
221 | }
222 | if (offline) {
223 | style.backgroundPosition = '-16px 0'
224 | } else {
225 | style.backgroundPosition = '0 0'
226 | }
227 | return (
228 |
229 | {
230 | offline ?
231 |
235 |
236 | Offline
237 |
238 | :
239 |
243 |
244 | Online
245 |
246 | }
247 |
248 | )
249 | })
250 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Dinnerd
24 |
25 |
26 |
27 |
28 |
29 |
30 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Loading...
47 |
48 |
49 |
50 | Taking forever?
51 | Try Reloading
54 |
55 |
56 |
57 |
58 |
59 |
75 |
76 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/public/static/addtohomescreen.css:
--------------------------------------------------------------------------------
1 | .ath-viewport * {
2 | -webkit-box-sizing: border-box;
3 | -moz-box-sizing: border-box;
4 | box-sizing: border-box;
5 | }
6 |
7 | .ath-viewport {
8 | position: relative;
9 | z-index: 2147483641;
10 | pointer-events: none;
11 |
12 | -webkit-tap-highlight-color: rgba(0,0,0,0);
13 | -webkit-touch-callout: none;
14 | -webkit-user-select: none;
15 | -moz-user-select: none;
16 | -ms-user-select: none;
17 | user-select: none;
18 | -webkit-text-size-adjust: none;
19 | -moz-text-size-adjust: none;
20 | -ms-text-size-adjust: none;
21 | -o-text-size-adjust: none;
22 | text-size-adjust: none;
23 | }
24 |
25 | .ath-modal {
26 | pointer-events: auto !important;
27 | background: rgba(0,0,0,0.6);
28 | }
29 |
30 | .ath-mandatory {
31 | background: #000;
32 | }
33 |
34 | .ath-container {
35 | pointer-events: auto !important;
36 | position: absolute;
37 | z-index: 2147483641;
38 | padding: 0.7em 0.6em;
39 | width: 18em;
40 |
41 | background: #eee;
42 | background-size: 100% auto;
43 |
44 | box-shadow: 0 0.2em 0 #d1d1d1;
45 |
46 | font-family: sans-serif;
47 | font-size: 15px;
48 | line-height: 1.5em;
49 | text-align: center;
50 | }
51 |
52 | .ath-container small {
53 | font-size: 0.8em;
54 | line-height: 1.3em;
55 | display: block;
56 | margin-top: 0.5em;
57 | }
58 |
59 | .ath-ios.ath-phone {
60 | bottom: 1.8em;
61 | left: 50%;
62 | margin-left: -9em;
63 | }
64 |
65 | .ath-ios6.ath-tablet {
66 | left: 5em;
67 | top: 1.8em;
68 | }
69 |
70 | .ath-ios7.ath-tablet {
71 | left: 0.7em;
72 | top: 1.8em;
73 | }
74 |
75 | .ath-ios8.ath-tablet {
76 | right: 0.4em;
77 | top: 1.8em;
78 | }
79 |
80 | .ath-android {
81 | bottom: 1.8em;
82 | left: 50%;
83 | margin-left: -9em;
84 | }
85 |
86 | /* close icon */
87 | .ath-container:before {
88 | content: '';
89 | position: relative;
90 | display: block;
91 | float: right;
92 | margin: -0.7em -0.6em 0 0.5em;
93 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAACECAMAAABmmnOVAAAAdVBMVEUAAAA5OTkzMzM7Ozs3NzdBQUFAQEA/Pz8+Pj5BQUFAQEA/Pz8+Pj5BQUFAQEA/Pz9BQUE+Pj4/Pz8/Pz8+Pj4/Pz8/Pz8/Pz8+Pj4/Pz8+Pj4/Pz8/Pz8/Pz8/Pz8/Pz8+Pj4/Pz8/Pz8/Pz8/Pz9AQEA/Pz+fdCaPAAAAJnRSTlMACQoNDjM4OTo7PEFCQ0RFS6ytsbS1tru8vcTFxu7x8vX19vf4+C5yomAAAAJESURBVHgBvdzLTsJAGEfxr4C2KBcVkQsIDsK8/yPaqIsPzVlyzrKrX/5p0kkXEz81L23otc9NpIbbWia2YVLqdnhlqFlhGWpSDHe1aopsSIpRb8gK0dC3G30b9rVmhWZIimTICsvQtx/FsuYOrWHoDjX3Gu31gzJxdki934WrAIOsAIOsAIOiAMPhPsJTgKGN0BVsYIVsYIVpYIVpYIVpYIVpYIVpYIVpYIVpYIVlAIVgEBRs8BRs8BRs8BRs8BRs8BRs8BRs8BRTNmgKNngKNngKNngKNngKNhiKGxgiOlZoBlaYBlaYBlaYBlaYBlaYBlaYBlaYBlZIBlBMfQMrVAMr2KAqBENSHFHhGEABhi5CV6gGUKgGUKgGUKgGUFwuqgEUvoEVsoEVpoEUpgEUggF+gKTKY+h1fxSlC7/Z+RrxOQ3fcEoAPPHZBlaYBlaYBlaYBlZYBlYIhvLBCstw7PgM7hkiWOEZWGEaWGEaWGEaIsakEAysmHkGVpxmvoEVqoEVpoEVpoEVpoEVpoEVpoEVkoEVgkFQsEFSsEFQsGEcoSvY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnY4CnmbNAUT2c2WAo2eAo2eAo2eAo2eAo2eArNEPFACjZ4CjZ4CjZ4CjaIird/rBvFH6llNCvewdli1URWCIakSIZesUaDoFg36dKFWk9zCZDei3TtwmCj7pC22AwikiIZPEU29IpFNliKxa/hC9DFITjQPYhcAAAAAElFTkSuQmCC);
94 | background-color: rgba(255,255,255,0.8);
95 | background-size: 50%;
96 | background-repeat: no-repeat;
97 | background-position: 50%;
98 | width: 2.7em;
99 | height: 2.7em;
100 | text-align: center;
101 | overflow: hidden;
102 | color: #a33;
103 | z-index: 2147483642;
104 | }
105 |
106 | .ath-container.ath-icon:before {
107 | position: absolute;
108 | top: 0;
109 | right: 0;
110 | margin: 0;
111 | float: none;
112 | }
113 |
114 | .ath-mandatory .ath-container:before {
115 | display: none;
116 | }
117 |
118 | .ath-container.ath-android:before {
119 | float: left;
120 | margin: -0.7em 0.5em 0 -0.6em;
121 | }
122 |
123 | .ath-container.ath-android.ath-icon:before {
124 | position: absolute;
125 | right: auto;
126 | left: 0;
127 | margin: 0;
128 | float: none;
129 | }
130 |
131 |
132 | /* applied only if the application icon is shown */
133 | .ath-container.ath-icon {
134 |
135 | }
136 |
137 | .ath-action-icon {
138 | display: inline-block;
139 | vertical-align: middle;
140 | background-position: 50%;
141 | background-repeat: no-repeat;
142 | text-indent: -9999em;
143 | overflow: hidden;
144 | }
145 |
146 | .ath-ios7 .ath-action-icon,
147 | .ath-ios8 .ath-action-icon {
148 | width: 1.6em;
149 | height: 1.6em;
150 | background-image:url(data:image/png;base64,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);
151 | margin-top: -0.3em;
152 | background-size: auto 100%;
153 | }
154 |
155 | .ath-ios6 .ath-action-icon {
156 | width: 1.8em;
157 | height: 1.8em;
158 | background-image:url(data:image/png;base64,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);
159 | margin-bottom: 0.4em;
160 | background-size: 100% auto;
161 | }
162 |
163 | .ath-android .ath-action-icon {
164 | width: 1.4em;
165 | height: 1.5em;
166 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAANlBMVEVmZmb///9mZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZW6fJrAAAAEXRSTlMAAAYHG21ub8fLz9DR8/T4+RrZ9owAAAB3SURBVHja7dNLDoAgDATQWv4gKve/rEajJOJiWLgg6WzpSyB0aHqHiNj6nL1lovb4C+hYzkSNAT7mryQFAVOeGAj4CjwEtgrWXpD/uZKtwEJApXt+Vn0flzRhgNiFZQkOXY0aADQZCOCPlsZJ46Rx0jhp3IiN2wGDHhxtldrlwQAAAABJRU5ErkJggg==);
167 | background-size: 100% auto;
168 | }
169 |
170 | .ath-container p {
171 | margin: 0;
172 | padding: 0;
173 | position: relative;
174 | z-index: 2147483642;
175 | text-shadow: 0 0.1em 0 #fff;
176 | font-size: 1.1em;
177 | }
178 |
179 | .ath-ios.ath-phone:after {
180 | content: '';
181 | background: #eee;
182 | position: absolute;
183 | width: 2em;
184 | height: 2em;
185 | bottom: -0.9em;
186 | left: 50%;
187 | margin-left: -1em;
188 | -webkit-transform: scaleX(0.9) rotate(45deg);
189 | transform: scaleX(0.9) rotate(45deg);
190 | box-shadow: 0.2em 0.2em 0 #d1d1d1;
191 | }
192 |
193 | .ath-ios.ath-tablet:after {
194 | content: '';
195 | background: #eee;
196 | position: absolute;
197 | width: 2em;
198 | height: 2em;
199 | top: -0.9em;
200 | left: 50%;
201 | margin-left: -1em;
202 | -webkit-transform: scaleX(0.9) rotate(45deg);
203 | transform: scaleX(0.9) rotate(45deg);
204 | z-index: 2147483641;
205 | }
206 |
207 | .ath-application-icon {
208 | position: relative;
209 | padding: 0;
210 | border: 0;
211 | margin: 0 auto 0.2em auto;
212 | height: 6em;
213 | width: 6em;
214 | z-index: 2147483642;
215 | }
216 |
217 | .ath-container.ath-ios .ath-application-icon {
218 | border-radius: 1em;
219 | box-shadow: 0 0.2em 0.4em rgba(0,0,0,0.3),
220 | inset 0 0.07em 0 rgba(255,255,255,0.5);
221 | margin: 0 auto 0.4em auto;
222 | }
223 |
224 | @media only screen and (orientation: landscape) {
225 | .ath-container.ath-phone {
226 | width: 24em;
227 | }
228 |
229 | .ath-android.ath-phone {
230 | margin-left: -12em;
231 | }
232 |
233 | .ath-ios.ath-phone {
234 | margin-left: -12em;
235 | }
236 |
237 | .ath-ios6:after {
238 | left: 39%;
239 | }
240 |
241 | .ath-ios8.ath-phone {
242 | left: auto;
243 | bottom: auto;
244 | right: 0.4em;
245 | top: 1.8em;
246 | }
247 |
248 | .ath-ios8.ath-phone:after {
249 | bottom: auto;
250 | top: -0.9em;
251 | left: 68%;
252 | z-index: 2147483641;
253 | box-shadow: none;
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/Group.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 |
4 | import store from './Store'
5 | import { ShowFirebaseError } from './Common'
6 |
7 | const randomGroupCode = function(noNumbers = 4, noLetters = 2) {
8 | // skipping I and O since they look like 1 and 0
9 | const letters = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
10 | // skipping 1 and 0 since they look like I and O
11 | const numbers = '23456789'
12 | let code = ''
13 | for (var i = 0; i < noLetters; i++) {
14 | code += letters[Math.floor(Math.random() * letters.length)]
15 | }
16 | for (i = 0; i < noNumbers; i++) {
17 | code += numbers[Math.floor(Math.random() * numbers.length)]
18 | }
19 | return code
20 | }
21 |
22 |
23 | const Group = observer(class Group extends Component {
24 |
25 | constructor(props) {
26 | super(props)
27 | this.state = {
28 | changeGroup: false,
29 | otherGroups: null,
30 | codes: null,
31 | }
32 | }
33 |
34 | componentDidMount() {
35 | this.fetchCurrentGroupCodes()
36 | this.fetchOtherGroups()
37 | }
38 |
39 | fetchCurrentGroupCodes = () => {
40 | if (store.currentGroup && store.settings.defaultGroupId) {
41 | const { database } = this.props
42 | database.ref('/group-codes')
43 | .once('value')
44 | .then(snapshot => {
45 | let codes = []
46 | snapshot.forEach(child => {
47 | let childData = child.val()
48 | if (childData.group === store.settings.defaultGroupId) {
49 | codes.push(childData.code)
50 | }
51 | })
52 | if (codes.length) {
53 | this.setState({codes: codes})
54 | }
55 | })
56 | }
57 | }
58 |
59 | fetchOtherGroups = () => {
60 | const { database } = this.props
61 | // if (store.currentGroup && store.settings.defaultGroupId) {
62 | database.ref('/user-groups/' + store.currentUser.uid)
63 | .once('value')
64 | .then(snapshot => {
65 | let otherGroups = []
66 | snapshot.forEach(function(child) {
67 | var childData = child.val()
68 | if (
69 | !store.settings.defaultGroupId ||
70 | childData.group !== store.settings.defaultGroupId
71 | ) {
72 | otherGroups.push({
73 | id: childData.group,
74 | name: childData.name,
75 | membership: childData.membership,
76 | })
77 | }
78 | })
79 | if (otherGroups.length) {
80 | if (!store.currentGroup && otherGroups.length === 1) {
81 | store.currentGroup = otherGroups[0]
82 | store.setSetting('defaultGroupId', otherGroups[0].id)
83 | this.fetchOtherGroups()
84 | } else {
85 | this.setState({otherGroups: otherGroups})
86 | }
87 | }
88 | })
89 | }
90 |
91 | render() {
92 | return (
93 |
94 |
Group
95 |
96 |
97 | Everything you save has to belong to a group.
98 | It can be just you in the group.
99 |
100 |
101 | {
102 | store.currentGroup ?
103 |
You're currently in the { store.currentGroup.name } group.
104 | : null
105 | }
106 |
107 | {
108 | this.state.codes ?
109 |
110 | : null
111 | }
112 |
113 | {
114 | this.state.otherGroups ?
115 | {
117 | store.currentGroup = group
118 | store.setSetting('defaultGroupId', group.id)
119 | this.fetchOtherGroups()
120 | this.fetchCurrentGroupCodes()
121 | this.props.onDefaultGroupChanged()
122 | }}
123 | otherGroups={this.state.otherGroups}/>
124 | : null
125 | }
126 |
127 | {
128 | this.state.changeGroup || !store.currentGroup ?
129 | {
132 | let codes = this.state.codes || []
133 | codes.push(code)
134 | this.setState({codes: codes})
135 | }}
136 | onClose={() => {
137 | this.setState({changeGroup: false})
138 | }}
139 | /> :
140 |
141 | }
142 |
143 | {
144 | this.state.joinOtherGroup ?
145 | {
148 | this.setState({joinOtherGroup: false})
149 | }}
150 | /> :
151 | {
155 | this.setState({joinOtherGroup: true})
156 | }}
157 | >
158 | Join Other Group
159 |
160 | }
161 |
162 |
167 | Close
168 |
169 |
170 | )
171 | }
172 | })
173 |
174 | export default Group
175 |
176 |
177 | class JoinCreateGroup extends Component {
178 |
179 | constructor(props) {
180 | super(props)
181 | this.state = {
182 | createError: null,
183 | joinError: null,
184 | }
185 | }
186 |
187 | render() {
188 |
189 | const { database } = this.props
190 |
191 | return (
192 |
193 |
264 |
265 |
293 |
294 | {/*
{
298 | this.props.onClose()
299 | }}
300 | >
301 | Cancel
302 | */}
303 |
304 | )
305 | }
306 | }
307 |
308 | class JoinOtherGroup extends Component {
309 |
310 | constructor(props) {
311 | super(props)
312 | this.state = {
313 | joinError: null,
314 | notFound: false,
315 | }
316 | }
317 |
318 | render() {
319 |
320 | const { database } = this.props
321 |
322 | return (
323 |
440 | )
441 | }
442 | }
443 |
444 |
445 | const ManageGroupCodes = ({ codes }) => {
446 | return (
447 |
448 |
To invite others into this group, give them one of these codes:
449 |
450 | {
451 | codes.map(code => {
452 | return { code }
453 | })
454 | }
455 |
456 |
457 | )
458 | }
459 |
460 | const ListOtherGroups = ({ otherGroups, joinOtherGroup }) => {
461 | return (
462 |
463 |
Your Groups
464 | {
465 | otherGroups.map(group => {
466 | return (
467 | {
470 | joinOtherGroup(group)
471 | }}
472 | >
473 | { group.name }
474 |
475 | )
476 | })
477 | }
478 |
479 | )
480 | }
481 |
--------------------------------------------------------------------------------
/src/Day.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Highlighter from 'react-highlight-words'
3 | import { observer } from 'mobx-react'
4 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
5 | import zenscroll from 'zenscroll'
6 | import Linkify from 'react-linkify'
7 | import { pure } from 'recompose'
8 | import shallowEqual from 'fbjs/lib/shallowEqual'
9 |
10 | import {
11 | format,
12 | formatDistanceStrict,
13 | isSameDay,
14 | startOfDay,
15 | addDays,
16 | subDays,
17 | } from 'date-fns/esm'
18 |
19 | import {
20 | makeDayId,
21 | makeWeekId,
22 | Heart,
23 | ShowWeekHeaderDates,
24 | debounce,
25 | pagifyPromptText,
26 | } from './Common'
27 | import store from './Store'
28 |
29 | const dateFns = {
30 | format: format,
31 | formatDistanceStrict: formatDistanceStrict,
32 | isSameDay: isSameDay,
33 | startOfDay: startOfDay,
34 | addDays: addDays,
35 | subDays: subDays,
36 | }
37 |
38 |
39 | const Day = observer(class Day extends Component {
40 |
41 | constructor(props) {
42 | super(props)
43 | const { day } = this.props
44 | this.state = {
45 | text: day.text,
46 | notes: day.notes,
47 | starred: day.starred,
48 | searchResults: {},
49 | edit: false,
50 | saving: false,
51 | saved: true,
52 | hadText: false,
53 | }
54 | this.saveChanges = this.saveChanges.bind(this)
55 | this.autoCompleteSearch = debounce(this.autoCompleteSearch.bind(this), 300)
56 | this.inputBlurred = this.inputBlurred.bind(this)
57 | this.inputFocused = this.inputFocused.bind(this)
58 | this.fieldClicked = this.fieldClicked.bind(this)
59 | }
60 |
61 | fieldClicked(field) {
62 | this.startEdit(field)
63 | }
64 |
65 | saveChanges() {
66 | this.props.updateDay(this.props.day, {
67 | text: this.state.text.trim(),
68 | notes: this.state.notes.trim(),
69 | starred: this.state.starred,
70 | }).then(r => {
71 | this.setState({
72 | text: this.state.text.trim(),
73 | notes: this.state.notes.trim(),
74 | saved: true,
75 | searchResults: {},
76 | })
77 | })
78 | }
79 |
80 | inputBlurred(event) {
81 | this.closeEditSoon = window.setTimeout(() => {
82 | this.setState({edit: false})
83 | }, 300)
84 | this.saveSoon = window.setTimeout(() => {
85 | this.saveChanges()
86 | }, 1000)
87 | }
88 |
89 | inputFocused(event) {
90 | if (event.target.setSelectionRange) {
91 | const inputLength = event.target.value.length
92 | event.target.setSelectionRange(inputLength, inputLength)
93 | }
94 | if (this.closeEditSoon) {
95 | window.clearTimeout(this.closeEditSoon)
96 | }
97 | if (this.saveSoon) {
98 | window.clearTimeout(this.saveSoon)
99 | }
100 | }
101 |
102 | autoCompleteSearch(text, field) {
103 | if (text.length < 2) {
104 | if (this.state.searchResults[field]) {
105 | this.setState({searchResults: {}})
106 | }
107 | return
108 | }
109 | let searchResults = {}
110 | const searchConfig = {
111 | fields: {
112 | text: field === 'text' ? 1 : 0,
113 | notes: field === 'notes' ? 1 : 0,
114 | },
115 | // Important otherwise the suggestions won't go away when
116 | // start typing a lot more.
117 | bool: 'AND',
118 | expand: true,
119 | }
120 | const results = this.props.searcher(text, searchConfig)
121 | let filteredResults = []
122 | results.forEach(r => {
123 | if (r.date !== this.props.day.date) {
124 | filteredResults.push(r)
125 | }
126 | })
127 | searchResults[field] = filteredResults
128 | this.setState({searchResults: searchResults})
129 | }
130 |
131 | startEdit(focusOn = 'text') {
132 | const { day } = this.props
133 | // const hadText =
134 | this.setState({
135 | text: day.text,
136 | notes: day.notes,
137 | starred: day.starred,
138 | edit: true,
139 | hadText: !!this.state.text,
140 | }, () => {
141 | const parentElement = document.querySelector(
142 | '#' + makeDayId(this.props.day.datetime)
143 | )
144 | if (parentElement) {
145 | const element = parentElement.querySelector(
146 | `textarea.${focusOn}`
147 | )
148 | if (element) {
149 | element.focus()
150 | }
151 | }
152 | })
153 | }
154 |
155 | shouldComponentUpdate(nextProps, nextState) {
156 | let a = {
157 | text: this.props.day.text,
158 | notes: this.props.day.notes,
159 | starred: this.props.day.starred,
160 | firstDateThisWeek: this.props.firstDateThisWeek,
161 | }
162 | let b = {
163 | text: nextProps.day.text,
164 | notes: nextProps.day.notes,
165 | starred: nextProps.day.starred,
166 | firstDateThisWeek: nextProps.firstDateThisWeek,
167 | }
168 | return !shallowEqual(a, b) || !shallowEqual(this.state, nextState)
169 | // // let b = nextProps
170 | // // console.log('Props?', shallowEqual(a, b), 'State?', shallowEqual(this.state, nextState));
171 | //
172 | // // return !shallowEqual(a, b) || !shallowEqual(this.state, nextState)
173 | // let r= !shallowEqual(a, b) || !shallowEqual(this.state, nextState)
174 | // if (r) {
175 | // if (!shallowEqual(a, b)) {
176 | // console.log('DIFFERENT this.props:', a, 'nextProps:', nextProps);
177 | // }
178 | // if (!shallowEqual(this.state, nextState)) {
179 | // console.log('DIFFERENT this.state:', this.state, 'nextState:', nextState);
180 | // }
181 | // }
182 | // return r
183 | }
184 |
185 | fieldClicked = (field) => {
186 | this.startEdit(field)
187 | }
188 |
189 | render() {
190 | let { day, firstDateThisWeek } = this.props
191 | let display
192 | if (this.state.edit) {
193 | display = (
194 |
195 |
252 |
253 |
254 | {
257 | this.setState({starred: !this.state.starred}, this.saveChanges)
258 | }}
259 | />
260 |
261 |
262 | {
263 | !this.state.saved || this.state.saving ?
264 | {
269 | if (this.saveSoon) {
270 | window.clearTimeout(this.saveSoon)
271 | }
272 | this.saveChanges()
273 | // If there was focus on any inputs, and you press a
274 | // button like that, that input triggers its onBlur,
275 | // so we want to prevent that from happening.
276 | if (this.closeEditSoon) {
277 | window.clearTimeout(this.closeEditSoon)
278 | }
279 | this.setState({saving: true})
280 | setTimeout(() => {
281 | this.setState({edit: false, saving: false})
282 | }, 1000)
283 | }}
284 | >{ this.state.saving ? 'Saving' : 'Save'}
285 | : null
286 | }
287 | {' '}
288 | {
292 | if (this.saveSoon) {
293 | window.clearTimeout(this.saveSoon)
294 | }
295 | this.setState({edit: false})
296 | }}
297 | >Close
298 | {' '}
299 | {
300 | this.state.text && this.state.hadText ?
301 | {
305 | store.copied = {
306 | date: day.date,
307 | text: this.state.text,
308 | notes: this.state.notes,
309 | starred: this.state.starred
310 | }
311 | if (this.saveSoon) {
312 | window.clearTimeout(this.saveSoon)
313 | }
314 | setTimeout(() => {
315 | this.setState({edit: false})
316 | }, 1000)
317 | }}
318 | >
319 | {
320 | store.copied && store.copied.date === day.date ?
321 | 'Copied': 'Copy'
322 | }
323 |
324 |
325 | : null
326 | }
327 | {' '}
328 | {
329 | store.copied && store.copied.date !== day.date && store.copied.text !== this.state.text ?
330 | {
334 | this.setState({
335 | text: store.copied.text,
336 | notes: store.copied.notes,
337 | starred: store.copied.starred,
338 | saved: false,
339 | })
340 | // If there was focus on any inputs, and you press a
341 | // button like that, that input triggers its onBlur,
342 | // so we want to prevent that from happening.
343 | if (this.closeEditSoon) {
344 | window.clearTimeout(this.closeEditSoon)
345 | }
346 | // this.saveChanges()
347 | }}>
348 | Paste
349 |
350 | : null
351 | }
352 |
353 |
354 |
355 | )
356 | } else {
357 | // Regular display mode
358 | display =
363 | }
364 |
365 | return (
366 |
372 |
373 | { firstDateThisWeek ?
: null }
374 |
375 |
{dateFns.format(day.datetime, 'dddd')}
376 |
377 |
378 |
379 |
380 | { display }
381 |
382 |
383 | )
384 | }
385 | })
386 |
387 | export default Day
388 |
389 |
390 | const ShowWeekdayHeadDate = pure(
391 | ({ datetime }) => {
392 | const now = new Date()
393 | let text
394 | if (dateFns.isSameDay(now, datetime)) {
395 | text = 'Today'
396 | } else if (dateFns.isSameDay(datetime, dateFns.subDays(now, 1))) {
397 | text = 'Yesterday'
398 | } else if (dateFns.isSameDay(datetime, dateFns.addDays(now, 1))) {
399 | text = 'Tomorrow'
400 | } else {
401 | text = dateFns.formatDistanceStrict(
402 | dateFns.startOfDay(now),
403 | datetime,
404 | {addSuffix: true}
405 | )
406 |
407 | }
408 |
409 | return (
410 |
411 | { text }
412 |
413 | )
414 |
415 | })
416 |
417 | export const DisplayDay = pure(
418 | ({ text, notes, starred, fieldClicked }) => {
419 | return (
420 |
421 | {
422 | !text.trim() && !notes.trim() ?
423 |
fieldClicked && fieldClicked('text')}>empty
424 | : null
425 | }
426 |
fieldClicked && fieldClicked('text')}>
429 | {
430 | text.split('\n').map((item, key) => (
431 | {item}
432 | ))
433 | }
434 |
435 |
fieldClicked && fieldClicked('notes')}>
438 |
440 | { notes }
441 |
442 |
443 | { starred ?
444 |
{}}
448 | />
: null
449 | }
450 |
451 | )
452 | })
453 |
454 |
455 | const ShowWeekHeader = pure(
456 | ({ datetime }) => {
457 | const id = makeWeekId(datetime)
458 | return (
459 | {
460 | const id = makeDayId(datetime)
461 | // XXX Consider using refs instead. See TODO
462 | const element = document.querySelector('#' + id)
463 | zenscroll.to(element)
464 | }}>
465 |
468 |
469 | )
470 | })
471 |
472 |
473 | const ShowTextAutocomplete = pure(
474 | ({ text, results, picked, field = 'text' }) => {
475 | if (!results) {
476 | return null
477 | }
478 | if (!results.length) {
479 | return null
480 | }
481 | if (!text.trim()) {
482 | return null
483 | }
484 | const searchWords = text.match(/\b(\w+)\b/g).map(word => {
485 | // return '\b' + word
486 | return word
487 | })
488 | return (
489 |
490 |
491 | {
492 | results.map(result => {
493 | return (
494 | picked(result[field])}
497 | >
498 |
503 |
504 | )
505 | })
506 | }
507 |
508 |
509 | )
510 | })
511 |
--------------------------------------------------------------------------------
/public/static/addtohomescreen.min.js:
--------------------------------------------------------------------------------
1 | !function(a,b){"function"==typeof define&&define.amd?define([],b):"object"==typeof exports?module.exports=b():a.returnExports=b()}(this,function(){function a(){window.removeEventListener("load",a,!1),f=!0}function b(a){return g=g||new b.Class(a)}function c(a,b){for(var c in b)a[c]=b[c];return a}function d(){"#ath"==document.location.hash&&history.replaceState("",window.document.title,document.location.href.split("#")[0]),h.test(document.location.href)&&history.replaceState("",window.document.title,document.location.href.replace(h,"$1")),i.test(document.location.search)&&history.replaceState("",window.document.title,document.location.href.replace(i,"$2"))}var e="addEventListener"in window,f=!1;"complete"===document.readyState?f=!0:e&&window.addEventListener("load",a,!1);var g,h=/\/ath(\/)?$/,i=/([\?&]ath=[^&]*$|&ath=[^&]*(&))/;b.intl={de_de:{ios:"Um diese Web-App zum Home-Bildschirm hinzuzufügen, tippen Sie auf %icon und dann Zum Home-Bildschirm .",android:'Um diese Web-App zum Home-Bildschirm hinzuzufügen, öffnen Sie das Menü und tippen dann auf Zum Startbildschirm hinzufügen . Wenn Ihr Gerät eine Menütaste hat, lässt sich das Browsermenü über diese öffnen. Ansonsten tippen Sie auf icon . '},da_dk:{ios:"For at tilføje denne web app til hjemmeskærmen: Tryk %icon og derefter Føj til hjemmeskærm .",android:'For at tilføje denne web app til hjemmeskærmen, åbn browser egenskaber menuen og tryk på Føj til hjemmeskærm . Denne menu kan tilgås ved at trykke på menu knappen, hvis din enhed har en, eller ved at trykke på det øverste højre menu ikon icon . '},en_us:{ios:"To add this web app to the home screen: tap %icon and then Add to Home Screen .",android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '},es_es:{ios:"Para añadir esta aplicación web a la pantalla de inicio: pulsa %icon y selecciona Añadir a pantalla de inicio .",android:'Para añadir esta aplicación web a la pantalla de inicio, abre las opciones y pulsa Añadir a pantalla inicio . El menú se puede acceder pulsando el botón táctil en caso de tenerlo, o bien el icono de la parte superior derecha de la pantalla icon . '},fi_fi:{ios:"Liitä tämä sovellus kotivalikkoon: klikkaa %icon ja tämän jälkeen Lisää kotivalikkoon .",android:'Lisätäksesi tämän sovelluksen aloitusnäytölle, avaa selaimen valikko ja klikkaa tähti -ikonia tai Lisää aloitusnäytölle tekstiä . Valikkoon pääsee myös painamalla menuvalikkoa, jos laitteessasi on sellainen tai koskettamalla oikealla yläkulmassa menu ikonia icon . '},fr_fr:{ios:"Pour ajouter cette application web sur l'écran d'accueil : Appuyez %icon et sélectionnez Ajouter sur l'écran d'accueil .",android:'Pour ajouter cette application web sur l\'écran d\'accueil : Appuyez sur le bouton "menu", puis sur Ajouter sur l\'écran d\'accueil . Le menu peut-être accessible en appyant sur le bouton "menu" du téléphone s\'il en possède un . Sinon, il se trouve probablement dans la coin supérieur droit du navigateur %icon. '},he_il:{ios:'להוספת האפליקציה למסך הבית: ללחוץ על %icon ואז הוסף למסך הבית . ',android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '},it_it:{ios:"Per aggiungere questa web app alla schermata iniziale: premi %icon e poi Aggiungi a Home .",android:'Per aggiungere questa web app alla schermata iniziale, apri il menu opzioni del browser e premi su Aggiungi alla homescreen . Puoi accedere al menu premendo il pulsante hardware delle opzioni se la tua device ne ha uno, oppure premendo l\'icona icon in alto a destra. '},ja_jp:{ios:"このウェプアプリをホーム画面に追加するために%iconを押してホーム画面に追加 。",android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '},ko_kr:{ios:"홈 화면에 바로가기 생성: %icon 을 클릭한 후 홈 화면에 추가 .",android:'브라우저 옵션 메뉴의 홈 화면에 추가 를 클릭하여 홈화면에 바로가기를 생성할 수 있습니다. 옵션 메뉴는 장치의 메뉴 버튼을 누르거나 오른쪽 상단의 메뉴 아이콘 icon 을 클릭하여 접근할 수 있습니다. '},nb_no:{ios:"For å installere denne appen på hjem-skjermen: trykk på %icon og deretter Legg til på Hjem-skjerm .",android:'For å legge til denne webappen på startsiden åpner en nettlesermenyen og velger Legg til på startsiden . Menyen åpnes ved å trykke på den fysiske menyknappen hvis enheten har det, eller ved å trykke på menyikonet øverst til høyre icon . '},pt_br:{ios:"Para adicionar este app à tela de início: clique %icon e então Tela de início .",android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '},pt_pt:{ios:"Para adicionar esta app ao ecrã principal: clique %icon e depois Ecrã principal .",android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '},nl_nl:{ios:"Om deze webapp op je telefoon te installeren, klik op %icon en dan Zet in beginscherm .",android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '},ru_ru:{ios:'Чтобы добавить этот сайт на свой домашний экран, нажмите на иконку %icon и затем На экран "Домой" .',android:'Чтобы добавить сайт на свой домашний экран, откройте меню браузера и нажмите на Добавить на главный экран . Меню можно вызвать, нажав на кнопку меню вашего телефона, если она есть. Или найдите иконку сверху справа иконка . '},sv_se:{ios:"För att lägga till denna webbapplikation på hemskärmen: tryck på %icon och därefter Lägg till på hemskärmen .",android:'För att lägga till den här webbappen på hemskärmen öppnar du webbläsarens alternativ-meny och väljer Lägg till på startskärmen . Man hittar menyn genom att trycka på hårdvaruknappen om din enhet har en sådan, eller genom att trycka på menyikonen högst upp till höger icon . '},zh_cn:{ios:"如要把应用程序加至主屏幕,请点击%icon, 然后添加到主屏幕 ",android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '},zh_tw:{ios:"如要把應用程式加至主屏幕, 請點擊%icon, 然後加至主屏幕 .",android:'To add this web app to the home screen open the browser option menu and tap on Add to homescreen . The menu can be accessed by pressing the menu hardware button if your device has one, or by tapping the top right menu icon icon . '}};for(var j in b.intl)b.intl[j.substr(0,2)]=b.intl[j];b.defaults={appID:"org.cubiq.addtohome",fontSize:15,debug:!1,logging:!1,modal:!1,mandatory:!1,autostart:!0,skipFirstVisit:!1,startDelay:1,lifespan:15,displayPace:1440,maxDisplayCount:0,icon:!0,message:"",validLocation:[],onInit:null,onShow:null,onRemove:null,onAdd:null,onPrivate:null,privateModeOverride:!1,detectHomescreen:!1};var k=window.navigator.userAgent,l=window.navigator;c(b,{hasToken:"#ath"==document.location.hash||h.test(document.location.href)||i.test(document.location.search),isRetina:window.devicePixelRatio&&window.devicePixelRatio>1,isIDevice:/iphone|ipod|ipad/i.test(k),isMobileChrome:k.indexOf("Android")>-1&&/Chrome\/[.0-9]*/.test(k)&&-1==k.indexOf("Version"),isMobileIE:k.indexOf("Windows Phone")>-1,language:l.language&&l.language.toLowerCase().replace("-","_")||""}),b.language=b.language&&b.language in b.intl?b.language:"en_us",b.isMobileSafari=b.isIDevice&&k.indexOf("Safari")>-1&&k.indexOf("CriOS")<0,b.OS=b.isIDevice?"ios":b.isMobileChrome?"android":b.isMobileIE?"windows":"unsupported",b.OSVersion=k.match(/(OS|Android) (\d+[_\.]\d+)/),b.OSVersion=b.OSVersion&&b.OSVersion[2]?+b.OSVersion[2].replace("_","."):0,b.isStandalone="standalone"in window.navigator&&window.navigator.standalone,b.isTablet=b.isMobileSafari&&k.indexOf("iPad")>-1||b.isMobileChrome&&k.indexOf("Mobile")<0,b.isCompatible=b.isMobileSafari&&b.OSVersion>=6||b.isMobileChrome;var m={lastDisplayTime:0,returningVisitor:!1,displayCount:0,optedout:!1,added:!1};b.removeSession=function(a){try{if(!localStorage)throw new Error("localStorage is not defined");localStorage.removeItem(a||b.defaults.appID)}catch(c){}},b.doLog=function(a){this.options.logging&&console.log(a)},b.Class=function(a){if(this.doLog=b.doLog,this.options=c({},b.defaults),c(this.options,a),a&&a.debug&&"undefined"==typeof a.logging&&(this.options.logging=!0),e){if(this.options.mandatory=this.options.mandatory&&("standalone"in window.navigator||this.options.debug),this.options.modal=this.options.modal||this.options.mandatory,this.options.mandatory&&(this.options.startDelay=-.5),this.options.detectHomescreen=this.options.detectHomescreen===!0?"hash":this.options.detectHomescreen,this.options.debug&&(b.isCompatible=!0,b.OS="string"==typeof this.options.debug?this.options.debug:"unsupported"==b.OS?"android":b.OS,b.OSVersion="ios"==b.OS?"8":"4"),this.container=document.documentElement,this.session=this.getItem(this.options.appID),this.session=this.session?JSON.parse(this.session):void 0,!b.hasToken||b.isCompatible&&this.session||(b.hasToken=!1,d()),!b.isCompatible)return void this.doLog("Add to homescreen: not displaying callout because device not supported");this.session=this.session||m;try{if(!localStorage)throw new Error("localStorage is not defined");localStorage.setItem(this.options.appID,JSON.stringify(this.session)),b.hasLocalStorage=!0}catch(f){b.hasLocalStorage=!1,this.options.onPrivate&&this.options.onPrivate.call(this)}for(var g=!this.options.validLocation.length,h=this.options.validLocation.length;h--;)if(this.options.validLocation[h].test(document.location.href)){g=!0;break}if(this.getItem("addToHome")&&this.optOut(),this.session.optedout)return void this.doLog("Add to homescreen: not displaying callout because user opted out");if(this.session.added)return void this.doLog("Add to homescreen: not displaying callout because already added to the homescreen");if(!g)return void this.doLog("Add to homescreen: not displaying callout because not a valid location");if(b.isStandalone)return this.session.added||(this.session.added=!0,this.updateSession(),this.options.onAdd&&b.hasLocalStorage&&this.options.onAdd.call(this)),void this.doLog("Add to homescreen: not displaying callout because in standalone mode");if(this.options.detectHomescreen){if(b.hasToken)return d(),this.session.added||(this.session.added=!0,this.updateSession(),this.options.onAdd&&b.hasLocalStorage&&this.options.onAdd.call(this)),void this.doLog("Add to homescreen: not displaying callout because URL has token, so we are likely coming from homescreen");"hash"==this.options.detectHomescreen?history.replaceState("",window.document.title,document.location.href+"#ath"):"smartURL"==this.options.detectHomescreen?history.replaceState("",window.document.title,document.location.href.replace(/(\/)?$/,"/ath$1")):history.replaceState("",window.document.title,document.location.href+(document.location.search?"&":"?")+"ath=")}if(!this.session.returningVisitor&&(this.session.returningVisitor=!0,this.updateSession(),this.options.skipFirstVisit))return void this.doLog("Add to homescreen: not displaying callout because skipping first visit");if(!this.options.privateModeOverride&&!b.hasLocalStorage)return void this.doLog("Add to homescreen: not displaying callout because browser is in private mode");this.ready=!0,this.options.onInit&&this.options.onInit.call(this),this.options.autostart&&(this.doLog("Add to homescreen: autostart displaying callout"),this.show())}},b.Class.prototype={events:{load:"_delayedShow",error:"_delayedShow",orientationchange:"resize",resize:"resize",scroll:"resize",click:"remove",touchmove:"_preventDefault",transitionend:"_removeElements",webkitTransitionEnd:"_removeElements",MSTransitionEnd:"_removeElements"},handleEvent:function(a){var b=this.events[a.type];b&&this[b](a)},show:function(a){if(this.options.autostart&&!f)return void setTimeout(this.show.bind(this),50);if(this.shown)return void this.doLog("Add to homescreen: not displaying callout because already shown on screen");var c=Date.now(),d=this.session.lastDisplayTime;if(a!==!0){if(!this.ready)return void this.doLog("Add to homescreen: not displaying callout because not ready");if(c-d<6e4*this.options.displayPace)return void this.doLog("Add to homescreen: not displaying callout because displayed recently");if(this.options.maxDisplayCount&&this.session.displayCount>=this.options.maxDisplayCount)return void this.doLog("Add to homescreen: not displaying callout because displayed too many times already")}this.shown=!0,this.session.lastDisplayTime=c,this.session.displayCount++,this.updateSession(),this.applicationIcon||("ios"==b.OS?this.applicationIcon=document.querySelector('head link[rel^=apple-touch-icon][sizes="152x152"],head link[rel^=apple-touch-icon][sizes="144x144"],head link[rel^=apple-touch-icon][sizes="120x120"],head link[rel^=apple-touch-icon][sizes="114x114"],head link[rel^=apple-touch-icon]'):this.applicationIcon=document.querySelector('head link[rel^="shortcut icon"][sizes="196x196"],head link[rel^=apple-touch-icon]'));var e="";"object"==typeof this.options.message&&b.language in this.options.message?e=this.options.message[b.language][b.OS]:"object"==typeof this.options.message&&b.OS in this.options.message?e=this.options.message[b.OS]:this.options.message in b.intl?e=b.intl[this.options.message][b.OS]:""!==this.options.message?e=this.options.message:b.OS in b.intl[b.language]&&(e=b.intl[b.language][b.OS]),e=""+e.replace("%icon",'icon ')+"
",this.viewport=document.createElement("div"),this.viewport.className="ath-viewport",this.options.modal&&(this.viewport.className+=" ath-modal"),this.options.mandatory&&(this.viewport.className+=" ath-mandatory"),this.viewport.style.position="absolute",this.element=document.createElement("div"),this.element.className="ath-container ath-"+b.OS+" ath-"+b.OS+(b.OSVersion+"").substr(0,1)+" ath-"+(b.isTablet?"tablet":"phone"),this.element.style.cssText="-webkit-transition-property:-webkit-transform,opacity;-webkit-transition-duration:0s;-webkit-transition-timing-function:ease-out;transition-property:transform,opacity;transition-duration:0s;transition-timing-function:ease-out;",this.element.style.webkitTransform="translate3d(0,-"+window.innerHeight+"px,0)",this.element.style.transform="translate3d(0,-"+window.innerHeight+"px,0)",this.options.icon&&this.applicationIcon&&(this.element.className+=" ath-icon",this.img=document.createElement("img"),this.img.className="ath-application-icon",this.img.addEventListener("load",this,!1),this.img.addEventListener("error",this,!1),this.img.src=this.applicationIcon.href,this.element.appendChild(this.img)),this.element.innerHTML+=e,this.viewport.style.left="-99999em",this.viewport.appendChild(this.element),this.container.appendChild(this.viewport),this.img?this.doLog("Add to homescreen: not displaying callout because waiting for img to load"):this._delayedShow()},_delayedShow:function(a){setTimeout(this._show.bind(this),1e3*this.options.startDelay+500)},_show:function(){var a=this;this.updateViewport(),window.addEventListener("resize",this,!1),window.addEventListener("scroll",this,!1),window.addEventListener("orientationchange",this,!1),this.options.modal&&document.addEventListener("touchmove",this,!0),this.options.mandatory||setTimeout(function(){a.element.addEventListener("click",a,!0)},1e3),setTimeout(function(){a.element.style.webkitTransitionDuration="0.5s ease-in",a.element.style.transitionDuration="0.5s ease-in",a.element.style.webkitTransform="translate3d(0,0,0)",a.element.style.transform="translate3d(0,0,0)"},0),this.options.lifespan&&(this.removeTimer=setTimeout(this.remove.bind(this),1e3*this.options.lifespan)),this.options.onShow&&this.options.onShow.call(this)},remove:function(){clearTimeout(this.removeTimer),this.img&&(this.img.removeEventListener("load",this,!1),this.img.removeEventListener("error",this,!1)),window.removeEventListener("resize",this,!1),window.removeEventListener("scroll",this,!1),window.removeEventListener("orientationchange",this,!1),document.removeEventListener("touchmove",this,!0),this.element.removeEventListener("click",this,!0),this.element.addEventListener("transitionend",this,!1),this.element.addEventListener("webkitTransitionEnd",this,!1),this.element.addEventListener("MSTransitionEnd",this,!1),this.element.style.webkitTransitionDuration="0.3s",this.element.style.opacity="0"},_removeElements:function(){this.element.removeEventListener("transitionend",this,!1),this.element.removeEventListener("webkitTransitionEnd",this,!1),this.element.removeEventListener("MSTransitionEnd",this,!1),this.container.removeChild(this.viewport),this.shown=!1,this.options.onRemove&&this.options.onRemove.call(this)},updateViewport:function(){if(this.shown){this.viewport.style.width=window.innerWidth+"px",this.viewport.style.height=window.innerHeight+"px",this.viewport.style.left=window.scrollX+"px",this.viewport.style.top=window.scrollY+"px";var a=document.documentElement.clientWidth;this.orientation=a>document.documentElement.clientHeight?"landscape":"portrait";var c="ios"==b.OS?"portrait"==this.orientation?screen.width:screen.height:screen.width;this.scale=screen.width>a?1:c/window.innerWidth,this.element.style.fontSize=this.options.fontSize/this.scale+"px"}},resize:function(){clearTimeout(this.resizeTimer),this.resizeTimer=setTimeout(this.updateViewport.bind(this),100)},updateSession:function(){b.hasLocalStorage!==!1&&localStorage&&localStorage.setItem(this.options.appID,JSON.stringify(this.session))},clearSession:function(){this.session=m,this.updateSession()},getItem:function(a){try{if(!localStorage)throw new Error("localStorage is not defined");return localStorage.getItem(a)}catch(c){b.hasLocalStorage=!1}},optOut:function(){this.session.optedout=!0,this.updateSession()},optIn:function(){this.session.optedout=!1,this.updateSession()},clearDisplayCount:function(){this.session.displayCount=0,this.updateSession()},_preventDefault:function(a){a.preventDefault(),a.stopPropagation()}},window.addToHomescreen=b});
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import elasticlunr from 'elasticlunr'
3 | import { observer } from 'mobx-react'
4 | import zenscroll from 'zenscroll'
5 | import * as firebase from 'firebase/app'
6 | import 'firebase/auth'
7 | import 'firebase/database'
8 | import { pure } from 'recompose'
9 |
10 | import {
11 | format,
12 | getTime,
13 | startOfWeek,
14 | addDays,
15 | toDate,
16 | } from 'date-fns/esm'
17 |
18 | // import Perf from 'react-addons-perf'
19 | // window.Perf = Perf
20 |
21 | import './App.css'
22 | import Nav from './Nav'
23 | import Days from './Days'
24 | import Settings from './Settings'
25 | import Favorites from './Favorites'
26 | import User from './User'
27 | import SignIn from './SignIn'
28 | import Search from './Search'
29 | import Group from './Group'
30 | import store from './Store'
31 | import { makeDayId, pagifyScrubText } from './Common'
32 |
33 | // Because I love namespaces
34 | const dateFns = {
35 | toDate: toDate,
36 | getTime: getTime,
37 | startOfWeek: startOfWeek,
38 | addDays: addDays,
39 | format: format,
40 | }
41 |
42 | const DATE_FORMAT = 'YYYY-MM-DD'
43 |
44 | // string to Date object
45 | const decodeDatetime = dateStr => dateFns.toDate(dateStr)
46 | // Date object to string
47 | const encodeDatetime = dateObj => dateFns.getTime(dateObj)
48 |
49 | if (process.env.REACT_APP_DEV === 'true') {
50 | store.dev = true
51 | document.title = 'Dev ' + document.title
52 | }
53 |
54 | const App = observer(class App extends Component {
55 | constructor() {
56 | super()
57 |
58 | this.state = {
59 | page: 'days',
60 | }
61 |
62 | // Initialize Firebase
63 | const config = {
64 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
65 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
66 | databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
67 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
68 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
69 | }
70 | const firebaseApp = firebase.initializeApp(config)
71 | this.auth = firebaseApp.auth()
72 | this.auth.onAuthStateChanged(this.onAuthStateChanged.bind(this))
73 | if (process.env.REACT_APP_FIREBASE_LOGGING === 'true') {
74 | firebaseApp.database.enableLogging(true)
75 | }
76 | this.database = firebaseApp.database()
77 | }
78 |
79 | onAuthStateChanged = (user) => {
80 |
81 | // Perf.start()
82 |
83 | if (user) {
84 | // User is signed in!
85 | // let profilePicUrl = user.photoURL
86 | // let userName = user.displayName
87 | store.currentUser = user
88 | // If you're signed in, let's set what you're current group is
89 | if (!store.currentGroup && store.settings.defaultGroupId) {
90 | this.database.ref('/groups/' + store.settings.defaultGroupId)
91 | .once('value')
92 | .then(snapshot => {
93 | if (snapshot.val()) {
94 | store.currentGroup = {
95 | id: store.settings.defaultGroupId,
96 | name: snapshot.val().name
97 | }
98 | }
99 | }, error => {
100 | console.warn('Unable to look up group');
101 | console.error(error);
102 | })
103 | } else {
104 | // console.warn('Figure out which is your default group!');
105 | this.setState({page: 'group'})
106 | }
107 |
108 | // Only bother for people who bother to sign in.
109 | if (window.addToHomescreen) {
110 | setTimeout(() => {
111 | // window.addToHomescreen({debug: true})
112 | window.addToHomescreen()
113 | }, 3000)
114 | }
115 |
116 | } else {
117 | // User is signed out!
118 | console.log('No user', 'Signed out?');
119 | store.currentUser = false
120 | }
121 | }
122 |
123 | componentDidMount() {
124 |
125 | // setTimeout(() => {
126 | // Perf.stop()
127 | // Perf.printWasted()
128 | // }, 20000)
129 |
130 | this.searchData = {}
131 |
132 | if (this.database) {
133 | this.database.ref('.info/connected')
134 | .on('value', snapshot => {
135 | if (snapshot.val()) { // Yay! Connected!
136 | if (this.setOfflineTimeout) {
137 | window.clearTimeout(this.setOfflineTimeout)
138 | }
139 | this.triggerNotOffline()
140 | } else {
141 | console.log('navigator.onLine?', navigator.onLine);
142 | if (store.offline === null) {
143 | // It it hasn't been set yet, give it some time to do so
144 | this.setOfflineTimeout = window.setTimeout(() => {
145 | this.triggerOffline()
146 | }, 2000);
147 | } else {
148 | this.triggerOffline()
149 | }
150 | }
151 | })
152 | }
153 |
154 | this.loadInitialWeek()
155 |
156 | if (store.settings.defaultGroupId) {
157 | // this.listenOnDayRefs()
158 |
159 | const searchIndexAsJson = localStorage.getItem('searchIndex')
160 | const searchDataAsJson = localStorage.getItem('searchData')
161 | if (searchIndexAsJson && searchDataAsJson) {
162 | // console.log('this.searchIndex created from JSON');
163 | this.searchIndex = elasticlunr.Index.load(
164 | JSON.parse(searchIndexAsJson)
165 | )
166 | this.searchData = JSON.parse(searchDataAsJson)
167 |
168 | // Offline or not, fill in as many days as we can from
169 | // the localStorage snapshot.
170 | const dayDates = Object.keys(this.searchData)
171 | if (dayDates.length) {
172 | dayDates.forEach(date => {
173 | let data = this.searchData[date]
174 | store.addDay(
175 | date,
176 | decodeDatetime(data.datetime),
177 | data.text,
178 | data.notes,
179 | data.starred
180 | )
181 | })
182 | }
183 |
184 | } else {
185 | // console.log('this.searchIndex created from, elasticlunr', elasticlunr);
186 | // this.searchIndex = elasticlunr(function () {
187 | // this.addField('text')
188 | // this.addField('notes')
189 | // this.setRef('date')
190 | // // not store the original JSON document to reduce the index size
191 | // this.saveDocument(false)
192 | // })
193 | // this.searchData = {}
194 | this.loadSearchIndex()
195 | }
196 | }
197 | }
198 |
199 | triggerNotOffline = () => {
200 | store.offline = false
201 | }
202 |
203 | triggerOffline = () => {
204 | store.offline = true
205 | }
206 |
207 | listenOnDayRefs = () => {
208 | if (!store.dateRangeStart) {
209 | throw new Error('store.dateRangeStart not set')
210 | }
211 | if (!store.dateRangeEnd) {
212 | throw new Error('store.dateRangeEnd not set')
213 | }
214 | if (this.daysRef) {
215 | this.daysRef.off()
216 | }
217 | this.daysRef = this.database.ref(
218 | 'groups/' + store.settings.defaultGroupId + '/days'
219 | )
220 | .orderByChild('datetime')
221 | .startAt(encodeDatetime(store.dateRangeStart))
222 | .endAt(encodeDatetime(store.dateRangeEnd))
223 |
224 | this.daysRef.on('child_added', child => {
225 | const data = child.val()
226 | // console.log('DAY ADDED!', child.key, data);
227 | store.addDay(
228 | child.key,
229 | decodeDatetime(data.datetime),
230 | data.text,
231 | data.notes,
232 | data.starred
233 | )
234 | if (this.differentSearchData(child.key, data)) {
235 | this.searchData[child.key] = {
236 | date: child.key,
237 | datetime: data.datetime,
238 | text: data.text,
239 | notes: data.notes,
240 | starred: data.starred,
241 | }
242 | localStorage.setItem('searchData', JSON.stringify(this.searchData))
243 | }
244 | })
245 | this.daysRef.on('child_changed', child => {
246 | // console.log('DAY CHANGED!', child.key, child.val());
247 | const data = child.val()
248 | store.addDay(
249 | child.key,
250 | decodeDatetime(data.datetime),
251 | data.text,
252 | data.notes,
253 | data.starred
254 | )
255 | if (this.differentSearchData(child.key, data)) {
256 |
257 | this.searchData[child.key] = {
258 | date: child.key,
259 | datetime: data.datetime,
260 | text: data.text,
261 | notes: data.notes,
262 | starred: data.starred,
263 | }
264 | localStorage.setItem('searchData', JSON.stringify(this.searchData))
265 | }
266 | })
267 | }
268 |
269 | differentSearchData = (date, data) => {
270 | // compare 'data' with 'this.searchData[date]' and if any of them is
271 | // different return true.
272 | if (!this.searchData[date]) {
273 | return true
274 | }
275 | let differentKeys = Object.keys(data).filter(key => {
276 | return this.searchData[date] && data[key] !== this.searchData[date][key]
277 | })
278 | return !!differentKeys.length
279 | }
280 |
281 | loadSearchIndex = () => {
282 | // console.log('this.searchIndex created from, elasticlunr', elasticlunr);
283 | this.searchIndex = elasticlunr(function () {
284 | this.addField('text')
285 | this.addField('notes')
286 | this.setRef('date')
287 | // not store the original JSON document to reduce the index size
288 | this.saveDocument(false)
289 | })
290 | this.searchData = {}
291 | if (this._searchCache) {
292 | this._searchCache = {}
293 | }
294 | this.database.ref(
295 | 'groups/' + store.settings.defaultGroupId + '/days'
296 | )
297 | .once('value', snapshot => {
298 | snapshot.forEach(child => {
299 | const data = child.val()
300 | if (data.text || data.notes) {
301 | this.searchIndex.addDoc({
302 | date: child.key,
303 | text: data.text,
304 | notes: data.notes,
305 | })
306 | this.searchData[child.key] = {
307 | date: child.key,
308 | datetime: data.datetime,
309 | text: data.text,
310 | notes: data.notes,
311 | starred: data.starred,
312 | }
313 | }
314 | })
315 | localStorage.setItem(
316 | 'searchIndex',
317 | JSON.stringify(this.searchIndex)
318 | )
319 | localStorage.setItem(
320 | 'searchData',
321 | JSON.stringify(this.searchData)
322 | )
323 | })
324 | }
325 |
326 | loadInitialWeek = () => {
327 | const weekStartsOnAMonday = store.settings.weekStartsOnAMonday || false
328 | store.firstDateThisWeek = dateFns.startOfWeek(
329 | new Date(), {weekStartsOn: weekStartsOnAMonday ? 1 : 0}
330 | )
331 | return this.loadWeek(store.firstDateThisWeek)
332 | }
333 |
334 | loadWeek = (firstDate) => { // should maybe be called loadBlankWeekDays
335 | if (!firstDate || typeof firstDate !== 'object') {
336 | throw new Error("Expect 'firstDate' to be a date object")
337 | }
338 | let lastDate = dateFns.addDays(firstDate, 7)
339 | store.extendDateRange(firstDate, lastDate)
340 | let dayNumbers = [0, 1, 2, 3, 4, 5, 6]
341 | dayNumbers.forEach(d => {
342 | let datetime = dateFns.addDays(firstDate, d)
343 | let date = dateFns.format(datetime, DATE_FORMAT)
344 | if (!store.days.has(date)) {
345 | // put a blank one in lieu
346 | // console.log('ADDING', date, datetime);
347 | store.addDay(date, datetime)
348 | } else {
349 | // console.log('SKIPPING', date, datetime);
350 | }
351 | })
352 | this.listenOnDayRefs()
353 | }
354 |
355 | updateDay = (day, data) => {
356 | // don't forget to update the big mutable
357 | day.text = data.text
358 | day.notes = data.notes
359 | day.starred = data.starred
360 |
361 | // update the search cache
362 | if (this._searchCache && this._searchCache[day.date]) {
363 | delete this._searchCache[day.date]
364 | }
365 |
366 | const dayRef = this.database.ref(
367 | 'groups/' + store.settings.defaultGroupId + '/days/' + day.date
368 | )
369 | this.searchIndex.addDoc({
370 | date: day.date,
371 | text: data.text,
372 | notes: data.notes,
373 | })
374 | localStorage.setItem('searchIndex', JSON.stringify(this.searchIndex))
375 | this.searchData[day.date] = {
376 | date: day.date,
377 | datetime: encodeDatetime(day.datetime),
378 | text: data.text,
379 | notes: data.notes,
380 | starred: data.starred,
381 | }
382 | localStorage.setItem('searchData', JSON.stringify(this.searchData))
383 | if (store.currentUser) {
384 | return dayRef.set({
385 | date: day.date, // XXX is this necessary
386 | datetime: encodeDatetime(day.datetime),
387 | text: data.text,
388 | notes: data.notes,
389 | starred: data.starred,
390 | })
391 | } else {
392 | return Promise.resolve(null)
393 | }
394 |
395 | }
396 |
397 | getFavorites = () => {
398 | return this.database.ref(
399 | 'groups/' + store.settings.defaultGroupId + '/days'
400 | )
401 | .orderByChild('starred')
402 | .equalTo(true)
403 | .once('value')
404 | .then(snapshot => {
405 | let results = []
406 | // Strange, snapshot has a .forEach but no .map or .filter
407 | snapshot.forEach(child => {
408 | results.push(child.val())
409 | })
410 | results.sort((a, b) => {
411 | return b.datetime - a.datetime
412 | })
413 | let hashes = new Set()
414 | results = results.filter(result => {
415 | let hash = result.text + result.notes
416 | hash = hash.toLowerCase()
417 | if (!hashes.has(hash)) {
418 | hashes.add(hash)
419 | return true
420 | }
421 | return false
422 | })
423 | return results
424 | })
425 | }
426 |
427 | searcher = (text, searchConfig) => {
428 | if (!text.trim()) {
429 | throw new Error('Empty search')
430 | }
431 | let found = this.searchIndex.search(
432 | text,
433 | searchConfig,
434 | )
435 | console.log('FOUND', found);
436 | if (!found.length) {
437 | return []
438 | }
439 | if (!this._searchCache) {
440 | // If this starts getting too large and bloated,
441 | // consider a LRU cache like
442 | // https://github.com/rsms/js-lru
443 | this._searchCache = {}
444 | }
445 | let refs = {}
446 | let cached = []
447 | found.forEach(f => {
448 | refs[f.ref] = f.score
449 | if (this._searchCache[f.ref]) {
450 | cached.push(this._searchCache[f.ref])
451 | }
452 | })
453 | if (found.length === cached.length) {
454 | // we had all of them cached!
455 | return cached
456 | }
457 | let uniqueTexts = new Set()
458 | let key = 'text'
459 | if (searchConfig.fields.notes) {
460 | key = 'notes'
461 | }
462 | return found.map(result => {
463 | return this.searchData[result.ref]
464 | }).filter(result => {
465 | let hash = result[key].toLowerCase()
466 | if (key === 'notes') {
467 | // This will make "some cookbook page 123" and "some cookbook p.100"
468 | // both into "some cookbook page ?"
469 | hash = pagifyScrubText(hash)
470 | }
471 | if (!uniqueTexts.has(hash)) {
472 | uniqueTexts.add(hash)
473 | return true
474 | }
475 | return false
476 | })
477 | }
478 |
479 | render() {
480 | let page = Loading...
481 |
482 | if (this.state.page === 'settings') {
483 | page = {
485 | this.setState({page: 'days'})
486 | }}
487 | onChangeWeekStart={() => {
488 | // if the user has changed start day of the week, re-load
489 | store.dateRangeStart = null
490 | store.dateRangeEnd = null
491 | this.loadInitialWeek()
492 | }}
493 | onClearCache={() => {
494 | localStorage.removeItem('searchData')
495 | localStorage.removeItem('searchIndex')
496 | window.location.reload(true)
497 | }}
498 | />
499 | } else if (this.state.page === 'search') {
500 | page = {
503 | this.setState({page: 'days'})
504 | // this.loadInitialWeek()
505 | }}
506 | />
507 | } else if (this.state.page === 'starred') {
508 | page = {
511 | this.setState({page: 'days'})
512 | if (!store.days.length) {
513 | this.loadInitialWeek()
514 | }
515 | }}
516 | />
517 | } else if (this.state.page === 'user') {
518 | page = {
521 | this.auth.signOut().then(() => {
522 | // Sign-out successful.
523 | this.setState({page: 'days'}, () => {
524 | store.currentUser = null
525 | if (!store.days.length) {
526 | this.loadInitialWeek()
527 | }
528 | })
529 | }, (error) => {
530 | // An error happened.
531 | console.warn('Unable to sign out');
532 | console.error(error);
533 | })
534 | }}
535 | onClosePage={e => {
536 | this.setState({page: 'days'})
537 | if (!store.days.length) {
538 | this.loadInitialWeek()
539 | }
540 | }}
541 | />
542 | } else if (this.state.page === 'signin') {
543 | page = {
546 | store.currentUser.sendEmailVerification().then(() => {
547 | // XXX flash message?
548 | console.log("Email verification email sent. Check your inbox.");
549 | }, error => {
550 | console.error(error);
551 | })
552 | }}
553 | onClosePage={e => {
554 | // XXX if there is already a group in store.settings
555 | // or something, then no need to redirect to the group page.
556 | this.setState({page: 'group'})
557 | }}
558 | />
559 | } else if (this.state.page === 'group' && store.currentUser) {
560 | page = {
564 | store.days.clear()
565 | this.loadInitialWeek()
566 | this.daysRef.off()
567 | this.listenOnDayRefs()
568 |
569 | this.loadSearchIndex()
570 | }}
571 | onClosePage={e => {
572 | this.setState({page: 'days'})
573 | if (!store.days.length) {
574 | this.loadInitialWeek()
575 | }
576 | }}
577 | />
578 | } else {
579 | if (this.state.page !== 'days' && store.currentUser) {
580 | // throw new Error(`Unsure about page '${page}'`)
581 | console.warn(`Unsure about page '${this.state.page}'`)
582 | }
583 | page = {
588 | this.setState({page: 'signin'})
589 | }}
590 | // firstDateThisWeek={store.firstDateThisWeek}
591 | />
592 | }
593 |
594 | return (
595 |
596 |
{
598 | this.setState({page: 'user'})
599 | }}
600 | onGotoSignIn={() => {
601 | this.setState({page: 'signin'})
602 | }}
603 | onGotoWeek={(resetDateRange = false) => {
604 | this.setState({page: 'days'}, () => {
605 | if (resetDateRange) {
606 | store.dateRangeStart = store.firstDateThisWeek
607 | store.dateRangeEnd = dateFns.addDays(store.firstDateThisWeek, 7)
608 | }
609 | const id = makeDayId(store.firstDateThisWeek)
610 | const element = document.querySelector('#' + id)
611 | setTimeout(() => {
612 | zenscroll.to(element)
613 | }, 100)
614 | })
615 | }}
616 | onGotoSettings={() => {
617 | this.setState({page: 'settings'})
618 | }}
619 | onGotoGroup={() => {
620 | this.setState({page: 'group'})
621 | }}
622 | onGotoStarred={() => {
623 | this.getFavorites().then(results => {
624 | // console.log('FAVORITES RESULT:', results);
625 | store.recentFavorites = results
626 | })
627 | this.setState({page: 'starred'})
628 | }}
629 | onGotoSearch={() => {
630 | this.setState({page: 'search'})
631 | }}
632 | />
633 |
634 |
635 | { page }
636 |
637 |
638 | );
639 | }
640 | })
641 |
642 | export default App
643 |
644 |
645 | const ShowOfflineWarning = pure(
646 | ({ offline }) => {
647 | if (offline !== true) {
648 | return null
649 | }
650 | return (
651 |
652 |
Offline!
653 |
654 | Seems you are offline :(
655 |
656 |
657 | {
661 | window.location.reload()
662 | }}
663 | >
664 | Try Reloading
665 |
666 |
667 |
668 | )
669 | })
670 |
--------------------------------------------------------------------------------