├── .env
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── img
│ ├── sort-icon.png
│ └── placeholder-user.jpg
├── setupTests.js
├── tests
│ └── components
│ │ ├── Header.test.js
│ │ ├── Loader.test.js
│ │ └── __snapshots__
│ │ ├── Header.test.js.snap
│ │ └── Loader.test.js.snap
├── components
│ ├── Footer
│ │ ├── style.scss
│ │ └── index.js
│ ├── Header
│ │ ├── style.scss
│ │ └── index.js
│ ├── Loader
│ │ ├── index.js
│ │ └── style.scss
│ ├── Modal
│ │ ├── index.js
│ │ └── style.scss
│ ├── Pagination
│ │ ├── style.scss
│ │ └── index.js
│ ├── Search
│ │ ├── style.scss
│ │ └── index.js
│ ├── DeleteUser
│ │ └── index.js
│ ├── DataTable
│ │ ├── style.scss
│ │ └── index.js
│ ├── UpdateUser
│ │ └── index.js
│ └── CreateUser
│ │ └── index.js
├── index.css
├── store
│ └── reducers
│ │ └── rootReducer.js
├── index.js
├── app
│ └── api.js
├── app.scss
├── serviceWorker.js
└── App.js
├── .gitignore
├── README.md
├── .github
└── workflows
│ └── config.yml
└── package.json
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_REQRES_API = "https://reqres.in/api"
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hasankemaldemirci/react-crud-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hasankemaldemirci/react-crud-app/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hasankemaldemirci/react-crud-app/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/img/sort-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hasankemaldemirci/react-crud-app/HEAD/src/img/sort-icon.png
--------------------------------------------------------------------------------
/src/img/placeholder-user.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hasankemaldemirci/react-crud-app/HEAD/src/img/placeholder-user.jpg
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme'
2 | import Adapter from 'enzyme-adapter-react-16'
3 |
4 | import 'jest-enzyme'
5 |
6 | Enzyme.configure({ adapter: new Adapter() })
--------------------------------------------------------------------------------
/src/tests/components/Header.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 |
4 | import Header from '../../components/Header/index'
5 |
6 | test('Should render Header correctly', () => {
7 | const wrapper = shallow()
8 | expect(wrapper).toMatchSnapshot()
9 | })
--------------------------------------------------------------------------------
/src/tests/components/Loader.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 |
4 | import Loader from '../../components/Loader/index'
5 |
6 | test('Should render Loader correctly', () => {
7 | const wrapper = shallow()
8 | expect(wrapper).toMatchSnapshot()
9 | })
--------------------------------------------------------------------------------
/src/components/Footer/style.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | position: fixed;
3 | bottom: 0;
4 | width: 100%;
5 | height: 50px;
6 | background-color: #eee;
7 | text-align: center;
8 | line-height: 50px;
9 | box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.15);
10 | z-index: 10;
11 | .copyright {
12 | color: #555;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Header/style.scss:
--------------------------------------------------------------------------------
1 | header {
2 | position: fixed;
3 | top: 0;
4 | width: 100%;
5 | height: 50px;
6 | background-color: #eee;
7 | line-height: 50px;
8 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
9 | z-index: 10;
10 | .logo {
11 | font-size: 18px;
12 | font-weight: 600;
13 | line-height: 1em;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Styles
4 | import "./style.scss";
5 |
6 | const Loader = () => {
7 | return (
8 |
9 |
12 |
13 | );
14 | };
15 |
16 | export default Loader;
17 |
--------------------------------------------------------------------------------
/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Styles
4 | import "./style.scss";
5 |
6 | const Footer = () => {
7 | return (
8 |
13 | );
14 | };
15 |
16 | export default Footer;
17 |
--------------------------------------------------------------------------------
/src/tests/components/__snapshots__/Header.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Should render Header correctly 1`] = `
4 |
18 | `;
19 |
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Styles
4 | import "./style.scss";
5 |
6 | const Header = () => {
7 | return (
8 |
15 | );
16 | };
17 |
18 | export default Header;
19 |
--------------------------------------------------------------------------------
/src/tests/components/__snapshots__/Loader.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Should render Loader correctly 1`] = `
4 |
7 |
17 |
18 | `;
19 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/store/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | users: []
3 | };
4 |
5 | const rootReducer = (state = initialState, action) => {
6 | switch (action.type) {
7 | case "SET_USERS":
8 | return { ...state, users: action.data };
9 | case "CREATE_USER":
10 | return { ...state, users: [...state.users, action.data] };
11 | default:
12 | return state;
13 | }
14 | };
15 |
16 | export default rootReducer;
17 |
--------------------------------------------------------------------------------
/src/components/Modal/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Styles
4 | import "./style.scss";
5 |
6 | const Modal = props => {
7 | const { children, activeModal } = props;
8 |
9 | return (
10 |
11 |
12 |
{activeModal.name}
13 |
{children}
14 |
15 |
16 | );
17 | };
18 |
19 | export default Modal;
20 |
--------------------------------------------------------------------------------
/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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 | React CRUD App
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { createStore } from "redux";
4 | import { Provider } from "react-redux";
5 | import { composeWithDevTools } from "redux-devtools-extension";
6 | import App from "./App";
7 | import * as serviceWorker from "./serviceWorker";
8 | import rootReducer from "./store/reducers/rootReducer";
9 |
10 | // Sweet Alert 2
11 | import Swal from "sweetalert2";
12 | import withReactContent from "sweetalert2-react-content";
13 |
14 | const MySwal = withReactContent(Swal);
15 | export default MySwal;
16 |
17 | const store = createStore(rootReducer, composeWithDevTools());
18 |
19 | ReactDOM.render(
20 |
21 |
22 | ,
23 | document.getElementById("root")
24 | );
25 |
26 | serviceWorker.unregister();
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://sonarcloud.io/dashboard?id=hasankemaldemirci_react-crud-app)
3 |
4 | # React CRUD App Example
5 |
6 | > A CRUD application which edits user records in React.
7 | Live Demo : https://react-crud-app-example.herokuapp.com
8 |
9 | ## Build Setup
10 |
11 | ```bash
12 | # install project
13 | $ git clone https://github.com/hasankemaldemirci/react-crud-app.git
14 |
15 | # open app directory
16 | $ cd react-crud-app
17 |
18 | # install dependencies
19 | $ npm i || npm install
20 |
21 | # serve with hot reload at localhost
22 | $ npm run dev
23 |
24 | # build for production
25 | $ npm build
26 | ```
27 |
--------------------------------------------------------------------------------
/.github/workflows/config.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | test:
14 | name: Tests
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [13.x, 14.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm ci
28 | - run: npm test
29 | env:
30 | CI: true
31 |
32 |
--------------------------------------------------------------------------------
/src/components/Modal/style.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | top: 0;
7 | left: 0;
8 | width: 100%;
9 | height: 100%;
10 | background-color: rgba(0, 0, 0, 0.75);
11 | z-index: 99;
12 | &-content {
13 | width: 90%;
14 | max-width: 500px;
15 | background-color: #fff;
16 | border-radius: 5px;
17 | padding: 30px;
18 | box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
19 | &__title {
20 | font-size: 26px;
21 | font-weight: 700;
22 | padding-bottom: 5px;
23 | border-bottom: 1px solid #e3e3e3;
24 | margin-bottom: 20px;
25 | }
26 | input {
27 | width: 100%;
28 | }
29 | }
30 | @media only screen and (max-width: 767px) {
31 | &-content {
32 | max-height: calc(100vh - 30px);
33 | overflow-y: auto;
34 | -webkit-overflow-scrolling: touch;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const apiURL = process.env.REACT_APP_REQRES_API;
4 |
5 | function getUsers() {
6 | const response = axios.get(`${apiURL}/users`);
7 |
8 | return response;
9 | }
10 |
11 | function getCreatedUser({ first_name, last_name, email }) {
12 | const response = axios.post(`${apiURL}/users`, {
13 | email,
14 | first_name,
15 | last_name
16 | });
17 |
18 | return response;
19 | }
20 |
21 | function getUpdatedUser(id, user) {
22 | const response = axios.put(`${apiURL}/users/${id}`, {
23 | avatar: user.avatar,
24 | id: id,
25 | email: user.email,
26 | first_name: user.first_name,
27 | last_name: user.last_name
28 | });
29 |
30 | return response;
31 | }
32 |
33 | function getDeletedUser(id) {
34 | const response = axios.delete(`${apiURL}/users/${id}`);
35 |
36 | return response;
37 | }
38 |
39 | export { getUsers, getCreatedUser, getUpdatedUser, getDeletedUser };
40 |
--------------------------------------------------------------------------------
/src/components/Pagination/style.scss:
--------------------------------------------------------------------------------
1 | .pagination {
2 | display: flex;
3 | justify-content: center;
4 | flex-wrap: wrap;
5 | width: 100%;
6 | margin-top: 20px;
7 | margin-bottom: 10px;
8 | &__pager {
9 | margin-left: 5px;
10 | margin-right: 5px;
11 | list-style: none;
12 | button {
13 | width: 80px;
14 | height: 35px;
15 | background: transparent;
16 | border: 1px solid #ccc;
17 | border-radius: 3px;
18 | font-size: 14px;
19 | color: #333;
20 | }
21 | &--number {
22 | button {
23 | width: 35px;
24 | padding-left: 5px;
25 | padding-right: 5px;
26 | }
27 | }
28 | &--active {
29 | button {
30 | background-color: #e3e3e3;
31 | font-weight: 500;
32 | }
33 | }
34 | }
35 | @media only screen and (max-width: 767px) {
36 | &__pager {
37 | &--number {
38 | display: none;
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/components/Search/style.scss:
--------------------------------------------------------------------------------
1 | .search-form {
2 | display: flex;
3 | align-items: center;
4 | .primary-btn {
5 | margin-left: 10px;
6 | }
7 | .reset-search-btn {
8 | display: inline-flex;
9 | align-items: center;
10 | margin-left: 15px;
11 | font-size: 14px;
12 | font-weight: 600;
13 | color: #666;
14 | cursor: pointer;
15 | strong {
16 | width: 20px;
17 | height: 20px;
18 | margin-left: 5px;
19 | background: var(--secondary-color);
20 | border-radius: 3px;
21 | font-size: 14px;
22 | text-align: center;
23 | line-height: 20px;
24 | color: #fff;
25 | }
26 | }
27 | @media only screen and (max-width: 480px) {
28 | flex-direction: column;
29 | width: 100%;
30 | .form-group {
31 | display: flex;
32 | width: 100%;
33 | input {
34 | flex-grow: 1;
35 | }
36 | }
37 | .reset-search-btn {
38 | margin-top: 10px;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/DeleteUser/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | const DeleteUser = props => {
4 | const [user, setUser] = useState(props.currentUser);
5 |
6 | const cancel = event => {
7 | event.preventDefault();
8 | props.setActiveModal({ active: false });
9 | };
10 |
11 | useEffect(() => {
12 | setUser(props.currentUser);
13 | }, [props]);
14 |
15 | return (
16 |
32 | );
33 | };
34 |
35 | export default DeleteUser;
36 |
--------------------------------------------------------------------------------
/src/components/Loader/style.scss:
--------------------------------------------------------------------------------
1 | .loader {
2 | position: absolute;
3 | top: 50%;
4 | left: 50%;
5 | transform: translate(-50%, -50%);
6 | width: 100%;
7 | max-width: 50px;
8 | text-align: center;
9 | svg {
10 | animation: 2s linear infinite svg-animation;
11 | max-width: 50px;
12 | }
13 | }
14 |
15 | @keyframes svg-animation {
16 | 0% {
17 | transform: rotateZ(0deg);
18 | }
19 | 100% {
20 | transform: rotateZ(360deg);
21 | }
22 | }
23 |
24 | circle {
25 | animation: 1.4s ease-in-out infinite both circle-animation;
26 | display: block;
27 | fill: transparent;
28 | stroke: var(--secondary-color);
29 | stroke-linecap: round;
30 | stroke-dasharray: 283;
31 | stroke-dashoffset: 280;
32 | stroke-width: 10px;
33 | transform-origin: 50% 50%;
34 | }
35 |
36 | // Circle animation.
37 | @keyframes circle-animation {
38 | 0%,
39 | 25% {
40 | stroke-dashoffset: 280;
41 | transform: rotate(0);
42 | }
43 |
44 | 50%,
45 | 75% {
46 | stroke-dashoffset: 75;
47 | transform: rotate(45deg);
48 | }
49 |
50 | 100% {
51 | stroke-dashoffset: 280;
52 | transform: rotate(360deg);
53 | }
54 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-crud-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.21.1",
7 | "node-sass": "^4.14.1",
8 | "react": "^16.13.1",
9 | "react-dom": "^16.13.1",
10 | "react-redux": "^7.2.1",
11 | "react-scripts": "3.4.3",
12 | "redux": "^4.0.5",
13 | "serve": "^11.3.2",
14 | "sweetalert2": "^9.17.1",
15 | "sweetalert2-react-content": "^3.0.3"
16 | },
17 | "scripts": {
18 | "dev": "react-scripts start",
19 | "start": "serve -s build",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject",
23 | "heroku-postbuild": "npm run build"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "devDependencies": {
41 | "enzyme": "^3.11.0",
42 | "enzyme-adapter-react-16": "^1.15.3",
43 | "jest-enzyme": "^7.1.2",
44 | "redux-devtools": "^3.6.1",
45 | "redux-devtools-extension": "^2.13.8"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/DataTable/style.scss:
--------------------------------------------------------------------------------
1 | .table-wrapper {
2 | overflow-x: auto;
3 | -webkit-overflow-scrolling: touch;
4 | }
5 |
6 | .data-table {
7 | width: 100%;
8 | min-width: 767px;
9 | border-collapse: collapse;
10 | thead {
11 | border-bottom: 1px solid #eee;
12 | }
13 | tr:nth-of-type(even) {
14 | background: #eee;
15 | }
16 | td,
17 | th {
18 | padding: 8px 10px;
19 | text-align: left;
20 | }
21 | .column-sort {
22 | display: flex;
23 | align-items: center;
24 | justify-content: space-between;
25 | cursor: pointer;
26 | img {
27 | max-width: 12px;
28 | }
29 | }
30 | .field-id {
31 | width: 1em;
32 | }
33 | .field-avatar {
34 | width: 1em;
35 | img {
36 | max-width: 64px;
37 | border-radius: 5px;
38 | }
39 | }
40 | .field-actions {
41 | button {
42 | width: 80px;
43 | height: 40px;
44 | border-radius: 5px;
45 | font-weight: 600;
46 | color: #fff;
47 | margin-left: 5px;
48 | margin-right: 5px;
49 | }
50 | &__update {
51 | background-color: var(--secondary-color);
52 | }
53 | &__delete {
54 | background-color: var(--danger-color);
55 | }
56 | }
57 | .no-record-message {
58 | font-weight: 600;
59 | text-align: center;
60 | }
61 | @media only screen and (max-width: 767px) {
62 | .no-record-message {
63 | text-align: left;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/Search/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | // Styles
4 | import "./style.scss";
5 |
6 | const Search = props => {
7 | const [searchTerm, setSearchTerm] = useState("");
8 | const [toggleReset, setToggleReset] = useState(false);
9 |
10 | const onInputChange = event => {
11 | const value = event.target.value;
12 | setSearchTerm(value);
13 | if (!value.length) {
14 | props.search(value);
15 | }
16 | };
17 |
18 | const handleSearchReset = () => {
19 | setToggleReset(false);
20 | props.resetSearch("");
21 | };
22 |
23 | const handleSubmit = event => {
24 | event.preventDefault();
25 | setSearchTerm("");
26 |
27 | if (searchTerm.length > 2) {
28 | setToggleReset(true);
29 | } else {
30 | setToggleReset(false);
31 | }
32 |
33 | props.search(searchTerm);
34 | };
35 |
36 | return (
37 |
57 | );
58 | };
59 |
60 | export default Search;
61 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | // Styles
4 | import "./style.scss";
5 |
6 | const Pagination = ({ totalResults, currentPage, paginate, pageSize }) => {
7 | const [pageNumbers, setPageNumbers] = useState([]);
8 |
9 | // Set Page Numbers
10 | useEffect(() => {
11 | const totalPageCount = Math.ceil(totalResults / pageSize);
12 | const page = [];
13 |
14 | for (let index = 1; index <= totalPageCount; index++) {
15 | page.push(index);
16 | }
17 |
18 | setPageNumbers(page);
19 | }, [totalResults, currentPage, pageSize]);
20 |
21 | return (
22 |
23 | {currentPage > 1 && totalResults > 5 && (
24 | -
25 |
26 |
27 | )}
28 | {pageNumbers.map((page, index) => {
29 | return (
30 | -
37 |
38 |
39 | );
40 | })}
41 | {currentPage < pageNumbers.length && (
42 | -
43 |
44 |
45 | )}
46 |
47 | );
48 | }
49 |
50 | export default Pagination;
51 |
--------------------------------------------------------------------------------
/src/components/UpdateUser/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | const UpdateUser = props => {
4 | const [user, setUser] = useState(props.currentUser);
5 |
6 | const onInputChange = event => {
7 | const { name, value } = event.target;
8 |
9 | setUser({ ...user, [name]: value });
10 | };
11 |
12 | const cancel = event => {
13 | event.preventDefault();
14 | props.setActiveModal({ active: false });
15 | };
16 |
17 | useEffect(() => {
18 | setUser(props.currentUser);
19 | }, [props]);
20 |
21 | return (
22 |
62 | );
63 | };
64 |
65 | export default UpdateUser;
66 |
--------------------------------------------------------------------------------
/src/components/CreateUser/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const CreateUser = props => {
4 | const initialData = { id: null, first_name: "", last_name: "", email: "" };
5 | const [user, setUser] = useState(initialData);
6 |
7 | const onInputChange = event => {
8 | const { name, value } = event.target;
9 |
10 | setUser({ ...user, [name]: value });
11 | };
12 |
13 | const cancel = event => {
14 | event.preventDefault();
15 | props.setActiveModal({ active: false });
16 | };
17 |
18 | return (
19 |
60 | );
61 | };
62 |
63 | export default CreateUser;
64 |
--------------------------------------------------------------------------------
/src/components/DataTable/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Styles
4 | import "./style.scss";
5 |
6 | // Images
7 | import PlaceholderImg from "../../img/placeholder-user.jpg";
8 | import SortIcon from "../../img/sort-icon.png";
9 |
10 | const DataTable = props => {
11 | return (
12 |
91 | );
92 | };
93 |
94 | export default DataTable;
95 |
--------------------------------------------------------------------------------
/src/app.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-color: #404040;
3 | --secondary-color: #4d4dff;
4 | --danger-color: #b33636;
5 | --font: "-apple-system, BlinkMacSystemFont", "Segoe UI", "Roboto", "Oxygen",
6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
7 | sans-serif;
8 | }
9 |
10 | html {
11 | box-sizing: border-box;
12 | }
13 |
14 | body {
15 | margin: 0;
16 | font-family: var(--font);
17 | -webkit-font-smoothing: antialiased;
18 | -moz-osx-font-smoothing: grayscale;
19 | color: var(--primary-color);
20 | }
21 |
22 | *,
23 | *:before,
24 | *:after {
25 | box-sizing: inherit;
26 | }
27 |
28 | a {
29 | text-decoration: none;
30 | color: var(--primary-color);
31 | transition: 0.2s;
32 | &:hover {
33 | color: var(--secondary-color);
34 | }
35 | }
36 |
37 | ol,
38 | ul {
39 | margin: 0;
40 | padding: 0;
41 | list-style: none;
42 | }
43 |
44 | button {
45 | font-family: var(--font);
46 | background-color: transparent;
47 | border: none;
48 | padding: 0;
49 | cursor: pointer;
50 | }
51 |
52 | img {
53 | display: block;
54 | max-width: 100%;
55 | height: auto;
56 | }
57 |
58 | figure {
59 | margin: 0;
60 | }
61 |
62 | input {
63 | font-family: var(--font);
64 | }
65 |
66 | .container {
67 | width: 100%;
68 | max-width: 1170px;
69 | margin-left: auto;
70 | margin-right: auto;
71 | padding-left: 15px;
72 | padding-right: 15px;
73 | }
74 |
75 | .hero {
76 | padding-top: 20px;
77 | padding-bottom: 20px;
78 | background-color: #eee;
79 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
80 | h1 {
81 | margin: 0;
82 | font-size: 26px;
83 | }
84 | }
85 |
86 | .content {
87 | min-height: 100vh;
88 | padding-top: 70px;
89 | padding-bottom: 70px;
90 | }
91 |
92 | .primary-btn {
93 | height: 40px;
94 | padding-left: 15px;
95 | padding-right: 15px;
96 | background-color: var(--secondary-color);
97 | border-radius: 5px;
98 | font-weight: 600;
99 | color: #fff;
100 | }
101 |
102 | .form-group {
103 | & + .form-group {
104 | margin-top: 20px;
105 | &--actions {
106 | margin-top: 30px;
107 | }
108 | }
109 | label {
110 | display: block;
111 | margin-bottom: 5px;
112 | font-size: 14px;
113 | font-weight: 500;
114 | opacity: 0.75;
115 | }
116 | input {
117 | height: 40px;
118 | padding-left: 15px;
119 | padding-right: 15px;
120 | border-radius: 3px;
121 | border: 1px solid #ccc;
122 | font-size: 14px;
123 | }
124 | &--actions {
125 | display: flex;
126 | justify-content: flex-end;
127 | button {
128 | width: 80px;
129 | height: 40px;
130 | border-radius: 5px;
131 | font-weight: 600;
132 | color: #fff;
133 | margin-left: 10px;
134 | }
135 | .primary-btn {
136 | background-color: var(--secondary-color);
137 | }
138 | .cancel-btn {
139 | background-color: var(--danger-color);
140 | }
141 | }
142 | }
143 |
144 | .toolbar {
145 | display: flex;
146 | align-items: center;
147 | justify-content: space-between;
148 | margin-bottom: 20px;
149 | @media only screen and (max-width: 480px) {
150 | flex-direction: column-reverse;
151 | > .primary-btn {
152 | width: 100%;
153 | margin-left: 0;
154 | margin-bottom: 15px;
155 | }
156 | }
157 | }
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import {
4 | getUsers,
5 | getCreatedUser,
6 | getUpdatedUser,
7 | getDeletedUser
8 | } from "./app/api";
9 |
10 | // Styles
11 | import "./app.scss";
12 |
13 | // Components
14 | import Header from "./components/Header";
15 | import Footer from "./components/Footer";
16 | import DataTable from "./components/DataTable";
17 | import CreateUser from "./components/CreateUser";
18 | import UpdateUser from "./components/UpdateUser";
19 | import DeleteUser from "./components/DeleteUser";
20 | import Modal from "./components/Modal";
21 | import Search from "./components/Search";
22 | import Pagination from "./components/Pagination";
23 | import Loader from "./components/Loader";
24 | import MySwal from "./index";
25 |
26 | function App() {
27 | const dispatch = useDispatch();
28 | const users = useSelector(state => state.users);
29 |
30 | const [loading, setLoading] = useState(false);
31 |
32 | const [currentUser, setCurrentUser] = useState({
33 | id: null,
34 | avatar: null,
35 | first_name: "",
36 | last_name: "",
37 | email: ""
38 | });
39 | const [activeModal, setActiveModal] = useState({ name: "", active: false });
40 | const [savedUsers, setSavedUsers] = useState(users);
41 | const [pageSize] = useState(5);
42 | const [currentPage, setCurrentPage] = useState(1);
43 | const [sorted, setSorted] = useState(false);
44 |
45 | const usersLastIndex = currentPage * pageSize;
46 | const usersFirstIndex = usersLastIndex - pageSize;
47 | const currentUsers = users.slice(usersFirstIndex, usersLastIndex);
48 |
49 | // Setting up Modal
50 | const setModal = modal => {
51 | search("");
52 | setActiveModal({ name: modal, active: true });
53 | };
54 |
55 | // Pagination
56 | const paginate = page => {
57 | setCurrentPage(page);
58 | };
59 |
60 | // Search
61 | const search = term => {
62 | if (term.length > 2) {
63 | setCurrentPage(1);
64 |
65 | const results = savedUsers.filter(user =>
66 | Object.keys(user).some(key =>
67 | user[key]
68 | .toString()
69 | .toLowerCase()
70 | .includes(term.toString().toLowerCase())
71 | )
72 | );
73 |
74 | dispatch({ type: "SET_USERS", data: results });
75 | } else if (!term.length) {
76 | dispatch({ type: "SET_USERS", data: savedUsers });
77 | }
78 | };
79 |
80 | // Sorting
81 | const sorting = key => {
82 | setSorted(!sorted);
83 | switch (key) {
84 | case "name":
85 | const nameSort = [...savedUsers].sort((a, b) => {
86 | return sorted
87 | ? a.first_name.localeCompare(b.first_name, "tr")
88 | : b.first_name.localeCompare(a.first_name, "tr");
89 | });
90 | dispatch({ type: "SET_USERS", data: nameSort });
91 | return;
92 | case "surname":
93 | const surnameSort = [...savedUsers].sort((a, b) => {
94 | return sorted
95 | ? a.last_name.localeCompare(b.last_name, "tr")
96 | : b.last_name.localeCompare(a.last_name, "tr");
97 | });
98 | dispatch({ type: "SET_USERS", data: surnameSort });
99 | return;
100 | case "email":
101 | const emailSort = [...savedUsers].sort((a, b) => {
102 | return sorted
103 | ? a.email.localeCompare(b.email, "tr")
104 | : b.email.localeCompare(a.email, "tr");
105 | });
106 | dispatch({ type: "SET_USERS", data: emailSort });
107 | return;
108 | default:
109 | break;
110 | }
111 | };
112 |
113 | // Create User
114 | const createUser = async user => {
115 | setActiveModal(false);
116 | setLoading(true);
117 |
118 | try {
119 | await getCreatedUser(user).then(res => {
120 | const result = res.data;
121 | MySwal.fire({
122 | icon: "success",
123 | title: "User created successfully."
124 | }).then(() => {
125 | dispatch({ type: "CREATE_USER", data: result });
126 | setSavedUsers([...users, result]);
127 | });
128 | });
129 | } catch (err) {
130 | MySwal.fire({
131 | icon: "error",
132 | title: "Failed to create user."
133 | });
134 | } finally {
135 | setLoading(false);
136 | }
137 | };
138 |
139 | // Update User
140 | const updateRow = user => {
141 | setModal("Update User");
142 |
143 | setCurrentUser({
144 | id: user.id,
145 | avatar: user.avatar,
146 | first_name: user.first_name,
147 | last_name: user.last_name,
148 | email: user.email
149 | });
150 | };
151 |
152 | const updateUser = async (id, updatedUser) => {
153 | setActiveModal(false);
154 | setLoading(true);
155 |
156 | try {
157 | await getUpdatedUser(id, updatedUser).then(res => {
158 | const result = res.data;
159 | MySwal.fire({
160 | icon: "success",
161 | title: "User updated successfully."
162 | }).then(() => {
163 | dispatch({
164 | type: "SET_USERS",
165 | data: users.map(user =>
166 | user.id === id ? Object.assign(user, result) : user
167 | )
168 | });
169 | });
170 | });
171 | } catch (err) {
172 | MySwal.fire({
173 | icon: "error",
174 | title: "Failed to update user."
175 | });
176 | } finally {
177 | setLoading(false);
178 | }
179 | };
180 |
181 | // Delete User
182 | const deleteRow = user => {
183 | setModal("Delete User");
184 |
185 | setCurrentUser({
186 | id: user.id,
187 | avatar: user.avatar,
188 | first_name: user.first_name,
189 | last_name: user.last_name,
190 | email: user.email
191 | });
192 | };
193 |
194 | const deleteUser = async id => {
195 | setActiveModal(false);
196 | setLoading(true);
197 |
198 | try {
199 | await getDeletedUser(id).then(() => {
200 | MySwal.fire({
201 | icon: "success",
202 | title: "User deleted successfully."
203 | }).then(() => {
204 | dispatch({
205 | type: "SET_USERS",
206 | data: users.filter(user => user.id !== id)
207 | });
208 | setSavedUsers(savedUsers.filter(user => user.id !== id));
209 | setCurrentPage(1);
210 | });
211 | });
212 | } catch (err) {
213 | MySwal.fire({
214 | icon: "error",
215 | title: "Failed to delete user."
216 | });
217 | } finally {
218 | setLoading(false);
219 | }
220 | };
221 |
222 | // Fetch Users
223 | const fetchUsers = async () => {
224 | setLoading(true);
225 |
226 | try {
227 | await getUsers().then(({ data }) => {
228 | setSavedUsers(data.data);
229 | dispatch({ type: "SET_USERS", data: data.data });
230 | });
231 | } catch (err) {
232 | MySwal.fire({
233 | icon: "error",
234 | title: "Failed to fetch users."
235 | });
236 | } finally {
237 | setTimeout(() => {
238 | setLoading(false);
239 | }, 500);
240 | }
241 | };
242 |
243 | useEffect(() => {
244 | fetchUsers();
245 | }, []);
246 |
247 | return (
248 |
249 |
250 |
251 |
252 | {loading ? (
253 |
254 | ) : (
255 |
256 |
257 |
258 |
264 |
265 |
271 |
277 |
278 | )}
279 |
280 |
281 | {activeModal.active && (
282 |
283 | {activeModal.name === "Create User" && (
284 |
288 | )}
289 | {activeModal.name === "Update User" && (
290 |
295 | )}
296 | {activeModal.name === "Delete User" && (
297 |
302 | )}
303 |
304 | )}
305 |
306 |
307 | );
308 | }
309 |
310 | export default App;
311 |
--------------------------------------------------------------------------------