├── .babelrc ├── .eslintrc.js ├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── app ├── components │ ├── AlignTheme.jsx │ ├── CheckInForm.jsx │ ├── CheckInFormContainer.jsx │ ├── Doorslam.jsx │ ├── Empty.jsx │ ├── GoalForm.jsx │ ├── GoalFormContainer.jsx │ ├── Landing.jsx │ ├── Loader.jsx │ ├── LocalSignin.jsx │ ├── LocalSignup.jsx │ ├── Login.jsx │ ├── Login.test.jsx │ ├── MilestoneForm.jsx │ ├── MilestoneFormContainer.jsx │ ├── Navbar.jsx │ ├── NotFound.jsx │ ├── ResourceCard.jsx │ ├── ResourceContainer.jsx │ ├── ResourceForm.jsx │ ├── Timelines.jsx │ ├── Upload.jsx │ ├── UploadCard.jsx │ ├── WhoAmI.jsx │ └── WhoAmI.test.jsx └── main.jsx ├── bin ├── build-branch.sh ├── deploy-heroku.sh ├── mkapplink.js └── setup ├── database.rules.json ├── dev.js ├── fire ├── index.js └── refs.js ├── firebase.json ├── functions ├── index.js ├── package.json └── yarn.lock ├── index.js ├── node_modules └── APP ├── package.json ├── public ├── default-placeholder.jpg ├── favicon.ico ├── index.html ├── lines.png ├── lines2.png ├── lines3.png ├── logo-large.png ├── logo-white.jpg ├── not-favicon.ico ├── old-logo.jpg └── style.css ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-2" 6 | ] 7 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "eslint-config-standard", 3 | root: true, 4 | parser: "babel-eslint", 5 | parserOptions: { 6 | sourceType: "module", 7 | ecmaVersion: 8 8 | }, 9 | ecmaFeatures: { 10 | jsx: true, 11 | }, 12 | plugins: ['react'], 13 | rules: { 14 | "space-before-function-paren": ["error", "never"], 15 | "prefer-const": "warn", 16 | "comma-dangle": ["error", "only-multiline"], 17 | "space-infix-ops": "off", // Until eslint #7489 lands 18 | "new-cap": "off", 19 | "no-unused-vars": ["error", { "varsIgnorePattern": "^_" }], 20 | "no-return-assign": "off", 21 | "no-unused-expressions": "off", 22 | "one-var": "off", 23 | "new-parens": "off", 24 | "indent": ["error", 2, {SwitchCase: 0}], 25 | "arrow-body-style": ["warn", "as-needed"], 26 | 27 | "no-unused-vars": "off", 28 | "react/jsx-uses-react": "error", 29 | "react/jsx-uses-vars": "error", 30 | "react/react-in-jsx-scope": "error", 31 | 32 | "import/first": "off", 33 | "operator-linebreak": "off", 34 | 35 | // This rule enforces a comma-first style, such as 36 | // npm uses. I think it's great, but it can look a bit weird, 37 | // so we're leaving it off for now (although stock Bones passes 38 | // the linter with it on). If you decide you want to enforce 39 | // this rule, change "off" to "error". 40 | "comma-style": ["off", "first", { 41 | exceptions: { 42 | ArrayExpression: true, 43 | ObjectExpression: true, 44 | } 45 | }], 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "align-a0b08": "align-a0b08", 4 | "production": "align-a0b08", 5 | "deploy": "align-a0b08" 6 | } 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all node_modules 2 | node_modules/* 3 | 4 | # ...except the symlink to ourselves. 5 | !node_modules/APP 6 | 7 | # Compiled JS 8 | public/bundle.js 9 | public/bundle.js.map 10 | 11 | # NPM errors 12 | npm-debug.log 13 | 14 | # Firebase debug log 15 | firebase-debug.log 16 | 17 | # DS_Store 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 Fullstack Academy of Code 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Align 2 | 3 | ## [align.fun](https://align.fun) 4 | Align is a dynamic web application for setting and managing long-term goals on a beautiful and intuitive interface. Users can create goals, check in, set milestones, save helpful resources, and store personal photos and videos along the journey. 5 | 6 | ## Composition 7 | Align uses: 8 | * Firebase 9 | * React (& React Router) 10 | * Node.js 11 | * Victory.JS 12 | * Material-UI 13 | * React-Bootstrap (for grid) 14 | * Webpack 15 | * Babel 16 | 17 | ## Creators 18 | Align is a collaboration between: 19 | * [Melanie Mohn](https://github.com/melaniemohn) 20 | * [Sara Kladky](https://github.com/kladky) 21 | * [Sophia Ciocca](https://github.com/sophiaciocca) 22 | -------------------------------------------------------------------------------- /app/components/AlignTheme.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | cyan500, cyan700, 3 | pinkA200, 4 | grey100, grey300, grey400, grey500, 5 | white, darkBlack, fullBlack, 6 | } from 'material-ui/styles/colors' 7 | import {fade} from 'material-ui/utils/colorManipulator' 8 | import spacing from 'material-ui/styles/spacing' 9 | 10 | export default { 11 | spacing: spacing, 12 | fontFamily: 'Roboto, sans-serif', 13 | palette: { 14 | primary1Color: '#888888', 15 | primary2Color: '#888888', 16 | primary3Color: '#888888', 17 | accent1Color: '#D17A83', 18 | accent2Color: '#D17A83', 19 | accent3Color: '#D17A83', 20 | textColor: darkBlack, 21 | alternateTextColor: white, 22 | canvasColor: white, 23 | borderColor: grey300, 24 | disabledColor: fade(darkBlack, 0.3), 25 | pickerHeaderColor: cyan500, 26 | clockCircleColor: fade(darkBlack, 0.07), 27 | shadowColor: fullBlack, 28 | }, 29 | flatButton: { primaryTextColor: '#888888'} 30 | } 31 | -------------------------------------------------------------------------------- /app/components/CheckInForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, browserHistory } from 'react-router' 3 | 4 | import firebase from 'APP/fire' 5 | const db = firebase.database() 6 | const goalsRef = db.ref('goals') 7 | let nameRef, descriptionRef, dateRef, uploadsRef, parentRef, notesRef 8 | 9 | import ReactQuill from 'react-quill' 10 | 11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 12 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 13 | import alignTheme from './AlignTheme' 14 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 15 | import TextField from 'material-ui/TextField' 16 | import SelectField from 'material-ui/SelectField' 17 | import MenuItem from 'material-ui/MenuItem' 18 | import DatePicker from 'material-ui/DatePicker' 19 | import RaisedButton from 'material-ui/RaisedButton' 20 | import Close from 'material-ui/svg-icons/navigation/close' 21 | import UploadForm from './Upload' 22 | import UploadCard from './UploadCard' 23 | 24 | export default class extends React.Component { 25 | constructor(props) { 26 | super() 27 | this.state = { 28 | name: '', 29 | description: '', 30 | isOpen: true, // Default checkin status is open 31 | date: new Date().getTime(), // Default date is today 32 | notes: '' 33 | } 34 | } 35 | 36 | componentDidMount() { 37 | // When the component mounts, start listening to the fireRef 38 | // we were given. 39 | this.listenTo(this.props.fireRef) 40 | } 41 | 42 | componentWillUnmount() { 43 | // When we unmount, stop listening. 44 | this.unsubscribe() 45 | } 46 | 47 | componentWillReceiveProps(incoming, outgoing) { 48 | // When the props sent to us by our parent component change, 49 | // start listening to the new firebase reference. 50 | this.listenTo(incoming.fireRef) 51 | } 52 | 53 | listenTo(fireRef) { 54 | // If we're already listening to a ref, stop listening there. 55 | if (this.unsubscribe) this.unsubscribe() 56 | 57 | // Set up aliases for our Firebase references: 58 | nameRef = fireRef.nameRef 59 | descriptionRef = fireRef.descriptionRef 60 | dateRef = fireRef.dateRef 61 | uploadsRef = fireRef.uploadsRef 62 | parentRef = fireRef.parentRef 63 | notesRef = fireRef.notesRef 64 | 65 | // Whenever a ref's value changes, set {value} on our state: 66 | const nameListener = nameRef.on('value', snapshot => 67 | this.setState({ name: snapshot.val() || '' })) 68 | 69 | const descriptionListener = descriptionRef.on('value', snapshot => { 70 | this.setState({ description: snapshot.val() || '' }) 71 | }) 72 | 73 | const dateListener = dateRef.on('value', snapshot => { 74 | this.setState({ date: snapshot.val() }) 75 | if (snapshot.val() === null) dateRef.set(new Date().getTime()) 76 | }) 77 | 78 | const uploadsListener = uploadsRef.on('value', snapshot => { 79 | if (snapshot.val()) this.setState({ uploads: Object.entries(snapshot.val()) }) 80 | }) 81 | 82 | const notesListener = notesRef.on('value', snapshot => { 83 | if (snapshot.val()) this.setState({ notes: snapshot.val() }) 84 | }) 85 | 86 | // Set unsubscribe to be a function that detaches the listener. 87 | this.unsubscribe = () => { 88 | nameRef.off('value', nameListener) 89 | descriptionRef.off('value', descriptionListener) 90 | dateRef.off('value', dateListener) 91 | uploadsRef.off('value', uploadsListener) 92 | notesRef.off('value', notesListener) 93 | } 94 | } 95 | 96 | // These 'write' functions are defined using class property syntax, 97 | // which is like 'this.writeName = event => (etc.), 98 | // which means they're always bound to 'this'. 99 | writeName = (event) => { 100 | nameRef.set(event.target.value) 101 | } 102 | 103 | writeDescription = (event) => { 104 | descriptionRef.set(event.target.value) 105 | } 106 | 107 | writeNotes = (event) => { 108 | notesRef.set(event) 109 | } 110 | 111 | writeIsOpen = (event, id) => { 112 | // For 'isOpen', we're setting it to false if the user says they already achieved it (first option, 0), 113 | // or true if they say they haven't (second option, 1) 114 | if (id === 0) { 115 | isOpenRef.set(false) 116 | } 117 | if (id === 1) { 118 | isOpenRef.set(true) 119 | } 120 | } 121 | 122 | writeDate = (event, date) => { 123 | // getTime converts regular date format to timestamp 124 | dateRef.set(date.getTime()) 125 | } 126 | 127 | deleteCheckIn = () => { 128 | let goalId = this.props.goalId 129 | let checkInId = this.props.checkInId 130 | this.unsubscribe() 131 | goalsRef.child(goalId).child('checkIns').child(checkInId).set(null) 132 | browserHistory.push('/') 133 | } 134 | 135 | render() { 136 | // Rendering form with material UI 137 | return ( 138 |
139 | 140 |
141 |
142 |
143 |
144 |

{this.state.name} browserHistory.push('/')} />

145 |
146 |
147 |
148 |
149 |
150 |
151 |

Check-In Information

152 |
153 | 160 |
161 |
162 | 170 |
171 |
172 | 173 |
174 |
175 |
176 |

Uploads:

177 | 178 |
179 | {this.state.uploads && this.state.uploads.map((upload, index) => { 180 | let uploadId = upload[0] 181 | let uploadInfo = upload[1] 182 | return ( 183 | 184 | ) 185 | }) 186 | } 187 |
188 |
189 |
190 |
191 |
192 |

Notes

193 | 197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 | ) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/components/CheckInFormContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route} from 'react-router' 3 | 4 | import {getCheckInRefs} from 'APP/fire/refs' 5 | 6 | import CheckInForm from './CheckInForm' 7 | 8 | export default ({params: {id, cid}}) => { 9 | // Generate the db refs for the check in that we want: 10 | const checkInRefs = getCheckInRefs(id, cid) 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/components/Doorslam.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import firebase from 'APP/fire' 3 | 4 | export default class extends React.Component { 5 | componentDidMount() { 6 | const {auth} = this.props 7 | this.unsubscribe = auth.onAuthStateChanged(user => this.setState({user})) 8 | setTimeout(() => this.setState({ready: true}), 200) 9 | } 10 | 11 | render() { 12 | const {user, ready} = this.state || {} 13 | const {Landing, Loader, children} = this.props 14 | if (user) return children 15 | if (!ready) return 16 | return 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/components/Empty.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Empty = () => { 4 | return ( 5 |
6 |

Looks like you don't have any goals yet!

7 |

Click on the + button below to add your first goal, and you'll see it appear on a timeline!

8 |
9 | ) 10 | } 11 | 12 | export default Empty 13 | -------------------------------------------------------------------------------- /app/components/GoalForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, browserHistory } from 'react-router' 3 | import firebase from 'APP/fire' 4 | const db = firebase.database() 5 | const auth = firebase.auth() 6 | const usersRef = db.ref('users') 7 | const goalsRef = db.ref('goals') 8 | let nameRef, descriptionRef, isOpenRef, startRef, endRef, colorRef, milestonesRef, checkInsRef, resourcesRef, uploadsRef, notesRef 9 | let newMilestonePath, newCheckInPath 10 | 11 | import ReactQuill from 'react-quill' 12 | 13 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 14 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 15 | import alignTheme from './AlignTheme' 16 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 17 | import TextField from 'material-ui/TextField' 18 | import SelectField from 'material-ui/SelectField' 19 | import MenuItem from 'material-ui/MenuItem' 20 | import DatePicker from 'material-ui/DatePicker' 21 | import RaisedButton from 'material-ui/RaisedButton' 22 | import { CirclePicker } from 'react-color' 23 | import { List, ListItem } from 'material-ui/List' 24 | import Edit from 'material-ui/svg-icons/content/create' 25 | import Add from 'material-ui/svg-icons/content/add' 26 | import Close from 'material-ui/svg-icons/navigation/close' 27 | import ResourceContainer from './ResourceContainer' 28 | import ResourceCard from './ResourceCard' 29 | import ResourceForm from './ResourceForm' 30 | import UploadForm from './Upload' 31 | import UploadCard from './UploadCard' 32 | 33 | export default class extends React.Component { 34 | constructor(props) { 35 | super() 36 | this.state = { 37 | name: '', 38 | description: '', 39 | isOpen: true, 40 | startDate: new Date().getTime(), 41 | endDate: new Date().getTime(), 42 | color: '#000', 43 | milestones: [], 44 | checkIns: [], 45 | resources: [], 46 | notes: '' 47 | } 48 | } 49 | 50 | componentDidMount() { 51 | // When the component mounts, start listening to the fireRef we were given. 52 | this.listenTo(this.props.fireRef) 53 | } 54 | 55 | componentWillUnmount() { 56 | // When we unmount, stop listening. 57 | this.unsubscribe() 58 | } 59 | 60 | componentWillReceiveProps(incoming, outgoing) { 61 | // When the props sent to us by our parent component change, 62 | // start listening to the new firebase reference. 63 | this.listenTo(incoming.fireRef) 64 | } 65 | 66 | listenTo(fireRef) { 67 | // If we're already listening to a ref, stop listening there. 68 | if (this.unsubscribe) this.unsubscribe() 69 | 70 | // Set up aliases for our Firebase references: 71 | nameRef = fireRef.nameRef 72 | descriptionRef = fireRef.descriptionRef 73 | isOpenRef = fireRef.isOpenRef 74 | startRef = fireRef.startRef 75 | endRef = fireRef.endRef 76 | colorRef = fireRef.colorRef 77 | milestonesRef = fireRef.milestonesRef 78 | checkInsRef = fireRef.checkInsRef 79 | resourcesRef = fireRef.resourcesRef 80 | uploadsRef = fireRef.uploadsRef 81 | notesRef = fireRef.notesRef 82 | 83 | // DATABASE LISTENERS: 84 | // Whenever a ref's value changes in Firebase, set {value} on our state. 85 | 86 | const nameListener = nameRef.on('value', snapshot => 87 | this.setState({ name: snapshot.val() || '' })) 88 | 89 | const descriptionListener = descriptionRef.on('value', snapshot => { 90 | this.setState({ description: snapshot.val() || '' }) 91 | }) 92 | 93 | const isOpenListener = isOpenRef.on('value', snapshot => { 94 | if (snapshot.val() === null) isOpenRef.set(true) 95 | this.setState({ isOpen: snapshot.val() }) 96 | }) 97 | 98 | const startDateListener = startRef.on('value', snapshot => { 99 | this.setState({ startDate: snapshot.val() }) 100 | if (snapshot.val() === null) startRef.set(new Date().getTime()) 101 | }) 102 | 103 | const endDateListener = endRef.on('value', snapshot => { 104 | this.setState({ endDate: snapshot.val() }) 105 | if (snapshot.val() === null) endRef.set(new Date().getTime()) 106 | }) 107 | 108 | const colorListener = colorRef.on('value', snapshot => { 109 | if (snapshot.val() === null) return this.setState({ 110 | hex: '#bcbbb9', 111 | hsl: { 112 | a: 1, 113 | h: 39.99999999999962, 114 | l: 0.7313725490196079, 115 | s: 0.02189781021897823 116 | }, 117 | hsv: { 118 | a: 1, 119 | h: 39.99999999999962, 120 | s: 0.01595744680851073, 121 | v: 0.7372549019607844 122 | }, 123 | oldHue: 250, 124 | rgb: { 125 | a: 1, 126 | b: 185, 127 | g: 187, 128 | r: 188 129 | }, 130 | source: 'hex' 131 | }) 132 | this.setState({ color: snapshot.val() }) 133 | }) 134 | 135 | const milestonesListener = milestonesRef.on('value', snapshot => { 136 | if (snapshot.val()) this.setState({ milestones: Object.entries(snapshot.val()) }) 137 | }) 138 | 139 | const checkInsListener = checkInsRef.on('value', snapshot => { 140 | if (snapshot.val()) this.setState({ checkIns: Object.entries(snapshot.val()) }) 141 | }) 142 | 143 | const resourcesListener = resourcesRef.on('value', snapshot => { 144 | if (snapshot.val()) this.setState({ resources: Object.keys(snapshot.val()) }) 145 | else this.setState({ resources: [] }) 146 | }) 147 | 148 | const uploadsListener = uploadsRef.on('value', snapshot => { 149 | if (snapshot.val()) this.setState({ uploads: Object.entries(snapshot.val()) }) 150 | }) 151 | 152 | const notesListener = notesRef.on('value', snapshot => { 153 | if (snapshot.val()) this.setState({ notes: snapshot.val() }) 154 | }) 155 | 156 | // Set unsubscribe to be a function that detaches the listener. 157 | this.unsubscribe = () => { 158 | nameRef.off('value', nameListener) 159 | descriptionRef.off('value', descriptionListener) 160 | isOpenRef.off('value', isOpenListener) 161 | startRef.off('value', startDateListener) 162 | endRef.off('value', endDateListener) 163 | colorRef.off('value', colorListener) 164 | milestonesRef.off('value', milestonesListener) 165 | checkInsRef.off('value', checkInsListener) 166 | resourcesRef.off('value', resourcesListener) 167 | uploadsRef.off('value', uploadsListener) 168 | notesRef.off('value', notesListener) 169 | } 170 | } 171 | 172 | writeName = (event) => { 173 | nameRef.set(event.target.value) 174 | } 175 | 176 | writeDescription = (event) => { 177 | descriptionRef.set(event.target.value) 178 | } 179 | 180 | writeNotes = (event) => { 181 | notesRef.set(event) 182 | } 183 | 184 | writeIsOpen = (event, id) => { 185 | if (id === 0) { 186 | isOpenRef.set(false) 187 | } 188 | if (id === 1) { 189 | isOpenRef.set(true) 190 | } 191 | } 192 | 193 | writeStartDate = (event, date) => { 194 | startRef.set(date.getTime()) 195 | } 196 | 197 | writeEndDate = (event, date) => { 198 | endRef.set(date.getTime()) 199 | } 200 | 201 | handleColorChange = (color, event) => { 202 | colorRef.set(color) 203 | } 204 | 205 | createNewMilestone = () => { 206 | let newMilestoneRef = milestonesRef.push() 207 | let newMilestonePath = `/milestone/${this.props.id}/${newMilestoneRef.key}` 208 | browserHistory.push(newMilestonePath) 209 | } 210 | 211 | createNewCheckIn = () => { 212 | let newCheckInRef = checkInsRef.push() 213 | let newCheckInPath = `/checkin/${this.props.id}/${newCheckInRef.key}` 214 | browserHistory.push(newCheckInPath) 215 | } 216 | 217 | deleteGoal = () => { 218 | let goalId = this.props.id 219 | let userId = auth.currentUser.uid 220 | this.unsubscribe() 221 | 222 | // to avoid multiple writes to firebase: 223 | // make an object of data to delete and pass it to the top level 224 | let dataToDelete = {} 225 | dataToDelete[`/goals/${goalId}`] = null 226 | dataToDelete[`/users/${userId}/goals/${goalId}`] = null 227 | db.ref().update(dataToDelete, function (error) { 228 | if (error) { 229 | console.log('Error deleting data: ', error) 230 | } 231 | }) 232 | browserHistory.push('/') 233 | } 234 | 235 | render() { 236 | const colorArray = ['#6CC2BD', '#5A809E', '#7C79A2', '#F57D7C', '#FFC1A6', '#ffd7a6', '#bcbbb9', '#9E898F', '#667762', '#35464D', '#386174', '#6B96C9'] 237 | return ( 238 |
239 | 240 |
241 |
242 |
243 |
244 |

{this.state.name} browserHistory.push('/')} />

245 |
246 |
247 |
248 |
249 |
250 |
251 |

Goal Information

252 |
253 | 260 |
261 |
262 | 270 |
271 |
272 | 277 | 278 | 279 | 280 |
281 |
282 | 283 |
284 |
285 | 286 |
287 |
288 |
289 |
290 |

Choose Color

291 | 292 |
293 |
294 |

Notes

295 | 300 |
301 |
302 |
303 |
304 |
305 |

Resources

306 | 307 |
308 | {this.state.resources && this.state.resources.map((resourceId, index) => { 309 | return ( 310 | 311 | ) 312 | }) 313 | } 314 |
315 |
316 |
317 |

Uploads

318 | 319 |
320 | {this.state.uploads && this.state.uploads.map((upload, index) => { 321 | const uploadId = upload[0] 322 | const uploadInfo = upload[1] 323 | return ( 324 | 325 | ) 326 | }) 327 | } 328 |
329 |
330 |
331 |
332 |
333 |
334 |

Milestones

335 | 336 | { 337 | this.state.milestones && this.state.milestones.map((milestone, index) => { 338 | let milestonePath = `/milestone/${this.props.id}/${milestone[0]}` 339 | return ( 340 | } containerElement={} > 341 | ) 342 | }) 343 | } 344 | } onTouchTap={this.createNewMilestone} >Add new 345 | 346 |
347 |
348 |
349 |
350 |

