├── .babelrc ├── .eslintrc.js ├── .firebaserc ├── .gitignore ├── README.md ├── database.rules.json ├── firebase.json ├── package.json └── src ├── app.js ├── constants.js ├── index.html ├── models ├── auth.js ├── feedback.js └── quotes.js ├── styles.css ├── utils └── index.js └── views ├── auth-panel.js ├── feedback-panel.js ├── main.js ├── quote.js └── quotes-list.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "standard", 3 | "plugins": [ 4 | "standard", 5 | "promise" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 6 9 | } 10 | }; -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "choo-firebase-2ec21" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | *.log 4 | .DS_Store 5 | yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) [![built with choo v4](https://img.shields.io/badge/built%20with%20choo-v4-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo) 2 | 3 | # choo-firebase 4 | Example showing how to use firebase with choo. 5 | 6 | Based on David Wallers wonderful [reduxfirebasedemo](https://github.com/krawaller/reduxfirebasedemo). 7 | 8 | Deployed at [choo-firebase-2ec21.firebaseapp.com/](https://choo-firebase-2ec21.firebaseapp.com/) 9 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": true, 4 | ".write": "auth != null" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "dist", 7 | "rewrites": [ 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "choo-firebase", 3 | "version": "0.0.1", 4 | "description": "Example of using firebase with choo.", 5 | "main": "index.js", 6 | "scripts": { 7 | "deploy": "npm run build && firebase deploy", 8 | "lint": "eslint 'src/**/*.js'", 9 | "lint:fix": "eslint 'src/**/*.js' --fix", 10 | "start": "budo src/app.js --dir=dist --live --pushstate --port 8080", 11 | "copy": "mkdir -p dist && cp src/index.html dist/", 12 | "build": "npm run copy && NODE_ENV=production browserify src/app.js -t envify -t sheetify/transform -t babelify -g unassertify -g uglifyify | uglifyjs -o dist/app.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/mw222rs/choo-firebase.git" 17 | }, 18 | "author": "Mattias W", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/mw222rs/choo-firebase/issues" 22 | }, 23 | "homepage": "https://github.com/mw222rs/choo-firebase#readme", 24 | "dependencies": { 25 | "choo": "^4.0.3", 26 | "firebase": "^3.6.5", 27 | "lodash.assign": "^4.2.0", 28 | "lodash.clonedeep": "^4.5.0", 29 | "lodash.map": "^4.6.0" 30 | }, 31 | "devDependencies": { 32 | "babel-preset-es2015": "^6.18.0", 33 | "babelify": "^7.3.0", 34 | "browserify": "^13.3.0", 35 | "budo": "^9.4.5", 36 | "envify": "^4.0.0", 37 | "es2020": "^1.1.9", 38 | "eslint": "^3.13.1", 39 | "eslint-config-standard": "^6.2.1", 40 | "eslint-plugin-promise": "^3.4.0", 41 | "eslint-plugin-standard": "^2.0.1", 42 | "insert-css": "^2.0.0", 43 | "sheetify": "^6.0.1", 44 | "uglify-js": "^2.7.5", 45 | "uglifyify": "^3.0.4", 46 | "unassertify": "^2.0.4", 47 | "yo-yoify": "^3.5.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const choo = require('choo') 2 | const sf = require('sheetify') 3 | const mainView = require('./views/main') 4 | 5 | // Firebase setup and initialization 6 | const firebase = require('firebase/app') 7 | require('firebase/auth') 8 | require('firebase/database') 9 | 10 | firebase.initializeApp({ 11 | apiKey: 'AIzaSyBMVVNLAtPx2jXrpbhU_3dnxpAPhrO6raE', 12 | authDomain: 'choo-firebase-2ec21.firebaseapp.com', 13 | databaseURL: 'https://choo-firebase-2ec21.firebaseio.com' 14 | }) 15 | 16 | // Sheetify 17 | sf('./styles.css', { global: true }) 18 | 19 | const app = choo() 20 | 21 | app.model(require('./models/auth')) 22 | app.model(require('./models/feedback')) 23 | app.model(require('./models/quotes')) 24 | 25 | app.router(['/', mainView]) 26 | 27 | const tree = app.start() 28 | document.body.appendChild(tree) 29 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | LOGGED_IN: 'LOGGED_IN', 3 | ANONYMOUS: 'ANONYMOUS', 4 | AWAITING_AUTH_RESPONSE: 'AWAITING_AUTH_RESPONSE', 5 | EDITING_QUOTE: 'EDITING_QUOTE', 6 | SUBMITTING_QUOTE: 'SUBMITTING_QUOTE' 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | choo firebase example 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/models/auth.js: -------------------------------------------------------------------------------- 1 | const C = require('../constants') 2 | const firebase = require('firebase/app') 3 | 4 | const auth = firebase.auth() 5 | 6 | module.exports = { 7 | namespace: 'auth', 8 | state: { 9 | currently: C.ANONYMOUS, 10 | username: null, 11 | uid: null 12 | }, 13 | reducers: { 14 | attemptingLogin: (state, data) => ({ 15 | currently: C.AWAITING_AUTH_RESPONSE, 16 | username: 'guest', 17 | uid: null 18 | }), 19 | logout: (state, data) => ({ 20 | currently: C.ANONYMOUS, 21 | username: 'guest', 22 | uid: null 23 | }), 24 | login: (state, data) => ({ 25 | currently: C.LOGGED_IN, 26 | username: data.username, 27 | uid: data.uid 28 | }) 29 | }, 30 | effects: { 31 | attemptLogin: (state, data, send, done) => { 32 | send('auth:attemptingLogin', done) 33 | const provider = new firebase.auth.GithubAuthProvider() 34 | auth.signInWithPopup(provider).catch(error => { 35 | send('feedback:displayError', { error }, done) 36 | send('auth:logout', done) 37 | }) 38 | }, 39 | logoutUser: (state, data, send, done) => { 40 | auth.signOut() 41 | send('auth:logout', done) 42 | } 43 | }, 44 | subscriptions: [ 45 | (send, done) => auth.onAuthStateChanged(user => { 46 | if (user) { 47 | send('auth:login', { 48 | username: user.displayName, 49 | uid: user.uid 50 | }, done) 51 | } else { 52 | send('auth:logout', done) 53 | } 54 | }) 55 | ] 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/models/feedback.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | namespace: 'feedback', 3 | state: { 4 | messages: [ 5 | {msg: 'Welcome to this little demo! It is meant to demonstrate three things:', error: false}, 6 | {msg: '1) How to use choo + Firebase', error: false}, 7 | {msg: '2) How to use authentication in a choo app', error: false}, 8 | {msg: '3) How awesome choo is', error: false} 9 | ] 10 | }, 11 | reducers: { 12 | dismiss: (state, data) => ({ 13 | messages: state.messages.filter((msg, i) => data.num !== i) 14 | }), 15 | displayError: (state, data) => ({messages: state.messages.concat({ msg: data.error, error: true })}), 16 | displayMessage: (state, data) => ({messages: state.messages.concat({ msg: data.message, error: false })}) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/models/quotes.js: -------------------------------------------------------------------------------- 1 | const assign = require('lodash.assign') 2 | const cloneDeep = require('lodash.clonedeep') 3 | const C = require('../constants') 4 | const utils = require('../utils') 5 | 6 | const firebase = require('firebase/app') 7 | 8 | const quotesRef = firebase.database().ref().child('quotes') 9 | 10 | module.exports = { 11 | namespace: 'quotes', 12 | state: { 13 | hasReceivedData: false, 14 | submittingNew: false, 15 | states: {}, 16 | data: {} 17 | }, 18 | reducers: { 19 | receiveQuotesData: (state, data) => assign({}, state, { hasReceivedData: true, data: data.data }), 20 | awaitNewQuoteResponse: (state, data) => assign({}, state, { submittingNew: true }), 21 | reveiceNewQuoteResponse: (state, data) => assign({}, state, { submittingNew: false }), 22 | setIsEditing: (state, data) => { 23 | const newState = cloneDeep(state) 24 | newState.states[data.qid] = C.EDITING_QUOTE 25 | return newState 26 | }, 27 | setFinishedEditing: (state, data) => { 28 | const newState = cloneDeep(state) 29 | delete newState.states[data.qid] 30 | return newState 31 | }, 32 | setIsSubmitting: (state, data) => { 33 | const newState = cloneDeep(state) 34 | newState.states[data.qid] = C.SUBMITTING_QUOTE 35 | return newState 36 | } 37 | }, 38 | effects: { 39 | deleteQuote: (state, data, send, done) => { 40 | const qid = data.qid 41 | send('quotes:setIsEditing', { qid }, done) 42 | quotesRef.child(qid).remove(error => { 43 | send('quotes:setFinishedEditing', qid, done) 44 | if (error) { 45 | send('feedback:displayError', { error: 'Deletion failed! ' + error }, done) 46 | } else { 47 | send('feedback:displayMessage', { message: 'Quote successfully deleted!' }, done) 48 | } 49 | }) 50 | }, 51 | submitQuoteEdit: (state, data, send, done) => { 52 | const {username, uid, qid, content} = data 53 | const error = utils.validateQuote(content) 54 | 55 | if (error) { 56 | send('feedback:displayError', { error }, done) 57 | } else { 58 | send('quotes:setIsSubmitting', { qid }, done) 59 | quotesRef.child(qid).set({content, username, uid}) 60 | .then(() => { 61 | send('feedback:displayMessage', { message: 'Update successfully saved!' }, done) 62 | send('quotes:setFinishedEditing', { qid }, done) 63 | }) 64 | .catch(err => { 65 | send('feedback:displayError', { err }) 66 | send('quotes:setFinishedEditing', { qid }, done) 67 | }) 68 | } 69 | }, 70 | submitNewQuote: (state, data, send, done) => { 71 | const {input, uid, username} = data 72 | const content = input.value 73 | const error = utils.validateQuote(content) 74 | 75 | if (error) { 76 | send('feedback:displayError', { error }, done) 77 | } else { 78 | send('quotes:awaitNewQuoteResponse', done) 79 | quotesRef.push({content, username, uid}) 80 | .then(() => { 81 | send('feedback:displayMessage', { message: 'Quote successfully saved!' }, done) 82 | send('quotes:reveiceNewQuoteResponse', done) 83 | input.value = '' 84 | }) 85 | .catch(error => { 86 | send('feedback:displayError', { error }, done) 87 | send('quotes:reveiceNewQuoteResponse', done) 88 | }) 89 | } 90 | } 91 | }, 92 | subscriptions: [ 93 | (send, done) => quotesRef.on('value', snapshot => { 94 | send('quotes:receiveQuotesData', { data: snapshot.val() }, done) 95 | }) 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 1em; 3 | } 4 | 5 | input { 6 | min-width: 300px; 7 | margin-right: 1em; 8 | } 9 | 10 | .wrapper { 11 | margin: 0 auto; 12 | max-width: 600px; 13 | } 14 | 15 | .authpanel, .feedback, .newquoteform, .quote { 16 | margin-bottom: 1em; 17 | padding: 3px; 18 | } 19 | 20 | .authpanel { 21 | border-bottom: 1px solid #CCC; 22 | } 23 | 24 | .feedback { 25 | border: 1px solid #555; 26 | padding: 0.5em; 27 | background-color: #BFE6C5; 28 | border-radius: 5px; 29 | } 30 | 31 | .feedback button { 32 | float: right; 33 | } 34 | 35 | .feedback.error { 36 | background-color: #E6C8C9; 37 | } 38 | 39 | .author { 40 | font-size: 0.85em; 41 | color: #555; 42 | } -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | validateQuote: (content) => { 3 | if (!content || content.length < 10) { 4 | return 'A quote needs at least 10 characters to be worthy of sharing with the world!' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/views/auth-panel.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const C = require('../constants') 3 | 4 | const authPanel = (auth, send) => { 5 | switch (auth.currently) { 6 | case C.LOGGED_IN: 7 | return html` 8 |
9 | Logged in as ${auth.username}. 10 | 11 |
12 | ` 13 | case C.AWAITING_AUTH_RESPONSE: 14 | return html` 15 |
16 | 17 |
18 | ` 19 | default: 20 | return html` 21 |
22 | 23 |
24 | ` 25 | } 26 | } 27 | 28 | module.exports = authPanel 29 | -------------------------------------------------------------------------------- /src/views/feedback-panel.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | 3 | const feedbackPanel = (feedback, send) => { 4 | const rows = feedback.messages.map((f, n) => { 5 | return html` 6 |
7 | ${f.msg} 8 | 9 |
10 | ` 11 | }) 12 | return html` 13 |
14 | ${rows} 15 |
16 | ` 17 | } 18 | 19 | module.exports = feedbackPanel 20 | -------------------------------------------------------------------------------- /src/views/main.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | 3 | const authPanel = require('./auth-panel') 4 | const feedbackPanel = require('./feedback-panel') 5 | const quotesList = require('./quotes-list') 6 | 7 | const mainView = (state, prev, send) => { 8 | return html` 9 |
10 | ${authPanel(state.auth, send)} 11 |
12 | ${feedbackPanel(state.feedback, send)} 13 | ${quotesList(state.quotes, state.auth, send)} 14 |
15 |
16 | ` 17 | } 18 | 19 | module.exports = mainView 20 | -------------------------------------------------------------------------------- /src/views/quote.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const C = require('../constants') 3 | 4 | const buttonMaker = (name, cb, ...cbArgs) => 5 | html`` 6 | 7 | const quote = (quote, qid, quoteState, auth, send) => { 8 | const onSubmit = event => { 9 | event.preventDefault() 10 | const content = event.target.parentElement.querySelector('input').value 11 | const {uid, username} = auth 12 | 13 | send('quotes:submitQuoteEdit', { content, qid, uid, username }) 14 | } 15 | if (quoteState === C.EDITING_QUOTE) { 16 | return html` 17 |
18 | 19 | ${buttonMaker('Cancel', send, 'quotes:setFinishedEditing', { qid })} 20 | ${buttonMaker('Submit', onSubmit)} 21 |
22 | ` 23 | } 24 | 25 | let button 26 | if (quote.uid !== auth.uid) { 27 | button = '' 28 | } else if (quoteState === C.SUBMITTING_QUOTE) { 29 | button = html`` 30 | } else { 31 | button = html` 32 | 33 | ${buttonMaker('Edit', send, 'quotes:setIsEditing', { qid })} 34 | ${buttonMaker('Delete', send, 'quotes:deleteQuote', { qid })} 35 | ` 36 | } 37 | 38 | return html` 39 |
40 | ${quote.username ? quote.username : html`A NAMELESS GHOUL`} said: 41 | ${quote.content} ${button} 42 |
43 | ` 44 | } 45 | 46 | module.exports = quote 47 | -------------------------------------------------------------------------------- /src/views/quotes-list.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | const map = require('lodash.map') 3 | const quote = require('./quote') 4 | 5 | const quotesList = (quotes, auth, send) => { 6 | const onSubmit = event => { 7 | event.preventDefault() 8 | 9 | const input = event.target.querySelector('input') 10 | const {uid, username} = auth 11 | 12 | send('quotes:submitNewQuote', { uid, username, input }) 13 | // event.target.querySelector('input').value = '' 14 | } 15 | 16 | const rows = map(quotes.data, (q, qid) => { 17 | const quoteState = quotes.states[qid] 18 | return quote(q, qid, quoteState, auth, send) 19 | }).reverse() 20 | 21 | return html` 22 |
23 | ${auth.uid 24 | ? html` 25 |
26 | 27 | 30 |
` 31 | : html`

Log in to add a new quote of your own!

`} 32 | ${quotes.hasReceivedData ? rows : 'Loading quotes...'} 33 |
34 | ` 35 | } 36 | 37 | module.exports = quotesList 38 | --------------------------------------------------------------------------------