├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------