├── .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 | [](http://standardjs.com/) [](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 |
`
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 |
--------------------------------------------------------------------------------