├── task-app-mobile ├── .watchmanconfig ├── app.json ├── .babelrc ├── .gitignore ├── App.js ├── src │ ├── reference.js │ ├── Button.js │ ├── TaskInput.js │ ├── styles.js │ ├── TaskList.js │ └── TaskListItem.js ├── package.json ├── .flowconfig └── README.md ├── screenshot.png ├── task-app-web ├── .firebaserc ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── src │ ├── App.js │ ├── index.js │ ├── reference.js │ ├── TaskInput.js │ ├── TaskList.js │ ├── TaskListItem.js │ ├── index.css │ └── registerServiceWorker.js ├── firebase.json ├── .gitignore ├── package.json ├── database.rules.json └── README.md └── README.md /task-app-mobile/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/griffinsockwell/react-firebase-crud/HEAD/screenshot.png -------------------------------------------------------------------------------- /task-app-web/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "react-firebase-crud" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /task-app-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/griffinsockwell/react-firebase-crud/HEAD/task-app-web/public/favicon.ico -------------------------------------------------------------------------------- /task-app-mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "sdkVersion": "20.0.0", 4 | "name": "Tasks", 5 | "slug": "task-app-mobile" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /task-app-mobile/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /task-app-web/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TaskInput from './TaskInput'; 3 | import TaskList from './TaskList'; 4 | 5 | const App = () => ( 6 |
7 | 8 | 9 |
10 | ); 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /task-app-web/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "build", 7 | "rewrites": [ 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /task-app-web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /task-app-mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # expo 7 | .expo/ 8 | 9 | # misc 10 | .DS_Store 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | -------------------------------------------------------------------------------- /task-app-mobile/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import styles from './src/styles'; 4 | import TaskInput from './src/TaskInput'; 5 | import TaskList from './src/TaskList'; 6 | 7 | const App = () => ( 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /task-app-web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "TASKS", 3 | "name": "React Firebase CRUD", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /task-app-web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /task-app-mobile/src/reference.js: -------------------------------------------------------------------------------- 1 | import * as firebase from 'firebase'; 2 | 3 | const config = { 4 | apiKey: 'AIzaSyBstL_t0GIeKI6voWzCWOd3HcGCNJdahXA', 5 | databaseURL: 'https://react-firebase-crud.firebaseio.com' 6 | }; 7 | firebase.initializeApp(config); 8 | 9 | const rootRef = firebase.database().ref(); 10 | export const tasksRef = rootRef.child('tasks'); 11 | export const timeRef = firebase.database.ServerValue.TIMESTAMP; 12 | -------------------------------------------------------------------------------- /task-app-web/src/reference.js: -------------------------------------------------------------------------------- 1 | import * as firebase from 'firebase'; 2 | 3 | const config = { 4 | apiKey: 'AIzaSyBstL_t0GIeKI6voWzCWOd3HcGCNJdahXA', 5 | databaseURL: 'https://react-firebase-crud.firebaseio.com' 6 | }; 7 | firebase.initializeApp(config); 8 | 9 | const rootRef = firebase.database().ref(); 10 | export const tasksRef = rootRef.child('tasks'); 11 | export const timeRef = firebase.database.ServerValue.TIMESTAMP; 12 | -------------------------------------------------------------------------------- /task-app-mobile/src/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import { MaterialIcons } from '@expo/vector-icons'; 4 | import styles from './styles'; 5 | 6 | const Button = props => ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default Button; 13 | -------------------------------------------------------------------------------- /task-app-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-app-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "firebase": "^4.3.1", 7 | "lodash": "^4.17.4", 8 | "react": "^15.6.1", 9 | "react-dom": "^15.6.1", 10 | "react-scripts": "1.0.13" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject", 17 | "serve": "npm run build && firebase serve", 18 | "deploy": "npm run build && firebase deploy" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /task-app-web/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "tasks": { 4 | ".read": "true", 5 | ".write": "true", 6 | "$task": { 7 | ".validate": "newData.hasChildren(['text', 'checked', 'starred', 'timestamp'])", 8 | "text": { 9 | ".validate": "newData.isString()" 10 | }, 11 | "checked": { 12 | ".validate": "newData.isBoolean()" 13 | }, 14 | "starred": { 15 | ".validate": "newData.isBoolean()" 16 | }, 17 | "timestamp": { 18 | ".validate": "newData.isNumber()" 19 | }, 20 | "$other": { 21 | ".validate": "false" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /task-app-mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-app-mobile", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "jest-expo": "~20.0.0", 7 | "react-native-scripts": "1.3.1", 8 | "react-test-renderer": "16.0.0-alpha.12" 9 | }, 10 | "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js", 11 | "scripts": { 12 | "start": "react-native-scripts start", 13 | "eject": "react-native-scripts eject", 14 | "android": "react-native-scripts android", 15 | "ios": "react-native-scripts ios", 16 | "test": "node node_modules/jest/bin/jest.js --watch" 17 | }, 18 | "jest": { 19 | "preset": "jest-expo" 20 | }, 21 | "dependencies": { 22 | "expo": "^20.0.0", 23 | "firebase": "^4.3.1", 24 | "lodash": "^4.17.4", 25 | "react": "16.0.0-alpha.12", 26 | "react-native": "^0.47.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /task-app-web/src/TaskInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tasksRef, timeRef } from './reference'; 3 | 4 | export default class TaskInput extends React.Component { 5 | state = { text: '' }; 6 | 7 | handleSubmit = event => { 8 | event.preventDefault(); 9 | const newTask = { 10 | text: this.state.text.trim(), 11 | checked: false, 12 | starred: false, 13 | timestamp: timeRef 14 | }; 15 | if (newTask.text.length) { 16 | tasksRef.push(newTask); 17 | this.setState({ text: '' }); 18 | } 19 | }; 20 | 21 | handleChange = event => { 22 | this.setState({ text: event.target.value }); 23 | }; 24 | 25 | render() { 26 | return ( 27 |
28 | 35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /task-app-mobile/src/TaskInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextInput } from 'react-native'; 3 | import { tasksRef, timeRef } from './reference'; 4 | import styles from './styles'; 5 | 6 | export default class TaskInput extends React.Component { 7 | state = { text: '' }; 8 | 9 | handleSubmit = () => { 10 | const newTask = { 11 | text: this.state.text.trim(), 12 | checked: false, 13 | starred: false, 14 | timestamp: timeRef 15 | }; 16 | if (newTask.text.length) { 17 | tasksRef.push(newTask); 18 | this.setState({ text: '' }); 19 | } 20 | }; 21 | 22 | handleChangeText = text => { 23 | this.setState({ text }); 24 | }; 25 | 26 | render() { 27 | return ( 28 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /task-app-mobile/src/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | App: { 5 | flex: 1, 6 | backgroundColor: '#EEE', 7 | paddingTop: 22, 8 | paddingLeft: 10, 9 | paddingRight: 10 10 | }, 11 | Button: { 12 | paddingHorizontal: 10 13 | }, 14 | TaskInput: { 15 | backgroundColor: '#4A90E2', 16 | height: 50, 17 | paddingLeft: 10, 18 | alignItems: 'center', 19 | marginVertical: 20, 20 | color: '#FFF', 21 | fontSize: 18 22 | }, 23 | TaskList_Empty: { 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | height: 100 27 | }, 28 | TaskList_EmptyText: { 29 | fontSize: 24, 30 | color: '#DDD' 31 | }, 32 | TaskListItem: { 33 | backgroundColor: '#fff', 34 | flexDirection: 'row', 35 | alignItems: 'center', 36 | marginBottom: 10, 37 | minHeight: 50, 38 | paddingVertical: 10 39 | }, 40 | TaskListItem_TextContainer: { 41 | flex: 1 42 | }, 43 | TaskListItem_Text: { 44 | color: '#4A4A4A', 45 | fontSize: 16 46 | }, 47 | TaskListItem_Checked: { 48 | color: '#9B9B9B', 49 | textDecorationLine: 'line-through' 50 | } 51 | }); 52 | 53 | export default styles; 54 | -------------------------------------------------------------------------------- /task-app-web/src/TaskList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import orderBy from 'lodash/orderBy'; 3 | import TaskListItem from './TaskListItem'; 4 | import { tasksRef } from './reference'; 5 | 6 | export default class TaskList extends React.Component { 7 | state = { 8 | tasks: [], 9 | tasksLoading: true 10 | }; 11 | 12 | componentDidMount() { 13 | tasksRef.on('value', snap => { 14 | const tasks = []; 15 | snap.forEach(shot => { 16 | tasks.push({ ...shot.val(), key: shot.key }); 17 | }); 18 | this.setState({ tasks, tasksLoading: false }); 19 | }); 20 | } 21 | 22 | render() { 23 | const { tasks, tasksLoading } = this.state; 24 | const orderedTasks = orderBy( 25 | tasks, 26 | ['checked', 'starred'], 27 | ['asc', 'desc'] 28 | ); 29 | 30 | let taskList; 31 | if (tasksLoading) { 32 | taskList =
Loading...
; 33 | } else if (tasks.length) { 34 | taskList = ( 35 | 40 | ); 41 | } else { 42 | taskList =
No Tasks
; 43 | } 44 | 45 | return taskList; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /task-app-mobile/src/TaskList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, View, Text } from 'react-native'; 3 | import orderBy from 'lodash/orderBy'; 4 | import TaskListItem from './TaskListItem'; 5 | import styles from './styles'; 6 | import { tasksRef } from './reference'; 7 | 8 | export default class TaskList extends React.Component { 9 | state = { 10 | tasks: [], 11 | tasksLoading: true 12 | }; 13 | 14 | componentDidMount() { 15 | tasksRef.on('value', snap => { 16 | const tasks = []; 17 | snap.forEach(shot => { 18 | tasks.push({ ...shot.val(), key: shot.key }); 19 | }); 20 | this.setState({ tasks, tasksLoading: false }); 21 | }); 22 | } 23 | 24 | renderItem = ({ item }) => { 25 | return ; 26 | }; 27 | 28 | render() { 29 | const { tasks, tasksLoading } = this.state; 30 | const orderedTasks = orderBy( 31 | tasks, 32 | ['checked', 'starred'], 33 | ['asc', 'desc'] 34 | ); 35 | 36 | let taskList; 37 | if (tasksLoading) { 38 | taskList = ( 39 | 40 | Loading... 41 | 42 | ); 43 | } else if (tasks.length) { 44 | taskList = ; 45 | } else { 46 | taskList = ( 47 | 48 | No Tasks 49 | 50 | ); 51 | } 52 | 53 | return taskList; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /task-app-web/src/TaskListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tasksRef } from './reference'; 3 | 4 | export default class TaskListItem extends React.Component { 5 | toggleChecked = () => { 6 | const { key, checked } = this.props.task; 7 | tasksRef.child(key).update({ checked: !checked }); 8 | }; 9 | 10 | toggleStarred = () => { 11 | const { key, starred } = this.props.task; 12 | tasksRef.child(key).update({ starred: !starred }); 13 | }; 14 | 15 | deleteTask = () => { 16 | const { key } = this.props.task; 17 | tasksRef.child(key).remove(); 18 | }; 19 | 20 | render() { 21 | const { task } = this.props; 22 | 23 | let buttonRight; 24 | if (task.checked) { 25 | buttonRight = ( 26 | 29 | ); 30 | } else if (task.starred) { 31 | buttonRight = ( 32 | 35 | ); 36 | } else { 37 | buttonRight = ( 38 | 41 | ); 42 | } 43 | 44 | return ( 45 |
  • 46 | 51 | 52 | 53 | {task.text} 54 | 55 | 56 | {buttonRight} 57 |
  • 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /task-app-web/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | body { 7 | background: #eee; 8 | font-family: 'Open Sans', sans-serif; 9 | } 10 | .App { 11 | width: 94%; 12 | max-width: 500px; 13 | margin: 0 auto; 14 | } 15 | .TaskInput { 16 | margin: 20px 0; 17 | } 18 | .TaskInput input { 19 | width: 100%; 20 | height: 50px; 21 | padding-left: 10px; 22 | border: none; 23 | color: #fff; 24 | font-family: 'Open Sans', sans-serif; 25 | font-size: 18px; 26 | background-color: #4a90e2; 27 | } 28 | .TaskInput input::-webkit-input-placeholder { 29 | color: #fff; 30 | font-family: 'Open Sans', sans-serif; 31 | font-size: 18px; 32 | } 33 | .TaskList-empty { 34 | width: 100%; 35 | text-align: center; 36 | margin-top: 80px; 37 | font-size: 30px; 38 | color: #ddd; 39 | } 40 | .TaskListItem { 41 | list-style-type: none; 42 | width: 100%; 43 | min-height: 50px; 44 | background: #fff; 45 | margin-bottom: 10px; 46 | display: flex; 47 | } 48 | .TaskListItem-checked { 49 | text-decoration: line-through; 50 | color: #9b9b9b !important; 51 | } 52 | .TaskListItem span { 53 | flex: 1; 54 | display: flex; 55 | align-items: center; 56 | padding: 10px 0; 57 | font-size: 15px; 58 | color: #4a4a4a; 59 | } 60 | .TaskListItem button { 61 | border: none; 62 | background: none; 63 | padding: 0 10px; 64 | cursor: pointer; 65 | color: #ccc; 66 | } 67 | .icon-grey { 68 | transition: all 0.2s; 69 | color: #9b9b9b; 70 | } 71 | .icon-grey:hover { 72 | color: #828282; 73 | } 74 | .icon-red { 75 | transition: all 0.2s; 76 | color: #d0011b; 77 | } 78 | .icon-red:hover { 79 | color: #9d0114; 80 | } 81 | .icon-yellow { 82 | transition: all 0.2s; 83 | color: #f8e81c; 84 | } 85 | .icon-yellow:hover { 86 | color: #dacb07; 87 | } 88 | -------------------------------------------------------------------------------- /task-app-mobile/src/TaskListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | import Button from './Button'; 4 | import styles from './styles'; 5 | import { tasksRef } from './reference'; 6 | 7 | export default class TaskListItem extends React.Component { 8 | toggleChecked = () => { 9 | const { key, checked } = this.props.task; 10 | tasksRef.child(key).update({ checked: !checked }); 11 | }; 12 | 13 | toggleStarred = () => { 14 | const { key, starred } = this.props.task; 15 | tasksRef.child(key).update({ starred: !starred }); 16 | }; 17 | 18 | deleteTask = () => { 19 | const { key } = this.props.task; 20 | tasksRef.child(key).remove(); 21 | }; 22 | 23 | render() { 24 | const { task } = this.props; 25 | const checkedText = task.checked ? styles.TaskListItem_Checked : ''; 26 | 27 | let buttonRight; 28 | if (task.checked) { 29 | buttonRight = ( 30 |