├── .env ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── react-redux-crud-example-web-api-demo.png ├── src ├── App.css ├── App.js ├── actions │ ├── tutorials.js │ └── types.js ├── components │ ├── add-tutorial.component.js │ ├── tutorial.component.js │ └── tutorials-list.component.js ├── http-common.js ├── index.css ├── index.js ├── logo.svg ├── reducers │ ├── index.js │ └── tutorials.js ├── reportWebVitals.js ├── services │ └── tutorial.service.js ├── setupTests.js └── store.js └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | PORT=8081 -------------------------------------------------------------------------------- /.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 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux CRUD App example with Axios and Rest API 2 | 3 | Build a React Redux CRUD Application to consume Web API using Axios, display and modify data with Router & Bootstrap. 4 | 5 | React Redux Tutorial Application in that: 6 | - Each Tutorial has id, title, description, published status. 7 | - We can create, retrieve, update, delete Tutorials. 8 | - There is a Search bar for finding Tutorials by title. 9 | 10 | ![react-redux-crud-example-web-api-demo](react-redux-crud-example-web-api-demo.png) 11 | 12 | For instruction, please visit: 13 | > [React Redux CRUD App example with Rest API](https://bezkoder.com/react-redux-crud-example/) 14 | 15 | Related Posts: 16 | > [React (without Redux) CRUD example to consume Web API](https://bezkoder.com/react-crud-web-api/) 17 | 18 | > [React (Hooks) CRUD example to consume Web API](https://bezkoder.com/react-hooks-crud-axios-api/) 19 | 20 | > [React Table example: CRUD App with react-table v7](https://bezkoder.com/react-table-example-hooks-crud/) 21 | 22 | Using Material UI instead of Bootstrap: 23 | > [React Material UI examples with a CRUD Application](https://bezkoder.com/react-material-ui-examples-crud/) 24 | 25 | More Practice: 26 | > [React Pagination example](https://bezkoder.com/react-pagination-material-ui/) 27 | 28 | > [React File Upload example](https://bezkoder.com/react-file-upload-axios/) 29 | 30 | > [React JWT Authentication & Authorization example](https://bezkoder.com/react-jwt-auth/) 31 | 32 | > [React + Redux: JWT Authentication & Authorization example](https://bezkoder.com/react-redux-jwt-auth/) 33 | 34 | Fullstack with Node.js Express: 35 | > [React.js + Node.js Express + MySQL](https://bezkoder.com/react-node-express-mysql/) 36 | 37 | > [React.js + Node.js Express + PostgreSQL](https://bezkoder.com/react-node-express-postgresql/) 38 | 39 | > [React.js + Node.js Express + MongoDB](https://bezkoder.com/react-node-express-mongodb-mern-stack/) 40 | 41 | Fullstack with Spring Boot: 42 | > [React.js + Spring Boot + MySQL](https://bezkoder.com/react-spring-boot-crud/) 43 | 44 | > [React.js + Spring Boot + PostgreSQL](https://bezkoder.com/spring-boot-react-postgresql/) 45 | 46 | > [React.js + Spring Boot + MongoDB](https://bezkoder.com/react-spring-boot-mongodb/) 47 | 48 | Fullstack with Django: 49 | 50 | > [React.js + Django Rest Framework](https://bezkoder.com/django-react-axios-rest-framework/) 51 | 52 | Serverless: 53 | > [React Firebase CRUD App with Realtime Database](https://bezkoder.com/react-firebase-crud/) 54 | 55 | > [React Firestore CRUD App example | Firebase Cloud Firestore](https://bezkoder.com/react-firestore-crud/) 56 | 57 | Integration (run back-end & front-end on same server/port) 58 | > [How to integrate React.js with Spring Boot](https://bezkoder.com/integrate-reactjs-spring-boot/) 59 | 60 | > [Integrate React with Node.js Express on same Server/Port](https://bezkoder.com/integrate-react-express-same-server-port/) 61 | 62 | 63 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 64 | 65 | ### Set port 66 | .env 67 | ``` 68 | PORT=8081 69 | ``` 70 | 71 | ## Project setup 72 | 73 | In the project directory, you can run: 74 | 75 | ``` 76 | npm install 77 | # or 78 | yarn install 79 | ``` 80 | 81 | or 82 | 83 | ### Compiles and hot-reloads for development 84 | 85 | ``` 86 | npm start 87 | # or 88 | yarn start 89 | ``` 90 | 91 | Open [http://localhost:8081](http://localhost:8081) to view it in the browser. 92 | 93 | The page will reload if you make edits. 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-crud-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "axios": "^0.21.1", 10 | "bootstrap": "^4.6.0", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-redux": "^7.2.3", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "4.0.3", 16 | "redux": "^4.0.5", 17 | "redux-thunk": "^2.3.0", 18 | "web-vitals": "^1.0.1" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "redux-devtools-extension": "^2.13.9" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/react-redux-axios-crud/ab341a75b5ebac3ce55f85c3e38d605cacbfc652/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/react-redux-axios-crud/ab341a75b5ebac3ce55f85c3e38d605cacbfc652/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/react-redux-axios-crud/ab341a75b5ebac3ce55f85c3e38d605cacbfc652/public/logo512.png -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-redux-crud-example-web-api-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/react-redux-axios-crud/ab341a75b5ebac3ce55f85c3e38d605cacbfc652/react-redux-crud-example-web-api-demo.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .list { 2 | text-align: left; 3 | max-width: 750px; 4 | margin: auto; 5 | } 6 | 7 | .submit-form { 8 | max-width: 300px; 9 | margin: auto; 10 | } 11 | 12 | .edit-form { 13 | max-width: 300px; 14 | margin: auto; 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; 3 | import "bootstrap/dist/css/bootstrap.min.css"; 4 | import "./App.css"; 5 | 6 | import AddTutorial from "./components/add-tutorial.component"; 7 | import Tutorial from "./components/tutorial.component"; 8 | import TutorialsList from "./components/tutorials-list.component"; 9 | 10 | class App extends Component { 11 | render() { 12 | return ( 13 | 14 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/actions/tutorials.js: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_TUTORIAL, 3 | RETRIEVE_TUTORIALS, 4 | UPDATE_TUTORIAL, 5 | DELETE_TUTORIAL, 6 | DELETE_ALL_TUTORIALS 7 | } from "./types"; 8 | 9 | import TutorialDataService from "../services/tutorial.service"; 10 | 11 | export const createTutorial = (title, description) => async (dispatch) => { 12 | try { 13 | const res = await TutorialDataService.create({ title, description }); 14 | 15 | dispatch({ 16 | type: CREATE_TUTORIAL, 17 | payload: res.data, 18 | }); 19 | 20 | return Promise.resolve(res.data); 21 | } catch (err) { 22 | return Promise.reject(err); 23 | } 24 | }; 25 | 26 | export const retrieveTutorials = () => async (dispatch) => { 27 | try { 28 | const res = await TutorialDataService.getAll(); 29 | 30 | dispatch({ 31 | type: RETRIEVE_TUTORIALS, 32 | payload: res.data, 33 | }); 34 | } catch (err) { 35 | console.log(err); 36 | } 37 | }; 38 | 39 | export const updateTutorial = (id, data) => async (dispatch) => { 40 | try { 41 | const res = await TutorialDataService.update(id, data); 42 | 43 | dispatch({ 44 | type: UPDATE_TUTORIAL, 45 | payload: data, 46 | }); 47 | 48 | return Promise.resolve(res.data); 49 | } catch (err) { 50 | return Promise.reject(err); 51 | } 52 | }; 53 | 54 | export const deleteTutorial = (id) => async (dispatch) => { 55 | try { 56 | await TutorialDataService.delete(id); 57 | 58 | dispatch({ 59 | type: DELETE_TUTORIAL, 60 | payload: { id }, 61 | }); 62 | } catch (err) { 63 | console.log(err); 64 | } 65 | }; 66 | 67 | export const deleteAllTutorials = () => async (dispatch) => { 68 | try { 69 | const res = await TutorialDataService.deleteAll(); 70 | 71 | dispatch({ 72 | type: DELETE_ALL_TUTORIALS, 73 | payload: res.data, 74 | }); 75 | 76 | return Promise.resolve(res.data); 77 | } catch (err) { 78 | return Promise.reject(err); 79 | } 80 | }; 81 | 82 | export const findTutorialsByTitle = (title) => async (dispatch) => { 83 | try { 84 | const res = await TutorialDataService.findByTitle(title); 85 | 86 | dispatch({ 87 | type: RETRIEVE_TUTORIALS, 88 | payload: res.data, 89 | }); 90 | } catch (err) { 91 | console.log(err); 92 | } 93 | }; -------------------------------------------------------------------------------- /src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const CREATE_TUTORIAL = "CREATE_TUTORIAL"; 2 | export const RETRIEVE_TUTORIALS = "RETRIEVE_TUTORIALS"; 3 | export const UPDATE_TUTORIAL = "UPDATE_TUTORIAL"; 4 | export const DELETE_TUTORIAL = "DELETE_TUTORIAL"; 5 | export const DELETE_ALL_TUTORIALS = "DELETE_ALL_TUTORIALS"; -------------------------------------------------------------------------------- /src/components/add-tutorial.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { createTutorial } from "../actions/tutorials"; 4 | 5 | class AddTutorial extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.onChangeTitle = this.onChangeTitle.bind(this); 9 | this.onChangeDescription = this.onChangeDescription.bind(this); 10 | this.saveTutorial = this.saveTutorial.bind(this); 11 | this.newTutorial = this.newTutorial.bind(this); 12 | 13 | this.state = { 14 | id: null, 15 | title: "", 16 | description: "", 17 | published: false, 18 | 19 | submitted: false, 20 | }; 21 | } 22 | 23 | onChangeTitle(e) { 24 | this.setState({ 25 | title: e.target.value, 26 | }); 27 | } 28 | 29 | onChangeDescription(e) { 30 | this.setState({ 31 | description: e.target.value, 32 | }); 33 | } 34 | 35 | saveTutorial() { 36 | const { title, description } = this.state; 37 | 38 | this.props 39 | .createTutorial(title, description) 40 | .then((data) => { 41 | this.setState({ 42 | id: data.id, 43 | title: data.title, 44 | description: data.description, 45 | published: data.published, 46 | 47 | submitted: true, 48 | }); 49 | console.log(data); 50 | }) 51 | .catch((e) => { 52 | console.log(e); 53 | }); 54 | } 55 | 56 | newTutorial() { 57 | this.setState({ 58 | id: null, 59 | title: "", 60 | description: "", 61 | published: false, 62 | 63 | submitted: false, 64 | }); 65 | } 66 | 67 | render() { 68 | return ( 69 |
70 | {this.state.submitted ? ( 71 |
72 |

You submitted successfully!

73 | 76 |
77 | ) : ( 78 |
79 |
80 | 81 | 90 |
91 | 92 |
93 | 94 | 103 |
104 | 105 | 108 |
109 | )} 110 |
111 | ); 112 | } 113 | } 114 | 115 | export default connect(null, { createTutorial })(AddTutorial); 116 | -------------------------------------------------------------------------------- /src/components/tutorial.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { updateTutorial, deleteTutorial } from "../actions/tutorials"; 4 | import TutorialDataService from "../services/tutorial.service"; 5 | 6 | class Tutorial extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.onChangeTitle = this.onChangeTitle.bind(this); 10 | this.onChangeDescription = this.onChangeDescription.bind(this); 11 | this.getTutorial = this.getTutorial.bind(this); 12 | this.updateStatus = this.updateStatus.bind(this); 13 | this.updateContent = this.updateContent.bind(this); 14 | this.removeTutorial = this.removeTutorial.bind(this); 15 | 16 | this.state = { 17 | currentTutorial: { 18 | id: null, 19 | title: "", 20 | description: "", 21 | published: false, 22 | }, 23 | message: "", 24 | }; 25 | } 26 | 27 | componentDidMount() { 28 | this.getTutorial(this.props.match.params.id); 29 | } 30 | 31 | onChangeTitle(e) { 32 | const title = e.target.value; 33 | 34 | this.setState(function (prevState) { 35 | return { 36 | currentTutorial: { 37 | ...prevState.currentTutorial, 38 | title: title, 39 | }, 40 | }; 41 | }); 42 | } 43 | 44 | onChangeDescription(e) { 45 | const description = e.target.value; 46 | 47 | this.setState((prevState) => ({ 48 | currentTutorial: { 49 | ...prevState.currentTutorial, 50 | description: description, 51 | }, 52 | })); 53 | } 54 | 55 | getTutorial(id) { 56 | TutorialDataService.get(id) 57 | .then((response) => { 58 | this.setState({ 59 | currentTutorial: response.data, 60 | }); 61 | console.log(response.data); 62 | }) 63 | .catch((e) => { 64 | console.log(e); 65 | }); 66 | } 67 | 68 | updateStatus(status) { 69 | var data = { 70 | id: this.state.currentTutorial.id, 71 | title: this.state.currentTutorial.title, 72 | description: this.state.currentTutorial.description, 73 | published: status, 74 | }; 75 | 76 | this.props 77 | .updateTutorial(this.state.currentTutorial.id, data) 78 | .then((reponse) => { 79 | console.log(reponse); 80 | 81 | this.setState((prevState) => ({ 82 | currentTutorial: { 83 | ...prevState.currentTutorial, 84 | published: status, 85 | }, 86 | })); 87 | 88 | this.setState({ message: "The status was updated successfully!" }); 89 | }) 90 | .catch((e) => { 91 | console.log(e); 92 | }); 93 | } 94 | 95 | updateContent() { 96 | this.props 97 | .updateTutorial(this.state.currentTutorial.id, this.state.currentTutorial) 98 | .then((reponse) => { 99 | console.log(reponse); 100 | 101 | this.setState({ message: "The tutorial was updated successfully!" }); 102 | }) 103 | .catch((e) => { 104 | console.log(e); 105 | }); 106 | } 107 | 108 | removeTutorial() { 109 | this.props 110 | .deleteTutorial(this.state.currentTutorial.id) 111 | .then(() => { 112 | this.props.history.push("/tutorials"); 113 | }) 114 | .catch((e) => { 115 | console.log(e); 116 | }); 117 | } 118 | 119 | render() { 120 | const { currentTutorial } = this.state; 121 | 122 | return ( 123 |
124 | {currentTutorial ? ( 125 |
126 |

Tutorial

127 |
128 |
129 | 130 | 137 |
138 |
139 | 140 | 147 |
148 | 149 |
150 | 153 | {currentTutorial.published ? "Published" : "Pending"} 154 |
155 |
156 | 157 | {currentTutorial.published ? ( 158 | 164 | ) : ( 165 | 171 | )} 172 | 173 | 179 | 180 | 187 |

{this.state.message}

188 |
189 | ) : ( 190 |
191 |
192 |

Please click on a Tutorial...

193 |
194 | )} 195 |
196 | ); 197 | } 198 | } 199 | 200 | export default connect(null, { updateTutorial, deleteTutorial })(Tutorial); 201 | -------------------------------------------------------------------------------- /src/components/tutorials-list.component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { 4 | retrieveTutorials, 5 | findTutorialsByTitle, 6 | deleteAllTutorials, 7 | } from "../actions/tutorials"; 8 | import { Link } from "react-router-dom"; 9 | 10 | class TutorialsList extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.onChangeSearchTitle = this.onChangeSearchTitle.bind(this); 14 | this.refreshData = this.refreshData.bind(this); 15 | this.setActiveTutorial = this.setActiveTutorial.bind(this); 16 | this.findByTitle = this.findByTitle.bind(this); 17 | this.removeAllTutorials = this.removeAllTutorials.bind(this); 18 | 19 | this.state = { 20 | currentTutorial: null, 21 | currentIndex: -1, 22 | searchTitle: "", 23 | }; 24 | } 25 | 26 | componentDidMount() { 27 | this.props.retrieveTutorials(); 28 | } 29 | 30 | onChangeSearchTitle(e) { 31 | const searchTitle = e.target.value; 32 | 33 | this.setState({ 34 | searchTitle: searchTitle, 35 | }); 36 | } 37 | 38 | refreshData() { 39 | this.setState({ 40 | currentTutorial: null, 41 | currentIndex: -1, 42 | }); 43 | } 44 | 45 | setActiveTutorial(tutorial, index) { 46 | this.setState({ 47 | currentTutorial: tutorial, 48 | currentIndex: index, 49 | }); 50 | } 51 | 52 | removeAllTutorials() { 53 | this.props 54 | .deleteAllTutorials() 55 | .then((response) => { 56 | console.log(response); 57 | this.refreshData(); 58 | }) 59 | .catch((e) => { 60 | console.log(e); 61 | }); 62 | } 63 | 64 | findByTitle() { 65 | this.refreshData(); 66 | 67 | this.props.findTutorialsByTitle(this.state.searchTitle); 68 | } 69 | 70 | render() { 71 | const { searchTitle, currentTutorial, currentIndex } = this.state; 72 | const { tutorials } = this.props; 73 | 74 | return ( 75 |
76 |
77 |
78 | 85 |
86 | 93 |
94 |
95 |
96 |
97 |

Tutorials List

98 | 99 | 114 | 115 | 121 |
122 |
123 | {currentTutorial ? ( 124 |
125 |

Tutorial

126 |
127 | {" "} 130 | {currentTutorial.title} 131 |
132 |
133 | {" "} 136 | {currentTutorial.description} 137 |
138 |
139 | {" "} 142 | {currentTutorial.published ? "Published" : "Pending"} 143 |
144 | 145 | 149 | Edit 150 | 151 |
152 | ) : ( 153 |
154 |
155 |

Please click on a Tutorial...

156 |
157 | )} 158 |
159 |
160 | ); 161 | } 162 | } 163 | 164 | const mapStateToProps = (state) => { 165 | return { 166 | tutorials: state.tutorials, 167 | }; 168 | }; 169 | 170 | export default connect(mapStateToProps, { 171 | retrieveTutorials, 172 | findTutorialsByTitle, 173 | deleteAllTutorials, 174 | })(TutorialsList); 175 | -------------------------------------------------------------------------------- /src/http-common.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default axios.create({ 4 | baseURL: "http://localhost:8080/api", 5 | headers: { 6 | "Content-type": "application/json" 7 | } 8 | }); -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import { Provider } from 'react-redux'; 6 | import store from './store'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import tutorials from "./tutorials"; 3 | 4 | export default combineReducers({ 5 | tutorials, 6 | }); 7 | -------------------------------------------------------------------------------- /src/reducers/tutorials.js: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_TUTORIAL, 3 | RETRIEVE_TUTORIALS, 4 | UPDATE_TUTORIAL, 5 | DELETE_TUTORIAL, 6 | DELETE_ALL_TUTORIALS, 7 | } from "../actions/types"; 8 | 9 | const initialState = []; 10 | 11 | function tutorialReducer(tutorials = initialState, action) { 12 | const { type, payload } = action; 13 | 14 | switch (type) { 15 | case CREATE_TUTORIAL: 16 | return [...tutorials, payload]; 17 | 18 | case RETRIEVE_TUTORIALS: 19 | return payload; 20 | 21 | case UPDATE_TUTORIAL: 22 | return tutorials.map((tutorial) => { 23 | if (tutorial.id === payload.id) { 24 | return { 25 | ...tutorial, 26 | ...payload, 27 | }; 28 | } else { 29 | return tutorial; 30 | } 31 | }); 32 | 33 | case DELETE_TUTORIAL: 34 | return tutorials.filter(({ id }) => id !== payload.id); 35 | 36 | case DELETE_ALL_TUTORIALS: 37 | return []; 38 | 39 | default: 40 | return tutorials; 41 | } 42 | }; 43 | 44 | export default tutorialReducer; -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/services/tutorial.service.js: -------------------------------------------------------------------------------- 1 | import http from "../http-common"; 2 | 3 | class TutorialDataService { 4 | getAll() { 5 | return http.get("/tutorials"); 6 | } 7 | 8 | get(id) { 9 | return http.get(`/tutorials/${id}`); 10 | } 11 | 12 | create(data) { 13 | return http.post("/tutorials", data); 14 | } 15 | 16 | update(id, data) { 17 | return http.put(`/tutorials/${id}`, data); 18 | } 19 | 20 | delete(id) { 21 | return http.delete(`/tutorials/${id}`); 22 | } 23 | 24 | deleteAll() { 25 | return http.delete(`/tutorials`); 26 | } 27 | 28 | findByTitle(title) { 29 | return http.get(`/tutorials?title=${title}`); 30 | } 31 | } 32 | 33 | export default new TutorialDataService(); -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import thunk from 'redux-thunk'; 4 | import rootReducer from './reducers'; 5 | 6 | const initialState = {}; 7 | 8 | const middleware = [thunk]; 9 | 10 | const store = createStore( 11 | rootReducer, 12 | initialState, 13 | composeWithDevTools(applyMiddleware(...middleware)) 14 | ); 15 | 16 | export default store; --------------------------------------------------------------------------------