├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── components ├── Header │ └── index.js └── Main │ ├── AddPerson.jsx │ ├── EmptyBox.jsx │ ├── Person.jsx │ ├── Persons.jsx │ ├── ShowPersons.jsx │ ├── ShowPersonsButton.jsx │ └── index.js ├── config └── theme.js ├── containers ├── App.js └── App.module.css ├── context └── index.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /build 6 | 7 | # editor 8 | /.vscode 9 | 10 | # misc 11 | .eslintcache 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Preview 2 | ![Person Manager](https://i.ibb.co/6mDHznG/screencapture-persons-manager-herokuapp-2020-12-20-11-55-27.png) 3 | 4 | ## Features 5 | - [x] Show persons list 6 | - [x] Add new person 7 | - [x] Delete person 8 | - [x] Edit person 9 | - [x] Reverse list 10 | - [x] Show message if not person 11 | - [x] Confirm pop up when delete task 12 | - [x] Custom scrollbar like Mac OS 13 | - [x] Show and Hide persons 14 | - [x] Handling person name (add, edit) 15 | - [x] Disable Show/Hide persons button when empty list 16 | - [x] Show the number of people on the list (and dynamic styling) 17 | 18 | ## Tools 19 | - UI Framework: [Chakra UI](https://chakra-ui.com) 20 | - Modal: [SweetAlert2](https://sweetalert2.github.io) 21 | - MacOS Scrollbar: [SimplebarReact](https://github.com/Grsmto/simplebar/tree/master/packages/simplebar-react), [SmoothScrollbar](https://idiotwu.github.io/smooth-scrollbar) 22 | 23 | ## Follow Me 24 | - Twitter: [@neysidev](https://twitter.com/neysidev) 25 | - Instagram: [@neysidev](https://instagram.com/neysidev) 26 | - LinkedIn: [@neysimehdi](https://linkedin.com/in/neysimehdi) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persons-manager", 3 | "version": "0.1.0", 4 | "private": true, 5 | "prettier": { 6 | "arrowParens": "avoid", 7 | "semi": false, 8 | "singleQuote": true, 9 | "trailingComma": "none" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "dependencies": { 18 | "@chakra-ui/icons": "^1.0.1", 19 | "@chakra-ui/react": "^1.6.1", 20 | "@emotion/react": "^11.4.0", 21 | "@emotion/styled": "^11.3.0", 22 | "@testing-library/jest-dom": "^5.11.4", 23 | "@testing-library/react": "^11.1.0", 24 | "@testing-library/user-event": "^12.1.10", 25 | "framer-motion": "^4.1.13", 26 | "immutability-helper": "^3.1.1", 27 | "react": "^17.0.1", 28 | "react-dom": "^17.0.1", 29 | "react-icons": "^4.1.0", 30 | "react-scripts": "4.0.1", 31 | "simplebar-react": "^2.3.0", 32 | "web-vitals": "^0.2.4" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/persons-manager/f4741614a1d479ba0e1e090102891f5cd57306b2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Persons Manager - Mehdi Neysi 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import { Badge, Heading, Text, VStack } from '@chakra-ui/react' 2 | import { useDataHandler } from '../../context' 3 | 4 | export default function Header() { 5 | const { persons } = useDataHandler() 6 | const { length } = persons 7 | 8 | return ( 9 | 18 | 19 | Person Manager 20 | 21 | 22 | The number of persons is: 23 | 24 | {length} 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Main/AddPerson.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import { BiPlus } from 'react-icons/bi' 3 | import { IconButton, Input, InputGroup } from '@chakra-ui/react' 4 | import { useDataHandler } from '../../context' 5 | 6 | export default function AddPerson() { 7 | const { person, addPerson, changeName } = useDataHandler() 8 | const addPersonInput = useRef(null) 9 | 10 | useEffect(() => { 11 | addPersonInput.current.focus() 12 | }, []) 13 | 14 | return ( 15 |
16 | 17 | 24 | } 31 | /> 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Main/EmptyBox.jsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertIcon } from '@chakra-ui/react' 2 | import { useDataHandler } from '../../context' 3 | 4 | export default function EmptyBox() { 5 | const { persons } = useDataHandler() 6 | 7 | return ( 8 | persons.length === 0 && ( 9 | 10 | 11 | There is no name, you can add one above. 12 | 13 | ) 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Main/Person.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, IconButton, Text, Spacer } from '@chakra-ui/react' 2 | import { BiTrashAlt, BiEdit } from 'react-icons/bi' 3 | 4 | export default function Person({ name, changed, deleted }) { 5 | return ( 6 | 20 | 21 | 22 | {name} 23 | 24 | 25 | 26 | } 31 | /> 32 | } 38 | /> 39 | 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Main/Persons.jsx: -------------------------------------------------------------------------------- 1 | import Person from './Person' 2 | import { useDataHandler } from '../../context' 3 | 4 | export default function Persons() { 5 | const { persons, editPerson, deletePerson } = useDataHandler() 6 | 7 | return ( 8 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Main/ShowPersons.jsx: -------------------------------------------------------------------------------- 1 | import { SlideFade } from '@chakra-ui/react' 2 | import Persons from './Persons' 3 | 4 | export default function ShowPersons({ open }) { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Main/ShowPersonsButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react' 2 | import { useDataHandler } from '../../context' 3 | 4 | export default function ShowPersonsButton({ open, toggle }) { 5 | const { persons } = useDataHandler() 6 | 7 | return ( 8 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Main/index.js: -------------------------------------------------------------------------------- 1 | import { Container, useDisclosure } from '@chakra-ui/react' 2 | 3 | import AddPerson from './AddPerson' 4 | import EmptyBox from './EmptyBox' 5 | import ShowPersons from './ShowPersons' 6 | import ShowPersonsButton from './ShowPersonsButton' 7 | 8 | export default function Main() { 9 | const { isOpen, onToggle } = useDisclosure() 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/config/theme.js: -------------------------------------------------------------------------------- 1 | import { extendTheme } from '@chakra-ui/react' 2 | 3 | export const theme = extendTheme({ 4 | styles: { 5 | global: { 6 | body: { 7 | fontFamily: 'Manrope' 8 | } 9 | } 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from '@chakra-ui/react' 2 | import SimpleBar from 'simplebar-react' 3 | import Header from '../components/Header' 4 | import Main from '../components/Main' 5 | import Provider from '../context' 6 | import { theme } from '../config/theme' 7 | import styles from './App.module.css' 8 | 9 | export default function App() { 10 | return ( 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/containers/App.module.css: -------------------------------------------------------------------------------- 1 | .scrollbar { 2 | max-height: 100vh; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /src/context/index.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from 'react' 2 | import update from 'immutability-helper' 3 | 4 | export const initialData = { 5 | persons: [], 6 | person: '', 7 | showPersons: true 8 | } 9 | 10 | export const Context = createContext(initialData) 11 | 12 | export default function Provider({ children }) { 13 | const [data, setData] = useState(initialData) 14 | 15 | function addPerson(event) { 16 | event.preventDefault() 17 | const { person, persons } = { ...data } 18 | 19 | if (person.trim() === '') { 20 | return alert('Please enter a person name.') 21 | } 22 | 23 | if (person.trim().length > 30) { 24 | return alert('The number of characters allowed is 50 characters.') 25 | } 26 | 27 | setData( 28 | update(data, { 29 | person: { $set: '' }, 30 | persons: { $push: [{ id: persons.length + 1, name: person }] } 31 | }) 32 | ) 33 | } 34 | 35 | function deletePerson(id) { 36 | const allPersons = [...data.persons] 37 | const personIndex = allPersons.findIndex(p => p.id === id) 38 | const personName = allPersons[personIndex].name 39 | 40 | allPersons.splice(personIndex, 1) 41 | const result = window.confirm( 42 | `Are you sure you want to delete "${personName}"?` 43 | ) 44 | 45 | if (result) { 46 | setData( 47 | update(data, { 48 | persons: { $set: allPersons } 49 | }) 50 | ) 51 | } 52 | } 53 | 54 | function editPerson(id) { 55 | const allPersons = [...data.persons] 56 | const personIndex = allPersons.findIndex(p => p.id === id) 57 | const targetPerson = allPersons[personIndex] 58 | 59 | const name = window.prompt('Edit Person', targetPerson.name) 60 | if (name.trim().length > 30) { 61 | return alert('The number of characters allowed is 50 characters.') 62 | } 63 | 64 | targetPerson.name = name.trim() 65 | setData( 66 | update(data, { 67 | persons: { $set: allPersons } 68 | }) 69 | ) 70 | } 71 | 72 | function changeName({ target }) { 73 | setData( 74 | update(data, { 75 | person: { $set: target.value } 76 | }) 77 | ) 78 | } 79 | 80 | function togglePersons() { 81 | setData( 82 | update(data, { 83 | showPersons: { $set: !data.showPersons } 84 | }) 85 | ) 86 | } 87 | 88 | return ( 89 | 99 | {children} 100 | 101 | ) 102 | } 103 | 104 | export function useDataHandler() { 105 | return useContext(Context) 106 | } 107 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import App from './containers/App' 3 | import 'simplebar/dist/simplebar.min.css' 4 | 5 | ReactDOM.render(, document.getElementById('app')) 6 | --------------------------------------------------------------------------------