├── .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 |
--------------------------------------------------------------------------------