├── public ├── asset │ └── css │ │ └── react.css └── index.html ├── src ├── reduxStores │ ├── configureStore.js │ ├── reducers │ │ ├── allReducers.js │ │ └── videoReducers.js │ ├── actions │ │ └── allActions.js │ ├── configureStore.dev.js │ └── configureStore.prod.js ├── constants │ ├── config.js │ └── axiosApi.js ├── components │ ├── footer │ │ ├── RelatedFiles.js │ │ └── FooterPresent.js │ ├── reduxApp │ │ ├── ReduxRelatedFiles.js │ │ ├── VideoPreview.js │ │ ├── VideoPlay.js │ │ ├── VideoPlayButtons.js │ │ ├── ReduxApp.js │ │ ├── VideoSearchResults.js │ │ ├── ReduxPlayedVideos.js │ │ ├── ReduxLikedVideos.js │ │ ├── ReduxAllVideos.js │ │ └── VideoSearchForm.js │ ├── websiteApp │ │ ├── WebsiteAppFileContent.js │ │ ├── WebsiteAppResults.js │ │ ├── WebsiteAppPresent.js │ │ └── WebsiteApp.js │ ├── axiosApp │ │ ├── AxiosAppResults.js │ │ ├── AxiosAppPresent.js │ │ └── AxiosApp.js │ ├── basicApp │ │ ├── BasicAppPresent.js │ │ └── BasicApp.js │ ├── header │ │ └── HeaderPresent.js │ └── hookApp │ │ └── BasicHooks.js ├── middlewares │ ├── sagaApiServices.js │ └── saga.js └── reactApp.js ├── .babelrc ├── .styleci.yml ├── .gitignore ├── doc └── websiteApp │ ├── 0.websiteConfig.json │ ├── 3.Code-examples.html │ ├── 1.What-included-in-this-starter.html │ └── 2.How-to-install-this-starter.html ├── .editorconfig ├── .eslintrc.json ├── webpack.config.js ├── package.json └── readme.md /public/asset/css/react.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: azure; 3 | color: rgb(20, 20, 20); 4 | } 5 | 6 | .redux-buttons a.active { 7 | color: yellow !important; 8 | } 9 | -------------------------------------------------------------------------------- /src/reduxStores/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === "production") { 2 | module.exports = require("./configureStore.prod"); 3 | } else { 4 | module.exports = require("./configureStore.prod"); 5 | } 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "useBuiltIns": false, 5 | }], 6 | "@babel/react" 7 | ], 8 | "plugins": [ 9 | "@babel/plugin-proposal-class-properties", 10 | "@babel/plugin-transform-runtime" 11 | ] 12 | } -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | risky: false 3 | enabled: 4 | - align_double_arrow 5 | - align_equals 6 | disabled: 7 | - unused_use 8 | - unalign_equals 9 | finder: 10 | exclude: 11 | - modules 12 | - node_modules 13 | - storage 14 | - vendor 15 | name: "*.php" 16 | not-name: "*.blade.php" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* -------------------------------------------------------------------------------- /src/reduxStores/reducers/allReducers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from "redux"; 4 | import { connectRouter } from "connected-react-router"; 5 | 6 | import videoReducers from "./videoReducers"; 7 | 8 | export default (storeHistory) => 9 | combineReducers({ 10 | router: connectRouter(storeHistory), 11 | videos: videoReducers, 12 | }); 13 | -------------------------------------------------------------------------------- /doc/websiteApp/0.websiteConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteName": "Simple website using Github as backend", 3 | "siteDescription": "Want built a simple ReactJs website? Why not use Github to maintain all the contents and use it as the backend API or database. This example list all .html files from your public Github repository through Github RESTful API", 4 | "pageTitleClass": "text-info" 5 | } 6 | -------------------------------------------------------------------------------- /doc/websiteApp/3.Code-examples.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | ReactJs code examples: 4 |

5 | 6 | Online DEMO https://alexstack.github.io/reactStarter/public/ 7 | Basic ReactJS example: using React state and components only 8 | Axios RESTful API search example: A form using axios to implement a RESTful API search 9 | Redux Saga reducer example: 10 | Hooks example: 11 |
12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml,js,json,css,scss,feature,eslintrc}] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [composer.json] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /src/constants/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | export const REACTPATH = { 3 | Basic: "/basic1", 4 | Axios: "/axios2", 5 | Redux: "/redux3", 6 | Website: "/document", 7 | BasicHooks: "/basicHooks", 8 | CustomHooks: "customHooks", 9 | }; 10 | 11 | export const REDUXPATH = { 12 | LikedVideos: "/LikedVideos2", 13 | PlayedVideos: "/PlayedVideos5", 14 | SearchVideos: "/SearchVideos3", 15 | }; 16 | 17 | export const myLog = (var1, var2, var3) => { 18 | console.log(var1, var2, var3); 19 | }; 20 | 21 | export const lastPath = (prefix = "/") => 22 | prefix + document.location.pathname.split("/").slice(-1)[0]; 23 | -------------------------------------------------------------------------------- /doc/websiteApp/1.What-included-in-this-starter.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | What included? 4 | 5 |

