├── 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 |
32 | 33 | 41 | 42 | 43 | 51 | 52 | 59 |
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 |
33 | 34 | 42 | 43 | 44 | 52 | 53 | 54 | 62 | 63 | 64 | 71 |
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 |
52 | 65 | 78 | 82 |
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 | ![Creating repo from a template step 1](./assets/template-step-1.png) 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 | ![Creating repo from a template step 2](./assets/template-step-2.png) 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 | ![GitHub Pages settings](./assets/repo-settings.png) 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 | ![Deployment status](./assets/deploy-status.png) 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 | ![How it works](./assets/how-it-works.png) 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. --------------------------------------------------------------------------------