├── README.en.md
├── 404.md
├── src
├── hooks
│ ├── index.js
│ └── useAuth.js
├── redux
│ ├── auth
│ │ ├── selectors.js
│ │ ├── slice.js
│ │ └── operation.js
│ ├── contacts
│ │ ├── selectors.js
│ │ ├── filterSlice.js
│ │ ├── operations.js
│ │ └── contactsSlice.js
│ └── store.js
├── components
│ ├── UserMenu
│ │ ├── UserMenu.styled.js
│ │ └── UserMenu.jsx
│ ├── AuthNav
│ │ ├── AuthNav.jsx
│ │ └── AuthNav.styled.js
│ ├── notifyOptions
│ │ └── notifyOptions.js
│ ├── AppBar
│ │ ├── AppBar.styled.js
│ │ └── AppBar.jsx
│ ├── Filter
│ │ ├── Filter.styled.js
│ │ └── Filter.jsx
│ ├── Layout
│ │ ├── Title.jsx
│ │ ├── Layout.jsx
│ │ └── Layout.styled.js
│ ├── Navigation
│ │ └── Navigation.jsx
│ ├── RestrictedRoute.js
│ ├── PrivateRoute.js
│ ├── ContactList
│ │ ├── ContactList.styled.js
│ │ └── ContactList.jsx
│ ├── theme.jsx
│ ├── FormList
│ │ ├── FormList.styled.js
│ │ └── FormList.jsx
│ ├── App.jsx
│ ├── LoginForm
│ │ ├── LoginForm.jsx
│ │ └── LoginForm.styled.js
│ └── RegisterForm
│ │ └── RegisterForm.jsx
├── pages
│ ├── Login.jsx
│ ├── Home
│ │ ├── Home.jsx
│ │ └── Home.styled.js
│ ├── Register.jsx
│ └── Contacts.jsx
├── index.js
└── index.css
├── jsconfig.json
├── public
├── favicon.ico
├── index.html
└── 404.html
├── assets
├── how-it-works.png
├── deploy-status.png
├── repo-settings.png
├── gh-actions-perm-1.png
├── gh-actions-perm-2.png
├── template-step-1.png
└── template-step-2.png
├── .editorconfig
├── .prettierrc.json
├── .gitignore
├── .github
└── workflows
│ └── deploy.yml
├── package.json
└── README.md
/README.en.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/404.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /404.html
3 | ---
4 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export * from './useAuth';
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": ["src"]
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/assets/how-it-works.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/assets/how-it-works.png
--------------------------------------------------------------------------------
/assets/deploy-status.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/assets/deploy-status.png
--------------------------------------------------------------------------------
/assets/repo-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/assets/repo-settings.png
--------------------------------------------------------------------------------
/assets/gh-actions-perm-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/assets/gh-actions-perm-1.png
--------------------------------------------------------------------------------
/assets/gh-actions-perm-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/assets/gh-actions-perm-2.png
--------------------------------------------------------------------------------
/assets/template-step-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/assets/template-step-1.png
--------------------------------------------------------------------------------
/assets/template-step-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IvanTymoshchuk/goit-react-hw-08-phonebook/HEAD/assets/template-step-2.png
--------------------------------------------------------------------------------
/src/redux/auth/selectors.js:
--------------------------------------------------------------------------------
1 | export const selectedIsLoggedIn = state => state.auth.isLoggedIn;
2 | export const selectedUser = state => state.auth.user;
3 | export const selectedIsRefreshing = state => state.auth.IsRefreshing;
--------------------------------------------------------------------------------
/src/components/UserMenu/UserMenu.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const Title = styled.h2`
4 | color: white;
5 | `;
6 | export const Container = styled.div`
7 | display: flex;
8 | align-items: center;
9 | `;
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | charset = utf-8
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/src/components/AuthNav/AuthNav.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from './AuthNav.styled';
2 |
3 | export const AuthNav = () => {
4 | return (
5 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/notifyOptions/notifyOptions.js:
--------------------------------------------------------------------------------
1 | export const notifyOptions = {
2 | position: 'bottom-left',
3 | autoClose: 5000,
4 | hideProgressBar: false,
5 | closeOnClick: true,
6 | pauseOnHover: true,
7 | draggable: true,
8 | progress: undefined,
9 | theme: 'colored',
10 | };
11 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "es5",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "arrowParens": "avoid",
11 | "proseWrap": "always"
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/AppBar/AppBar.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const Header = styled.header`
4 | display: flex;
5 | justify-content: space-around;
6 | align-items: center;
7 | margin-bottom: 16px;
8 | padding:14px;
9 | border-bottom: 1px solid #2a363b;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/components/Filter/Filter.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled'
2 |
3 | export const FormFilter = styled.form`
4 | display: flex;
5 | justify-content: center;
6 | `
7 | export const LabelFilter = styled.label`
8 | color: ${(p) => p.theme.colors.grey};
9 | `
10 | export const InputFilter = styled.input``
11 |
--------------------------------------------------------------------------------
/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 | import { LoginForm } from 'components/LoginForm/LoginForm';
3 |
4 | export default function Login() {
5 | return (
6 |
7 |
8 | Login
9 |
10 |
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/src/components/Layout/Title.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import {Title} from './Layout.styled'
3 |
4 | const GlobalTitle = ({title}) => {
5 | return (
6 | {title}
7 | );
8 | }
9 |
10 | GlobalTitle.propTypes = {
11 | title: PropTypes.string.isRequired
12 | }
13 |
14 | export default GlobalTitle;
--------------------------------------------------------------------------------
/src/pages/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import { Container, Title, Link } from './Home.styled';
2 |
3 | export default function Home() {
4 | return (
5 |
6 | Welcome to Phonebook
7 |
8 | Try it now!
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/Register.jsx:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 | import { RegisterForm } from 'components/RegisterForm/RegisterForm';
3 |
4 | export default function Register() {
5 | return (
6 |
7 |
8 | Registration
9 |
10 |
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/src/components/AuthNav/AuthNav.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | export const Link = styled(NavLink)`
5 | padding: 10px;
6 | border-radius: 4px;
7 | text-decoration: none;
8 | color: white;
9 | font-weight: 500;
10 |
11 | &.active {
12 | background-color: orangered;
13 | }
14 | `;
--------------------------------------------------------------------------------
/src/components/Layout/Layout.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from 'react';
2 | import { Outlet } from 'react-router-dom';
3 |
4 | import {AppBar} from '../AppBar/AppBar';
5 |
6 | export const Layout = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 | >
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Navigation/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from '../AuthNav/AuthNav.styled';
2 | import { useAuth } from 'hooks';
3 |
4 | export const Navigation = () => {
5 | const { isLoggedIn } = useAuth();
6 |
7 | return (
8 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/Layout/Layout.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const Container = styled.div`
4 | padding: 40px;
5 | width: 400px;
6 | margin: 0 auto;
7 | `;
8 |
9 | export const Title = styled.h1`
10 | text-align: center;
11 | margin-top: 30px;
12 | margin-bottom: 30px;
13 | font-size: ${(p) => p.theme.fontSize.xl};
14 | color: ${(p) => p.theme.colors.white};
15 | `;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | #Junk
4 | .vscode/
5 | .idea/
6 |
7 | # dependencies
8 | /node_modules
9 | /.pnp
10 | .pnp.js
11 |
12 | # testing
13 | /coverage
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
--------------------------------------------------------------------------------
/src/redux/contacts/selectors.js:
--------------------------------------------------------------------------------
1 | export const getContacts = state => state.contacts.items;
2 |
3 | export const getFilter = state => state.filter;
4 |
5 |
6 | export const getVisibleContacts = state => {
7 | const contacts = getContacts(state);
8 | const filter = getFilter(state);
9 | const normalizedFilter = filter.toLowerCase();
10 |
11 | return contacts.filter(contact =>
12 | contact.name.toLowerCase().includes(normalizedFilter)
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/RestrictedRoute.js:
--------------------------------------------------------------------------------
1 | import { useAuth } from 'hooks';
2 | import { Navigate } from 'react-router-dom';
3 |
4 | /**
5 | * - If the route is restricted and the user is logged in, render a to redirectTo
6 | * - Otherwise render the component
7 | */
8 |
9 | export const RestrictedRoute = ({ component: Component, redirectTo = '/' }) => {
10 | const { isLoggedIn } = useAuth();
11 |
12 | return isLoggedIn ? : Component;
13 | };
14 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import {
3 | selectedUser,
4 | selectedIsLoggedIn,
5 | selectedIsRefreshing,
6 | } from 'redux/auth/selectors';
7 |
8 | export const useAuth = () => {
9 | const isLoggedIn = useSelector(selectedIsLoggedIn);
10 | const isRefreshing = useSelector(selectedIsRefreshing);
11 | const user = useSelector(selectedUser);
12 |
13 | return {
14 | isLoggedIn,
15 | isRefreshing,
16 | user,
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/AppBar/AppBar.jsx:
--------------------------------------------------------------------------------
1 | import { Navigation } from '../Navigation/Navigation';
2 | import { UserMenu } from '../UserMenu/UserMenu';
3 | import { AuthNav } from '../AuthNav/AuthNav';
4 | import { useAuth } from 'hooks';
5 | import { Header } from './AppBar.styled';
6 |
7 | export const AppBar = () => {
8 | const { isLoggedIn } = useAuth();
9 |
10 | return (
11 |
12 |
13 | {isLoggedIn ? : }
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/redux/contacts/filterSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialFilterState = '';
4 |
5 | const filterSlice = createSlice({
6 | name: 'filter',
7 | initialState: initialFilterState,
8 | reducers: {
9 | changeFilter(state, action) {
10 | return (state = action.payload); // Оновлення значення з попереднього
11 | },
12 | },
13 | });
14 |
15 | export const { changeFilter } = filterSlice.actions;
16 |
17 | export const filterReducer = filterSlice.reducer;
18 |
--------------------------------------------------------------------------------
/src/components/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import { useAuth } from 'hooks';
3 |
4 | /**
5 | * - If the route is private and the user is logged in, render the component
6 | * - Otherwise render to redirectTo
7 | */
8 |
9 | export const PrivateRoute = ({ component: Component, redirectTo = '/' }) => {
10 | const { isLoggedIn, isRefreshing } = useAuth();
11 | const shouldRedirect = !isLoggedIn && !isRefreshing;
12 |
13 | return shouldRedirect ? : Component;
14 | };
--------------------------------------------------------------------------------
/src/components/ContactList/ContactList.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const ListWrap = styled.ul`
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | flex-direction: column;
8 | padding: ${(p) => p.theme.space[4]}px;
9 | `;
10 | export const List = styled.li`
11 | padding: 10px;
12 | margin-bottom: 5px;
13 | display: flex;
14 | align-items: center;
15 | justify-content: space-between;
16 | font-size: ${(p) => p.theme.fontSize.m};
17 | color: ${(p) => p.theme.colors.white};
18 | `;
19 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and deploy to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | build-and-deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout 🛎️
12 | uses: actions/checkout@v2.3.1
13 |
14 | - name: Install, lint, build 🔧
15 | run: |
16 | npm install
17 | npm run lint:js
18 | npm run build
19 |
20 | - name: Deploy 🚀
21 | uses: JamesIves/github-pages-deploy-action@4.1.0
22 | with:
23 | branch: gh-pages
24 | folder: build
25 |
--------------------------------------------------------------------------------
/src/components/UserMenu/UserMenu.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { logOut } from 'redux/auth/operation';
3 | import { useAuth } from 'hooks';
4 | import { Button } from '../FormList/FormList.styled';
5 | import {Container,Title} from './UserMenu.styled'
6 |
7 | export const UserMenu = () => {
8 | const dispatch = useDispatch();
9 | const { user } = useAuth();
10 |
11 | return (
12 |
13 | Welcome, {user.name}
14 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/theme.jsx:
--------------------------------------------------------------------------------
1 | export const theme = {
2 | colors: {
3 | black: '#000000',
4 | white: '#ffff',
5 | grey:'#fff',
6 | green:'green',
7 | orange: '#cd7305'
8 | },
9 | space: [0, 2, 4, 8, 16, 32, 64, 128, 256],
10 |
11 | fontSize: {
12 | s:'14px',
13 | m: '16px',
14 | l: '24px',
15 | xl: '36px',
16 |
17 | },
18 |
19 | lineHeight: {
20 | body: '1.5',
21 | heading: '1.125',
22 | },
23 | border: {
24 | none: 'none',
25 | },
26 | borderRadius: {
27 | none: '0',
28 | },
29 | boxShadow: {
30 | textShadow: '0 1px 1px rgba(236, 230, 230, 0.05)',
31 | boxShadow:' inset 0 -5px 45px rgba(100, 100, 100, 0.2)',
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { Provider } from 'react-redux';
4 | import { store, persistor } from 'redux/store';
5 | import { ThemeProvider } from '@emotion/react';
6 | import { BrowserRouter } from 'react-router-dom';
7 | import { PersistGate } from 'redux-persist/integration/react';
8 | import { theme } from './components/theme';
9 | import { App } from 'components/App';
10 | import './index.css';
11 |
12 | ReactDOM.createRoot(document.getElementById('root')).render(
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
--------------------------------------------------------------------------------
/src/components/Filter/Filter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormFilter, LabelFilter } from './Filter.styled';
3 | import { Input } from '../FormList/FormList.styled';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { getFilter } from 'redux/contacts/selectors';
6 | import { changeFilter } from 'redux/contacts/filterSlice';
7 |
8 | const Filter = () => {
9 | const value = useSelector(getFilter);
10 | const dispatch = useDispatch();
11 |
12 | const handleChange = e => {
13 | dispatch(changeFilter(e.target.value));
14 | };
15 |
16 | return (
17 |
18 |
19 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Filter;
31 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | export const Container = styled.div`
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | flex-direction: column;
9 | min-height: calc(100vh - 50px);
10 | `;
11 | export const Title = styled.h1`
12 | font-weight: 500;
13 | font-size: 48px;
14 | text-align: center;
15 | color: white;
16 | `;
17 |
18 | export const Link = styled(NavLink)`
19 | margin-top: 25px;
20 | padding: 10px;
21 | border: 0px solid transparent;
22 | border-radius: 4px;
23 | text-decoration: none;
24 | color: white;
25 | background-color: #ff4500;
26 | box-shadow: gray;
27 | opacity: 1;
28 | transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
29 | &:hover,
30 | &:focus {
31 | opacity: 0.8;
32 | transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
33 | }
34 | `;
35 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import-normalize; /* bring in normalize.css styles */
2 |
3 | html {
4 | height: 100%;
5 | }
6 |
7 | body {
8 | margin: 0 auto;
9 | font-size: 14px;
10 | line-height: 18px;
11 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
12 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
13 | sans-serif;
14 | padding:0;
15 | font-family: sans-serif;
16 | background: linear-gradient(#141e30, #243b55);
17 | }
18 |
19 | code {
20 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
21 | monospace;
22 | }
23 |
24 | h1,
25 | h2,
26 | h3,
27 | h4,
28 | h5,
29 | h6,
30 | p,
31 | ul {
32 | margin: 0;
33 | }
34 | ul {
35 | padding: 0;
36 | list-style: none;
37 | }
38 | img {
39 | display: block;
40 | padding: 0;
41 | height: auto;
42 | }
43 | a,
44 | button,
45 | input {
46 | text-decoration: none;
47 | cursor: pointer;
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/ContactList/ContactList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ListWrap, List } from './ContactList.styled';
3 | import { Button } from 'components/FormList/FormList.styled';
4 | import { UserDeleteOutlined } from '@ant-design/icons';
5 | import { useSelector, useDispatch } from 'react-redux';
6 | import { getVisibleContacts } from 'redux/contacts/selectors';
7 | import { deleteContact } from 'redux/contacts/operations';
8 |
9 | const ContactList = () => {
10 | const contacts = useSelector(getVisibleContacts);
11 | const dispatch = useDispatch();
12 |
13 | return (
14 |
15 | {contacts.map(({ id, name, number }) => (
16 |
17 | {name + ' : ' + number}
18 |
19 |
22 |
23 | ))}
24 |
25 | );
26 | };
27 |
28 | export default ContactList;
29 |
--------------------------------------------------------------------------------
/src/pages/Contacts.jsx:
--------------------------------------------------------------------------------
1 | import { HelmetProvider } from 'react-helmet-async';
2 | import { useEffect } from 'react';
3 | import { useDispatch } from 'react-redux';
4 | import { ToastContainer } from 'react-toastify';
5 | import 'react-toastify/dist/ReactToastify.css';
6 | import { fetchAll } from 'redux/contacts/operations';
7 | import FormList from '../components/FormList/FormList';
8 | import ContactList from '../components/ContactList/ContactList';
9 | import Filter from '../components/Filter/Filter';
10 | import GlobalTitle from '../components/Layout/Title';
11 |
12 | const Contacts = () => {
13 | const dispatch = useDispatch();
14 |
15 | useEffect(() => {
16 | dispatch(fetchAll());
17 | }, [dispatch]);
18 | return (
19 | <>
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | >
29 | );
30 | };
31 |
32 | export default Contacts;
33 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore,getDefaultMiddleware } from '@reduxjs/toolkit';
2 | import { authReducer } from 'redux/auth/slice';
3 | import contactsReducer from 'redux/contacts/contactsSlice';
4 | import { filterReducer } from 'redux/contacts/filterSlice';
5 | import storage from 'redux-persist/lib/storage';
6 |
7 | import {
8 | persistStore,
9 | persistReducer,
10 | FLUSH,
11 | REHYDRATE,
12 | PAUSE,
13 | PERSIST,
14 | PURGE,
15 | REGISTER,
16 | } from 'redux-persist';
17 |
18 | const middleware = [
19 | ...getDefaultMiddleware({
20 | serializableCheck: {
21 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
22 | },
23 | }),
24 | ];
25 |
26 |
27 | const authPersistConfig = {
28 | key: 'auth',
29 | storage,
30 | whitelist: ['token'],
31 | };
32 |
33 |
34 | export const store = configureStore({
35 | reducer: {
36 | contacts: contactsReducer,
37 | filter: filterReducer,
38 | auth: persistReducer(authPersistConfig, authReducer),
39 | },
40 | middleware,
41 | devTools: process.env.NODE_ENV === 'development',
42 | });
43 |
44 |
45 | export const persistor = persistStore(store);
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 | Phonebook
14 |
15 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/redux/contacts/operations.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { createAsyncThunk } from '@reduxjs/toolkit';
3 |
4 | // axios.defaults.baseURL = 'https://6443bb28466f7c2b4b593743.mockapi.io';
5 |
6 | export const fetchAll = createAsyncThunk(
7 | 'contacts/fetchAll',
8 | async (_, thunkAPI) => {
9 | try {
10 | const response = await axios.get('/contacts');
11 | return response.data;
12 | } catch (e) {
13 | return thunkAPI.rejectWithValue(e.message);
14 | }
15 | }
16 | );
17 |
18 | export const addContact = createAsyncThunk(
19 | 'contacts/addContact',
20 | async (contact, thunkAPI) => {
21 | try {
22 | const response = await axios.post('/contacts', contact);
23 | return response.data;
24 | } catch (e) {
25 | return thunkAPI.rejectWithValue(e.message);
26 | }
27 | }
28 | );
29 |
30 | export const deleteContact = createAsyncThunk(
31 | 'contacts/deleteContact',
32 | async (contactId, thunkAPI) => {
33 | try {
34 | const response = await axios.delete(`/contacts/${contactId}`);
35 | return response.data;
36 | } catch (e) {
37 | return thunkAPI.rejectWithValue(e.message);
38 | }
39 | }
40 | );
41 |
--------------------------------------------------------------------------------
/src/redux/contacts/contactsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { fetchAll, addContact, deleteContact } from './operations';
3 |
4 | const initialState = {
5 | items: [],
6 | error: null,
7 | };
8 |
9 | const contactsSlice = createSlice({
10 | name: 'contacts',
11 | initialState,
12 | reducers: {},
13 | extraReducers: builder => {
14 | builder
15 | .addCase(fetchAll.rejected, (state, action) => {
16 | state.error = action.error.message;
17 | })
18 | .addCase(fetchAll.fulfilled, (state, action) => {
19 | state.error = null;
20 | state.items = action.payload;
21 | })
22 | .addCase(addContact.rejected, (state, action) => {
23 | state.error = action.error.message;
24 | })
25 | .addCase(addContact.fulfilled, (state, action) => {
26 | state.items = [...state.items, action.payload];
27 | state.error = null;
28 | })
29 | .addCase(deleteContact.rejected, (state, action) => {
30 | state.error = action.error.message;
31 | })
32 | .addCase(deleteContact.fulfilled, (state, action) => {
33 | state.items = state.items.filter(item => item.id !== action.payload.id);
34 | state.error = null;
35 | });
36 | },
37 | });
38 |
39 | const { reducer: contactsReducer } = contactsSlice;
40 | export default contactsReducer;
41 |
--------------------------------------------------------------------------------
/src/redux/auth/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { register, logIn, logOut, refreshUser } from 'redux/auth/operation';
3 |
4 | const initialState = {
5 | user: { name: null, email: null },
6 | token: null,
7 | isLoggedIn: false,
8 | isRefreshing: false,
9 | };
10 |
11 | const authSlice = createSlice({
12 | name: 'auth',
13 | initialState,
14 | extraReducers: builder =>
15 | builder
16 | .addCase(register.fulfilled, (state, action) => {
17 | state.user = action.payload.user;
18 | state.token = action.payload.token;
19 | state.isLoggedIn = true;
20 | })
21 |
22 | .addCase(logIn.fulfilled, (state, action) => {
23 | state.user = action.payload.user;
24 | state.token = action.payload.token;
25 | state.isLoggedIn = true;
26 | })
27 |
28 | .addCase(logOut.fulfilled, state => {
29 | state.user = { name: null, email: null };
30 | state.token = null;
31 | state.isLoggedIn = false;
32 | })
33 |
34 | .addCase(refreshUser.pending, state => {
35 | state.isRefreshing = true;
36 | })
37 | .addCase(refreshUser.fulfilled, (state, action) => {
38 | state.user = action.payload;
39 | state.isLoggedIn = true;
40 | state.isRefreshing = false;
41 | })
42 | .addCase(refreshUser.rejected, state => {
43 | state.isRefreshing = false;
44 | }),
45 | });
46 |
47 | export const authReducer = authSlice.reducer;
48 |
--------------------------------------------------------------------------------
/src/components/FormList/FormList.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const Form = styled.form`
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | `;
8 | export const Label = styled.label`
9 | color: ${(p) => p.theme.colors.white};
10 | `;
11 | export const Input = styled.input`
12 | width: 350px;
13 | margin-bottom: 15px;
14 | background: rgba(0, 0, 0, 0.3);
15 | border: ${(p) => p.theme.border.none};
16 | outline: none;
17 | padding: 10px;
18 | font-size: ${(p) => p.theme.fontSize.s};
19 | color: ${(p) => p.theme.colors.grey};
20 | text-shadow: ${(p) => p.theme.boxShadow.textShadow};
21 | border: 1px solid rgba(0, 0, 0, 0.3);
22 | border-radius: 4px;
23 | box-shadow:${(p) => p.theme.boxShadow.textShadow};
24 | &:focus {
25 | box-shadow:${(p) => p.theme.boxShadow.boxShadow};
26 | }
27 | `;
28 |
29 | export const Button = styled.button`
30 | display: flex;
31 | align-items: center;
32 | gap: 10px;
33 | color: ${(p) => p.theme.colors.white};
34 | padding: 5px 10px 5px;
35 |
36 | background: rgba(0, 0, 0, 0.3);
37 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
38 | border: 1px solid rgba(0, 0, 0, 0.3);
39 | border-radius: 4px;
40 | box-shadow: ${(p) => p.theme.boxShadow.boxShadow};
41 | margin-left:15px;
42 | :focus,
43 | :hover {
44 | color: ${(p) => p.theme.colors.green};
45 | box-shadow: ${(p) => p.theme.boxShadow.boxShadow};
46 | }
47 | `;
48 |
49 | export const Span = styled.span`
50 | display: flex;
51 | margin-bottom: 3px;
52 | `;
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "goit-react-hw-08-phonebook",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "https://vanTymoshchuk.github.io/goit-react-hw-08-phonebook/",
6 | "dependencies": {
7 | "@ant-design/icons": "^5.1.4",
8 | "@emotion/react": "^11.11.1",
9 | "@emotion/styled": "^11.11.0",
10 | "@reduxjs/toolkit": "^1.9.5",
11 | "@testing-library/jest-dom": "^5.16.3",
12 | "@testing-library/react": "^12.1.4",
13 | "@testing-library/user-event": "^13.5.0",
14 | "axios": "^1.4.0",
15 | "react": "^18.1.0",
16 | "react-dom": "^18.1.0",
17 | "react-helmet": "^6.1.0",
18 | "react-helmet-async": "^1.3.0",
19 | "react-redux": "^8.1.1",
20 | "react-router-dom": "^6.14.1",
21 | "react-scripts": "5.0.1",
22 | "react-toastify": "^9.1.3",
23 | "redux": "^4.2.1",
24 | "redux-persist": "^6.0.0",
25 | "shortid": "^2.2.16",
26 | "web-vitals": "^2.1.3"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject",
33 | "lint:js": "eslint src/**/*.{js,jsx}"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "react-app",
38 | "react-app/jest"
39 | ]
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, lazy } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Route, Routes } from 'react-router-dom';
4 | import { useAuth } from 'hooks';
5 | import { refreshUser } from 'redux/auth/operation';
6 | import { Layout } from 'components/Layout/Layout';
7 | import { PrivateRoute } from './PrivateRoute';
8 | import { RestrictedRoute } from './RestrictedRoute';
9 |
10 | const HomePage = lazy(() => import('../pages/Home/Home'));
11 | const RegisterPage = lazy(() => import('../pages/Register'));
12 | const LoginPage = lazy(() => import('../pages/Login'));
13 | const Contacts = lazy(() => import('../pages/Contacts'));
14 |
15 | export const App = () => {
16 | const dispatch = useDispatch();
17 | const { isRefreshing } = useAuth();
18 |
19 | useEffect(() => {
20 | dispatch(refreshUser());
21 | }, [dispatch]);
22 |
23 | return isRefreshing ? (
24 | Refreshing user...
25 | ) : (
26 |
27 | }>
28 | } />
29 |
30 | } />
34 | }
35 | />
36 | } />
40 | }
41 | />
42 | } />
46 | }
47 | />
48 |
49 | } />
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/components/LoginForm/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { logIn } from 'redux/auth/operation';
3 | import {
4 | Container,
5 | ContainerBox,
6 | Form,
7 | Input,
8 | Button,
9 | Title,
10 | Span,
11 | } from '../LoginForm/LoginForm.styled';
12 |
13 | export const LoginForm = () => {
14 | const dispatch = useDispatch();
15 |
16 | const handleSubmit = e => {
17 | e.preventDefault();
18 | const form = e.currentTarget;
19 | dispatch(
20 | logIn({
21 | email: form.elements.email.value,
22 | password: form.elements.password.value,
23 | })
24 | );
25 | form.reset();
26 | };
27 |
28 | return (
29 |
30 | Login
31 |
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/src/redux/auth/operation.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { createAsyncThunk } from '@reduxjs/toolkit';
3 |
4 | axios.defaults.baseURL = 'https://connections-api.herokuapp.com/';
5 |
6 | const setAuthHeader = token => {
7 | axios.defaults.headers.common.Authorization = `Bearer ${token}`;
8 | };
9 |
10 | const clearAuthHeader = () => {
11 | axios.defaults.headers.common.Authorization = '';
12 | };
13 |
14 | export const register = createAsyncThunk(
15 | 'auth/register',
16 | async (credentials, thunkAPI) => {
17 | try {
18 | const res = await axios.post('/users/signup', credentials);
19 | console.log(res.data);
20 | setAuthHeader(res.data.token);
21 | return res.data;
22 | } catch (error) {
23 | return thunkAPI.rejectWithValue(error.message);
24 | }
25 | }
26 | );
27 |
28 | export const logIn = createAsyncThunk(
29 | 'auth/login',
30 | async (credentials, thunkAPI) => {
31 | try {
32 | const res = await axios.post('/users/login', credentials);
33 | setAuthHeader(res.data.token);
34 | return res.data;
35 | } catch (error) {
36 | return thunkAPI.rejectWithValue(error.message);
37 | }
38 | }
39 | );
40 |
41 | export const logOut = createAsyncThunk('auth/logout', async (_, thunkAPI) => {
42 | try {
43 | await axios.post('/users/logout');
44 | clearAuthHeader();
45 | } catch (error) {
46 | return thunkAPI.rejectWithValue(error.message);
47 | }
48 | });
49 |
50 | export const refreshUser = createAsyncThunk(
51 | 'auth/refresh',
52 | async (_, thunkAPI) => {
53 | const state = thunkAPI.getState();
54 | const persistedToken = state.auth.token;
55 |
56 | if (persistedToken === null) {
57 | return thunkAPI.rejectWithValue('Unable to fetch user');
58 | }
59 |
60 | try {
61 | setAuthHeader(persistedToken);
62 | const res = await axios.get('/users/current');
63 | return res.data;
64 | } catch (error) {
65 | return thunkAPI.rejectWithValue(error.message);
66 | }
67 | }
68 | );
69 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/components/RegisterForm/RegisterForm.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { register } from 'redux/auth/operation';
3 | import {
4 | Container,
5 | ContainerBox,
6 | Form,
7 | Input,
8 | Button,
9 | Title,
10 | Span,
11 | } from '../LoginForm/LoginForm.styled';
12 |
13 | export const RegisterForm = () => {
14 | const dispatch = useDispatch();
15 |
16 | const handleSubmit = e => {
17 | e.preventDefault();
18 | const form = e.currentTarget;
19 | dispatch(
20 | register({
21 | name: form.elements.name.value,
22 | email: form.elements.email.value,
23 | password: form.elements.password.value,
24 | })
25 | );
26 | form.reset();
27 | };
28 |
29 | return (
30 |
31 | Register
32 |
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/src/components/FormList/FormList.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { toast } from 'react-toastify';
4 | import { nanoid } from '@reduxjs/toolkit';
5 | import { UserAddOutlined } from '@ant-design/icons';
6 | import { Form, Label, Input, Button, Span } from './FormList.styled';
7 | import { notifyOptions } from '../notifyOptions/notifyOptions';
8 | import { getVisibleContacts } from 'redux/contacts/selectors';
9 | import { addContact } from 'redux/contacts/operations';
10 |
11 | const FormList = () => {
12 | const [name, setName] = useState('');
13 | const [number, setNumber] = useState('');
14 |
15 | const contacts = useSelector(getVisibleContacts);
16 | const dispatch = useDispatch();
17 |
18 | const handleSubmit = event => {
19 | event.preventDefault();
20 |
21 | const normalizedName = name.toLowerCase();
22 | const isAdded = contacts.find(
23 | el => el.name.toLowerCase() === normalizedName
24 | );
25 |
26 | if (isAdded) {
27 | toast.error(`${name}: is already in contacts`, notifyOptions);
28 | return;
29 | }
30 |
31 | dispatch(addContact({ id: nanoid(), name, number }));
32 | setName('');
33 | setNumber('');
34 | };
35 |
36 | const handleChange = e => {
37 | const { name, value } = e.target;
38 | switch (name) {
39 | case 'name':
40 | setName(value);
41 | break;
42 | case 'number':
43 | setNumber(value);
44 | break;
45 | default:
46 | return;
47 | }
48 | };
49 |
50 | return (
51 |
83 | );
84 | };
85 |
86 | export default FormList;
87 |
--------------------------------------------------------------------------------
/src/components/LoginForm/LoginForm.styled.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const Form = styled.form``;
4 |
5 | export const Container = styled.div`
6 | position: absolute;
7 | top: 50%;
8 | left: 50%;
9 | width: 400px;
10 | padding: 40px;
11 | transform: translate(-50%, -50%);
12 | background: rgba(0, 0, 0, 0.5);
13 | box-sizing: border-box;
14 | box-shadow: 0 15px 25px rgba(0, 0, 0, 0.6);
15 | border-radius: 10px;
16 | `;
17 | export const ContainerBox = styled.div`
18 | position: relative;
19 | `;
20 |
21 | export const Title = styled.h2`
22 | margin: 0 0 30px;
23 | padding: 0;
24 | color: #fff;
25 | text-align: center;
26 | `;
27 | export const Input = styled.input`
28 | width: 100%;
29 | padding: 10px 0;
30 | font-size: 16px;
31 | color: #fff;
32 | margin-bottom: 30px;
33 | border: none;
34 | border-bottom: 1px solid #fff;
35 | outline: none;
36 | background: transparent;
37 | :focus,
38 | :valid {
39 | top: -20px;
40 | left: 0;
41 | color: #03e9f4;
42 | font-size: 12px;
43 | }
44 | `;
45 |
46 | export const Button = styled.button`
47 | position: relative;
48 | display: inline-block;
49 | padding: 10px 20px;
50 | color: #03e9f4;
51 | font-size: 16px;
52 | text-decoration: none;
53 | text-transform: uppercase;
54 | overflow: hidden;
55 | transition: 0.5s;
56 | letter-spacing: 4px;
57 | background: transparent;
58 | :hover {
59 | background: #03e9f4;
60 | color: #fff;
61 | border-radius: 5px;
62 | box-shadow: 0 0 5px #03e9f4, 0 0 25px #03e9f4, 0 0 50px #03e9f4,
63 | 0 0 100px #03e9f4;
64 | }
65 | `;
66 | export const Span = styled.span`
67 | position: absolute;
68 | display: block;
69 | :nth-of-type(1) {
70 | top: 0;
71 | left: -100%;
72 | width: 100%;
73 | height: 2px;
74 | background: linear-gradient(90deg, transparent, #03e9f4);
75 | animation: btn-anim1 1s linear infinite;
76 | @keyframes btn-anim1 {
77 | 0% {
78 | left: -100%;
79 | }
80 | 50%,
81 | 100% {
82 | left: 100%;
83 | }
84 | }
85 | }
86 | :nth-of-type(2) {
87 | top: -100%;
88 | right: 0;
89 | width: 2px;
90 | height: 100%;
91 | background: linear-gradient(180deg, transparent, #03e9f4);
92 | animation: btn-anim2 1s linear infinite;
93 | animation-delay: 0.25s;
94 | }
95 | @keyframes btn-anim2 {
96 | 0% {
97 | top: -100%;
98 | }
99 | 50%,
100 | 100% {
101 | top: 100%;
102 | }
103 | }
104 | :nth-of-type(3) {
105 | bottom: 0;
106 | right: -100%;
107 | width: 100%;
108 | height: 2px;
109 | background: linear-gradient(270deg, transparent, #03e9f4);
110 | animation: btn-anim3 1s linear infinite;
111 | animation-delay: 0.5s;
112 | }
113 | @keyframes btn-anim3 {
114 | 0% {
115 | right: -100%;
116 | }
117 | 50%,
118 | 100% {
119 | right: 100%;
120 | }
121 | }
122 | :nth-of-type(4) {
123 | bottom: -100%;
124 | left: 0;
125 | width: 2px;
126 | height: 100%;
127 | background: linear-gradient(360deg, transparent, #03e9f4);
128 | animation: btn-anim4 1s linear infinite;
129 | animation-delay: 0.75s;
130 | }
131 | @keyframes btn-anim4 {
132 | 0% {
133 | bottom: -100%;
134 | }
135 | 50%,
136 | 100% {
137 | bottom: 100%;
138 | }
139 | }
140 | `;
141 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React homework template
2 |
3 | This project was created with
4 | [Create React App](https://github.com/facebook/create-react-app). To get
5 | acquainted and configure additional features
6 | [refer to documentation](https://facebook.github.io/create-react-app/docs/getting-started).
7 |
8 | ## Creating a repository by template
9 |
10 | Use this GoIT repository as a template for creating a repository
11 | of your project. To use it just tap the `«Use this template»` button and choose
12 | `«Create a new repository»` option, as you can see on the image below.
13 |
14 | 
15 |
16 | The page for creating a new repository will open on the next step. Fill out
17 | the Name field and make sure the repository is public, then click
18 | `«Create repository from template»` button.
19 |
20 | 
21 |
22 | You now have a personal project repository, having a repository-template file
23 | and folder structure. After that, you can work with it as you would with any
24 | other private repository: clone it on your computer, write code, commit, and
25 | send it to GitHub.
26 |
27 | ## Preparing for coding
28 |
29 | 1. Make sure you have an LTS version of Node.js installed on your computer.
30 | [Download and install](https://nodejs.org/en/) if needed.
31 | 2. Install the project's base dependencies with the `npm install` command.
32 | 3. Start development mode by running the `npm start` command.
33 | 4. Go to [http://localhost:3000](http://localhost:3000) in your browser. This
34 | page will automatically reload after saving changes to the project files.
35 |
36 | ## Deploy
37 |
38 | The production version of the project will automatically be linted, built, and
39 | deployed to GitHub Pages, in the `gh-pages` branch, every time the `main` branch
40 | is updated. For example, after a direct push or an accepted pull request. To do
41 | this, you need to edit the `homepage` field in the `package.json` file,
42 | replacing `your_username` and `your_repo_name` with your own, and submit the
43 | changes to GitHub.
44 |
45 | ```json
46 | "homepage": "https://your_username.github.io/your_repo_name/"
47 | ```
48 |
49 | Next, you need to go to the settings of the GitHub repository (`Settings` >
50 | `Pages`) and set the distribution of the production version of files from the
51 | `/root` folder of the `gh-pages` branch, if this was not done automatically.
52 |
53 | 
54 |
55 | ### Deployment status
56 |
57 | The deployment status of the latest commit is displayed with an icon next to its
58 | ID.
59 |
60 | - **Yellow color** - the project is being built and deployed.
61 | - **Green color** - deployment completed successfully.
62 | - **Red color** - an error occurred during linting, build or deployment.
63 |
64 | More detailed information about the status can be viewed by clicking on the
65 | icon, and in the drop-down window, follow the link `Details`.
66 |
67 | 
68 |
69 | ### Live page
70 |
71 | After some time, usually a couple of minutes, the live page can be viewed at the
72 | address specified in the edited `homepage` property. For example, here is a link
73 | to a live version for this repository
74 | [https://goitacademy.github.io/react-homework-template](https://goitacademy.github.io/react-homework-template).
75 |
76 | If a blank page opens, make sure there are no errors in the `Console` tab
77 | related to incorrect paths to the CSS and JS files of the project (**404**). You
78 | most likely have the wrong value for the `homepage` property in the
79 | `package.json` file.
80 |
81 | ### Routing
82 |
83 | If your application uses the `react-router-dom` library for routing, you must
84 | additionally configure the `` component by passing the exact name
85 | of your repository in the `basename` prop. Slashes at the beginning and end of
86 | the line are required.
87 |
88 | ```jsx
89 |
90 |
91 |
92 | ```
93 |
94 | ## How it works
95 |
96 | 
97 |
98 | 1. After each push to the `main` branch of the GitHub repository, a special
99 | script (GitHub Action) is launched from the `.github/workflows/deploy.yml`
100 | file.
101 | 2. All repository files are copied to the server, where the project is
102 | initialized and linted and built before deployment.
103 | 3. If all steps are successful, the built production version of the project
104 | files is sent to the `gh-pages` branch. Otherwise, the script execution log
105 | will indicate what the problem is.
--------------------------------------------------------------------------------