├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── deploy.sh ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── assets └── logo.svg ├── components ├── App.jsx ├── App.scss ├── App.test.js ├── Header │ ├── Footer.jsx │ ├── Header.jsx │ └── Header.scss ├── MainView │ ├── Layout.jsx │ ├── MainView.jsx │ └── MainView.scss ├── UserDetails │ ├── UserDetails.jsx │ └── UserDetails.scss └── UserList │ ├── UserList.jsx │ └── UserList.scss ├── index.js ├── index.scss ├── services └── userService.js └── setupTests.js /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://localhost:8080 -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL= -------------------------------------------------------------------------------- /.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 | 25 | .idea 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React User Manager 2 | This project is a React application providing basic user management functionalities - adding, filtering, and editing users. 3 | 4 | It was developed as an example frontend application for aws deployment practice. 5 | 6 | ![React User Manager Screen](./public/screen.png) 7 | 8 | ## Available Scripts 9 | 10 | In the project directory, you can run: 11 | 12 | ### `npm start` 13 | 14 | Runs the app in the development mode.
15 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 16 | 17 | The page will reload if you make edits.
18 | You will also see any lint errors in the console. 19 | 20 | ### `npm test` 21 | 22 | Launches the test runner in the interactive watch mode.
23 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 24 | 25 | ### `npm run build` 26 | 27 | Builds the app for production to the `build` folder.
28 | It correctly bundles React in production mode and optimizes the build for the best performance. 29 | 30 | The build is minified and the filenames include the hashes.
31 | Your app is ready to be deployed! 32 | 33 | ##### This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 34 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ -z "$1" ]; then 4 | printf "Usage $0 server_url\n" 5 | exit 1 6 | fi 7 | 8 | server_url=$1 9 | 10 | export REACT_APP_API_URL=${server_url} 11 | 12 | npm install 13 | 14 | npm run build 15 | 16 | printf "Stopping process running on port 5000 if exists\n" 17 | sudo fuser -k 5000/tcp 18 | 19 | printf "Running the application\n" 20 | npx serve -s -n build 21 | 22 | printf "Exit" 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-user-manager", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "bootstrap": "^4.4.1", 11 | "fuzzy": "^0.1.3", 12 | "lodash": "^4.17.15", 13 | "node-sass": "^4.13.1", 14 | "react": "^16.13.0", 15 | "react-dom": "^16.13.0", 16 | "react-scripts": "3.4.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderprovider/React-User-Manage/b18fb512cd25f94d7fd65df6b0e372c6b8d40649/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/coderprovider/React-User-Manage/b18fb512cd25f94d7fd65df6b0e372c6b8d40649/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderprovider/React-User-Manage/b18fb512cd25f94d7fd65df6b0e372c6b8d40649/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 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.css"; 2 | import React from "react"; 3 | import "./App.scss"; 4 | import "./Header/Header"; 5 | import Header from "./Header/Header"; 6 | import MainView from "./MainView/MainView"; 7 | 8 | function App() { 9 | return ( 10 |
11 |
12 | 13 |
14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/components/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | } -------------------------------------------------------------------------------- /src/components/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/Header/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "../../assets/logo.svg"; 3 | import "./Header.scss"; 4 | 5 | const Header = () => { 6 | return ( 7 |
8 | logo 9 |
React User Manager
10 | logo 11 | {/* logo */} 12 |
13 | ); 14 | }; 15 | 16 | export default Header; 17 | -------------------------------------------------------------------------------- /src/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "../../assets/logo.svg"; 3 | import "./Header.scss"; 4 | 5 | const Header = () => { 6 | return ( 7 |
8 | logo 9 |
10 |
React User Manager
11 |
12 | logo 13 |
14 | ); 15 | }; 16 | 17 | export default Header; 18 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .App-logo { 2 | height: 5vmin; 3 | pointer-events: none; 4 | } 5 | 6 | @media (prefers-reduced-motion: no-preference) { 7 | .App-logo { 8 | animation: App-logo-spin infinite 20s linear; 9 | } 10 | } 11 | 12 | .App-header { 13 | background-color: #282c34; 14 | min-height: 10vh; 15 | flex: 0 1 auto; 16 | display: flex; 17 | flex-direction: row; 18 | align-items: center; 19 | justify-content: center; 20 | font-size: calc(5px + 2vmin); 21 | color: white; 22 | } 23 | 24 | .App-link { 25 | color: #61dafb; 26 | } 27 | 28 | @keyframes App-logo-spin { 29 | from { 30 | transform: rotate(0deg); 31 | } 32 | to { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/MainView/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import _ from 'lodash' 3 | import fuzzy from "fuzzy"; 4 | import UserList from "../UserList/UserList"; 5 | import "./MainView.scss" 6 | import UserDetails from "../UserDetails/UserDetails"; 7 | import userService from "../../services/userService"; 8 | 9 | const NEW_USER = { 10 | name: '', 11 | email: '', 12 | password: '', 13 | address: {street: '', zipCode: '', city: '', country: ''} 14 | } 15 | 16 | class MainView extends Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | users: [], 21 | selectedUser: NEW_USER, 22 | isNewUser: true, 23 | filterPattern: '', 24 | } 25 | } 26 | 27 | filteredUsers = () => { 28 | return fuzzy 29 | .filter( 30 | this.state.filterPattern, 31 | this.state.users, 32 | { 33 | extract: (el) => el.name 34 | } 35 | ) 36 | .map((item) => item.original) 37 | } 38 | 39 | componentWillMount() { 40 | console.log('Will mount') 41 | userService.fetchUsers().then((users) => { 42 | this.setState({ 43 | ...this.state, 44 | users: [...users] 45 | }) 46 | }) 47 | } 48 | 49 | selectUser = (userId) => { 50 | this.setState({ 51 | ...this.state, 52 | isNewUser: false, 53 | selectedUser: _.find(this.state.users, {id: userId}) 54 | }) 55 | } 56 | 57 | handleNewUserButtonClick = () => { 58 | this.setState({ 59 | ...this.state, 60 | isNewUser: true, 61 | selectedUser: NEW_USER 62 | }) 63 | } 64 | 65 | saveNewUser = (newUser) => { 66 | userService.addUser(newUser).then(savedUser => { 67 | console.log("Saved", savedUser) 68 | this.setState({ 69 | ...this.state, 70 | isNewUser: false, 71 | selectedUser: savedUser, 72 | users: [...this.state.users, savedUser] 73 | }) 74 | }) 75 | } 76 | 77 | updateUser = (userToUpdate) => { 78 | userService.updateUser(userToUpdate).then(() => { 79 | const updatedUsers = [...this.state.users] 80 | updatedUsers[userToUpdate.id - 1] = userToUpdate 81 | this.setState({ 82 | ...this.state, 83 | users: updatedUsers 84 | }) 85 | }) 86 | } 87 | 88 | saveUser = (userToSave) => { 89 | if (this.state.isNewUser) { 90 | this.saveNewUser(userToSave) 91 | } else { 92 | this.updateUser(userToSave) 93 | } 94 | } 95 | 96 | onSelectedUserChange = (selectedUser) => { 97 | this.setState({ 98 | ...this.state, 99 | selectedUser: selectedUser 100 | }) 101 | } 102 | 103 | onFilterPatternChange = (pattern) => { 104 | this.setState({ 105 | ...this.state, 106 | filterPattern: pattern 107 | }) 108 | } 109 | 110 | render() { 111 | return ( 112 |
113 |
114 |
115 | User List 116 |
117 | 120 |
121 |
122 |
123 | {this.state.isNewUser ? "New User" : "User Details"} 124 |
125 | 127 |
128 |
129 | ) 130 | } 131 | }; 132 | 133 | export default MainView; -------------------------------------------------------------------------------- /src/components/MainView/MainView.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import _ from 'lodash' 3 | import fuzzy from "fuzzy"; 4 | import UserList from "../UserList/UserList"; 5 | import "./MainView.scss" 6 | import UserDetails from "../UserDetails/UserDetails"; 7 | import userService from "../../services/userService"; 8 | 9 | const NEW_USER = { 10 | name: '', 11 | email: '', 12 | password: '', 13 | address: {street: '', zipCode: '', city: '', country: ''} 14 | } 15 | 16 | class MainView extends Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | users: [], 21 | selectedUser: NEW_USER, 22 | isNewUser: true, 23 | filterPattern: '', 24 | } 25 | } 26 | 27 | filteredUsers = () => { 28 | return fuzzy 29 | .filter( 30 | this.state.filterPattern, 31 | this.state.users, 32 | { 33 | extract: (el) => el.name 34 | } 35 | ) 36 | .map((item) => item.original) 37 | } 38 | 39 | componentWillMount() { 40 | console.log('Will mount') 41 | userService.fetchUsers().then((users) => { 42 | this.setState({ 43 | ...this.state, 44 | users: [...users] 45 | }) 46 | }) 47 | } 48 | 49 | selectUser = (userId) => { 50 | this.setState({ 51 | ...this.state, 52 | isNewUser: false, 53 | selectedUser: _.find(this.state.users, {id: userId}) 54 | }) 55 | } 56 | 57 | handleNewUserButtonClick = () => { 58 | this.setState({ 59 | ...this.state, 60 | isNewUser: true, 61 | selectedUser: NEW_USER 62 | }) 63 | } 64 | 65 | saveNewUser = (newUser) => { 66 | userService.addUser(newUser).then(savedUser => { 67 | console.log("Saved", savedUser) 68 | this.setState({ 69 | ...this.state, 70 | isNewUser: false, 71 | selectedUser: savedUser, 72 | users: [...this.state.users, savedUser] 73 | }) 74 | }) 75 | } 76 | 77 | updateUser = (userToUpdate) => { 78 | userService.updateUser(userToUpdate).then(() => { 79 | const updatedUsers = [...this.state.users] 80 | updatedUsers[userToUpdate.id - 1] = userToUpdate 81 | this.setState({ 82 | ...this.state, 83 | users: updatedUsers 84 | }) 85 | }) 86 | } 87 | 88 | saveUser = (userToSave) => { 89 | if (this.state.isNewUser) { 90 | this.saveNewUser(userToSave) 91 | } else { 92 | this.updateUser(userToSave) 93 | } 94 | } 95 | 96 | onSelectedUserChange = (selectedUser) => { 97 | this.setState({ 98 | ...this.state, 99 | selectedUser: selectedUser 100 | }) 101 | } 102 | 103 | onFilterPatternChange = (pattern) => { 104 | this.setState({ 105 | ...this.state, 106 | filterPattern: pattern 107 | }) 108 | } 109 | 110 | // uploadImg = (newUser) => { 111 | // userService.addUser(newUser).then(savedUser => { 112 | // console.log("Saved", savedUser) 113 | // this.setState({ 114 | // ...this.state, 115 | // isNewUser: false, 116 | // selectedUser: savedUser, 117 | // users: [...this.state.users, savedUser] 118 | // }) 119 | // }) 120 | // } 121 | 122 | render() { 123 | return ( 124 |
125 |
126 |
127 | User List 128 |
129 | 132 |
133 |
134 |
135 | {this.state.isNewUser ? "New User" : "User Details"} 136 |
137 | 139 |
140 |
141 | ) 142 | } 143 | }; 144 | 145 | export default MainView; -------------------------------------------------------------------------------- /src/components/MainView/MainView.scss: -------------------------------------------------------------------------------- 1 | .main-view-container { 2 | margin: 3% 0 3%; 3 | display: flex; 4 | flex: 1 1 auto; 5 | flex-direction: row; 6 | height: 100%; 7 | width: 100%; 8 | max-width: 100%; 9 | } 10 | 11 | .left-column { 12 | flex: 0 1 50%; 13 | justify-content: center; 14 | align-items: center; 15 | border: 1px solid #61dafb; 16 | margin-left: 5%; 17 | 18 | } 19 | 20 | .right-column { 21 | flex: 0 1 50%; 22 | justify-content: center; 23 | align-items: center; 24 | border: 1px solid #61dafb; 25 | border-left-width: 0; 26 | margin-right: 5%; 27 | } 28 | 29 | .column-title { 30 | font-size: large; 31 | font-weight: 700; 32 | border-bottom: 1px solid #61dafb; 33 | height: 40px; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/UserDetails/UserDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./UserDetails.scss"; 3 | 4 | const UserDetails = (props) => { 5 | const updateName = (e) => { 6 | // props.onChange({...props.user, name: e.target.value}) 7 | props.onChange({ ...props.user, name: e.target.value }); 8 | }; 9 | 10 | const updateEmail = (e) => { 11 | props.onChange({ ...props.user, email: e.target.value }); 12 | }; 13 | 14 | const updatePassword = (e) => { 15 | props.onChange({ ...props.user, password: e.target.value }); 16 | }; 17 | 18 | const updateStreet = (e) => { 19 | props.onChange({ 20 | ...props.user, 21 | address: { ...props.user.address, street: e.target.value }, 22 | }); 23 | }; 24 | 25 | const updateZipCode = (e) => { 26 | props.onChange({ 27 | ...props.user, 28 | address: { ...props.user.address, zipCode: e.target.value }, 29 | }); 30 | }; 31 | 32 | const updateCity = (e) => { 33 | props.onChange({ 34 | ...props.user, 35 | address: { ...props.user.address, city: e.target.value }, 36 | }); 37 | }; 38 | 39 | const updateCountry = (e) => { 40 | props.onChange({ 41 | ...props.user, 42 | address: { ...props.user.address, country: e.target.value }, 43 | }); 44 | }; 45 | 46 | // const updateProfile = (e) => { 47 | // props.onChange({...props.user, address: {...props.user.address, "city": e.target.value}}) 48 | // } 49 | 50 | const saveUserDetails = (e) => { 51 | e.preventDefault(); 52 | props.onSave(props.user); 53 | }; 54 | return ( 55 |
56 |
57 |
58 | 59 | 67 |
68 |
69 |
70 |
71 | 72 | 80 |
81 |
82 | 83 | 91 |
92 |
93 |
94 | 95 | 103 |
104 |
105 |
106 | 107 | 114 |
115 |
116 | 117 | 124 |
125 |
126 | 127 | 134 |
135 |
136 | 143 |
144 | ); 145 | }; 146 | 147 | export default UserDetails; 148 | -------------------------------------------------------------------------------- /src/components/UserDetails/UserDetails.scss: -------------------------------------------------------------------------------- 1 | .user-details-form { 2 | margin: 3vmin; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/UserList/UserList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import "./UserList.scss" 3 | 4 | const UserList = (props) => { 5 | 6 | const handleFilterPatternChange = (e)=>{ 7 | props.onFilterPatternChange(e.target.value) 8 | } 9 | return ( 10 |
11 |
12 |
13 |
14 | New user 15 |
16 | 18 |
19 |
20 | {props.users && props.users.map((u) => 21 |
props.onUserSelect(u.id)}> 22 | {u.name} 23 |
24 | )} 25 |
26 | ) 27 | }; 28 | 29 | export default UserList; -------------------------------------------------------------------------------- /src/components/UserList/UserList.scss: -------------------------------------------------------------------------------- 1 | .top-container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-content: center; 6 | } 7 | 8 | .new-user-button { 9 | margin: auto; 10 | flex: 0 1 auto; 11 | width: 30%; 12 | } 13 | 14 | .user-search-box { 15 | margin-top: 2vmin; 16 | flex: 0 1 auto; 17 | } 18 | 19 | .user:hover { 20 | background: #c7d0d7; 21 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import './index.scss'; 5 | import App from './components/App'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | min-height: 100%; 9 | height: 100%; 10 | width: 100%; 11 | } 12 | 13 | #root { 14 | min-height: 100%; 15 | height: 100% 16 | 17 | } 18 | 19 | code { 20 | font-family: source-code-pro, Monaco, Consolas, 'Courier New', 21 | monospace; 22 | } 23 | 24 | * { 25 | -webkit-box-sizing: border-box; 26 | -moz-box-sizing: border-box; 27 | box-sizing: border-box; 28 | } 29 | 30 | button:focus { 31 | border-color: transparent !important; 32 | outline: none; 33 | } -------------------------------------------------------------------------------- /src/services/userService.js: -------------------------------------------------------------------------------- 1 | // For now users are stored in the array. Will be changed for the backend communication later 2 | import axios from 'axios' 3 | 4 | const fetchUsers = () => { 5 | return axios.get(process.env.REACT_APP_API_URL + '/users') 6 | .then(res => { 7 | console.log(res.data) 8 | return res.data 9 | }); 10 | } 11 | 12 | const addUser = (newUser) => { 13 | return axios.post(process.env.REACT_APP_API_URL + '/users', newUser).then(res => { 14 | return res.data 15 | }) 16 | } 17 | 18 | const updateUser = (userToUpdate) => { 19 | return axios.post(process.env.REACT_APP_API_URL + '/users/' + userToUpdate.id, userToUpdate).then(res => { 20 | return res.data 21 | }) 22 | } 23 | 24 | export default { 25 | fetchUsers, 26 | addUser, 27 | updateUser 28 | } -------------------------------------------------------------------------------- /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/extend-expect'; 6 | --------------------------------------------------------------------------------