├── .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 | 
 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 |     
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 |     
 9 |       {persons
10 |         .map(person => (
11 |            editPerson(person.id)}
15 |             deleted={() => deletePerson(person.id)}
16 |           />
17 |         ))
18 |         .reverse()}
19 |     
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 | 
--------------------------------------------------------------------------------