├── .babelrc
├── .firebaserc
├── .gitignore
├── LICENSE.md
├── README.md
├── database.rules.json
├── firebase.json
├── images
├── logo.png
├── logo_small.png
├── logo_white.png
├── logo_white_small.png
├── step_create.png
├── step_input.png
├── step_stop.png
└── step_timer.png
├── package.json
├── public
├── favicon.png
├── fonts.css
├── index.html
├── logo_big.png
├── material.min.css
├── material.min.js
└── roast_profile.png
├── scss
└── app.scss
├── src
├── actions.js
├── app.jsx
├── auth.js
├── components
│ ├── App.jsx
│ ├── Dialog.jsx
│ ├── Header.jsx
│ ├── Home.jsx
│ ├── Instructions.jsx
│ ├── LoginForm.jsx
│ ├── Main.jsx
│ ├── NewRoastForm.jsx
│ ├── PostRoastNoteForm.jsx
│ ├── RoastChart.jsx
│ ├── RoastList.jsx
│ ├── RoastPointInput.jsx
│ ├── RoastPointsList.jsx
│ ├── RoastProfile.jsx
│ ├── Settings.jsx
│ ├── Spinner.jsx
│ ├── StopWatch.jsx
│ ├── UnitSwitcher.jsx
│ └── utils
│ │ ├── Button.jsx
│ │ ├── Card.jsx
│ │ ├── CardAction.jsx
│ │ ├── CardContent.jsx
│ │ ├── CardTitle.jsx
│ │ ├── NavigationLink.jsx
│ │ ├── Radio.jsx
│ │ └── Table.jsx
├── constants.js
├── containers
│ ├── AppContainer.js
│ ├── DialogContainer.js
│ ├── DrawerContainer.js
│ ├── HeaderContainer.js
│ ├── LoginFormContainer.js
│ ├── MainContainer.js
│ ├── NewRoastFormContainer.js
│ ├── PostRoastNoteFormContainer.js
│ ├── RoastListContainer.js
│ ├── RoastPointInputContainer.js
│ ├── RoastPointsListContainer.js
│ ├── RoastProfileContainer.js
│ ├── SettingsContainer.js
│ ├── StopWatchContainer.js
│ └── UnitSwitcherContainer.js
├── demoData.js
├── helpers.js
├── history.js
└── reducers
│ ├── authReducer.js
│ ├── dataLoadingReducer.js
│ ├── dialogReducer.js
│ ├── editingFieldReducer.js
│ ├── index.js
│ ├── newRoastReducer.js
│ ├── roastInProgressReducer.js
│ ├── roastsReducer.js
│ ├── settingsReducer.js
│ └── stopWatchReducer.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": ["transform-object-rest-spread"]
4 | }
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "bobonroast"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | firebase-debug.log
4 |
5 | bower_components
6 | node_modules
7 |
8 | public/bundle.js
9 | public/common.js
10 | public/bundle.js.map
11 | public/common.js.map
12 |
13 | */npm-debug.log
14 | */**/npm-debug.log
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The GNU General Public License, Version 2, June 1991 (GPLv2)
2 | ============================================================
3 |
4 | > Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5 | > 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
6 |
7 | Everyone is permitted to copy and distribute verbatim copies of this license
8 | document, but changing it is not allowed.
9 |
10 |
11 | Preamble
12 | --------
13 |
14 | The licenses for most software are designed to take away your freedom to share
15 | and change it. By contrast, the GNU General Public License is intended to
16 | guarantee your freedom to share and change free software--to make sure the
17 | software is free for all its users. This General Public License applies to most
18 | of the Free Software Foundation's software and to any other program whose
19 | authors commit to using it. (Some other Free Software Foundation software is
20 | covered by the GNU Lesser General Public License instead.) You can apply it to
21 | your programs, too.
22 |
23 | When we speak of free software, we are referring to freedom, not price. Our
24 | General Public Licenses are designed to make sure that you have the freedom to
25 | distribute copies of free software (and charge for this service if you wish),
26 | that you receive source code or can get it if you want it, that you can change
27 | the software or use pieces of it in new free programs; and that you know you can
28 | do these things.
29 |
30 | To protect your rights, we need to make restrictions that forbid anyone to deny
31 | you these rights or to ask you to surrender the rights. These restrictions
32 | translate to certain responsibilities for you if you distribute copies of the
33 | software, or if you modify it.
34 |
35 | For example, if you distribute copies of such a program, whether gratis or for a
36 | fee, you must give the recipients all the rights that you have. You must make
37 | sure that they, too, receive or can get the source code. And you must show them
38 | these terms so they know their rights.
39 |
40 | We protect your rights with two steps: (1) copyright the software, and (2) offer
41 | you this license which gives you legal permission to copy, distribute and/or
42 | modify the software.
43 |
44 | Also, for each author's protection and ours, we want to make certain that
45 | everyone understands that there is no warranty for this free software. If the
46 | software is modified by someone else and passed on, we want its recipients to
47 | know that what they have is not the original, so that any problems introduced by
48 | others will not reflect on the original authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software patents. We wish
51 | to avoid the danger that redistributors of a free program will individually
52 | obtain patent licenses, in effect making the program proprietary. To prevent
53 | this, we have made it clear that any patent must be licensed for everyone's free
54 | use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and modification
57 | follow.
58 |
59 |
60 | Terms And Conditions For Copying, Distribution And Modification
61 | ---------------------------------------------------------------
62 |
63 | **0.** This License applies to any program or other work which contains a notice
64 | placed by the copyright holder saying it may be distributed under the terms of
65 | this General Public License. The "Program", below, refers to any such program or
66 | work, and a "work based on the Program" means either the Program or any
67 | derivative work under copyright law: that is to say, a work containing the
68 | Program or a portion of it, either verbatim or with modifications and/or
69 | translated into another language. (Hereinafter, translation is included without
70 | limitation in the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not covered by
73 | this License; they are outside its scope. The act of running the Program is not
74 | restricted, and the output from the Program is covered only if its contents
75 | constitute a work based on the Program (independent of having been made by
76 | running the Program). Whether that is true depends on what the Program does.
77 |
78 | **1.** You may copy and distribute verbatim copies of the Program's source code
79 | as you receive it, in any medium, provided that you conspicuously and
80 | appropriately publish on each copy an appropriate copyright notice and
81 | disclaimer of warranty; keep intact all the notices that refer to this License
82 | and to the absence of any warranty; and give any other recipients of the Program
83 | a copy of this License along with the Program.
84 |
85 | You may charge a fee for the physical act of transferring a copy, and you may at
86 | your option offer warranty protection in exchange for a fee.
87 |
88 | **2.** You may modify your copy or copies of the Program or any portion of it,
89 | thus forming a work based on the Program, and copy and distribute such
90 | modifications or work under the terms of Section 1 above, provided that you also
91 | meet all of these conditions:
92 |
93 | * **a)** You must cause the modified files to carry prominent notices stating
94 | that you changed the files and the date of any change.
95 |
96 | * **b)** You must cause any work that you distribute or publish, that in whole
97 | or in part contains or is derived from the Program or any part thereof, to
98 | be licensed as a whole at no charge to all third parties under the terms of
99 | this License.
100 |
101 | * **c)** If the modified program normally reads commands interactively when
102 | run, you must cause it, when started running for such interactive use in the
103 | most ordinary way, to print or display an announcement including an
104 | appropriate copyright notice and a notice that there is no warranty (or
105 | else, saying that you provide a warranty) and that users may redistribute
106 | the program under these conditions, and telling the user how to view a copy
107 | of this License. (Exception: if the Program itself is interactive but does
108 | not normally print such an announcement, your work based on the Program is
109 | not required to print an announcement.)
110 |
111 | These requirements apply to the modified work as a whole. If identifiable
112 | sections of that work are not derived from the Program, and can be reasonably
113 | considered independent and separate works in themselves, then this License, and
114 | its terms, do not apply to those sections when you distribute them as separate
115 | works. But when you distribute the same sections as part of a whole which is a
116 | work based on the Program, the distribution of the whole must be on the terms of
117 | this License, whose permissions for other licensees extend to the entire whole,
118 | and thus to each and every part regardless of who wrote it.
119 |
120 | Thus, it is not the intent of this section to claim rights or contest your
121 | rights to work written entirely by you; rather, the intent is to exercise the
122 | right to control the distribution of derivative or collective works based on the
123 | Program.
124 |
125 | In addition, mere aggregation of another work not based on the Program with the
126 | Program (or with a work based on the Program) on a volume of a storage or
127 | distribution medium does not bring the other work under the scope of this
128 | License.
129 |
130 | **3.** You may copy and distribute the Program (or a work based on it, under
131 | Section 2) in object code or executable form under the terms of Sections 1 and 2
132 | above provided that you also do one of the following:
133 |
134 | * **a)** Accompany it with the complete corresponding machine-readable source
135 | code, which must be distributed under the terms of Sections 1 and 2 above on
136 | a medium customarily used for software interchange; or,
137 |
138 | * **b)** Accompany it with a written offer, valid for at least three years, to
139 | give any third party, for a charge no more than your cost of physically
140 | performing source distribution, a complete machine-readable copy of the
141 | corresponding source code, to be distributed under the terms of Sections 1
142 | and 2 above on a medium customarily used for software interchange; or,
143 |
144 | * **c)** Accompany it with the information you received as to the offer to
145 | distribute corresponding source code. (This alternative is allowed only for
146 | noncommercial distribution and only if you received the program in object
147 | code or executable form with such an offer, in accord with Subsection b
148 | above.)
149 |
150 | The source code for a work means the preferred form of the work for making
151 | modifications to it. For an executable work, complete source code means all the
152 | source code for all modules it contains, plus any associated interface
153 | definition files, plus the scripts used to control compilation and installation
154 | of the executable. However, as a special exception, the source code distributed
155 | need not include anything that is normally distributed (in either source or
156 | binary form) with the major components (compiler, kernel, and so on) of the
157 | operating system on which the executable runs, unless that component itself
158 | accompanies the executable.
159 |
160 | If distribution of executable or object code is made by offering access to copy
161 | from a designated place, then offering equivalent access to copy the source code
162 | from the same place counts as distribution of the source code, even though third
163 | parties are not compelled to copy the source along with the object code.
164 |
165 | **4.** You may not copy, modify, sublicense, or distribute the Program except as
166 | expressly provided under this License. Any attempt otherwise to copy, modify,
167 | sublicense or distribute the Program is void, and will automatically terminate
168 | your rights under this License. However, parties who have received copies, or
169 | rights, from you under this License will not have their licenses terminated so
170 | long as such parties remain in full compliance.
171 |
172 | **5.** You are not required to accept this License, since you have not signed
173 | it. However, nothing else grants you permission to modify or distribute the
174 | Program or its derivative works. These actions are prohibited by law if you do
175 | not accept this License. Therefore, by modifying or distributing the Program (or
176 | any work based on the Program), you indicate your acceptance of this License to
177 | do so, and all its terms and conditions for copying, distributing or modifying
178 | the Program or works based on it.
179 |
180 | **6.** Each time you redistribute the Program (or any work based on the
181 | Program), the recipient automatically receives a license from the original
182 | licensor to copy, distribute or modify the Program subject to these terms and
183 | conditions. You may not impose any further restrictions on the recipients'
184 | exercise of the rights granted herein. You are not responsible for enforcing
185 | compliance by third parties to this License.
186 |
187 | **7.** If, as a consequence of a court judgment or allegation of patent
188 | infringement or for any other reason (not limited to patent issues), conditions
189 | are imposed on you (whether by court order, agreement or otherwise) that
190 | contradict the conditions of this License, they do not excuse you from the
191 | conditions of this License. If you cannot distribute so as to satisfy
192 | simultaneously your obligations under this License and any other pertinent
193 | obligations, then as a consequence you may not distribute the Program at all.
194 | For example, if a patent license would not permit royalty-free redistribution of
195 | the Program by all those who receive copies directly or indirectly through you,
196 | then the only way you could satisfy both it and this License would be to refrain
197 | entirely from distribution of the Program.
198 |
199 | If any portion of this section is held invalid or unenforceable under any
200 | particular circumstance, the balance of the section is intended to apply and the
201 | section as a whole is intended to apply in other circumstances.
202 |
203 | It is not the purpose of this section to induce you to infringe any patents or
204 | other property right claims or to contest validity of any such claims; this
205 | section has the sole purpose of protecting the integrity of the free software
206 | distribution system, which is implemented by public license practices. Many
207 | people have made generous contributions to the wide range of software
208 | distributed through that system in reliance on consistent application of that
209 | system; it is up to the author/donor to decide if he or she is willing to
210 | distribute software through any other system and a licensee cannot impose that
211 | choice.
212 |
213 | This section is intended to make thoroughly clear what is believed to be a
214 | consequence of the rest of this License.
215 |
216 | **8.** If the distribution and/or use of the Program is restricted in certain
217 | countries either by patents or by copyrighted interfaces, the original copyright
218 | holder who places the Program under this License may add an explicit
219 | geographical distribution limitation excluding those countries, so that
220 | distribution is permitted only in or among countries not thus excluded. In such
221 | case, this License incorporates the limitation as if written in the body of this
222 | License.
223 |
224 | **9.** The Free Software Foundation may publish revised and/or new versions of
225 | the General Public License from time to time. Such new versions will be similar
226 | in spirit to the present version, but may differ in detail to address new
227 | problems or concerns.
228 |
229 | Each version is given a distinguishing version number. If the Program specifies
230 | a version number of this License which applies to it and "any later version",
231 | you have the option of following the terms and conditions either of that version
232 | or of any later version published by the Free Software Foundation. If the
233 | Program does not specify a version number of this License, you may choose any
234 | version ever published by the Free Software Foundation.
235 |
236 | **10.** If you wish to incorporate parts of the Program into other free programs
237 | whose distribution conditions are different, write to the author to ask for
238 | permission. For software which is copyrighted by the Free Software Foundation,
239 | write to the Free Software Foundation; we sometimes make exceptions for this.
240 | Our decision will be guided by the two goals of preserving the free status of
241 | all derivatives of our free software and of promoting the sharing and reuse of
242 | software generally.
243 |
244 |
245 | No Warranty
246 | -----------
247 |
248 | **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
249 | THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
250 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
251 | "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
252 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
253 | PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
254 | PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
255 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
256 |
257 | **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
258 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
259 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
260 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
261 | INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
262 | BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
263 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
264 | OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
265 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bobonroast
2 |
3 | > A place to record and keep track of your coffee roast profiles in real-time.
4 |
5 | This is the codebase for [Bobon Profiles](https://roast.bobon.coffee)
6 |
7 | ## Prerequisite
8 |
9 | Bobon Profiles is a React - Redux app built on top of a Firebase database.
10 |
11 | ### If you are developing a separate project from this (using a different database)
12 |
13 | Create your own Firebase project. After that, update FIREBASE constant in `src/constants.js` with your own info.
14 |
15 | Enable Google and Facebook authentication for the Firebase App.
16 |
17 | ## Development
18 |
19 | ```sh
20 | $ npm install
21 | $ npm run watch
22 | ```
23 |
24 | After that, access the site at https://localhost:8080 (IMPORTANT: HTTPS, not HTTP - so we can use Firebase redirect authentication with Facebook and Google from the development site)
25 |
26 | ## Contributing
27 |
28 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](https://github.com/nathandao/bobonroastprofile/issues)
29 |
30 | ## Author
31 |
32 | **Nathan Dao**
33 |
34 | * [github/nathandao](https://github.com/nathandao)
35 | * [twitter/nathan_dao](http://twitter.com/nathan_dao)
36 |
37 | ## License
38 |
39 | Bobon Profiles is distributed under GPLv2 license. TLDR:
40 |
41 | You MUST:
42 |
43 | - Include the original code and where to get Bobon Roast
44 | - Opensource your project
45 | - Include Bobon Roast's copyright
46 | - State changes made to Bobon Roast
47 | - Include the GPLv2 lincense in your source code
48 |
49 | You can:
50 |
51 | - Commercial use
52 | - Modify
53 | - Distribute
54 | - Place Warranty
55 |
56 | You cannot:
57 |
58 | - Sublicense
59 | - Hold liable
60 |
61 |
62 | Copyright © 2016 [Nathan Dao](https://guynathan.com)
63 | Licensed under the GPLv2 license.
64 |
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {"rules":
2 | {
3 | "roasts": {
4 | "$uid": {
5 | ".write": "auth !== null",
6 | ".read": "$uid === auth.uid",
7 | "$roast": {
8 | ".read": "data.child('uid').val() === auth.uid",
9 | ".write": "data.child('uid').val() === auth.uid"
10 | }
11 | }
12 | },
13 |
14 | "settings": {
15 | "$uid": {
16 | ".write": "$uid === auth.uid",
17 | ".read": "$uid === auth.uid"
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "hosting": {
6 | "public": "public",
7 | "rewrites": [
8 | {
9 | "source": "**",
10 | "destination": "/index.html"
11 | }
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/logo.png
--------------------------------------------------------------------------------
/images/logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/logo_small.png
--------------------------------------------------------------------------------
/images/logo_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/logo_white.png
--------------------------------------------------------------------------------
/images/logo_white_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/logo_white_small.png
--------------------------------------------------------------------------------
/images/step_create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/step_create.png
--------------------------------------------------------------------------------
/images/step_input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/step_input.png
--------------------------------------------------------------------------------
/images/step_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/step_stop.png
--------------------------------------------------------------------------------
/images/step_timer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/images/step_timer.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bobonroast",
3 | "version": "0.1.0",
4 | "description": "A place to record and keep track of your coffee roast profiles",
5 | "main": "index.js",
6 | "bin": {
7 | "webpack-dev-server": "./webpack-dev-server.js",
8 | "webpack": "./webpack.js"
9 | },
10 | "scripts": {
11 | "watch": "webpack-dev-server --https --hot --inline --progress --colors --content-base public",
12 | "build": "webpack --progress --colors -p",
13 | "deploy": "webpack --progress --colors -p && firebase deploy"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/project-bobon/bobonroastprofile.git"
18 | },
19 | "author": "Nathan Dao",
20 | "license": "GPLv2",
21 | "bugs": {
22 | "url": "https://github.com/project-bobon/bobonroastprofile/issues"
23 | },
24 | "homepage": "https://github.com/project-bobon/bobonroastprofile#readme",
25 | "devDependencies": {
26 | "autoprefixer-loader": "^3.2.0",
27 | "babel-core": "^6.9.1",
28 | "babel-loader": "^6.2.4",
29 | "babel-plugin-transform-object-rest-spread": "^6.8.0",
30 | "babel-preset-es2015": "^6.9.0",
31 | "babel-preset-react": "^6.5.0",
32 | "chart.js": "^2.1.4",
33 | "css-loader": "^0.23.1",
34 | "file-loader": "^0.8.5",
35 | "firebase": "^3.0.3",
36 | "html-webpack-plugin": "^2.19.0",
37 | "moment": "^2.13.0",
38 | "node-sass": "^3.7.0",
39 | "react": "^15.1.0",
40 | "react-addons-css-transition-group": "^15.1.0",
41 | "react-addons-transition-group": "^15.1.0",
42 | "react-chartist": "^0.10.2",
43 | "react-chartjs": "git://github.com/malloc-fi/react-chartjs.git",
44 | "react-dom": "^15.1.0",
45 | "react-ga": "^2.1.0",
46 | "react-redux": "^4.4.5",
47 | "react-router": "^2.4.1",
48 | "redux": "^3.5.2",
49 | "redux-thunk": "^2.1.0",
50 | "sass-loader": "^3.2.0",
51 | "source-map-loader": "^0.1.5",
52 | "style-loader": "^0.13.1",
53 | "url-loader": "^0.5.7",
54 | "webpack": "^1.13.1",
55 | "webpack-dev-server": "^1.14.1"
56 | },
57 | "dependencies": {}
58 | }
59 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/public/favicon.png
--------------------------------------------------------------------------------
/public/fonts.css:
--------------------------------------------------------------------------------
1 | /* cyrillic-ext */
2 | @font-face {
3 | font-family: 'Roboto';
4 | font-style: normal;
5 | font-weight: 100;
6 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/ty9dfvLAziwdqQ2dHoyjphkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
7 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
8 | }
9 | /* cyrillic */
10 | @font-face {
11 | font-family: 'Roboto';
12 | font-style: normal;
13 | font-weight: 100;
14 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/frNV30OaYdlFRtH2VnZZdhkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
16 | }
17 | /* greek-ext */
18 | @font-face {
19 | font-family: 'Roboto';
20 | font-style: normal;
21 | font-weight: 100;
22 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/gwVJDERN2Amz39wrSoZ7FxkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
23 | unicode-range: U+1F00-1FFF;
24 | }
25 | /* greek */
26 | @font-face {
27 | font-family: 'Roboto';
28 | font-style: normal;
29 | font-weight: 100;
30 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/aZMswpodYeVhtRvuABJWvBkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
31 | unicode-range: U+0370-03FF;
32 | }
33 | /* vietnamese */
34 | @font-face {
35 | font-family: 'Roboto';
36 | font-style: normal;
37 | font-weight: 100;
38 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/VvXUGKZXbHtX_S_VCTLpGhkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
39 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
40 | }
41 | /* latin-ext */
42 | @font-face {
43 | font-family: 'Roboto';
44 | font-style: normal;
45 | font-weight: 100;
46 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/e7MeVAyvogMqFwwl61PKhBkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
47 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
48 | }
49 | /* latin */
50 | @font-face {
51 | font-family: 'Roboto';
52 | font-style: normal;
53 | font-weight: 100;
54 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/2tsd397wLxj96qwHyNIkxHYhjbSpvc47ee6xR_80Hnw.woff2) format('woff2');
55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
56 | }
57 | /* cyrillic-ext */
58 | @font-face {
59 | font-family: 'Roboto';
60 | font-style: normal;
61 | font-weight: 300;
62 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/0eC6fl06luXEYWpBSJvXCIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
63 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
64 | }
65 | /* cyrillic */
66 | @font-face {
67 | font-family: 'Roboto';
68 | font-style: normal;
69 | font-weight: 300;
70 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Fl4y0QdOxyyTHEGMXX8kcYX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
72 | }
73 | /* greek-ext */
74 | @font-face {
75 | font-family: 'Roboto';
76 | font-style: normal;
77 | font-weight: 300;
78 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/-L14Jk06m6pUHB-5mXQQnYX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
79 | unicode-range: U+1F00-1FFF;
80 | }
81 | /* greek */
82 | @font-face {
83 | font-family: 'Roboto';
84 | font-style: normal;
85 | font-weight: 300;
86 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/I3S1wsgSg9YCurV6PUkTOYX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
87 | unicode-range: U+0370-03FF;
88 | }
89 | /* vietnamese */
90 | @font-face {
91 | font-family: 'Roboto';
92 | font-style: normal;
93 | font-weight: 300;
94 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/NYDWBdD4gIq26G5XYbHsFIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
95 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
96 | }
97 | /* latin-ext */
98 | @font-face {
99 | font-family: 'Roboto';
100 | font-style: normal;
101 | font-weight: 300;
102 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Pru33qjShpZSmG3z6VYwnYX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
103 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
104 | }
105 | /* latin */
106 | @font-face {
107 | font-family: 'Roboto';
108 | font-style: normal;
109 | font-weight: 300;
110 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Hgo13k-tfSpn0qi1SFdUfZBw1xU1rKptJj_0jans920.woff2) format('woff2');
111 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
112 | }
113 | /* cyrillic-ext */
114 | @font-face {
115 | font-family: 'Roboto';
116 | font-style: normal;
117 | font-weight: 400;
118 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/sTdaA6j0Psb920Vjv-mrzH-_kf6ByYO6CLYdB4HQE-Y.woff2) format('woff2');
119 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
120 | }
121 | /* cyrillic */
122 | @font-face {
123 | font-family: 'Roboto';
124 | font-style: normal;
125 | font-weight: 400;
126 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/uYECMKoHcO9x1wdmbyHIm3-_kf6ByYO6CLYdB4HQE-Y.woff2) format('woff2');
127 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
128 | }
129 | /* greek-ext */
130 | @font-face {
131 | font-family: 'Roboto';
132 | font-style: normal;
133 | font-weight: 400;
134 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/tnj4SB6DNbdaQnsM8CFqBX-_kf6ByYO6CLYdB4HQE-Y.woff2) format('woff2');
135 | unicode-range: U+1F00-1FFF;
136 | }
137 | /* greek */
138 | @font-face {
139 | font-family: 'Roboto';
140 | font-style: normal;
141 | font-weight: 400;
142 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/_VYFx-s824kXq_Ul2BHqYH-_kf6ByYO6CLYdB4HQE-Y.woff2) format('woff2');
143 | unicode-range: U+0370-03FF;
144 | }
145 | /* vietnamese */
146 | @font-face {
147 | font-family: 'Roboto';
148 | font-style: normal;
149 | font-weight: 400;
150 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/NJ4vxlgWwWbEsv18dAhqnn-_kf6ByYO6CLYdB4HQE-Y.woff2) format('woff2');
151 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
152 | }
153 | /* latin-ext */
154 | @font-face {
155 | font-family: 'Roboto';
156 | font-style: normal;
157 | font-weight: 400;
158 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/Ks_cVxiCiwUWVsFWFA3Bjn-_kf6ByYO6CLYdB4HQE-Y.woff2) format('woff2');
159 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
160 | }
161 | /* latin */
162 | @font-face {
163 | font-family: 'Roboto';
164 | font-style: normal;
165 | font-weight: 400;
166 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/oMMgfZMQthOryQo9n22dcuvvDin1pK8aKteLpeZ5c0A.woff2) format('woff2');
167 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
168 | }
169 | /* cyrillic-ext */
170 | @font-face {
171 | font-family: 'Roboto';
172 | font-style: normal;
173 | font-weight: 500;
174 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/ZLqKeelYbATG60EpZBSDy4X0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
175 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
176 | }
177 | /* cyrillic */
178 | @font-face {
179 | font-family: 'Roboto';
180 | font-style: normal;
181 | font-weight: 500;
182 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/oHi30kwQWvpCWqAhzHcCSIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
183 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
184 | }
185 | /* greek-ext */
186 | @font-face {
187 | font-family: 'Roboto';
188 | font-style: normal;
189 | font-weight: 500;
190 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/rGvHdJnr2l75qb0YND9NyIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
191 | unicode-range: U+1F00-1FFF;
192 | }
193 | /* greek */
194 | @font-face {
195 | font-family: 'Roboto';
196 | font-style: normal;
197 | font-weight: 500;
198 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/mx9Uck6uB63VIKFYnEMXrYX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
199 | unicode-range: U+0370-03FF;
200 | }
201 | /* vietnamese */
202 | @font-face {
203 | font-family: 'Roboto';
204 | font-style: normal;
205 | font-weight: 500;
206 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/mbmhprMH69Zi6eEPBYVFhYX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
207 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
208 | }
209 | /* latin-ext */
210 | @font-face {
211 | font-family: 'Roboto';
212 | font-style: normal;
213 | font-weight: 500;
214 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/oOeFwZNlrTefzLYmlVV1UIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
215 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
216 | }
217 | /* latin */
218 | @font-face {
219 | font-family: 'Roboto';
220 | font-style: normal;
221 | font-weight: 500;
222 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/RxZJdnzeo3R5zSexge8UUZBw1xU1rKptJj_0jans920.woff2) format('woff2');
223 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
224 | }
225 | /* cyrillic-ext */
226 | @font-face {
227 | font-family: 'Roboto';
228 | font-style: normal;
229 | font-weight: 700;
230 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/77FXFjRbGzN4aCrSFhlh3oX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
231 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
232 | }
233 | /* cyrillic */
234 | @font-face {
235 | font-family: 'Roboto';
236 | font-style: normal;
237 | font-weight: 700;
238 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/isZ-wbCXNKAbnjo6_TwHToX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
239 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
240 | }
241 | /* greek-ext */
242 | @font-face {
243 | font-family: 'Roboto';
244 | font-style: normal;
245 | font-weight: 700;
246 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/UX6i4JxQDm3fVTc1CPuwqoX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
247 | unicode-range: U+1F00-1FFF;
248 | }
249 | /* greek */
250 | @font-face {
251 | font-family: 'Roboto';
252 | font-style: normal;
253 | font-weight: 700;
254 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/jSN2CGVDbcVyCnfJfjSdfIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
255 | unicode-range: U+0370-03FF;
256 | }
257 | /* vietnamese */
258 | @font-face {
259 | font-family: 'Roboto';
260 | font-style: normal;
261 | font-weight: 700;
262 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/PwZc-YbIL414wB9rB1IAPYX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
263 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
264 | }
265 | /* latin-ext */
266 | @font-face {
267 | font-family: 'Roboto';
268 | font-style: normal;
269 | font-weight: 700;
270 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/97uahxiqZRoncBaCEI3aW4X0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
271 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
272 | }
273 | /* latin */
274 | @font-face {
275 | font-family: 'Roboto';
276 | font-style: normal;
277 | font-weight: 700;
278 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/d-6IYplOFocCacKzxwXSOJBw1xU1rKptJj_0jans920.woff2) format('woff2');
279 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
280 | }
281 | /* cyrillic-ext */
282 | @font-face {
283 | font-family: 'Roboto';
284 | font-style: normal;
285 | font-weight: 900;
286 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v15/s7gftie1JANC-QmDJvMWZoX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
287 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
288 | }
289 | /* cyrillic */
290 | @font-face {
291 | font-family: 'Roboto';
292 | font-style: normal;
293 | font-weight: 900;
294 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v15/3Y_xCyt7TNunMGg0Et2pnoX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
295 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
296 | }
297 | /* greek-ext */
298 | @font-face {
299 | font-family: 'Roboto';
300 | font-style: normal;
301 | font-weight: 900;
302 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v15/WeQRRE07FDkIrr29oHQgHIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
303 | unicode-range: U+1F00-1FFF;
304 | }
305 | /* greek */
306 | @font-face {
307 | font-family: 'Roboto';
308 | font-style: normal;
309 | font-weight: 900;
310 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v15/jyIYROCkJM3gZ4KV00YXOIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
311 | unicode-range: U+0370-03FF;
312 | }
313 | /* vietnamese */
314 | @font-face {
315 | font-family: 'Roboto';
316 | font-style: normal;
317 | font-weight: 900;
318 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v15/phsu-QZXz1JBv0PbFoPmEIX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
319 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
320 | }
321 | /* latin-ext */
322 | @font-face {
323 | font-family: 'Roboto';
324 | font-style: normal;
325 | font-weight: 900;
326 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v15/9_7S_tWeGDh5Pq3u05RVkoX0hVgzZQUfRDuZrPvH3D8.woff2) format('woff2');
327 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
328 | }
329 | /* latin */
330 | @font-face {
331 | font-family: 'Roboto';
332 | font-style: normal;
333 | font-weight: 900;
334 | src: local('Roboto Black'), local('Roboto-Black'), url(https://fonts.gstatic.com/s/roboto/v15/mnpfi9pxYH-Go5UiibESIpBw1xU1rKptJj_0jans920.woff2) format('woff2');
335 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
336 | }
337 | /* cyrillic-ext */
338 | @font-face {
339 | font-family: 'Roboto';
340 | font-style: italic;
341 | font-weight: 400;
342 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://fonts.gstatic.com/s/roboto/v15/WxrXJa0C3KdtC7lMafG4dRkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
343 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
344 | }
345 | /* cyrillic */
346 | @font-face {
347 | font-family: 'Roboto';
348 | font-style: italic;
349 | font-weight: 400;
350 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://fonts.gstatic.com/s/roboto/v15/OpXUqTo0UgQQhGj_SFdLWBkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
351 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
352 | }
353 | /* greek-ext */
354 | @font-face {
355 | font-family: 'Roboto';
356 | font-style: italic;
357 | font-weight: 400;
358 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://fonts.gstatic.com/s/roboto/v15/1hZf02POANh32k2VkgEoUBkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
359 | unicode-range: U+1F00-1FFF;
360 | }
361 | /* greek */
362 | @font-face {
363 | font-family: 'Roboto';
364 | font-style: italic;
365 | font-weight: 400;
366 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://fonts.gstatic.com/s/roboto/v15/cDKhRaXnQTOVbaoxwdOr9xkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
367 | unicode-range: U+0370-03FF;
368 | }
369 | /* vietnamese */
370 | @font-face {
371 | font-family: 'Roboto';
372 | font-style: italic;
373 | font-weight: 400;
374 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://fonts.gstatic.com/s/roboto/v15/K23cxWVTrIFD6DJsEVi07RkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
375 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
376 | }
377 | /* latin-ext */
378 | @font-face {
379 | font-family: 'Roboto';
380 | font-style: italic;
381 | font-weight: 400;
382 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://fonts.gstatic.com/s/roboto/v15/vSzulfKSK0LLjjfeaxcREhkAz4rYn47Zy2rvigWQf6w.woff2) format('woff2');
383 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
384 | }
385 | /* latin */
386 | @font-face {
387 | font-family: 'Roboto';
388 | font-style: italic;
389 | font-weight: 400;
390 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://fonts.gstatic.com/s/roboto/v15/vPcynSL0qHq_6dX7lKVByXYhjbSpvc47ee6xR_80Hnw.woff2) format('woff2');
391 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
392 | }
393 | /* cyrillic-ext */
394 | @font-face {
395 | font-family: 'Roboto';
396 | font-style: italic;
397 | font-weight: 700;
398 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcC_ZraR2Tg8w2lzm7kLNL0-w.woff2) format('woff2');
399 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
400 | }
401 | /* cyrillic */
402 | @font-face {
403 | font-family: 'Roboto';
404 | font-style: italic;
405 | font-weight: 700;
406 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcC14sYYdJg5dU2qzJEVSuta0.woff2) format('woff2');
407 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
408 | }
409 | /* greek-ext */
410 | @font-face {
411 | font-family: 'Roboto';
412 | font-style: italic;
413 | font-weight: 700;
414 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcC1BW26QxpSj-_ZKm_xT4hWw.woff2) format('woff2');
415 | unicode-range: U+1F00-1FFF;
416 | }
417 | /* greek */
418 | @font-face {
419 | font-family: 'Roboto';
420 | font-style: italic;
421 | font-weight: 700;
422 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcCwt_Rm691LTebKfY2ZkKSmI.woff2) format('woff2');
423 | unicode-range: U+0370-03FF;
424 | }
425 | /* vietnamese */
426 | @font-face {
427 | font-family: 'Roboto';
428 | font-style: italic;
429 | font-weight: 700;
430 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcC9DiNsR5a-9Oe_Ivpu8XWlY.woff2) format('woff2');
431 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
432 | }
433 | /* latin-ext */
434 | @font-face {
435 | font-family: 'Roboto';
436 | font-style: italic;
437 | font-weight: 700;
438 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcC6E8kM4xWR1_1bYURRojRGc.woff2) format('woff2');
439 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
440 | }
441 | /* latin */
442 | @font-face {
443 | font-family: 'Roboto';
444 | font-style: italic;
445 | font-weight: 700;
446 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://fonts.gstatic.com/s/roboto/v15/t6Nd4cfPRhZP44Q5QAjcC4gp9Q8gbYrhqGlRav_IXfk.woff2) format('woff2');
447 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
448 | }
449 |
450 | /* fallback */
451 | @font-face {
452 | font-family: 'Material Icons';
453 | font-style: normal;
454 | font-weight: 400;
455 | src: local('Material Icons'), local('MaterialIcons-Regular'), url(https://fonts.gstatic.com/s/materialicons/v17/2fcrYFNaTjcS6g4U3t-Y5UEw0lE80llgEseQY3FEmqw.woff2) format('woff2');
456 | }
457 |
458 | .material-icons {
459 | font-family: 'Material Icons';
460 | font-weight: normal;
461 | font-style: normal;
462 | font-size: 24px;
463 | line-height: 1;
464 | letter-spacing: normal;
465 | text-transform: none;
466 | display: inline-block;
467 | white-space: nowrap;
468 | word-wrap: normal;
469 | direction: ltr;
470 | -webkit-font-feature-settings: 'liga';
471 | -webkit-font-smoothing: antialiased;
472 | }
473 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Bobon Profiles
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/logo_big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/public/logo_big.png
--------------------------------------------------------------------------------
/public/roast_profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-bobon/bobonroastprofile/d70af19cdd0f49cfdc12ebed344a621890176cd5/public/roast_profile.png
--------------------------------------------------------------------------------
/scss/app.scss:
--------------------------------------------------------------------------------
1 | // Colors.
2 | $indigo-500: #3F51B5;
3 | $indigo-400: #5C6BC0;
4 | $indigo-900: #1A237E;
5 |
6 | $grey-50: #fafafa;
7 | $grey-100: #f5f5f5;
8 | $grey-200: #eeeeee;
9 | $grey-300: #e0e0e0;
10 | $grey-400: #bdbdbd;
11 | $grey-700: #616161;
12 | $grey-800: #424242;
13 | $grey-900: #212121;
14 |
15 | $red-500: #F44336;
16 | $red-800: #C62828;
17 | $red-900: #B71C1C;
18 |
19 | $teal-500: #009688;
20 | $teal-600: #00897B;
21 | $teal-800: #00695C;
22 | $teal-700: #00796B;
23 |
24 | $deep-orange-900: #BF360C;
25 |
26 | // Util classes.
27 | .bobon-util__full-width {
28 | width: 100%;
29 | }
30 |
31 | .bobon-util__half-width {
32 | width: 50%;
33 | float: left;
34 | }
35 |
36 | .bobon-radio {
37 | margin-right: 25px;
38 | }
39 |
40 | .mdl-card {
41 | background: none;
42 | background: $grey-50;
43 | border: 1px solid $grey-300;
44 | }
45 |
46 | .mdl-card__supporting-text {
47 | color: $grey-900;
48 | }
49 |
50 | .mdl-card__title {
51 | background: #009688;
52 | color: $grey-100;
53 | }
54 |
55 | .mdl-card {
56 | .mdl-card {
57 | margin: 0px;
58 | }
59 | }
60 |
61 | .mdl-card {
62 | min-height: 100px;
63 | }
64 |
65 | .mdl-cell {
66 | min-height: 0;
67 | }
68 |
69 | .material-icons {
70 | margin-right: 10px;
71 | }
72 |
73 | // Layout.
74 | body {
75 | background: $grey-200;
76 | color: $grey-700;
77 | min-height: 100%;
78 | }
79 |
80 | plaintext {
81 | white-space: pre-wrap;
82 | font-family: 'Roboto', 'Helvetica', "Arial";
83 | }
84 |
85 | .bobon-logo {
86 | color: #fff;
87 | font-weight: 200;
88 | width: 60px;
89 | font-size: 18px;
90 | }
91 |
92 | .bobon-user-avatar {
93 | width: 50px;
94 | height: 50px;
95 | min-width: 50px;
96 | border-radius: 50%;
97 | overflow: hidden;
98 | box-sizing: border-box;
99 | line-height: 50px !important;
100 | padding: 0px !important;
101 | text-align: center;
102 | position: relative;
103 | margin-left: 10px;
104 |
105 | &:focus {
106 | outline: none;
107 | }
108 |
109 | img {
110 | position: absolute;
111 | top: 0px;
112 | left: 0px;
113 | width: 100%;
114 | }
115 |
116 | .material-icons {
117 | font-size: 50px;
118 | margin: 0px;
119 | }
120 | }
121 |
122 | .bobon-menu-user-menu {
123 | background: $teal-600;
124 |
125 | li {
126 | color: $grey-100;
127 |
128 | &:hover {
129 | background: $teal-500;
130 | }
131 | }
132 | }
133 |
134 | .bobon-unit-switcher-menu {
135 | button {
136 | color: $grey-100;
137 | background: $teal-600;
138 | border-radius: 18px;
139 |
140 | &:hover {
141 | background: $teal-800;
142 | }
143 | }
144 |
145 | .mdl-menu {
146 | background: $teal-600;
147 |
148 | .mdl-menu__item {
149 | color: $grey-100;
150 | &:hover {
151 | background: $teal-500;
152 | }
153 | }
154 | }
155 |
156 | }
157 |
158 | .bobon-page-content {
159 | padding: 0px;
160 | box-sizing: border-box;
161 | }
162 |
163 | .bobon-textfield-wrapper {
164 | padding: 0 10px;
165 | box-sizing: border-box;
166 | float: left;
167 |
168 | > .mdl-textfield {
169 | width: 100%;
170 | }
171 | }
172 |
173 | .bobon-main-content {
174 | width: 100%;
175 | margin: 0 auto;
176 | }
177 |
178 | .bobon-text-with-icon {
179 | position: relative;
180 | padding-left: 30px;
181 |
182 | .material-icons {
183 | position: absolute;
184 | top: 50%;
185 | margin-top: -12px;
186 | left: 0px;
187 | }
188 | }
189 |
190 | .bobon-roast-temp-form {
191 | width: 100%;
192 | position: relative;
193 | box-sizing: content-box;
194 | padding-right: 240px;
195 |
196 | input[type="submit"] {
197 | position: absolute;
198 | top: 0px;
199 | right: 0px;
200 | height: 50px;
201 | line-height: 50px;
202 | width: 200px;
203 | text-align: center;
204 | box-sizing: border-box;
205 | margin: 0;
206 | }
207 | }
208 |
209 | .bobon-roast-temp-input {
210 | width: 100%;
211 | border: none;
212 | box-shadow: none;
213 | border-bottom: 1px solid #e3e3e3;
214 | font-size: 3em;
215 | padding: 0;
216 | display: block;
217 | height: 50px;
218 | line-height: 50px;
219 | &:focus {
220 | outline-width: 0;
221 | }
222 | }
223 |
224 | @keyframes hot {
225 | 0% {
226 | color: #B71C1C;
227 | }
228 | 25% {
229 | color: #E64A19;
230 | }
231 | 50% {
232 | color: #FFEE58;
233 | }
234 | 100% {
235 | color: #E64A19;
236 | }
237 | }
238 |
239 | .bobon-roast-status--roast_pending {
240 | color: #FF6F00;
241 | }
242 |
243 |
244 | .bobon-roast-status--roast_in_progress {
245 | color: #B71C1C;
246 | min-width: 1px;
247 | width: 38px;
248 | padding: 0px;
249 | text-align: center;
250 |
251 | .material-icons {
252 | margin: 0px;
253 | animation: hot 1s ease-out infinite;
254 | }
255 | }
256 |
257 | .bobon-roast-status--roast_completed {
258 | color: #43A047;
259 | }
260 |
261 | header {
262 | .mdl-button {
263 | &.mdl-button--colored {
264 | &.bobon-roast-status--roast_in_progress {
265 | border-radius: 18px;
266 | color: #fff;
267 | background: $teal-600;
268 | margin-right: 15px;
269 |
270 | &:hover {
271 | background: $teal-800;
272 | }
273 | }
274 | }
275 | }
276 | }
277 |
278 | .bobon-section {
279 | width: 100%;
280 | clear: both;
281 |
282 | &.bobon-section-colored {
283 | background: rgb(33, 150, 243);
284 | color: #ebebeb;
285 | }
286 | }
287 |
288 | .bobon-chart-title {
289 | font-size: 1.2em;
290 | padding: 7px 0;
291 | color: $indigo-900;
292 | text-transform: uppercase;
293 | }
294 |
295 | .bobon-stopwatch {
296 | height: 50px;
297 | position: relative;
298 | padding-left: 130px;
299 | }
300 |
301 | .bobon-stopwatch-time {
302 | font-size: 3em;
303 | line-height: 50px;
304 | height: 50px;
305 | }
306 |
307 | .bobon-stopwatch-button {
308 | position: absolute;
309 | left: 0px;
310 | top: 0px;
311 | height: 100%;
312 | line-height: 50px;
313 | text-align: center;
314 | background: $deep-orange-900 !important;
315 | color: $grey-100 !important;
316 | }
317 |
318 | .bobon-header-button {
319 | position: fixed;
320 | top: 35px;
321 | z-index: 99;
322 | }
323 |
324 | .bobon-header-button-add-new {
325 | left: 16px;
326 | }
327 |
328 | .mdl-layout__content {
329 | padding-top: 20px;
330 | }
331 |
332 | .bobon-dialog {
333 | width: 400px;
334 | border: 0;
335 | z-index: 9999;
336 | overflow: visible;
337 | display: inline-block;
338 | }
339 |
340 | .bobon-dialog-container {
341 | display: table;
342 | position: fixed;
343 | width: 100%;
344 | height: 100%;
345 | top: 0px;
346 | left: 0px;
347 | z-index: 9999;
348 |
349 | &:before {
350 | content: "";
351 | display: block;
352 | background: rgba(0,0,0,0.7);
353 | position: fixed;
354 | top: 0px;
355 | left: 0px;
356 | width: 100%;
357 | height: 100%;
358 | z-index: 9;
359 | }
360 | }
361 |
362 | .bobon-dialog-cell {
363 | display: table-cell;
364 | vertical-align: middle;
365 | text-align: center;
366 |
367 | .mdl-card {
368 | text-align: left;
369 | }
370 |
371 | .mdl-card__title {
372 | font-size: 1.5em;
373 | font-weight: 200;
374 | padding-top: 30px;
375 | padding-bottom: 30px;
376 | }
377 | }
378 |
379 | .bobon-spinner {
380 | width: 50px;
381 | height: 50px;
382 | position: fixed;
383 | top: 50%;
384 | left: 50%;
385 | margin-top: -25px;
386 | margin-left: -25px;
387 | }
388 |
389 | .bobon-transition-enter {
390 | transform: translateY(30px);
391 | opacity: 0.01;
392 | transition: opacity 0.3s ease, transform 0.3s ease;
393 |
394 | &.bobon-transition-enter-active {
395 | transform: translateY(0);
396 | opacity: 1;
397 | }
398 | }
399 |
400 | .bobon-transition-leave {
401 | display: none;
402 |
403 | &.bobon-transition-leave-active {
404 | opacity: 0;
405 | }
406 |
407 | }
408 |
409 | .bobon-transition-appear {
410 | transform: translateY(30px);
411 | opacity: 0.01;
412 |
413 | &.bobon-transition-appear-active {
414 | transform: translateY(0);
415 | opacity: 1;
416 | transition: opacity 0.3s ease, transform 0.3s ease;
417 | }
418 | }
419 |
420 | .bobon-header-appear {
421 | transform: translateY(-30px);
422 | opacity: 0.01;
423 |
424 | &.bobon-header-appear-active {
425 | transform: translateY(0);
426 | opacity: 1;
427 | transition: opacity 0.3s ease, transform 0.3s ease;
428 | }
429 | }
430 |
431 |
432 | .bobon-table-cell--beans-name {
433 | white-space: normal;
434 | min-width: 150px;
435 | }
436 |
437 | .bobon-roast-profile-sidebar {
438 | padding-left: 0px;
439 | padding-right: 0px;
440 |
441 | > .mdl-cell {
442 | width: 100%;
443 | margin-left: 0px;
444 | margin-right: 0px;
445 | }
446 | }
447 |
448 | .mdl-grid {
449 | align-items: flex-start;
450 | }
451 |
452 | .bobon-login-form {
453 | position: fixed;
454 | width: 300px;
455 | left: 50%;
456 | margin-left: -150px;
457 | top: 50%;
458 | margin-top: -145px;
459 | border: 0;
460 | }
461 |
462 | .bobon-anon {
463 | position: fixed;
464 | top: 0px;
465 | left: 0px;
466 | width: 100%;
467 | height: 100%;
468 | background: #009688;
469 | }
470 |
471 |
472 | .bobon-instructions {
473 | text-align: center;
474 | padding: 0px;
475 |
476 | .mdl-cell {
477 | position: relative;
478 | padding-top: 180px;
479 | padding-bottom: 20px;
480 |
481 | img {
482 | max-width: 100%;
483 | }
484 |
485 | .mdl-typography--font-thin {
486 | position: absolute;
487 | top: 20px;
488 | left: 0px;
489 | width: 100%;
490 | box-sizing: border-box;
491 | padding: 0 25px;
492 | margin-bottom: 40px;
493 | color: #fff;
494 | }
495 | }
496 | }
497 |
498 | .bobon-home-anonymous {
499 | position: fixed;
500 | width: 100%;
501 | height: 100%;
502 | top: 0px;
503 | left: 0px;
504 |
505 | canvas {
506 | position: fixed;
507 | bottom: 0px;
508 | left: 50%;
509 | margin-left: -1200px;
510 | }
511 |
512 | .bobon-instructions {
513 | position: fixed;
514 | width: 100%;
515 | bottom: 80px;
516 | color: #fff;
517 | }
518 | }
519 |
520 | .bobon-footer {
521 | padding: 10px 20px;
522 | background: $teal-500;
523 | color: #fff;
524 | font-weight: 200;
525 | font-size: 0.9em;
526 | text-align: right;
527 | bottom: 0px;
528 | box-sizing: border-box;
529 | min-height: 40px;
530 |
531 | a {
532 | font-weight: 200;
533 | color: #fff;
534 | }
535 | }
536 |
537 | .bobon-form__new-roast {
538 | width: 100%;
539 | margin: 0;
540 | padding: 10px;
541 | box-sizing: border-box;
542 | float: left;
543 | }
544 |
545 | .bobon-list__compare-select {
546 | max-height: 200px;
547 | overflow-y: auto;
548 | }
549 |
550 | @media screen and (max-width: 1024px) {
551 | .mdl-layout__header {
552 | display: flex;
553 | }
554 |
555 | .mdl-layout {
556 | > span {
557 | z-index: 9999;
558 | }
559 | }
560 |
561 | .mdl-button-nav-start-roast {
562 | display: none;
563 | }
564 |
565 | .bobon-button-text--nav-action {
566 | display: none;
567 | }
568 |
569 | .bobon-util__half-width {
570 | width: 100% !important;
571 | }
572 |
573 | .bobon-header-button {
574 | top: 25px;
575 | }
576 |
577 | header {
578 | .mdl-navigation {
579 | .mdl-button {
580 | padding: 0 10px;
581 | min-width: 10px;
582 |
583 | .material-icons {
584 | margin-right: 0px;
585 | }
586 | }
587 | }
588 | }
589 | }
590 |
591 | @media screen and (max-width: 839px) {
592 | .mdl-grid {
593 | padding: 8px 0;
594 | }
595 |
596 | .bobon-table-cell--hidden-mobile {
597 | display: none;
598 | }
599 |
600 | .bobon-instructions {
601 | .mdl-cell {
602 | padding-top: 10px;
603 |
604 | .mdl-typography--font-thin {
605 | position: relative;
606 | }
607 | }
608 | }
609 | }
610 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase';
2 |
3 | import C from './constants';
4 | import history from './history';
5 |
6 | // Auth actions.
7 | export const listeningToAuth = () => {
8 | return {
9 | type: C.LISTENING_TO_AUTH
10 | };
11 | };
12 |
13 | export const loginRequest = (method = 'google', nextPath = '/') => {
14 | return {
15 | type: C.LOGIN_REQUEST,
16 | method,
17 | nextPath
18 | };
19 | };
20 |
21 | export const loginSuccess = (user, nextPath = '/') => {
22 | history.push(nextPath);
23 | return {
24 | type: C.LOGIN_SUCCESS,
25 | user,
26 | nextPath
27 | };
28 | };
29 |
30 | export const logout = (nextPath = '/') => {
31 | history.push(nextPath);
32 | return {
33 | type: C.LOGOUT,
34 | nextPath
35 | };
36 | };
37 |
38 | export const startListeningToAuth = () => {
39 | return function (dispatch) {
40 | dispatch(listeningToAuth());
41 | // Start listening to firebase auth changes.
42 | C.FIREBASE.auth().onAuthStateChanged(authData => {
43 | // If logged in.
44 | if (authData) {
45 | dispatch(loginSuccess(authData));
46 | } else {
47 | dispatch(logout());
48 | }
49 | });
50 | };
51 | };
52 |
53 | // Roasts actions.
54 | export const startListeningToRoasts = () => {
55 | if (C.FIREBASE.auth().currentUser) {
56 | const uid = C.FIREBASE.auth().currentUser.uid;
57 | return (dispatch, getState) => {
58 | let roastsRef = C.FIREBASE.app().database().ref('roasts').equalTo('uid', uid);
59 | roastsRef.on('value', snapshot => {
60 | console.log(snapshot.val());
61 | dispatch(fetchedRoasts(snapshot.val()));
62 | }, err => {
63 | console.log(err);
64 | });
65 | };
66 | } else {
67 | return (dispatch, getState) => {
68 | dispatch(logout());
69 | };
70 | }
71 | };
72 |
73 | export const fetchedRoasts = (roasts) => {
74 | return {
75 | type: C.FETCHED_ROASTS,
76 | roasts
77 | };
78 | };
79 |
80 | export const compareRoasts = (roastId, compareId) => {
81 | return {
82 | type: C.COMPARE_ROASTS,
83 | roastId,
84 | compareId
85 | };
86 | };
87 |
88 | export const addFirstCrack = (roastId, firstCrackTime) => {
89 | return {
90 | type: C.ADD_FIRST_CRACK,
91 | roastId,
92 | firstCrackTime
93 | };
94 | };
95 |
96 | // New roast actions.
97 | export const createNewRoast = (roastDetails) => {
98 | return {
99 | type: C.CREATING_NEW_ROAST,
100 | roastDetails: roastDetails
101 | };
102 | };
103 |
104 | export const removeRoast = roastId => {
105 | return {
106 | type: C.REMOVE_ROAST,
107 | roastId
108 | };
109 | };
110 |
111 | export const createNewRoastSuccess = (roastData) => {
112 | return {
113 | type: C.CREATE_NEW_ROAST_SUCCESS,
114 | roastData: roastData
115 | };
116 | };
117 |
118 | export const createNewRoastFailed = (error) => {
119 | return {
120 | type: C.CREATE_NEW_ROAST_FAILED,
121 | error
122 | };
123 | };
124 |
125 | export const updateRoastValue = (roastId, field, value) => {
126 | return {
127 | type: C.UPDATE_ROAST_VALUE,
128 | roastId,
129 | field,
130 | value
131 | };
132 | };
133 |
134 | export const updateCurrentRoastValue = (field, value) => {
135 | return {
136 | type: C.UPDATE_CURRENT_ROAST_VALUE,
137 | field,
138 | value
139 | };
140 | };
141 |
142 | // Field actions.
143 | export const toggleEditing = (roastId, field) => {
144 | return {
145 | type: C.TOGGLE_EDITING_FIELD,
146 | roastId,
147 | field
148 | };
149 | };
150 |
151 | // Stopwatch actions.
152 | export const startStopWatch = (roastId, roastStart, tick) => {
153 | let uid = C.FIREBASE.auth().currentUser.uid;
154 | let roastRef = C.FIREBASE.app().database().ref(`roasts/${uid}/${roastId}`);
155 |
156 | roastRef.update({
157 | status: C.ROAST_IN_PROGRESS,
158 | roastStart
159 | });
160 |
161 | return {
162 | type: C.STOPWATCH_START,
163 | roastId,
164 | tick
165 | };
166 | };
167 |
168 | export const resumeStopWatch = (roastId, roastStart, tick) => {
169 | return {
170 | type: C.STOPWATCH_RESUME,
171 | roastId,
172 | tick
173 | };
174 | };
175 |
176 | export const tickStopWatch = (roastStart) => {
177 | return {
178 | type: C.STOPWATCH_TICK,
179 | roastStart
180 | };
181 | };
182 |
183 | export const stopStopWatch = (roastId) => {
184 | let uid = C.FIREBASE.auth().currentUser.uid;
185 | let roastRef = C.FIREBASE.app().database().ref(`roasts/${uid}/${roastId}`);
186 |
187 | let promise = roastRef.update({
188 | status: C.ROAST_COMPLETED
189 | });
190 |
191 | return {
192 | type: C.STOPWATCH_STOP,
193 | roastId,
194 | promise
195 | };
196 | };
197 |
198 | // Roast progress actions.
199 | export const checkRoastInProgress = roasts => {
200 | return {
201 | type: C.CHECK_ROAST_IN_PROGRESS,
202 | roasts
203 | };
204 | };
205 |
206 | // Dialog actions.
207 | export const showDialog = ({
208 | dialogType='info',
209 | noAction = null,
210 | noText = 'No',
211 | text,
212 | yesAction,
213 | yesText = 'Yes'
214 | }) => {
215 | return {
216 | dialogType,
217 | noAction,
218 | noText,
219 | text,
220 | type: C.SHOW_DIALOG,
221 | yesAction,
222 | yesText
223 | };
224 | };
225 |
226 | export const clearDialog = () => {
227 | return {
228 | type: C.CLEAR_DIALOG
229 | };
230 | };
231 |
232 | // Data loading.
233 | export const loadingData = () => {
234 | return {
235 | type: C.LOADING_DATA
236 | };
237 | }
238 |
239 | export const loadedData = () => {
240 | return {
241 | type: C.LOADED_DATA
242 | };
243 | }
244 |
245 | // Settings.
246 | export const updateSetting = (setting, value) => {
247 | return {
248 | type: C.UPDATE_SETTING,
249 | setting,
250 | value
251 | };
252 | }
253 |
254 | export const fetchedSettings = settings => {
255 | return {
256 | type: C.FETCHED_SETTINGS,
257 | settings
258 | };
259 | }
260 |
--------------------------------------------------------------------------------
/src/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactGA from 'react-ga';
3 | import thunkMiddleware from 'redux-thunk';
4 | import { Provider } from 'react-redux';
5 | import { Router, Route, IndexRoute, Link, Redirect } from 'react-router';
6 | import { createStore, applyMiddleware, compose } from 'redux';
7 | import { render } from 'react-dom';
8 |
9 | import AppContainer from './containers/AppContainer';
10 | import C from './constants';
11 | import Home from './components/Home';
12 | import MainContainer from './containers/MainContainer';
13 | import NewRoastFormContainer from './containers/NewRoastFormContainer';
14 | import RoastProfileContainer from './containers/RoastProfileContainer';
15 | import SettingsContainer from './containers/SettingsContainer';
16 | import auth from './auth';
17 | import history from './history';
18 | import rootReducer from './reducers/index';
19 | import {
20 | checkRoastInProgress,
21 | fetchedRoasts,
22 | fetchedSettings,
23 | listeningToAuth,
24 | loadedData,
25 | loadingData,
26 | loginSuccess,
27 | logout
28 | } from './actions';
29 |
30 | const store = applyMiddleware(thunkMiddleware)(createStore)(rootReducer, {}
31 | ,window.devToolsExtension && window.devToolsExtension()
32 | );
33 |
34 | // Analytics
35 | ReactGA.initialize(C.GOOGLE_ANALYTICS_CODE);
36 |
37 | const logPageView = () => {
38 | if (window.location.hostname !== 'localhost') {
39 | ReactGA.pageview(window.location.hash);
40 | }
41 | };
42 |
43 | const routes = (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 |
58 | render(
59 |
60 | { routes }
61 | ,
62 | document.getElementById("root")
63 | );
64 |
65 | store.dispatch(listeningToAuth());
66 |
67 | // Start listening to firebase auth changes.
68 | C.FIREBASE.auth().onAuthStateChanged((user) => {
69 | // If logged in.
70 | if (user) {
71 | store.dispatch(loginSuccess(user));
72 |
73 | // Listen to roast changes
74 | let roastsRef = C.FIREBASE.app().database().ref(`/roasts/${user.uid}`);
75 |
76 | store.dispatch(loadingData());
77 | roastsRef.on('value', snapshot => {
78 | store.dispatch(fetchedRoasts(snapshot.val()));
79 | store.dispatch(loadedData());
80 | store.dispatch(checkRoastInProgress(snapshot.val()));
81 | }, err => {
82 | console.log(err);
83 | });
84 |
85 | // Listen to settings changes.
86 | let settingsRef = C.FIREBASE.app().database().ref(`/settings/${user.uid}`);
87 |
88 | store.dispatch(loadingData());
89 | settingsRef.on('value', snapshot => {
90 | store.dispatch(loadedData());
91 | store.dispatch(fetchedSettings(snapshot.val()));
92 | });
93 |
94 | } else {
95 | C.FIREBASE.auth().getRedirectResult().then(function(result) {
96 | if (!result.user) {
97 | store.dispatch(logout());
98 | } else {
99 | store.dispatch(loginSuccess(result.user));
100 | }
101 | });
102 | }
103 | }, err => {
104 | console.log(err);
105 | });
106 |
--------------------------------------------------------------------------------
/src/auth.js:
--------------------------------------------------------------------------------
1 | import { browserHistory } from 'react-router';
2 | import C from './constants';
3 |
4 | const auth = {
5 | login: (provider) => {
6 | let authProvider = null;
7 |
8 | switch (provider) {
9 | case 'facebook':
10 | authProvider = new firebase.auth.FacebookAuthProvider();
11 | break;
12 |
13 | case 'google':
14 | authProvider = new firebase.auth.GoogleAuthProvider();
15 | break;
16 |
17 | default:
18 | break;
19 | }
20 |
21 | if (window.location.protocol === 'http') {
22 | return C.FIREBASE.auth().signInWithPopup(authProvider);
23 | } else {
24 | return C.FIREBASE.auth().signInWithRedirect(authProvider);
25 | }
26 | },
27 |
28 | isLoggedIn: () => {
29 | return C.FIREBASE.auth().currentUser;
30 | },
31 |
32 | checkAuth: (nextState, replace, cb) => {
33 | if (!C.FIREBASE.auth().currentUser) {
34 | replace({
35 | pathname: '/',
36 | state: { nextPathname: nextState.location.pathname }
37 | });
38 | }
39 | cb();
40 | }
41 | };
42 |
43 | export default auth;
44 |
--------------------------------------------------------------------------------
/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
3 | import { Link, browserHistory } from 'react-router';
4 |
5 | import C from '../constants';
6 | import DialogContainer from '../containers/DialogContainer';
7 | import HeaderContainer from '../containers/HeaderContainer';
8 | import Spinner from './Spinner';
9 |
10 | require('../../scss/app.scss');
11 |
12 | class App extends React.Component {
13 |
14 | static propTypes() {
15 | return {
16 | uid: PropTypes.string,
17 | authStatus: PropTypes.string.isRequired,
18 | userName: PropTypes.string
19 | };
20 | }
21 |
22 | componentDidUpdate() {
23 | componentHandler.upgradeDom();
24 | }
25 |
26 | pageContent() {
27 | let content = ;
28 | if (!this.props.dataLoading) {
29 | content = (
30 |
37 | { React.cloneElement(this.props.children, {
38 | key: this.props.location.pathname
39 | }) }
40 |
41 | );
42 | }
43 | return content;
44 | }
45 |
46 | render() {
47 | let extraClass = '';
48 | let content = null;
49 |
50 | if (this.props.authStatus !== C.LOGGED_IN) {
51 | extraClass = "bobon-anon";
52 | }
53 |
54 | if (this.props.authStatus === C.LOGGING_IN) {
55 | content = ;
56 | } else {
57 | content = (
58 |
59 |
60 |
61 |
62 |
63 | { this.pageContent() }
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | return content;
78 | }
79 | };
80 |
81 | export default App;
82 |
--------------------------------------------------------------------------------
/src/components/Dialog.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | import Button from './utils/Button';
4 | import Card from './utils/Card';
5 | import CardTitle from './utils/CardTitle';
6 | import CardAction from './utils/CardAction';
7 |
8 | class Dialog extends React.Component {
9 | static propTypes() {
10 | return {
11 | clearDialog: PropTypes.func.isRequired,
12 | dialogType: PropTypes.string,
13 | noAction: PropTypes.func,
14 | noText: PropTypes.string,
15 | text: PropTypes.string.isRequired,
16 | yesAction: PropTypes.func,
17 | yesText: PropTypes.string
18 | }
19 | }
20 |
21 | render() {
22 | let content = null;
23 | if (this.props.text) {
24 | content = (
25 |
26 |
27 |
28 |
29 | { this.props.text }
30 |
31 |
32 |
43 |
44 |
55 |
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | return content;
63 | }
64 | }
65 |
66 | export default Dialog;
67 |
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
3 |
4 | import Button from './utils/Button';
5 | import C from '../constants';
6 | import history from '../history';
7 | import UnitSwitcherContainer from '../containers/UnitSwitcherContainer';
8 |
9 | class Header extends React.Component {
10 |
11 | static propTypes() {
12 | return {
13 | authStatus: PropTypes.string.isRequired,
14 | photoURL: PropTypes.string,
15 | userName: PropTypes.string
16 | };
17 | }
18 |
19 | userMenu() {
20 | let content = null;
21 |
22 | if (this.props.authStatus === C.LOGGED_IN) {
23 | content = (
24 |
29 |
30 | -
34 | account_circle
35 | { this.props.userName ? this.props.userName : this.props.email }
36 |
37 |
38 | - {
41 | history.push('/')
42 | } }
43 | >
44 | timeline
45 | My roasts
46 |
47 |
48 | - {
52 | history.push('/settings');
53 | } }
54 | >
55 | settings
56 | Settings
57 |
58 |
59 | -
63 | exit_to_app
64 | Logout
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | return content;
72 | }
73 |
74 | profilePhoto() {
75 | let photo = null;
76 |
77 | if (this.props.authStatus === C.LOGGED_IN) {
78 | if (this.props.photoURL && this.props.photoURL !== '') {
79 | photo = (
80 |
89 | );
90 | } else {
91 | photo = (
92 |
99 | );
100 | }
101 | } else {
102 | photo = (
103 | {
105 | e.preventDefault();
106 | history.push('/')
107 | } }
108 | >
109 | Bobon Profiles
110 |
111 | );
112 | }
113 |
114 | return photo;
115 | }
116 |
117 | actionButton() {
118 | let actionButton = null;
119 |
120 | if (this.props.authStatus === C.LOGGED_IN) {
121 | actionButton = (
122 |
123 |
124 | { this.newRoastBtnNav() }
125 |
126 |
136 |
137 |
140 | My roasts
141 |
142 |
143 | );
144 | }
145 |
146 | return actionButton;
147 | }
148 |
149 | roastInProgress() {
150 | let content = null;
151 |
152 | if (this.props.roastInProgress) {
153 | content = (
154 |
162 | );
163 | }
164 |
165 | return content;
166 | }
167 |
168 | newRoastBtnNav() {
169 | let content = null;
170 | let location = this.props.location;
171 |
172 | if (
173 | this.props.authStatus === C.LOGGED_IN &&
174 | this.props.roastInProgress === null
175 | ) {
176 | content = (
177 |
187 | );
188 | }
189 |
190 | return content;
191 | }
192 |
193 | newRoastBtn() {
194 | let content = null;
195 | let location = this.props.location;
196 |
197 | if (
198 | this.props.authStatus === C.LOGGED_IN &&
199 | this.props.roastInProgress === null
200 | ) {
201 | content = (
202 |
203 |
212 |
213 | Start a new roast!
214 |
215 |
216 | );
217 | }
218 |
219 | return content;
220 | }
221 |
222 | unitSwitcher() {
223 | let content = null;
224 | if (this.props.authStatus === C.LOGGED_IN) {
225 | content = ;
226 | }
227 |
228 | return content;
229 | }
230 |
231 | render() {
232 | return (
233 |
239 |
240 |
241 |
242 |
243 |
244 |
250 |
251 | { this.newRoastBtn() }
252 |
253 | { this.userMenu() }
254 |
255 | );
256 | }
257 |
258 | }
259 |
260 | export default Header;
261 |
--------------------------------------------------------------------------------
/src/components/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LoginFormContainer from '../containers/LoginFormContainer';
3 |
4 | class Home extends React.Component {
5 | render() {
6 | return ;
7 | }
8 | }
9 |
10 | export default Home;
11 |
--------------------------------------------------------------------------------
/src/components/Instructions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const imgCreate = require('../../images/step_create.png');
4 | const imgInput = require('../../images/step_input.png');
5 | const imgStart = require('../../images/step_timer.png');
6 | const imgStop = require('../../images/step_stop.png');
7 |
8 | class Instructions extends React.Component {
9 | render() {
10 | return (
11 |
12 |
15 |
16 | 1. Create roast
17 |
18 |

19 |
20 |
21 |
24 |
25 | 2. Drop beans & start timer
26 |
27 |

28 |
29 |
30 |
33 |
34 | 3. Enter temperatures. Push FIRST CRACK when it happens
35 |
36 |

37 |
38 |
39 |
42 |
43 | 4. Stop timer & complete the roast
44 |
45 |

46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default Instructions;
53 |
--------------------------------------------------------------------------------
/src/components/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { browserHistory } from 'react-router';
3 |
4 | import Button from './utils/Button';
5 | import C from '../constants';
6 | import Card from './utils/Card';
7 | import CardAction from './utils/CardAction';
8 | import CardContent from './utils/CardContent';
9 | import CardTitle from './utils/CardTitle';
10 |
11 | const LoginForm = ({ authStatus, onLoginBtnClick }) => {
12 | let content = null;
13 |
14 | if (authStatus !== C.LOGGED_IN) {
15 | content = (
16 |
17 |
18 |
19 | Bobon Profiles
20 |
21 |
22 |
23 |
24 | A real-time profile platform for home roasting coffee
25 |
26 |
27 |
28 |
31 |
34 |
35 |
36 | );
37 | }
38 |
39 | return content;
40 | };
41 |
42 | LoginForm.propTypes = {
43 | authStatus: PropTypes.string.isRequired,
44 | onLoginBtnClick: PropTypes.func.isRequired
45 | };
46 |
47 | export default LoginForm;
48 |
--------------------------------------------------------------------------------
/src/components/Main.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Line } from 'react-chartjs';
3 |
4 | import C from '../constants';
5 | import Home from './Home';
6 | import RoastListContainer from '../containers/RoastListContainer';
7 | import { demoDataset, demoChartOptions } from '../demoData';
8 |
9 |
10 | class Main extends React.Component {
11 | static propTypes() {
12 | return {
13 | authStatus: PropTypes.string.isRequired
14 | };
15 | }
16 |
17 | render() {
18 | let content = null;
19 | if (this.props.authStatus === C.LOGGED_IN) {
20 | content = ();
21 | } else {
22 | content = (
23 |
24 |
30 |
31 |
32 | );
33 | }
34 |
35 | return content;
36 | }
37 | }
38 |
39 | export default Main;
40 |
--------------------------------------------------------------------------------
/src/components/NewRoastForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import history from '../history';
3 |
4 | import Button from './utils/Button';
5 | import Card from './utils/Card';
6 | import CardContent from './utils/CardContent';
7 | import CardTitle from './utils/CardTitle';
8 | import Spinner from './Spinner';
9 | import UnitSwitcherContainer from '../containers/UnitSwitcherContainer';
10 |
11 | class NewRoastForm extends React.Component {
12 | cancelButton() {
13 | return(
14 |
26 | );
27 | }
28 |
29 | submitButton() {
30 | if (this.props.disabled === true) {
31 | return(
32 |
42 | );
43 | } else {
44 | return(
45 |
54 | );
55 | }
56 | }
57 |
58 | render() {
59 | if (this.props.processing) {
60 | return ;
61 | } else {
62 | return (
63 |
64 |
65 |
66 |
67 |
68 | timer
69 | Create new roast
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
214 |
215 |
216 | );
217 | }
218 | }
219 | }
220 |
221 | export default NewRoastForm;
222 |
--------------------------------------------------------------------------------
/src/components/PostRoastNoteForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import C from '../constants';
4 | import Card from './utils/Card';
5 | import CardAction from './utils/CardAction';
6 | import CardContent from './utils/CardContent';
7 | import CardTitle from './utils/CardTitle';
8 |
9 | class PostRoastNoteForm extends React.Component {
10 |
11 | componentDidUpdate(props) {
12 | componentHandler.upgradeDom();
13 | }
14 |
15 | noteInput() {
16 | let content = null;
17 | let autoFocus = false;
18 |
19 | if (this.props.editing) {
20 | autoFocus = true;
21 | }
22 | content = (
23 |
33 | );
34 |
35 | return content;
36 | }
37 |
38 | actionButton() {
39 | let content = null;
40 |
41 | if (this.props.editing === C.FIELD_STATUS_NOT_EDITING) {
42 | let btnText = 'Edit';
43 |
44 | if (this.props.postRoastNote === '' || !this.props.postRoastNote) {
45 | btnText = 'Add Comment';
46 | }
47 |
48 | content = (
49 |
50 |
62 |
63 | );
64 | }
65 |
66 | return content;
67 | }
68 |
69 | noteForm() {
70 | if ((this.props.postRoastNote === '' || this.props.postRoastNote === null) &&
71 | this.props.editing !== C.FIELD_STATUS_EDITING
72 | ) {
73 | return null;
74 | }
75 |
76 | let content = (
77 |
78 |
79 | { this.props.postRoastNote }
80 |
81 |
82 | );
83 |
84 | if (this.props.editing === C.FIELD_STATUS_EDITING) {
85 | content = (
86 |
87 |
121 |
122 | );
123 | }
124 |
125 | return content;
126 | }
127 |
128 | render() {
129 | let content = null;
130 |
131 | if (this.props.status === C.ROAST_COMPLETED) {
132 | content = (
133 |
134 |
135 |
136 | receipt
137 | Post-roasting notes
138 |
139 |
140 |
141 | { this.noteForm() }
142 |
143 | { this.actionButton() }
144 |
145 | );
146 | }
147 |
148 | return content;
149 | }
150 | }
151 |
152 | export default PostRoastNoteForm;
153 |
--------------------------------------------------------------------------------
/src/components/RoastChart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Line } from 'react-chartjs';
3 |
4 | import C from '../constants';
5 | import { displayTemp } from '../helpers';
6 |
7 | class RoastChart extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | redraw: false,
13 | compare: null
14 | };
15 | }
16 |
17 | componentWillReceiveProps(nextProps) {
18 | // Persistent comparing state.
19 | if (this.props.compare) {
20 | this.setState({
21 | compare: this.props.compare,
22 | redraw: false
23 | });
24 | }
25 |
26 | if (this.props && this.props.roastPoints && nextProps.roastPoints) {
27 | if (Object.keys(nextProps.roastPoints).length < Object.keys(this.props.roastPoints).length) {
28 | this.setState({ redraw: true });
29 | } else {
30 | this.setState({ redraw: false });
31 | }
32 | } else {
33 | this.setState({ redraw: false });
34 | }
35 |
36 | if (
37 | this.props.compare && nextProps.compare &&
38 | this.props.compare.created !== nextProps.compare.created
39 | ) {
40 | this.setState({ redraw: true });
41 | }
42 |
43 | if (this.props.unitSystem !== nextProps.unitSystem) {
44 | this.setState({ redraw: true });
45 | }
46 | }
47 |
48 | beautifyTime(value) {
49 | let m = Math.floor(value / 60);
50 | let s = value % 60;
51 |
52 | if (s === 0) {
53 | return m;
54 | } else {
55 | // Add empty label, but show line on 30 sec intervals.
56 | return '';
57 | }
58 | }
59 |
60 | createRoastPointsDataset(roastPoints) {
61 | return Object.keys(roastPoints).map(
62 | key => {
63 | return {
64 | x: roastPoints[key].elapsed / 60000,
65 | y: displayTemp(roastPoints[key].temperature, this.props.unitSystem)
66 | };
67 | }
68 | ).sort((a,b) => {
69 | return a.x - b.x;
70 | });
71 | }
72 |
73 | createRateOfRoastDataset(dataSet) {
74 | let ror = [];
75 | for (var i = 0; i < dataSet.length; i++) {
76 | if (i === 0) {
77 | ror.push({
78 | x: dataSet[0].x,
79 | y: 0
80 | });
81 | } else {
82 | let tangent = (dataSet[i].y - dataSet[i - 1].y) / (dataSet[i].x - dataSet[i - 1].x);
83 | ror.push({
84 | x: dataSet[i].x,
85 | y: tangent.toFixed(2)
86 | });
87 | }
88 | }
89 | return ror;
90 | }
91 |
92 | render() {
93 | let redraw = false;
94 | let tempMax = 270;
95 | let tempMin = 50;
96 | let tempStepSize = 10;
97 | let rateMax = 55;
98 |
99 | if (this.props.unitSystem === C.IMPERIAL) {
100 | tempMax = 520;
101 | tempMin = 120;
102 | tempStepSize = 20;
103 | rateMax = 100;
104 | }
105 |
106 | if (this.props.roastPoints) {
107 | let chartData = {};
108 | let maxX = 14;
109 | let data = this.createRoastPointsDataset(this.props.roastPoints);
110 | let ror = this.createRateOfRoastDataset(data);
111 |
112 | // Set max x scale.
113 | if (data.length > 0) {
114 | let lastMin = data[data.length - 1].x;
115 | if (lastMin + 1 > maxX) {
116 | maxX = lastMin + 1;
117 | redraw = true;
118 | }
119 | }
120 |
121 | chartData = {
122 | labels: [],
123 | datasets: [
124 | // Temperature points.
125 | {
126 | label: 'temp 1',
127 | data,
128 | fill: false,
129 | yAxisID: 'temp'
130 | },
131 | // Rate of roast.
132 | {
133 | label: 'rate 1',
134 | data: ror,
135 | fill: false,
136 | yAxisID: 'rate'
137 | },
138 | // First crack - basically adding 2 points vertically on
139 | // top of each other.
140 | {
141 | label: 'first crack 1',
142 | data: [
143 | { x: 0, y: 0 },
144 | { x: 0, y: 1000 }
145 | ],
146 | fill: false,
147 | yAxisID: 'temp'
148 | }
149 | ]
150 | };
151 |
152 | if (this.props.firstCrack) {
153 | chartData.datasets[2].data = [
154 | { x: this.props.firstCrack / 60000, y: 50 },
155 | { x: this.props.firstCrack / 60000, y: 1000 }
156 | ];
157 | }
158 |
159 | let compare = this.props.compare;
160 | if (compare === null) {
161 | compare = this.state.compare;
162 | }
163 |
164 | if (compare !== null) {
165 | let comparePoints = compare.roastPoints;
166 | let compareData = this.createRoastPointsDataset(comparePoints);
167 | let compareRor = this.createRateOfRoastDataset(compareData);
168 |
169 | let lastMin = compareData[compareData.length - 1].x;
170 | if (lastMin + 1 > maxX) {
171 | maxX = lastMin + 1;
172 | redraw = true;
173 | }
174 |
175 | chartData.datasets.push({
176 | label: 'temp 2',
177 | data: compareData,
178 | fill: false,
179 | yAxisID: 'temp'
180 | });
181 |
182 | chartData.datasets.push({
183 | label: 'rate 2',
184 | data: compareRor,
185 | fill: false,
186 | yAxisID: 'rate'
187 | });
188 |
189 | if (compare.firstCrack) {
190 | let compareFirstCrack = compare.firstCrack / 60000;
191 | chartData.datasets.push(
192 | {
193 | label: 'first crack 2',
194 | data: [
195 | { x: compareFirstCrack, y: 50 },
196 | { x: compareFirstCrack, y: 1000 }
197 | ],
198 | fill: false,
199 | yAxisID: 'temp'
200 | }
201 | );
202 | }
203 |
204 | if (this.state.compare === null && this.state.redraw === false) {
205 | redraw = true;
206 | }
207 | }
208 |
209 | // Assign colors to each dataset.
210 | chartData.datasets.forEach((v, i) => {
211 | let color = C.CHART_COLORS[i % C.CHART_COLORS.length];
212 | chartData.datasets[i].borderColor = color;
213 | chartData.datasets[i].backgroundColor = color;
214 | chartData.datasets[i].borderWidth = 1;
215 | });
216 |
217 | let chartOptions = {
218 | defaultFontFamily: 'Roboto',
219 | animation: {
220 | duration: 1000
221 | },
222 | scales: {
223 | xAxes: [{
224 | type: 'linear',
225 | position: 'bottom',
226 | id: 'time',
227 | ticks: {
228 | max: maxX,
229 | min: 0,
230 | stepSize: 1
231 | }
232 | }],
233 | yAxes: [
234 | {
235 | id: 'temp',
236 | type: 'linear',
237 | position: 'left',
238 | ticks: {
239 | max: tempMax,
240 | min: tempMin,
241 | stepSize: tempStepSize
242 | }
243 | },
244 | {
245 | id: 'rate',
246 | position: 'right',
247 | type: 'linear',
248 | ticks: {
249 | max: rateMax,
250 | min: 0,
251 | stepSize: 5
252 | }
253 | },
254 | ]
255 | },
256 | maintainAspectRatio: true,
257 | responsive: true,
258 | tooltips: {
259 | titleFontFamily: 'Roboto',
260 | titleFontStyle: 'normal',
261 | callbacks: {
262 | title: (item, data) => {
263 | let xLabel = item[0].xLabel;
264 | let min = xLabel / 1 << 0;
265 | let sec = xLabel % 1 * 60 << 0;
266 | if (sec < 10) {
267 | sec = '0' + sec;
268 | }
269 | if (min < 10) {
270 | min = '0' + min;
271 | }
272 | return 'elapsed: ' + min + ':' + sec;
273 | }
274 | }
275 | }
276 | };
277 |
278 | if (redraw || this.state.redraw) {
279 | return ;
286 | } else {
287 | return ;
293 | }
294 |
295 | } else {
296 | return null;
297 | }
298 | }
299 | }
300 |
301 | export default RoastChart;
302 |
--------------------------------------------------------------------------------
/src/components/RoastList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 |
4 | import Button from './utils/Button';
5 | import C from '../constants';
6 | import Card from './utils/Card';
7 | import CardAction from './utils/CardAction';
8 | import CardContent from './utils/CardContent';
9 | import CardTitle from './utils/CardTitle';
10 | import Instructions from './Instructions';
11 | import NavigationLink from '../components/utils/NavigationLink';
12 | import history from '../history';
13 | import {
14 | displayTemp,
15 | displayWeight
16 | } from '../helpers';
17 |
18 | class RoastList extends React.Component {
19 |
20 | roastStatus(statusText) {
21 | switch(statusText) {
22 | case C.ROAST_PENDING:
23 | return 'pending';
24 | break;
25 |
26 | case C.ROAST_COMPLETED:
27 | return 'completed';
28 | break;
29 |
30 | case C.ROAST_IN_PROGRESS:
31 | return 'in progress';
32 | break;
33 |
34 | default:
35 | return statusText;
36 | break;
37 | }
38 | }
39 |
40 | lastRoastPointDuration(roastPoints) {
41 | let duration = null;
42 | if (roastPoints) {
43 | let roastKey = Object.keys(roastPoints).pop();
44 | duration = moment(roastPoints[roastKey].elapsed).format('mm:ss');
45 | }
46 |
47 | return duration;
48 | }
49 |
50 | roastRows() {
51 | let content = null;
52 |
53 | if (this.props.roasts) {
54 | content = Object.keys(this.props.roasts).map(key => {
55 | let roast = this.props.roasts[key];
56 | let roastDate = '';
57 |
58 | if (roast.roastStart) {
59 | roastDate = moment(roast.roastStart).format('DD-MM-YY HH:mm');
60 | }
61 |
62 | return (
63 | {
65 | e.preventDefault();
66 | history.push(`/roasts/${key}`);
67 | }}
68 | >
69 |
72 | { roast.beansName }
73 | |
74 |
75 |
78 |
79 | fiber_manual_record
80 | { this.roastStatus(roast.status) }
81 |
82 | |
83 |
84 |
85 | { roastDate }
86 | |
87 |
88 |
89 | { roast.beansMoisture } %
90 | |
91 |
92 |
93 | { displayWeight(roast.batchSize, this.props.unitSystem) } { this.props.weightUnit }
94 | |
95 |
96 |
97 | { this.lastRoastPointDuration(roast.roastPoints) }
98 | |
99 |
100 |
101 | { roast.firstCrack ? moment(roast.firstCrack).format('mm:ss') : '-' }
102 | |
103 |
104 |
107 |
122 | |
123 |
124 | );
125 | });
126 | }
127 |
128 | return content;
129 | }
130 |
131 | render() {
132 | let content = null;
133 | if (this.props.roasts && Object.keys(this.props.roasts).length > 0) {
134 | content = (
135 |
136 |
137 |
138 |
139 |
140 | timeline
141 | My roasts
142 |
143 |
144 |
145 |
148 |
149 |
150 |
153 | Bean's name
154 | |
155 |
158 | Status
159 | |
160 |
161 | Roast date
162 | |
163 |
164 | Moisture
165 | |
166 |
167 | Batch Size
168 | |
169 |
170 | Duration
171 | |
172 |
173 | 1st crack
174 | |
175 |
176 | Del
177 | |
178 |
179 |
180 |
181 |
182 | { this.roastRows() }
183 |
184 |
185 |
186 |
187 | );
188 | } else {
189 | content = (
190 |
191 |
192 |
193 |
194 |
195 | timeline
196 | You have not recorded any roast profile, yet.
197 |
198 |
199 |
200 |
201 |
202 |
203 |
211 |
212 |
213 |
214 |
215 | );
216 | }
217 |
218 | return content;
219 | }
220 | }
221 |
222 | export default RoastList;
223 |
--------------------------------------------------------------------------------
/src/components/RoastPointInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import C from '../constants';
3 |
4 | import Button from './utils/Button';
5 |
6 | class RoastPointInput extends React.Component {
7 |
8 | componentDidUpdate() {
9 | // Upgrades all upgradable components (i.e. with 'mdl-js-*' class).
10 | componentHandler.upgradeDom();
11 | }
12 |
13 | disabled() {
14 | if (this.props.status === C.ROAST_IN_PROGRESS) {
15 | return false;
16 | } else {
17 | return true;
18 | }
19 | }
20 |
21 | render() {
22 | if (this.props.status === C.ROAST_PENDING ||
23 | this.props.status === C.ROAST_IN_PROGRESS) {
24 | return (
25 |
64 | );
65 | } else {
66 | return null;
67 | }
68 | }
69 | }
70 |
71 | export default RoastPointInput;
72 |
--------------------------------------------------------------------------------
/src/components/RoastPointsList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Table from './utils/Table';
3 |
4 | class RoastPointsList extends React.Component {
5 | roastPoints() {
6 | return Object.keys(this.props.roastPoints).map(
7 | key => {
8 | return (
9 |
10 |
11 | { this.props.roastPoints[key].elapsed }
12 | |
13 |
14 | { this.props.roastPoints[key].temperature }
15 | |
16 |
17 | );
18 | }
19 | );
20 | }
21 |
22 | render() {
23 | return (
24 |
25 |
26 |
27 | Time Stamp |
28 | Temperature |
29 |
30 |
31 |
32 |
33 | { this.roastPoints() }
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | export default RoastPointsList;
41 |
--------------------------------------------------------------------------------
/src/components/RoastProfile.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 |
4 | import Button from './utils/Button';
5 | import C from '../constants';
6 | import Card from './utils/Card';
7 | import CardAction from './utils/CardAction';
8 | import CardContent from './utils/CardContent';
9 | import CardTitle from './utils/CardTitle';
10 | import PostRoastNoteFormContainer from '../containers/PostRoastNoteFormContainer';
11 | import RoastChart from './RoastChart';
12 | import RoastPointInputContainer from '../containers/RoastPointInputContainer';
13 | import RoastPointsListContainer from '../containers/RoastPointsListContainer';
14 | import StopWatchContainer from '../containers/StopWatchContainer';
15 | import {
16 | displayTemp,
17 | displayWeight
18 | } from '../helpers';
19 |
20 | class RoastProfile extends React.Component {
21 |
22 | beansMoisture() {
23 | let content = null;
24 | if (this.props.beansMoisture) {
25 | content = (
26 |
27 |
28 | opacity
29 | { this.props.beansMoisture } %
30 |
31 |
32 | );
33 | }
34 |
35 | return content;
36 | }
37 |
38 | stopWatch() {
39 | let content = null;
40 |
41 | if (this.props.roastInProgress &&
42 | this.props.roastInProgress !== this.props.roastId
43 | ) {
44 | content = null;
45 | } else if (this.props.status === C.ROAST_PENDING ||
46 | this.props.status === C.ROAST_IN_PROGRESS
47 | ) {
48 | content = (
49 |
50 |
55 |
56 | );
57 | }
58 |
59 | return content;
60 | }
61 |
62 | tempInput() {
63 | let content = null;
64 | if (this.props.roastInProgress &&
65 | this.props.roastInProgress !== this.props.roastId
66 | ) {
67 | content = null;
68 | } else if (
69 | this.props.status === C.ROAST_PENDING ||
70 | this.props.status === C.ROAST_IN_PROGRESS
71 | ) {
72 | content = (
73 |
74 |
75 |
76 |
77 | add_circle
78 | Temperature input / { this.props.tempUnit }
79 |
80 |
81 |
82 |
89 |
90 |
91 |
101 |
102 |
111 |
112 |
113 |
114 | );
115 | }
116 |
117 | return content;
118 | }
119 |
120 | postRoastNote() {
121 | let content = null;
122 |
123 | if (this.props.status === C.ROAST_COMPLETED) {
124 | content = (
125 |
129 | );
130 | }
131 |
132 | return content;
133 | }
134 |
135 | roastNote() {
136 | return(
137 |
138 |
139 |
140 | description
141 | Roasting Notes
142 |
143 |
144 |
145 |
146 | { this.props.roastNote }
147 |
148 |
149 |
150 | );
151 | }
152 |
153 | roastPointsList() {
154 | return (
155 |
156 |
157 | Temperature points
158 |
159 |
160 |
161 |
162 |
163 | );
164 | }
165 |
166 | magicButton() {
167 | return (
168 |
177 | );
178 | }
179 |
180 | roastTime() {
181 | if (this.props.status === C.ROAST_PENDING) {
182 | return null;
183 | } else {
184 | return (
185 |
186 |
187 | event
188 | { moment(this.props.roastStart).format('DD/MM/YYYY - h:mm') }
189 |
190 |
191 | );
192 | }
193 | }
194 |
195 | status() {
196 | let content = null;
197 | let statusText = '';
198 |
199 | if (this.props.status) {
200 |
201 | switch (this.props.status) {
202 | case C.ROAST_PENDING:
203 | statusText = 'Pending';
204 | break;
205 |
206 | case C.ROAST_IN_PROGRESS:
207 | statusText = 'In progress';
208 | break;
209 |
210 | default:
211 | statusText = 'Completed';
212 | break;
213 | }
214 |
215 | content = (
216 |
217 |
218 | fiber_manual_record
219 | { statusText }
220 |
221 |
222 | );
223 | }
224 |
225 | return content;
226 | }
227 |
228 | selectCompare() {
229 | if (typeof this.props.roastIds === 'undefined') {
230 | return null;
231 | }
232 |
233 | let roastIdList = this.props.roastIds.map(roast => {
234 | return (
235 | {
237 | this.props.compareRoasts(this.props.roastId, roast.id);
238 | } }
239 | >
240 | { '[' + moment(roast.roastStart).format('DD/MM/YYYY HH:mm') + '] ' + roast.value }
241 |
242 | );
243 | });
244 |
245 | let buttonText = "Compare with a previous roast profile";
246 |
247 | if (this.props.compare) {
248 | buttonText = '[' + moment(this.props.compare.roastStart).format('DD/MM/YYYY HH:mm') + '] ' + this.props.compare.beansName;
249 | }
250 |
251 | return (
252 |
253 |
259 |
262 | { roastIdList }
263 |
264 |
265 | );
266 | }
267 |
268 | lastRoastPointId() {
269 | if (this.props.roastPoints) {
270 | return Object.keys(this.props.roastPoints).pop();
271 | } else {
272 | return null;
273 | }
274 | }
275 |
276 | roastDuration() {
277 | let min = '00';
278 | let sec = '00';
279 |
280 | if (this.props.hasOwnProperty('roastPoints')) {
281 | let duration = this.props.roastPoints[this.lastRoastPointId()].elapsed;
282 | min = duration / 60000 << 0;
283 | sec = duration / 1000 % 60 << 0;
284 |
285 | if (min < 10) {
286 | min = '0' + min;
287 | }
288 |
289 | if (sec < 10) {
290 | sec = '0' + sec;
291 | }
292 | }
293 |
294 | return `${min}:${sec}`;
295 | }
296 |
297 | render() {
298 | return (
299 |
300 |
301 |
302 |
303 | assessment
304 | { this.props.beansName }
305 |
306 |
307 |
308 |
309 |
310 |
318 |
319 |
320 |
323 | { this.selectCompare() }
324 | { this.stopWatch() }
325 | { this.tempInput() }
326 |
327 |
328 |
329 |
330 | playlist_add_check
331 | Roast details
332 |
333 |
334 |
335 |
336 |
337 | label
338 | { this.props.beansName }
339 |
340 |
341 |
342 | { this.status() }
343 | { this.roastTime() }
344 |
345 |
346 |
347 | alarm
348 | { this.roastDuration() }
349 |
350 |
351 |
352 |
353 |
354 | shopping_basket
355 | { displayWeight(
356 | this.props.batchSize,
357 | this.props.unitSystem
358 | ) } { this.props.weightUnit }
359 |
360 |
361 |
362 | { this.beansMoisture() }
363 |
364 |
365 |
366 |
367 | { this.roastNote() }
368 | { this.postRoastNote() }
369 |
370 |
371 |
372 | );
373 | }
374 | }
375 |
376 | export default RoastProfile;
377 |
--------------------------------------------------------------------------------
/src/components/Settings.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import C from '../constants';
4 | import Card from './utils/Card';
5 | import CardTitle from './utils/CardTitle';
6 | import CardAction from './utils/CardAction';
7 | import CardContent from './utils/CardContent';
8 | import Radio from './utils/Radio';
9 |
10 | class Settings extends React.Component {
11 | shouldComponentUpdate(nextProps) {
12 | return nextProps.unitSystem !== this.props.unitSystem;
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 | settings
21 | Settings
22 |
23 |
24 |
25 |
26 | straighten
27 | Unit System
28 |
29 |
30 |
31 |
32 |
52 |
53 |
54 |
55 | );
56 | }
57 | }
58 |
59 | export default Settings;
60 |
--------------------------------------------------------------------------------
/src/components/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Spinner = () => (
4 |
5 | );
6 |
7 | export default Spinner;
8 |
--------------------------------------------------------------------------------
/src/components/StopWatch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import C from '../constants';
3 |
4 | import Button from './utils/Button';
5 | import Card from './utils/Card';
6 | import CardTitle from './utils/CardTitle';
7 | import CardAction from './utils/CardAction';
8 | import CardContent from './utils/CardContent';
9 |
10 | class StopWatch extends React.Component {
11 |
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | elapsed: 0
16 | };
17 | }
18 |
19 | componentWillMount() {
20 | // If already in progress, continue stopwatch.
21 | if (this.props.status === C.ROAST_IN_PROGRESS) {
22 | this.props.resumeStopWatch(
23 | this.props.roastId,
24 | this.props.roastStart,
25 | setInterval(() => {
26 | this.setState({
27 | elapsed: Date.now() - this.props.roastStart
28 | });
29 | }, 100)
30 | );
31 | }
32 | }
33 |
34 |
35 | startButton() {
36 | let content = null;
37 |
38 | if (this.props.status === C.ROAST_PENDING) {
39 | content = (
40 |
57 | );
58 | }
59 |
60 | return content;
61 | }
62 |
63 | stopButton() {
64 | let content = null;
65 |
66 | if (this.props.status === C.ROAST_IN_PROGRESS) {
67 | content = (
68 |
79 | );
80 | }
81 |
82 | return content;
83 | }
84 |
85 | currentElapsedTime() {
86 | let t = this.state.elapsed;
87 | let min = (t / 1000 / 60) << 0;
88 | let sec = (t / 1000) % 60 << 0;
89 | let fsec = (t % 1000) / 100 << 0;
90 |
91 | if (min < 10) {
92 | min = '0' + min;
93 | }
94 |
95 | if (sec < 10) {
96 | sec = '0' + sec;
97 | }
98 |
99 | return `${ min } : ${ sec } : ${ fsec }`;
100 | }
101 |
102 | render() {
103 | let content = null;
104 | if (
105 | this.props.status === C.ROAST_PENDING ||
106 | this.props.status === C.ROAST_IN_PROGRESS
107 | ) {
108 | content = (
109 |
110 |
111 |
112 | timer
113 | Timer
114 |
115 |
116 |
117 | { this.currentElapsedTime() }
118 |
119 |
120 | { this.startButton() }
121 | { this.stopButton() }
122 |
123 |
124 | );
125 | }
126 |
127 | return content;
128 | }
129 | }
130 |
131 | export default StopWatch;
132 |
--------------------------------------------------------------------------------
/src/components/UnitSwitcher.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class UnitSwitcher extends React.Component {
4 | render() {
5 | let switcherId = this.props.customId ? this.props.customId : "bobon-unit-switcher";
6 |
7 | return (
8 |
12 |
20 |
21 |
26 | - {
29 | this.props.updateUnitSystem(this.props.altUnitSystem, this.props.unitSystem);
30 | } }
31 | >
32 | straighten
33 | { this.props.altTempUnit } - { this.props.altWeightUnit }
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default UnitSwitcher;
42 |
--------------------------------------------------------------------------------
/src/components/utils/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Button extends React.Component {
4 |
5 | render() {
6 | if (this.props.disabled === true) {
7 |
8 | return (
9 |
16 | );
17 |
18 | } else {
19 |
20 | return (
21 |
28 | );
29 |
30 | }
31 | }
32 | }
33 |
34 | export default Button;
35 |
--------------------------------------------------------------------------------
/src/components/utils/Card.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Card extends React.Component {
4 | render() {
5 | return (
6 |
7 | { this.props.children }
8 |
9 | );
10 | }
11 | }
12 |
13 | export default Card;
14 |
--------------------------------------------------------------------------------
/src/components/utils/CardAction.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class CardAction extends React.Component {
4 | render() {
5 | return(
6 |
7 | { this.props.children }
8 |
9 | );
10 | }
11 | }
12 |
13 | export default CardAction;
14 |
--------------------------------------------------------------------------------
/src/components/utils/CardContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class CardContent extends React.Component {
4 | render() {
5 | return(
6 |
7 | { this.props.children }
8 |
9 | );
10 | }
11 | }
12 |
13 | export default CardContent;
14 |
--------------------------------------------------------------------------------
/src/components/utils/CardTitle.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class CardTitle extends React.Component {
4 | render() {
5 | return (
6 |
7 | { this.props.children }
8 |
9 | );
10 | }
11 | }
12 |
13 | export default CardTitle;
14 |
--------------------------------------------------------------------------------
/src/components/utils/NavigationLink.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import history from '../../history';
3 |
4 | class NavigationLink extends React.Component {
5 |
6 | static propTypes() {
7 | return {
8 | path: PropTypes.string.isRequired
9 | };
10 | }
11 |
12 | render() {
13 | return(
14 | {
15 | e.preventDefault();
16 | history.push(this.props.path);
17 | } }
18 | >
19 | { this.props.children }
20 |
21 | );
22 | }
23 | }
24 |
25 | export default NavigationLink;
26 |
--------------------------------------------------------------------------------
/src/components/utils/Radio.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Radio extends React.Component {
4 |
5 | shouldComponentUpdate(nextProps) {
6 | return nextProps.checked !== this.props.checked;
7 | }
8 |
9 | radioInput() {
10 | let content = null;
11 |
12 | if (this.props.checked === true) {
13 | content =
21 | } else {
22 | content =
29 | }
30 |
31 | return content;
32 | }
33 |
34 | render() {
35 | return (
36 |
45 | );
46 | }
47 | }
48 |
49 | export default Radio ;
50 |
--------------------------------------------------------------------------------
/src/components/utils/Table.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Table extends React.Component {
4 | render() {
5 | return (
6 |
7 | { this.props.children }
8 |
9 | );
10 | }
11 | }
12 |
13 | export default Table;
14 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase';
2 |
3 | const firebaseConf = {
4 | apiKey: "AIzaSyAHHNXxfL04NxxSYBIawr1qG15p6L9gex0",
5 | authDomain: "bobonroast.firebaseapp.com",
6 | databaseURL: "https://bobonroast.firebaseio.com",
7 | storageBucket: "bobonroast.appspot.com"
8 | };
9 | firebase.initializeApp(firebaseConf);
10 |
11 | const C = {
12 | // Auth actions.
13 | LOGIN_REQUEST: 'LOGIN_REQUEST',
14 | LOGIN_SUCCESS: 'LOGIN_SUCCESS',
15 | LOGIN_FAILED: 'LOGIN_FAILED',
16 | LOGOUT: 'LOGOUT',
17 | LISTENING_TO_AUTH: 'LISTENING_TO_AUTH',
18 |
19 | // Roasts actions.
20 | FETCHING_ROASTS: 'FETCHING_ROASTS',
21 | FETCHED_ROASTS: 'FETCHED_ROASTS',
22 | COMPARE_ROASTS: 'COMPARE_ROASTS',
23 | ADD_FIRST_CRACK: 'ADD_FIRST_CRACK',
24 |
25 | CREATING_NEW_ROAST: 'CREATING_NEW_ROAST',
26 | REMOVE_ROAST: 'REMOVE_ROAST',
27 | CREATE_NEW_ROAST_SUCCESS: 'CREATE_NEW_ROAST_SUCCESS',
28 | CREATE_NEW_ROAST_FAILED: 'CREATE_NEW_ROAST_FAILED',
29 | UPDATE_ROAST_VALUE: 'UPDATE_ROAST_VALUE',
30 | UPDATE_CURRENT_ROAST_VALUE: 'UPDATE_CURRENT_ROAST_VALUE',
31 | TOGGLE_EDITING_FIELD: 'TOGGLE_EDITING_FIELD',
32 |
33 | // Editing types.
34 | FIELD_POST_ROAST_NOTE: 'FIELD_POST_ROAST_NOTE',
35 |
36 | // Editing field staatus.
37 | FIELD_STATUS_LOADING: 'FIELD_STATUS_LOADING',
38 | FIELD_STATUS_EDITING: 'FIELD_STATUS_EDITING',
39 | FIELD_STATUS_NOT_EDITING: 'FIELD_STATUS_NOT_EDITING',
40 |
41 | // Roasting actions.
42 | ROAST_START: 'ROAST_START',
43 | CHECK_ROAST_IN_PROGRESS: 'CHECK_ROAST_IN_PROGRESS',
44 | CLEAR_ROAST_IN_PROGRESS: 'CLEAR_ROAST_IN_PROGRESS',
45 |
46 | // StopWatch actions.
47 | STOPWATCH_START: 'STOPWATCH_START',
48 | STOPWATCH_TICK: 'STOPWATCH_TICK',
49 | STOPWATCH_STOP: 'STOPWATCH_STOP',
50 | STOPWATCH_RESUME: 'STOPWATCH_STOP',
51 |
52 | // New roast status.
53 | ROAST_UNSAVED: 'ROAST_UNSAVED',
54 | ROAST_PENDING: 'ROAST_PENDING',
55 | ROAST_CREATED: 'ROAST_CREATED',
56 | ROAST_IN_PROGRESS: 'ROAST_IN_PROGRESS',
57 | ROAST_COMPLETED: 'ROAST_COMPLETED',
58 |
59 | // Auth states.
60 | LOGGED_IN: 'LOGGED_IN',
61 | LOGGING_IN: 'LOGGING_IN',
62 | LOGGED_OUT: 'LOGGED_OUT',
63 |
64 | // Dialog.
65 | SHOW_DIALOG: 'SHOW_DIALOG',
66 | CLEAR_DIALOG: 'CLEAR_DIALOG',
67 |
68 | // MISC.
69 | FIREBASE: firebase,
70 | CHART_COLORS: ['#B71C1C', '#F9A825', '#AD1457', '#00796B', '#26C6DA', '#388E3C'],
71 |
72 | // Data loading.
73 | LOADING_DATA: 'LOADING_DATA',
74 | LOADED_DATA: 'LOADED_DATA',
75 |
76 | // Google analytics
77 | GOOGLE_ANALYTICS_CODE: 'UA-79419231-1',
78 |
79 | // Settings actions.
80 | UPDATE_SETTING: 'UPDATE_SETTING',
81 | FETCHED_SETTINGS: 'FETCHED_SETTINGS',
82 |
83 | // Settings.
84 | METRIC: 'METRIC',
85 | IMPERIAL: 'IMPERIAL',
86 | METRIC_TEMP_SYMBOL: '°C',
87 | METRIC_WEIGHT_SYMBOL: 'kg',
88 | IMPERIAL_TEMP_SYMBOL: '°F',
89 | IMPERIAL_WEIGHT_SYMBOL: 'lbs'
90 | };
91 |
92 | export default C;
93 |
--------------------------------------------------------------------------------
/src/containers/AppContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import App from '../components/App';
3 |
4 | const mapStateToProps = (state, ownProps) => {
5 | return {
6 | ...ownProps,
7 | authStatus: state.auth.authStatus,
8 | dataLoading: state.dataLoading
9 | };
10 | };
11 |
12 | const AppContainer = connect(mapStateToProps)(App);
13 |
14 | export default AppContainer;
15 |
--------------------------------------------------------------------------------
/src/containers/DialogContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Dialog from '../components/Dialog';
3 | import { clearDialog } from '../actions';
4 |
5 | const mapStateToProps = state => {
6 | return {
7 | dialogType: state.dialog.dialogType,
8 | noAction: state.dialog.noAction,
9 | noText: state.dialog.noText,
10 | text: state.dialog.text,
11 | yesAction: state.dialog.yesAction,
12 | yesText: state.dialog.yesText
13 | };
14 | };
15 |
16 | const mapDispatchToProps = dispatch => {
17 | return {
18 | clearDialog: () => {
19 | dispatch(clearDialog());
20 | }
21 | };
22 | }
23 |
24 | const DialogContainer = connect(
25 | mapStateToProps,
26 | mapDispatchToProps
27 | )(Dialog);
28 |
29 | export default DialogContainer;
30 |
--------------------------------------------------------------------------------
/src/containers/DrawerContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Drawer from '../components/Drawer';
3 |
4 | const mapStateToProps = state => {
5 | return {
6 | photoURL: state.auth.photoURL,
7 | userName: state.auth.userName,
8 | authStatus: state.auth.authStatus
9 | };
10 | };
11 |
12 | const DrawerContainer = connect(mapStateToProps)(Drawer);
13 |
14 | export default DrawerContainer;
15 |
--------------------------------------------------------------------------------
/src/containers/HeaderContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Header from '../components/Header';
3 | import { logout } from '../actions';
4 | import history from '../history';
5 |
6 | import C from '../constants';
7 |
8 | const mapStateToProps = (state, ownProps) => {
9 | return {
10 | authStatus: state.auth.authStatus,
11 | location: ownProps.location,
12 | photoURL: state.auth.photoURL,
13 | roastInProgress: state.roastInProgress,
14 | userName: state.auth.userName,
15 | email: state.auth.email
16 | };
17 | };
18 |
19 | const mapDispatchToProps = dispatch => {
20 | return {
21 | logout: e => {
22 | console.log(e);
23 | e.preventDefault();
24 | C.FIREBASE.auth().signOut().then(() => {
25 | dispatch(logout());
26 | location.reload();
27 | });
28 | }
29 | };
30 | }
31 |
32 | const HeaderContainer = connect(
33 | mapStateToProps,
34 | mapDispatchToProps
35 | )(Header);
36 |
37 | export default HeaderContainer;
38 |
--------------------------------------------------------------------------------
/src/containers/LoginFormContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import LoginForm from '../components/LoginForm';
3 | import { loginRequest, loginSuccess } from '../actions';
4 | import auth from '../auth';
5 | import firebase from 'firebase';
6 | import C from '../constants';
7 |
8 | const mapStateToProps = state => {
9 | return {
10 | authStatus: state.auth.authStatus
11 | };
12 | };
13 |
14 | const mapDispatchToProps = dispatch => {
15 | return {
16 | onLoginBtnClick: (e, method) => {
17 | e.preventDefault();
18 | dispatch(loginRequest());
19 |
20 | let authProvider = null;
21 |
22 | switch (method) {
23 | case 'facebook':
24 | authProvider = new firebase.auth.FacebookAuthProvider();
25 | break;
26 |
27 | case 'google':
28 | authProvider = new firebase.auth.GoogleAuthProvider();
29 | break;
30 |
31 | default:
32 | break;
33 | }
34 |
35 | if (window.location.protocol === 'http') {
36 | C.FIREBASE.auth().signInWithPopup(authProvider);
37 | } else {
38 | C.FIREBASE.auth().signInWithRedirect(authProvider).then(v => {
39 | console.log(v);
40 | });
41 | }
42 | }
43 | };
44 | };
45 |
46 | const LoginFormContainer = connect(
47 | mapStateToProps,
48 | mapDispatchToProps
49 | )(LoginForm);
50 |
51 | export default LoginFormContainer;
52 |
--------------------------------------------------------------------------------
/src/containers/MainContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Main from '../components/Main';
3 |
4 | const mapStateToProps = state => {
5 | return {
6 | authStatus: state.auth.authStatus
7 | };
8 | };
9 |
10 | const MainContainer = connect(mapStateToProps)(Main);
11 |
12 | export default MainContainer;
13 |
--------------------------------------------------------------------------------
/src/containers/NewRoastFormContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import C from '../constants';
4 | import NewRoastForm from '../components/NewRoastForm';
5 | import history from '../history';
6 | import {
7 | createNewRoast,
8 | createNewRoastFailed,
9 | updateCurrentRoastValue,
10 | createNewRoastSuccess
11 | } from '../actions';
12 | import {
13 | metricTemp,
14 | metricWeight
15 | } from '../helpers';
16 |
17 | const mapStateToProps = state => {
18 | const roast = state.newRoast;
19 | let disabled = true;
20 | let tempUnit = '°C';
21 | let weightUnit = 'kg';
22 |
23 | if (state.settings.unitSystem === C.IMPERIAL) {
24 | tempUnit = '°F';
25 | weightUnit = 'lbs';
26 | }
27 |
28 | if (roast.initialTemp && roast.beansName && roast.batchSize) {
29 | disabled = false;
30 | }
31 |
32 | return {
33 | batchSize: roast.batchSize || '',
34 | beansMoisture: roast.beansMoisture || '',
35 | beansName: roast.beansName || '',
36 | disabled,
37 | initialTemp: roast.initialTemp || '',
38 | processing: roast.status === C.ROAST_PENDING,
39 | roastInProgress: state.roastInProgress,
40 | roastNote: roast.roastNote || '',
41 | uid: state.auth.uid,
42 | tempUnit,
43 | weightUnit,
44 | unitSystem: state.settings.unitSystem
45 | };
46 | };
47 |
48 | const mapDispatchToProps = dispatch => {
49 | return {
50 | onSubmit: e => {
51 | e.preventDefault();
52 |
53 | const f = e.target;
54 | const unitSystem = f.unitSystem.value;
55 | const roastNote = f.roastNote.value;
56 | const uid = f.uid.value;
57 | const beansName = f.beansName.value;
58 | const beansMoisture = f.beansMoisture.value ? parseFloat(f.beansMoisture.value) : '';
59 | const batchSize = metricWeight(
60 | parseFloat(f.batchSize.value),
61 | unitSystem
62 | );
63 | const initialTemp = metricTemp(
64 | parseFloat(f.initialTemp.value),
65 | unitSystem
66 | );
67 |
68 | // Always convert to metric in database.
69 | if (beansName !== '' && batchSize !== '' && initialTemp !== '' && uid !== '') {
70 | dispatch(createNewRoast({
71 | roastNote,
72 | beansName,
73 | batchSize,
74 | beansMoisture,
75 | initialTemp,
76 | uid
77 | }));
78 |
79 | let roastRef = C.FIREBASE.database().ref(`/roasts/${uid}`);
80 |
81 | roastRef.push({
82 | created: Date.now(),
83 | status: C.ROAST_PENDING,
84 | roastStart: 0,
85 | initialTemp,
86 | roastNote,
87 | beansMoisture,
88 | beansName,
89 | batchSize,
90 | roastPoints: [],
91 | uid
92 | }, err => {
93 | dispatch(createNewRoastFailed(err));
94 | }).then((newRoast) => {
95 | dispatch(createNewRoastSuccess(newRoast));
96 |
97 | // Create initial roast point.
98 | let ref = C.FIREBASE.app().database().ref(`roasts/${uid}/${newRoast.key}/roastPoints`);
99 |
100 | ref.push({
101 | elapsed: 0,
102 | temperature: initialTemp
103 | });
104 |
105 | history.push(`roasts/${newRoast.key}`)
106 | });
107 | }
108 | },
109 |
110 | onChange: e => {
111 | dispatch(updateCurrentRoastValue(e.target.id, e.target.value));
112 | }
113 | };
114 | };
115 |
116 | const NewRoastFormContainer = connect(
117 | mapStateToProps,
118 | mapDispatchToProps
119 | )(NewRoastForm);
120 |
121 | export default NewRoastFormContainer;
122 |
--------------------------------------------------------------------------------
/src/containers/PostRoastNoteFormContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import C from '../constants';
3 | import { updateRoastValue, toggleEditing } from '../actions';
4 | import PostRoastNoteForm from '../components/PostRoastNoteForm';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | let postRoastNote = '';
8 | let editings = state.editingFields[ownProps.roastId] || {};
9 | let editing = C.FIELD_STATUS_LOADING;
10 |
11 | if (state.roasts[ownProps.roastId]) {
12 | postRoastNote = state.roasts[ownProps.roastId].postRoastNote;
13 | }
14 |
15 | return {
16 | editing: editings[C.FIELD_POST_ROAST_NOTE] || C.FIELD_STATUS_NOT_EDITING,
17 | postRoastNote,
18 | roastId: ownProps.roastId
19 | };
20 | };
21 |
22 | const mapDispatchToProps = dispatch => {
23 | return {
24 | onSubmit: e => {
25 |
26 | e.preventDefault();
27 |
28 | let roastId = e.target.roastId.value;
29 | let postRoastNote = e.target.postRoastNote.value;
30 | let uid = C.FIREBASE.auth().currentUser.uid;
31 | let ref = C.FIREBASE.database().ref(`roasts/${uid}/${roastId}/postRoastNote`);
32 |
33 | ref.set(postRoastNote, () => {
34 | dispatch(updateRoastValue(
35 | roastId,
36 | C.FIELD_POST_ROAST_NOTE,
37 | postRoastNote
38 | ));
39 |
40 |
41 | dispatch(toggleEditing(roastId, C.FIELD_POST_ROAST_NOTE));
42 | });
43 | },
44 |
45 | toggleEditing: (roastId, field) => {
46 | dispatch(toggleEditing(roastId, field));
47 | }
48 | };
49 | };
50 |
51 | const PostRoastNoteFormContainer = connect(
52 | mapStateToProps,
53 | mapDispatchToProps
54 | )(PostRoastNoteForm);
55 |
56 | export default PostRoastNoteFormContainer;
57 |
--------------------------------------------------------------------------------
/src/containers/RoastListContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import RoastList from '../components/RoastList';
3 | import { removeRoast } from '../actions';
4 | import C from '../constants';
5 | import { showDialog } from '../actions';
6 | import moment from 'moment';
7 |
8 | const mapStateToProps = state => {
9 | let unitSystem = state.settings.unitSystem;
10 | return {
11 | roasts: state.roasts,
12 | unitSystem,
13 | tempUnit: unitSystem === C.IMPERIAL ? '°F' : '°C',
14 | weightUnit: unitSystem === C.IMPERIAL ? 'lbs' : 'kg'
15 | };
16 | };
17 |
18 | const mapDispatchToProps = dispatch => {
19 | return {
20 | removeRoast: (roastId, beansName) => {
21 | dispatch(showDialog({
22 | yesAction: () => {
23 | let uid = C.FIREBASE.auth().currentUser.uid;
24 | let ref = C.FIREBASE.app().database().ref(`roasts/${uid}/${roastId}`);
25 |
26 | ref.remove(() => {
27 | dispatch(removeRoast(roastId));
28 | });
29 | },
30 | text: `Are you sure to delete "${ beansName }"? This cannot be undone.`
31 | }));
32 | }
33 | };
34 | };
35 |
36 | const RoastListContainer = connect(
37 | mapStateToProps,
38 | mapDispatchToProps
39 | )(RoastList);
40 |
41 | export default RoastListContainer;
42 |
--------------------------------------------------------------------------------
/src/containers/RoastPointInputContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import C from '../constants';
4 | import RoastPointInput from '../components/RoastPointInput';
5 | import { metricTemp } from '../helpers';
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | let unitSystem = state.settings.unitSystem;
9 | return {
10 | roastId: ownProps.roastId,
11 | roastStart: ownProps.roastStart,
12 | status: ownProps.status,
13 | unitSystem,
14 | tempUnit: unitSystem === C.IMPERIAL ? C.IMPERIAL_TEMP_SYMBOL : C.METRIC_TEMP_SYMBOL
15 | };
16 | };
17 |
18 | const mapDispatchToProps = dispatch => {
19 | return {
20 | onSubmit: (e, unitSystem) => {
21 | e.preventDefault();
22 |
23 | const temp = metricTemp(parseFloat(e.target.roastTemp.value), unitSystem);
24 | const roastId = e.target.roastId.value;
25 | const elapsed = Date.now() - parseFloat(e.target.roastStart.value);
26 | const uid = C.FIREBASE.auth().currentUser.uid;
27 | const ref = C.FIREBASE.app().database().ref(`roasts/${uid}/${roastId}/roastPoints`);
28 |
29 | ref.push({
30 | temperature: temp,
31 | elapsed
32 | });
33 |
34 | e.target.roastTemp.value = '';
35 | }
36 | };
37 | };
38 |
39 | const RoastPointInputContainer = connect(
40 | mapStateToProps,
41 | mapDispatchToProps
42 | )(RoastPointInput);
43 |
44 | export default RoastPointInputContainer;
45 |
--------------------------------------------------------------------------------
/src/containers/RoastPointsListContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import RoastPointsList from '../components/RoastPointsList';
3 |
4 | const mapStateToProps = (state, ownProps) => {
5 | let roastId = ownProps.roastId;
6 | let roastPoints = {};
7 |
8 | if (state.roasts.hasOwnProperty(roastId) &&
9 | state.roasts[roastId].hasOwnProperty('roastPoints')
10 | ) {
11 | roastPoints = state.roasts[roastId].roastPoints;
12 | }
13 |
14 | return {
15 | roastPoints
16 | };
17 | };
18 |
19 | const RoastPointsListContainer = connect(
20 | mapStateToProps
21 | )(RoastPointsList);
22 |
23 | export default RoastPointsListContainer;
24 |
--------------------------------------------------------------------------------
/src/containers/RoastProfileContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import RoastProfile from '../components/RoastProfile';
3 | import { compareRoasts, addFirstCrack } from '../actions';
4 | import C from '../constants';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | if (typeof state.roasts[ownProps.params.roastId] !== 'undefined') {
8 | const unitSystem = state.settings.unitSystem;
9 | let compare = null;
10 |
11 | if (state.roasts[ownProps.params.roastId].hasOwnProperty('compare') &&
12 | state.roasts[ownProps.params.roastId].compare
13 | ) {
14 | compare = state.roasts[state.roasts[ownProps.params.roastId].compare];
15 | }
16 |
17 | return {
18 | ...state.roasts[ownProps.params.roastId],
19 | roastId: ownProps.params.roastId,
20 | roastInProgress: state.roastInProgress,
21 | compare,
22 | unitSystem,
23 | tempUnit: unitSystem === C.IMPERIAL ? C.IMPERIAL_TEMP_SYMBOL : C.METRIC_TEMP_SYMBOL,
24 | weightUnit: unitSystem === C.IMPERIAL ? C.IMPERIAL_WEIGHT_SYMBOL : C.METRIC_WEIGHT_SYMBOL,
25 | roastIds: Object.keys(state.roasts).map(roastId => {
26 | return {
27 | id: roastId,
28 | value: state.roasts[roastId].beansName,
29 | roastStart: state.roasts[roastId].roastStart
30 | };
31 | }).filter(v => {
32 | return v.id !== ownProps.params.roastId;
33 | })
34 | };
35 | } else {
36 | return {};
37 | }
38 | };
39 |
40 | const getLastRoastPointId = roastPoints => {
41 | if (roastPoints) {
42 | return Object.keys(roastPoints).pop();
43 | } else {
44 | return null;
45 | }
46 | };
47 |
48 | const mapDispatchToProps = dispatch => {
49 | return {
50 | onChangeCompare: (e, roastId) => {
51 | dispatch(compareRoasts(roastId, e.target.value));
52 | },
53 | compareRoasts: (roastId, compareId) => {
54 | dispatch(compareRoasts(roastId, compareId));
55 | },
56 | undoLastTemperature: (roastId, roastPoints) => {
57 | // Only remove points if there are more than 1 roast points.
58 | // The initial temperature point should never be removed.
59 | if (roastPoints && Object.keys(roastPoints).length > 1) {
60 | let lastRoastPointId = getLastRoastPointId(roastPoints);
61 | let uid = C.FIREBASE.auth().currentUser.uid;
62 | let ref = C.FIREBASE.app().database().ref(`/roasts/${uid}/${roastId}/roastPoints/${lastRoastPointId}`);
63 | ref.remove();
64 | }
65 | },
66 | addFirstCrack: (roastId, roastStart) => {
67 | let uid = C.FIREBASE.auth().currentUser.uid;
68 | let ref = C.FIREBASE.app().database().ref(`/roasts/${uid}/${roastId}/firstCrack`);
69 | let firstCrackTime = Date.now() - roastStart;
70 | dispatch(addFirstCrack(roastId, firstCrackTime));
71 | ref.set(firstCrackTime);
72 | }
73 | };
74 | };
75 |
76 | const RoastProfileContainer = connect(
77 | mapStateToProps,
78 | mapDispatchToProps
79 | )(RoastProfile);
80 |
81 | export default RoastProfileContainer;
82 |
--------------------------------------------------------------------------------
/src/containers/SettingsContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import Settings from '../components/Settings';
4 | import { updateSetting } from '../actions';
5 | import C from '../constants';
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | return {
9 | unitSystem: state.settings.unitSystem
10 | };
11 | };
12 |
13 | const mapDispatchToProps = dispatch => {
14 | return {
15 | onChangeUnitSystem: (e, currentUnitSystem) => {
16 | // Only update if the value changes.
17 | if (e.target.value !== currentUnitSystem) {
18 | let uid = C.FIREBASE.auth().currentUser.uid;
19 | let ref = C.FIREBASE.app().database().ref(`settings/${uid}/unitSystem`);
20 | let value = e.target.value;
21 |
22 | ref.set(value, err => {
23 | if (err) {
24 | console.error(err);
25 | }
26 | });
27 | }
28 | }
29 | };
30 | };
31 |
32 | const SettingsContainer = connect(
33 | mapStateToProps,
34 | mapDispatchToProps
35 | )(Settings);
36 |
37 | export default SettingsContainer;
38 |
--------------------------------------------------------------------------------
/src/containers/StopWatchContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { startStopWatch, resumeStopWatch, tickStopWatch, stopStopWatch, clearRoastInProgress } from '../actions';
3 | import StopWatch from '../components/StopWatch';
4 |
5 | const mapStateToProps = (state, ownProps) => {
6 | return {
7 | elapsed: state.stopWatch.elapsed,
8 | roastId: ownProps.roastId,
9 | roastStart: ownProps.roastStart,
10 | status: ownProps.status,
11 | tick: state.stopWatch.tick
12 | };
13 | };
14 |
15 | const mapDispatchToProps = dispatch => {
16 | return {
17 | startStopWatch: (roastId, roastStart, tick) => {
18 | dispatch(startStopWatch(roastId, roastStart, tick));
19 | },
20 |
21 | resumeStopWatch: (roastId, roastStart, tick) => {
22 | dispatch(resumeStopWatch(roastId, roastStart, tick));
23 | },
24 |
25 | stopStopWatch: (roastId, tick) => {
26 | clearInterval(tick);
27 | dispatch(stopStopWatch(roastId));
28 | }
29 | };
30 | };
31 |
32 | const StopWatchContainer = connect(mapStateToProps, mapDispatchToProps)(StopWatch);
33 |
34 | export default StopWatchContainer;
35 |
--------------------------------------------------------------------------------
/src/containers/UnitSwitcherContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import C from '../constants';
4 | import UnitSwitcher from '../components/UnitSwitcher';
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | let unitSystem = state.settings.unitSystem;
8 | return {
9 | unitSystem,
10 | altUnitSystem: unitSystem === C.IMPERIAL ? C.METRIC : C.IMPERIAL,
11 | tempUnit: unitSystem === C.IMPERIAL ? C.IMPERIAL_TEMP_SYMBOL : C.METRIC_TEMP_SYMBOL,
12 | weightUnit: unitSystem === C.IMPERIAL ? C.IMPERIAL_WEIGHT_SYMBOL : C.METRIC_WEIGHT_SYMBOL,
13 | altTempUnit: unitSystem === C.IMPERIAL ? C.METRIC_TEMP_SYMBOL : C.IMPERIAL_TEMP_SYMBOL,
14 | altWeightUnit: unitSystem === C.IMPERIAL ? C.METRIC_WEIGHT_SYMBOL : C.IMPERIAL_WEIGHT_SYMBOL,
15 | customClass: ownProps.customClass
16 | };
17 | };
18 |
19 | const mapDispatchToProps = dispatch => {
20 | return {
21 | updateUnitSystem: (unitSystem, currentUnitSystem) => {
22 | if (unitSystem !== currentUnitSystem) {
23 | const uid = C.FIREBASE.auth().currentUser.uid;
24 | const ref = C.FIREBASE.app().database().ref(`/settings/${ uid }/unitSystem`);
25 | ref.set(unitSystem, err => {
26 | if (err) {
27 | console.error(err);
28 | }
29 | });
30 | }
31 | }
32 | };
33 | };
34 |
35 | const UnitSwitcherContainer = connect(
36 | mapStateToProps,
37 | mapDispatchToProps
38 | )(UnitSwitcher);
39 |
40 | export default UnitSwitcherContainer;
41 |
--------------------------------------------------------------------------------
/src/demoData.js:
--------------------------------------------------------------------------------
1 | export const demoDataset = {
2 | "labels":[],
3 | "datasets":[
4 | {"label":"temp 1","data":[{"x":0,"y":150},{"x":0.39416666666666667,"y":137},{"x":0.6643,"y":130},{"x":0.82205,"y":125},{"x":1.0293666666666668,"y":120},{"x":1.4826833333333334,"y":110},{"x":1.78335,"y":105},{"x":1.9583,"y":104},{"x":2.109383333333333,"y":101},{"x":2.386433333333333,"y":100},{"x":2.7411166666666666,"y":101},{"x":3.10165,"y":105},{"x":3.5406833333333334,"y":110},{"x":3.935033333333333,"y":115},{"x":4.254833333333333,"y":120},{"x":4.64535,"y":125},{"x":5.0022,"y":130},{"x":5.331666666666667,"y":135},{"x":5.69025,"y":140},{"x":5.9647,"y":145},{"x":6.289883333333333,"y":150},{"x":6.628333333333333,"y":155},{"x":6.991483333333333,"y":160},{"x":7.361516666666667,"y":165},{"x":7.900833333333333,"y":170},{"x":8.356983333333334,"y":175},{"x":8.84215,"y":180},{"x":9.2757,"y":185},{"x":9.769883333333333,"y":190},{"x":10.20775,"y":195},{"x":10.635633333333333,"y":200},{"x":11.014816666666666,"y":205},{"x":11.445233333333332,"y":210},{"x":11.8819,"y":215},{"x":12.250016666666667,"y":216},{"x":13.099216666666667,"y":212}],"fill":false,"yAxisID":"temp","borderColor":"rgba(183,28,28,0.4)","backgroundColor":"rgba(183,28,28,0.4)","borderWidth":1},{"label":"rate 1","data":[{"x":0,"y":0},{"x":0.39416666666666667,"y":"-32.98"},{"x":0.6643,"y":"-25.91"},{"x":0.82205,"y":"-31.70"},{"x":1.0293666666666668,"y":"-24.12"},{"x":1.4826833333333334,"y":"-22.06"},{"x":1.78335,"y":"-16.63"},{"x":1.9583,"y":"-5.72"},{"x":2.109383333333333,"y":"-19.86"},{"x":2.386433333333333,"y":"-3.61"},{"x":2.7411166666666666,"y":"2.82"},{"x":3.10165,"y":"11.09"},{"x":3.5406833333333334,"y":"11.39"},{"x":3.935033333333333,"y":"12.68"},{"x":4.254833333333333,"y":"15.63"},{"x":4.64535,"y":"12.80"},{"x":5.0022,"y":"14.01"},{"x":5.331666666666667,"y":"15.18"},{"x":5.69025,"y":"13.94"},{"x":5.9647,"y":"18.22"},{"x":6.289883333333333,"y":"15.38"},{"x":6.628333333333333,"y":"14.77"},{"x":6.991483333333333,"y":"13.77"},{"x":7.361516666666667,"y":"13.51"},{"x":7.900833333333333,"y":"9.27"},{"x":8.356983333333334,"y":"10.96"},{"x":8.84215,"y":"10.31"},{"x":9.2757,"y":"11.53"},{"x":9.769883333333333,"y":"10.12"},{"x":10.20775,"y":"11.42"},{"x":10.635633333333333,"y":"11.69"},{"x":11.014816666666666,"y":"13.19"},{"x":11.445233333333332,"y":"11.62"},{"x":11.8819,"y":"11.45"},{"x":12.250016666666667,"y":"2.72"},{"x":13.099216666666667,"y":"-4.71"}],"fill":false,"yAxisID":"rate","fill": true,"borderColor":"rgba(249,168,37,0.4)","backgroundColor":"rgba(249,168,37,0.4)","borderWidth":1},{"label":"first crack 1","data":[{"x":11.204766666666666,"y":50},{"x":11.204766666666666,"y":1000}],"fill":false,"yAxisID":"temp","fill": true,"borderColor":"#AD1457","backgroundColor":"#AD1457","borderWidth":1},{"label":"temp 2","data":[{"x":0,"y":150},{"x":0.60815,"y":125},{"x":1.2954666666666668,"y":110},{"x":1.66715,"y":107},{"x":2.28505,"y":107},{"x":2.5820333333333334,"y":106},{"x":3.4804833333333334,"y":110},{"x":3.9413666666666667,"y":115},{"x":4.418933333333333,"y":120},{"x":4.787316666666666,"y":126},{"x":5.7703,"y":140},{"x":6.165016666666666,"y":150},{"x":6.567033333333334,"y":155},{"x":6.8451,"y":160},{"x":7.836616666666667,"y":175},{"x":8.264933333333333,"y":180},{"x":8.595533333333334,"y":185},{"x":8.961233333333332,"y":190},{"x":9.322966666666666,"y":195},{"x":9.685816666666666,"y":200},{"x":10.03865,"y":205},{"x":10.4486,"y":210},{"x":10.86455,"y":215},{"x":11.5202,"y":219},{"x":11.8839,"y":220}],"fill":false,"yAxisID":"temp","borderColor":"rgba(0,121,107,0.4)","backgroundColor":"rgba(0,121,107,0.4)","borderWidth":1},{"label":"rate 2","data":[{"x":0,"y":0},{"x":0.60815,"y":"-41.11"},{"x":1.2954666666666668,"y":"-21.82"},{"x":1.66715,"y":"-8.07"},{"x":2.28505,"y":"0.00"},{"x":2.5820333333333334,"y":"-3.37"},{"x":3.4804833333333334,"y":"4.45"},{"x":3.9413666666666667,"y":"10.85"},{"x":4.418933333333333,"y":"10.47"},{"x":4.787316666666666,"y":"16.29"},{"x":5.7703,"y":"14.24"},{"x":6.165016666666666,"y":"25.33"},{"x":6.567033333333334,"y":"12.44"},{"x":6.8451,"y":"17.98"},{"x":7.836616666666667,"y":"15.13"},{"x":8.264933333333333,"y":"11.67"},{"x":8.595533333333334,"y":"15.12"},{"x":8.961233333333332,"y":"13.67"},{"x":9.322966666666666,"y":"13.82"},{"x":9.685816666666666,"y":"13.78"},{"x":10.03865,"y":"14.17"},{"x":10.4486,"y":"12.20"},{"x":10.86455,"y":"12.02"},{"x":11.5202,"y":"6.10"},{"x":11.8839,"y":"2.75"}],"fill":false,"yAxisID":"rate","fill": true,"borderColor":"rgba(38,198,218,0.4)","backgroundColor":"rgba(38,198,218,0.4)","borderWidth":1},{"label":"first crack 2","data":[{"x":10.182066666666667,"y":50},{"x":10.182066666666667,"y":1000}],"fill":false,"yAxisID":"temp","fill": true,"borderColor":"#388E3C","backgroundColor":"#388E3C","borderWidth":1}]};
5 |
6 | export const demoChartOptions = {
7 | "defaultFontFamily":"Roboto",
8 | "scales": {
9 | "xAxes": [
10 | {
11 | "type":"linear",
12 | "position":"bottom",
13 | "id":"time",
14 | "ticks": {
15 | "max":12.099216666666667,
16 | "min":0,
17 | "stepSize":1
18 | }
19 | }
20 | ],
21 | "yAxes": [
22 | {
23 | "id":"temp",
24 | "type":"linear",
25 | "position":"left",
26 | "ticks": {
27 | "max":270,
28 | "min":50,
29 | "stepSize":10
30 | }
31 | },
32 | {
33 | "id":"rate",
34 | "position":"right",
35 | "type":"linear",
36 | "ticks": {
37 | "max":55,
38 | "min":0,
39 | "stepSize":5
40 | }
41 | }
42 | ]
43 | },
44 | "maintainAspectRatio": true,
45 | "responsive": false,
46 | "tooltips": {
47 | "titleFontFamily":"Roboto",
48 | "titleFontStyle":"normal",
49 | "callbacks": {
50 | title: (item, data) => {
51 | let xLabel = item[0].xLabel;
52 | let min = xLabel / 1 << 0;
53 | let sec = xLabel % 1 * 60 << 0;
54 | if (sec < 10) {
55 | sec = '0' + sec;
56 | }
57 | if (min < 10) {
58 | min = '0' + min;
59 | }
60 | return 'elapsed: ' + min + ':' + sec;
61 | }
62 | }
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | import C from './constants';
2 |
3 | // Data is stored in database in metric form.
4 | export const metricWeight = (weight, unitSystem) => {
5 | if (unitSystem === C.IMPERIAL) {
6 | return weight * 0.453592;
7 | } else {
8 | return weight;
9 | }
10 | };
11 |
12 | export const metricTemp = (temp, unitSystem) => {
13 | if (unitSystem === C.IMPERIAL) {
14 | return (temp - 32) / 1.8;
15 | } else {
16 | return temp;
17 | }
18 | };
19 |
20 | // When displaying data, convert from metric to imperial when needed.
21 | export const displayTemp = (temp, unitSystem, fixed = true) => {
22 | let result = temp;
23 | if (unitSystem === C.IMPERIAL) {
24 | result = temp * 1.8 + 32;
25 | }
26 | return fixed ? result.toFixed(2) : result;
27 | };
28 |
29 | export const displayWeight = (weight, unitSystem, fixed = true) => {
30 | let result = weight;
31 | if (unitSystem === C.IMPERIAL) {
32 | result = weight / 0.454592;
33 | }
34 | return fixed ? result.toFixed(2) : result;
35 | };
36 |
--------------------------------------------------------------------------------
/src/history.js:
--------------------------------------------------------------------------------
1 | import { hashHistory } from 'react-router';
2 | //import { browserHistory } from 'react-router';
3 |
4 | const history = hashHistory;
5 | //const history = browserHistory;
6 |
7 | export default history;
8 |
--------------------------------------------------------------------------------
/src/reducers/authReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 | import history from '../history';
3 |
4 | const initialState = {
5 | authProvider: null,
6 | authStatus: C.LOGGING_IN,
7 | email: null,
8 | listeningToAuth: false,
9 | photoURL: null,
10 | uid: null,
11 | userName: null
12 | };
13 |
14 | const authReducer = (currentState = initialState, action) => {
15 |
16 | switch(action.type) {
17 |
18 | case C.LISTENING_TO_AUTH:
19 | return {
20 | ...currentState,
21 | listeningToAuth: true
22 | };
23 | break;
24 |
25 | case C.LOGIN_REQUEST:
26 | return {
27 | ...currentState,
28 | authStatus: C.LOGGING_IN
29 | };
30 | break;
31 |
32 | case C.LOGIN_SUCCESS:
33 | return {
34 | ...currentState,
35 | authStatus: C.LOGGED_IN,
36 | uid: action.user.uid,
37 | photoURL: action.user.photoURL,
38 | userName: action.user.displayName,
39 | email: action.user.email
40 | };
41 | break;
42 |
43 | case C.LOGOUT:
44 | return {
45 | ...currentState,
46 | authStatus: C.LOGGED_OUT,
47 | uid: null,
48 | userName: null,
49 | photoURL: null,
50 | userName: null,
51 | email: null
52 | };
53 | break;
54 |
55 | default:
56 | return currentState;
57 | }
58 | };
59 |
60 | export default authReducer;
61 |
--------------------------------------------------------------------------------
/src/reducers/dataLoadingReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 | import history from '../history';
3 |
4 | const initialState = false;
5 |
6 | const dataLoadingReducer = (currentState = initialState, action) => {
7 |
8 | switch(action.type) {
9 |
10 | case C.LOADING_DATA:
11 | return true;
12 | break;
13 |
14 | case C.LOADED_DATA:
15 | default:
16 | return false;
17 | break;
18 | }
19 | };
20 |
21 | export default dataLoadingReducer;
22 |
--------------------------------------------------------------------------------
/src/reducers/dialogReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 | import history from '../history';
3 |
4 | const initialState = {
5 | text: null,
6 | yesAction: null,
7 | noAction: null,
8 | yesText: null,
9 | noText: null,
10 | dialogType: null
11 | };
12 |
13 | const dialogReducer = (currentState = initialState, action) => {
14 |
15 | switch(action.type) {
16 | case C.SHOW_DIALOG:
17 | return {
18 | dialogType: action.dialogType,
19 | noAction: action.noAction,
20 | noText: action.noText || 'No',
21 | text: action.text,
22 | yesAction: action.yesAction,
23 | yesText: action.yesText || 'Yes'
24 | };
25 | break;
26 |
27 | case C.CLEAR_DIALOG:
28 | return initialState;
29 | break;
30 |
31 | default:
32 | return initialState;
33 | break;
34 | }
35 | }
36 |
37 | export default dialogReducer;
38 |
--------------------------------------------------------------------------------
/src/reducers/editingFieldReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 |
3 | const initialState = {};
4 |
5 | const editingFieldReducer = (currentState = initialState, action) => {
6 | switch(action.type) {
7 |
8 | case C.TOGGLE_EDITING_FIELD:
9 | let editing = C.FIELD_STATUS_EDITING;
10 | let roastFields = {};
11 | let editingFields = currentState;
12 |
13 | if (editingFields.hasOwnProperty(action.roastId)) {
14 | roastFields = editingFields[action.roastId];
15 | if (roastFields.hasOwnProperty(action.field)) {
16 | if (roastFields[action.field] === C.FIELD_STATUS_EDITING) {
17 | editing = C.FIELD_STATUS_NOT_EDITING;
18 | } else if (roastFields[action.field] === C.FIELD_STATUS_NOT_EDITING) {
19 | editing = C.FIELD_STATUS_EDITING;
20 | }
21 | }
22 | }
23 |
24 | return {
25 | ...currentState,
26 | [action.roastId]: {
27 | ...roastFields,
28 | [action.field]: editing
29 | }
30 | };
31 | break;
32 |
33 | default:
34 | return currentState;
35 | break;
36 | }
37 | };
38 |
39 | export default editingFieldReducer;
40 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import authReducer from './authReducer';
2 | import dataLoadingReducer from './dataLoadingReducer';
3 | import dialogReducer from './dialogReducer';
4 | import editingFieldReducer from './editingFieldReducer';
5 | import newRoastReducer from './newRoastReducer';
6 | import roastInProgressReducer from './roastInProgressReducer';
7 | import roastsReducer from './roastsReducer';
8 | import settingsReducer from './settingsReducer';
9 | import stopWatchReducer from './stopWatchReducer';
10 | import { combineReducers } from 'redux';
11 |
12 | export default combineReducers({
13 | auth: authReducer,
14 | dataLoading: dataLoadingReducer,
15 | dialog: dialogReducer,
16 | editingFields: editingFieldReducer,
17 | newRoast: newRoastReducer,
18 | roastInProgress: roastInProgressReducer,
19 | roasts: roastsReducer,
20 | settings: settingsReducer,
21 | stopWatch: stopWatchReducer
22 | });
23 |
--------------------------------------------------------------------------------
/src/reducers/newRoastReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 | import history from '../history';
3 |
4 | const initialState = {
5 | // New roast details.
6 | roastNote: null,
7 | beansName: null,
8 | batchSize: null,
9 | moisture: null,
10 |
11 | // Roast monitor.
12 | status: C.ROAST_UNSAVED
13 | };
14 |
15 | const newRoastReducer = (currentState = initialState, action) => {
16 | switch(action.type) {
17 |
18 | case C.CREATING_NEW_ROAST:
19 | return {
20 | ...currentState,
21 | status: C.ROAST_PENDING
22 | };
23 | break;
24 |
25 | case C.CREATE_NEW_ROAST_SUCCESS:
26 | return initialState;
27 | break;
28 |
29 | case C.CREATE_NEW_ROAST_FAILED:
30 | return {
31 | ...currentState,
32 | status: C.ROAST_UNSAVED
33 | };
34 | break;
35 |
36 | case C.UPDATE_CURRENT_ROAST_VALUE:
37 | return {
38 | ...currentState,
39 | [action.field]: action.value
40 | };
41 | break;
42 |
43 | default:
44 | return currentState;
45 | }
46 | };
47 |
48 | export default newRoastReducer;
49 |
--------------------------------------------------------------------------------
/src/reducers/roastInProgressReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 |
3 | const initialState = null;
4 |
5 | const roastInProgressReducer = (currentState = initialState, action) => {
6 | switch(action.type) {
7 |
8 | case C.CHECK_ROAST_IN_PROGRESS:
9 | if (action.roasts) {
10 | let roastId = currentState;
11 | let roastInProgress = Object.keys(action.roasts).find(key => {
12 | return action.roasts[key].status === C.ROAST_IN_PROGRESS;
13 | });
14 |
15 | if (roastInProgress) {
16 | roastId = roastInProgress;
17 | } else {
18 | roastId = initialState;
19 | }
20 |
21 | return roastId;
22 | } else {
23 | return null;
24 | }
25 | break;
26 |
27 | case C.CLEAR_ROAST_IN_PROGRESS:
28 | return initialState;
29 | break;
30 |
31 | default:
32 | return currentState;
33 | }
34 | };
35 |
36 | export default roastInProgressReducer;
37 |
--------------------------------------------------------------------------------
/src/reducers/roastsReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 | import history from '../history';
3 |
4 | const initialState = {};
5 |
6 | const roastsReducer = (currentState = initialState, action) => {
7 | switch(action.type) {
8 |
9 | case C.FETCHED_ROASTS:
10 | let inversedRoasts = {};
11 | if (action.roasts) {
12 | Object.keys(action.roasts).sort((a, b)=> {
13 | return action.roasts[b].created - action.roasts[a].created;
14 | }).forEach(key => {
15 | inversedRoasts[key] = action.roasts[key];
16 | });
17 | }
18 | return inversedRoasts;
19 | break;
20 |
21 | case C.COMPARE_ROASTS:
22 | return {
23 | ...currentState,
24 | [action.roastId]: {
25 | ...currentState[action.roastId],
26 | compare: action.compareId
27 | }
28 | };
29 | break;
30 |
31 | case C.ADD_FIRST_CRACK:
32 | return {
33 | ...currentState,
34 | [action.roastId]: {
35 | ...currentState[action.roastId],
36 | firstCrack: action.firstCrackTime
37 | }
38 | };
39 | break;
40 |
41 | default:
42 | return currentState;
43 | }
44 | };
45 |
46 | export default roastsReducer;
47 |
--------------------------------------------------------------------------------
/src/reducers/settingsReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 |
3 | const initialState = {
4 | unitSystem: C.METRIC
5 | };
6 |
7 | let settingsReducer = (currentState = initialState, action) => {
8 | switch (action.type) {
9 | case C.UPDATE_SETTING:
10 | return {
11 | ...currentState,
12 | [action.setting]: action.value
13 | };
14 | break;
15 |
16 | case C.FETCHED_SETTINGS:
17 | return {
18 | ...currentState,
19 | ...action.settings
20 | };
21 | break;
22 |
23 | default:
24 | return currentState;
25 | break;
26 | }
27 | }
28 |
29 | export default settingsReducer;
30 |
--------------------------------------------------------------------------------
/src/reducers/stopWatchReducer.js:
--------------------------------------------------------------------------------
1 | import C from '../constants';
2 |
3 | const initialState = {
4 | roastStart: null,
5 | tick: null
6 | };
7 |
8 | const stopWatchReducer = (currentState = initialState, action) => {
9 | switch(action.type) {
10 |
11 | case C.STOPWATCH_START:
12 | case C.STOPWATCH_RESUME:
13 | return {
14 | roastStart: action.roastStart,
15 | tick: action.tick
16 | };
17 | break;
18 |
19 | case C.STOPWATCH_STOP:
20 | return initialState;
21 | break;
22 |
23 | default:
24 | return currentState;
25 | }
26 | };
27 |
28 | export default stopWatchReducer;
29 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 | var srcPath = path.resolve(__dirname, 'src');
4 |
5 | module.exports = {
6 | entry: {
7 | bundle: path.join(srcPath, 'app.jsx'),
8 | common: ['react', 'react-router', 'redux', 'react-redux', 'moment']
9 | },
10 |
11 | output: {
12 | path: path.resolve(__dirname, 'public'),
13 | filename: '[name].js',
14 | publicPath: '/'
15 | },
16 |
17 | resolve: {
18 | extensions: ['', '.jsx', '.js'],
19 | modulesDirectories: ['node_modules', 'bower_components']
20 | },
21 |
22 | module: {
23 | loaders: [
24 | {
25 | loader: 'babel',
26 | exclude: /(node_modules|bower_components)/
27 | },
28 | {
29 | test: /\.(jpg|png)$/,
30 | loaders: ['url?limit=25000'],
31 | include: path.resolve(__dirname, 'images')
32 | },
33 | {
34 | test: /\.scss$/,
35 | loaders: [
36 | 'style',
37 | 'css?sourceMap&-restructuring&aggressiveMerging',
38 | 'autoprefixer',
39 | 'sass?sourceMap'
40 | ]
41 | }
42 | ]
43 | },
44 |
45 | plugins: [
46 | new webpack.optimize.DedupePlugin(),
47 | new webpack.optimize.OccurrenceOrderPlugin(),
48 | new webpack.optimize.CommonsChunkPlugin(
49 | {
50 | name: "common",
51 | minChunks: 2
52 | }
53 | ),
54 | new webpack.DefinePlugin({
55 | 'process.env': {
56 | 'NODE_ENV': JSON.stringify('production')
57 | }
58 | })
59 | ]
60 | };
61 |
--------------------------------------------------------------------------------