├── .env
├── .firebaserc
├── .gitignore
├── README.md
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
├── index.js
├── package-lock.json
└── package.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── components
├── LayoutHeader.js
├── Loading.css
├── Loading.js
├── PrivateRoute.js
├── Upload.css
└── Upload.js
├── config
└── firebase.js
├── firebase
├── authencation.js
├── firebase.js
├── firestore.js
├── index.js
└── storage.js
├── helpers
├── query-string.js
└── strict-uri-encode.js
├── hoc
├── index.js
├── withFirebase.js
└── withLoading.js
├── index.css
├── index.js
├── logo.svg
├── modules
├── auth
│ ├── actions.js
│ ├── constants.js
│ ├── index.js
│ ├── reducer.js
│ ├── saga.js
│ ├── selections.js
│ ├── service.js
│ └── test
│ │ ├── actions.test.js
│ │ ├── reducer.test.js
│ │ └── selections.test.js
├── track
│ ├── actions.js
│ ├── constants.js
│ ├── index.js
│ ├── reducer.js
│ ├── sagas.js
│ ├── selectors.js
│ └── service.js
└── user
│ ├── actions.js
│ ├── constants.js
│ ├── index.js
│ ├── reducer.js
│ ├── sagas.js
│ ├── selectors.js
│ └── service.js
├── pages
├── AppLayout.js
├── login
│ ├── Login.js
│ ├── Register.js
│ └── index.js
└── other
│ ├── List.js
│ ├── NewTrack.js
│ ├── Profile.js
│ ├── ReviewTrack.js
│ ├── UserForm.js
│ ├── UserList.js
│ ├── components
│ └── Track.js
│ └── index.js
├── reducers.js
├── registerServiceWorker.js
├── sagas.js
└── store
└── index.js
/.env:
--------------------------------------------------------------------------------
1 | PORT=3002
2 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "travelear-1470701732966"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /functions/node_modules
6 |
7 | # testing
8 | /coverage
9 |
10 | # production
11 | /build
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | .idea
24 | yarn.lock
25 | /.firebase
26 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | },
16 | "firestore": {
17 | "rules": "firestore.rules",
18 | "indexes": "firestore.indexes.json"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": []
3 | }
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | service cloud.firestore {
2 | match /databases/{database}/documents {
3 | match /{document=**} {
4 | allow read: if true;
5 | allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
6 | }
7 |
8 | match /users/{userId} {
9 | allow write: if request.auth.uid == userId
10 | }
11 |
12 | match /tracks/{trackId} {
13 | allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'creator';
14 | allow delete: if request.auth.uid == resource.data.author && !resource.data.isPublic;
15 | allow update: if request.auth.uid == resource.data.author;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const admin = require('firebase-admin');
3 | const _ = require('lodash');
4 |
5 | admin.initializeApp(functions.config().firebase);
6 |
7 | const db = admin.firestore();
8 | db.settings({ timestampsInSnapshots: true });
9 |
10 | const auth = admin.auth();
11 |
12 | exports.deleteUser = functions
13 | .firestore
14 | .document('users/{uid}')
15 | .onDelete((event, context) => {
16 | const uid = context.params.uid;
17 | return auth.deleteUser(uid)
18 | .then(function() {
19 | return { success: true };
20 | });
21 | });
22 |
23 | exports.deleteTrack = functions
24 | .firestore
25 | .document('tracks/{tid}')
26 | .onDelete((snap, context) => {
27 | const tid = context.params.tid;
28 | const track = snap.data();
29 | const userRef = db.collection('users').doc(track.author)
30 |
31 | return userRef.get().then(doc => {
32 | userRef.update({
33 | tracks: _.filter(doc.data().tracks, id => id !== tid)
34 | });
35 | });
36 | });
37 |
38 | exports.addTrack = functions
39 | .firestore
40 | .document('tracks/{tid}')
41 | .onCreate((snap, context) => {
42 | const tid = context.params.tid;
43 | const track = snap.data();
44 | const userRef = db.collection('users').doc(track.author)
45 |
46 | return userRef.get().then(doc => {
47 | userRef.update({
48 | tracks: _.concat(doc.data().tracks, tid)
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "serve": "firebase serve --only functions",
6 | "shell": "firebase functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "firebase deploy --only functions",
9 | "logs": "firebase functions:log"
10 | },
11 | "dependencies": {
12 | "firebase-admin": "~6.0.0",
13 | "firebase-functions": "^2.0.3",
14 | "lodash": "^4.17.11"
15 | },
16 | "private": true
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "travelear-admin",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "classnames": "^2.2.6",
7 | "currency-formatter": "^1.4.3",
8 | "firebase": "^5.3.0",
9 | "husky": "^0.14.3",
10 | "immutable": "^3.8.2",
11 | "lint-staged": "^7.2.2",
12 | "moment": "^2.22.2",
13 | "numeral": "^2.0.6",
14 | "prettier": "^1.14.2",
15 | "prop-types": "^15.6.2",
16 | "react": "^16.4.2",
17 | "react-dom": "^16.4.2",
18 | "react-loadable": "^5.4.0",
19 | "react-redux": "^5.0.7",
20 | "react-router-dom": "^4.3.1",
21 | "react-scripts": "1.1.5",
22 | "recompose": "^0.28.1",
23 | "redux": "^4.0.0",
24 | "redux-immutable": "^4.0.0",
25 | "redux-saga": "^0.16.0",
26 | "reselect": "^3.0.1",
27 | "uuid": "^3.3.2",
28 | "uuid-v4": "^0.1.0"
29 | },
30 | "lint-staged": {
31 | "src/**/*.{js,jsx,json,css}": [
32 | "prettier --single-quote --write",
33 | "git add"
34 | ]
35 | },
36 | "scripts": {
37 | "start": "react-scripts start",
38 | "build": "react-scripts build",
39 | "test": "react-scripts test --env=jsdom",
40 | "eject": "react-scripts eject",
41 | "precommit": "lint-staged"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dony2100/Travelear-Mocks-admin/0ea8aaecd13b86544c887e48521bce094f48975f/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Travelear
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .flex-column {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 | .flex-full {
6 | flex: 1;
7 | }
8 | .flex-center {
9 | justify-content: center;
10 | align-items: center;
11 | }
12 | .margin-large-button {
13 | margin: 25px 0px;
14 | }
15 | .width-small-button {
16 | width: 200px;
17 | }
18 | .container {
19 | flex: 1;
20 | }
21 | .header {
22 | min-height: 83px;
23 | background-color: #1c2834;
24 | flex-direction: row;
25 | justify-content: space-between;
26 | align-items: center;
27 | display: flex;
28 | }
29 | .header a {
30 | text-decoration-line: none;
31 | }
32 | .header a span {
33 | color: #e74d31;
34 | font-weight: bolder;
35 | font-size: 40px;
36 | margin-left: 50px;
37 | margin-right: 50px;
38 | }
39 | .header-menu {
40 | flex: 1;
41 | flex-direction: row;
42 | justify-content: flex-end;
43 | display: flex;
44 | margin-top: 20px;
45 | }
46 | .header-menu a {
47 | color: #ffffff;
48 | font-size: 19px;
49 | font-weight: bolder;
50 | padding: 20px;
51 | display: block;
52 | }
53 |
54 | .form {
55 | display: flex;
56 | flex-direction: column;
57 | align-items: center;
58 | }
59 | .login {
60 | width: 300px;
61 | margin: 25px 20px 0px 20px;
62 | }
63 | .title-page {
64 | font-size: 28px;
65 | color: #1c2834;
66 | font-weight: bolder;
67 | margin-top: 50px;
68 | margin-bottom: 25px;
69 | display: block;
70 | width: 100%;
71 | }
72 | .detail-container {
73 | width: 400px;
74 | background-color: #ffffff;
75 | padding: 20px;
76 | }
77 | .detail-info {
78 | display: flex;
79 | flex-direction: row;
80 | margin: 20px 0px;
81 | }
82 | .detail-info-right {
83 | margin-left: 15px;
84 | }
85 | .detail-info-right span,
86 | .detail-info-right div {
87 | margin-bottom: 5px;
88 | }
89 | .detail-info-right div span {
90 | margin-right: 10px;
91 | }
92 | .detail-name,
93 | .list-edit {
94 | font-size: 20px;
95 | font-weight: bolder;
96 | color: #1c2834;
97 | }
98 | .button-public-detail {
99 | margin-top: 25px;
100 | }
101 | .new-track {
102 | width: 700px;
103 | }
104 | .new-track img,
105 | .new-track audio {
106 | margin-bottom: 5px;
107 | }
108 | .button-submit-new {
109 | margin-top: 10px;
110 | }
111 | .profile-width-full {
112 | width: 100%;
113 | }
114 | .profile-image {
115 | width: 130px;
116 | height: 130px;
117 | background-color: #d8d8d8;
118 | margin-bottom: 5px;
119 | display: flex;
120 | justify-content: center;
121 | align-items: center;
122 | }
123 | .profile-image span {
124 | font-weight: bolder;
125 | color: #1c2834;
126 | margin: 0px 30px;
127 | text-align: center;
128 | }
129 | .row_new_track {
130 | width: 100%;
131 | display: flex;
132 | flex-direction: row;
133 | }
134 | .row_new_track input:first-of-type {
135 | margin-right: 15px;
136 | }
137 | .row_new_track input:nth-of-type(2) {
138 | margin-left: 15px;
139 | }
140 | .list {
141 | width: 900px;
142 | margin: 25px;
143 | }
144 | .list-detail {
145 | width: 395px;
146 | display: inline-block;
147 | background-color: #ffffff;
148 | margin: 15px 0px;
149 | padding: 20px;
150 | }
151 | .list-detail span a {
152 | margin-right: 10px;
153 | }
154 | .list-detail:nth-child(2n + 1) {
155 | margin-right: 15px;
156 | }
157 | .list-detail:nth-child(2n + 0) {
158 | margin-left: 15px;
159 | }
160 | .audio {
161 | width: 100%;
162 | }
163 |
164 | .error {
165 | color: red;
166 | text-align: left;
167 | width: 100%;
168 | }
169 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Route, BrowserRouter as Router, Switch } from 'react-router-dom';
3 | import { Provider } from 'react-redux';
4 | import { auth, getDocument } from './firebase';
5 | import AppLayout from './pages/AppLayout';
6 | import PrivateRoute from './components/PrivateRoute';
7 | import configureStore from './store';
8 | import { LoginPage, RegisterPage } from './pages/login';
9 | import { Loading } from './components/Loading';
10 | import './App.css';
11 | import './index.css';
12 |
13 | const store = configureStore({
14 | auth: {
15 | isLogin: false,
16 | user: {},
17 | loading: false
18 | }
19 | });
20 |
21 | class App extends PureComponent {
22 | constructor(props) {
23 | super(props);
24 | this.state = {
25 | isReady: false
26 | };
27 | }
28 |
29 | /**
30 | * Add an observer for changes to the user's sign-in state.
31 | */
32 | componentDidMount() {
33 | auth.onAuthStateChanged(async user => {
34 | if (user) {
35 | const userInfo = await getDocument('users', user.uid);
36 | const userData = {
37 | uid: user.uid,
38 | email: user.email,
39 | ...userInfo
40 | };
41 | store.dispatch({
42 | type: 'auth/LOGIN_SUCCESS',
43 | payload: { user: userData }
44 | });
45 | }
46 | this.setState({
47 | isReady: true
48 | });
49 | });
50 | }
51 |
52 | render() {
53 | const { isReady } = this.state;
54 | if (!isReady) {
55 | return ;
56 | }
57 | return (
58 |
59 |
60 |
61 |
62 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 | }
75 |
76 | export default App;
77 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/components/LayoutHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router-dom';
4 |
5 | class LayoutHeader extends Component {
6 | render() {
7 | const { auth } = this.props;
8 | const { user } = auth;
9 | return (
10 |
11 |
12 |
travelear
13 |
14 | {auth.isLogin && (
15 |
16 | {user.role === 'admin' &&
User}
17 | {(user.role === 'admin' || user.role === 'creator') && (
18 |
New
19 | )}
20 |
21 | Profile
22 |
23 |
Logout
24 |
25 | )}
26 |
27 | );
28 | }
29 | }
30 |
31 | LayoutHeader.propTypes = {
32 | auth: PropTypes.object.isRequired,
33 | logout: PropTypes.func.isRequired
34 | };
35 |
36 | LayoutHeader.defaultProps = {};
37 |
38 | export default LayoutHeader;
39 |
--------------------------------------------------------------------------------
/src/components/Loading.css:
--------------------------------------------------------------------------------
1 | .wrap-loading {
2 | text-align: center;
3 | margin-top: 200px;
4 | }
5 |
6 | .loading {
7 | display: inline-block;
8 | width: 64px;
9 | height: 64px;
10 | }
11 | .loading:after {
12 | content: ' ';
13 | display: block;
14 | width: 46px;
15 | height: 46px;
16 | margin: 1px;
17 | border-radius: 50%;
18 | border: 5px solid #000;
19 | border-color: #000 transparent #000 transparent;
20 | animation: loading 1.2s linear infinite;
21 | }
22 | @keyframes loading {
23 | 0% {
24 | transform: rotate(0deg);
25 | }
26 | 100% {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Loading.css';
3 | export const Loading = () => (
4 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/components/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Route, Redirect, withRouter } from 'react-router-dom';
4 | import { connect } from 'react-redux';
5 | import { makeSelectAuth } from '../modules/auth';
6 |
7 | const PrivateRoute = ({ component: Component, isLogin, ...rest }) => (
8 |
11 | isLogin ? (
12 |
13 | ) : (
14 |
20 | )
21 | }
22 | />
23 | );
24 |
25 | PrivateRoute.propTypes = {
26 | component: PropTypes.any.isRequired,
27 | isLogin: PropTypes.bool.isRequired,
28 | location: PropTypes.object
29 | };
30 |
31 | PrivateRoute.defaultProps = {
32 | location: {}
33 | };
34 |
35 | const mapStateToProps = state => {
36 | const { isLogin } = makeSelectAuth(state);
37 | return {
38 | isLogin
39 | };
40 | };
41 |
42 | export default withRouter(connect(mapStateToProps)(PrivateRoute));
43 |
--------------------------------------------------------------------------------
/src/components/Upload.css:
--------------------------------------------------------------------------------
1 | .form-input {
2 | width: 100%;
3 | }
4 |
5 | .form-input-upload {
6 | width: 100%;
7 | }
8 |
9 | .form-input-upload input,
10 | .form-input-upload button {
11 | float: left;
12 | }
13 |
14 | .form-input-upload input {
15 | width: calc(100% - 140px);
16 | margin-right: 10px;
17 | }
18 |
19 | .form-input-upload button {
20 | width: 100px;
21 | }
22 | .form-input-upload span {
23 | width: 106px;
24 | height: 32px;
25 | display: block;
26 | background-color: #1c2834;
27 | margin: 5px 0px;
28 | float: right;
29 | color: #ffffff;
30 | font-weight: bolder;
31 | line-height: 32px;
32 | text-align: center;
33 | cursor: pointer;
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Upload.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import uuid from 'uuid';
4 |
5 | import { uploadFile } from '../firebase/storage';
6 | import './Upload.css';
7 |
8 | class Upload extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | progress: 0
13 | };
14 | this.onUpload = this.onUpload.bind(this);
15 | this.onClick = this.onClick.bind(this);
16 | this.inputFile = React.createRef();
17 | }
18 |
19 | onUpload(e) {
20 | const { accept } = this.props;
21 | const { name } = this.props;
22 | const file = e.target.files[0];
23 | let ref = file.name;
24 | if (accept === 'audio') {
25 | ref = `audio/$${uuid()}-${file.name}`;
26 | } else {
27 | ref = `image/$${uuid()}-${file.name}`;
28 | }
29 | uploadFile(file, ref, progress => this.setState({ progress }))
30 | .then(value => {
31 | this.props.onChange({
32 | target: {
33 | name,
34 | value
35 | }
36 | });
37 | })
38 | .catch(error => console.log(error));
39 | }
40 |
41 | onClick(e) {
42 | this.inputFile.current.click();
43 | }
44 |
45 | render() {
46 | const { value, type, placeholder, name, accept } = this.props;
47 | let acceptFile = 'image/*';
48 | if (accept === 'audio') {
49 | acceptFile = 'audio/*';
50 | }
51 |
52 | return (
53 |
54 | {type === 'thumb' ? (
55 |
56 | {value === '' ? (
57 |
profile image
58 | ) : (
59 |

60 | )}
61 |
62 | ) : (
63 |
64 |
70 | Choose
71 |
72 | )}
73 | {this.state.progress ?
{this.state.progress}% : null}
74 |
83 |
84 | );
85 | }
86 | }
87 |
88 | Upload.propTypes = {
89 | onChange: PropTypes.func,
90 | value: PropTypes.string,
91 | accept: PropTypes.oneOf(['image', 'audio']),
92 | type: PropTypes.oneOf(['thumb', 'input']),
93 | name: PropTypes.string,
94 | placeholder: PropTypes.string
95 | };
96 |
97 | Upload.defaultProps = {
98 | value: '',
99 | accept: 'image',
100 | type: 'thumb',
101 | name: 'image',
102 | placeholder: 'Upload'
103 | };
104 |
105 | export default Upload;
106 |
--------------------------------------------------------------------------------
/src/config/firebase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Config firebase app
3 | * @type {{apiKey: string, authDomain: string, databaseURL: string, projectId: string, storageBucket: string, messagingSenderId: string}}
4 | */
5 | export const config = {
6 | apiKey: "AIzaSyBHIkYnZY3Z221RJ3lfO5KShMKMo8EJedg",
7 | authDomain: "travelear-df75e.firebaseapp.com",
8 | databaseURL: "https://travelear-df75e.firebaseio.com",
9 | projectId: "travelear-df75e",
10 | storageBucket: "travelear-df75e.appspot.com",
11 | messagingSenderId: "1035569732494"
12 | };
13 |
14 | /**
15 | * firestore setting
16 | * @type {{timestampsInSnapshots: boolean}}
17 | */
18 | export const fireStoreSetting = {
19 | timestampsInSnapshots: true
20 | };
21 |
--------------------------------------------------------------------------------
/src/firebase/authencation.js:
--------------------------------------------------------------------------------
1 | import { auth } from './firebase';
2 |
3 | /**
4 | * Sign in firebase email and password
5 | * @param email
6 | * @param password
7 | * @param remember
8 | * @returns {Promise}
9 | */
10 | export function signIn(email, password, remember = false) {
11 | const persistence = remember ? 'local' : 'none';
12 | return new Promise((resolve, reject) => {
13 | auth
14 | .setPersistence(persistence)
15 | .then(() =>
16 | auth
17 | .signInWithEmailAndPassword(email, password)
18 | .then(() => resolve({ success: true }))
19 | )
20 | .catch(error => reject(error));
21 | });
22 | }
23 |
24 | /**
25 | * Sign up user firebase with email and password
26 | * @param email
27 | * @param password
28 | * @returns {Promise}
29 | */
30 | export function signUp(email, password) {
31 | return new Promise((resolve, reject) => {
32 | auth
33 | .createUserWithEmailAndPassword(email, password)
34 | .then(() => {
35 | const { uid, email } = auth.currentUser;
36 | return resolve({ uid, email });
37 | })
38 | .catch(error => reject(error));
39 | });
40 | }
41 |
42 | /**
43 | * Sign out
44 | * @returns {Promise}
45 | */
46 | export function signOut() {
47 | return new Promise((resolve, reject) => {
48 | auth
49 | .signOut()
50 | .then(() => resolve())
51 | .catch(error => reject(error));
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/src/firebase/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 |
3 | import 'firebase/auth';
4 | import 'firebase/storage';
5 | import 'firebase/firestore';
6 |
7 | import { config, fireStoreSetting } from '../config/firebase';
8 |
9 | // Initialize the default app
10 | export const defaultApp = firebase.initializeApp(config);
11 |
12 | export const auth = defaultApp.auth();
13 | export const storage = defaultApp.storage();
14 | export const firestore = defaultApp.firestore();
15 |
16 | firestore.settings(fireStoreSetting);
17 |
--------------------------------------------------------------------------------
/src/firebase/firestore.js:
--------------------------------------------------------------------------------
1 | import { firestore } from './firebase';
2 |
3 | /**
4 | * format data from snapshot
5 | * @param entity
6 | * @param ModalData
7 | * @returns {*}
8 | */
9 | export function unwrapSnapshot(entity, ModalData = null) {
10 | if (ModalData) {
11 | return new ModalData({ id: entity.id, ...entity.data() });
12 | }
13 | return Object.assign({}, { id: entity.id }, entity.data());
14 | }
15 |
16 | /**
17 | * Get collection data from path
18 | * @param path
19 | * @param ModalData
20 | * @returns {Promise}
21 | */
22 |
23 | export const getCollection = async (path, ModalData = null) =>
24 | firestore
25 | .collection(path)
26 | .get()
27 | .then(querySnapshot => {
28 | const entities = [];
29 | querySnapshot.forEach(entity => {
30 | entities.push(unwrapSnapshot(entity, ModalData));
31 | });
32 | return entities;
33 | });
34 |
35 | /**
36 | * Add document to collection
37 | * @param collection
38 | * @param data
39 | * @returns {Promise}
40 | */
41 | export const addDocument = async (collection, data) =>
42 | firestore.collection(collection).add(data);
43 |
44 | /**
45 | * Update document to collection
46 | * @param collection
47 | * @param id
48 | * @param data
49 | * @returns {Promise}
50 | */
51 | export const updateDocument = async (collection, id, data) =>
52 | firestore
53 | .collection(collection)
54 | .doc(id)
55 | .update(data);
56 |
57 | /**
58 | * Remove document
59 | * @param collection
60 | * @param id
61 | * @returns {Promise}
62 | */
63 | export const removeDocument = async (collection, id) =>
64 | firestore
65 | .collection(collection)
66 | .doc(id)
67 | .delete();
68 |
69 | /**
70 | * Get document
71 | * @param collection
72 | * @param id
73 | * @returns {Promise<({}&{id}&any)>}
74 | */
75 | export const getDocument = async (collection, id) =>
76 | firestore
77 | .collection(collection)
78 | .doc(id)
79 | .get()
80 | .then(data => unwrapSnapshot(data));
81 |
82 | /**
83 | * Firestore Model
84 | */
85 | export class Firestore {
86 | constructor(actions, modalClass, collection) {
87 | this.actions = actions;
88 | this.modalClass = modalClass;
89 | this.collection = collection;
90 | }
91 |
92 | subscribe(emit, options = {}) {
93 | let query = firestore.collection(this.collection);
94 | // make where query
95 | if ('whereQueries' in options) {
96 | const { whereQueries } = options;
97 | whereQueries.forEach(({ attr, eq, value }) => {
98 | query = query.where(attr, eq, value);
99 | });
100 | }
101 | // make orderBy query
102 | if ('orderBy' in options) {
103 | const { orderBy } = options;
104 | orderBy.forEach(({ attr, value }) => {
105 | query = query.orderBy(attr, value);
106 | });
107 | }
108 | return query.onSnapshot(
109 | snapshot => {
110 | snapshot.docChanges().forEach(change => {
111 | if (change.type === 'removed') {
112 | emit(this.actions.onRemove(change.doc.id));
113 | } else {
114 | emit(
115 | this.actions.onMutate(unwrapSnapshot(change.doc, this.modalClass))
116 | );
117 | }
118 | });
119 | emit(this.actions.onSuccess());
120 | },
121 | error => {
122 | console.log(error);
123 | }
124 | );
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/firebase/index.js:
--------------------------------------------------------------------------------
1 | export { auth, firestore, storage } from './firebase';
2 | export {
3 | Firestore,
4 | getCollection,
5 | addDocument,
6 | updateDocument,
7 | removeDocument,
8 | getDocument
9 | } from './firestore';
10 | export { signIn, signOut, signUp } from './authencation';
11 |
--------------------------------------------------------------------------------
/src/firebase/storage.js:
--------------------------------------------------------------------------------
1 | import { storage } from './firebase';
2 |
3 | export function uploadFile(file, name, cb = (progress) => console.log(progress)) {
4 | const storageRef = storage.ref();
5 | return new Promise((resolve, reject) => {
6 | const uploadTask = storageRef.child(name).put(file, {});
7 | uploadTask.on(
8 | 'state_changed',
9 | snapshot => {
10 | const progress =
11 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
12 | cb(progress);
13 | },
14 | error => {
15 | // Handle unsuccessful uploads
16 | if (error) {
17 | reject(error);
18 | }
19 | },
20 | () => {
21 | uploadTask.snapshot.ref.getDownloadURL().then(function(downloadURL) {
22 | resolve(downloadURL);
23 | });
24 | }
25 | );
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/helpers/query-string.js:
--------------------------------------------------------------------------------
1 | /**
2 | * query-string npm packages with version > 5 fails to minify
3 | * https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-build-fails-to-minify
4 | */
5 | const strictUriEncode = require('./strict-uri-encode');
6 | const decodeComponent = require('decode-uri-component');
7 |
8 | /*eslint-disable */
9 | function encoderForArrayFormat(options) {
10 | switch (options.arrayFormat) {
11 | case 'index':
12 | return (key, value, index) => {
13 | return value === null
14 | ? [encode(key, options), '[', index, ']'].join('')
15 | : [
16 | encode(key, options),
17 | '[',
18 | encode(index, options),
19 | ']=',
20 | encode(value, options)
21 | ].join('');
22 | };
23 | case 'bracket':
24 | return (key, value) => {
25 | return value === null
26 | ? [encode(key, options), '[]'].join('')
27 | : [encode(key, options), '[]=', encode(value, options)].join('');
28 | };
29 | default:
30 | return (key, value) => {
31 | return value === null
32 | ? encode(key, options)
33 | : [encode(key, options), '=', encode(value, options)].join('');
34 | };
35 | }
36 | }
37 |
38 | function parserForArrayFormat(options) {
39 | let result;
40 |
41 | switch (options.arrayFormat) {
42 | case 'index':
43 | return (key, value, accumulator) => {
44 | result = /\[(\d*)\]$/.exec(key);
45 |
46 | key = key.replace(/\[\d*\]$/, '');
47 |
48 | if (!result) {
49 | accumulator[key] = value;
50 | return;
51 | }
52 |
53 | if (accumulator[key] === undefined) {
54 | accumulator[key] = {};
55 | }
56 |
57 | accumulator[key][result[1]] = value;
58 | };
59 | case 'bracket':
60 | return (key, value, accumulator) => {
61 | result = /(\[\])$/.exec(key);
62 | key = key.replace(/\[\]$/, '');
63 |
64 | if (!result) {
65 | accumulator[key] = value;
66 | return;
67 | }
68 |
69 | if (accumulator[key] === undefined) {
70 | accumulator[key] = [value];
71 | return;
72 | }
73 |
74 | accumulator[key] = [].concat(accumulator[key], value);
75 | };
76 | default:
77 | return (key, value, accumulator) => {
78 | if (accumulator[key] === undefined) {
79 | accumulator[key] = value;
80 | return;
81 | }
82 |
83 | accumulator[key] = [].concat(accumulator[key], value);
84 | };
85 | }
86 | }
87 |
88 | function encode(value, options) {
89 | if (options.encode) {
90 | return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
91 | }
92 |
93 | return value;
94 | }
95 |
96 | function decode(value, options) {
97 | if (options.decode) {
98 | return decodeComponent(value);
99 | }
100 |
101 | return value;
102 | }
103 |
104 | function keysSorter(input) {
105 | if (Array.isArray(input)) {
106 | return input.sort();
107 | }
108 |
109 | if (typeof input === 'object') {
110 | return keysSorter(Object.keys(input))
111 | .sort((a, b) => Number(a) - Number(b))
112 | .map(key => input[key]);
113 | }
114 |
115 | return input;
116 | }
117 |
118 | function extract(input) {
119 | const queryStart = input.indexOf('?');
120 | if (queryStart === -1) {
121 | return '';
122 | }
123 | return input.slice(queryStart + 1);
124 | }
125 |
126 | function parse(input, options) {
127 | options = Object.assign({ decode: true, arrayFormat: 'none' }, options);
128 |
129 | const formatter = parserForArrayFormat(options);
130 |
131 | // Create an object with no prototype
132 | const ret = Object.create(null);
133 |
134 | if (typeof input !== 'string') {
135 | return ret;
136 | }
137 |
138 | input = input.trim().replace(/^[?#&]/, '');
139 |
140 | if (!input) {
141 | return ret;
142 | }
143 |
144 | for (const param of input.split('&')) {
145 | let [key, value] = param.replace(/\+/g, ' ').split('=');
146 |
147 | // Missing `=` should be `null`:
148 | // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
149 | value = value === undefined ? null : decode(value, options);
150 |
151 | formatter(decode(key, options), value, ret);
152 | }
153 |
154 | return Object.keys(ret)
155 | .sort()
156 | .reduce((result, key) => {
157 | const value = ret[key];
158 | if (
159 | Boolean(value) &&
160 | typeof value === 'object' &&
161 | !Array.isArray(value)
162 | ) {
163 | // Sort object keys, not values
164 | result[key] = keysSorter(value);
165 | } else {
166 | result[key] = value;
167 | }
168 |
169 | return result;
170 | }, Object.create(null));
171 | }
172 |
173 | exports.extract = extract;
174 | exports.parse = parse;
175 |
176 | exports.stringify = (obj, options) => {
177 | const defaults = {
178 | encode: true,
179 | strict: true,
180 | arrayFormat: 'none'
181 | };
182 |
183 | options = Object.assign(defaults, options);
184 |
185 | if (options.sort === false) {
186 | options.sort = () => {};
187 | }
188 |
189 | const formatter = encoderForArrayFormat(options);
190 |
191 | return obj
192 | ? Object.keys(obj)
193 | .sort(options.sort)
194 | .map(key => {
195 | const value = obj[key];
196 |
197 | if (value === undefined) {
198 | return '';
199 | }
200 |
201 | if (value === null) {
202 | return encode(key, options);
203 | }
204 |
205 | if (Array.isArray(value)) {
206 | const result = [];
207 |
208 | for (const value2 of value.slice()) {
209 | if (value2 === undefined) {
210 | continue;
211 | }
212 |
213 | result.push(formatter(key, value2, result.length));
214 | }
215 |
216 | return result.join('&');
217 | }
218 |
219 | return encode(key, options) + '=' + encode(value, options);
220 | })
221 | .filter(x => x.length > 0)
222 | .join('&')
223 | : '';
224 | };
225 |
226 | exports.parseUrl = (input, options) => {
227 | return {
228 | url: input.split('?')[0] || '',
229 | query: parse(extract(input), options)
230 | };
231 | };
232 |
--------------------------------------------------------------------------------
/src/helpers/strict-uri-encode.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable */
2 | module.exports = str =>
3 | encodeURIComponent(str).replace(
4 | /[!'()*]/g,
5 | x =>
6 | `%${x
7 | .charCodeAt(0)
8 | .toString(16)
9 | .toUpperCase()}`
10 | );
11 |
--------------------------------------------------------------------------------
/src/hoc/index.js:
--------------------------------------------------------------------------------
1 | export { withLoading } from './withLoading';
2 | export {
3 | withSubscribe,
4 | withData,
5 | defaultPropsData,
6 | handleSubmit
7 | } from './withFirebase';
8 |
--------------------------------------------------------------------------------
/src/hoc/withFirebase.js:
--------------------------------------------------------------------------------
1 | import { lifecycle, compose, withState, withHandlers } from 'recompose';
2 | import queryString from '../helpers/query-string';
3 | import { addDocument, updateDocument, getDocument } from '../firebase';
4 |
5 | /**
6 | * Higher order function to inject actions to subscribe and unsubscribe something into a component
7 | * @param {function} subscribe
8 | * @param {function} unsubscribe
9 | */
10 | export function withSubscribe(subscribe, unsubscribe) {
11 | return lifecycle({
12 | componentWillMount() {
13 | this.props.dispatch(subscribe());
14 | },
15 | componentWillUnmount() {
16 | this.props.dispatch(unsubscribe());
17 | }
18 | });
19 | }
20 |
21 | /**
22 | * HOC to bind data for a component to a query string in url location and pass it as props to the component
23 | * updateData and updateLoading are state updater. Refer to defaultPropsData HOC
24 | */
25 | export const withData = lifecycle({
26 | componentWillMount() {
27 | const { search } = this.props.location;
28 | const { collection, id } = queryString.parse(search);
29 | const { updateData, updateLoading } = this.props;
30 | if (id) {
31 | getDocument(collection, id)
32 | .then(data => {
33 | updateData(data);
34 | updateLoading(false);
35 | })
36 | .catch(error => {
37 | updateLoading(false);
38 | });
39 | } else {
40 | updateLoading(false);
41 | }
42 | }
43 | });
44 |
45 | /**
46 | * Compose multiple state updaters into a single higher-order component
47 | */
48 | export const defaultPropsData = compose(
49 | withState('loading', 'updateLoading', true),
50 | withState('pending', 'updatePending', false),
51 | withState('data', 'updateData', {})
52 | );
53 |
54 | /**
55 | * HOC to inject a function to submit a form
56 | */
57 | export const handleSubmit = withHandlers({
58 | handleSave: props => async (id = null, values, collectionName = null) => {
59 | const { search } = props.location;
60 | const { collection } = queryString.parse(search);
61 | if (id) {
62 | return updateDocument(collectionName || collection, id, values);
63 | }
64 | const docRef = await addDocument(collectionName || collection, values);
65 | return updateDocument(collectionName || collection, docRef.id, { id: docRef.id });
66 | }
67 | });
68 |
--------------------------------------------------------------------------------
/src/hoc/withLoading.js:
--------------------------------------------------------------------------------
1 | import { branch, renderComponent } from 'recompose';
2 | import { Loading } from '../components/Loading';
3 |
4 | const isLoading = ({ loading }) => loading;
5 |
6 | /**
7 | * HOC to add a spinner while loading
8 | */
9 | export const withLoading = branch(isLoading, renderComponent(Loading));
10 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | min-height: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 | body {
7 | flex: 1;
8 | margin: 0;
9 | padding: 0;
10 | font-family: sans-serif;
11 | background-color: #f3f3f3;
12 | display: flex;
13 | flex-direction: column;
14 | color: #6f6f6f;
15 | }
16 | input {
17 | width: calc(100% - 20px);
18 | background-color: #ffffff;
19 | border-radius: 0;
20 | border: 1px solid #f2f2f2;
21 | margin: 5px 0px;
22 | padding: 7px 10px;
23 | box-shadow: 0.5px 0.5px gray;
24 | font-weight: bolder;
25 | }
26 | textarea {
27 | width: calc(100% - 20px);
28 | border: none;
29 | padding: 7px 10px;
30 | margin: 5px 0px;
31 | font-weight: bolder;
32 | box-shadow: 0.5px 0.5px gray;
33 | }
34 | button {
35 | background-color: #1c2834;
36 | color: #ffffff;
37 | font-weight: bolder;
38 | font-size: 16px;
39 | width: 240px;
40 | padding: 6px 0px;
41 | margin: 5px 0px;
42 | }
43 | #root {
44 | flex: 1;
45 | display: flex;
46 | flex-direction: column;
47 | }
48 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/modules/auth/actions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Auth Actions
3 | */
4 |
5 | import * as Actions from './constants';
6 |
7 | /**
8 | * Login action
9 | * @param email
10 | * @param password
11 | * @param remember
12 | * @returns {{type, payload: {email: *, password: *, remember: boolean}}}
13 | */
14 | export function login(email, password, remember = false) {
15 | return {
16 | type: Actions.LOGIN_REQUEST,
17 | payload: {
18 | email,
19 | password,
20 | remember
21 | }
22 | };
23 | }
24 |
25 | /**
26 | * Logout action
27 | * @returns {{type}}
28 | */
29 | export function logout() {
30 | return {
31 | type: Actions.LOGOUT_REQUEST
32 | };
33 | }
34 |
35 | /**
36 | * Register action
37 | * @param email
38 | * @param password
39 | * @returns {{type: string, payload: {email: *, password: *}}}
40 | */
41 | export function register(email, password) {
42 | return {
43 | type: Actions.SIGN_UP_REQUEST,
44 | payload: {
45 | email,
46 | password
47 | }
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/src/modules/auth/constants.js:
--------------------------------------------------------------------------------
1 | export const LOGIN_REQUEST = 'auth/LOGIN_REQUEST';
2 | export const LOGIN_SUCCESS = 'auth/LOGIN_SUCCESS';
3 | export const LOGIN_ERROR = 'auth/LOGIN_ERROR';
4 |
5 | export const LOGOUT_REQUEST = 'auth/LOGOUT_REQUEST';
6 | export const LOGOUT_SUCCESS = 'auth/LOGOUT_SUCCESS';
7 | export const LOGOUT_ERROR = 'auth/LOGOUT_ERROR';
8 |
9 | export const SIGN_UP_REQUEST = 'auth/SIGN_UP_REQUEST';
10 | export const SIGN_UP_ERROR = 'auth/SIGN_UP_ERROR';
11 |
--------------------------------------------------------------------------------
/src/modules/auth/index.js:
--------------------------------------------------------------------------------
1 | import authSaga from './saga';
2 | import authReducer from './reducer';
3 |
4 | export { login, logout, register } from './actions';
5 | export { authSaga, authReducer };
6 | export { makeSelectAuth } from './selections';
7 |
--------------------------------------------------------------------------------
/src/modules/auth/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import * as Actions from './constants';
3 |
4 | const initState = fromJS({
5 | loading: false,
6 | isLogin: false,
7 | user: {}
8 | });
9 |
10 | export default function authReducer(state = initState, action = {}) {
11 | switch (action.type) {
12 | case Actions.LOGIN_REQUEST:
13 | return state.set('loading', true);
14 | case Actions.LOGIN_SUCCESS:
15 | return state.merge({
16 | loading: false,
17 | isLogin: true,
18 | user: action.payload.user
19 | });
20 | case Actions.LOGOUT_REQUEST:
21 | case Actions.LOGIN_ERROR:
22 | case Actions.LOGOUT_SUCCESS:
23 | return initState;
24 | default:
25 | return state;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/modules/auth/saga.js:
--------------------------------------------------------------------------------
1 | import { call, put, takeLatest } from 'redux-saga/effects';
2 | import * as Actions from './constants';
3 | import { signInRequest, signOutRequest, signUpRequest } from './service';
4 | import { auth, getDocument, updateDocument } from '../../firebase';
5 |
6 | /**
7 | * login effect
8 | * @param {{ payload: object }} param0
9 | */
10 | export function* loginSaga({ payload }) {
11 | const { email, password, remember } = payload;
12 | try {
13 | yield call(signInRequest, { email, password, remember });
14 | const uid = auth.currentUser.uid;
15 | const user = yield call(getDocument, 'users', uid);
16 | yield put({
17 | type: 'auth/LOGIN_SUCCESS',
18 | payload: { user }
19 | });
20 | } catch (error) {
21 | console.log(error);
22 | yield put({
23 | type: Actions.LOGIN_ERROR,
24 | payload: { error }
25 | });
26 | }
27 | }
28 |
29 | /**
30 | * register effect
31 | * @param {{ payload: object }} param0
32 | */
33 | export function* registerSaga({ payload }) {
34 | const { email, password } = payload;
35 | try {
36 | const user = yield call(signUpRequest, { email, password });
37 | const userData = { id: user.uid, role: 'user', email: user.email, uid: user.uid, joined: new Date() };
38 | yield call(updateDocument, 'users', user.uid, userData);
39 | yield put({
40 | type: 'auth/LOGIN_SUCCESS',
41 | payload: { user: userData }
42 | });
43 | } catch (error) {
44 | console.log(error);
45 | yield put({
46 | type: Actions.SIGN_UP_ERROR,
47 | payload: { error }
48 | });
49 | }
50 | }
51 |
52 | /**
53 | * logout effect
54 | * @param {{ payload: object }} param0
55 | */
56 | function* logoutSaga() {
57 | try {
58 | yield call(signOutRequest);
59 | yield put({ type: Actions.LOGOUT_SUCCESS });
60 | } catch (error) {
61 | console.log(error);
62 | }
63 | }
64 |
65 | /**
66 | * Spawns a saga on each action dispatched to the store that matches pattern. and automatically cancels any previous saga task started previous if it's still running
67 | */
68 | export default function* authSaga() {
69 | yield takeLatest(Actions.LOGIN_REQUEST, loginSaga);
70 | yield takeLatest(Actions.SIGN_UP_REQUEST, registerSaga);
71 | yield takeLatest(Actions.LOGOUT_REQUEST, logoutSaga);
72 | }
73 |
--------------------------------------------------------------------------------
/src/modules/auth/selections.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const selectAuth = state => state.get('auth');
4 |
5 | export const makeSelectAuth = createSelector(selectAuth, auth => auth.toJS());
6 |
--------------------------------------------------------------------------------
/src/modules/auth/service.js:
--------------------------------------------------------------------------------
1 | import { signIn, signOut, signUp } from '../../firebase';
2 |
3 | /**
4 | * firebase sign in api
5 | * @param {object} input payload
6 | */
7 | const signInRequest = async input => {
8 | const { email, password, remember } = input;
9 | await signIn(email, password, remember);
10 | };
11 |
12 | /**
13 | * firebase sign up api
14 | * @param {object} param0 payload
15 | */
16 | const signUpRequest = ({ email, password }) => signUp(email, password);
17 |
18 | /**
19 | * firebase sign out api
20 | */
21 | const signOutRequest = async () => {
22 | await signOut();
23 | };
24 | export { signInRequest, signOutRequest, signUpRequest };
25 |
--------------------------------------------------------------------------------
/src/modules/auth/test/actions.test.js:
--------------------------------------------------------------------------------
1 | import { logout, login } from '../actions';
2 | import { LOGIN_REQUEST, LOGOUT_REQUEST } from '../constants';
3 |
4 | describe('Auth actions', () => {
5 | describe('Logout Action', () => {
6 | it('has a type of LOGOUT_REQUEST', () => {
7 | const expected = {
8 | type: LOGOUT_REQUEST
9 | };
10 | expect(logout()).toEqual(expected);
11 | });
12 | });
13 |
14 | describe('Login Action', () => {
15 | it('has a type of LOGIN_REQUEST and login info', () => {
16 | const email = 'isuperdev007@gmail.com';
17 | const password = '123456';
18 | const expected = {
19 | type: LOGIN_REQUEST,
20 | payload: {
21 | email,
22 | password,
23 | remember: false
24 | }
25 | };
26 | expect(login(email, password)).toEqual(expected);
27 | });
28 |
29 | it('has a type of LOGIN_REQUEST and login info and remember property', () => {
30 | const email = 'isuperdev007@gmail.com';
31 | const password = '123456';
32 | const expected = {
33 | type: LOGIN_REQUEST,
34 | payload: {
35 | email,
36 | password,
37 | remember: true
38 | }
39 | };
40 | expect(login(email, password, true)).toEqual(expected);
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/modules/auth/test/reducer.test.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import authReducer from '../reducer';
3 | import { LOGIN_SUCCESS, LOGIN_REQUEST, LOGOUT_SUCCESS } from '../constants';
4 |
5 | describe('Reducer', () => {
6 | it('should handle init app', () => {
7 | const state = undefined;
8 | const action = { type: '' };
9 | const stateInit = authReducer(state, action).toJS();
10 | expect(stateInit.loading).toBeFalsy();
11 | expect(stateInit.isLogin).toBeFalsy();
12 | expect(stateInit.user).toEqual({});
13 | });
14 |
15 | it('should handle login success', () => {
16 | const initState = fromJS({
17 | loading: false,
18 | isLogin: false,
19 | user: {}
20 | });
21 | const user = {
22 | email: 'isuperdev007@gmail.com',
23 | uid: '111222333'
24 | };
25 | const expectedState = fromJS({
26 | loading: false,
27 | isLogin: true,
28 | user
29 | });
30 | const action = {
31 | type: LOGIN_SUCCESS,
32 | payload: { user }
33 | };
34 | const state = authReducer(initState, action);
35 | expect(state.toJS()).toEqual(expectedState.toJS());
36 | expect(state.getIn(['user', 'uid'])).toBe('111222333');
37 | });
38 |
39 | it('should handle login request', () => {
40 | const initState = fromJS({
41 | loading: false,
42 | isLogin: false,
43 | user: {}
44 | });
45 | const expectedState = fromJS({
46 | loading: true,
47 | isLogin: false,
48 | user: {}
49 | });
50 | const user = {
51 | email: 'isuperdev007@gmail.com',
52 | uid: '111222333'
53 | };
54 | const action = {
55 | type: LOGIN_REQUEST,
56 | payload: { user }
57 | };
58 | const state = authReducer(initState, action);
59 | expect(state.toJS()).toEqual(expectedState.toJS());
60 | });
61 |
62 | it('should handle logout request', () => {
63 | const user = {
64 | email: 'isuperdev007@gmail.com',
65 | uid: '111222333'
66 | };
67 | const initState = fromJS({
68 | loading: false,
69 | isLogin: true,
70 | user
71 | });
72 | const expectedState = fromJS({
73 | loading: false,
74 | isLogin: false,
75 | user: {}
76 | });
77 | const action = {
78 | type: LOGOUT_SUCCESS
79 | };
80 | const state = authReducer(initState, action);
81 | expect(state.toJS()).toEqual(expectedState.toJS());
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/src/modules/auth/test/selections.test.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import { makeSelectAuth } from '../selections';
3 |
4 | describe('Select auth', () => {
5 | it('select auth from state', () => {
6 | const state = fromJS({
7 | auth: {
8 | email: 'admin@gmail.com',
9 | uid: '111222333'
10 | }
11 | });
12 | expect(makeSelectAuth(state)).toEqual(state.get('auth').toJS());
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/modules/track/actions.js:
--------------------------------------------------------------------------------
1 | import { SUBSCRIBE, UNSUBSCRIBE, REMOVE_REQUEST } from './constants';
2 |
3 | /**
4 | * Subscribe track
5 | * @returns {{type}}
6 | */
7 | export function subscribe() {
8 | return {
9 | type: SUBSCRIBE
10 | };
11 | }
12 |
13 | /**
14 | * Un subscribe track
15 | * @returns {{type}}
16 | */
17 | export function unsubscribe() {
18 | return {
19 | type: UNSUBSCRIBE
20 | };
21 | }
22 |
23 | /**
24 | * Remove track
25 | * @param id
26 | * @returns {{type, payload: {id: *}}}
27 | */
28 | export function remove(id) {
29 | return {
30 | type: REMOVE_REQUEST,
31 | payload: {
32 | id
33 | }
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/track/constants.js:
--------------------------------------------------------------------------------
1 | export const SUBSCRIBE = 'module/track/SUBSCRIBE';
2 | export const UNSUBSCRIBE = 'module/track/UNSUBSCRIBE';
3 | export const SUCCESS_SUBSCRIBE = 'module/track/SUCCESS_SUBSCRIBE';
4 |
5 | export const REMOVE_REQUEST = 'module/track/REMOVE_REQUEST';
6 |
7 | export const MUTATE = 'module/track/MUTATE';
8 | export const REMOVED = 'module/track/REMOVED';
9 |
--------------------------------------------------------------------------------
/src/modules/track/index.js:
--------------------------------------------------------------------------------
1 | import trackSagas from './sagas';
2 |
3 | export { subscribe, unsubscribe, remove } from './actions';
4 | export { trackReducer } from './reducer';
5 | export { makeSelectTrack, makeSelectPending } from './selectors';
6 | export { trackSagas };
7 |
--------------------------------------------------------------------------------
/src/modules/track/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import { MUTATE, REMOVED, SUBSCRIBE, SUCCESS_SUBSCRIBE } from './constants';
3 | import * as Actions from '../auth/constants';
4 |
5 | export const initState = new fromJS({
6 | loading: false,
7 | data: []
8 | });
9 |
10 | export function trackReducer(state = initState, { type, payload }) {
11 | const data = state.get('data');
12 | switch (type) {
13 | case SUBSCRIBE:
14 | return state.set('loading', true);
15 | case SUCCESS_SUBSCRIBE:
16 | return state.set('loading', false);
17 | case MUTATE:
18 | // Update data
19 | if (data.find(value => value.get('id') === payload.data.get('id'))) {
20 | return state.set(
21 | 'data',
22 | data.map(value => {
23 | if (value.get('id') === payload.data.get('id')) {
24 | return payload.data;
25 | }
26 | return value;
27 | })
28 | );
29 | }
30 | // Add new data
31 | return state.set('data', data.push(payload.data));
32 | case REMOVED:
33 | return state.set(
34 | 'data',
35 | data.filter(value => value.get('id') !== payload.id)
36 | );
37 | case Actions.LOGOUT_SUCCESS:
38 | return initState;
39 | default:
40 | return state;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/modules/track/sagas.js:
--------------------------------------------------------------------------------
1 | import { eventChannel } from 'redux-saga';
2 | import {
3 | call,
4 | cancel,
5 | fork,
6 | put,
7 | take,
8 | takeEvery,
9 | } from 'redux-saga/effects';
10 | import { SUBSCRIBE, UNSUBSCRIBE, REMOVE_REQUEST } from './constants';
11 | import { removeService, subscribeService } from './service';
12 |
13 | /**
14 | * saga effect
15 | */
16 | function* subscribe() {
17 | let options = {
18 | orderBy: [
19 | { attr: 'timestamp', value: 'desc' }
20 | ]
21 | };
22 | const channel = yield eventChannel(emit => subscribeService(emit, options));
23 | while (true) {
24 | const action = yield take(channel);
25 | yield put(action);
26 | }
27 | }
28 |
29 | /**
30 | * saga effect
31 | */
32 | function* watchRequestSubscribeTrack() {
33 | while (true) {
34 | yield take(SUBSCRIBE);
35 | const chanel = yield fork(subscribe);
36 |
37 | yield take(UNSUBSCRIBE);
38 | yield cancel(chanel);
39 | }
40 | }
41 |
42 | /**
43 | * saga effect
44 | */
45 | function* removeTrack({ payload }) {
46 | const { id } = payload;
47 | try {
48 | yield call(removeService, id);
49 | } catch (error) {
50 | console.log(error);
51 | }
52 | }
53 |
54 | export default function* trackSagas() {
55 | yield fork(watchRequestSubscribeTrack);
56 | yield takeEvery(REMOVE_REQUEST, removeTrack);
57 | }
58 |
--------------------------------------------------------------------------------
/src/modules/track/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | // select Track
4 | const selectTrack = state => state.get('track');
5 |
6 | // make select Track
7 | export const makeSelectTrack = createSelector(selectTrack, track =>
8 | track.toJS()
9 | );
10 |
11 | // make select Pending
12 | export const makeSelectPending = createSelector(
13 | selectTrack,
14 | track => track.toJS().pending
15 | );
16 |
--------------------------------------------------------------------------------
/src/modules/track/service.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 | import { Firestore, removeDocument } from '../../firebase';
3 | import { MUTATE, REMOVED, SUCCESS_SUBSCRIBE } from './constants';
4 |
5 | const collection = 'tracks';
6 |
7 | const Track = new Record({
8 | id: null,
9 | name: '',
10 | author: '',
11 | authorName: '',
12 | duration: '',
13 | location: '',
14 | image: {},
15 | file: '',
16 | latitude: '',
17 | longitude: '',
18 | plays: '',
19 | recorded: '',
20 | bio: '',
21 | tags: 0,
22 | isPublic: false,
23 | timestamp: '',
24 | });
25 |
26 | const Model = new Firestore(
27 | {
28 | onMutate: data => ({
29 | type: MUTATE,
30 | payload: { data }
31 | }),
32 | onRemove: id => ({
33 | type: REMOVED,
34 | payload: { id }
35 | }),
36 | onSuccess: () => ({ type: SUCCESS_SUBSCRIBE })
37 | },
38 | Track,
39 | collection
40 | );
41 |
42 | export const subscribeService = (emit, options) =>
43 | Model.subscribe(emit, options);
44 | export const removeService = id => removeDocument(collection, id);
45 |
--------------------------------------------------------------------------------
/src/modules/user/actions.js:
--------------------------------------------------------------------------------
1 | import { SUBSCRIBE, UNSUBSCRIBE, REMOVE_REQUEST } from './constants';
2 |
3 | /**
4 | * Subscribe user
5 | * @returns {{type}}
6 | */
7 | export function subscribe() {
8 | return {
9 | type: SUBSCRIBE
10 | };
11 | }
12 |
13 | /**
14 | * Un subscribe user
15 | * @returns {{type}}
16 | */
17 | export function unsubscribe() {
18 | return {
19 | type: UNSUBSCRIBE
20 | };
21 | }
22 |
23 | /**
24 | * Remove user
25 | * @param id
26 | * @returns {{type, payload: {id: *}}}
27 | */
28 | export function remove(id) {
29 | return {
30 | type: REMOVE_REQUEST,
31 | payload: {
32 | id
33 | }
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/user/constants.js:
--------------------------------------------------------------------------------
1 | export const SUBSCRIBE = 'module/user/SUBSCRIBE';
2 | export const UNSUBSCRIBE = 'module/user/UNSUBSCRIBE';
3 | export const SUCCESS_SUBSCRIBE = 'module/user/SUCCESS_SUBSCRIBE';
4 |
5 | export const REMOVE_REQUEST = 'module/user/REMOVE_REQUEST';
6 |
7 | export const MUTATE = 'module/user/MUTATE';
8 | export const REMOVED = 'module/user/REMOVED';
9 |
--------------------------------------------------------------------------------
/src/modules/user/index.js:
--------------------------------------------------------------------------------
1 | import userSagas from './sagas';
2 |
3 | export { subscribe, unsubscribe, remove } from './actions';
4 | export { userReducer } from './reducer';
5 | export { makeSelectUser, makeSelectPending } from './selectors';
6 | export { userSagas };
7 |
--------------------------------------------------------------------------------
/src/modules/user/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import { MUTATE, REMOVED, SUBSCRIBE, SUCCESS_SUBSCRIBE } from './constants';
3 | import * as Actions from '../auth/constants';
4 |
5 | export const initState = new fromJS({
6 | loading: false,
7 | data: []
8 | });
9 |
10 | export function userReducer(state = initState, { type, payload }) {
11 | const data = state.get('data');
12 | switch (type) {
13 | case SUBSCRIBE:
14 | return state.set('loading', true);
15 | case SUCCESS_SUBSCRIBE:
16 | return state.set('loading', false);
17 | case MUTATE:
18 | // Update data
19 | if (data.find(value => value.get('id') === payload.data.get('id'))) {
20 | return state.set(
21 | 'data',
22 | data.map(value => {
23 | if (value.get('id') === payload.data.get('id')) {
24 | return payload.data;
25 | }
26 | return value;
27 | })
28 | );
29 | }
30 | // Add new data
31 | return state.set('data', data.push(payload.data));
32 | case REMOVED:
33 | return state.set(
34 | 'data',
35 | data.filter(value => value.get('id') !== payload.id)
36 | );
37 | case Actions.LOGOUT_SUCCESS:
38 | return initState;
39 | default:
40 | return state;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/modules/user/sagas.js:
--------------------------------------------------------------------------------
1 | import { eventChannel } from 'redux-saga';
2 | import { call, cancel, fork, put, take, takeEvery } from 'redux-saga/effects';
3 | import { SUBSCRIBE, UNSUBSCRIBE, REMOVE_REQUEST } from './constants';
4 | import { removeService, subscribeService } from './service';
5 |
6 | function* subscribe() {
7 | let options = {};
8 | const channel = yield eventChannel(emit => subscribeService(emit, options));
9 | while (true) {
10 | const action = yield take(channel);
11 | yield put(action);
12 | }
13 | }
14 |
15 | function* watchRequestSubscribeTrack() {
16 | while (true) {
17 | yield take(SUBSCRIBE);
18 | const chanel = yield fork(subscribe);
19 |
20 | yield take(UNSUBSCRIBE);
21 | yield cancel(chanel);
22 | }
23 | }
24 |
25 | function* removeTrack({ payload }) {
26 | const { id } = payload;
27 | try {
28 | yield call(removeService, id);
29 | } catch (error) {
30 | console.log(error);
31 | }
32 | }
33 |
34 | export default function* trackSagas() {
35 | yield fork(watchRequestSubscribeTrack);
36 | yield takeEvery(REMOVE_REQUEST, removeTrack);
37 | }
38 |
--------------------------------------------------------------------------------
/src/modules/user/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | // select User
4 | const selectUser = state => state.get('user');
5 |
6 | // make select User
7 | export const makeSelectUser = createSelector(selectUser, track => track.toJS());
8 |
9 | // make select Pending
10 | export const makeSelectPending = createSelector(
11 | selectUser,
12 | track => track.toJS().pending
13 | );
14 |
--------------------------------------------------------------------------------
/src/modules/user/service.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 | import { Firestore, removeDocument } from '../../firebase';
3 | import { MUTATE, REMOVED, SUCCESS_SUBSCRIBE } from './constants';
4 |
5 | const collection = 'users';
6 |
7 | const Track = new Record({
8 | id: null,
9 | email: '',
10 | role: ''
11 | });
12 |
13 | const Model = new Firestore(
14 | {
15 | onMutate: data => ({
16 | type: MUTATE,
17 | payload: { data }
18 | }),
19 | onRemove: id => ({
20 | type: REMOVED,
21 | payload: { id }
22 | }),
23 | onSuccess: () => ({ type: SUCCESS_SUBSCRIBE })
24 | },
25 | Track,
26 | collection
27 | );
28 |
29 | export const subscribeService = (emit, options) =>
30 | Model.subscribe(emit, options);
31 | export const removeService = id => removeDocument(collection, id);
32 |
--------------------------------------------------------------------------------
/src/pages/AppLayout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Switch, Route, withRouter } from 'react-router-dom';
5 | import { connect } from 'react-redux';
6 | import { makeSelectAuth, logout } from '../modules/auth';
7 | import LayoutHeader from '../components/LayoutHeader';
8 |
9 | import {
10 | ListPage,
11 | NewTrack,
12 | Profile,
13 | ReviewTrack,
14 | UserForm,
15 | UserList
16 | } from './other';
17 |
18 | class AppLayout extends Component {
19 | constructor(props) {
20 | super(props);
21 | this.logout = this.logout.bind(this);
22 | }
23 | logout(e) {
24 | e.preventDefault();
25 | this.props.dispatch(logout());
26 | }
27 | render() {
28 | const { auth } = this.props;
29 | const {
30 | user: { role }
31 | } = auth;
32 | return (
33 |
34 |
35 |
36 |
37 |
43 |
49 |
55 | {role !== 'user' && (
56 |
61 | )}
62 |
68 |
73 | Not found
} />
74 |
75 |
76 | );
77 | }
78 | }
79 |
80 | AppLayout.propTypes = {
81 | dispatch: PropTypes.func.isRequired,
82 | location: PropTypes.object.isRequired
83 | };
84 |
85 | const mapStateToProps = state => {
86 | const auth = makeSelectAuth(state);
87 | return {
88 | auth
89 | };
90 | };
91 |
92 | export default withRouter(connect(mapStateToProps)(AppLayout));
93 |
--------------------------------------------------------------------------------
/src/pages/login/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { connect } from 'react-redux';
5 | import { Redirect } from 'react-router-dom';
6 |
7 | import { login } from '../../modules/auth';
8 |
9 | class Login extends Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | username: '',
14 | password: ''
15 | };
16 | this.onLogin = this.onLogin.bind(this);
17 | }
18 | onLogin(e) {
19 | e.preventDefault();
20 | const { username, password } = this.state;
21 | this.props.dispatch(login(username, password, true));
22 | }
23 | render() {
24 | const {
25 | auth: { isLogin }
26 | } = this.props;
27 | const { username, password } = this.state;
28 | if (isLogin) {
29 | return ;
30 | }
31 | return (
32 |
33 |
Login Page
34 |
53 |
54 | );
55 | }
56 | }
57 |
58 | Login.propTypes = {
59 | dispatch: PropTypes.func.isRequired,
60 | auth: PropTypes.shape({
61 | isLogin: PropTypes.bool.isRequired,
62 | loading: PropTypes.bool.isRequired
63 | }).isRequired
64 | };
65 |
66 | Login.defaultProps = {};
67 |
68 | const mapStateToProps = state => ({
69 | auth: state.get('auth').toJS()
70 | });
71 |
72 | const mapDispatchToProps = dispatch => ({
73 | dispatch
74 | });
75 |
76 | export default connect(
77 | mapStateToProps,
78 | mapDispatchToProps
79 | )(Login);
80 |
--------------------------------------------------------------------------------
/src/pages/login/Register.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { connect } from 'react-redux';
5 | import { Redirect } from 'react-router-dom';
6 |
7 | import { register } from '../../modules/auth';
8 |
9 | class Login extends Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | username: '',
14 | password: ''
15 | };
16 | this.onSubmit = this.onSubmit.bind(this);
17 | }
18 | onSubmit(e) {
19 | e.preventDefault();
20 | const { username, password } = this.state;
21 | this.props.dispatch(register(username, password));
22 | }
23 | render() {
24 | const {
25 | auth: { isLogin }
26 | } = this.props;
27 | const { username, password } = this.state;
28 | if (isLogin) {
29 | return ;
30 | }
31 | return (
32 |
33 |
Register Page
34 |
53 |
54 | );
55 | }
56 | }
57 |
58 | Login.propTypes = {
59 | dispatch: PropTypes.func.isRequired,
60 | auth: PropTypes.shape({
61 | isLogin: PropTypes.bool.isRequired,
62 | loading: PropTypes.bool.isRequired
63 | }).isRequired
64 | };
65 |
66 | Login.defaultProps = {};
67 |
68 | const mapStateToProps = state => ({
69 | auth: state.get('auth').toJS()
70 | });
71 |
72 | const mapDispatchToProps = dispatch => ({
73 | dispatch
74 | });
75 |
76 | export default connect(
77 | mapStateToProps,
78 | mapDispatchToProps
79 | )(Login);
80 |
--------------------------------------------------------------------------------
/src/pages/login/index.js:
--------------------------------------------------------------------------------
1 | import loadable from 'react-loadable';
2 |
3 | // Use react-loadable to make component-centric code splitting.
4 |
5 | export const LoginPage = loadable({
6 | loader: () => import('./Login'),
7 | loading: () => null
8 | });
9 |
10 | export const RegisterPage = loadable({
11 | loader: () => import('./Register'),
12 | loading: () => null
13 | });
14 |
--------------------------------------------------------------------------------
/src/pages/other/List.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { connect } from 'react-redux';
5 | import { compose } from 'recompose';
6 | import sortBy from 'lodash/sortBy';
7 |
8 | import { withSubscribe } from '../../hoc';
9 | import { makeSelectTrack, subscribe, unsubscribe } from '../../modules/track';
10 | import { makeSelectAuth } from '../../modules/auth';
11 | import { Track } from './components/Track';
12 |
13 | class List extends Component {
14 | render() {
15 | const { data, role, useId } = this.props;
16 | return (
17 |
18 |
19 | {data.map(track => (
20 |
21 | )).reverse()}
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | List.propTypes = {
29 | loading: PropTypes.bool.isRequired,
30 | data: PropTypes.array.isRequired,
31 | dispatch: PropTypes.func.isRequired,
32 | match: PropTypes.object.isRequired,
33 | role: PropTypes.string,
34 | useId: PropTypes.string.isRequired
35 | };
36 |
37 | List.defaultProps = {
38 | role: 'user'
39 | };
40 |
41 | /**
42 | * Filter tracks by the user role
43 | * - Admin has full access to all tracks
44 | * - Creators can't see all pending tracks created by other creators
45 | * - Normal users can only see public tracks
46 | * @param {State} state
47 | */
48 | const mapStateToProps = state => {
49 | const { loading, data } = makeSelectTrack(state);
50 |
51 | const {
52 | user: { role, id }
53 | } = makeSelectAuth(state);
54 |
55 | let preData;
56 | if (role === 'admin') {
57 | preData = data;
58 | } else if (role === 'creator') {
59 | preData = data.filter(track => track.isPublic || track.author === id)
60 | } else {
61 | preData = data.filter(track => track.isPublic)
62 | }
63 |
64 | return {
65 | loading,
66 | data: sortBy(preData, 'timestamp'),
67 | role,
68 | useId: id
69 | };
70 | };
71 |
72 | const mapDispatchToProps = dispatch => ({ dispatch });
73 |
74 | const withReducer = connect(
75 | mapStateToProps,
76 | mapDispatchToProps
77 | );
78 | const withData = withSubscribe(subscribe, unsubscribe);
79 |
80 | export default compose(
81 | withReducer,
82 | withData
83 | )(List);
84 |
--------------------------------------------------------------------------------
/src/pages/other/NewTrack.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import moment from 'moment';
5 | import { compose } from 'redux';
6 | import isEmpty from 'lodash/isEmpty';
7 | import { makeSelectAuth } from '../../modules/auth';
8 | import { removeService } from '../../modules/track/service';
9 | import {
10 | defaultPropsData,
11 | handleSubmit,
12 | withData,
13 | withLoading
14 | } from '../../hoc';
15 | import queryString from '../../helpers/query-string';
16 |
17 | import Upload from '../../components/Upload';
18 |
19 | import { connect } from 'react-redux';
20 | import { REMOVED } from '../../modules/track/constants';
21 |
22 | const initialValues = {
23 | name: '',
24 | location: '',
25 | latitude: '',
26 | longitude: '',
27 | tags: '',
28 | image: '',
29 | file: '',
30 | bio: ''
31 | };
32 |
33 | class NewTrack extends Component {
34 | constructor(props) {
35 | super(props);
36 | const track = this.props.data;
37 | this.state = {
38 | track: isEmpty(track) ?
39 | initialValues :
40 | {
41 | ...track,
42 | recorded: moment(track.recorded.toDate()).format('YYYY-MM-DD')
43 | },
44 | errors: {}
45 | };
46 | this.handleSubmit = this.handleSubmit.bind(this);
47 | this.handleChange = this.handleChange.bind(this);
48 | this.handleClear = this.handleClear.bind(this);
49 | this.handleDelete = this.handleDelete.bind(this);
50 | this.handleAudioDuration = this.handleAudioDuration.bind(this);
51 | }
52 |
53 | componentDidMount() {
54 | const { user } = this.props;
55 | const track = this.props.data;
56 | if (user.role === 'creator' && track.isPublic) {
57 | alert('Track approved, you can not edit content.');
58 | }
59 | }
60 |
61 | handleSubmit(e) {
62 | e.preventDefault();
63 |
64 | const { user } = this.props;
65 | const { track } = this.state;
66 |
67 | if (user.role === 'creator' && track.isPublic) {
68 | alert('Track approved, you can not edit content.');
69 | return;
70 | }
71 |
72 | this.props.updatePending(true);
73 | const { search } = this.props.location;
74 | const { id } = queryString.parse(search);
75 | const errors = this.validate(track);
76 |
77 | if (isEmpty(errors)) {
78 | let preData = track;
79 | if (!id) {
80 | preData = {
81 | isPublic: false,
82 | author: user.id,
83 | authorName: user.name ? user.name : '',
84 | plays: 0,
85 | duration: '',
86 | ...track,
87 | recorded: moment(track.recorded).toDate(),
88 | timestamp: new Date(),
89 | };
90 | } else {
91 | preData.recorded = moment(track.recorded).toDate();
92 | }
93 |
94 | this.props
95 | .handleSave(id, preData)
96 | .then(() => {
97 | alert('Save successfully!');
98 | this.props.history.push('/');
99 | })
100 | .catch(error => {
101 | alert(error.message);
102 | this.props.updatePending(false);
103 | });
104 | } else {
105 | this.setState({
106 | errors
107 | });
108 | }
109 | }
110 |
111 | handleAudioDuration(e) {
112 | const duration = e.target.duration;
113 | const transformed = moment.utc(moment.duration(duration, 'second').asMilliseconds());
114 | let durationString;
115 |
116 | if (transformed.hour()) {
117 | durationString = transformed.format('HH:m:ss');
118 | } else {
119 | durationString = transformed.format('m:ss');
120 | }
121 |
122 | this.setState({
123 | ...this.state,
124 | track: {
125 | ...this.state.track,
126 | duration: durationString
127 | }
128 | })
129 | }
130 |
131 | transformValue(name, value) {
132 | if (name === "latitude" || name === "longitude") {
133 | return parseFloat(value);
134 | }
135 | return value;
136 | }
137 |
138 | handleChange(e) {
139 | const { value, name } = e.target;
140 | const { track } = this.state;
141 | this.setState({
142 | track: { ...track, [name]: this.transformValue(name, value) }
143 | });
144 | }
145 |
146 | handleClear() {
147 | this.setState({
148 | track: initialValues
149 | });
150 | }
151 |
152 | handleDelete() {
153 | const { track } = this.state;
154 | removeService(track.id)
155 | .then(() => {
156 | this.props.dispatch({ type: REMOVED, payload: { id: track.id } });
157 | this.props.history.push('/');
158 | alert('Remove successfully!');
159 | })
160 | .catch(error => {
161 | alert(error.message);
162 | });
163 | }
164 | validate(data) {
165 | let errors = {};
166 | if (!data.name) {
167 | errors = { ...errors, name: 'Name is required!' };
168 | }
169 | if (!data.location) {
170 | errors = { ...errors, location: 'Location is required!' };
171 | }
172 | if (!data.latitude) {
173 | errors = { ...errors, latitude: 'Latitude is required!' };
174 | }
175 | if (!data.longitude) {
176 | errors = { ...errors, longitude: 'Longitude is required!' };
177 | }
178 | if (!data.image) {
179 | errors = { ...errors, image: 'Image is required!' };
180 | }
181 | if (!data.file) {
182 | errors = { ...errors, file: 'Audio is required!' };
183 | }
184 | if (!data.recorded) {
185 | errors = { ...errors, recorded: 'Recorded date is required!' };
186 | }
187 | return errors;
188 | }
189 | showButtonDelete() {
190 | const { track } = this.state;
191 | const { user } = this.props;
192 | if (!track.id) {
193 | return null;
194 | } else {
195 | if (
196 | user.role === 'admin' ||
197 | (user.role === 'creator' && !track.isPublic)
198 | ) {
199 | return (
200 |
206 | );
207 | }
208 | return null;
209 | }
210 | }
211 | render() {
212 | const { track, errors } = this.state;
213 | return (
214 |
215 |
216 |
321 | {this.showButtonDelete()}
322 |
325 |
326 |
327 | );
328 | }
329 | }
330 |
331 | NewTrack.propTypes = {
332 | location: PropTypes.object.isRequired,
333 | history: PropTypes.object.isRequired,
334 | handleSave: PropTypes.func.isRequired,
335 | updatePending: PropTypes.func.isRequired,
336 | pending: PropTypes.bool.isRequired
337 | };
338 |
339 | NewTrack.defaultProps = {};
340 |
341 | const mapStateToProps = state => {
342 | const { user } = makeSelectAuth(state);
343 | return {
344 | user
345 | };
346 | };
347 |
348 | const mapDispatchToProps = dispatch => ({ dispatch });
349 |
350 | const withReducer = connect(
351 | mapStateToProps,
352 | mapDispatchToProps
353 | );
354 |
355 | export default compose(
356 | withReducer,
357 | defaultPropsData,
358 | withData,
359 | withLoading,
360 | handleSubmit
361 | )(NewTrack);
362 |
--------------------------------------------------------------------------------
/src/pages/other/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { compose } from "redux";
5 | import { connect } from "react-redux";
6 | import isEmpty from "lodash/isEmpty";
7 |
8 | import {
9 | withData,
10 | withLoading,
11 | defaultPropsData,
12 | handleSubmit
13 | } from "../../hoc";
14 | import queryString from "../../helpers/query-string";
15 |
16 | import Upload from "../../components/Upload";
17 | import { makeSelectAuth } from "../../modules/auth";
18 | import { firestore } from "../../firebase";
19 |
20 | class Profile extends Component {
21 | constructor(props) {
22 | super(props);
23 | this.state = {
24 | user: this.props.data,
25 | errors: {}
26 | };
27 | this.handleSubmit = this.handleSubmit.bind(this);
28 | this.handleChange = this.handleChange.bind(this);
29 | }
30 |
31 | updateTracks(id, authorName) {
32 | firestore
33 | .collection("tracks")
34 | .where("author", "==", id)
35 | .get()
36 | .then(function(querySnapshot) {
37 | const batch = firestore.batch();
38 | querySnapshot.forEach(function(doc) {
39 | const setTrack = firestore.collection("tracks").doc(doc.id);
40 | batch.update(setTrack, { authorName });
41 | });
42 | batch.commit().then(function() {
43 | console.log("Update successfully!");
44 | });
45 | })
46 | .catch(function(error) {
47 | console.log("Error getting documents: ", error);
48 | });
49 | }
50 |
51 | handleSubmit(e) {
52 | e.preventDefault();
53 | this.props.updatePending(true);
54 | const { search } = this.props.location;
55 | const { id } = queryString.parse(search);
56 | const { user } = this.state;
57 | const errors = this.validate(user);
58 | const { role, email } = this.props.user;
59 | if (isEmpty(errors)) {
60 | this.props
61 | .handleSave(id, { ...user, role, email })
62 | .then(() => {
63 | alert("Save successfully!");
64 | this.updateTracks(id, user.name);
65 | this.props.updatePending(false);
66 | })
67 | .catch(error => {
68 | this.props.updatePending(false);
69 | });
70 | } else {
71 | this.setState({ errors });
72 | }
73 | }
74 |
75 | handleChange(e) {
76 | const { value, name } = e.target;
77 | this.setState({
78 | user: { ...this.state.user, [name]: value }
79 | });
80 | }
81 |
82 | validate(data) {
83 | let errors = {};
84 | if (!data.name) {
85 | errors = { ...errors, name: "Name is required!" };
86 | }
87 | if (!data.bio) {
88 | errors = { ...errors, bio: "Bio is required!" };
89 | }
90 | return errors;
91 | }
92 |
93 | render() {
94 | const { user, errors } = this.state;
95 |
96 | return (
97 |
141 | );
142 | }
143 | }
144 |
145 | Profile.propTypes = {
146 | location: PropTypes.object.isRequired,
147 | user: PropTypes.object.isRequired,
148 | history: PropTypes.object.isRequired,
149 | handleSave: PropTypes.func.isRequired,
150 | updatePending: PropTypes.func.isRequired,
151 | pending: PropTypes.bool.isRequired
152 | };
153 |
154 | Profile.defaultProps = {};
155 |
156 | const mapStateToProps = state => {
157 | const { user } = makeSelectAuth(state);
158 | return {
159 | user
160 | };
161 | };
162 |
163 | const mapDispatchToProps = dispatch => ({ dispatch });
164 |
165 | const withReducer = connect(
166 | mapStateToProps,
167 | mapDispatchToProps
168 | );
169 |
170 | export default compose(
171 | withReducer,
172 | defaultPropsData,
173 | withData,
174 | withLoading,
175 | handleSubmit
176 | )(Profile);
177 |
--------------------------------------------------------------------------------
/src/pages/other/ReviewTrack.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { compose } from 'redux';
5 | import {
6 | defaultPropsData,
7 | handleSubmit,
8 | withData,
9 | withLoading
10 | } from '../../hoc';
11 | import queryString from '../../helpers/query-string';
12 | import { removeService } from '../../modules/track/service';
13 | import { makeSelectTrack } from '../../modules/track';
14 | import { REMOVED } from '../../modules/track/constants';
15 | import { connect } from 'react-redux';
16 |
17 | class ReviewTrack extends Component {
18 | constructor(props) {
19 | super(props);
20 | this.state = {
21 | track: this.props.data
22 | };
23 | this.handleSubmit = this.handleSubmit.bind(this);
24 | this.handleRemove = this.handleRemove.bind(this);
25 | }
26 |
27 | handleSubmit() {
28 | this.props.updatePending(true);
29 | const { search } = this.props.location;
30 | const { id } = queryString.parse(search);
31 | this.props
32 | .handleSave(id, { ...this.state.track, isPublic: true })
33 | .then(() => {
34 | alert('Publish successfully!');
35 | this.props.updatePending(false);
36 | this.props.history.push('/');
37 | })
38 | .catch(error => {
39 | alert(error.message);
40 | this.props.updatePending(false);
41 | });
42 | }
43 |
44 | handleRemove() {
45 | const { track } = this.state;
46 | removeService(track.id)
47 | .then(() => {
48 | this.props.dispatch({ type: REMOVED, payload: { id: track.id } });
49 | this.props.history.push('/');
50 | alert('Remove successfully!');
51 | })
52 | .catch(error => {
53 | alert(error.message);
54 | });
55 | }
56 |
57 | render() {
58 | const { track } = this.state;
59 | return (
60 |
61 |
62 |
Review Track
63 |
64 |
65 |

71 |
72 |
{track.name}
73 |
{track.authorName}
74 |
{track.location}
75 |
76 | {track.latitude}
77 | {track.longitude}
78 |
79 |
{track.tags}
80 |
81 |
82 |
86 |
87 |
93 |
96 |
97 |
98 | );
99 | }
100 | }
101 |
102 | ReviewTrack.propTypes = {
103 | location: PropTypes.object.isRequired,
104 | history: PropTypes.object.isRequired,
105 | handleSave: PropTypes.func.isRequired,
106 | updatePending: PropTypes.func.isRequired,
107 | pending: PropTypes.bool.isRequired
108 | };
109 |
110 | ReviewTrack.defaultProps = {};
111 |
112 | const mapStateToProps = state => {
113 | const { loading, data } = makeSelectTrack(state);
114 | return {
115 | loading,
116 | data
117 | };
118 | };
119 |
120 | const mapDispatchToProps = dispatch => ({ dispatch });
121 |
122 | const withReducer = connect(
123 | mapStateToProps,
124 | mapDispatchToProps
125 | );
126 |
127 | export default compose(
128 | withReducer,
129 | defaultPropsData,
130 | withData,
131 | withLoading,
132 | handleSubmit
133 | )(ReviewTrack);
134 |
--------------------------------------------------------------------------------
/src/pages/other/UserForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { compose } from 'redux';
5 | import isEmpty from 'lodash/isEmpty';
6 | import {
7 | defaultPropsData,
8 | handleSubmit,
9 | withData,
10 | withLoading
11 | } from '../../hoc';
12 | import queryString from '../../helpers/query-string';
13 |
14 | const initialValues = {
15 | email: '',
16 | role: ''
17 | };
18 |
19 | class UserForm extends Component {
20 | constructor(props) {
21 | super(props);
22 | const user = this.props.data;
23 | this.state = {
24 | user: isEmpty(user) ? initialValues : user,
25 | errors: {}
26 | };
27 | this.handleSubmit = this.handleSubmit.bind(this);
28 | this.handleChange = this.handleChange.bind(this);
29 | }
30 |
31 | handleSubmit(e) {
32 | e.preventDefault();
33 |
34 | const { user } = this.state;
35 | this.props.updatePending(true);
36 | const { search } = this.props.location;
37 | const { id } = queryString.parse(search);
38 | this.props
39 | .handleSave(id, user, 'users')
40 | .then(() => {
41 | alert('Updated successfully!');
42 | this.props.history.push('/admin/user');
43 | })
44 | .catch(error => {
45 | alert(error.message);
46 | this.props.updatePending(false);
47 | });
48 | }
49 |
50 | handleChange(e) {
51 | const { value, name } = e.target;
52 | const { user } = this.state;
53 | this.setState({
54 | user: { ...user, [name]: value }
55 | });
56 | }
57 |
58 | render() {
59 | const { user } = this.state;
60 | return (
61 |
62 |
63 |
84 |
85 |
86 | );
87 | }
88 | }
89 |
90 | UserForm.propTypes = {
91 | location: PropTypes.object.isRequired,
92 | history: PropTypes.object.isRequired,
93 | handleSave: PropTypes.func.isRequired,
94 | updatePending: PropTypes.func.isRequired,
95 | pending: PropTypes.bool.isRequired
96 | };
97 |
98 | UserForm.defaultProps = {};
99 |
100 | export default compose(
101 | defaultPropsData,
102 | withData,
103 | withLoading,
104 | handleSubmit
105 | )(UserForm);
106 |
--------------------------------------------------------------------------------
/src/pages/other/UserList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { connect } from 'react-redux';
5 | import { compose } from 'recompose';
6 | import { Link } from 'react-router-dom';
7 |
8 | import { withSubscribe } from '../../hoc';
9 | import {
10 | makeSelectUser,
11 | subscribe,
12 | unsubscribe,
13 | remove
14 | } from '../../modules/user';
15 |
16 | class UserList extends Component {
17 | constructor(props, context) {
18 | super(props, context);
19 | this.onDelete = this.onDelete.bind(this);
20 | }
21 |
22 | onDelete(id) {
23 | //eslint-disable-next-line
24 | confirm('Are you want delete this user?')
25 | ? this.props.dispatch(remove(id))
26 | : null;
27 | }
28 |
29 | render() {
30 | const { data } = this.props;
31 | return (
32 |
33 |
34 |
35 |
36 | ID |
37 | Name |
38 | Email |
39 | Role |
40 | Action |
41 |
42 |
43 |
44 | {data.map(user => (
45 |
46 | {user.id} |
47 | {user.name} |
48 | {user.email} |
49 | {user.role} |
50 |
51 |
52 | Edit
53 |
54 |
60 | |
61 |
62 | ))}
63 |
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | UserList.propTypes = {
71 | loading: PropTypes.bool.isRequired,
72 | data: PropTypes.array.isRequired,
73 | dispatch: PropTypes.func.isRequired,
74 | match: PropTypes.object.isRequired
75 | };
76 |
77 | UserList.defaultProps = {};
78 |
79 | const mapStateToProps = state => {
80 | const { loading, data } = makeSelectUser(state);
81 | return {
82 | loading,
83 | data
84 | };
85 | };
86 |
87 | const mapDispatchToProps = dispatch => ({ dispatch });
88 |
89 | const withReducer = connect(
90 | mapStateToProps,
91 | mapDispatchToProps
92 | );
93 | const withData = withSubscribe(subscribe, unsubscribe);
94 |
95 | export default compose(
96 | withReducer,
97 | withData
98 | )(UserList);
99 |
--------------------------------------------------------------------------------
/src/pages/other/components/Track.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { Link } from "react-router-dom";
5 |
6 | export class Track extends PureComponent {
7 | renderEditButton(track, role, useId) {
8 | // invisible for any user
9 | if (role === "user") {
10 | return null;
11 | }
12 |
13 | if (role === 'creator' && track.author !== useId) {
14 | return null;
15 | }
16 |
17 | // invisible on any public tracks for any creators
18 | if (role === 'creator' && track.isPublic) {
19 | return null;
20 | }
21 |
22 | return (
23 |
27 | Edit
28 |
29 | );
30 | }
31 |
32 | renderReviewButton(track, role) {
33 | // invisible for any user
34 | if (role === "user") {
35 | return null;
36 | }
37 |
38 | if (role === "admin" && !track.isPublic) {
39 | return (
40 |
44 | Review Track
45 |
46 | );
47 | }
48 | return null;
49 | }
50 |
51 | render() {
52 | const { track, role, useId } = this.props;
53 | return (
54 |
55 |
56 | {this.renderEditButton(track, role, useId)}
57 | {this.renderReviewButton(track, role)}
58 |
59 |
60 |

61 |
62 |
{track.name}
63 |
{track.authorName}
64 |
{track.location}
65 |
66 | {track.latitude}
67 | {track.longitude}
68 |
69 |
{track.tags}
70 |
71 |
72 |
76 |
77 | );
78 | }
79 | }
80 |
81 | Track.propTypes = {
82 | track: PropTypes.object.isRequired,
83 | role: PropTypes.string,
84 | useId: PropTypes.string.isRequired
85 | };
86 |
87 | Track.defaultProps = {
88 | role: "user"
89 | };
90 |
--------------------------------------------------------------------------------
/src/pages/other/index.js:
--------------------------------------------------------------------------------
1 | import loadable from 'react-loadable';
2 |
3 | // Use react-loadable to make component-centric code splitting.
4 |
5 | export const ListPage = loadable({
6 | loader: () => import('./List'),
7 | loading: () => null
8 | });
9 |
10 | export const NewTrack = loadable({
11 | loader: () => import('./NewTrack'),
12 | loading: () => null
13 | });
14 |
15 | export const Profile = loadable({
16 | loader: () => import('./Profile'),
17 | loading: () => null
18 | });
19 |
20 | export const ReviewTrack = loadable({
21 | loader: () => import('./ReviewTrack'),
22 | loading: () => null
23 | });
24 |
25 | export const UserList = loadable({
26 | loader: () => import('./UserList'),
27 | loading: () => null
28 | });
29 |
30 | export const UserForm = loadable({
31 | loader: () => import('./UserForm'),
32 | loading: () => null
33 | });
34 |
--------------------------------------------------------------------------------
/src/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux-immutable';
2 |
3 | import { trackReducer } from './modules/track';
4 | import { authReducer } from './modules/auth';
5 | import { userReducer } from './modules/user';
6 |
7 | // Turns an object whose values are different reducing functions into a single reducing function you can pass to createStore
8 | const rootApp = combineReducers({
9 | track: trackReducer,
10 | auth: authReducer,
11 | user: userReducer
12 | });
13 |
14 | export default rootApp;
15 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/sagas.js:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects';
2 | import { authSaga } from './modules/auth';
3 | import { trackSagas } from './modules/track';
4 | import { userSagas } from './modules/user';
5 |
6 | /**
7 | * Creates an effect description that instructs the middleware to run multiple effects in parallel and wait for all of them to complete
8 | */
9 | export default function* rootSagas() {
10 | yield all([authSaga(), trackSagas(), userSagas()]);
11 | }
12 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create the store with dynamic reducers
3 | */
4 |
5 | import { createStore, applyMiddleware, compose } from 'redux';
6 | import createSagaMiddleware from 'redux-saga';
7 | import { fromJS } from 'immutable';
8 |
9 | import reducers from '../reducers';
10 | import sagas from '../sagas';
11 |
12 | // Creates a Redux middleware and connects the Sagas to the Redux Store
13 | const sagaMiddleware = createSagaMiddleware();
14 |
15 | export default function configureStore(initialState = {}) {
16 | // Create the store with middlewares
17 | const middlewares = [sagaMiddleware];
18 |
19 | const enhancers = [applyMiddleware(...middlewares)];
20 |
21 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose
22 | /* eslint-disable no-underscore-dangle */
23 | const composeEnhancers =
24 | process.env.NODE_ENV !== 'production' &&
25 | typeof window === 'object' &&
26 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
27 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ shouldHotReload: false })
28 | : compose;
29 |
30 | // Create a redux store that holds the complete state tree of the app.
31 | const store = createStore(
32 | reducers,
33 | // Deeply converts plain JS objects and arrays to immutable Maps and Lists
34 | fromJS(initialState),
35 | composeEnhancers(...enhancers)
36 | );
37 |
38 | // Extensions
39 | store.runSaga = sagaMiddleware.run(sagas);
40 |
41 | return store;
42 | }
43 |
--------------------------------------------------------------------------------