├── .eslintignore ├── .prettierignore ├── .husky └── pre-commit ├── .DS_Store ├── public ├── favicon.ico ├── images │ └── icons │ │ ├── launch.png │ │ ├── custom_icon.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png ├── firebase-messaging-sw.js ├── manifest.json └── index.html ├── src ├── constants │ ├── urls.js │ └── staticText.js ├── utils │ ├── history.js │ ├── interceptor.js │ ├── privateRoute.js │ ├── notifications.js │ └── formUtils │ │ ├── validator.js │ │ └── index.js ├── components │ └── Home │ │ ├── home.scss │ │ └── index.js ├── sagas │ ├── rootSaga.js │ ├── request.js │ └── authSagas.js ├── containers │ ├── Home │ │ └── homeContainer.js │ └── App │ │ └── index.js ├── actions │ └── Auth │ │ ├── actionTypes.js │ │ └── index.js ├── service │ └── api.js ├── common │ ├── Footer │ │ └── index.js │ ├── Layout │ │ └── index.js │ └── Header │ │ └── index.js ├── index.css ├── reducers │ ├── rootReducer.js │ └── authReducer.js ├── push-notification.js ├── store.js ├── index.js ├── assets │ └── images │ │ └── user.svg └── serviceWorker.js ├── test-env.js ├── jest-setup.js ├── babel.config.js ├── .pretiierrc ├── .eslintrc.json ├── .gitignore ├── jsconfig.json ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/.DS_Store -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/constants/urls.js: -------------------------------------------------------------------------------- 1 | const URLs = { 2 | // Auth API's 3 | GET_LIST: 'posts', 4 | }; 5 | 6 | export default URLs; 7 | -------------------------------------------------------------------------------- /src/utils/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | export default createBrowserHistory(); 4 | -------------------------------------------------------------------------------- /test-env.js: -------------------------------------------------------------------------------- 1 | global.React = require("react"); 2 | global.ReactDOM = require('react-dom'); 3 | 4 | module.exports = 'IMAGE_MOCK'; 5 | -------------------------------------------------------------------------------- /public/images/icons/launch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/launch.png -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /public/images/icons/custom_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/custom_icon.png -------------------------------------------------------------------------------- /public/images/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/images/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-144x144.png -------------------------------------------------------------------------------- /public/images/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-152x152.png -------------------------------------------------------------------------------- /public/images/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/images/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-384x384.png -------------------------------------------------------------------------------- /public/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /public/images/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-72x72.png -------------------------------------------------------------------------------- /public/images/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/react-redux-sagas-boilerplate/HEAD/public/images/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/components/Home/home.scss: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const StyledHeader = styled.div` 4 | flex-grow: 1;`; 5 | 6 | -------------------------------------------------------------------------------- /src/constants/staticText.js: -------------------------------------------------------------------------------- 1 | const Texts = { 2 | CHECK_API_CALL: 'Check api call', 3 | GET_NOTIFCATION: 'Get notification', 4 | }; 5 | 6 | export default Texts; 7 | -------------------------------------------------------------------------------- /src/sagas/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import authSagas from './authSagas'; 3 | 4 | export default function* rootSaga() { 5 | yield all([authSagas()]); 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@babel/plugin-transform-react-jsx', '@babel/plugin-syntax-dynamic-import'], 3 | presets: ['@babel/preset-env', '@babel/preset-react'], 4 | }; 5 | -------------------------------------------------------------------------------- /.pretiierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /src/containers/Home/homeContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from 'components/Home'; 3 | 4 | const HomeContainer = () => { 5 | return ; 6 | }; 7 | 8 | export default HomeContainer; 9 | -------------------------------------------------------------------------------- /src/actions/Auth/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const GET_LIST = 'GET_LIST'; 2 | export const GET_LIST_SUCCESS = 'GET_LIST_SUCCESS'; 3 | export const GET_LIST_FAILURE = 'GET_LIST_FAILURE'; 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/service/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default axios.create({ 4 | baseURL: 'https://jsonplaceholder.typicode.com/', 5 | withCredentials: false, 6 | crossDomain: true, 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | 'Access-Control-Allow-Origin': '*', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/interceptor.js: -------------------------------------------------------------------------------- 1 | import api from 'service/api'; 2 | 3 | export function interceptor() { 4 | const localToken = localStorage.getItem('token'); 5 | api.interceptors.request.use((config) => { 6 | if (localToken) { 7 | config.headers.Authorization = `JWT ${localToken}`; 8 | } 9 | return config; 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/actions/Auth/index.js: -------------------------------------------------------------------------------- 1 | import { GET_LIST, GET_LIST_SUCCESS, GET_LIST_FAILURE } from './actionTypes'; 2 | 3 | export const getList = () => ({ 4 | type: GET_LIST, 5 | }); 6 | 7 | export const getListSuccess = (data) => ({ 8 | type: GET_LIST_SUCCESS, 9 | payload: data, 10 | }); 11 | 12 | export const getListFailure = () => ({ 13 | type: GET_LIST_FAILURE, 14 | }); 15 | -------------------------------------------------------------------------------- /src/common/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = (props) => ( 4 | 13 | ); 14 | 15 | export default Footer; 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["plugin:react/recommended", "standard", "prettier"], 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["react"], 16 | "rules": { 17 | "node/no-callback-literal": "off", 18 | "camelcase": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import { interceptor } from 'utils/interceptor'; 4 | import HomeContainer from 'containers/Home/homeContainer'; 5 | 6 | export default function App() { 7 | interceptor(); 8 | return ( 9 | 10 | 11 | } /> 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | #lock files 15 | package-lock.json 16 | yarn.lock 17 | 18 | # misc 19 | .DS_Store 20 | .env 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | .eslintcache -------------------------------------------------------------------------------- /src/utils/privateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | // eslint-disable-next-line react/prop-types 5 | export function PrivateRoute({ component: Component, ...rest }) { 6 | return ( 7 | 10 | localStorage.getItem('token') ? ( 11 | 12 | ) : ( 13 | 14 | ) 15 | } 16 | /> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import authReducer from './authReducer'; 4 | import { reducer as formReducer } from 'redux-form'; 5 | 6 | const appReducer = combineReducers({ 7 | routing: routerReducer, 8 | auth: authReducer, 9 | form: formReducer, 10 | }); 11 | 12 | const rootReducer = (state, action) => { 13 | // console.log("RESET_ALL_DATA action", action) 14 | if (action.type === 'RESET_ALL_DATA') { 15 | state = { 16 | auth: state.auth, 17 | }; 18 | } 19 | return appReducer(state, action); 20 | }; 21 | 22 | export default rootReducer; 23 | -------------------------------------------------------------------------------- /src/push-notification.js: -------------------------------------------------------------------------------- 1 | import * as firebase from 'firebase'; 2 | 3 | export const initializeFirebase = () => { 4 | const config = { 5 | messagingSenderId: '510999633078', 6 | }; 7 | firebase.initializeApp(config); 8 | }; 9 | 10 | export const askForPermissionToReceiveNotifications = async () => { 11 | try { 12 | const messaging = firebase.messaging(); 13 | await messaging.requestPermission(); 14 | const token = await messaging.getToken(); 15 | console.log('token :', token); 16 | alert(token); 17 | localStorage.setItem('notification-token', token); 18 | 19 | return token; 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/sagas/request.js: -------------------------------------------------------------------------------- 1 | import api from 'service/api'; 2 | 3 | export const getRequest = async (requestUrl) => 4 | api 5 | .get(requestUrl) 6 | .then((resp) => resp) 7 | .catch((error) => error.response); 8 | 9 | export const postRequest = async (requestUrl, data) => 10 | api 11 | .post(requestUrl, data) 12 | .then((resp) => resp) 13 | .catch((error) => error.response); 14 | 15 | export const postFormDataRequest = async (requestUrl, data) => { 16 | const formData = new FormData(); 17 | Object.keys(data).map((item) => formData.set(item, data[item])); 18 | return api 19 | .post(requestUrl, formData) 20 | .then((resp) => resp) 21 | .catch((error) => error.response); 22 | }; 23 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "components/*": [ 6 | "src/components/*" 7 | ], 8 | "containers/*": [ 9 | "src/containers/*" 10 | ], 11 | "utils/*": [ 12 | "src/utils/*" 13 | ], 14 | "common/*": [ 15 | "src/common/*" 16 | ], 17 | "reducers/*": [ 18 | "src/reducers/*" 19 | ], 20 | "sagas/*": [ 21 | "src/sagas/*" 22 | ], 23 | "actions/*": [ 24 | "src/actions/*" 25 | ], 26 | "service/*": [ 27 | "src/service/*" 28 | ], 29 | "constants/*": [ 30 | "src/constants/*" 31 | ] 32 | } 33 | }, 34 | "include": [ 35 | "src" 36 | ] 37 | } -------------------------------------------------------------------------------- /public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/5.5.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/5.5.0/firebase-messaging.js'); 3 | firebase.initializeApp({ 4 | messagingSenderId: "510999633078" 5 | }); 6 | const messaging = firebase.messaging(); 7 | // messaging.usePublicVapidKey("BEsBewuGG1j0DvOr65lpGnMXHGWwqwvrLCn5VAIZ0M6v9EAGX0EfNgQ7AGDrpXfFZIt8IhPUJteObE3Gnb9XL9s"); 8 | 9 | // if ('serviceWorker' in navigator) { 10 | // navigator.serviceWorker.register('../firebase-messaging-sw.js') 11 | // .then(function(registration) { 12 | // console.log('Registration successful, scope is:', registration.scope); 13 | // }).catch(function(err) { 14 | // console.log('Service worker registration failed, error:', err); 15 | // }); 16 | // } -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | import persistState from 'redux-localstorage'; 4 | import rootReducer from 'reducers/rootReducer'; 5 | import rootSagas from 'sagas/rootSaga'; 6 | 7 | // Create sagas middleware 8 | const sagaMiddleware = createSagaMiddleware(); 9 | const composeEnhancers = 10 | (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && 11 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 12 | trace: true, 13 | traceLimit: 100, 14 | })) || 15 | compose; 16 | 17 | export default function configureStore() { 18 | const store = createStore( 19 | rootReducer, 20 | composeEnhancers(applyMiddleware(sagaMiddleware), persistState('auth')) 21 | ); 22 | // Running sagas 23 | sagaMiddleware.run(rootSagas); 24 | return store; 25 | } 26 | -------------------------------------------------------------------------------- /src/sagas/authSagas.js: -------------------------------------------------------------------------------- 1 | import { all, call, put, takeLatest } from 'redux-saga/effects'; 2 | import { GET_LIST } from 'actions/Auth/actionTypes'; 3 | import { getListSuccess, getListFailure } from 'actions/Auth'; 4 | import { getRequest } from './request'; 5 | import { pushNotification } from 'utils/notifications'; 6 | import URls from 'constants/urls'; 7 | 8 | function* getJsonData(action) { 9 | try { 10 | const response = yield call(getRequest, URls.GET_LIST); 11 | if (response.data) { 12 | pushNotification('Get data success', 'success', 'TOP_CENTER', 1000); 13 | yield put(getListSuccess(response.data)); 14 | } 15 | } catch (error) { 16 | pushNotification('Get data failure', 'error', 'TOP_CENTER', 1000); 17 | yield put(getListFailure()); 18 | } 19 | } 20 | 21 | function* watchGetRequest() { 22 | yield takeLatest(GET_LIST, getJsonData); 23 | } 24 | 25 | export default function* sagas() { 26 | yield all([watchGetRequest()]); 27 | } 28 | -------------------------------------------------------------------------------- /src/common/Layout/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Header from '../Header'; 4 | import Footer from '../Footer'; 5 | 6 | const Layout = (props) => { 7 | const [isScrolled, setScroll] = useState(window.scrollY > 30); 8 | useEffect(() => { 9 | document.addEventListener('scroll', handleScroll); 10 | return () => { 11 | document.removeEventListener('scroll', handleScroll); 12 | }; 13 | }, []); 14 | 15 | const handleScroll = () => { 16 | const Y = window.scrollY; 17 | setScroll(Y > 30); 18 | }; 19 | 20 | return ( 21 | 22 |
23 | {props.children} 24 |