├── src ├── db │ └── index.js ├── components │ ├── Home.js │ ├── Editor.js │ ├── Login.js │ ├── Article.js │ ├── Profile.js │ ├── Register.js │ ├── Settings.js │ ├── ProfileFavorites.js │ ├── Footer.js │ ├── Header.js │ └── App.js ├── reducers │ ├── home.js │ ├── profile.js │ ├── settings.js │ ├── auth.js │ ├── article.js │ ├── editor.js │ ├── common.js │ └── articleList.js ├── index.js ├── reducer.js ├── store.js ├── index.html ├── constants │ └── actionTypes.js └── middleware.js ├── .gitattributes ├── logo.png ├── .firebaserc ├── database.rules.json ├── storage.rules ├── .editorconfig ├── functions ├── index.js ├── package.json └── .eslintrc.json ├── firebase.json ├── .gitignore ├── readme.md ├── gulpfile.js ├── LICENSE ├── dist └── index.html ├── package.json ├── .eslintrc.json └── .github └── contributing.md /src/db/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doowb/firebase-realworld-example-app/HEAD/logo.png -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "react-realworld-example-app" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "auth != null", 4 | ".write": "auth != null" 5 | } 6 | } -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Home = (props) => (
Home
); 4 | 5 | export default Home; 6 | -------------------------------------------------------------------------------- /src/components/Editor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Editor = (props) => (
Editor
); 4 | 5 | export default Editor; 6 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Login = (props) => (
Login
); 4 | 5 | export default Login; 6 | -------------------------------------------------------------------------------- /src/components/Article.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Article = (props) => (
Article
); 4 | 5 | export default Article; 6 | -------------------------------------------------------------------------------- /src/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Profile = (props) => (
Profile
); 4 | 5 | export default Profile; 6 | -------------------------------------------------------------------------------- /src/components/Register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Register = (props) => (
Register
); 4 | 5 | export default Register; 6 | -------------------------------------------------------------------------------- /src/components/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Settings = (props) => (
Settings
); 4 | 5 | export default Settings; 6 | -------------------------------------------------------------------------------- /src/components/ProfileFavorites.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ProfileFavorites = (props) => (
ProfileFavorites
); 4 | 5 | export default ProfileFavorites; 6 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth!=null; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{**/{actual,fixtures,expected,templates}/**,*.md}] 12 | trim_trailing_whitespace = false 13 | insert_final_newline = false -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /src/reducers/home.js: -------------------------------------------------------------------------------- 1 | import { HOME_PAGE_LOADED, HOME_PAGE_UNLOADED } from '../constants/actionTypes'; 2 | 3 | export default (state = {}, action) => { 4 | switch (action.type) { 5 | case HOME_PAGE_LOADED: 6 | return { 7 | ...state, 8 | tags: action.payload[0].tags 9 | }; 10 | case HOME_PAGE_UNLOADED: 11 | return {}; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = ({appName}) => ( 4 | 12 | ); 13 | 14 | export default Footer; 15 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "functions": { 6 | "predeploy": [ 7 | "npm --prefix \"$RESOURCE_DIR\" run lint" 8 | ] 9 | }, 10 | "hosting": { 11 | "public": "dist", 12 | "ignore": [ 13 | "firebase.json", 14 | "**/.*", 15 | "**/node_modules/**" 16 | ], 17 | "rewrites": [ 18 | { 19 | "source": "**", 20 | "destination": "/index.html" 21 | } 22 | ] 23 | }, 24 | "storage": { 25 | "rules": "storage.rules" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | *.DS_Store 3 | *.sublime-* 4 | Thumbs.db 5 | 6 | # test related, or directories generated by tests 7 | test/actual 8 | actual 9 | coverage 10 | .nyc* 11 | 12 | # npm 13 | node_modules 14 | npm-debug.log 15 | 16 | # yarn 17 | yarn.lock 18 | yarn-error.log 19 | 20 | # misc 21 | _gh_pages 22 | _draft 23 | _drafts 24 | bower_components 25 | vendor 26 | temp 27 | tmp 28 | TODO.md 29 | package-lock.json 30 | 31 | # IDEs and editors 32 | .idea 33 | .project 34 | .classpath 35 | *.launch 36 | .settings 37 | 38 | firebase-debug.log 39 | -------------------------------------------------------------------------------- /src/reducers/profile.js: -------------------------------------------------------------------------------- 1 | import { 2 | PROFILE_PAGE_LOADED, 3 | PROFILE_PAGE_UNLOADED, 4 | FOLLOW_USER, 5 | UNFOLLOW_USER 6 | } from '../constants/actionTypes'; 7 | 8 | export default (state = {}, action) => { 9 | switch (action.type) { 10 | case PROFILE_PAGE_LOADED: 11 | return { 12 | ...action.payload[0].profile 13 | }; 14 | case PROFILE_PAGE_UNLOADED: 15 | return {}; 16 | case FOLLOW_USER: 17 | case UNFOLLOW_USER: 18 | return { 19 | ...action.payload.profile 20 | }; 21 | default: 22 | return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import { Provider } from 'react-redux'; 3 | import React from 'react'; 4 | import { store, history} from './store'; 5 | 6 | import { Route, Switch } from 'react-router-dom'; 7 | import { ConnectedRouter } from 'react-router-redux'; 8 | 9 | import App from './components/App'; 10 | 11 | ReactDOM.render(( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ), document.getElementById('app')); 20 | -------------------------------------------------------------------------------- /src/reducers/settings.js: -------------------------------------------------------------------------------- 1 | import { 2 | SETTINGS_SAVED, 3 | SETTINGS_PAGE_UNLOADED, 4 | ASYNC_START 5 | } from '../constants/actionTypes'; 6 | 7 | export default (state = {}, action) => { 8 | switch (action.type) { 9 | case SETTINGS_SAVED: 10 | return { 11 | ...state, 12 | inProgress: false, 13 | errors: action.error ? action.payload.errors : null 14 | }; 15 | case SETTINGS_PAGE_UNLOADED: 16 | return {}; 17 | case ASYNC_START: 18 | return { 19 | ...state, 20 | inProgress: true 21 | }; 22 | default: 23 | return state; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase serve --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "dependencies": { 13 | "firebase-admin": "~5.12.0", 14 | "firebase-functions": "^1.0.1" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^4.12.0", 18 | "eslint-plugin-promise": "^3.6.0" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import article from './reducers/article'; 2 | import articleList from './reducers/articleList'; 3 | import auth from './reducers/auth'; 4 | import { combineReducers } from 'redux'; 5 | import common from './reducers/common'; 6 | import editor from './reducers/editor'; 7 | import home from './reducers/home'; 8 | import profile from './reducers/profile'; 9 | import settings from './reducers/settings'; 10 | import { routerReducer } from 'react-router-redux'; 11 | 12 | export default combineReducers({ 13 | article, 14 | articleList, 15 | auth, 16 | common, 17 | editor, 18 | home, 19 | profile, 20 | settings, 21 | router: routerReducer 22 | }); 23 | -------------------------------------------------------------------------------- /src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN, 3 | REGISTER, 4 | LOGIN_PAGE_UNLOADED, 5 | REGISTER_PAGE_UNLOADED, 6 | ASYNC_START, 7 | UPDATE_FIELD_AUTH 8 | } from '../constants/actionTypes'; 9 | 10 | export default (state = {}, action) => { 11 | switch (action.type) { 12 | case LOGIN: 13 | case REGISTER: 14 | return { 15 | ...state, 16 | inProgress: false, 17 | errors: action.error ? action.payload.errors : null 18 | }; 19 | case LOGIN_PAGE_UNLOADED: 20 | case REGISTER_PAGE_UNLOADED: 21 | return {}; 22 | case ASYNC_START: 23 | if (action.subtype === LOGIN || action.subtype === REGISTER) { 24 | return { ...state, inProgress: true }; 25 | } 26 | break; 27 | case UPDATE_FIELD_AUTH: 28 | return { ...state, [action.key]: action.value }; 29 | default: 30 | return state; 31 | } 32 | 33 | return state; 34 | }; 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ![RealWorld Example App](logo.png) 2 | 3 | > ### Firebase codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API. 4 | 5 | 6 | ### [Demo](https://github.com/gothinkster/realworld)    [RealWorld](https://github.com/gothinkster/realworld) 7 | 8 | 9 | This codebase was created to demonstrate a fully fledged fullstack application built with **Firebase** including CRUD operations, authentication, routing, pagination, and more. 10 | 11 | We've gone to great lengths to adhere to the **Firebase** community styleguides & best practices. 12 | 13 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. 14 | 15 | 16 | # How it works 17 | 18 | > Describe the general architecture of your app here 19 | 20 | # Getting started 21 | 22 | > npm install, npm start, etc. 23 | 24 | -------------------------------------------------------------------------------- /src/reducers/article.js: -------------------------------------------------------------------------------- 1 | import { 2 | ARTICLE_PAGE_LOADED, 3 | ARTICLE_PAGE_UNLOADED, 4 | ADD_COMMENT, 5 | DELETE_COMMENT 6 | } from '../constants/actionTypes'; 7 | 8 | export default (state = {}, action) => { 9 | switch (action.type) { 10 | case ARTICLE_PAGE_LOADED: 11 | return { 12 | ...state, 13 | article: action.payload[0].article, 14 | comments: action.payload[1].comments 15 | }; 16 | case ARTICLE_PAGE_UNLOADED: 17 | return {}; 18 | case ADD_COMMENT: 19 | return { 20 | ...state, 21 | commentErrors: action.error ? action.payload.errors : null, 22 | comments: action.error ? 23 | null : 24 | (state.comments || []).concat([action.payload.comment]) 25 | }; 26 | case DELETE_COMMENT: 27 | const commentId = action.commentId 28 | return { 29 | ...state, 30 | comments: state.comments.filter(comment => comment.id !== commentId) 31 | }; 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux'; 2 | import { createLogger } from 'redux-logger' 3 | import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; 4 | import { promiseMiddleware, localStorageMiddleware } from './middleware'; 5 | import reducer from './reducer'; 6 | 7 | import { routerMiddleware } from 'react-router-redux' 8 | import createHistory from 'history/createBrowserHistory'; 9 | 10 | export const history = createHistory(); 11 | 12 | // Build the middleware for intercepting and dispatching navigation actions 13 | const myRouterMiddleware = routerMiddleware(history); 14 | 15 | const getMiddleware = () => { 16 | if (process.env.NODE_ENV === 'production') { 17 | return applyMiddleware(myRouterMiddleware, promiseMiddleware, localStorageMiddleware); 18 | } else { 19 | // Enable additional logging in non-production environments. 20 | return applyMiddleware(myRouterMiddleware, promiseMiddleware, localStorageMiddleware, createLogger()) 21 | } 22 | }; 23 | 24 | export const store = createStore(reducer, composeWithDevTools(getMiddleware())); 25 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const buffer = require('vinyl-buffer'); 5 | const browserify = require('browserify'); 6 | const source = require('vinyl-source-stream'); 7 | 8 | const buildSource = function() { 9 | const b = browserify({ 10 | entries: './src/index.js', 11 | debug: true, 12 | // Tell browserify that firebase is loaded already 13 | external: ['firebase'] 14 | }) 15 | .transform('babelify', { 16 | plugins: ['transform-object-rest-spread'], 17 | presets: ['env', 'react'] 18 | }); 19 | 20 | return b.bundle() 21 | .pipe(source('app.js')) 22 | .pipe(buffer()) 23 | .pipe(gulp.dest('./dist/assets/js')); 24 | }; 25 | 26 | const copy = function() { 27 | return gulp.src('src/*.html') 28 | .pipe(gulp.dest('dist')); 29 | }; 30 | 31 | const build = gulp.parallel([copy, buildSource]); 32 | 33 | gulp.task('browserify', buildSource); 34 | gulp.task('copy', copy) 35 | 36 | gulp.task('build', build); 37 | gulp.task('dev', gulp.series(build, function watch() { 38 | gulp.watch(['src/**/*'], build); 39 | })); 40 | 41 | gulp.task('default', build); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018, Brian Woodward. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Conduit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Conduit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/reducers/editor.js: -------------------------------------------------------------------------------- 1 | import { 2 | EDITOR_PAGE_LOADED, 3 | EDITOR_PAGE_UNLOADED, 4 | ARTICLE_SUBMITTED, 5 | ASYNC_START, 6 | ADD_TAG, 7 | REMOVE_TAG, 8 | UPDATE_FIELD_EDITOR 9 | } from '../constants/actionTypes'; 10 | 11 | export default (state = {}, action) => { 12 | switch (action.type) { 13 | case EDITOR_PAGE_LOADED: 14 | return { 15 | ...state, 16 | articleSlug: action.payload ? action.payload.article.slug : '', 17 | title: action.payload ? action.payload.article.title : '', 18 | description: action.payload ? action.payload.article.description : '', 19 | body: action.payload ? action.payload.article.body : '', 20 | tagInput: '', 21 | tagList: action.payload ? action.payload.article.tagList : [] 22 | }; 23 | case EDITOR_PAGE_UNLOADED: 24 | return {}; 25 | case ARTICLE_SUBMITTED: 26 | return { 27 | ...state, 28 | inProgress: null, 29 | errors: action.error ? action.payload.errors : null 30 | }; 31 | case ASYNC_START: 32 | if (action.subtype === ARTICLE_SUBMITTED) { 33 | return { ...state, inProgress: true }; 34 | } 35 | break; 36 | case ADD_TAG: 37 | return { 38 | ...state, 39 | tagList: state.tagList.concat([state.tagInput]), 40 | tagInput: '' 41 | }; 42 | case REMOVE_TAG: 43 | return { 44 | ...state, 45 | tagList: state.tagList.filter(tag => tag !== action.tag) 46 | }; 47 | case UPDATE_FIELD_EDITOR: 48 | return { ...state, [action.key]: action.value }; 49 | default: 50 | return state; 51 | } 52 | 53 | return state; 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-realworld-example-app", 3 | "description": "Fullstack Firebase Realworld example application.", 4 | "version": "0.1.0", 5 | "homepage": "https://react-realworld-example-app.firebaseapp.com/", 6 | "author": "Brian Woodward (https://doowb.com)", 7 | "contributors": [ 8 | "Alon Bukai (http://www.alonbukai.com)", 9 | "Eric Simons (https://twitter.com/ericsimons40)", 10 | "Esakki Raj (http://esakkiraj.github.io)", 11 | "Sandeesh (https://github.com/SandeeshS)", 12 | "Udo Kramer (https://instagram.com/optikfluffel)" 13 | ], 14 | "repository": "doowb/firebase-realworld-example-app", 15 | "bugs": { 16 | "url": "https://github.com/doowb/firebase-realworld-example-app/issues" 17 | }, 18 | "license": "MIT", 19 | "files": [ 20 | "index.js" 21 | ], 22 | "main": "index.js", 23 | "engines": { 24 | "node": ">=4" 25 | }, 26 | "scripts": { 27 | "test": "mocha" 28 | }, 29 | "keywords": [ 30 | "firebase", 31 | "kit", 32 | "realworld", 33 | "starter" 34 | ], 35 | "devDependencies": { 36 | "babel-core": "^6.26.0", 37 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 38 | "babel-preset-env": "^1.6.1", 39 | "babel-preset-react": "^6.24.1", 40 | "babelify": "^8.0.0", 41 | "browserify": "^16.1.1", 42 | "gulp": "^4.0.0", 43 | "history": "^4.7.2", 44 | "react": "^16.3.1", 45 | "react-dom": "^16.3.1", 46 | "react-redux": "^5.0.7", 47 | "react-router": "^4.2.0", 48 | "react-router-dom": "^4.2.2", 49 | "react-router-redux": "^5.0.0-alpha.9", 50 | "redux": "^3.7.2", 51 | "redux-devtools-extension": "^2.13.2", 52 | "redux-logger": "^3.0.6", 53 | "vinyl-buffer": "^1.0.1", 54 | "vinyl-source-stream": "^2.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const APP_LOAD = 'APP_LOAD'; 2 | export const REDIRECT = 'REDIRECT'; 3 | export const ARTICLE_SUBMITTED = 'ARTICLE_SUBMITTED'; 4 | export const SETTINGS_SAVED = 'SETTINGS_SAVED'; 5 | export const DELETE_ARTICLE = 'DELETE_ARTICLE'; 6 | export const SETTINGS_PAGE_UNLOADED = 'SETTINGS_PAGE_UNLOADED'; 7 | export const HOME_PAGE_LOADED = 'HOME_PAGE_LOADED'; 8 | export const HOME_PAGE_UNLOADED = 'HOME_PAGE_UNLOADED'; 9 | export const ARTICLE_PAGE_LOADED = 'ARTICLE_PAGE_LOADED'; 10 | export const ARTICLE_PAGE_UNLOADED = 'ARTICLE_PAGE_UNLOADED'; 11 | export const ADD_COMMENT = 'ADD_COMMENT'; 12 | export const DELETE_COMMENT = 'DELETE_COMMENT'; 13 | export const ARTICLE_FAVORITED = 'ARTICLE_FAVORITED'; 14 | export const ARTICLE_UNFAVORITED = 'ARTICLE_UNFAVORITED'; 15 | export const SET_PAGE = 'SET_PAGE'; 16 | export const APPLY_TAG_FILTER = 'APPLY_TAG_FILTER'; 17 | export const CHANGE_TAB = 'CHANGE_TAB'; 18 | export const PROFILE_PAGE_LOADED = 'PROFILE_PAGE_LOADED'; 19 | export const PROFILE_PAGE_UNLOADED = 'PROFILE_PAGE_UNLOADED'; 20 | export const LOGIN = 'LOGIN'; 21 | export const LOGOUT = 'LOGOUT'; 22 | export const REGISTER = 'REGISTER'; 23 | export const LOGIN_PAGE_UNLOADED = 'LOGIN_PAGE_UNLOADED'; 24 | export const REGISTER_PAGE_UNLOADED = 'REGISTER_PAGE_UNLOADED'; 25 | export const ASYNC_START = 'ASYNC_START'; 26 | export const ASYNC_END = 'ASYNC_END'; 27 | export const EDITOR_PAGE_LOADED = 'EDITOR_PAGE_LOADED'; 28 | export const EDITOR_PAGE_UNLOADED = 'EDITOR_PAGE_UNLOADED'; 29 | export const ADD_TAG = 'ADD_TAG'; 30 | export const REMOVE_TAG = 'REMOVE_TAG'; 31 | export const UPDATE_FIELD_AUTH = 'UPDATE_FIELD_AUTH'; 32 | export const UPDATE_FIELD_EDITOR = 'UPDATE_FIELD_EDITOR'; 33 | export const FOLLOW_USER = 'FOLLOW_USER'; 34 | export const UNFOLLOW_USER = 'UNFOLLOW_USER'; 35 | export const PROFILE_FAVORITES_PAGE_UNLOADED = 'PROFILE_FAVORITES_PAGE_UNLOADED'; 36 | export const PROFILE_FAVORITES_PAGE_LOADED = 'PROFILE_FAVORITES_PAGE_LOADED'; 37 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | // import agent from './agent'; 2 | import { ASYNC_START, ASYNC_END, LOGIN, LOGOUT, REGISTER } from './constants/actionTypes'; 3 | 4 | const promiseMiddleware = store => next => action => { 5 | if (isPromise(action.payload)) { 6 | store.dispatch({ type: ASYNC_START, subtype: action.type }); 7 | 8 | const currentView = store.getState().viewChangeCounter; 9 | const skipTracking = action.skipTracking; 10 | 11 | action.payload 12 | .then(res => { 13 | const currentState = store.getState(); 14 | if (!skipTracking && currentState.viewChangeCounter !== currentView) { 15 | return; 16 | } 17 | console.log('RESULT', res); 18 | action.payload = res; 19 | store.dispatch({ type: ASYNC_END, promise: action.payload }); 20 | store.dispatch(action); 21 | }) 22 | .catch(err => { 23 | const currentState = store.getState(); 24 | if (!skipTracking && currentState.viewChangeCounter !== currentView) { 25 | return; 26 | } 27 | console.log('ERROR', err); 28 | action.error = true; 29 | action.payload = err.response.body; 30 | if (!action.skipTracking) { 31 | store.dispatch({ type: ASYNC_END, promise: action.payload }); 32 | } 33 | store.dispatch(action); 34 | }); 35 | 36 | return; 37 | } 38 | 39 | next(action); 40 | }; 41 | 42 | const localStorageMiddleware = store => next => action => { 43 | if (action.type === REGISTER || action.type === LOGIN) { 44 | if (!action.error) { 45 | window.localStorage.setItem('jwt', action.payload.user.token); 46 | agent.setToken(action.payload.user.token); 47 | } 48 | } else if (action.type === LOGOUT) { 49 | window.localStorage.setItem('jwt', ''); 50 | agent.setToken(null); 51 | } 52 | 53 | next(action); 54 | }; 55 | 56 | function isPromise(v) { 57 | return v && typeof v.then === 'function'; 58 | } 59 | 60 | export { promiseMiddleware, localStorageMiddleware }; 61 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const LoggedOutView = ({currentUser}) => { 5 | if (!currentUser) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | return null; 22 | }; 23 | 24 | const LoggedInView = ({currentUser}) => { 25 | if (currentUser) { 26 | return ( 27 | 48 | ); 49 | } 50 | 51 | return null; 52 | }; 53 | 54 | const Header = ({appName, currentUser}) => ( 55 | 65 | ); 66 | 67 | export default Header; 68 | -------------------------------------------------------------------------------- /src/reducers/common.js: -------------------------------------------------------------------------------- 1 | import { 2 | APP_LOAD, 3 | REDIRECT, 4 | LOGOUT, 5 | ARTICLE_SUBMITTED, 6 | SETTINGS_SAVED, 7 | LOGIN, 8 | REGISTER, 9 | DELETE_ARTICLE, 10 | ARTICLE_PAGE_UNLOADED, 11 | EDITOR_PAGE_UNLOADED, 12 | HOME_PAGE_UNLOADED, 13 | PROFILE_PAGE_UNLOADED, 14 | PROFILE_FAVORITES_PAGE_UNLOADED, 15 | SETTINGS_PAGE_UNLOADED, 16 | LOGIN_PAGE_UNLOADED, 17 | REGISTER_PAGE_UNLOADED 18 | } from '../constants/actionTypes'; 19 | 20 | const defaultState = { 21 | appName: 'Conduit', 22 | token: null, 23 | viewChangeCounter: 0 24 | }; 25 | 26 | export default (state = defaultState, action) => { 27 | switch (action.type) { 28 | case APP_LOAD: 29 | return { 30 | ...state, 31 | token: action.token || null, 32 | appLoaded: true, 33 | currentUser: action.payload ? action.payload.user : null 34 | }; 35 | case REDIRECT: 36 | return { ...state, redirectTo: null }; 37 | case LOGOUT: 38 | return { ...state, redirectTo: '/', token: null, currentUser: null }; 39 | case ARTICLE_SUBMITTED: 40 | const redirectUrl = `/article/${action.payload.article.slug}`; 41 | return { ...state, redirectTo: redirectUrl }; 42 | case SETTINGS_SAVED: 43 | return { 44 | ...state, 45 | redirectTo: action.error ? null : '/', 46 | currentUser: action.error ? null : action.payload.user 47 | }; 48 | case LOGIN: 49 | case REGISTER: 50 | return { 51 | ...state, 52 | redirectTo: action.error ? null : '/', 53 | token: action.error ? null : action.payload.user.token, 54 | currentUser: action.error ? null : action.payload.user 55 | }; 56 | case DELETE_ARTICLE: 57 | return { ...state, redirectTo: '/' }; 58 | case ARTICLE_PAGE_UNLOADED: 59 | case EDITOR_PAGE_UNLOADED: 60 | case HOME_PAGE_UNLOADED: 61 | case PROFILE_PAGE_UNLOADED: 62 | case PROFILE_FAVORITES_PAGE_UNLOADED: 63 | case SETTINGS_PAGE_UNLOADED: 64 | case LOGIN_PAGE_UNLOADED: 65 | case REGISTER_PAGE_UNLOADED: 66 | return { ...state, viewChangeCounter: state.viewChangeCounter + 1 }; 67 | default: 68 | return state; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/reducers/articleList.js: -------------------------------------------------------------------------------- 1 | import { 2 | ARTICLE_FAVORITED, 3 | ARTICLE_UNFAVORITED, 4 | SET_PAGE, 5 | APPLY_TAG_FILTER, 6 | HOME_PAGE_LOADED, 7 | HOME_PAGE_UNLOADED, 8 | CHANGE_TAB, 9 | PROFILE_PAGE_LOADED, 10 | PROFILE_PAGE_UNLOADED, 11 | PROFILE_FAVORITES_PAGE_LOADED, 12 | PROFILE_FAVORITES_PAGE_UNLOADED 13 | } from '../constants/actionTypes'; 14 | 15 | export default (state = {}, action) => { 16 | switch (action.type) { 17 | case ARTICLE_FAVORITED: 18 | case ARTICLE_UNFAVORITED: 19 | return { 20 | ...state, 21 | articles: state.articles.map(article => { 22 | if (article.slug === action.payload.article.slug) { 23 | return { 24 | ...article, 25 | favorited: action.payload.article.favorited, 26 | favoritesCount: action.payload.article.favoritesCount 27 | }; 28 | } 29 | return article; 30 | }) 31 | }; 32 | case SET_PAGE: 33 | return { 34 | ...state, 35 | articles: action.payload.articles, 36 | articlesCount: action.payload.articlesCount, 37 | currentPage: action.page 38 | }; 39 | case APPLY_TAG_FILTER: 40 | return { 41 | ...state, 42 | pager: action.pager, 43 | articles: action.payload.articles, 44 | articlesCount: action.payload.articlesCount, 45 | tab: null, 46 | tag: action.tag, 47 | currentPage: 0 48 | }; 49 | case HOME_PAGE_LOADED: 50 | return { 51 | ...state, 52 | pager: action.pager, 53 | tags: action.payload[0].tags, 54 | articles: action.payload[1].articles, 55 | articlesCount: action.payload[1].articlesCount, 56 | currentPage: 0, 57 | tab: action.tab 58 | }; 59 | case HOME_PAGE_UNLOADED: 60 | return {}; 61 | case CHANGE_TAB: 62 | return { 63 | ...state, 64 | pager: action.pager, 65 | articles: action.payload.articles, 66 | articlesCount: action.payload.articlesCount, 67 | tab: action.tab, 68 | currentPage: 0, 69 | tag: null 70 | }; 71 | case PROFILE_PAGE_LOADED: 72 | case PROFILE_FAVORITES_PAGE_LOADED: 73 | return { 74 | ...state, 75 | pager: action.pager, 76 | articles: action.payload[1].articles, 77 | articlesCount: action.payload[1].articlesCount, 78 | currentPage: 0 79 | }; 80 | case PROFILE_PAGE_UNLOADED: 81 | case PROFILE_FAVORITES_PAGE_UNLOADED: 82 | return {}; 83 | default: 84 | return state; 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import db from '../db'; 2 | import Header from './Header'; 3 | import Footer from './Footer'; 4 | import React from 'react'; 5 | import { connect } from 'react-redux'; 6 | import { APP_LOAD, REDIRECT } from '../constants/actionTypes'; 7 | import { Route, Switch } from 'react-router-dom'; 8 | import Article from '../components/Article'; 9 | import Editor from '../components/Editor'; 10 | import Home from '../components/Home'; 11 | import Login from '../components/Login'; 12 | import Profile from '../components/Profile'; 13 | import ProfileFavorites from '../components/ProfileFavorites'; 14 | import Register from '../components/Register'; 15 | import Settings from '../components/Settings'; 16 | import { store } from '../store'; 17 | import { push } from 'react-router-redux'; 18 | 19 | const mapStateToProps = state => { 20 | return { 21 | appLoaded: state.common.appLoaded, 22 | appName: state.common.appName, 23 | currentUser: state.common.currentUser, 24 | redirectTo: state.common.redirectTo 25 | }}; 26 | 27 | const mapDispatchToProps = dispatch => ({ 28 | onLoad: (payload, token) => dispatch({ type: APP_LOAD, payload, token, skipTracking: true }), 29 | onRedirect: () => dispatch({ type: REDIRECT }) 30 | }); 31 | 32 | class App extends React.Component { 33 | componentWillReceiveProps(nextProps) { 34 | // if (nextProps.redirectTo) { 35 | // // this.context.router.replace(nextProps.redirectTo); 36 | // store.dispatch(push(nextProps.redirectTo)); 37 | // this.props.onRedirect(); 38 | // } 39 | } 40 | 41 | componentDidMount() { 42 | // const token = window.localStorage.getItem('jwt'); 43 | // if (token) { 44 | // db.setToken(token); 45 | // } 46 | 47 | this.props.onLoad(); 48 | } 49 | 50 | render() { 51 | if (this.props.appLoaded) { 52 | return ( 53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
68 | ); 69 | } 70 | 71 | return ( 72 |
73 |
74 |
76 | ); 77 | } 78 | } 79 | 80 | export default connect(mapStateToProps, mapDispatchToProps)(App); 81 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | 9 | "globals": { 10 | "document": false, 11 | "navigator": false, 12 | "window": false 13 | }, 14 | 15 | "rules": { 16 | "accessor-pairs": 2, 17 | "arrow-spacing": [2, { "before": true, "after": true }], 18 | "block-spacing": [2, "always"], 19 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 20 | "comma-dangle": [2, "never"], 21 | "comma-spacing": [2, { "before": false, "after": true }], 22 | "comma-style": [2, "last"], 23 | "constructor-super": 2, 24 | "curly": [2, "multi-line"], 25 | "dot-location": [2, "property"], 26 | "eol-last": 2, 27 | "eqeqeq": [2, "allow-null"], 28 | "generator-star-spacing": [2, { "before": true, "after": true }], 29 | "handle-callback-err": [2, "^(err|error)$" ], 30 | "indent": [2, 2, { "SwitchCase": 1 }], 31 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 32 | "keyword-spacing": [2, { "before": true, "after": true }], 33 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 34 | "new-parens": 2, 35 | "no-array-constructor": 2, 36 | "no-caller": 2, 37 | "no-class-assign": 2, 38 | "no-cond-assign": 2, 39 | "no-const-assign": 2, 40 | "no-control-regex": 2, 41 | "no-debugger": 2, 42 | "no-delete-var": 2, 43 | "no-dupe-args": 2, 44 | "no-dupe-class-members": 2, 45 | "no-dupe-keys": 2, 46 | "no-duplicate-case": 2, 47 | "no-empty-character-class": 2, 48 | "no-eval": 2, 49 | "no-ex-assign": 2, 50 | "no-extend-native": 2, 51 | "no-extra-bind": 2, 52 | "no-extra-boolean-cast": 2, 53 | "no-extra-parens": [2, "functions"], 54 | "no-fallthrough": 2, 55 | "no-floating-decimal": 2, 56 | "no-func-assign": 2, 57 | "no-implied-eval": 2, 58 | "no-inner-declarations": [2, "functions"], 59 | "no-invalid-regexp": 2, 60 | "no-irregular-whitespace": 2, 61 | "no-iterator": 2, 62 | "no-label-var": 2, 63 | "no-labels": 2, 64 | "no-lone-blocks": 2, 65 | "no-mixed-spaces-and-tabs": 2, 66 | "no-multi-spaces": 2, 67 | "no-multi-str": 2, 68 | "no-multiple-empty-lines": [2, { "max": 1 }], 69 | "no-native-reassign": 0, 70 | "no-negated-in-lhs": 2, 71 | "no-new": 2, 72 | "no-new-func": 2, 73 | "no-new-object": 2, 74 | "no-new-require": 2, 75 | "no-new-wrappers": 2, 76 | "no-obj-calls": 2, 77 | "no-octal": 2, 78 | "no-octal-escape": 2, 79 | "no-proto": 0, 80 | "no-redeclare": 2, 81 | "no-regex-spaces": 2, 82 | "no-return-assign": 2, 83 | "no-self-compare": 2, 84 | "no-sequences": 2, 85 | "no-shadow-restricted-names": 2, 86 | "no-spaced-func": 2, 87 | "no-sparse-arrays": 2, 88 | "no-this-before-super": 2, 89 | "no-throw-literal": 2, 90 | "no-trailing-spaces": 0, 91 | "no-undef": 2, 92 | "no-undef-init": 2, 93 | "no-unexpected-multiline": 2, 94 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 95 | "no-unreachable": 2, 96 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 97 | "no-useless-call": 0, 98 | "no-with": 2, 99 | "one-var": [0, { "initialized": "never" }], 100 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 101 | "padded-blocks": [0, "never"], 102 | "quotes": [2, "single", "avoid-escape"], 103 | "radix": 2, 104 | "semi": [2, "always"], 105 | "semi-spacing": [2, { "before": false, "after": true }], 106 | "space-before-blocks": [2, "always"], 107 | "space-before-function-paren": [2, "never"], 108 | "space-in-parens": [2, "never"], 109 | "space-infix-ops": 2, 110 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 111 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 112 | "use-isnan": 2, 113 | "valid-typeof": 2, 114 | "wrap-iife": [2, "any"], 115 | "yoda": [2, "never"] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /functions/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | // Required for certain syntax usages 4 | "ecmaVersion": 6 5 | }, 6 | "plugins": [ 7 | "promise" 8 | ], 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | // Removed rule "disallow the use of console" from recommended eslint rules 12 | "no-console": "off", 13 | 14 | // Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules 15 | "no-regex-spaces": "off", 16 | 17 | // Removed rule "disallow the use of debugger" from recommended eslint rules 18 | "no-debugger": "off", 19 | 20 | // Removed rule "disallow unused variables" from recommended eslint rules 21 | "no-unused-vars": "off", 22 | 23 | // Removed rule "disallow mixed spaces and tabs for indentation" from recommended eslint rules 24 | "no-mixed-spaces-and-tabs": "off", 25 | 26 | // Removed rule "disallow the use of undeclared variables unless mentioned in /*global */ comments" from recommended eslint rules 27 | "no-undef": "off", 28 | 29 | // Warn against template literal placeholder syntax in regular strings 30 | "no-template-curly-in-string": 1, 31 | 32 | // Warn if return statements do not either always or never specify values 33 | "consistent-return": 1, 34 | 35 | // Warn if no return statements in callbacks of array methods 36 | "array-callback-return": 1, 37 | 38 | // Require the use of === and !== 39 | "eqeqeq": 2, 40 | 41 | // Disallow the use of alert, confirm, and prompt 42 | "no-alert": 2, 43 | 44 | // Disallow the use of arguments.caller or arguments.callee 45 | "no-caller": 2, 46 | 47 | // Disallow null comparisons without type-checking operators 48 | "no-eq-null": 2, 49 | 50 | // Disallow the use of eval() 51 | "no-eval": 2, 52 | 53 | // Warn against extending native types 54 | "no-extend-native": 1, 55 | 56 | // Warn against unnecessary calls to .bind() 57 | "no-extra-bind": 1, 58 | 59 | // Warn against unnecessary labels 60 | "no-extra-label": 1, 61 | 62 | // Disallow leading or trailing decimal points in numeric literals 63 | "no-floating-decimal": 2, 64 | 65 | // Warn against shorthand type conversions 66 | "no-implicit-coercion": 1, 67 | 68 | // Warn against function declarations and expressions inside loop statements 69 | "no-loop-func": 1, 70 | 71 | // Disallow new operators with the Function object 72 | "no-new-func": 2, 73 | 74 | // Warn against new operators with the String, Number, and Boolean objects 75 | "no-new-wrappers": 1, 76 | 77 | // Disallow throwing literals as exceptions 78 | "no-throw-literal": 2, 79 | 80 | // Require using Error objects as Promise rejection reasons 81 | "prefer-promise-reject-errors": 2, 82 | 83 | // Enforce “for” loop update clause moving the counter in the right direction 84 | "for-direction": 2, 85 | 86 | // Enforce return statements in getters 87 | "getter-return": 2, 88 | 89 | // Disallow await inside of loops 90 | "no-await-in-loop": 2, 91 | 92 | // Disallow comparing against -0 93 | "no-compare-neg-zero": 2, 94 | 95 | // Warn against catch clause parameters from shadowing variables in the outer scope 96 | "no-catch-shadow": 1, 97 | 98 | // Disallow identifiers from shadowing restricted names 99 | "no-shadow-restricted-names": 2, 100 | 101 | // Enforce return statements in callbacks of array methods 102 | "callback-return": 2, 103 | 104 | // Require error handling in callbacks 105 | "handle-callback-err": 2, 106 | 107 | // Warn against string concatenation with __dirname and __filename 108 | "no-path-concat": 1, 109 | 110 | // Prefer using arrow functions for callbacks 111 | "prefer-arrow-callback": 1, 112 | 113 | // Return inside each then() to create readable and reusable Promise chains. 114 | // Forces developers to return console logs and http calls in promises. 115 | "promise/always-return": 2, 116 | 117 | //Enforces the use of catch() on un-returned promises 118 | "promise/catch-or-return": 2, 119 | 120 | // Warn against nested then() or catch() statements 121 | "promise/no-nesting": 1 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to firebase-realworld-example-app 2 | 3 | First and foremost, thank you! We appreciate that you want to contribute to firebase-realworld-example-app, your time is valuable, and your contributions mean a lot to us. 4 | 5 | ## Important! 6 | 7 | By contributing to this project, you: 8 | 9 | * Agree that you have authored 100% of the content 10 | * Agree that you have the necessary rights to the content 11 | * Agree that you have received the necessary permissions from your employer to make the contributions (if applicable) 12 | * Agree that the content you contribute may be provided under the Project license(s) 13 | 14 | ## Getting started 15 | 16 | **What does "contributing" mean?** 17 | 18 | Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following: 19 | 20 | - Updating or correcting documentation 21 | - Feature requests 22 | - Bug reports 23 | 24 | If you'd like to learn more about contributing in general, the [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) has a lot of useful information. 25 | 26 | **Showing support for firebase-realworld-example-app** 27 | 28 | Please keep in mind that open source software is built by people like you, who spend their free time creating things the rest the community can use. 29 | 30 | Don't have time to contribute? No worries, here are some other ways to show your support for firebase-realworld-example-app: 31 | 32 | - star the [project](https://github.com/doowb/firebase-realworld-example-app) 33 | - tweet your support for firebase-realworld-example-app 34 | 35 | ## Issues 36 | 37 | ### Before creating an issue 38 | 39 | Please try to determine if the issue is caused by an underlying library, and if so, create the issue there. Sometimes this is difficult to know. We only ask that you attempt to give a reasonable attempt to find out. Oftentimes the readme will have advice about where to go to create issues. 40 | 41 | Try to follow these guidelines 42 | 43 | - **Avoid creating issues for implementation help**. It's much better for discoverability, SEO, and semantics - to keep the issue tracker focused on bugs and feature requests - to ask implementation-related questions on [stackoverflow.com][so] 44 | - **Investigate the issue**: 45 | - **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue. 46 | - Create the issue in the appropriate repository. 47 | 48 | ### Creating an issue 49 | 50 | Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue: 51 | 52 | - **version**: please note the version of firebase-realworld-example-app are you using 53 | - **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using 54 | - **error messages**: please paste any error messages into the issue, or a [gist](https://gist.github.com/) 55 | 56 | ### Closing issues 57 | 58 | The original poster or the maintainer's of firebase-realworld-example-app may close an issue at any time. Typically, but not exclusively, issues are closed when: 59 | 60 | - The issue is resolved 61 | - The project's maintainers have determined the issue is out of scope 62 | - An issue is clearly a duplicate of another issue, in which case the duplicate issue will be linked. 63 | - A discussion has clearly run its course 64 | 65 | 66 | ## Next steps 67 | 68 | **Tips for creating idiomatic issues** 69 | 70 | Spending just a little extra time to review best practices and brush up on your contributing skills will, at minimum, make your issue easier to read, easier to resolve, and more likely to be found by others who have the same or similar issue in the future. At best, it will open up doors and potential career opportunities by helping you be at your best. 71 | 72 | The following resources were hand-picked to help you be the most effective contributor you can be: 73 | 74 | - The [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) is a great place for newcomers to start, but there is also information for experienced contributors there. 75 | - Take some time to learn basic markdown. We can't stress this enough. Don't start pasting code into GitHub issues before you've taken a moment to review this [markdown cheatsheet](https://gist.github.com/jonschlinkert/5854601) 76 | - The GitHub guide to [basic markdown](https://help.github.com/articles/markdown-basics/) is another great markdown resource. 77 | - Learn about [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). And if you want to really go above and beyond, read [mastering markdown](https://guides.github.com/features/mastering-markdown/). 78 | 79 | At the very least, please try to: 80 | 81 | - Use backticks to wrap code. This ensures that it retains its formatting and isn't modified when it's rendered by GitHub, and makes the code more readable to others 82 | - When applicable, use syntax highlighting by adding the correct language name after the first "code fence" 83 | 84 | 85 | [so]: http://stackoverflow.com/questions/tagged/firebase-realworld-example-app 86 | --------------------------------------------------------------------------------