Check Ins

351 | 352 | { 353 | this.state.checkIns && this.state.checkIns.map((checkin, index) => { 354 | let checkinPath = `/checkin/${this.props.id}/${checkin[0]}` 355 | return ( 356 | } containerElement={} > 357 | ) 358 | }) 359 | } 360 | } onTouchTap={this.createNewCheckIn} >Add new 361 | 362 |
363 |
364 |
365 |
366 |
367 |
browserHistory.push('/')} />
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 | ) 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /app/components/GoalFormContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route} from 'react-router' 3 | 4 | import {getGoalRefs} from 'APP/fire/refs' 5 | 6 | import GoalForm from './GoalForm' 7 | 8 | export default ({params: {id}}) => { 9 | // call goalRefs function with the current id to generate reference paths 10 | // to all the values for the current goal in firebase 11 | const goalRefs = getGoalRefs(id) 12 | return ( 13 |
14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /app/components/Landing.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Paper from 'material-ui/Paper' 3 | 4 | import Login from './Login' 5 | 6 | const moduleStyle = { 7 | width: '50vw', 8 | minWidth: '500px', 9 | backgroundColor: '#fff', 10 | margin: 'auto', 11 | color: '#000' 12 | } 13 | 14 | 15 | const Landing = () => { 16 | return ( 17 |
18 | }> 19 | 20 |
21 | ) 22 | } 23 | 24 | export default Landing 25 | -------------------------------------------------------------------------------- /app/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CircularProgress from 'material-ui/CircularProgress' 3 | 4 | const Loader = () => { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default Loader 13 | -------------------------------------------------------------------------------- /app/components/LocalSignin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import firebase from 'APP/fire' 3 | import { browserHistory } from 'react-router' 4 | 5 | import TextField from 'material-ui/TextField' 6 | import RaisedButton from 'material-ui/RaisedButton' 7 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 8 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 9 | import alignTheme from './AlignTheme' 10 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 11 | import FontIcon from 'material-ui/FontIcon' 12 | 13 | const google = new firebase.auth.GoogleAuthProvider() 14 | 15 | const buttonStyle = { 16 | margin: 12, 17 | } 18 | 19 | export default class extends React.Component { 20 | constructor(props) { 21 | super() 22 | this.state = { 23 | email: '', 24 | password: '', 25 | showInvalidAlert: false, 26 | errorMessage: '' 27 | } 28 | 29 | this.handleChange = this.handleChange.bind(this) 30 | this.handleSubmit = this.handleSubmit.bind(this) 31 | } 32 | 33 | handleGoogleLogin() { 34 | firebase.auth().signInWithPopup(google).then(function(result) { 35 | // This gives you a Google Access Token. You can use it to access the Google API. 36 | var token = result.credential.accessToken 37 | // The signed-in user info. 38 | var user = result.user 39 | // ... 40 | }).catch(function(error) { 41 | // Handle Errors here. 42 | var errorCode = error.code 43 | var errorMessage = error.message 44 | // The email of the user's account used. 45 | var email = error.email 46 | // The firebase.auth.AuthCredential type that was used. 47 | var credential = error.credential 48 | // ... 49 | }) 50 | } 51 | 52 | handleFailedLogin(message) { 53 | return ( 54 |
55 |

{message}

56 |
57 | ) 58 | } 59 | 60 | handleChange(event) { 61 | this.setState({ 62 | [event.target.name]: event.target.value, 63 | showInvalidAlert: false 64 | }) 65 | } 66 | 67 | handleSubmit(event) { 68 | event.preventDefault() 69 | firebase.auth().signInWithEmailAndPassword(this.state.email, this.state.password) 70 | .catch(error => { 71 | const errorMessage = error.message 72 | this.setState({ 73 | errorMessage: errorMessage, 74 | showInvalidAlert: true, 75 | }) 76 | console.error(error) 77 | }) 78 | } 79 | 80 | render() { 81 | return ( 82 | 83 |
84 |
85 |
86 | 88 |
89 |
90 | 92 |
93 |
94 | 95 |
96 |
97 |
98 |
Or:
99 |
100 | 105 |
106 | 107 | {this.state.showInvalidAlert ? this.handleFailedLogin(this.state.errorMessage) : null} 108 |
109 |
110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/components/LocalSignup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import firebase from 'APP/fire' 3 | import { browserHistory } from 'react-router' 4 | import TextField from 'material-ui/TextField' 5 | import RaisedButton from 'material-ui/RaisedButton' 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 7 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 8 | import alignTheme from './AlignTheme' 9 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 10 | 11 | const db = firebase.database() 12 | const usersRef = db.ref('users') 13 | let newUser 14 | const buttonStyle = { 15 | margin: 12, 16 | } 17 | 18 | export default class extends React.Component { 19 | constructor(props) { 20 | super() 21 | this.state = { 22 | email: '', 23 | password: '', 24 | showInvalidAlert: false, 25 | errorMessage: '' 26 | } 27 | 28 | this.handleChange = this.handleChange.bind(this) 29 | this.handleSubmit = this.handleSubmit.bind(this) 30 | } 31 | 32 | handleFailedLogin(message) { 33 | return ( 34 |
35 |

{message}

36 |
37 | ) 38 | } 39 | 40 | handleChange(e) { 41 | this.setState({ 42 | [e.target.name]: e.target.value, 43 | showInvalidAlert: false 44 | }) 45 | } 46 | 47 | handleSubmit(e) { 48 | e.preventDefault() 49 | firebase.auth().createUserWithEmailAndPassword(this.state.email, this.state.password) 50 | .then(() => firebase.auth().onAuthStateChanged((user) => { 51 | if (user) { 52 | user.updateProfile({ 53 | displayName: this.state.name 54 | }) 55 | } 56 | })) 57 | .catch(error => { 58 | const errorMessage = error.message; 59 | this.setState({ 60 | errorMessage: errorMessage, 61 | showInvalidAlert: true, 62 | }) 63 | console.error(error) 64 | }) 65 | 66 | 67 | } 68 | 69 | render() { 70 | return ( 71 | 72 |
73 |
74 |
75 | 77 |
78 |
79 | 81 |
82 |
83 | 85 |
86 |
87 | 88 |
89 |
90 | 91 | {this.state.showInvalidAlert ? this.handleFailedLogin(this.state.errorMessage) : null} 92 |
93 |
94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import firebase from 'APP/fire' 3 | import { PanelGroup, Panel } from 'react-bootstrap' 4 | import RaisedButton from 'material-ui/RaisedButton' 5 | import FontIcon from 'material-ui/FontIcon' 6 | import { Tabs, Tab } from 'material-ui/Tabs'; 7 | import SwipeableViews from 'react-swipeable-views'; 8 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 9 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 10 | import alignTheme from './AlignTheme' 11 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 12 | 13 | import LocalSignin from './LocalSignin' 14 | import LocalSignup from './LocalSignup' 15 | 16 | const google = new firebase.auth.GoogleAuthProvider() 17 | 18 | const tabStyles = { 19 | headline: { 20 | fontSize: 4, 21 | paddingTop: 16, 22 | marginBottom: 12, 23 | fontWeight: 400, 24 | }, 25 | slide: { 26 | padding: 10, 27 | fontSize: '125%' 28 | }, 29 | } 30 | 31 | export default class LandingPage extends React.Component { 32 | 33 | constructor(props) { 34 | super(props) 35 | this.state = { 36 | slideIndex: 0, 37 | } 38 | } 39 | 40 | handleTabChange = (value) => { 41 | this.setState({ 42 | slideIndex: value, 43 | }) 44 | } 45 | 46 | componentDidMount() { 47 | let newImage = document.createElement('img') 48 | newImage.setAttribute('src', './lines3.png') 49 | newImage.setAttribute('id', 'login-image') 50 | newImage.setAttribute('style', 'margin-top: -421px; width: 100vw;') 51 | document.body.appendChild(newImage) 52 | document.getElementById('main').setAttribute('style', 'position: relative;') 53 | } 54 | 55 | componentWillUnmount() { 56 | let rmImage = document.getElementById('login-image') 57 | document.body.removeChild(rmImage) 58 | document.getElementById('main').setAttribute('style', 'position: initial;') 59 | } 60 | 61 | render() { 62 | return ( 63 | 64 |
65 | 66 | 67 | 68 | 69 | 71 |
72 | 73 |
74 |
75 | 76 |
77 |
78 |
79 |
80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/components/Login.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | 8 | import Login from './Login' 9 | 10 | /* global describe it beforeEach */ 11 | describe('', () => { 12 | let root, fakeAuth 13 | beforeEach('render the root', () => { 14 | fakeAuth = { 15 | signInWithPopup: spy(), 16 | signInWithRedirect: spy(), 17 | } 18 | root = shallow() 19 | }) 20 | 21 | it('logs in with google', () => { 22 | const button = root.find('button.google.login') 23 | expect(button).to.have.length(1) 24 | button.simulate('click') 25 | expect(fakeAuth.signInWithPopup).to.have.been.calledWithMatch({providerId: 'google.com'}) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /app/components/MilestoneForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, browserHistory } from 'react-router' 3 | 4 | import firebase from 'APP/fire' 5 | const db = firebase.database() 6 | const goalsRef = db.ref('goals') 7 | let nameRef, descriptionRef, isOpenRef, dateRef, uploadsRef, parentRef, resourcesRef, notesRef 8 | 9 | import ReactQuill from 'react-quill' 10 | 11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 12 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 13 | import alignTheme from './AlignTheme' 14 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 15 | import TextField from 'material-ui/TextField' 16 | import SelectField from 'material-ui/SelectField' 17 | import MenuItem from 'material-ui/MenuItem' 18 | import DatePicker from 'material-ui/DatePicker' 19 | import RaisedButton from 'material-ui/RaisedButton' 20 | import { GridList, GridTile } from 'material-ui/GridList' 21 | import Close from 'material-ui/svg-icons/navigation/close' 22 | import UploadForm from './Upload' 23 | import UploadCard from './UploadCard' 24 | import ResourceCard from './ResourceCard' 25 | import ResourceForm from './ResourceForm' 26 | import ResourceContainer from './ResourceContainer' 27 | 28 | export default class extends React.Component { 29 | constructor(props) { 30 | super() 31 | this.state = { 32 | name: '', 33 | description: '', 34 | isOpen: true, 35 | date: new Date().getTime(), 36 | notes: '' 37 | } 38 | } 39 | 40 | componentDidMount() { 41 | // When the component mounts, start listening to the fireRef 42 | // we were given. 43 | this.listenTo(this.props.fireRef) 44 | } 45 | 46 | componentWillUnmount() { 47 | // When we unmount, stop listening. 48 | this.unsubscribe() 49 | } 50 | 51 | componentWillReceiveProps(incoming, outgoing) { 52 | // When the props sent to us by our parent component change, 53 | // start listening to the new firebase reference. 54 | this.listenTo(incoming.fireRef) 55 | } 56 | 57 | listenTo(fireRef) { 58 | // If we're already listening to a ref, stop listening there. 59 | if (this.unsubscribe) this.unsubscribe() 60 | 61 | nameRef = fireRef.nameRef 62 | descriptionRef = fireRef.descriptionRef 63 | isOpenRef = fireRef.isOpenRef 64 | dateRef = fireRef.dateRef 65 | uploadsRef = fireRef.uploadsRef 66 | parentRef = fireRef.parentRef 67 | resourcesRef = fireRef.resourcesRef 68 | notesRef = fireRef.notesRef 69 | 70 | // Whenever our ref's value changes, set {value} on our state. 71 | // const listener = fireRef.on('value', snapshot => 72 | // this.setState({value: snapshot.val()})) 73 | 74 | // HEY ALL let's refactor to just listen to parent element 75 | const nameListener = nameRef.on('value', snapshot => 76 | this.setState({ name: snapshot.val() || '' })) 77 | 78 | const descriptionListener = descriptionRef.on('value', snapshot => { 79 | this.setState({ description: snapshot.val() || '' }) 80 | }) 81 | 82 | const isOpenListener = isOpenRef.on('value', snapshot => { 83 | this.setState({ isOpen: snapshot.val() }) 84 | if (snapshot.val() === null) isOpenRef.set(true) 85 | }) 86 | 87 | const dateListener = dateRef.on('value', snapshot => { 88 | this.setState({ date: snapshot.val() }) 89 | if (snapshot.val() === null) dateRef.set(new Date().getTime()) 90 | }) 91 | 92 | const resourcesListener = resourcesRef.on('value', snapshot => { 93 | if (snapshot.val()) this.setState({ resources: Object.keys(snapshot.val()) }) 94 | else this.setState({resources: []}) 95 | }) 96 | 97 | const uploadsListener = uploadsRef.on('value', snapshot => { 98 | if (snapshot.val()) this.setState({ uploads: Object.entries(snapshot.val()) }) 99 | }) 100 | 101 | const notesListener = notesRef.on('value', snapshot => { 102 | if (snapshot.val()) this.setState({ notes: snapshot.val() }) 103 | }) 104 | 105 | // Set unsubscribe to be a function that detaches the listener. 106 | this.unsubscribe = () => { 107 | nameRef.off('value', nameListener) 108 | descriptionRef.off('value', descriptionListener) 109 | isOpenRef.off('value', isOpenListener) 110 | dateRef.off('value', dateListener) 111 | resourcesRef.off('value', resourcesListener) 112 | uploadsRef.off('value', uploadsListener) 113 | notesRef.off('value', notesListener) 114 | } 115 | } 116 | 117 | writeName = (event) => { 118 | nameRef.set(event.target.value) 119 | } 120 | 121 | writeDescription = (event) => { 122 | descriptionRef.set(event.target.value) 123 | } 124 | 125 | writeNotes = (event) => { 126 | notesRef.set(event) 127 | } 128 | 129 | writeIsOpen = (event, id) => { 130 | // for 'isOpen', we're setting it to false if the user says they already achieved it, or true if they say they haven't 131 | if (id === 0) { 132 | isOpenRef.set(false) 133 | } 134 | if (id === 1) { 135 | isOpenRef.set(true) 136 | } 137 | } 138 | 139 | writeDate = (event, date) => { 140 | dateRef.set(date.getTime()) 141 | } 142 | 143 | deleteMilestone = () => { 144 | let goalId = this.props.goalId 145 | let milestoneId = this.props.milestoneId 146 | this.unsubscribe() 147 | goalsRef.child(goalId).child('milestones').child(milestoneId).set(null) 148 | browserHistory.push('/') 149 | } 150 | 151 | render() { 152 | return ( 153 |
154 | 155 |
156 |
157 |
158 |
159 |

{this.state.name} browserHistory.push('/')} />

160 |
161 |
162 |
163 |
164 |
165 |
166 |

Milestone Information

167 |
168 | 175 |
176 |
177 | 182 | 183 | 184 | 185 |
186 |
187 | 188 |
189 |
190 |
191 |

Notes

192 | 197 |
198 |
199 |
200 |
201 |

Resources

202 | 203 |
204 | {this.state.resources && this.state.resources.map((resourceId, index) => { 205 | return ( 206 |
207 | 208 |
209 | ) 210 | }) 211 | } 212 |
213 |
214 |
215 |

Uploads

216 | 217 |
218 | {this.state.uploads && this.state.uploads.map((upload, index) => { 219 | const uploadId = upload[0] 220 | const uploadInfo = upload[1] 221 | return ( 222 | 223 | ) 224 | }) 225 | } 226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 | ) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /app/components/MilestoneFormContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route} from 'react-router' 3 | import {getMilestoneRefs} from 'APP/fire/refs' 4 | 5 | import MilestoneForm from './MilestoneForm' 6 | 7 | export default ({params: {id, mid}}) => { 8 | const milestoneRefs = getMilestoneRefs(id, mid) 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link, browserHistory } from 'react-router' 3 | 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 5 | import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme' 6 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 7 | import FlatButton from 'material-ui/FlatButton' 8 | 9 | import { AppBar, Tabs, Tab } from 'material-ui' 10 | 11 | import WhoAmI from './WhoAmI' 12 | 13 | import firebase from 'APP/fire' 14 | const auth = firebase.auth() 15 | 16 | export const Navbar = ({ user, auth }) => 17 | browserHistory.push('/')} 20 | iconElementLeft={} 21 | onLeftIconButtonTouchTap={() => browserHistory.push('/')} 22 | iconElementRight={auth.currentUser ?
: null } 23 | iconStyleRight={{display: 'flex', alignItems: 'center', marginTop: 0}} 24 | id='nav' 25 | > 26 |
27 | 28 | export default class extends Component { 29 | componentDidMount() { 30 | this.unsubscribe = auth.onAuthStateChanged(user => this.setState({ user })) 31 | } 32 | 33 | componentWillUnmount() { 34 | this.unsubscribe() 35 | } 36 | 37 | render() { 38 | const { user } = this.state || {} 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | const NotFound = props => { 5 | const {pathname} = props.location || {pathname: '<< no path >>'} 6 | console.error('NotFound: %s not found (%o)', pathname, props) 7 | return ( 8 |
9 |

Looks like there's no page here

10 |

Lost? Here's a way home.

11 |
12 | ) 13 | } 14 | 15 | export default NotFound 16 | -------------------------------------------------------------------------------- /app/components/ResourceCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { Link, browserHistory } from "react-router" 3 | 4 | import firebase from "APP/fire" 5 | const db = firebase.database() 6 | 7 | import MuiThemeProvider from "material-ui/styles/MuiThemeProvider" 8 | import lightBaseTheme from "material-ui/styles/baseThemes/lightBaseTheme" 9 | import alignTheme from "./AlignTheme" 10 | import getMuiTheme from "material-ui/styles/getMuiTheme" 11 | import FlatButton from "material-ui/FlatButton" 12 | import IconButton from "material-ui/IconButton" 13 | import { 14 | Card, 15 | CardActions, 16 | CardHeader, 17 | CardMedia, 18 | CardText 19 | } from "material-ui/Card" 20 | import ContentEdit from "material-ui/svg-icons/content/create" 21 | import ContentLink from "material-ui/svg-icons/content/link" 22 | import Delete from "material-ui/svg-icons/content/clear" 23 | 24 | let urlRef, titleRef, imageRef, descriptionRef, milestoneRef 25 | 26 | export default class extends Component { 27 | constructor(props) { 28 | super() 29 | this.state = { 30 | title: '', 31 | url: '', 32 | image: '', 33 | description: '' 34 | } 35 | } 36 | 37 | componentDidMount() { 38 | // When the component mounts, start listening to the fireRef we were given. 39 | this.listenTo(this.props.fireRef) 40 | } 41 | 42 | componentWillUnmount() { 43 | // When we unmount, stop listening. 44 | this.unsubscribe() 45 | } 46 | 47 | componentWillReceiveProps(incoming, outgoing) { 48 | // When the props sent to us by our parent component change, 49 | // start listening to the new firebase reference. 50 | this.listenTo(incoming.fireRef) 51 | } 52 | 53 | listenTo(fireRef) { 54 | // If we're already listening to a ref, stop listening there. 55 | if (this.unsubscribe) this.unsubscribe() 56 | 57 | titleRef = fireRef.titleRef 58 | urlRef = fireRef.urlRef 59 | imageRef = fireRef.imageRef 60 | descriptionRef = fireRef.descriptionRef 61 | milestoneRef = fireRef.milestoneRef 62 | 63 | // Whenever our ref's value changes, set {value} on our state. 64 | 65 | const titleListener = titleRef.on("value", snapshot => 66 | this.setState({ title: snapshot.val() }) 67 | ); 68 | 69 | const urlListener = urlRef.on("value", snapshot => { 70 | this.setState({ url: snapshot.val() }); 71 | }); 72 | 73 | const imageListener = imageRef.on("value", snapshot => { 74 | this.setState({ image: snapshot.val() }); 75 | }); 76 | 77 | const descriptionListener = descriptionRef.on("value", snapshot => { 78 | this.setState({ description: snapshot.val() }); 79 | }); 80 | 81 | const milestoneListener = milestoneRef.on("value", snapshot => { 82 | this.setState({ mileId: snapshot.val() }); 83 | }); 84 | 85 | // Set unsubscribe to be a function that detaches the listener. 86 | this.unsubscribe = () => { 87 | titleRef.off("value", titleListener); 88 | urlRef.off("value", urlListener); 89 | imageRef.off("value", imageListener); 90 | descriptionRef.off("value", descriptionListener); 91 | milestoneRef.off("value", milestoneListener); 92 | }; 93 | } 94 | 95 | deleteResource = () => { 96 | // we want to delete resources from the goal they live on, as well as the milestone if one exists 97 | // for now, though, don't delete resources from resources object itself (on same level as goals) 98 | const resourceId = this.props.id 99 | const goalId = this.props.goalId 100 | const milestoneId = this.state.mileId 101 | this.unsubscribe() 102 | 103 | let dataToDelete = {} 104 | dataToDelete[`/goals/${goalId}/resources/${resourceId}`] = null // Set resource to null on goals object 105 | if (this.state.mileId) { 106 | dataToDelete[ // if resource also exists on a milestone, set it to null there too 107 | `/goals/${goalId}/milestones/${milestoneId}/resources/${resourceId}` 108 | ] = null 109 | } 110 | db.ref().update(dataToDelete, function(error) { 111 | if (error) { 112 | console.log("Error deleting data: ", error); 113 | } 114 | }) 115 | } 116 | 117 | render() { 118 | // Rendering form with material UI 119 | return ( 120 | 121 | 126 | 131 | 132 | 138 | 139 | {this.state.description} 140 | 141 | } 146 | /> 147 | } 151 | onClick={this.deleteResource} 152 | /> 153 | 154 | 155 | 156 | ) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/components/ResourceContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Route} from 'react-router' 3 | 4 | import {getResourceRefs} from 'APP/fire/refs' 5 | let resourceRefs 6 | 7 | import ResourceCard from './ResourceCard' 8 | 9 | export default function(props) { 10 | const id = props.resourceId 11 | const goalId = props.goalId 12 | const resourceRefs = getResourceRefs(id, goalId) 13 | return ( 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/components/ResourceForm.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import firebase from 'APP/fire' 3 | const db = firebase.database() 4 | const resourcesRef = db.ref('resources') 5 | 6 | import {MuiThemeProvider, getMuiTheme} from 'material-ui/styles' 7 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 8 | import alignTheme from './AlignTheme' 9 | import {TextField, IconButton, RaisedButton} from 'material-ui' 10 | import ContentAdd from 'material-ui/svg-icons/content/add' 11 | 12 | import $ from 'jquery' 13 | 14 | export default class extends Component { 15 | constructor(props) { 16 | super() 17 | this.state = { 18 | url: '' 19 | } 20 | } 21 | 22 | // Don't write URL to firebase yet... first, make the API call 23 | // then write title, image, and description based on JSON we get back 24 | 25 | handleChange = (event) => { 26 | this.setState({ 27 | url: event.target.value 28 | }) 29 | } 30 | 31 | handleSubmit = (event) => { 32 | event.preventDefault() 33 | const target = this.state.url 34 | $.ajax({ 35 | url: 'https://api.linkpreview.net', 36 | dataType: 'jsonp', 37 | data: {q: target, key: '59546c0da716e80a54030151e45fe4e025d32430c753a'}, 38 | success: response => { 39 | let key = resourcesRef.push().key 40 | if (this.props.milestoneRef) { 41 | // Add resource URL to parent goal's uploads: 42 | this.props.goalRef.child('resources').child(key).set({ 43 | resourceURL: response.url, 44 | milestoneId: this.props.milestoneId 45 | }) 46 | // Add resource URL to milestone: 47 | this.props.milestoneRef.child(key).set({ 48 | resourceURL: response.url 49 | }) 50 | } else { 51 | // Otherwise, just add resource directly to goal 52 | this.props.goalRef.child(key).set({ 53 | resourceURL: response.url 54 | }) 55 | } 56 | resourcesRef.child(key).set(response) 57 | } 58 | }) 59 | this.setState({url: ''}) 60 | } 61 | 62 | render() { 63 | return ( 64 | 65 |
66 | 74 | {this.state.url && 75 | 76 | 77 | 78 | } 79 | 80 |
81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/components/Timelines.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link, browserHistory } from 'react-router' 3 | import firebase from 'APP/fire' 4 | const db = firebase.database() 5 | const auth = firebase.auth() 6 | let goalsRef = db.ref('goals') 7 | let usersRef = db.ref('users') 8 | let currentUserGoalsRef, goalsListener 9 | let goalRefs = {} 10 | 11 | import { VictoryAxis, VictoryChart, VictoryLabel, VictoryLine, VictoryBrushContainer, VictoryZoomContainer, VictoryScatter, VictoryTooltip } from 'victory' 12 | import FloatingActionButton from 'material-ui/FloatingActionButton' 13 | import alignTheme from './AlignTheme' 14 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 15 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 16 | import ContentAdd from 'material-ui/svg-icons/content/add' 17 | import Popover from 'material-ui/Popover' 18 | import Menu from 'material-ui/Menu' 19 | import MenuItem from 'material-ui/MenuItem' 20 | 21 | import Loader from './Loader' 22 | import Empty from './Empty' 23 | 24 | // eventually, we'll sort goals array by priority / activity level, so displaying by index will have more significance 25 | 26 | export default class extends Component { 27 | constructor(props) { 28 | super() 29 | this.state = { 30 | ready: false, 31 | menuOpen: false, 32 | goals: [], // The actual goals that happen to belong to the user 33 | openGoal: {} 34 | } 35 | } 36 | 37 | /********* VICTORY FUNCTIONS: *********/ 38 | 39 | getScatterData(goal, index, goalId) { 40 | var data = [] 41 | var endSymbol = this.chooseEndSymbol(goal) 42 | let color 43 | if (goal.color) color = goal.color.hex 44 | else color = '#888' 45 | // push start and end dates to data array 46 | data.push({ x: new Date(goal.startDate), key: `/goal/${goalId}`, y: index, label: `${goal.name} \n start date: \n ${new Date(goal.startDate).toDateString()}`, symbol: 'circle', strokeWidth: 7, fill: color }) 47 | data.push({ x: new Date(goal.endDate), key: `/goal/${goalId}`, y: index, label: `${goal.name} \n end date: \n ${new Date(goal.endDate).toDateString()}`, symbol: endSymbol, strokeWidth: 7, fill: color }) 48 | // then iterate over the milestones object and push each date to the array 49 | if (goal.milestones) { 50 | for (var id in goal.milestones) { 51 | var milestone = goal.milestones[id] 52 | var milestoneFill = this.chooseMilestoneFill(goal, milestone) 53 | data.push({ x: new Date(milestone.displayDate), key: `/milestone/${goalId}/${id}`, y: index, label: milestone.name, symbol: 'square', strokeWidth: 3, size: 5, fill: milestoneFill }) 54 | } 55 | } 56 | if (goal.checkIns) { 57 | for (var id in goal.checkIns) { 58 | var checkin = goal.checkIns[id] 59 | data.push({ x: new Date(checkin.displayDate), key: `/checkin/${goalId}/${id}`, y: index, label: checkin.name, symbol: 'diamond', strokeWidth: 3, fill: color }) 60 | } 61 | } 62 | return data 63 | } 64 | 65 | chooseEndSymbol(goal) { 66 | if (goal.isOpen) return 'circle' 67 | else return 'star' 68 | } 69 | 70 | chooseMilestoneFill(goal, milestone) { 71 | if (milestone.isOpen) return 'white' 72 | else return goal.color.hex 73 | } 74 | 75 | getLineData(goal, index) { 76 | var data = [] 77 | // push start and end dates to data array 78 | // maybe make end date of completed goals into a star?? 79 | data.push({ x: new Date(goal.startDate), y: index, label: `${goal.name}` }) 80 | data.push({ x: new Date(goal.endDate), y: index }) 81 | // then iterate over the milestones object and push each date to the array 82 | if (goal.milestones) { 83 | for (var id in goal.milestones) { 84 | var milestone = goal.milestones[id] 85 | data.push({ x: new Date(milestone.displayDate), y: index }) 86 | } 87 | } 88 | if (goal.checkIns) { 89 | for (var id in goal.checkIns) { 90 | var checkin = goal.checkIns[id] 91 | data.push({ x: new Date(checkin.displayDate), y: index }) 92 | } 93 | } 94 | return data 95 | } 96 | 97 | handleZoom(domain) { 98 | this.setState({ selectedDomain: domain }) 99 | } 100 | 101 | handleBrush(domain) { 102 | this.setState({ zoomDomain: domain }) 103 | } 104 | 105 | /********* MATERIAL-UI FUNCTIONS: *********/ 106 | 107 | handleLineTap = (event, goal) => { 108 | let ourTop = event.pageY + window.scrollY 109 | let ourLeft = event.pageX + window.scrollX // Just putting these scroll values in case for some reason it scrolls 110 | const ourBbox = { 111 | bottom: event.target.getBoundingClientRect().bottom, 112 | right: event.target.getBoundingClientRect().right, 113 | width: event.target.getBoundingClientRect().width, 114 | left: ourLeft, 115 | top: ourTop 116 | } 117 | event.preventDefault() // Prevents ghost click 118 | this.setState({ 119 | menuOpen: true, 120 | anchorEl: { 121 | getBoundingClientRect() { 122 | return ourBbox 123 | } 124 | }, 125 | openGoal: goal 126 | }) 127 | } 128 | 129 | handleRequestClose = () => { 130 | this.setState({ 131 | menuOpen: false, 132 | }) 133 | } 134 | 135 | /********* MATERIAL-UI FUNCTIONS: *********/ 136 | 137 | viewCurrentTimeline = (event) => { 138 | event.preventDefault() 139 | let openGoalUrl = `/goal/${this.state.openGoal[0]}` 140 | browserHistory.push(openGoalUrl) 141 | } 142 | 143 | addMilestoneToCurrentTimeline = (event) => { 144 | event.preventDefault() 145 | let currentGoalId = this.state.openGoal[0] 146 | let newMilestoneRef = goalsRef.child(currentGoalId).child('milestones').push() 147 | let newMilestonePath = `/milestone/${this.state.openGoal[0]}/${newMilestoneRef.key}` 148 | browserHistory.push(newMilestonePath) 149 | } 150 | 151 | addCheckinToCurrentTimeline = (event) => { 152 | event.preventDefault() 153 | let currentGoalId = this.state.openGoal[0] 154 | let newCheckinRef = goalsRef.child(currentGoalId).child('checkIns').push() 155 | let newCheckinPath = `/checkin/${this.state.openGoal[0]}/${newCheckinRef.key}` 156 | browserHistory.push(newCheckinPath) 157 | } 158 | 159 | deleteCurrentTimeline = (event) => { 160 | event.preventDefault() 161 | let goalId = this.state.openGoal[0] 162 | let userId = this.state.userId 163 | 164 | // To avoid multiple writes to firebase: 165 | // Make an object of data to delete and pass it to the top level 166 | let dataToDelete = {} 167 | dataToDelete[`/goals/${goalId}`] = null 168 | dataToDelete[`/users/${userId}/goals/${goalId}`] = null 169 | db.ref().update(dataToDelete, function(error) { 170 | if (error) { 171 | console.log('Error deleting data: ', error) 172 | } 173 | }) 174 | } 175 | 176 | /********* FIREBASE FUNCTIONS: *********/ 177 | 178 | createNewGoal = (event) => { 179 | event.preventDefault() 180 | // Check to see if the index of the menu item is the index of the add goal item, aka 0 181 | let newGoalRef = goalsRef.push() 182 | let newGoalId = newGoalRef.key 183 | let newGoalPath = `/goal/${newGoalId}` 184 | let newUserGoalRelation = currentUserGoalsRef.child(newGoalId).set(true) // Takes ID of the new Goal, and adds it as a key: true in user's goal object 185 | browserHistory.push(newGoalPath) 186 | } 187 | 188 | componentDidMount() { 189 | this.unsubscribeAuth = auth.onAuthStateChanged(user => { 190 | if (user) { 191 | const userId = user.uid 192 | this.setState({userId: userId}) 193 | currentUserGoalsRef = usersRef.child(userId).child('goals') 194 | this.listenTo(currentUserGoalsRef) 195 | } 196 | }) 197 | } 198 | 199 | componentWillUnmount() { 200 | // When we unmount, stop listening. 201 | this.unsubscribe && this.unsubscribe() 202 | this.unsubscribeAuth() 203 | } 204 | 205 | componentWillReceiveProps(incoming, outgoing) { 206 | // When the props sent to us by our parent component change, 207 | // start listening to the new firebase reference. 208 | this.listenTo(incoming.fireRef) 209 | } 210 | 211 | unsubscribeGoals() { 212 | if (this.userGoalUnsubscribers) this.userGoalUnsubscribers.forEach(x => x()) 213 | } 214 | 215 | listenTo(fireRef) { 216 | if (this.unsubscribe) this.unsubscribe() 217 | this.unsubscribeGoals() 218 | goalsListener = fireRef.on('value', (snapshot) => { 219 | const goals = {} 220 | let userGoalIds 221 | if (snapshot.val()) userGoalIds = Object.keys(snapshot.val()) 222 | else userGoalIds = [] 223 | if (!userGoalIds.length) { 224 | this.setState({ready: true}) 225 | } 226 | this.userGoalUnsubscribers = 227 | userGoalIds.map(goalId => { 228 | const ref = goalsRef.child(goalId) 229 | let listener = ref.on('value', (goalSnapshot) => { 230 | goals[goalId] = goalSnapshot.val() 231 | this.setState({ goals: Object.entries(goals), ready: true }) 232 | }) 233 | return () => ref.off('value', listener) 234 | }) 235 | }) 236 | 237 | // Set unsubscribe to be a function that detaches the listener. 238 | this.unsubscribe = () => { 239 | this.unsubscribeGoals() 240 | fireRef.off('value', goalsListener) 241 | } 242 | } 243 | 244 | render() { 245 | const chartStyle = { parent: { width: '100%', padding: '0', margin: '0'} } 246 | const sansSerif = `'Roboto', 'Helvetica Neue', Helvetica, sans-serif` 247 | const { goals } = this.state 248 | if (!this.state.ready) return 249 | 250 | return ( 251 |
252 |
253 | {this.state.goals.length > 0 ? 254 | 267 | } 268 | padding={{top: 0, left: 0, right: 0, bottom: 0}} 269 | > 270 | 283 | } 293 | /> 294 | 295 | { 296 | this.state.goals && this.state.goals.map((goal, index) => { 297 | // Get goal info out of goal array: index 0 is goal id and index 1 is object with all other data 298 | let goalId = goal[0] 299 | let goalInfo = goal[1] 300 | let color 301 | if (goalInfo.color) color = goalInfo.color.hex 302 | else color = '#888' // Makes a default goal color, in case user didn't set one 303 | 304 | return ( 305 | { this.handleLineTap(event, goal) } 320 | } 321 | }]} 322 | data={this.getLineData(goalInfo, index)} 323 | labelComponent={} 324 | /> 325 | ) 326 | }) 327 | }{ 328 | this.state.goals && this.state.goals.map((goal, index) => { 329 | let goalId = goal[0] 330 | let goalInfo = goal[1] 331 | let color 332 | if (goalInfo.color) color = goalInfo.color.hex 333 | else color = '#888' 334 | 335 | return ( 336 | { 349 | let goalPath = props.data[props.index].key 350 | browserHistory.push(goalPath) 351 | } 352 | } 353 | }]} 354 | data={this.getScatterData(goalInfo, index, goalId)} 355 | labelComponent={} 356 | /> 357 | ) 358 | }) 359 | } 360 | 361 | :
} 362 |
363 | 364 | {/* Overview chart at the bottom 365 | (Only shown if there are goals) */} 366 | 367 | {this.state.goals.length > 0 ? 368 |
369 | 379 | } 380 | > 381 | 389 | { 390 | this.state.goals && this.state.goals.map((goal, index) => { 391 | let goalInfo = goal[1] 392 | let color 393 | if (goalInfo.color) color = goalInfo.color.hex 394 | else color = '#888' 395 | 396 | return ( 397 | 410 | ) 411 | }) 412 | } 413 | 414 |
: null } 415 | 416 | 417 | 418 | 419 | 420 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 |
434 | 435 | ) 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /app/components/Upload.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import firebase from 'APP/fire' 3 | import FileUploader from 'react-firebase-file-uploader' 4 | import Add from 'material-ui/svg-icons/content/add' 5 | import CircularProgress from 'material-ui/CircularProgress' 6 | import RaisedButton from 'material-ui/RaisedButton' 7 | const db = firebase.database() 8 | 9 | class InputButton extends Component { 10 | constructor(props) { 11 | super() 12 | this.state = { 13 | 14 | } 15 | } 16 | 17 | componentWillReceiveProps(newProps, oldProps) { 18 | this.setState({onChange: newProps.onChange}) 19 | } 20 | 21 | render() { 22 | return ( 23 | } 27 | primary={true} 28 | > 29 | 30 | 31 | ) 32 | } 33 | } 34 | 35 | class Upload extends Component { 36 | constructor(props) { 37 | super() 38 | this.state = { 39 | image: '', 40 | isUploading: false, 41 | progress: 0 42 | } 43 | } 44 | 45 | handleUploadStart = () => this.setState({isUploading: true, progress: 0}); 46 | handleProgress = (progress) => this.setState({progress}); 47 | handleUploadError = (error) => { 48 | this.setState({isUploading: false}) 49 | console.error(error) 50 | } 51 | handleUploadSuccess = (filename) => { 52 | this.setState({image: filename, progress: 100, isUploading: false}) 53 | firebase.storage().ref('images').child(filename).getDownloadURL().then(url => { 54 | // check to see if upload is to milestone, check in, or just goal: 55 | const key = this.props.goalRef.push().key 56 | if (this.props.milestoneRef) { 57 | // add image URL to parent goal's uploads: 58 | this.props.goalRef.child('uploads').child(key).set({ 59 | imageURL: url, 60 | milestoneId: this.props.milestoneId 61 | }) 62 | // add image URL to milestone: 63 | this.props.milestoneRef.child(key).set({ 64 | imageURL: url 65 | }) 66 | } else if (this.props.checkInRef) { 67 | // add image URL to parent goal's uploads: 68 | this.props.goalRef.child('uploads').child(key).set({ 69 | imageURL: url, 70 | checkInId: this.props.checkInId 71 | }) 72 | // add image URL to check in: 73 | this.props.checkInRef.child(key).set({ 74 | imageURL: url 75 | }) 76 | } else { 77 | this.props.goalRef.child(key).set({ 78 | imageURL: url 79 | }) 80 | } 81 | }) 82 | }; 83 | 84 | render() { 85 | return ( 86 |
87 |
88 | 89 | {this.state.isUploading && 90 |

Progress: {this.state.progress}

91 | } 92 | {this.state.isUploading && 93 | 99 | } 100 | 111 | 112 |
113 | ) 114 | } 115 | } 116 | 117 | export default Upload 118 | -------------------------------------------------------------------------------- /app/components/UploadCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 4 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' 5 | import alignTheme from './AlignTheme' 6 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 7 | import TextField from 'material-ui/TextField' 8 | import {Card, CardMedia, CardText} from 'material-ui/Card' 9 | 10 | import firebase from 'APP/fire' 11 | const db = firebase.database() 12 | let captionRef, parentRef, child, childRef 13 | 14 | // we're basically adding a child (or updating child) for something that already exists 15 | // this component will receive goalRef, plus optional milestoneRef or checkInRef, as props 16 | // if we have milestone or checkIn refs, we'll also want to write to the goalRef (parent)? 17 | // BUT if we only get goalRef, we'll also want to check whether we can write to milestone or checkIn? 18 | 19 | export default class extends Component { 20 | constructor(props) { 21 | super() 22 | this.state = { 23 | caption: '' 24 | } 25 | } 26 | 27 | componentDidMount() { 28 | // When the component mounts, start listening to the fireRef we were given. 29 | this.listenTo(this.props.goalRef) 30 | } 31 | 32 | componentWillUnmount() { 33 | // When we unmount, stop listening. 34 | this.unsubscribe() 35 | } 36 | 37 | componentWillReceiveProps(incoming, outgoing) { 38 | // When the props sent by our parent component change, start listening to the new reference. 39 | this.listenTo(incoming.goalRef) 40 | } 41 | 42 | listenTo(goalRef) { 43 | // If we're already listening to a ref, stop listening there. 44 | if (this.unsubscribe) this.unsubscribe() 45 | 46 | let mileId = this.props.milestoneId 47 | let checkInId = this.props.checkInId 48 | const uploadId = this.props.uploadId 49 | 50 | // what if we just make it so that you can't update captions for mstone / checkin uploads on the goal?? 51 | if (this.props.milestoneRef) { 52 | captionRef = goalRef.child('milestones').child(mileId).child('uploads').child(uploadId).child('caption') 53 | parentRef = goalRef.child('uploads').child(uploadId).child('caption') 54 | } else if (this.props.checkInRef) { 55 | captionRef = goalRef.child('checkIns').child(checkInId).child('uploads').child(uploadId).child('caption') 56 | parentRef = goalRef.child('uploads').child(uploadId).child('caption') 57 | } else { 58 | captionRef = goalRef.child(uploadId).child('caption') 59 | } 60 | 61 | /* 62 | // here's an attempt to start doing this by child instead of by parent; requires adjusting what we pass down as goalRefs 63 | const uploadId = this.props.uploadId 64 | const mileId = goalRef.child(uploadId).child('milestoneId') || null 65 | const checkInId = goalRef.child(uploadId).child('checkInId') || null 66 | 67 | captionRef = goalRef.child('uploads').child(uploadId).child('caption') 68 | if (mileId) { 69 | childRef = goalRef.child('milestones').child(mileId).child('uploads').child(uploadId).child('caption') 70 | } else if (checkInId) { 71 | childRef = goalRef.child('checkIns').child(checkInId).child('uploads').child(uploadId).child('caption') 72 | } else { 73 | childRef = null // ?? 74 | } 75 | */ 76 | 77 | // Whenever our ref's value changes, set {value} on our state 78 | const captionListener = captionRef.on('value', snapshot => { 79 | this.setState({ caption: snapshot.val() }) 80 | }) 81 | 82 | // Set unsubscribe to be a function that detaches the listener. 83 | this.unsubscribe = () => { 84 | captionRef.off('value', captionListener) 85 | } 86 | } 87 | 88 | writeCaption = (event) => { 89 | captionRef.set(event.target.value) 90 | if (parentRef) parentRef.set(event.target.value) 91 | if (childRef) childRef.set(event.target.value) 92 | } 93 | 94 | render() { 95 | const textStyle = { width: 215 } 96 | 97 | return ( 98 | 99 | 100 | 101 | 102 | 103 | 104 | 112 | 113 | 114 | 115 | ) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/components/WhoAmI.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import firebase from 'APP/fire' 3 | import {browserHistory} from 'react-router' 4 | import FlatButton from 'material-ui/FlatButton' 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 6 | import alignTheme from './AlignTheme' 7 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 8 | const auth = firebase.auth() 9 | 10 | export const name = user => { 11 | if (!user) { 12 | return 'Nobody' 13 | } 14 | return user.displayName 15 | } 16 | 17 | export const WhoAmI = ({user, auth}) => 18 |
19 | {name(user)} 20 | 21 | { 22 | auth.signOut() 23 | .then(() => { // After logging out, redirect to login/landing page 24 | browserHistory.push('/')}) 25 | }} primary={true} /> 26 | 27 |
28 | 29 | export default class extends React.Component { 30 | componentDidMount() { 31 | const {auth} = this.props 32 | this.unsubscribe = auth.onAuthStateChanged(user => this.setState({user})) 33 | } 34 | 35 | componentWillUnmount() { 36 | this.unsubscribe() 37 | } 38 | 39 | render() { 40 | const {user} = this.state || {} 41 | return 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/components/WhoAmI.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | import {createStore} from 'redux' 8 | 9 | import WhoAmIContainer, {WhoAmI} from './WhoAmI' 10 | import Login from './Login' 11 | 12 | /* global describe it beforeEach */ 13 | describe('', () => { 14 | describe('when nobody is logged in', () => { 15 | let root 16 | beforeEach('render the root', () => 17 | root = shallow() 18 | ) 19 | 20 | it('says hello to Nobody', () => { 21 | expect(root.text()).to.contain('Nobody') 22 | }) 23 | }) 24 | 25 | describe('when an anonymous user is logged in', () => { 26 | const user = { 27 | displayName: null, 28 | isAnonymous: true, 29 | } 30 | let root 31 | beforeEach('render the root', () => 32 | root = shallow() 33 | ) 34 | 35 | it('says hello to Anonymous', () => { 36 | expect(root.text()).to.contain('Anonymous') 37 | }) 38 | 39 | it('displays a Login component', () => { 40 | expect(root.find(Login)).to.have.length(1) 41 | }) 42 | }) 43 | 44 | describe('when a user is logged in', () => { 45 | const user = { 46 | isAnonymous: false, 47 | displayName: 'Grace Hopper', 48 | } 49 | const fakeAuth = {signOut: spy()} 50 | let root 51 | beforeEach('render the root', () => 52 | root = shallow() 53 | ) 54 | 55 | it('has a logout button', () => { 56 | expect(root.find('button.logout')).to.have.length(1) 57 | }) 58 | 59 | it('calls props.auth.signOut when logout is tapped', () => { 60 | root.find('button.logout').simulate('click') 61 | expect(fakeAuth.signOut).to.have.been.called 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /app/main.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import React from 'react' 3 | import { Router, Route, IndexRedirect, browserHistory } from 'react-router' 4 | import { render } from 'react-dom' 5 | 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 7 | import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme' 8 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 9 | import { AppBar, FlatButton } from 'material-ui' 10 | 11 | import injectTapEventPlugin from 'react-tap-event-plugin' 12 | injectTapEventPlugin() 13 | 14 | import WhoAmI from './components/WhoAmI' 15 | import NotFound from './components/NotFound' 16 | import Upload from './components/Upload' 17 | import GoalFormContainer from './components/GoalFormContainer' 18 | import MilestoneFormContainer from './components/MilestoneFormContainer' 19 | import CheckInFormContainer from './components/CheckInFormContainer' 20 | import Timelines from './components/Timelines' 21 | import Landing from './components/Landing' 22 | import Loader from './components/Loader' 23 | import Navbar from './components/Navbar' 24 | import Doorslam from './components/Doorslam' 25 | 26 | import firebase from 'APP/fire' 27 | 28 | // Get the auth API from Firebase. 29 | const auth = firebase.auth() 30 | 31 | // Our root App component just renders a little frame with a nav 32 | // and whatever children the router gave us. 33 | const App = ({ children }) => 34 | 35 |
36 | 37 | {/* In theory you can use MUI components in this and its children? http://www.material-ui.com/#/components */} 38 | {/* Render our children (whatever the router gives us) */} 39 | {children} 40 |
41 |
42 | 43 | render( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | , 55 | document.getElementById('main') 56 | ) 57 | -------------------------------------------------------------------------------- /bin/build-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Paths to add to the deployment branch. 4 | # 5 | # These paths will be added with git add -f, to include build artifacts 6 | # we normally ignore in the branch we push to heroku. 7 | build_paths="public" 8 | 9 | # colors 10 | red='\033[0;31m' 11 | blue='\033[0;34m' 12 | off='\033[0m' 13 | 14 | echoed() { 15 | echo "${blue}${*}${off}" 16 | $* 17 | } 18 | 19 | if [[ $(git status --porcelain 2> /dev/null | grep -v '$\?\?' | tail -n1) != "" ]]; then 20 | echo "${red}Uncommitted changes would be lost. Commit or stash these changes:${off}" 21 | git status 22 | exit 1 23 | fi 24 | 25 | # Our branch name is build/commit-sha-hash 26 | version="$(git log -n1 --pretty=format:%H)" 27 | branch_name="build/${version}" 28 | 29 | 30 | function create_build_branch() { 31 | git branch "${branch_name}" 32 | git checkout "${branch_name}" 33 | return 0 34 | } 35 | 36 | function commit_build_artifacts() { 37 | # Add our build paths. -f means "even if it's in .gitignore'". 38 | git add -f "${build_paths}" 39 | 40 | # Commit the build artifacts on the branch. 41 | git commit -a -m "Built ${version} on $(date)." 42 | 43 | # Always succeed. 44 | return 0 45 | } 46 | 47 | # We expect to be sourced by some file that defines a deploy 48 | # function. If deploy() isn't defined, define a stub function. 49 | if [[ -z $(type -t deploy) ]]; then 50 | function deploy() { 51 | echo '(No deployment step defined.)' 52 | return 0 53 | } 54 | fi 55 | 56 | ( 57 | create_build_branch && 58 | echoed yarn && 59 | echoed npm run build && 60 | commit_build_artifacts && 61 | deploy 62 | 63 | # Regardless of whether we succeeded or failed, go back to 64 | # the previous branch. 65 | git checkout - 66 | ) 67 | -------------------------------------------------------------------------------- /bin/deploy-heroku.sh: -------------------------------------------------------------------------------- 1 | # By default, we git push our build branch to heroku master. 2 | # You can specify DEPLOY_REMOTE and DEPLOY_BRANCH to configure 3 | # this. 4 | deploy_remote="${DEPLOY_REMOTE:-heroku}" 5 | deploy_branch="${DEPLOY_BRANCH:-master}" 6 | 7 | deploy() { 8 | git push -f "$deploy_remote" "$branch_name:$deploy_branch" 9 | } 10 | 11 | . "$(dirname $0)/build-branch.sh" 12 | -------------------------------------------------------------------------------- /bin/mkapplink.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | // 'bin/setup' is a symlink pointing to this file, which makes a 6 | // symlink in your project's main node_modules folder that points to 7 | // the root of your project's directory. 8 | 9 | const chalk = require('chalk') 10 | , fs = require('fs') 11 | , {resolve} = require('path') 12 | 13 | , appLink = resolve(__dirname, '..', 'node_modules', 'APP') 14 | 15 | , symlinkError = error => 16 | `******************************************************************* 17 | ${appLink} must point to '..' 18 | 19 | This symlink lets you require('APP/some/path') rather than 20 | ../../../some/path 21 | 22 | I tried to create it, but got this error: 23 | ${error.message} 24 | 25 | You might try this: 26 | 27 | rm ${appLink} 28 | 29 | Then run me again. 30 | 31 | ~ xoxo, bones 32 | ********************************************************************` 33 | 34 | function makeAppSymlink() { 35 | console.log(`Linking '${appLink}' to '..'`) 36 | try { 37 | // fs.unlinkSync docs: https://nodejs.org/api/fs.html#fs_fs_unlinksync_path 38 | try { fs.unlinkSync(appLink) } catch (swallowed) { } 39 | // fs.symlinkSync docs: https://nodejs.org/api/fs.html#fs_fs_symlinksync_target_path_type 40 | const linkType = process.platform === 'win32' ? 'junction' : 'dir' 41 | fs.symlinkSync('..', appLink, linkType) 42 | } catch (error) { 43 | console.error(chalk.red(symlinkError(error))) 44 | // process.exit docs: https://nodejs.org/api/process.html#process_process_exit_code 45 | process.exit(1) 46 | } 47 | console.log(`Ok, created ${appLink}`) 48 | } 49 | 50 | function ensureAppSymlink() { 51 | try { 52 | // readlinkSync docs: https://nodejs.org/api/fs.html#fs_fs_readlinksync_path_options 53 | const currently = fs.readlinkSync(appLink) 54 | if (currently !== '..') { 55 | throw new Error(`${appLink} is pointing to '${currently}' rather than '..'`) 56 | } 57 | } catch (error) { 58 | makeAppSymlink() 59 | } 60 | } 61 | 62 | if (module === require.main) { 63 | ensureAppSymlink() 64 | } 65 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | mkapplink.js -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "auth != null", 4 | ".write": "auth != null" 5 | } 6 | } -------------------------------------------------------------------------------- /dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Concurrently run our various dev tasks. 3 | * 4 | * Usage: node dev 5 | **/ 6 | 7 | const app = require('.') 8 | , chalk = require('chalk'), {bold} = chalk 9 | , {red, green, blue, cyan, yellow} = bold 10 | , dev = module.exports = () => run({ 11 | server: task(app.package.scripts['start'], {color: blue}), 12 | build: task(app.package.scripts['build-watch'], {color: green}), 13 | // lint: task(app.package.scripts['lint-watch'], {color: cyan}), 14 | // test: task(app.package.scripts['test-watch'], {color: yellow}) 15 | }) 16 | 17 | const taskEnvironment = (path=require('path')) => { 18 | const env = {} 19 | for (const key in process.env) { 20 | env[key] = process.env[key] 21 | } 22 | Object.assign(env, { 23 | NODE_ENV: 'development', 24 | PATH: [ path.join(app.root, 'node_modules', '.bin') 25 | , process.env.PATH ].join(path.delimiter) 26 | }) 27 | return env 28 | } 29 | 30 | function run(tasks) { 31 | Object.keys(tasks) 32 | .map(name => tasks[name](name)) 33 | } 34 | 35 | function task(command, { 36 | spawn=require('child_process').spawn, 37 | path=require('path'), 38 | color 39 | }={}) { 40 | return name => { 41 | const stdout = log({name, color}, process.stdout) 42 | , stderr = log({name, color, text: red}, process.stderr) 43 | , proc = spawn(command, { 44 | shell: true, 45 | stdio: 'pipe', 46 | env: taskEnvironment(), 47 | }).on('error', stderr) 48 | .on('exit', (code, signal) => { 49 | stderr(`Exited with code ${code}`) 50 | if (signal) stderr(`Exited with signal ${signal}`) 51 | }) 52 | proc.stdout.on('data', stdout) 53 | proc.stderr.on('data', stderr) 54 | } 55 | } 56 | 57 | function log({ 58 | name, 59 | ts=timestamp, 60 | color=none, 61 | text=none, 62 | }, out=process.stdout) { 63 | return data => data.toString() 64 | // Strip out screen-clearing control sequences, which really 65 | // muck up the output. 66 | .replace('\u001b[2J', '') 67 | .replace('\u001b[1;3H', '') 68 | .split('\n') 69 | .forEach(line => out.write(`${color(`${ts()} ${name} \t⎹ `)}${text(line)}\n`)) 70 | } 71 | 72 | const dateformat = require('dateformat') 73 | function timestamp() { 74 | return dateformat('yyyy-mm-dd HH:MM:ss (Z)') 75 | } 76 | 77 | function none(x) { return x } 78 | 79 | if (module === require.main) { dev() } 80 | -------------------------------------------------------------------------------- /fire/index.js: -------------------------------------------------------------------------------- 1 | const firebase = require('firebase') 2 | 3 | // -- // -- // -- // Firebase Config // -- // -- // -- // 4 | const config = { 5 | apiKey: 'AIzaSyAv8u4ojMJxzEykV47bgeL4U2dIKBu5x0o', 6 | authDomain: 'align-a0b08.firebaseapp.com', 7 | databaseURL: 'https://align-a0b08.firebaseio.com', 8 | projectId: 'align-a0b08', 9 | storageBucket: 'align-a0b08.appspot.com', 10 | messagingSenderId: '578906705389' 11 | } 12 | // -- // -- // -- // -- // -- // -- // -- // -- // -- // 13 | 14 | // Initialize the app, but make sure to do it only once. 15 | // (We need this for the tests. The test runner busts the require 16 | // cache when in watch mode; this will cause us to evaluate this 17 | // file multiple times. Without this protection, we would try to 18 | // initialize the app again, which causes Firebase to throw. 19 | // 20 | // This is why global state makes a sad panda.) 21 | firebase.__bonesApp || (firebase.__bonesApp = firebase.initializeApp(config)) 22 | 23 | module.exports = firebase 24 | -------------------------------------------------------------------------------- /fire/refs.js: -------------------------------------------------------------------------------- 1 | import firebase from 'APP/fire' 2 | const db = firebase.database() 3 | 4 | exports.getGoalRefs = id => ({ 5 | nameRef: db.ref('goals').child(id).child('name'), 6 | descriptionRef: db.ref('goals').child(id).child('description'), 7 | isOpenRef: db.ref('goals').child(id).child('isOpen'), 8 | startRef: db.ref('goals').child(id).child('startDate'), 9 | endRef: db.ref('goals').child(id).child('endDate'), 10 | colorRef: db.ref('goals').child(id).child('color'), 11 | milestonesRef: db.ref('goals').child(id).child('milestones'), 12 | checkInsRef: db.ref('goals').child(id).child('checkIns'), 13 | resourcesRef: db.ref('goals').child(id).child('resources'), // will return an object of resource ids on a goal 14 | uploadsRef: db.ref('goals').child(id).child('uploads'), 15 | notesRef: db.ref('goals').child(id).child('notes') 16 | }) 17 | 18 | exports.getMilestoneRefs = (goalId, mileId) => ({ 19 | milestoneRef: db.ref('goals').child(goalId).child('milestones').child(mileId), 20 | nameRef: db.ref('goals').child(goalId).child('milestones').child(mileId).child('name'), 21 | descriptionRef: db.ref('goals').child(goalId).child('milestones').child(mileId).child('description'), 22 | isOpenRef: db.ref('goals').child(goalId).child('milestones').child(mileId).child('isOpen'), 23 | dateRef: db.ref('goals').child(goalId).child('milestones').child(mileId).child('displayDate'), 24 | resourcesRef: db.ref('goals').child(goalId).child('milestones').child(mileId).child('resources'), // will return an object of resource ids on the milestone 25 | uploadsRef: db.ref('goals').child(goalId).child('milestones').child(mileId).child('uploads'), 26 | parentRef: db.ref('goals').child(goalId), 27 | notesRef: db.ref('goals').child(goalId).child('milestones').child(mileId).child('notes') 28 | }) 29 | 30 | exports.getCheckInRefs = (goalId, checkId) => ({ 31 | checkInRef: db.ref('goals').child(goalId).child('checkIns').child(checkId), 32 | nameRef: db.ref('goals').child(goalId).child('checkIns').child(checkId).child('name'), 33 | descriptionRef: db.ref('goals').child(goalId).child('checkIns').child(checkId).child('description'), 34 | dateRef: db.ref('goals').child(goalId).child('checkIns').child(checkId).child('displayDate'), 35 | resourcesRef: db.ref('goals').child(goalId).child('checkIns').child(checkId).child('resources'), // will return an object of resource ids on the check-in 36 | uploadsRef: db.ref('goals').child(goalId).child('checkIns').child(checkId).child('uploads'), 37 | parentRef: db.ref('goals').child(goalId), 38 | notesRef: db.ref('goals').child(goalId).child('checkIns').child(checkId).child('notes') 39 | }) 40 | 41 | exports.getResourceRefs = (id, goalId) => ({ 42 | resourceRef: db.ref('resources').child(id), 43 | urlRef: db.ref('resources').child(id).child('url'), 44 | titleRef: db.ref('resources').child(id).child('title'), 45 | imageRef: db.ref('resources').child(id).child('image'), 46 | descriptionRef: db.ref('resources').child(id).child('description'), 47 | milestoneRef: db.ref('goals').child(goalId).child('resources').child(id).child('milestoneId') 48 | }) 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | var functions = require('firebase-functions') 2 | 3 | // // Start writing Firebase Functions 4 | // // https://firebase.google.com/preview/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https().onRequest((request, response) => { 7 | // response.send('Hello from Firebase!') 8 | // }) 9 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Firebase Functions", 4 | "dependencies": { 5 | "firebase": "^3.1", 6 | "firebase-admin": "^5.0.0", 7 | "firebase-functions": "^0.5.8" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /functions/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/express-serve-static-core@*": 6 | version "4.0.49" 7 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.49.tgz#3438d68d26e39db934ba941f18e3862a1beeb722" 8 | dependencies: 9 | "@types/node" "*" 10 | 11 | "@types/express@^4.0.33": 12 | version "4.0.36" 13 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2" 14 | dependencies: 15 | "@types/express-serve-static-core" "*" 16 | "@types/serve-static" "*" 17 | 18 | "@types/jsonwebtoken@^7.1.32", "@types/jsonwebtoken@^7.1.33": 19 | version "7.2.1" 20 | resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.1.tgz#44a0282b48d242d0760eab0ce6cf570a62eabf96" 21 | dependencies: 22 | "@types/node" "*" 23 | 24 | "@types/lodash@^4.14.34": 25 | version "4.14.68" 26 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.68.tgz#754fbab68bd2bbb69547dc8ce7574f7012eed7f6" 27 | 28 | "@types/mime@*": 29 | version "1.3.1" 30 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.1.tgz#2cf42972d0931c1060c7d5fa6627fce6bd876f2f" 31 | 32 | "@types/node@*": 33 | version "8.0.8" 34 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.8.tgz#0dc4ca2c6f6fc69baee16c5e928c4a627f517ada" 35 | 36 | "@types/serve-static@*": 37 | version "1.7.31" 38 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.7.31.tgz#15456de8d98d6b4cff31be6c6af7492ae63f521a" 39 | dependencies: 40 | "@types/express-serve-static-core" "*" 41 | "@types/mime" "*" 42 | 43 | "@types/sha1@^1.1.0": 44 | version "1.1.0" 45 | resolved "https://registry.yarnpkg.com/@types/sha1/-/sha1-1.1.0.tgz#461eb18906d25e8d07c4678a0ed4f9ca07e46dd9" 46 | dependencies: 47 | "@types/node" "*" 48 | 49 | accepts@~1.3.3: 50 | version "1.3.3" 51 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 52 | dependencies: 53 | mime-types "~2.1.11" 54 | negotiator "0.6.1" 55 | 56 | array-flatten@1.1.1: 57 | version "1.1.1" 58 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 59 | 60 | base64url@2.0.0, base64url@^2.0.0: 61 | version "2.0.0" 62 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 63 | 64 | buffer-equal-constant-time@1.0.1: 65 | version "1.0.1" 66 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 67 | 68 | "charenc@>= 0.0.1": 69 | version "0.0.2" 70 | resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 71 | 72 | content-disposition@0.5.2: 73 | version "0.5.2" 74 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 75 | 76 | content-type@~1.0.2: 77 | version "1.0.2" 78 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 79 | 80 | cookie-signature@1.0.6: 81 | version "1.0.6" 82 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 83 | 84 | cookie@0.3.1: 85 | version "0.3.1" 86 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 87 | 88 | "crypt@>= 0.0.1": 89 | version "0.0.2" 90 | resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" 91 | 92 | debug@2.6.7: 93 | version "2.6.7" 94 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" 95 | dependencies: 96 | ms "2.0.0" 97 | 98 | depd@1.1.0, depd@~1.1.0: 99 | version "1.1.0" 100 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 101 | 102 | destroy@~1.0.4: 103 | version "1.0.4" 104 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 105 | 106 | dom-storage@^2.0.2: 107 | version "2.0.2" 108 | resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.0.2.tgz#ed17cbf68abd10e0aef8182713e297c5e4b500b0" 109 | 110 | ecdsa-sig-formatter@1.0.9: 111 | version "1.0.9" 112 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 113 | dependencies: 114 | base64url "^2.0.0" 115 | safe-buffer "^5.0.1" 116 | 117 | ee-first@1.1.1: 118 | version "1.1.1" 119 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 120 | 121 | encodeurl@~1.0.1: 122 | version "1.0.1" 123 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 124 | 125 | escape-html@~1.0.3: 126 | version "1.0.3" 127 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 128 | 129 | etag@~1.8.0: 130 | version "1.8.0" 131 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" 132 | 133 | express@^4.0.33: 134 | version "4.15.3" 135 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" 136 | dependencies: 137 | accepts "~1.3.3" 138 | array-flatten "1.1.1" 139 | content-disposition "0.5.2" 140 | content-type "~1.0.2" 141 | cookie "0.3.1" 142 | cookie-signature "1.0.6" 143 | debug "2.6.7" 144 | depd "~1.1.0" 145 | encodeurl "~1.0.1" 146 | escape-html "~1.0.3" 147 | etag "~1.8.0" 148 | finalhandler "~1.0.3" 149 | fresh "0.5.0" 150 | merge-descriptors "1.0.1" 151 | methods "~1.1.2" 152 | on-finished "~2.3.0" 153 | parseurl "~1.3.1" 154 | path-to-regexp "0.1.7" 155 | proxy-addr "~1.1.4" 156 | qs "6.4.0" 157 | range-parser "~1.2.0" 158 | send "0.15.3" 159 | serve-static "1.12.3" 160 | setprototypeof "1.0.3" 161 | statuses "~1.3.1" 162 | type-is "~1.6.15" 163 | utils-merge "1.0.0" 164 | vary "~1.1.1" 165 | 166 | faye-websocket@0.9.3: 167 | version "0.9.3" 168 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.3.tgz#482a505b0df0ae626b969866d3bd740cdb962e83" 169 | dependencies: 170 | websocket-driver ">=0.5.1" 171 | 172 | finalhandler@~1.0.3: 173 | version "1.0.3" 174 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" 175 | dependencies: 176 | debug "2.6.7" 177 | encodeurl "~1.0.1" 178 | escape-html "~1.0.3" 179 | on-finished "~2.3.0" 180 | parseurl "~1.3.1" 181 | statuses "~1.3.1" 182 | unpipe "~1.0.0" 183 | 184 | firebase-admin@^5.0.0: 185 | version "5.0.0" 186 | resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-5.0.0.tgz#fadae56c99be4fb56c781007d6719d153fe8fd7b" 187 | dependencies: 188 | "@types/jsonwebtoken" "^7.1.33" 189 | faye-websocket "0.9.3" 190 | jsonwebtoken "7.1.9" 191 | node-forge "0.7.1" 192 | 193 | firebase-functions@^0.5.8: 194 | version "0.5.8" 195 | resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-0.5.8.tgz#b8def16a6f9e777046c1bf303b3d08adb9cf5c29" 196 | dependencies: 197 | "@types/express" "^4.0.33" 198 | "@types/jsonwebtoken" "^7.1.32" 199 | "@types/lodash" "^4.14.34" 200 | "@types/sha1" "^1.1.0" 201 | express "^4.0.33" 202 | jsonwebtoken "^7.1.9" 203 | lodash "^4.6.1" 204 | sha1 "^1.1.1" 205 | 206 | firebase@^3.1: 207 | version "3.9.0" 208 | resolved "https://registry.yarnpkg.com/firebase/-/firebase-3.9.0.tgz#c4237f50f58eeb25081b1839d6cbf175f8f7ed9b" 209 | dependencies: 210 | dom-storage "^2.0.2" 211 | faye-websocket "0.9.3" 212 | jsonwebtoken "^7.3.0" 213 | promise-polyfill "^6.0.2" 214 | xmlhttprequest "^1.8.0" 215 | 216 | forwarded@~0.1.0: 217 | version "0.1.0" 218 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 219 | 220 | fresh@0.5.0: 221 | version "0.5.0" 222 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" 223 | 224 | hoek@2.x.x: 225 | version "2.16.3" 226 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 227 | 228 | http-errors@~1.6.1: 229 | version "1.6.1" 230 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" 231 | dependencies: 232 | depd "1.1.0" 233 | inherits "2.0.3" 234 | setprototypeof "1.0.3" 235 | statuses ">= 1.3.1 < 2" 236 | 237 | inherits@2.0.3: 238 | version "2.0.3" 239 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 240 | 241 | ipaddr.js@1.3.0: 242 | version "1.3.0" 243 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" 244 | 245 | isemail@1.x.x: 246 | version "1.2.0" 247 | resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" 248 | 249 | joi@^6.10.1: 250 | version "6.10.1" 251 | resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" 252 | dependencies: 253 | hoek "2.x.x" 254 | isemail "1.x.x" 255 | moment "2.x.x" 256 | topo "1.x.x" 257 | 258 | jsonwebtoken@7.1.9: 259 | version "7.1.9" 260 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.1.9.tgz#847804e5258bec5a9499a8dc4a5e7a3bae08d58a" 261 | dependencies: 262 | joi "^6.10.1" 263 | jws "^3.1.3" 264 | lodash.once "^4.0.0" 265 | ms "^0.7.1" 266 | xtend "^4.0.1" 267 | 268 | jsonwebtoken@^7.1.9, jsonwebtoken@^7.3.0: 269 | version "7.4.1" 270 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz#7ca324f5215f8be039cd35a6c45bb8cb74a448fb" 271 | dependencies: 272 | joi "^6.10.1" 273 | jws "^3.1.4" 274 | lodash.once "^4.0.0" 275 | ms "^2.0.0" 276 | xtend "^4.0.1" 277 | 278 | jwa@^1.1.4: 279 | version "1.1.5" 280 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 281 | dependencies: 282 | base64url "2.0.0" 283 | buffer-equal-constant-time "1.0.1" 284 | ecdsa-sig-formatter "1.0.9" 285 | safe-buffer "^5.0.1" 286 | 287 | jws@^3.1.3, jws@^3.1.4: 288 | version "3.1.4" 289 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 290 | dependencies: 291 | base64url "^2.0.0" 292 | jwa "^1.1.4" 293 | safe-buffer "^5.0.1" 294 | 295 | lodash.once@^4.0.0: 296 | version "4.1.1" 297 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 298 | 299 | lodash@^4.6.1: 300 | version "4.17.4" 301 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 302 | 303 | media-typer@0.3.0: 304 | version "0.3.0" 305 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 306 | 307 | merge-descriptors@1.0.1: 308 | version "1.0.1" 309 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 310 | 311 | methods@~1.1.2: 312 | version "1.1.2" 313 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 314 | 315 | mime-db@~1.27.0: 316 | version "1.27.0" 317 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" 318 | 319 | mime-types@~2.1.11, mime-types@~2.1.15: 320 | version "2.1.15" 321 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" 322 | dependencies: 323 | mime-db "~1.27.0" 324 | 325 | mime@1.3.4: 326 | version "1.3.4" 327 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 328 | 329 | moment@2.x.x: 330 | version "2.18.1" 331 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" 332 | 333 | ms@2.0.0, ms@^2.0.0: 334 | version "2.0.0" 335 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 336 | 337 | ms@^0.7.1: 338 | version "0.7.3" 339 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" 340 | 341 | negotiator@0.6.1: 342 | version "0.6.1" 343 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 344 | 345 | node-forge@0.7.1: 346 | version "0.7.1" 347 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" 348 | 349 | on-finished@~2.3.0: 350 | version "2.3.0" 351 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 352 | dependencies: 353 | ee-first "1.1.1" 354 | 355 | parseurl@~1.3.1: 356 | version "1.3.1" 357 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 358 | 359 | path-to-regexp@0.1.7: 360 | version "0.1.7" 361 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 362 | 363 | promise-polyfill@^6.0.2: 364 | version "6.0.2" 365 | resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.0.2.tgz#d9c86d3dc4dc2df9016e88946defd69b49b41162" 366 | 367 | proxy-addr@~1.1.4: 368 | version "1.1.4" 369 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" 370 | dependencies: 371 | forwarded "~0.1.0" 372 | ipaddr.js "1.3.0" 373 | 374 | qs@6.4.0: 375 | version "6.4.0" 376 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 377 | 378 | range-parser@~1.2.0: 379 | version "1.2.0" 380 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 381 | 382 | safe-buffer@^5.0.1: 383 | version "5.1.1" 384 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 385 | 386 | send@0.15.3: 387 | version "0.15.3" 388 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" 389 | dependencies: 390 | debug "2.6.7" 391 | depd "~1.1.0" 392 | destroy "~1.0.4" 393 | encodeurl "~1.0.1" 394 | escape-html "~1.0.3" 395 | etag "~1.8.0" 396 | fresh "0.5.0" 397 | http-errors "~1.6.1" 398 | mime "1.3.4" 399 | ms "2.0.0" 400 | on-finished "~2.3.0" 401 | range-parser "~1.2.0" 402 | statuses "~1.3.1" 403 | 404 | serve-static@1.12.3: 405 | version "1.12.3" 406 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" 407 | dependencies: 408 | encodeurl "~1.0.1" 409 | escape-html "~1.0.3" 410 | parseurl "~1.3.1" 411 | send "0.15.3" 412 | 413 | setprototypeof@1.0.3: 414 | version "1.0.3" 415 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 416 | 417 | sha1@^1.1.1: 418 | version "1.1.1" 419 | resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848" 420 | dependencies: 421 | charenc ">= 0.0.1" 422 | crypt ">= 0.0.1" 423 | 424 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 425 | version "1.3.1" 426 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 427 | 428 | topo@1.x.x: 429 | version "1.1.0" 430 | resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" 431 | dependencies: 432 | hoek "2.x.x" 433 | 434 | type-is@~1.6.15: 435 | version "1.6.15" 436 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 437 | dependencies: 438 | media-typer "0.3.0" 439 | mime-types "~2.1.15" 440 | 441 | unpipe@~1.0.0: 442 | version "1.0.0" 443 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 444 | 445 | utils-merge@1.0.0: 446 | version "1.0.0" 447 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 448 | 449 | vary@~1.1.1: 450 | version "1.1.1" 451 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" 452 | 453 | websocket-driver@>=0.5.1: 454 | version "0.6.5" 455 | resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" 456 | dependencies: 457 | websocket-extensions ">=0.1.1" 458 | 459 | websocket-extensions@>=0.1.1: 460 | version "0.1.1" 461 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" 462 | 463 | xmlhttprequest@^1.8.0: 464 | version "1.8.0" 465 | resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" 466 | 467 | xtend@^4.0.1: 468 | version "4.0.1" 469 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 470 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {resolve} = require('path') 4 | , chalk = require('chalk') 5 | , pkg = require('./package.json') 6 | , debug = require('debug')(`${pkg.name}:boot`) 7 | 8 | // This will load a secrets file from 9 | // 10 | // ~/.your_app_name.env.js 11 | // or ~/.your_app_name.env.json 12 | // 13 | // and add it to the environment. 14 | // Note that this needs to be in your home directory, not the project's root directory 15 | const env = process.env 16 | , secretsFile = resolve(require('homedir')(), `.${pkg.name}.env`) 17 | 18 | try { 19 | Object.assign(env, require(secretsFile)) 20 | } catch (error) { 21 | debug('%s: %s', secretsFile, error.message) 22 | debug('%s: env file not found or invalid, moving on', secretsFile) 23 | } 24 | 25 | module.exports = { 26 | get name() { return pkg.name }, 27 | get isTesting() { return !!global.it }, 28 | get isProduction() { 29 | return env.NODE_ENV === 'production' 30 | }, 31 | get isDevelopment() { 32 | return env.NODE_ENV === 'development' 33 | }, 34 | get baseUrl() { 35 | return env.BASE_URL || `http://localhost:${module.exports.port}` 36 | }, 37 | get port() { 38 | return env.PORT || 1337 39 | }, 40 | get root() { 41 | return __dirname 42 | }, 43 | package: pkg, 44 | env, 45 | } 46 | -------------------------------------------------------------------------------- /node_modules/APP: -------------------------------------------------------------------------------- 1 | .. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "align", 3 | "version": "0.0.1", 4 | "description": "A happy little burning skeleton.", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">= 7.0.0" 8 | }, 9 | "scripts": { 10 | "dev": "node dev", 11 | "validate": "check-node-version --node '>= 7.0.0'", 12 | "setup": "./bin/setup", 13 | "prep": "npm run validate && npm run setup", 14 | "postinstall": "npm run prep", 15 | "build": "webpack", 16 | "build-watch": "npm run build -- -w", 17 | "build-dev": "cross-env NODE_ENV=development npm run build-watch", 18 | "build-branch": "bin/build-branch.sh", 19 | "start": "firebase serve", 20 | "test": "mocha --compilers js:babel-register --watch-extensions js,jsx app/**/*.test.js app/**/*.test.jsx server/**/*.test.js fire/**/*.test.js", 21 | "test-watch": "npm run test -- --watch --reporter=min", 22 | "seed": "node db/seed.js", 23 | "lint": "esw . --ignore-path .gitignore --ext '.js,.jsx'", 24 | "lint-watch": "npm run lint -- -w" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/queerviolet/bones.git" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "redux", 33 | "skeleton" 34 | ], 35 | "author": "Ashi Krishnan ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/queerviolet/bones/issues" 39 | }, 40 | "homepage": "https://github.com/queerviolet/bones#readme", 41 | "dependencies": { 42 | "axios": "^0.15.2", 43 | "babel-preset-stage-2": "^6.18.0", 44 | "bcryptjs": "^2.4.0", 45 | "body-parser": "^1.15.2", 46 | "chai-enzyme": "^0.5.2", 47 | "chalk": "^1.1.3", 48 | "check-node-version": "^1.1.2", 49 | "concurrently": "^3.1.0", 50 | "cookie-session": "^2.0.0-alpha.1", 51 | "enzyme": "^2.5.1", 52 | "express": "^4.14.0", 53 | "finalhandler": "^1.0.0", 54 | "firebase": "^3.9.0", 55 | "homedir": "^0.6.0", 56 | "immutable": "^3.8.1", 57 | "jquery": "^3.2.1", 58 | "material-ui": "^0.18.3", 59 | "passport": "^0.3.2", 60 | "passport-facebook": "^2.1.1", 61 | "passport-github2": "^0.1.10", 62 | "passport-google-oauth": "^1.0.0", 63 | "passport-local": "^1.0.0", 64 | "pg": "^6.1.0", 65 | "pretty-error": "^2.0.2", 66 | "prop-types": "^15.5.10", 67 | "react": "^15.3.2", 68 | "react-bootstrap": "^0.31.0", 69 | "react-color": "^2.13.0", 70 | "react-dom": "^15.3.2", 71 | "react-firebase-file-uploader": "^2.2.1", 72 | "react-flexbox-grid": "^1.1.3", 73 | "react-quill": "^1.0.0", 74 | "react-redux": "^4.4.5", 75 | "react-router": "^3.0.0", 76 | "react-swipeable-views": "^0.12.3", 77 | "react-tap-event-plugin": "^2.0.1", 78 | "redux": "^3.6.0", 79 | "redux-devtools-extension": "^2.13.0", 80 | "redux-logger": "^2.7.0", 81 | "redux-thunk": "^2.1.0", 82 | "sequelize": "^3.24.6", 83 | "sinon": "^1.17.6", 84 | "sinon-chai": "^2.8.0", 85 | "uuid": "^3.1.0", 86 | "victory": "^0.21.0" 87 | }, 88 | "devDependencies": { 89 | "babel": "^6.5.2", 90 | "babel-core": "^6.18.0", 91 | "babel-eslint": "^7.2.2", 92 | "babel-loader": "^6.2.7", 93 | "babel-preset-es2015": "^6.18.0", 94 | "babel-preset-react": "^6.16.0", 95 | "chai": "^3.5.0", 96 | "cross-env": "^3.1.4", 97 | "dateformat": "^2.0.0", 98 | "eslint": "^3.19.0", 99 | "eslint-config-standard": "^10.2.1", 100 | "eslint-plugin-import": "^2.2.0", 101 | "eslint-plugin-node": "^4.2.2", 102 | "eslint-plugin-promise": "^3.5.0", 103 | "eslint-plugin-react": "^6.10.3", 104 | "eslint-plugin-standard": "^3.0.1", 105 | "eslint-watch": "^3.1.0", 106 | "mocha": "^3.1.2", 107 | "nodemon": "^1.11.0", 108 | "supertest": "^3.0.0", 109 | "volleyball": "^1.4.1", 110 | "webpack": "^2.2.1", 111 | "webpack-livereload-plugin": "^0.10.0" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /public/default-placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/default-placeholder.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | a l i g n 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/lines.png -------------------------------------------------------------------------------- /public/lines2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/lines2.png -------------------------------------------------------------------------------- /public/lines3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/lines3.png -------------------------------------------------------------------------------- /public/logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/logo-large.png -------------------------------------------------------------------------------- /public/logo-white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/logo-white.jpg -------------------------------------------------------------------------------- /public/not-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/not-favicon.ico -------------------------------------------------------------------------------- /public/old-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/align-capstone/align/10e302541a2b89b4811adb9191e1c4e16c63ce32/public/old-logo.jpg -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* background: #34223e; /* fallback for old browsers 2 | background: -webkit-linear-gradient(#34223e, #0f142b); 3 | background: -moz-linear-gradient(#34223e, #0f142b); 4 | background: -o-linear-gradient(#34223e, #0f142b); 5 | background: linear-gradient(#34223e, #0f142b); W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 6 | 7 | html, body { 8 | width: 100%; 9 | height: 100vh; 10 | margin: 0; 11 | background: rgb(153,153,153); 12 | background: -moz-linear-gradient(top, rgba(30,87,153,1) 0%, rgba(255,255,255,1) 0%, rgba(255,249,249,1) 5%, rgba(238,238,238,1) 100%, rgba(195,214,229,1) 100%) fixed; 13 | background: -webkit-linear-gradient(top, rgba(30,87,153,1) 0%,rgba(255,255,255,1) 0%,rgba(255,249,249,1) 5%,rgba(238,238,238,1) 100%,rgba(195,214,229,1) 100%) fixed; 14 | background: linear-gradient(to bottom, rgba(30,87,153,1) 0%,rgba(255,255,255,1) 0%,rgba(255,249,249,1) 5%,rgba(238,238,238,1) 100%,rgba(195,214,229,1) 100%) fixed; 15 | } 16 | 17 | body { 18 | font-family: 'Roboto', sans-serif; 19 | } 20 | 21 | h1, h2, h3, h4 { 22 | font-family: 'Roboto', sans-serif; 23 | color: #888; 24 | } 25 | 26 | #nav img { 27 | opacity: 0.6; 28 | } 29 | 30 | #nav img:hover { 31 | cursor: pointer; 32 | } 33 | 34 | .landing { 35 | margin-top: 65px; 36 | } 37 | 38 | #loader { 39 | position: absolute !important; 40 | top: 40% !important; 41 | left: 50% !important; 42 | } 43 | 44 | .flexy-columns { 45 | display: flex; 46 | flex-wrap: wrap; 47 | 48 | columns: 2; 49 | } 50 | 51 | /*come back to the following two classes. if too many 52 | images or resources uploaded, they spill out of their containers.*/ 53 | .resource-container { 54 | display: flex; 55 | flex-flow: column wrap; 56 | height: 1000px; 57 | float: left; 58 | } 59 | 60 | .uploads-container { 61 | display: flex; 62 | flex-flow: column wrap; 63 | height: 890px; 64 | } 65 | 66 | #mockup-container { 67 | padding: 30px; 68 | } 69 | 70 | #mockup-container .container-fluid { 71 | background-color: #fff; 72 | padding: 0 0 30px 0; 73 | box-shadow: 0 0 1em #888; 74 | border-radius: 6px; 75 | height: 65vh; 76 | overflow: hidden; 77 | 78 | } 79 | 80 | #faux-modal-body { 81 | overflow: scroll; 82 | overflow-x: hidden; 83 | height: 90%; 84 | } 85 | 86 | #faux-modal-body .row { 87 | padding-left: 15px; 88 | padding-right: 15px; 89 | } 90 | 91 | .faux-header { 92 | border-bottom: 1px dashed #ccc !important; 93 | padding-left: 15px; 94 | padding-right: 15px; 95 | } 96 | 97 | #close-icon { 98 | float: right; 99 | margin-right: 10px; 100 | } 101 | 102 | #close-icon:hover { 103 | cursor: pointer; 104 | } 105 | 106 | #close-icon svg { 107 | fill: #888 !important; 108 | } 109 | 110 | .container-fluid { 111 | margin: 0 30px 30px; 112 | } 113 | 114 | .chart1 { 115 | height: 80vh; 116 | } 117 | 118 | .chart2 { 119 | height: 20vh; 120 | } 121 | 122 | .timeline-container { 123 | height: 100vh; 124 | position:fixed; 125 | top:0px; 126 | bottom:0px; 127 | left:0px; 128 | right:0px; 129 | } 130 | 131 | .signupform, .login, .oauth { 132 | text-align: center; 133 | } 134 | 135 | .form-group { 136 | margin-bottom: 15px; 137 | width: 100%; 138 | margin: auto; 139 | } 140 | 141 | #empty-message { 142 | color: #BDBDBD; 143 | width: 50%; /* width and margin horizontally center it */ 144 | margin: auto; 145 | position: relative; /* relative position & transform vertically center it */ 146 | top: 60%; 147 | transform: translateY(-50%); 148 | text-align: center; 149 | } 150 | 151 | #empty-message h4 { 152 | font-weight: 300; 153 | } 154 | 155 | .row { 156 | margin-bottom: 20px; 157 | padding-bottom: 30px; 158 | border-bottom: 1px dashed #ccc; 159 | } 160 | 161 | .row:last-child { 162 | margin-bottom: 0px; 163 | padding-bottom: 0px; 164 | border-bottom: 0px dashed #ccc; 165 | } 166 | 167 | .upload-card, .resource-card { 168 | margin: 10px; 169 | display: inline-block; 170 | vertical-align: top; 171 | } 172 | 173 | .upload-card hr, .resource-card hr { 174 | width: 85% !important; 175 | } 176 | 177 | .upload-container label { 178 | margin-top: 22px; 179 | font-weight: 700; 180 | font-family: Roboto, sans-serif; 181 | font-size: 16px; 182 | color: rgba(0, 0, 0, 0.3); 183 | } 184 | 185 | .upload-container input[type="file"] { 186 | width: 0.1px; 187 | height: 0.1px; 188 | opacity: 0; 189 | overflow: hidden; 190 | position: absolute; 191 | z-index: -1; 192 | } 193 | 194 | #bottom-buttons div { 195 | display: inline-block; 196 | } 197 | 198 | #button-container { 199 | margin-right: 15px; 200 | } 201 | 202 | /*! 203 | * Quill Editor v1.2.6 204 | * https://quilljs.com/ 205 | * Copyright (c) 2014, Jason Chen 206 | * Copyright (c) 2013, salesforce.com 207 | */ 208 | 209 | .ql-editor { 210 | background-color: #fff; 211 | } 212 | 213 | .ql-container { 214 | box-sizing: border-box; 215 | font-family: Helvetica, Arial, sans-serif; 216 | font-size: 13px; 217 | height: 100%; 218 | margin: 0px; 219 | position: relative; 220 | } 221 | .ql-container.ql-disabled .ql-tooltip { 222 | visibility: hidden; 223 | } 224 | .ql-container.ql-disabled .ql-editor ul[data-checked] > li::before { 225 | pointer-events: none; 226 | } 227 | .ql-clipboard { 228 | left: -100000px; 229 | height: 1px; 230 | overflow-y: hidden; 231 | position: absolute; 232 | top: 50%; 233 | } 234 | .ql-clipboard p { 235 | margin: 0; 236 | padding: 0; 237 | } 238 | .ql-editor { 239 | box-sizing: border-box; 240 | cursor: text; 241 | line-height: 1.42; 242 | height: 100%; 243 | outline: none; 244 | overflow-y: auto; 245 | padding: 12px 15px; 246 | tab-size: 4; 247 | -moz-tab-size: 4; 248 | text-align: left; 249 | white-space: pre-wrap; 250 | word-wrap: break-word; 251 | } 252 | .ql-editor p, 253 | .ql-editor ol, 254 | .ql-editor ul, 255 | .ql-editor pre, 256 | .ql-editor blockquote, 257 | .ql-editor h1, 258 | .ql-editor h2, 259 | .ql-editor h3, 260 | .ql-editor h4, 261 | .ql-editor h5, 262 | .ql-editor h6 { 263 | margin: 0; 264 | padding: 0; 265 | counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; 266 | } 267 | .ql-editor ol, 268 | .ql-editor ul { 269 | padding-left: 1.5em; 270 | } 271 | .ql-editor ol > li, 272 | .ql-editor ul > li { 273 | list-style-type: none; 274 | } 275 | .ql-editor ul > li::before { 276 | content: '\2022'; 277 | } 278 | .ql-editor ul[data-checked=true], 279 | .ql-editor ul[data-checked=false] { 280 | pointer-events: none; 281 | } 282 | .ql-editor ul[data-checked=true] > li *, 283 | .ql-editor ul[data-checked=false] > li * { 284 | pointer-events: all; 285 | } 286 | .ql-editor ul[data-checked=true] > li::before, 287 | .ql-editor ul[data-checked=false] > li::before { 288 | color: #777; 289 | cursor: pointer; 290 | pointer-events: all; 291 | } 292 | .ql-editor ul[data-checked=true] > li::before { 293 | content: '\2611'; 294 | } 295 | .ql-editor ul[data-checked=false] > li::before { 296 | content: '\2610'; 297 | } 298 | .ql-editor li::before { 299 | display: inline-block; 300 | white-space: nowrap; 301 | width: 1.2em; 302 | text-align: right; 303 | margin-right: 0.3em; 304 | margin-left: -1.5em; 305 | } 306 | .ql-editor li.ql-direction-rtl::before { 307 | text-align: left; 308 | margin-left: 0.3em; 309 | } 310 | .ql-editor ol li, 311 | .ql-editor ul li { 312 | padding-left: 1.5em; 313 | } 314 | .ql-editor ol li { 315 | counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; 316 | counter-increment: list-num; 317 | } 318 | .ql-editor ol li:before { 319 | content: counter(list-num, decimal) '. '; 320 | } 321 | .ql-editor ol li.ql-indent-1 { 322 | counter-increment: list-1; 323 | } 324 | .ql-editor ol li.ql-indent-1:before { 325 | content: counter(list-1, lower-alpha) '. '; 326 | } 327 | .ql-editor ol li.ql-indent-1 { 328 | counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; 329 | } 330 | .ql-editor ol li.ql-indent-2 { 331 | counter-increment: list-2; 332 | } 333 | .ql-editor ol li.ql-indent-2:before { 334 | content: counter(list-2, lower-roman) '. '; 335 | } 336 | .ql-editor ol li.ql-indent-2 { 337 | counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9; 338 | } 339 | .ql-editor ol li.ql-indent-3 { 340 | counter-increment: list-3; 341 | } 342 | .ql-editor ol li.ql-indent-3:before { 343 | content: counter(list-3, decimal) '. '; 344 | } 345 | .ql-editor ol li.ql-indent-3 { 346 | counter-reset: list-4 list-5 list-6 list-7 list-8 list-9; 347 | } 348 | .ql-editor ol li.ql-indent-4 { 349 | counter-increment: list-4; 350 | } 351 | .ql-editor ol li.ql-indent-4:before { 352 | content: counter(list-4, lower-alpha) '. '; 353 | } 354 | .ql-editor ol li.ql-indent-4 { 355 | counter-reset: list-5 list-6 list-7 list-8 list-9; 356 | } 357 | .ql-editor ol li.ql-indent-5 { 358 | counter-increment: list-5; 359 | } 360 | .ql-editor ol li.ql-indent-5:before { 361 | content: counter(list-5, lower-roman) '. '; 362 | } 363 | .ql-editor ol li.ql-indent-5 { 364 | counter-reset: list-6 list-7 list-8 list-9; 365 | } 366 | .ql-editor ol li.ql-indent-6 { 367 | counter-increment: list-6; 368 | } 369 | .ql-editor ol li.ql-indent-6:before { 370 | content: counter(list-6, decimal) '. '; 371 | } 372 | .ql-editor ol li.ql-indent-6 { 373 | counter-reset: list-7 list-8 list-9; 374 | } 375 | .ql-editor ol li.ql-indent-7 { 376 | counter-increment: list-7; 377 | } 378 | .ql-editor ol li.ql-indent-7:before { 379 | content: counter(list-7, lower-alpha) '. '; 380 | } 381 | .ql-editor ol li.ql-indent-7 { 382 | counter-reset: list-8 list-9; 383 | } 384 | .ql-editor ol li.ql-indent-8 { 385 | counter-increment: list-8; 386 | } 387 | .ql-editor ol li.ql-indent-8:before { 388 | content: counter(list-8, lower-roman) '. '; 389 | } 390 | .ql-editor ol li.ql-indent-8 { 391 | counter-reset: list-9; 392 | } 393 | .ql-editor ol li.ql-indent-9 { 394 | counter-increment: list-9; 395 | } 396 | .ql-editor ol li.ql-indent-9:before { 397 | content: counter(list-9, decimal) '. '; 398 | } 399 | .ql-editor .ql-indent-1:not(.ql-direction-rtl) { 400 | padding-left: 3em; 401 | } 402 | .ql-editor li.ql-indent-1:not(.ql-direction-rtl) { 403 | padding-left: 4.5em; 404 | } 405 | .ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right { 406 | padding-right: 3em; 407 | } 408 | .ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right { 409 | padding-right: 4.5em; 410 | } 411 | .ql-editor .ql-indent-2:not(.ql-direction-rtl) { 412 | padding-left: 6em; 413 | } 414 | .ql-editor li.ql-indent-2:not(.ql-direction-rtl) { 415 | padding-left: 7.5em; 416 | } 417 | .ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right { 418 | padding-right: 6em; 419 | } 420 | .ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right { 421 | padding-right: 7.5em; 422 | } 423 | .ql-editor .ql-indent-3:not(.ql-direction-rtl) { 424 | padding-left: 9em; 425 | } 426 | .ql-editor li.ql-indent-3:not(.ql-direction-rtl) { 427 | padding-left: 10.5em; 428 | } 429 | .ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right { 430 | padding-right: 9em; 431 | } 432 | .ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right { 433 | padding-right: 10.5em; 434 | } 435 | .ql-editor .ql-indent-4:not(.ql-direction-rtl) { 436 | padding-left: 12em; 437 | } 438 | .ql-editor li.ql-indent-4:not(.ql-direction-rtl) { 439 | padding-left: 13.5em; 440 | } 441 | .ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right { 442 | padding-right: 12em; 443 | } 444 | .ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right { 445 | padding-right: 13.5em; 446 | } 447 | .ql-editor .ql-indent-5:not(.ql-direction-rtl) { 448 | padding-left: 15em; 449 | } 450 | .ql-editor li.ql-indent-5:not(.ql-direction-rtl) { 451 | padding-left: 16.5em; 452 | } 453 | .ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right { 454 | padding-right: 15em; 455 | } 456 | .ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right { 457 | padding-right: 16.5em; 458 | } 459 | .ql-editor .ql-indent-6:not(.ql-direction-rtl) { 460 | padding-left: 18em; 461 | } 462 | .ql-editor li.ql-indent-6:not(.ql-direction-rtl) { 463 | padding-left: 19.5em; 464 | } 465 | .ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right { 466 | padding-right: 18em; 467 | } 468 | .ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right { 469 | padding-right: 19.5em; 470 | } 471 | .ql-editor .ql-indent-7:not(.ql-direction-rtl) { 472 | padding-left: 21em; 473 | } 474 | .ql-editor li.ql-indent-7:not(.ql-direction-rtl) { 475 | padding-left: 22.5em; 476 | } 477 | .ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right { 478 | padding-right: 21em; 479 | } 480 | .ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right { 481 | padding-right: 22.5em; 482 | } 483 | .ql-editor .ql-indent-8:not(.ql-direction-rtl) { 484 | padding-left: 24em; 485 | } 486 | .ql-editor li.ql-indent-8:not(.ql-direction-rtl) { 487 | padding-left: 25.5em; 488 | } 489 | .ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right { 490 | padding-right: 24em; 491 | } 492 | .ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right { 493 | padding-right: 25.5em; 494 | } 495 | .ql-editor .ql-indent-9:not(.ql-direction-rtl) { 496 | padding-left: 27em; 497 | } 498 | .ql-editor li.ql-indent-9:not(.ql-direction-rtl) { 499 | padding-left: 28.5em; 500 | } 501 | .ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right { 502 | padding-right: 27em; 503 | } 504 | .ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right { 505 | padding-right: 28.5em; 506 | } 507 | .ql-editor .ql-video { 508 | display: block; 509 | max-width: 100%; 510 | } 511 | .ql-editor .ql-video.ql-align-center { 512 | margin: 0 auto; 513 | } 514 | .ql-editor .ql-video.ql-align-right { 515 | margin: 0 0 0 auto; 516 | } 517 | .ql-editor .ql-bg-black { 518 | background-color: #000; 519 | } 520 | .ql-editor .ql-bg-red { 521 | background-color: #e60000; 522 | } 523 | .ql-editor .ql-bg-orange { 524 | background-color: #f90; 525 | } 526 | .ql-editor .ql-bg-yellow { 527 | background-color: #ff0; 528 | } 529 | .ql-editor .ql-bg-green { 530 | background-color: #008a00; 531 | } 532 | .ql-editor .ql-bg-blue { 533 | background-color: #06c; 534 | } 535 | .ql-editor .ql-bg-purple { 536 | background-color: #93f; 537 | } 538 | .ql-editor .ql-color-white { 539 | color: #fff; 540 | } 541 | .ql-editor .ql-color-red { 542 | color: #e60000; 543 | } 544 | .ql-editor .ql-color-orange { 545 | color: #f90; 546 | } 547 | .ql-editor .ql-color-yellow { 548 | color: #ff0; 549 | } 550 | .ql-editor .ql-color-green { 551 | color: #008a00; 552 | } 553 | .ql-editor .ql-color-blue { 554 | color: #06c; 555 | } 556 | .ql-editor .ql-color-purple { 557 | color: #93f; 558 | } 559 | .ql-editor .ql-font-serif { 560 | font-family: Georgia, Times New Roman, serif; 561 | } 562 | .ql-editor .ql-font-monospace { 563 | font-family: Monaco, Courier New, monospace; 564 | } 565 | .ql-editor .ql-size-small { 566 | font-size: 0.75em; 567 | } 568 | .ql-editor .ql-size-large { 569 | font-size: 1.5em; 570 | } 571 | .ql-editor .ql-size-huge { 572 | font-size: 2.5em; 573 | } 574 | .ql-editor .ql-direction-rtl { 575 | direction: rtl; 576 | text-align: inherit; 577 | } 578 | .ql-editor .ql-align-center { 579 | text-align: center; 580 | } 581 | .ql-editor .ql-align-justify { 582 | text-align: justify; 583 | } 584 | .ql-editor .ql-align-right { 585 | text-align: right; 586 | } 587 | .ql-editor.ql-blank::before { 588 | color: rgba(0,0,0,0.6); 589 | content: attr(data-placeholder); 590 | font-style: italic; 591 | pointer-events: none; 592 | position: absolute; 593 | } 594 | .ql-snow.ql-toolbar:after, 595 | .ql-snow .ql-toolbar:after { 596 | clear: both; 597 | content: ''; 598 | display: table; 599 | } 600 | .ql-snow.ql-toolbar button, 601 | .ql-snow .ql-toolbar button { 602 | background: none; 603 | border: none; 604 | cursor: pointer; 605 | display: inline-block; 606 | float: left; 607 | height: 24px; 608 | padding: 3px 5px; 609 | width: 28px; 610 | } 611 | .ql-snow.ql-toolbar button svg, 612 | .ql-snow .ql-toolbar button svg { 613 | float: left; 614 | height: 100%; 615 | } 616 | .ql-snow.ql-toolbar button:active:hover, 617 | .ql-snow .ql-toolbar button:active:hover { 618 | outline: none; 619 | } 620 | .ql-snow.ql-toolbar input.ql-image[type=file], 621 | .ql-snow .ql-toolbar input.ql-image[type=file] { 622 | display: none; 623 | } 624 | .ql-snow.ql-toolbar button:hover, 625 | .ql-snow .ql-toolbar button:hover, 626 | .ql-snow.ql-toolbar button.ql-active, 627 | .ql-snow .ql-toolbar button.ql-active, 628 | .ql-snow.ql-toolbar .ql-picker-label:hover, 629 | .ql-snow .ql-toolbar .ql-picker-label:hover, 630 | .ql-snow.ql-toolbar .ql-picker-label.ql-active, 631 | .ql-snow .ql-toolbar .ql-picker-label.ql-active, 632 | .ql-snow.ql-toolbar .ql-picker-item:hover, 633 | .ql-snow .ql-toolbar .ql-picker-item:hover, 634 | .ql-snow.ql-toolbar .ql-picker-item.ql-selected, 635 | .ql-snow .ql-toolbar .ql-picker-item.ql-selected { 636 | color: #06c; 637 | } 638 | .ql-snow.ql-toolbar button:hover .ql-fill, 639 | .ql-snow .ql-toolbar button:hover .ql-fill, 640 | .ql-snow.ql-toolbar button.ql-active .ql-fill, 641 | .ql-snow .ql-toolbar button.ql-active .ql-fill, 642 | .ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill, 643 | .ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill, 644 | .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill, 645 | .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill, 646 | .ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill, 647 | .ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill, 648 | .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill, 649 | .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill, 650 | .ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill, 651 | .ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill, 652 | .ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill, 653 | .ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill, 654 | .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, 655 | .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, 656 | .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, 657 | .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, 658 | .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, 659 | .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, 660 | .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, 661 | .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill { 662 | fill: #06c; 663 | } 664 | .ql-snow.ql-toolbar button:hover .ql-stroke, 665 | .ql-snow .ql-toolbar button:hover .ql-stroke, 666 | .ql-snow.ql-toolbar button.ql-active .ql-stroke, 667 | .ql-snow .ql-toolbar button.ql-active .ql-stroke, 668 | .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke, 669 | .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke, 670 | .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke, 671 | .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke, 672 | .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke, 673 | .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke, 674 | .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, 675 | .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, 676 | .ql-snow.ql-toolbar button:hover .ql-stroke-miter, 677 | .ql-snow .ql-toolbar button:hover .ql-stroke-miter, 678 | .ql-snow.ql-toolbar button.ql-active .ql-stroke-miter, 679 | .ql-snow .ql-toolbar button.ql-active .ql-stroke-miter, 680 | .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, 681 | .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, 682 | .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, 683 | .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, 684 | .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, 685 | .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, 686 | .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, 687 | .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter { 688 | stroke: #06c; 689 | } 690 | @media (pointer: coarse) { 691 | .ql-snow.ql-toolbar button:hover:not(.ql-active), 692 | .ql-snow .ql-toolbar button:hover:not(.ql-active) { 693 | color: #444; 694 | } 695 | .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill, 696 | .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill, 697 | .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, 698 | .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill { 699 | fill: #444; 700 | } 701 | .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke, 702 | .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke, 703 | .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, 704 | .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter { 705 | stroke: #444; 706 | } 707 | } 708 | .ql-snow { 709 | box-sizing: border-box; 710 | } 711 | .ql-snow * { 712 | box-sizing: border-box; 713 | } 714 | .ql-snow .ql-hidden { 715 | display: none; 716 | } 717 | .ql-snow .ql-out-bottom, 718 | .ql-snow .ql-out-top { 719 | visibility: hidden; 720 | } 721 | .ql-snow .ql-tooltip { 722 | position: absolute; 723 | transform: translateY(10px); 724 | } 725 | .ql-snow .ql-tooltip a { 726 | cursor: pointer; 727 | text-decoration: none; 728 | } 729 | .ql-snow .ql-tooltip.ql-flip { 730 | transform: translateY(-10px); 731 | } 732 | .ql-snow .ql-formats { 733 | display: inline-block; 734 | vertical-align: middle; 735 | } 736 | .ql-snow .ql-formats:after { 737 | clear: both; 738 | content: ''; 739 | display: table; 740 | } 741 | .ql-snow .ql-stroke { 742 | fill: none; 743 | stroke: #444; 744 | stroke-linecap: round; 745 | stroke-linejoin: round; 746 | stroke-width: 2; 747 | } 748 | .ql-snow .ql-stroke-miter { 749 | fill: none; 750 | stroke: #444; 751 | stroke-miterlimit: 10; 752 | stroke-width: 2; 753 | } 754 | .ql-snow .ql-fill, 755 | .ql-snow .ql-stroke.ql-fill { 756 | fill: #444; 757 | } 758 | .ql-snow .ql-empty { 759 | fill: none; 760 | } 761 | .ql-snow .ql-even { 762 | fill-rule: evenodd; 763 | } 764 | .ql-snow .ql-thin, 765 | .ql-snow .ql-stroke.ql-thin { 766 | stroke-width: 1; 767 | } 768 | .ql-snow .ql-transparent { 769 | opacity: 0.4; 770 | } 771 | .ql-snow .ql-direction svg:last-child { 772 | display: none; 773 | } 774 | .ql-snow .ql-direction.ql-active svg:last-child { 775 | display: inline; 776 | } 777 | .ql-snow .ql-direction.ql-active svg:first-child { 778 | display: none; 779 | } 780 | .ql-snow .ql-editor h1 { 781 | font-size: 2em; 782 | } 783 | .ql-snow .ql-editor h2 { 784 | font-size: 1.5em; 785 | } 786 | .ql-snow .ql-editor h3 { 787 | font-size: 1.17em; 788 | } 789 | .ql-snow .ql-editor h4 { 790 | font-size: 1em; 791 | } 792 | .ql-snow .ql-editor h5 { 793 | font-size: 0.83em; 794 | } 795 | .ql-snow .ql-editor h6 { 796 | font-size: 0.67em; 797 | } 798 | .ql-snow .ql-editor a { 799 | text-decoration: underline; 800 | } 801 | .ql-snow .ql-editor blockquote { 802 | border-left: 4px solid #ccc; 803 | margin-bottom: 5px; 804 | margin-top: 5px; 805 | padding-left: 16px; 806 | } 807 | .ql-snow .ql-editor code, 808 | .ql-snow .ql-editor pre { 809 | background-color: #f0f0f0; 810 | border-radius: 3px; 811 | } 812 | .ql-snow .ql-editor pre { 813 | white-space: pre-wrap; 814 | margin-bottom: 5px; 815 | margin-top: 5px; 816 | padding: 5px 10px; 817 | } 818 | .ql-snow .ql-editor code { 819 | font-size: 85%; 820 | padding-bottom: 2px; 821 | padding-top: 2px; 822 | } 823 | .ql-snow .ql-editor code:before, 824 | .ql-snow .ql-editor code:after { 825 | content: "\A0"; 826 | letter-spacing: -2px; 827 | } 828 | .ql-snow .ql-editor pre.ql-syntax { 829 | background-color: #23241f; 830 | color: #f8f8f2; 831 | overflow: visible; 832 | } 833 | .ql-snow .ql-editor img { 834 | max-width: 100%; 835 | } 836 | .ql-snow .ql-picker { 837 | color: #444; 838 | display: inline-block; 839 | float: left; 840 | font-size: 14px; 841 | font-weight: 500; 842 | height: 24px; 843 | position: relative; 844 | vertical-align: middle; 845 | } 846 | .ql-snow .ql-picker-label { 847 | cursor: pointer; 848 | display: inline-block; 849 | height: 100%; 850 | padding-left: 8px; 851 | padding-right: 2px; 852 | position: relative; 853 | width: 100%; 854 | } 855 | .ql-snow .ql-picker-label::before { 856 | display: inline-block; 857 | line-height: 22px; 858 | } 859 | .ql-snow .ql-picker-options { 860 | background-color: #fff; 861 | display: none; 862 | min-width: 100%; 863 | padding: 4px 8px; 864 | position: absolute; 865 | white-space: nowrap; 866 | } 867 | .ql-snow .ql-picker-options .ql-picker-item { 868 | cursor: pointer; 869 | display: block; 870 | padding-bottom: 5px; 871 | padding-top: 5px; 872 | } 873 | .ql-snow .ql-picker.ql-expanded .ql-picker-label { 874 | color: #ccc; 875 | z-index: 2; 876 | } 877 | .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill { 878 | fill: #ccc; 879 | } 880 | .ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke { 881 | stroke: #ccc; 882 | } 883 | .ql-snow .ql-picker.ql-expanded .ql-picker-options { 884 | display: block; 885 | margin-top: -1px; 886 | top: 100%; 887 | z-index: 1; 888 | } 889 | .ql-snow .ql-color-picker, 890 | .ql-snow .ql-icon-picker { 891 | width: 28px; 892 | } 893 | .ql-snow .ql-color-picker .ql-picker-label, 894 | .ql-snow .ql-icon-picker .ql-picker-label { 895 | padding: 2px 4px; 896 | } 897 | .ql-snow .ql-color-picker .ql-picker-label svg, 898 | .ql-snow .ql-icon-picker .ql-picker-label svg { 899 | right: 4px; 900 | } 901 | .ql-snow .ql-icon-picker .ql-picker-options { 902 | padding: 4px 0px; 903 | } 904 | .ql-snow .ql-icon-picker .ql-picker-item { 905 | height: 24px; 906 | width: 24px; 907 | padding: 2px 4px; 908 | } 909 | .ql-snow .ql-color-picker .ql-picker-options { 910 | padding: 3px 5px; 911 | width: 152px; 912 | } 913 | .ql-snow .ql-color-picker .ql-picker-item { 914 | border: 1px solid transparent; 915 | float: left; 916 | height: 16px; 917 | margin: 2px; 918 | padding: 0px; 919 | width: 16px; 920 | } 921 | .ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg { 922 | position: absolute; 923 | margin-top: -9px; 924 | right: 0; 925 | top: 50%; 926 | width: 18px; 927 | } 928 | .ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before, 929 | .ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before, 930 | .ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before, 931 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before, 932 | .ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before, 933 | .ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before { 934 | content: attr(data-label); 935 | } 936 | .ql-snow .ql-picker.ql-header { 937 | width: 98px; 938 | } 939 | .ql-snow .ql-picker.ql-header .ql-picker-label::before, 940 | .ql-snow .ql-picker.ql-header .ql-picker-item::before { 941 | content: 'Normal'; 942 | } 943 | .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, 944 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { 945 | content: 'Heading 1'; 946 | } 947 | .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, 948 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { 949 | content: 'Heading 2'; 950 | } 951 | .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, 952 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { 953 | content: 'Heading 3'; 954 | } 955 | .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, 956 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { 957 | content: 'Heading 4'; 958 | } 959 | .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, 960 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { 961 | content: 'Heading 5'; 962 | } 963 | .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, 964 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { 965 | content: 'Heading 6'; 966 | } 967 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { 968 | font-size: 2em; 969 | } 970 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { 971 | font-size: 1.5em; 972 | } 973 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { 974 | font-size: 1.17em; 975 | } 976 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { 977 | font-size: 1em; 978 | } 979 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { 980 | font-size: 0.83em; 981 | } 982 | .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { 983 | font-size: 0.67em; 984 | } 985 | .ql-snow .ql-picker.ql-font { 986 | width: 108px; 987 | } 988 | .ql-snow .ql-picker.ql-font .ql-picker-label::before, 989 | .ql-snow .ql-picker.ql-font .ql-picker-item::before { 990 | content: 'Sans Serif'; 991 | } 992 | .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before, 993 | .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before { 994 | content: 'Serif'; 995 | } 996 | .ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before, 997 | .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before { 998 | content: 'Monospace'; 999 | } 1000 | .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before { 1001 | font-family: Georgia, Times New Roman, serif; 1002 | } 1003 | .ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before { 1004 | font-family: Monaco, Courier New, monospace; 1005 | } 1006 | .ql-snow .ql-picker.ql-size { 1007 | width: 98px; 1008 | } 1009 | .ql-snow .ql-picker.ql-size .ql-picker-label::before, 1010 | .ql-snow .ql-picker.ql-size .ql-picker-item::before { 1011 | content: 'Normal'; 1012 | } 1013 | .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before, 1014 | .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before { 1015 | content: 'Small'; 1016 | } 1017 | .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before, 1018 | .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before { 1019 | content: 'Large'; 1020 | } 1021 | .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before, 1022 | .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before { 1023 | content: 'Huge'; 1024 | } 1025 | .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before { 1026 | font-size: 10px; 1027 | } 1028 | .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before { 1029 | font-size: 18px; 1030 | } 1031 | .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before { 1032 | font-size: 32px; 1033 | } 1034 | .ql-snow .ql-color-picker.ql-background .ql-picker-item { 1035 | background-color: #fff; 1036 | } 1037 | .ql-snow .ql-color-picker.ql-color .ql-picker-item { 1038 | background-color: #000; 1039 | } 1040 | .ql-toolbar.ql-snow { 1041 | border: 1px solid #ccc; 1042 | box-sizing: border-box; 1043 | font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 1044 | padding: 8px; 1045 | } 1046 | .ql-toolbar.ql-snow .ql-formats { 1047 | margin-right: 15px; 1048 | } 1049 | .ql-toolbar.ql-snow .ql-picker-label { 1050 | border: 1px solid transparent; 1051 | } 1052 | .ql-toolbar.ql-snow .ql-picker-options { 1053 | border: 1px solid transparent; 1054 | box-shadow: rgba(0,0,0,0.2) 0 2px 8px; 1055 | } 1056 | .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label { 1057 | border-color: #ccc; 1058 | } 1059 | .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options { 1060 | border-color: #ccc; 1061 | } 1062 | .ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected, 1063 | .ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover { 1064 | border-color: #000; 1065 | } 1066 | .ql-toolbar.ql-snow + .ql-container.ql-snow { 1067 | border-top: 0px; 1068 | } 1069 | .ql-snow .ql-tooltip { 1070 | background-color: #fff; 1071 | border: 1px solid #ccc; 1072 | box-shadow: 0px 0px 5px #ddd; 1073 | color: #444; 1074 | padding: 5px 12px; 1075 | white-space: nowrap; 1076 | } 1077 | .ql-snow .ql-tooltip::before { 1078 | content: "Visit URL:"; 1079 | line-height: 26px; 1080 | margin-right: 8px; 1081 | } 1082 | .ql-snow .ql-tooltip input[type=text] { 1083 | display: none; 1084 | border: 1px solid #ccc; 1085 | font-size: 13px; 1086 | height: 26px; 1087 | margin: 0px; 1088 | padding: 3px 5px; 1089 | width: 170px; 1090 | } 1091 | .ql-snow .ql-tooltip a.ql-preview { 1092 | display: inline-block; 1093 | max-width: 200px; 1094 | overflow-x: hidden; 1095 | text-overflow: ellipsis; 1096 | vertical-align: top; 1097 | } 1098 | .ql-snow .ql-tooltip a.ql-action::after { 1099 | border-right: 1px solid #ccc; 1100 | content: 'Edit'; 1101 | margin-left: 16px; 1102 | padding-right: 8px; 1103 | } 1104 | .ql-snow .ql-tooltip a.ql-remove::before { 1105 | content: 'Remove'; 1106 | margin-left: 8px; 1107 | } 1108 | .ql-snow .ql-tooltip a { 1109 | line-height: 26px; 1110 | } 1111 | .ql-snow .ql-tooltip.ql-editing a.ql-preview, 1112 | .ql-snow .ql-tooltip.ql-editing a.ql-remove { 1113 | display: none; 1114 | } 1115 | .ql-snow .ql-tooltip.ql-editing input[type=text] { 1116 | display: inline-block; 1117 | } 1118 | .ql-snow .ql-tooltip.ql-editing a.ql-action::after { 1119 | border-right: 0px; 1120 | content: 'Save'; 1121 | padding-right: 0px; 1122 | } 1123 | .ql-snow .ql-tooltip[data-mode=link]::before { 1124 | content: "Enter link:"; 1125 | } 1126 | .ql-snow .ql-tooltip[data-mode=formula]::before { 1127 | content: "Enter formula:"; 1128 | } 1129 | .ql-snow .ql-tooltip[data-mode=video]::before { 1130 | content: "Enter video:"; 1131 | } 1132 | .ql-snow a { 1133 | color: #06c; 1134 | } 1135 | .ql-container.ql-snow { 1136 | border: 1px solid #ccc; 1137 | } 1138 | 1139 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const LiveReloadPlugin = require('webpack-livereload-plugin') 4 | , devMode = require('.').isDevelopment 5 | 6 | /** 7 | * Fast source maps rebuild quickly during development, but only give a link 8 | * to the line where the error occurred. The stack trace will show the bundled 9 | * code, not the original code. Keep this on `false` for slower builds but 10 | * usable stack traces. Set to `true` if you want to speed up development. 11 | */ 12 | 13 | , USE_FAST_SOURCE_MAPS = false 14 | 15 | module.exports = { 16 | entry: './app/main.jsx', 17 | output: { 18 | path: __dirname, 19 | filename: './public/bundle.js' 20 | }, 21 | context: __dirname, 22 | devtool: devMode && USE_FAST_SOURCE_MAPS 23 | ? 'cheap-module-eval-source-map' 24 | : 'source-map', 25 | resolve: { 26 | extensions: ['.js', '.jsx', '.json', '*'] 27 | }, 28 | module: { 29 | rules: [{ 30 | test: /jsx?$/, 31 | exclude: /(node_modules|bower_components)/, 32 | use: [{ 33 | loader: 'babel-loader', 34 | options: { 35 | presets: ['react', 'es2015', 'stage-2'] 36 | } 37 | }] 38 | }] 39 | }, 40 | plugins: devMode 41 | ? [new LiveReloadPlugin({appendScriptTag: true})] 42 | : [] 43 | } 44 | --------------------------------------------------------------------------------