Lots of React required packages, such as react-router-dom, axios, redux, saga, hook, webpack ... You do not 6 | need 7 | install 8 | them again, use them directly in your code. 9 | Using webpack to export the compiled js file, tidy and clean and flexible. 10 | npm run dev will automatically open a local dev demo page which can detect js file changes and show the result 11 | immediately. 12 | There are some code examples for demo and reuse purpose. We can easy change them to fit our requirements. 13 |
14 | -------------------------------------------------------------------------------- /src/components/footer/RelatedFiles.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const RelatedFiles = (props) => { 5 | return ( 6 |
7 |
8 |
9 | Related js files for this example 10 |
11 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | // RelatedFiles.propTypes = {}; 24 | export default RelatedFiles; 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parser": "babel-eslint", 8 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 2018, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react", 22 | "prettier" 23 | ], 24 | "rules": { 25 | "strict": 0, 26 | "valid-jsdoc": 2, 27 | "react/jsx-uses-react": 2, 28 | "react/jsx-uses-vars": 2, 29 | "react/react-in-jsx-scope": 2, 30 | "react/jsx-filename-extension": [1, { 31 | "extensions": [".js", ".jsx"] 32 | }] 33 | } 34 | } -------------------------------------------------------------------------------- /src/components/reduxApp/ReduxRelatedFiles.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React, { Component } from "react"; 3 | import RelatedFiles from "../footer/RelatedFiles"; 4 | 5 | const ReduxRelatedFiles = () => ( 6 |
7 | 8 |
  • 9 | src/components/reduxApp/ReduxApp.js --- React-Redux main entry point 10 |
  • 11 |
  • 12 | src/components/reduxApp/*.js --- React Components 13 |
  • 14 |
  • 15 | src/reduxStores/*.js --- Redux configuration & actions & reducers 16 |
  • 17 |
  • 18 | src/middlewares/*.js --- Redux middlewares: saga, api services 19 |
  • 20 |
    21 |
    22 | ); 23 | export default ReduxRelatedFiles; 24 | -------------------------------------------------------------------------------- /src/components/footer/FooterPresent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const FooterPresent = () => { 5 | return ( 6 |
    7 |
    8 | Page loaded at {new Date().toLocaleDateString()} - 9 | {new Date().toLocaleTimeString()} 10 |
    11 | 17 | Github 18 | 19 |
    20 |
    21 |
    22 | ); 23 | }; 24 | 25 | FooterPresent.propTypes = {}; 26 | export default FooterPresent; 27 | -------------------------------------------------------------------------------- /src/reduxStores/actions/allActions.js: -------------------------------------------------------------------------------- 1 | import { createActions } from "reduxsauce"; 2 | 3 | /* ------------- Types and Action Creators ------------- */ 4 | 5 | const { Types, Creators } = createActions({ 6 | listItemRequest: ["keyword", "nextPageToken"], 7 | listItemSuccess: ["allData", "keyword", "nextPageToken"], 8 | listItemFailure: ["error"], 9 | 10 | viewItemRequest: ["item"], 11 | viewItemSuccess: ["backendData", "item"], 12 | viewItemFailure: ["error"], 13 | 14 | likeItemRequest: ["item"], 15 | likeItemSuccess: ["backendData", "item"], 16 | likeItemFailure: ["error"], 17 | 18 | unlikeItemRequest: ["item"], 19 | unlikeItemSuccess: ["backendData", "item"], 20 | unlikeItemFailure: ["error"], 21 | 22 | deleteItemRequest: ["id"], 23 | deleteItemSuccess: ["id", "page"], 24 | deleteItemFailure: ["error"], 25 | 26 | setFilterKeyRequest: ["value"], 27 | setFilterKeySuccess: ["value"], 28 | setFilterKeyFailure: ["error"], 29 | 30 | showLoading: ["loading", "nextPageToken"], 31 | }); 32 | 33 | export const actionTypes = Types; 34 | export const taskTypes = Types; 35 | export default Creators; 36 | -------------------------------------------------------------------------------- /src/components/reduxApp/VideoPreview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import VideoPlayButtons from "./VideoPlayButtons"; 4 | 5 | const VideoPreview = ({ item, handleFieldChange, index, wasLiked }) => { 6 | return ( 7 |
    8 | 16 | 17 |
    18 |
    19 | {index + 1} 20 | {item.snippet.title} 21 |
    22 | 28 |
    29 |
    30 | ); 31 | }; 32 | 33 | VideoPreview.propTypes = { 34 | item: PropTypes.object, 35 | handleFieldChange: PropTypes.func, 36 | }; 37 | 38 | export default VideoPreview; 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 4 | 5 | module.exports = { 6 | // mode: "development", 7 | entry: "./src/reactApp.js", 8 | output: { 9 | path: path.resolve("public/asset/js"), 10 | filename: "reactApp.js", 11 | }, 12 | // define babel loader 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | loader: "babel-loader", 18 | // use: ["babel-loader", "style-loader", "css-loader"], 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | devtool: "inline-source-map", 24 | devServer: { 25 | historyApiFallback: true, 26 | // contentBase: "public", 27 | compress: false, 28 | // inline: true, 29 | // progress: true, 30 | hot: true, 31 | port: 9999, 32 | }, 33 | plugins: [ 34 | new CleanWebpackPlugin(), 35 | new HtmlWebpackPlugin({ 36 | title: "Hot Module Replacement", 37 | template: "public/index.html", 38 | }), 39 | ], 40 | resolve: { 41 | extensions: [".vue", ".js", ".css", "jsx"], 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /doc/websiteApp/2.How-to-install-this-starter.html: -------------------------------------------------------------------------------- 1 |
    2 |

    How to install

    3 | 4 | 31 |
    32 | -------------------------------------------------------------------------------- /src/components/reduxApp/VideoPlay.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import VideoPlayButtons from "./VideoPlayButtons"; 4 | 5 | const VideoPlay = ({ item, handleFieldChange, index, wasLiked }) => { 6 | return ( 7 |
    8 |
    9 | 16 |
    17 | 18 |
    19 |
    20 | {index + 1} 21 | {item.snippet.title} 22 |
    23 | 24 | 30 |
    31 |
    32 | ); 33 | }; 34 | 35 | VideoPlay.propTypes = { 36 | item: PropTypes.object, 37 | handleFieldChange: PropTypes.func, 38 | }; 39 | 40 | export default VideoPlay; 41 | -------------------------------------------------------------------------------- /src/middlewares/sagaApiServices.js: -------------------------------------------------------------------------------- 1 | import { youtubeApi, backendApi } from "../constants/axiosApi"; 2 | 3 | const listItem = (keyword, nextPageToken) => { 4 | return youtubeApi.get(`/search`, { 5 | params: { 6 | part: "snippet", 7 | order: "rating", // date, viewCount 8 | q: keyword, 9 | type: "video", 10 | videoDefinition: "high", 11 | maxResults: 10, 12 | pageToken: nextPageToken, 13 | }, 14 | }); 15 | }; 16 | 17 | const viewItem = (item) => { 18 | return true; 19 | }; 20 | 21 | const likeItem = (item) => { 22 | return backendApi.post(`/videos`, { 23 | method: "POST", 24 | params: { 25 | videoId: item.id.videoId, 26 | userId: 1, 27 | }, 28 | }); 29 | }; 30 | 31 | const unlikeItem = (item) => { 32 | return backendApi.delete(`/videos/${item.id.videoId}`, { 33 | method: "DELETE", 34 | params: { 35 | videoId: item.id.videoId, 36 | userId: 1, 37 | }, 38 | }); 39 | }; 40 | 41 | const deleteItem = (id) => { 42 | return backendApi.delete(`/videos/${id}`, { 43 | method: "DELETE", 44 | params: { 45 | videoId: id, 46 | userId: 1, 47 | }, 48 | }); 49 | }; 50 | 51 | const ApiMethods = { 52 | listItem, 53 | viewItem, 54 | likeItem, 55 | unlikeItem, 56 | deleteItem, 57 | }; 58 | 59 | export default ApiMethods; 60 | -------------------------------------------------------------------------------- /src/components/websiteApp/WebsiteAppFileContent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const WebsiteAppFileContent = ({ 5 | total, 6 | fileContent, 7 | handleFieldChange, 8 | loading, 9 | }) => { 10 | return ( 11 |
    12 | 21 | 22 |
    28 |
    29 |
    30 | 39 |
    40 |
    41 |
    42 | ); 43 | }; 44 | 45 | WebsiteAppFileContent.propTypes = { 46 | total: PropTypes.number, 47 | fileContent: PropTypes.string, 48 | handleFieldChange: PropTypes.func, 49 | loading: PropTypes.bool, 50 | }; 51 | 52 | export default WebsiteAppFileContent; 53 | -------------------------------------------------------------------------------- /src/reactApp.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 4 | import BasicApp from "./components/basicApp/BasicApp"; 5 | import AxiosApp from "./components/axiosApp/AxiosApp"; 6 | import WebsiteApp from "./components/websiteApp/WebsiteApp"; 7 | import HeaderPresent from "./components/header/HeaderPresent"; 8 | import FooterPresent from "./components/footer/FooterPresent"; 9 | import ReduxApp from "./components/reduxApp/ReduxApp"; 10 | import BasicHooks from "./components/hookApp/BasicHooks"; 11 | import { REACTPATH } from "./constants/config"; 12 | 13 | export default class ReactApp extends React.Component { 14 | render() { 15 | return ( 16 | 17 | 18 |
    19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
    28 | 29 |
    30 | ); 31 | } 32 | } 33 | 34 | if (document.getElementById("react-js-basic-app")) { 35 | ReactDOM.render(, document.getElementById("react-js-basic-app")); 36 | } 37 | -------------------------------------------------------------------------------- /src/reduxStores/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history"; 2 | import { createStore, applyMiddleware, compose } from "redux"; 3 | import { createLogger } from "redux-logger"; 4 | import createRootReducer from "../reduxStores/reducers/allReducers"; 5 | import { routerMiddleware } from "connected-react-router"; 6 | import { persistStore, persistReducer } from "redux-persist"; 7 | import storage from "redux-persist/lib/storage"; 8 | import { REACTPATH } from "../constants/config"; 9 | 10 | export const history = createBrowserHistory({ basename: REACTPATH.Redux }); 11 | 12 | const initialState = { 13 | //router: null, 14 | // videos: null, 15 | }; 16 | 17 | const persistConfig = { 18 | key: "root", 19 | storage, 20 | /** 21 | * Blacklist state that we do not need/want to persist 22 | */ 23 | blacklist: ["router"], 24 | }; 25 | 26 | const configureStore = (sagaMiddleware, storeHistory) => { 27 | //const sagaMiddleware = createSagaMiddleware() 28 | const reducers = createRootReducer(storeHistory); 29 | // console.log(reducers.router); 30 | const store = createStore( 31 | persistReducer(persistConfig, reducers), // root reducer with router state 32 | initialState, 33 | compose( 34 | applyMiddleware( 35 | routerMiddleware(storeHistory), // for dispatching history actions 36 | sagaMiddleware, 37 | createLogger() 38 | ) 39 | // window.__REDUX_DEVTOOLS_EXTENSION__ && 40 | // window.__REDUX_DEVTOOLS_EXTENSION__() 41 | //DevTools.instrument() 42 | ) 43 | ); 44 | //sagaMiddleware.run(rootSaga) 45 | const persistor = persistStore(store); 46 | 47 | return { store, persistor }; 48 | }; 49 | 50 | export default configureStore; 51 | -------------------------------------------------------------------------------- /src/reduxStores/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history"; 2 | import { createStore, applyMiddleware, compose } from "redux"; 3 | //import { createLogger } from "redux-logger"; 4 | import createRootReducer from "./reducers/allReducers"; 5 | import { routerMiddleware } from "connected-react-router"; 6 | import { persistStore, persistReducer } from "redux-persist"; 7 | import storage from "redux-persist/lib/storage"; 8 | import { REACTPATH } from "../constants/config"; 9 | 10 | export const history = createBrowserHistory({ basename: REACTPATH.Redux }); 11 | 12 | const initialState = { 13 | //router: null, 14 | assignedTasks: [], 15 | login: {}, 16 | myOwnTasks: {}, 17 | }; 18 | 19 | const persistConfig = { 20 | key: "root", 21 | storage, 22 | /** 23 | * Blacklist state that we do not need/want to persist 24 | */ 25 | blacklist: ["router"], 26 | }; 27 | 28 | const configureStore = (sagaMiddleware, storeHistory) => { 29 | //const sagaMiddleware = createSagaMiddleware() 30 | const reducers = createRootReducer(storeHistory); 31 | //console.log(reducers.router); 32 | const store = createStore( 33 | persistReducer(persistConfig, reducers), // root reducer with router state 34 | initialState, 35 | compose( 36 | applyMiddleware( 37 | routerMiddleware(storeHistory), // for dispatching history actions 38 | sagaMiddleware 39 | //createLogger() 40 | ) 41 | // window.__REDUX_DEVTOOLS_EXTENSION__ && 42 | // window.__REDUX_DEVTOOLS_EXTENSION__() 43 | //DevTools.instrument() 44 | ) 45 | ); 46 | //sagaMiddleware.run(rootSaga) 47 | const persistor = persistStore(store); 48 | 49 | return { store, persistor }; 50 | }; 51 | 52 | export default configureStore; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hook-redux-starter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "dependencies": { 7 | "axios": "^0.21.2", 8 | "babel-preset-env": "^1.7.0", 9 | "connected-react-router": "^6.8.0", 10 | "js-base64": "^2.6.4", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-hot-loader": "^4.13.0", 14 | "react-redux": "^7.2.1", 15 | "react-router-dom": "^5.2.0", 16 | "redux": "^4.0.5", 17 | "redux-logger": "^3.0.6", 18 | "redux-persist": "^6.0.0", 19 | "redux-saga": "^1.1.3", 20 | "reduxsauce": "^1.2.0", 21 | "webpack-cli": "^4.9.1", 22 | "webpack-dev-server": "^4.6.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.11.6", 26 | "@babel/plugin-transform-runtime": "^7.11.5", 27 | "@babel/preset-env": "^7.11.5", 28 | "@babel/preset-react": "^7.10.4", 29 | "babel-core": "^6.26.3", 30 | "babel-loader": "^8.1.0", 31 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 32 | "babel-plugin-transform-runtime": "^6.23.0", 33 | "babel-preset-es2015": "^6.24.1", 34 | "babel-preset-react": "^6.24.1", 35 | "babel-runtime": "^6.26.0", 36 | "clean-webpack-plugin": "^3.0.0", 37 | "css-loader": "^5.2.6", 38 | "html-webpack-plugin": "^4.5.0", 39 | "mini-css-extract-plugin": "^0.9.0", 40 | "node-forge": ">=0.10.0", 41 | "style-loader": "^1.2.1", 42 | "webpack": "^5.65.0" 43 | }, 44 | "scripts": { 45 | "test": "echo \"Error: no test specified\" && exit 1", 46 | "dev": "webpack-dev-server --open --hot --mode development", 47 | "watch": "webpack --watch", 48 | "build": "webpack --mode production" 49 | }, 50 | "author": "", 51 | "license": "ISC" 52 | } 53 | -------------------------------------------------------------------------------- /src/components/axiosApp/AxiosAppResults.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const AxiosAppResults = ({ 5 | total, 6 | searchResults, 7 | handleFieldChange, 8 | loading, 9 | }) => { 10 | return ( 11 | <> 12 | {searchResults.length > 0 && 13 | searchResults.map((item, index) => ( 14 |
    15 |
    16 | {index + 1}{" "} 17 | 18 | {item.name} 19 | 20 |
    21 | 22 | {item.downloads} 23 |
    24 |
    25 |
    {item.description}
    26 |
    27 | ))} 28 | {total > 0 && total > searchResults.length && ( 29 | 38 | )} 39 | {/* Show additional information, pages, total */} 40 | {total === 0 &&
    No result
    } 41 | {total > 0 && ( 42 |
    43 | Total results: {total} - Displaying {searchResults.length} of {total} 44 |
    45 | )} 46 | 47 | ); 48 | }; 49 | 50 | AxiosAppResults.propTypes = { 51 | total: PropTypes.number, 52 | searchResults: PropTypes.array, 53 | handleFieldChange: PropTypes.func, 54 | loading: PropTypes.bool, 55 | }; 56 | 57 | export default AxiosAppResults; 58 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React-Hook-Redux-Saga-Webpack-Starter 2 | 3 | - Create a new ReactJs project without use Create-React-App. Popular packages such as Axios,Redux, Saga, Hooks are ready to use immediately. Compile codes by webpack and can hot reload code changes - it will automatically refresh the dev page. save your time! 4 | 5 | ## What included? 6 | 7 | - Lots of React required packages, such as react-router-dom, axios, redux, saga, hook, webpack ... You do not need install them again, use them directly in your code. 8 | - Using webpack to export the compiled js file, tidy and clean and flexible. 9 | - **npm run dev** will automatically open a local dev demo page which can detect js file changes and show the result immediately. 10 | - There are some code examples for demo and reuse purpose. We can easy change them to fit our requirements. 11 | 12 | ## ReactJs code examples: 13 | 14 | - **Online DEMO** https://alexstack.github.io/reactStarter/ 15 | - Basic ReactJS example: using React state and components only 16 | - Axios RESTful API search example: A form using axios to implement a RESTful API search 17 | - Redux Saga reducer example: 18 | - Hooks example: 19 | 20 | ## How to use? 21 | 22 | - git clone https://github.com/AlexStack/React-Hook-Redux-Saga-Webpack-Starter.git reactApp 23 | - cd reactApp 24 | - **npm install** --- This will install all related react packages in 3 minutes. You need install npm first if you never use npm before. The npm can be [download here](https://nodejs.org/en/download/) 25 | - **npm run dev** --- Now you will see a local ReactJs demo page opened in your browser: http://localhost:9999/ - this dev demo page will automatically refresh when it detect any react js code changes 26 | - **npm run build** --- This will build the production js file into public/js/reactApp.js 27 | - Start from scratch : Edit or delete all files in the src folder. The main entry point file is src/reactApp.js 28 | -------------------------------------------------------------------------------- /src/components/reduxApp/VideoPlayButtons.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { REDUXPATH, lastPath } from "../../constants/config"; 5 | 6 | const VideoPlayButtons = ({ 7 | isPlaying, 8 | wasLiked, 9 | handleFieldChange, 10 | index, 11 | }) => { 12 | return ( 13 |
    14 | {!isPlaying && ( 15 | 23 | )} 24 | 25 | {wasLiked ? ( 26 | <> 27 | {lastPath() != REDUXPATH.LikedVideos && ( 28 | 33 | All Liked Videos 34 | 35 | )} 36 | 37 | 45 | 46 | ) : ( 47 | 55 | )} 56 |
    57 | ); 58 | }; 59 | 60 | VideoPlayButtons.propTypes = { 61 | isPlaying: PropTypes.bool, 62 | wasLiked: PropTypes.bool, 63 | handleFieldChange: PropTypes.func, 64 | index: PropTypes.number, 65 | }; 66 | 67 | export default VideoPlayButtons; 68 | -------------------------------------------------------------------------------- /src/components/reduxApp/ReduxApp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import { Provider } from "react-redux"; 6 | import { Route, Switch } from "react-router-dom"; 7 | import { ConnectedRouter } from "connected-react-router"; 8 | import { PersistGate } from "redux-persist/integration/react"; 9 | import configureStore, { 10 | history as storeHistory, 11 | } from "../../reduxStores/configureStore"; 12 | import rootSaga from "../../middlewares/saga"; 13 | import createSagaMiddleware from "redux-saga"; 14 | 15 | import ReduxAllVideos from "./ReduxAllVideos"; 16 | import ReduxLikedVideos from "./ReduxLikedVideos"; 17 | import ReduxPlayedVideos from "./ReduxPlayedVideos"; 18 | import ReduxRelatedFiles from "./ReduxRelatedFiles"; 19 | import { REDUXPATH } from "../../constants/config"; 20 | 21 | const sagaMiddleware = createSagaMiddleware(); 22 | const { store, persistor } = configureStore(sagaMiddleware, storeHistory); 23 | sagaMiddleware.run(rootSaga); 24 | 25 | export default class ReduxApp extends React.Component { 26 | render() { 27 | return ( 28 | 29 | 30 | 31 | {/* */} 32 | 33 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {/* */} 48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/websiteApp/WebsiteAppResults.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const WWebsiteAppResults = ({ 5 | total, 6 | searchResults, 7 | handleFieldChange, 8 | loading, 9 | }) => { 10 | return ( 11 | <> 12 | {searchResults.length > 0 && 13 | searchResults.map((item, index) => ( 14 |
    15 |
    16 | {index + 1}{" "} 17 | 28 |
    29 |
    30 | ))} 31 | {total > 0 && total > searchResults.length && ( 32 | 41 | )} 42 | 43 | {/* Show additional information, pages, total */} 44 | {total === 0 &&
    No result
    } 45 | {total > 0 && searchResults.length != total && ( 46 |
    47 | Found {searchResults.length} result 48 | {searchResults.length > 1 && <>s} in {total} 49 |
    50 | )} 51 | 52 | ); 53 | }; 54 | 55 | WWebsiteAppResults.propTypes = { 56 | total: PropTypes.number, 57 | searchResults: PropTypes.array, 58 | handleFieldChange: PropTypes.func, 59 | loading: PropTypes.bool, 60 | }; 61 | 62 | export default WWebsiteAppResults; 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AlexStack ReactJs Starter 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 |
    24 |
    25 |
    26 |
    27 | 28 | Loading the react example... 29 |
    30 |
    31 |
    32 |
    33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/basicApp/BasicAppPresent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { REACTPATH, lastPath } from "../../constants/config"; 4 | 5 | const BasicAppPresent = ({ 6 | clickTimes, 7 | keyPressTimes, 8 | pressedKey, 9 | secretKey, 10 | handleFieldChange, 11 | }) => { 12 | return ( 13 |
    14 |
    15 |

    Secret key guess game!

    16 |
    17 | {REACTPATH.BasicHooks !== document.location.pathname ? ( 18 | 19 | Basic ReactJS example: using React this.state and components only 20 | 21 | ) : ( 22 | 23 | Basic Hooks example: DO NOT 24 | USE this.state and componentDidMount 25 | 26 | )} 27 |
    28 |
    29 | {secretKey 30 | ? "Secret key pressed, Game start now!" 31 | : "First person: Press a secret key first!"} 32 |
    33 | 34 |
    35 | Second person: Try to guess what is the secret key by press other keys 36 |
    37 |
    38 | {secretKey === pressedKey && ( 39 | <> 40 |

    Congratulation, you won!

    41 |
    42 | You've tried {keyPressTimes} times, your score is{" "} 43 | {100 - (keyPressTimes - 1) * 2}/100 44 |
    45 | Press Esc key to start the game again! or 46 |
    47 | 53 | 54 | )} 55 | {secretKey && secretKey !== pressedKey && ( 56 |
    57 | Pressed Key: {pressedKey} 58 | Key Press Times: {keyPressTimes} 59 | 65 |
    66 | )} 67 |
    68 | ); 69 | }; 70 | 71 | BasicAppPresent.propTypes = { 72 | clickTimes: PropTypes.number, 73 | keyPressTimes: PropTypes.number, 74 | pressedKey: PropTypes.string, 75 | secretKey: PropTypes.string, 76 | handleFieldChange: PropTypes.func, 77 | }; 78 | export default BasicAppPresent; 79 | -------------------------------------------------------------------------------- /src/components/reduxApp/VideoSearchResults.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import VideoPreview from "./VideoPreview"; 4 | import VideoPlay from "./VideoPlay"; 5 | 6 | const VideoSearchResults = ({ 7 | total, 8 | searchResults, 9 | likedItems, 10 | handleFieldChange, 11 | loading, 12 | playVideoId, 13 | }) => { 14 | return ( 15 |
    16 | {searchResults.length > 0 && 17 | searchResults.map((item, index) => { 18 | const likedItem = likedItems.find( 19 | (obj) => obj.id.videoId == item.id.videoId 20 | ); 21 | const wasLiked = likedItem ? true : false; 22 | return ( 23 |
    24 | {playVideoId !== item.id.videoId ? ( 25 | 31 | ) : ( 32 | 38 | )} 39 |
    40 | ); 41 | })} 42 | 43 | {total > 0 && total > searchResults.length && searchResults.length > 0 && ( 44 |
    45 | 54 |
    55 | )} 56 | {/* Show additional information, pages, total */} 57 | {total === 0 && ( 58 |
    59 | No result 60 |
    61 | )} 62 | {total > 0 && ( 63 |
    64 | Displaying{" "} 65 | {searchResults.length} of {total} Videos 66 |
    67 | )} 68 |
    69 | ); 70 | }; 71 | 72 | VideoSearchResults.propTypes = { 73 | total: PropTypes.number, 74 | searchResults: PropTypes.array, 75 | likedItems: PropTypes.array, 76 | handleFieldChange: PropTypes.func, 77 | loading: PropTypes.bool, 78 | playVideoId: PropTypes.string, 79 | }; 80 | 81 | export default VideoSearchResults; 82 | -------------------------------------------------------------------------------- /src/components/header/HeaderPresent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink, Link } from "react-router-dom"; 3 | import { REACTPATH, REDUXPATH } from "../../constants/config"; 4 | 5 | const hideNavbar = (action) => { 6 | document.getElementById("collapsibleNavbar").classList.remove("show"); 7 | }; 8 | 9 | const HeaderPresent = () => { 10 | return ( 11 |
    12 |

    ReactJS Starter

    13 | 81 |
    82 | ); 83 | }; 84 | 85 | export default HeaderPresent; 86 | -------------------------------------------------------------------------------- /src/components/basicApp/BasicApp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React, { Component } from "react"; 3 | import PropTypes from "prop-types"; 4 | import BasicAppPresent from "./BasicAppPresent"; 5 | import RelatedFiles from "../footer/RelatedFiles"; 6 | 7 | export default class BasicApp extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | clickTimes: 0, 13 | keyPressTimes: 0, 14 | pressedKey: "null", 15 | secretKey: null, 16 | }; 17 | 18 | this.handleFieldChange = this.handleFieldChange.bind(this); 19 | this.handleKeyPress = this.handleKeyPress.bind(this); 20 | } 21 | 22 | componentDidMount() { 23 | console.log("componentDidMount", this.state); 24 | document.addEventListener("keydown", this.handleKeyPress, false); 25 | } 26 | 27 | componentDidUpdate() { 28 | console.log("componentDidUpdate", this.state); 29 | } 30 | 31 | handleFieldChange = (event) => { 32 | this.setState({ 33 | clickTimes: this.state.clickTimes + 1, 34 | keyPressTimes: 0, 35 | pressedKey: "null", 36 | secretKey: null, 37 | }); 38 | }; 39 | 40 | handleKeyPress = (event) => { 41 | if (this.state.secretKey === null) { 42 | this.setState({ 43 | secretKey: event.key, 44 | }); 45 | } else if (event.key === "Escape") { 46 | this.setState({ 47 | clickTimes: 0, 48 | keyPressTimes: 0, 49 | pressedKey: "null", 50 | secretKey: null, 51 | }); 52 | } else if (this.state.pressedKey === this.state.secretKey) { 53 | // do nothing 54 | } else { 55 | this.setState({ 56 | pressedKey: event.key, 57 | keyPressTimes: this.state.keyPressTimes + 1, 58 | }); 59 | } 60 | 61 | console.log("key pressed event:", event); 62 | }; 63 | 64 | render() { 65 | return ( 66 |
    67 | 74 | 75 | 76 |
  • 77 | src/reactApp.js --- React main entry point 78 |
  • 79 |
  • 80 | src/components/BasicApp/*.js --- React Components 81 |
  • 82 |
  • 83 | src/components/header/*.js --- Reusable Header Components 84 |
  • 85 |
  • 86 | src/components/footer/*.js --- Reusable Footer Components 87 |
  • 88 |
    89 |
    90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/middlewares/saga.js: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from "redux-saga/effects"; 2 | import sagaApi from "./sagaApiServices"; 3 | // import { push } from "connected-react-router"; 4 | import allActions, { actionTypes } from "../reduxStores/actions/allActions"; 5 | import { myLog } from "../constants/config"; 6 | 7 | function* listItem(action) { 8 | yield put(allActions.showLoading(true, action.nextPageToken)); 9 | 10 | try { 11 | const json = yield call( 12 | sagaApi.listItem, 13 | action.keyword, 14 | action.nextPageToken 15 | ); 16 | console.log("listItem-mylog1", action.type, json); 17 | yield put( 18 | allActions.listItemSuccess( 19 | json.data, 20 | action.keyword, 21 | action.nextPageToken 22 | ) 23 | ); 24 | } catch (e) { 25 | yield put(allActions.listItemFailure(e.message)); 26 | } 27 | } 28 | 29 | function* viewItem(action) { 30 | try { 31 | const json = yield call(sagaApi.viewItem, action.item); 32 | console.log("viewItem-mylog2", action.type, json); 33 | yield put(allActions.viewItemSuccess(json.data, action.item)); 34 | } catch (e) { 35 | yield put(allActions.viewItemFailure(e.message)); 36 | } 37 | } 38 | 39 | function* likeItem(action) { 40 | try { 41 | const json = yield call(sagaApi.likeItem, action.item); 42 | console.log("likeItem-mylog3", action.type, json); 43 | yield put(allActions.likeItemSuccess(json.data, action.item)); 44 | } catch (e) { 45 | yield put(allActions.likeItemFailure(e.message)); 46 | } 47 | } 48 | 49 | function* unlikeItem(action) { 50 | try { 51 | const json = yield call(sagaApi.unlikeItem, action.item); 52 | console.log("unlikeItem-mylog3", action.type, json); 53 | yield put(allActions.unlikeItemSuccess(json.data, action.item)); 54 | } catch (e) { 55 | yield put(allActions.unlikeItemFailure(e.message)); 56 | } 57 | } 58 | 59 | function* deleteItem(action) { 60 | try { 61 | const json = yield call(sagaApi.deleteItem, action.id); 62 | myLog(action, json); 63 | yield put(allActions.deleteItemSuccess(action.id)); 64 | // yield put(push("/listPage")); 65 | } catch (e) { 66 | yield put(allActions.deleteItemFailure(e.message)); 67 | } 68 | } 69 | 70 | function* setFilterKey(action) { 71 | try { 72 | yield put(allActions.setFilterKeySuccess(action.value)); 73 | } catch (e) { 74 | yield put(allActions.setFilterKeyFailure(e.message)); 75 | } 76 | } 77 | 78 | function* rootSaga() { 79 | yield takeLatest(actionTypes.LIST_ITEM_REQUEST, listItem); 80 | yield takeLatest(actionTypes.VIEW_ITEM_REQUEST, viewItem); 81 | yield takeLatest(actionTypes.LIKE_ITEM_REQUEST, likeItem); 82 | yield takeLatest(actionTypes.UNLIKE_ITEM_REQUEST, unlikeItem); 83 | yield takeLatest(actionTypes.DELETE_ITEM_REQUEST, deleteItem); 84 | 85 | yield takeLatest(actionTypes.SET_FILTER_KEY_REQUEST, setFilterKey); 86 | } 87 | 88 | export default rootSaga; 89 | -------------------------------------------------------------------------------- /src/components/axiosApp/AxiosAppPresent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import AxiosAppResults from "./AxiosAppResults"; 4 | 5 | const AxiosAppPresent = ({ 6 | keyword, 7 | total, 8 | searchResults, 9 | handleFieldChange, 10 | handleSearchSubmit, 11 | loading 12 | }) => { 13 | return ( 14 |
    15 |
    16 |

    Axios RESTful API search example!

    17 |
    18 | Axios RESTful API search example: A form using axios to implement a 19 | RESTful API search 20 |
    21 |
    22 | 23 |
    24 |
    25 |
    26 |
    27 | 28 | Keyword 29 | 30 |
    31 | 40 |
    41 | 48 |
    49 |
    50 | 53 |
    54 |
    55 |
    56 |
    57 | 58 | {loading && searchResults.length == 0 && 59 |
    60 | Loading... 61 |
    62 | } 63 | 64 | {/* Display results */} 65 | 71 |
    72 | ); 73 | }; 74 | 75 | AxiosAppPresent.propTypes = { 76 | keyword: PropTypes.string, 77 | total: PropTypes.number, 78 | searchResults: PropTypes.array, 79 | handleFieldChange: PropTypes.func, 80 | handleSearchSubmit: PropTypes.func, 81 | loading: PropTypes.bool, 82 | }; 83 | 84 | export default AxiosAppPresent; 85 | -------------------------------------------------------------------------------- /src/constants/axiosApi.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | // for axiosApp example 4 | export const packagistApi = axios.create({ 5 | baseURL: "https://packagist.org", 6 | timeout: 3000, 7 | responseType: "json", 8 | params: { 9 | per_page: 5, 10 | }, 11 | headers: { 12 | "Content-Type": "application/json", 13 | "X-Custom-Header": "AlexStack react starter example", 14 | Authorization: "Client Id change-here-if-authorization-needed", 15 | }, 16 | }); 17 | 18 | // for reduxApp example 19 | export const youtubeApi = axios.create({ 20 | baseURL: "https://www.googleapis.com/youtube/v3", 21 | timeout: 3000, 22 | responseType: "json", 23 | params: { 24 | // replace below key to your own google-youtube-api-key for more quotas 25 | // key: "AIzaSyBLd6DZyQNP"+"CudkOueydclFOp"+"SJklOMvnw", 26 | key: "AIza"+"SyCSUjqlOe2fK8_2Iw8IXTy9RZ8"+"XOGQ3Mvg", 27 | // key: "AIzaSyB5i9Kkxj8j"+"33eO9rKWx82Xda1"+"XmDovy5I", 28 | maxResults: 10, 29 | crossDomain: true, 30 | }, 31 | headers: { 32 | "Content-Type": "application/json", 33 | // "X-Custom-Header": "AlexStack react starter example", 34 | // Authorization: "Client Id change-here-if-authorization-needed", 35 | }, 36 | }); 37 | 38 | // FAKE backend api for create/update/delete action of a particular user 39 | export const backendApi = axios.create({ 40 | baseURL: "https://reqres.in/api", 41 | timeout: 5000, 42 | responseType: "json", 43 | params: { 44 | from: "AlexStack react starter", 45 | }, 46 | headers: { 47 | "Content-Type": "application/json", 48 | "X-Custom-Header": "AlexStack react starter example", 49 | Authorization: "Client Id change-here-if-authorization-needed", 50 | }, 51 | }); 52 | 53 | // githubContentsApi for websiteApp example 54 | export const githubRepository = { 55 | name: "AlexStack/React-Hook-Redux-Saga-Webpack-Starter", 56 | branch: "master", 57 | initDirectory: "doc/websiteApp", 58 | configFile: "0.websiteConfig.json", 59 | wrongFormatMsg: 60 | "
    Error: File source not start with div, please ask the admin to fix it
    ", 61 | axiosErrorMsg: 62 | "
    Get API result failed, please ask the admin to check the console log
    ", 63 | token1: "0655a262b7ca299897f235", 64 | token2: "cde0d924c4e4c05cde", 65 | }; 66 | 67 | export const githubContentsApi = axios.create({ 68 | baseURL: 69 | "https://api.github.com/repos/" + githubRepository.name + "/contents", 70 | timeout: 3000, 71 | responseType: "json", 72 | params: { 73 | ref: githubRepository.branch, 74 | }, 75 | headers: { 76 | "Content-Type": "application/json", 77 | Authorization: "token " + githubRepository.token1 + githubRepository.token2, 78 | }, 79 | }); 80 | 81 | // get github raw file to avoid api rate limit 82 | export const githubRawFile = axios.create({ 83 | baseURL: 84 | "https://raw.githubusercontent.com/" + 85 | githubRepository.name + 86 | "/" + 87 | githubRepository.branch, 88 | timeout: 3000, 89 | responseType: "text", 90 | headers: { 91 | "Content-Type": "text/plain", 92 | }, 93 | }); 94 | 95 | export default packagistApi; 96 | -------------------------------------------------------------------------------- /src/components/hookApp/BasicHooks.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import BasicAppPresent from "../basicApp/BasicAppPresent"; 3 | import RelatedFiles from "../footer/RelatedFiles"; 4 | 5 | const BasicHooks = () => { 6 | const [clickTimes, setClickTimes] = useState(0); 7 | const [keyPressTimes, setKeyPressTimes] = useState(0); 8 | const [pressedKey, setPressedKey] = useState("null"); 9 | const [secretKey, setSecretKey] = useState(null); 10 | const gameRef = useRef(); 11 | const pressedKeyRef = useRef(); 12 | 13 | useEffect(() => { 14 | document.addEventListener("keydown", handleKeyPress, false); 15 | // invoke below cleanup function first when the useEffect re-call again(not the initial render) 16 | return () => { 17 | document.removeEventListener("keydown", handleKeyPress); 18 | 19 | pressedKeyRef.current.focus(); 20 | }; 21 | }, [secretKey, pressedKey]); 22 | 23 | useEffect(() => { 24 | // example of use ref 25 | gameRef.current.style.background = "rgba(0, 123, 255, 0.17)"; 26 | }, []); // Only invoke once == ComponentDidMount() 27 | 28 | const handleFieldChange = () => { 29 | setSecretKey(null); 30 | setClickTimes(clickTimes + 1); 31 | setKeyPressTimes(0); 32 | setPressedKey("null"); 33 | }; 34 | 35 | const handleKeyPress = (event) => { 36 | if (secretKey === null) { 37 | setSecretKey(event.key); 38 | } else if (event.key === "Escape") { 39 | setSecretKey(null); 40 | setClickTimes(0); 41 | setKeyPressTimes(0); 42 | setPressedKey("null"); 43 | } else if (pressedKey === secretKey) { 44 | // do nothing 45 | } else { 46 | setPressedKey(event.key); 47 | setKeyPressTimes(keyPressTimes + 1); 48 | } 49 | }; 50 | 51 | return ( 52 |
    53 |
    54 | 61 | 62 |
    63 | 71 |
    72 |
    73 | 74 | 75 |
  • 76 | src/reactApp.js --- React main entry point 77 |
  • 78 |
  • 79 | src/hookApp/BasicHooks.js --- Hooks Components NOT use this.state & 80 | componentDidMount 81 |
  • 82 |
  • 83 | src/components/basicApp/BasicAppPresent.js --- The same child 84 | component as the basic react example 85 |
  • 86 |
    87 |
    88 | ); 89 | }; 90 | 91 | export default BasicHooks; 92 | -------------------------------------------------------------------------------- /src/components/websiteApp/WebsiteAppPresent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import WebsiteAppResults from "./WebsiteAppResults"; 4 | import WebsiteAppFileContent from "./WebsiteAppFileContent"; 5 | 6 | const WebsiteAppPresent = ({ 7 | config, 8 | keyword, 9 | total, 10 | searchResults, 11 | fileContent, 12 | handleFieldChange, 13 | handleSearchSubmit, 14 | loading, 15 | }) => { 16 | return ( 17 |
    18 |
    19 |

    {config && config.siteName}

    20 |
    {config && config.siteDescription}
    21 |
    22 | 23 |
    24 |
    25 |
    26 |
    27 | 28 | Keyword 29 | 30 |
    31 | 40 | {/*
    41 | 52 |
    */} 53 |
    54 | 61 |
    62 |
    63 |
    64 |
    65 | 66 | {loading && searchResults.length == 0 && ( 67 |
    68 | Loading... 69 |
    70 | )} 71 | 72 | {/* Display results */} 73 | 74 | {fileContent ? ( 75 | 81 | ) : ( 82 | 88 | )} 89 |
    90 | ); 91 | }; 92 | 93 | WebsiteAppPresent.propTypes = { 94 | config: PropTypes.object, 95 | keyword: PropTypes.string, 96 | total: PropTypes.number, 97 | searchResults: PropTypes.array, 98 | fileContent: PropTypes.string, 99 | handleFieldChange: PropTypes.func, 100 | handleSearchSubmit: PropTypes.func, 101 | loading: PropTypes.bool, 102 | }; 103 | 104 | export default WebsiteAppPresent; 105 | -------------------------------------------------------------------------------- /src/components/axiosApp/AxiosApp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React, { Component } from "react"; 3 | import PropTypes from "prop-types"; 4 | import packagistApi from "../../constants/axiosApi"; 5 | import AxiosAppPresent from "./AxiosAppPresent"; 6 | import RelatedFiles from "../footer/RelatedFiles"; 7 | 8 | export default class AxiosApp extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | keyword: "react api", 14 | searchResults: [], 15 | page: 1, 16 | perPage: 5, 17 | total: null, 18 | loading: false, 19 | }; 20 | 21 | this.handleFieldChange = this.handleFieldChange.bind(this); 22 | this.handleSearchSubmit = this.handleSearchSubmit.bind(this); 23 | } 24 | 25 | componentDidMount() { 26 | console.log("componentDidMount", this.state); 27 | } 28 | 29 | componentDidUpdate() { 30 | console.log("componentDidUpdate", this.state); 31 | } 32 | 33 | handleFieldChange = (event) => { 34 | if (event.target.name == "keyword") { 35 | this.setState({ 36 | keyword: event.target.value, 37 | }); 38 | } else if (event.target.name == "perPage") { 39 | this.setState({ 40 | perPage: event.target.value, 41 | }); 42 | } else if (event.target.name == "loadMore") { 43 | const pageNumber = this.state.page + 1; 44 | this.setState({ 45 | loading: true, 46 | }); 47 | this.getApiResult(pageNumber); 48 | } 49 | 50 | console.log(this.state); 51 | }; 52 | 53 | handleSearchSubmit = (event) => { 54 | event.preventDefault(); 55 | this.setState({ 56 | searchResults: [], 57 | total: null, 58 | loading: true, 59 | }); 60 | this.getApiResult(1); 61 | }; 62 | 63 | async getApiResult(pageNumber) { 64 | const res = await packagistApi 65 | .get("/search.json", { 66 | params: { 67 | q: this.state.keyword, 68 | per_page: this.state.perPage, 69 | page: pageNumber, 70 | }, 71 | }) 72 | .catch((error) => { 73 | console.log(error, res); 74 | this.setState({ 75 | loading: false, 76 | }); 77 | }); 78 | 79 | if (res.data) { 80 | console.log(res.data); 81 | if (pageNumber > 1) { 82 | this.setState({ 83 | searchResults: [...this.state.searchResults, ...res.data.results], 84 | total: res.data.total, 85 | loading: false, 86 | page: pageNumber, 87 | }); 88 | } else { 89 | this.setState({ 90 | searchResults: res.data.results, 91 | total: res.data.total, 92 | loading: false, 93 | page: 1, 94 | }); 95 | } 96 | } 97 | return res; 98 | } 99 | 100 | render() { 101 | return ( 102 |
    103 | 111 | 112 | 113 |
  • 114 | src/components/AxiosApp/*.js --- React Components 115 |
  • 116 |
  • 117 | src/api/AxiosApi.js --- Axios common settings 118 |
  • 119 |
    120 |
    121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/components/reduxApp/ReduxPlayedVideos.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React, { Component } from "react"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | import allActions from "../../reduxStores/actions/allActions"; 6 | import VideoSearchForm from "./VideoSearchForm"; 7 | import VideoSearchResults from "./VideoSearchResults"; 8 | 9 | class ReduxPlayedVideos extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | keyword: "funny", 15 | searchResults: [], 16 | page: 1, 17 | perPage: 5, 18 | total: null, 19 | loading: false, 20 | playVideoId: null, 21 | }; 22 | 23 | this.handleFieldChange = this.handleFieldChange.bind(this); 24 | this.handleSearchSubmit = this.handleSearchSubmit.bind(this); 25 | } 26 | 27 | componentDidMount() { 28 | window.scrollTo(0, 0); 29 | // console.log("componentDidMount", this.state); 30 | } 31 | 32 | componentDidUpdate() { 33 | // console.log("componentDidUpdate", this.state); 34 | } 35 | 36 | handleFieldChange = (event) => { 37 | if (event.target.name == "keyword") { 38 | this.setState({ 39 | keyword: event.target.value, 40 | }); 41 | } else if (event.target.name == "playVideo") { 42 | const item = this.props.videos.viewedItems[ 43 | event.target.getAttribute("data-index") 44 | ]; 45 | this.setState({ 46 | playVideoId: item.id.videoId, 47 | }); 48 | this.props.dispatch(allActions.viewItemRequest(item)); 49 | } else if (event.target.name == "like") { 50 | this.props.dispatch( 51 | allActions.likeItemRequest( 52 | this.props.videos.viewedItems[event.target.getAttribute("data-index")] 53 | ) 54 | ); 55 | } else if (event.target.name == "unlike") { 56 | this.props.dispatch( 57 | allActions.unlikeItemRequest( 58 | this.props.videos.viewedItems[event.target.getAttribute("data-index")] 59 | ) 60 | ); 61 | } else if (event.target.name == "perPage") { 62 | this.setState({ 63 | perPage: event.target.value, 64 | }); 65 | } 66 | // console.log(event.target.getAttribute("data-index")); 67 | }; 68 | 69 | handleSearchSubmit = (event) => { 70 | event.preventDefault(); 71 | this.setState({ 72 | searchResults: [], 73 | total: null, 74 | loading: true, 75 | }); 76 | this.props.dispatch(allActions.listItemRequest(this.state.keyword)); 77 | }; 78 | 79 | render() { 80 | return ( 81 |
    82 | 90 | 91 | 99 |
    100 | ); 101 | } 102 | } 103 | 104 | const mapStateToProps = (reduxState) => { 105 | // console.log("mapStateToProps ", reduxState); 106 | return reduxState; 107 | }; 108 | 109 | ReduxPlayedVideos.propTypes = { 110 | dispatch: PropTypes.func.isRequired, 111 | }; 112 | 113 | export default connect(mapStateToProps, null)(ReduxPlayedVideos); 114 | -------------------------------------------------------------------------------- /src/components/reduxApp/ReduxLikedVideos.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React, { Component } from "react"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | import allActions from "../../reduxStores/actions/allActions"; 6 | import VideoSearchForm from "./VideoSearchForm"; 7 | import VideoSearchResults from "./VideoSearchResults"; 8 | 9 | class ReduxLikedVideos extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | keyword: "funny", 15 | searchResults: [], 16 | page: 1, 17 | perPage: 5, 18 | total: null, 19 | loading: false, 20 | playVideoId: null, 21 | }; 22 | 23 | this.handleFieldChange = this.handleFieldChange.bind(this); 24 | this.handleSearchSubmit = this.handleSearchSubmit.bind(this); 25 | } 26 | 27 | componentDidMount() { 28 | window.scrollTo(0, 0); 29 | // console.log("componentDidMount", this.state); 30 | // this.props.dispatch(allActions.listItemRequest(this.state.keyword)); 31 | } 32 | 33 | componentDidUpdate() { 34 | // console.log("componentDidUpdate", this.state); 35 | } 36 | 37 | handleFieldChange = (event) => { 38 | if (event.target.name == "keyword") { 39 | this.setState({ 40 | keyword: event.target.value, 41 | }); 42 | } else if (event.target.name == "playVideo") { 43 | const item = this.props.videos.likedItems[ 44 | event.target.getAttribute("data-index") 45 | ]; 46 | this.setState({ 47 | playVideoId: item.id.videoId, 48 | }); 49 | this.props.dispatch(allActions.viewItemRequest(item)); 50 | } else if (event.target.name == "like") { 51 | this.props.dispatch( 52 | allActions.likeItemRequest( 53 | this.props.videos.likedItems[event.target.getAttribute("data-index")] 54 | ) 55 | ); 56 | } else if (event.target.name == "unlike") { 57 | this.props.dispatch( 58 | allActions.unlikeItemRequest( 59 | this.props.videos.likedItems[event.target.getAttribute("data-index")] 60 | ) 61 | ); 62 | } else if (event.target.name == "perPage") { 63 | this.setState({ 64 | perPage: event.target.value, 65 | }); 66 | } 67 | 68 | // console.log(event.target.getAttribute("data-index")); 69 | }; 70 | 71 | handleSearchSubmit = (event) => { 72 | event.preventDefault(); 73 | this.setState({ 74 | searchResults: [], 75 | total: null, 76 | loading: true, 77 | }); 78 | this.props.dispatch(allActions.listItemRequest(this.state.keyword)); 79 | }; 80 | 81 | render() { 82 | return ( 83 |
    84 | 92 | 93 | 101 |
    102 | ); 103 | } 104 | } 105 | 106 | const mapStateToProps = (reduxState) => { 107 | // console.log("mapStateToProps ", reduxState); 108 | return reduxState; 109 | }; 110 | 111 | ReduxLikedVideos.propTypes = { 112 | dispatch: PropTypes.func.isRequired, 113 | }; 114 | 115 | export default connect(mapStateToProps, null)(ReduxLikedVideos); 116 | -------------------------------------------------------------------------------- /src/reduxStores/reducers/videoReducers.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "reduxsauce"; 2 | import { actionTypes } from "../actions/allActions"; 3 | import { REDUXPATH, lastPath } from "../../constants/config"; 4 | 5 | // the initial state of this reducer 6 | export const INITIAL_STATE = { 7 | allItems: [], 8 | likedItems: [], 9 | viewedItems: [], 10 | keywords: ["Just for laugh", "Cat", "Dog", "Mac Pro", "SpaceX", "Covid-19"], 11 | extraInfo: { 12 | etag: null, 13 | loading: false, 14 | nextPageToken: null, 15 | resultsPerPage: 5, 16 | totalResults: null, 17 | errorMsg: null, 18 | }, 19 | }; 20 | 21 | const listItems = (state = INITIAL_STATE, action) => { 22 | const allItems = action.nextPageToken 23 | ? [...state.allItems, ...action.allData.items] 24 | : action.allData.items; 25 | return { 26 | ...state, 27 | ...{ 28 | allItems: allItems, 29 | keywords: [ 30 | action.keyword, 31 | ...state.keywords.filter((t) => t !== action.keyword), 32 | ], 33 | extraInfo: { 34 | loading: false, 35 | nextPageToken: action.allData.nextPageToken, 36 | resultsPerPage: action.allData.pageInfo.resultsPerPage, 37 | totalResults: action.allData.pageInfo.totalResults, 38 | errorMsg: null, 39 | etag: action.allData.etag, 40 | }, 41 | }, 42 | }; 43 | }; 44 | 45 | const showLoading = (state = INITIAL_STATE, action) => { 46 | const allItems = action.nextPageToken ? state.allItems : []; 47 | const totalResults = action.nextPageToken 48 | ? state.extraInfo.totalResults 49 | : null; 50 | return { 51 | ...state, 52 | ...{ 53 | extraInfo: { 54 | loading: action.loading, 55 | totalResults: totalResults, 56 | }, 57 | allItems: allItems, 58 | }, 59 | }; 60 | }; 61 | 62 | const listItemFailure = (state = INITIAL_STATE, action) => { 63 | return { 64 | ...state, 65 | ...{ 66 | extraInfo: { 67 | loading: false, 68 | errorMsg: "Something wrong with the youtube API response", 69 | }, 70 | }, 71 | }; 72 | }; 73 | 74 | const likeItem = (state = INITIAL_STATE, action) => { 75 | return { 76 | ...state, 77 | ...{ likedItems: [action.item, ...state.likedItems] }, 78 | }; 79 | }; 80 | 81 | const unlikeItem = (state = INITIAL_STATE, action) => { 82 | const newLikedItems = state.likedItems.filter( 83 | (t) => t.id.videoId !== action.item.id.videoId 84 | ); 85 | return { 86 | ...state, 87 | likedItems: newLikedItems, 88 | }; 89 | }; 90 | 91 | const viewItem = (state = INITIAL_STATE, action) => { 92 | if (lastPath() == REDUXPATH.PlayedVideos) { 93 | return state; // nothing should change when you are on the PlayedVideos page 94 | } 95 | 96 | // remove the duplicate videos and put the latest played video on the top 97 | return { 98 | ...state, 99 | ...{ 100 | viewedItems: [ 101 | action.item, 102 | ...state.viewedItems.filter( 103 | (item) => item.id.videoId !== action.item.id.videoId 104 | ), 105 | ], 106 | }, 107 | }; 108 | }; 109 | 110 | const deleteItem = (state = INITIAL_STATE, action) => { 111 | // console.log(action, action, action); 112 | return { 113 | ...state, 114 | items: state.items.filter((item) => item.id !== action.id), 115 | }; 116 | }; 117 | 118 | const setFilterKey = (state = INITIAL_STATE, action) => { 119 | return { ...state, filterKey: action.value }; 120 | }; 121 | 122 | // map our action types to our reducer functions 123 | export const HANDLERS = { 124 | [actionTypes.LIST_ITEM_SUCCESS]: listItems, 125 | [actionTypes.VIEW_ITEM_SUCCESS]: viewItem, 126 | [actionTypes.LIKE_ITEM_SUCCESS]: likeItem, 127 | [actionTypes.UNLIKE_ITEM_SUCCESS]: unlikeItem, 128 | [actionTypes.DELETE_ITEM_SUCCESS]: deleteItem, 129 | [actionTypes.SET_FILTER_KEY_SUCCESS]: setFilterKey, 130 | [actionTypes.SHOW_LOADING]: showLoading, 131 | }; 132 | 133 | export default createReducer(INITIAL_STATE, HANDLERS); 134 | -------------------------------------------------------------------------------- /src/components/reduxApp/ReduxAllVideos.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React, { Component } from "react"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | 6 | import allActions from "../../reduxStores/actions/allActions"; 7 | import VideoSearchForm from "./VideoSearchForm"; 8 | import VideoSearchResults from "./VideoSearchResults"; 9 | 10 | class ReduxAllVideos extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | keyword: "funny", 16 | searchResults: [], 17 | total: null, 18 | playVideoId: null, 19 | }; 20 | 21 | this.handleFieldChange = this.handleFieldChange.bind(this); 22 | this.handleSearchSubmit = this.handleSearchSubmit.bind(this); 23 | } 24 | 25 | componentDidMount() { 26 | if (this.props.videos.keywords.length > 0) { 27 | this.setState({ 28 | keyword: this.props.videos.keywords[0], 29 | }); 30 | } 31 | console.log("componentDidMount", this.state); 32 | } 33 | 34 | componentDidUpdate() { 35 | console.log("componentDidUpdate", this.state); 36 | } 37 | 38 | handleFieldChange = (event) => { 39 | if (event.target.name == "keyword") { 40 | this.setState({ 41 | keyword: event.target.value.trim(), 42 | }); 43 | } else if (event.target.name == "playVideo") { 44 | const item = this.props.videos.allItems[ 45 | event.target.getAttribute("data-index") 46 | ]; 47 | this.setState({ 48 | playVideoId: item.id.videoId, 49 | }); 50 | this.props.dispatch(allActions.viewItemRequest(item)); 51 | } else if (event.target.name == "like") { 52 | this.props.dispatch( 53 | allActions.likeItemRequest( 54 | this.props.videos.allItems[event.target.getAttribute("data-index")] 55 | ) 56 | ); 57 | } else if (event.target.name == "unlike") { 58 | this.props.dispatch( 59 | allActions.unlikeItemRequest( 60 | this.props.videos.allItems[event.target.getAttribute("data-index")] 61 | ) 62 | ); 63 | } else if (event.target.name == "keywordHistory") { 64 | this.setState({ 65 | keyword: event.target.value, 66 | searchResults: [], 67 | total: null, 68 | }); 69 | this.startSearch(event.target.value); 70 | } else if (event.target.name == "loadMore") { 71 | this.startSearch( 72 | this.state.keyword, 73 | this.props.videos.extraInfo.nextPageToken 74 | ); 75 | } 76 | 77 | console.log(event.target.getAttribute("data-index")); 78 | }; 79 | 80 | handleSearchSubmit = (event) => { 81 | event.preventDefault(); 82 | this.setState({ 83 | searchResults: [], 84 | total: null, 85 | }); 86 | 87 | this.startSearch(this.state.keyword); 88 | }; 89 | 90 | startSearch = (keyword, nextPageToken) => { 91 | this.props.dispatch(allActions.listItemRequest(keyword, nextPageToken)); 92 | }; 93 | 94 | render() { 95 | return ( 96 |
    97 | 105 | 106 | {this.props.videos.extraInfo.errorMsg && ( 107 |
    108 | {this.props.videos.extraInfo.errorMsg} 109 |
    110 | )} 111 | 112 | 120 |
    121 | ); 122 | } 123 | } 124 | 125 | const mapStateToProps = (reduxState) => { 126 | // console.log("mapStateToProps ", reduxState); 127 | return reduxState; 128 | }; 129 | 130 | ReduxAllVideos.propTypes = { 131 | dispatch: PropTypes.func.isRequired, 132 | }; 133 | 134 | export default connect(mapStateToProps, null)(ReduxAllVideos); 135 | -------------------------------------------------------------------------------- /src/components/reduxApp/VideoSearchForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { NavLink, Link } from "react-router-dom"; 4 | import { REDUXPATH, lastPath, myLog } from "../../constants/config"; 5 | 6 | const VideoSearchForm = ({ 7 | keyword, 8 | keywordHistory, 9 | searchResults, 10 | handleFieldChange, 11 | handleSearchSubmit, 12 | loading, 13 | }) => { 14 | const showSearchForm = 15 | lastPath() == REDUXPATH.SearchVideos || lastPath() == "/" ? true : false; 16 | // myLog(lastPath(), showSearchForm); 17 | return ( 18 |
    19 | {showSearchForm ? ( 20 |
    21 |
    22 |

    Redux Video Search Example!

    23 |
    24 | Redux Video search example: An video search and like APP using 25 | react-redux-saga API search 26 |
    27 |
    28 |
    29 |
    30 |
    31 | 35 | Keyword 36 | 37 |
    38 | 47 | {/*
    48 | 59 |
    */} 60 |
    61 | 68 |
    69 |
    70 |
    71 |
    72 | ) : ( 73 |
    74 |

    Redux User Dashboard Example

    75 |
    76 | This is the user dashboard example 77 |
    78 |
    79 | )} 80 | 81 |
    82 |
    83 | {showSearchForm && 84 | keywordHistory.length > 0 && 85 | keywordHistory.map((k, index) => { 86 | if (k != keyword) { 87 | return ( 88 | 97 | ); 98 | } 99 | })} 100 |
    101 | 102 |
    103 | {!showSearchForm && ( 104 | 109 | Search Videos 110 | 111 | )} 112 | 113 | 118 | My Liked Videos 119 | 120 | 121 | 126 | Recently Played Videos 127 | 128 |
    129 | 130 | {loading && searchResults.length == 0 && ( 131 |
    132 | Loading ...... 133 |
    134 | )} 135 |
    136 |
    137 | ); 138 | }; 139 | 140 | VideoSearchForm.propTypes = { 141 | keyword: PropTypes.string, 142 | keywordHistory: PropTypes.array, 143 | searchResults: PropTypes.array, 144 | handleFieldChange: PropTypes.func, 145 | handleSearchSubmit: PropTypes.func, 146 | loading: PropTypes.bool, 147 | }; 148 | 149 | export default VideoSearchForm; 150 | -------------------------------------------------------------------------------- /src/components/websiteApp/WebsiteApp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import React, { Component } from "react"; 3 | import PropTypes from "prop-types"; 4 | import { 5 | githubContentsApi, 6 | githubRawFile, 7 | githubRepository, 8 | } from "../../constants/axiosApi"; 9 | import WebsiteAppPresent from "./WebsiteAppPresent"; 10 | import RelatedFiles from "../footer/RelatedFiles"; 11 | import { Base64 } from "js-base64"; 12 | 13 | export default class WebsiteApp extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | keyword: "", 19 | searchResults: [], 20 | config: null, 21 | fileContent: null, 22 | total: null, 23 | loading: false, 24 | }; 25 | 26 | this.handleFieldChange = this.handleFieldChange.bind(this); 27 | this.handleSearchSubmit = this.handleSearchSubmit.bind(this); 28 | } 29 | 30 | componentDidMount() { 31 | console.log("componentDidMount", this.state); 32 | this.getRawFileContent( 33 | githubRepository.initDirectory + "/" + githubRepository.configFile 34 | ); 35 | this.listAllFiles(); 36 | } 37 | 38 | componentDidUpdate() { 39 | console.log("componentDidUpdate", this.state); 40 | } 41 | 42 | handleFieldChange = (event) => { 43 | if (event.target.name == "keyword") { 44 | this.setState({ 45 | keyword: event.target.value.trim(), 46 | }); 47 | } else if (event.target.name == "showDetails") { 48 | event.preventDefault(); 49 | this.setState({ 50 | loading: true, 51 | }); 52 | // this.getApiResult(event.target.value); 53 | this.getRawFileContent(event.target.value); 54 | } else if (event.target.name == "returnToList") { 55 | event.preventDefault(); 56 | this.setState({ 57 | fileContent: null, 58 | }); 59 | } else if (event.target.name == "displayAll") { 60 | event.preventDefault(); 61 | this.setState({ 62 | keyword: "", 63 | }); 64 | } 65 | 66 | console.log(this.state); 67 | }; 68 | 69 | handleSearchSubmit = (event) => { 70 | event.preventDefault(); 71 | if (!this.state.keyword && this.state.searchResults.length == 0) { 72 | this.setState({ 73 | searchResults: [], 74 | total: null, 75 | loading: true, 76 | fileContent: null, 77 | }); 78 | this.listAllFiles(); 79 | } else if (this.state.fileContent) { 80 | this.setState({ 81 | fileContent: null, 82 | }); 83 | } else { 84 | console.log("do nothing"); 85 | } 86 | }; 87 | 88 | listAllFiles = (dir = githubRepository.initDirectory) => { 89 | this.getApiResult(dir); 90 | }; 91 | 92 | async getApiResult(fileName) { 93 | const results = await githubContentsApi 94 | .get("/" + fileName, { 95 | params: { 96 | ref: githubRepository.branch, 97 | }, 98 | }) 99 | .catch((error) => { 100 | // console.log(error, error.response); 101 | this.setState({ 102 | loading: false, 103 | fileContent: 104 | githubRepository.axiosErrorMsg + 105 | "
    " + 106 | error.response.data.message + 107 | "
    ", 108 | }); 109 | }); 110 | 111 | if (results.data) { 112 | if (results.data.type) { 113 | // console.log(Base64.decode(results.data.content)); 114 | this.setState({ 115 | fileContent: Base64.decode(results.data.content), 116 | loading: false, 117 | }); 118 | } else { 119 | const searchResults = results.data.filter( 120 | (item) => item.type == "file" && item.name.indexOf(".html") != -1 121 | ); 122 | 123 | this.setState({ 124 | searchResults: searchResults, 125 | total: searchResults.length, 126 | loading: false, 127 | }); 128 | } 129 | } 130 | return results; 131 | } 132 | 133 | async getRawFileContent(fileName) { 134 | const results = await githubRawFile.get("/" + fileName).catch((error) => { 135 | // console.log(error, error.response); 136 | this.setState({ 137 | loading: false, 138 | fileContent: 139 | githubRepository.axiosErrorMsg + 140 | "
    " + 141 | error.response.data.message + 142 | "
    ", 143 | }); 144 | }); 145 | 146 | if (results.data) { 147 | if (fileName.indexOf(githubRepository.configFile) != -1) { 148 | console.log(results); 149 | this.setState({ 150 | config: results.data, 151 | }); 152 | } 153 | const fileContent = 154 | results.data.trim().indexOf("
    { 167 | if (this.state.keyword) { 168 | return this.state.searchResults.filter( 169 | (item) => 170 | item.name 171 | .toLowerCase() 172 | .replace(/-/g, " ") 173 | .indexOf(this.state.keyword.toLowerCase()) != -1 174 | ); 175 | } 176 | 177 | return this.state.searchResults; 178 | }; 179 | 180 | render() { 181 | return ( 182 |
    183 | 193 | 194 | 195 |
  • 196 | src/components/WebsiteApp/*.js --- React Components 197 |
  • 198 |
  • 199 | src/api/AxiosApi.js --- Axios common settings 200 |
  • 201 |
    202 |
    203 | ); 204 | } 205 | } 206 | --------------------------------------------------------------------------------