├── .babelrc ├── .eslintrc.json ├── .github └── workflows │ └── linters.yml ├── .gitignore ├── .stylelintrc.json ├── LICENSE ├── README.md ├── murple_logo.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── AddBook.js ├── Book.js ├── Button.js ├── ListBooks.js └── Navbar.js ├── index.css ├── index.js ├── logo.svg ├── redux ├── books │ ├── booksArrSlice.js │ └── thunks.js ├── categories │ └── categoriesSlice.js └── store.js ├── reportWebVitals.js ├── routes ├── Categories.js ├── ErrorPage.js └── Home.js └── setupTests.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "plugins": ["@babel/plugin-syntax-jsx"] 6 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": ["airbnb", "plugin:react/recommended", "plugin:react-hooks/recommended"], 16 | "plugins": ["react"], 17 | "rules": { 18 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 19 | "react/react-in-jsx-scope": "off", 20 | "import/no-unresolved": "off", 21 | "no-shadow": "off" 22 | }, 23 | "overrides": [ 24 | { 25 | "files": ["src/**/*Slice.js"], 26 | // avoid state param assignment 27 | "rules": { "no-param-reassign": ["error", { "props": false }] } 28 | } 29 | ], 30 | "ignorePatterns": [ 31 | "dist/", 32 | "build/" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "18.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@7.x eslint-config-airbnb@18.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@4.x @babel/eslint-parser@7.x @babel/core@7.x @babel/plugin-syntax-jsx@7.x @babel/preset-env@7.x @babel/preset-react@7.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | [ -f .babelrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.babelrc 22 | - name: ESLint Report 23 | run: npx eslint "**/*.{js,jsx}" 24 | stylelint: 25 | name: Stylelint 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: "18.x" 32 | - name: Setup Stylelint 33 | run: | 34 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 35 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 36 | - name: Stylelint Report 37 | run: npx stylelint "**/*.{css,scss}" 38 | nodechecker: 39 | name: node_modules checker 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Check node_modules existence 44 | run: | 45 | if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .history/ 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": [ 6 | true, 7 | { 8 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 9 | } 10 | ], 11 | "scss/at-rule-no-unknown": [ 12 | true, 13 | { 14 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 15 | } 16 | ], 17 | "csstree/validator": true 18 | }, 19 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"] 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Salim Bamahfoodh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | logo 5 |
6 | 7 |

Welcome to my project 😃

8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 |

BookStore

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | # 📗 Table of Contents 25 | 26 | - [📗 Table of Contents](#-table-of-contents) 27 | - [📖 \[BookStore\] ](#-bookstore-) 28 | - [🛠 Built With ](#-built-with-) 29 | - [Tech Stack ](#tech-stack-) 30 | - [Key Features ](#key-features-) 31 | - [🚀 Live Demo ](#-live-demo-) 32 | - [💻 Getting Started ](#-getting-started-) 33 | - [Prerequisites](#prerequisites) 34 | - [Setup](#setup) 35 | - [Install](#install) 36 | - [Usage](#usage) 37 | - [Run tests](#run-tests) 38 | - [👥 Author ](#-author-) 39 | - [👥 Collaborator ](#-collaborator-) 40 | - [🔭 Future Features ](#-future-features-) 41 | - [🤝 Contributing ](#-contributing-) 42 | - [⭐️ Show your support ](#️-show-your-support-) 43 | - [🙏 Acknowledgments ](#-acknowledgments-) 44 | - [📝 License ](#-license-) 45 | 46 | 47 | 48 | # 📖 [BookStore] 49 | 50 | 51 | **BookStore** is a website that stores books names, authors, genre, and the user's progress on reading. Implementing this project to learn how to use React and Redux in the context of a real application. 52 | 53 | ## 🛠 Built With 54 | 55 | ### Tech Stack 56 | 57 |
58 | Client 59 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | ### Key Features 72 | 73 | 74 | - **Easy to the eyes and cultivating at the same time** 75 | - **Dynamic creation of content** 76 | 77 |

(back to top)

78 | 79 | 88 | 89 | 90 | 91 | ## 🚀 Live Demo 92 | 93 | 94 | - [Live Demo Link](https://bookstore-m1ib.onrender.com) 95 | 96 |

(back to top)

97 | 98 | 99 | 100 | 101 | ## 💻 Getting Started 102 | 103 | To get a local copy up and running, follow these steps: 104 | 105 | - Clone this repo as described in the setup section. 106 | - Make modifications as preferred 107 | 108 | 109 | ### Prerequisites 110 | 111 | In order to run this project you need: To clone or fork and run it in a browser 112 | 113 | 114 | ### Setup 115 | 116 | Clone this repository to your desired folder: 117 | 118 | Example commands: 119 | 120 | ```sh 121 | cd my-folder 122 | git clone https://github.com/Salimer/BookStore.git 123 | ``` 124 | 125 | using Ubuntu: 126 | 127 | ```sh 128 | cd my-desired-folder 129 | git clone https://github.com/Salimer/BookStore.git 130 | ``` 131 | 132 | For more information on how to clone or fork a repository: 133 | - How to clone a repo 134 | - How to fork a repo 135 | 136 | ### Install 137 | 138 | - Run ` npm install ` 139 | 140 | ### Usage 141 | 142 | To run the project, execute the following command: 143 | 144 | - Run `npm start` 145 | 146 | ### Run tests 147 | 148 | To run tests, run the following command: 149 | 150 | - to test validation errors `npx hint .` 151 | 152 | - to test CSS linter errors `npx stylelint "**/*.{css,scss}"` 153 | 154 | - to test JavaScript linter errors `npx eslint .` 155 | 156 | - to run the jest tests `npx test` 157 | 158 |

(back to top)

159 | 160 | 161 | 162 | ## 👥 Author 163 | 164 | 👤 **Salim Bamahfoodh** 165 | 166 | 167 | - GitHub: [@Salimer](https://github.com/Salimer) 168 | 169 | ## 👥 Collaborator 170 | 171 | 👤 **Martín Ezequiel González** 172 | 173 | - GitHub: [@Mar12358](https://github.com/Mar12358) 174 | - Twitter: [@MarezegonZ](https://twitter.com/MarezegonZ) 175 | - LinkedIn: [Martin Ezequiel Gonzalez](https://www.linkedin.com/in/martin-ezequiel-gonzalez-30a413260/) 176 | 177 | 178 | 179 | 180 | 181 | ## 🔭 Future Features 182 | 183 | 184 | 185 | Future changes: 186 | - Add animations and transitions 187 | - Add more styles and colors 188 | 189 |

(back to top)

190 | 191 | 192 | 193 | ## 🤝 Contributing 194 | 195 | Contributions, issues, and feature requests are welcome! 196 | 197 | Feel free to check the [issues page](https://github.com/Salimer/Math-magicians/issues). 198 | 199 |

(back to top)

200 | 201 | 202 | 203 | ## ⭐️ Show your support 204 | 205 | 206 | Give a ⭐️ if you like this project! 207 | 208 |

(back to top)

209 | 210 | 211 | 212 | ## 🙏 Acknowledgments 213 | 214 | 215 | - Thanks to all Microverse community 216 | 217 | 218 |

(back to top)

219 | 220 | 237 | 238 | 239 | 240 | ## 📝 License 241 | 242 | This project is [MIT](./LICENSE) licensed. 243 | 244 | 245 |

(back to top)

246 | -------------------------------------------------------------------------------- /murple_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salimer/Bookstore/e4914e62e38e266abad732e3d5df999ac0d9b19f/murple_logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookstore", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.9.5", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.4.0", 11 | "localforage": "^1.10.0", 12 | "match-sorter": "^6.3.1", 13 | "prop-types": "^15.8.1", 14 | "react": "^18.2.0", 15 | "react-circular-progressbar": "^2.1.0", 16 | "react-dom": "^18.2.0", 17 | "react-icons": "^4.9.0", 18 | "react-redux": "^8.0.7", 19 | "react-router-dom": "^6.12.0", 20 | "react-scripts": "5.0.1", 21 | "sort-by": "^1.2.0", 22 | "styled-components": "^5.3.10", 23 | "uuid": "^9.0.0", 24 | "web-vitals": "^2.1.4" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.22.1", 52 | "@babel/eslint-parser": "^7.21.8", 53 | "@babel/plugin-syntax-jsx": "^7.21.4", 54 | "@babel/preset-react": "^7.22.3", 55 | "eslint": "^7.32.0", 56 | "eslint-config-airbnb": "^18.2.1", 57 | "eslint-plugin-import": "^2.27.5", 58 | "eslint-plugin-jsx-a11y": "^6.7.1", 59 | "eslint-plugin-react": "^7.32.2", 60 | "eslint-plugin-react-hooks": "^4.6.0", 61 | "stylelint": "^13.13.1", 62 | "stylelint-config-standard": "^21.0.0", 63 | "stylelint-csstree-validator": "^1.9.0", 64 | "stylelint-scss": "^3.21.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salimer/Bookstore/e4914e62e38e266abad732e3d5df999ac0d9b19f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Bookstore 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salimer/Bookstore/e4914e62e38e266abad732e3d5df999ac0d9b19f/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salimer/Bookstore/e4914e62e38e266abad732e3d5df999ac0d9b19f/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;700&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | background-color: #fafafa; 11 | } 12 | 13 | ul { 14 | list-style: none; 15 | } 16 | 17 | a { 18 | text-decoration: none; 19 | } 20 | 21 | .App { 22 | text-align: center; 23 | } 24 | 25 | .App-logo { 26 | height: 40vmin; 27 | pointer-events: none; 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | .App-logo { 32 | animation: App-logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .App-header { 37 | background-color: #282c34; 38 | min-height: 100vh; 39 | display: flex; 40 | flex-direction: column; 41 | align-items: center; 42 | justify-content: center; 43 | font-size: calc(10px + 2vmin); 44 | color: white; 45 | } 46 | 47 | .App-link { 48 | color: #61dafb; 49 | } 50 | 51 | @keyframes App-logo-spin { 52 | from { 53 | transform: rotate(0deg); 54 | } 55 | 56 | to { 57 | transform: rotate(360deg); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import { Provider } from 'react-redux'; 3 | import Home from './routes/Home'; 4 | import Categories from './routes/Categories'; 5 | import './App.css'; 6 | import Navbar from './components/Navbar'; 7 | import store from './redux/store'; 8 | 9 | function App() { 10 | return ( 11 | 12 | 13 | 14 | } /> 15 | } /> 16 | 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/AddBook.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useDispatch } from 'react-redux'; 4 | import { postBook } from '../redux/books/thunks'; 5 | 6 | const AddBook = () => { 7 | const dispatch = useDispatch(); 8 | const [title, setTitle] = useState(''); 9 | const [author, setAuthor] = useState(''); 10 | 11 | const handleSubmit = async (e) => { 12 | e.preventDefault(); 13 | if (!title || !author) return; 14 | try { 15 | await dispatch(postBook([title, author])); 16 | setTitle(''); 17 | setAuthor(''); 18 | } catch (error) { console.log(error); } 19 | }; 20 | 21 | return ( 22 |
23 |

Add Book

24 |
25 | setTitle(e.target.value)} /> 26 | setAuthor(e.target.value)} /> 27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default AddBook; 34 | 35 | const Section = styled.section` 36 | border-top: 1px solid #e8e8e8; 37 | margin: 2rem 6.25rem; 38 | .title { 39 | height: 1.5rem; 40 | margin: 1.813rem auto 1.188rem 0; 41 | font-size: 1.25rem; 42 | font-weight: bold; 43 | font-stretch: normal; 44 | font-style: normal; 45 | line-height: normal; 46 | letter-spacing: -0.18px; 47 | color: #888; 48 | } 49 | .form { 50 | display: flex; 51 | justify-content: space-between; 52 | 53 | .book-title { 54 | height: 2.813rem; 55 | border-radius: 4px; 56 | border: solid 1px var(--white); 57 | background-color: #fff; 58 | padding-left: 1rem; 59 | width: 55.15%; 60 | } 61 | .book-author { 62 | height: 2.813rem; 63 | border-radius: 4px; 64 | border: solid 1px var(--white); 65 | background-color: #fff; 66 | padding-left: 1rem; 67 | width: 23.67%; 68 | } 69 | .add-book { 70 | height: 2.813rem; 71 | border-radius: 3px; 72 | color: #fff; 73 | background-color: #2e90f0; 74 | width: 15.28%; 75 | } 76 | } 77 | 78 | @media (max-width: 540px) { 79 | margin-right: 0; 80 | margin-left: 0; 81 | padding: 0 1.5rem; 82 | 83 | .form { 84 | .book-title { 85 | padding-left: 0.3rem; 86 | } 87 | 88 | .book-author { 89 | padding-left: 0.3rem; 90 | } 91 | } 92 | } 93 | `; 94 | -------------------------------------------------------------------------------- /src/components/Book.js: -------------------------------------------------------------------------------- 1 | import { CircularProgressbar } from 'react-circular-progressbar'; 2 | import 'react-circular-progressbar/dist/styles.css'; 3 | import styled from 'styled-components'; 4 | import PropTypes from 'prop-types'; 5 | import { useDispatch } from 'react-redux'; 6 | import Button from './Button'; 7 | import { deleteBook } from '../redux/books/thunks'; 8 | 9 | const Book = ({ book }) => { 10 | const dispatch = useDispatch(); 11 | const handleDeleteBook = async () => { 12 | try { 13 | await dispatch(deleteBook(book.id)); 14 | } catch (error) { 15 | console.log(error); 16 | } 17 | }; 18 | return ( 19 |
20 |
21 |
22 |

{book.category}

23 |

{book.title}

24 |

{book.author}

25 |
26 |
27 | 28 | 36 | 37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 |

60%

45 |

Completed

46 |
47 |
48 |
49 |
50 |

CURRENT CHAPTER

51 |

Chapter 13

52 |
53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | Book.propTypes = { 60 | book: PropTypes.shape({ 61 | id: PropTypes.string.isRequired, 62 | category: PropTypes.string.isRequired, 63 | title: PropTypes.string.isRequired, 64 | author: PropTypes.string.isRequired, 65 | }).isRequired, 66 | }; 67 | 68 | export default Book; 69 | 70 | const Section = styled.section` 71 | border: solid 1px #f0f0f0; 72 | display: flex; 73 | justify-content: space-between; 74 | background-color: #fff; 75 | margin: 1rem 6.25rem; 76 | padding: 2rem 1.5rem; 77 | 78 | .left { 79 | display: flex; 80 | flex-direction: column; 81 | justify-content: center; 82 | width: 40%; 83 | .book-details { 84 | .genre { 85 | height: 1.125rem; 86 | opacity: 0.5; 87 | font-size: 0.875rem; 88 | font-weight: bold; 89 | font-stretch: normal; 90 | font-style: normal; 91 | line-height: normal; 92 | letter-spacing: normal; 93 | color: #121212; 94 | } 95 | .title { 96 | height: 1.813rem; 97 | font-size: 1.375rem; 98 | font-weight: bold; 99 | font-stretch: normal; 100 | font-style: normal; 101 | line-height: normal; 102 | letter-spacing: -0.2px; 103 | color: #121212; 104 | } 105 | .author { 106 | height: 1.188rem; 107 | margin: 0.25rem 0 0; 108 | font-size: 0.875rem; 109 | font-weight: 300; 110 | font-stretch: normal; 111 | font-style: normal; 112 | line-height: normal; 113 | letter-spacing: normal; 114 | color: #4386bf; 115 | } 116 | } 117 | .interactions { 118 | padding-top: 1rem; 119 | margin: 0.188rem 0.938rem 0.125rem 0; 120 | 121 | .comments { 122 | margin: 0.188rem 0 0.125rem 0; 123 | font-size: 0.875rem; 124 | font-weight: 300; 125 | font-stretch: normal; 126 | font-style: normal; 127 | line-height: normal; 128 | letter-spacing: normal; 129 | color: #4386bf; 130 | background-color: #fff; 131 | border: none; 132 | } 133 | .remove { 134 | height: 1.188rem; 135 | font-size: 0.875rem; 136 | font-weight: 300; 137 | font-stretch: normal; 138 | font-style: normal; 139 | line-height: normal; 140 | letter-spacing: normal; 141 | color: #4386bf; 142 | background-color: #fff; 143 | border-right: 1px solid #e8e8e8; 144 | border-left: 1px solid #e8e8e8; 145 | border-top: none; 146 | border-bottom: none; 147 | padding: 0 0.938rem; 148 | margin: 0 0.938rem; 149 | } 150 | .edit { 151 | height: 1.188rem; 152 | font-size: 0.875rem; 153 | font-weight: 300; 154 | font-stretch: normal; 155 | font-style: normal; 156 | line-height: normal; 157 | letter-spacing: normal; 158 | color: #4386bf; 159 | background-color: #fff; 160 | border: none; 161 | } 162 | } 163 | } 164 | 165 | .middle { 166 | display: flex; 167 | align-items: center; 168 | 169 | .circle { 170 | max-width: 6.25rem; 171 | } 172 | .progress-text { 173 | border-right: 1px solid #e8e8e8; 174 | 175 | .percentage { 176 | height: 2.438rem; 177 | margin: 0.063rem 5.75rem 0 0.688rem; 178 | font-size: 2rem; 179 | font-weight: normal; 180 | font-stretch: normal; 181 | font-style: normal; 182 | line-height: normal; 183 | letter-spacing: normal; 184 | color: #121212; 185 | } 186 | .completed { 187 | height: 1.125rem; 188 | margin: 0.438rem 4.938rem 0.75rem 0.75rem; 189 | opacity: 0.5; 190 | font-size: 0.875rem; 191 | font-weight: normal; 192 | font-stretch: normal; 193 | font-style: normal; 194 | line-height: normal; 195 | letter-spacing: normal; 196 | color: #121212; 197 | } 198 | } 199 | } 200 | .right { 201 | display: flex; 202 | flex-direction: column; 203 | justify-content: center; 204 | 205 | .current-chapter-container { 206 | .current-chapter { 207 | height: 1.125rem; 208 | // margin: 0.063rem 3.625rem 0 3.688rem; 209 | opacity: 0.5; 210 | font-size: 0.813rem; 211 | font-weight: 300; 212 | font-stretch: normal; 213 | font-style: normal; 214 | line-height: normal; 215 | letter-spacing: normal; 216 | color: #121212; 217 | } 218 | .book-chapter { 219 | height: 1.313rem; 220 | margin: 0.438rem 0 0.25rem 0; 221 | font-size: 1rem; 222 | font-weight: 300; 223 | font-stretch: normal; 224 | font-style: normal; 225 | line-height: normal; 226 | letter-spacing: -0.4px; 227 | color: #121212; 228 | } 229 | } 230 | .update-progress { 231 | margin: 0.75rem 0 0.563rem 0; 232 | padding: 0.438rem 1.188rem 0.5rem 1.375rem; 233 | border-radius: 3px; 234 | background-color: #2e90f0; 235 | color: #c3daf0; 236 | } 237 | } 238 | 239 | @media (max-width: 1000px) { 240 | flex-direction: column; 241 | align-items: center; 242 | border-radius: 1rem; 243 | 244 | .left { 245 | width: 100%; 246 | align-items: start; 247 | order: 1; 248 | padding-bottom: 3rem; 249 | border-bottom: 1px solid rgb(232, 232, 232); 250 | } 251 | 252 | .middle { 253 | width: 100%; 254 | flex-direction: column; 255 | align-items: start; 256 | order: 2; 257 | padding-bottom: 3rem; 258 | border-bottom: 1px solid rgb(232, 232, 232); 259 | padding-top: 3rem; 260 | display: flex; 261 | 262 | .circle { 263 | display: none; 264 | } 265 | 266 | .progress-text { 267 | border-right: none; 268 | 269 | .percentage { 270 | margin-left: 0; 271 | } 272 | .completed { 273 | margin-left: 0; 274 | } 275 | } 276 | } 277 | 278 | .right { 279 | width: 100%; 280 | align-items: start; 281 | order: 3; 282 | padding-top: 3rem; 283 | } 284 | } 285 | 286 | @media (max-width: 540px) { 287 | margin: 3rem 0; 288 | } 289 | `; 290 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Button = ({ onClick, className, children }) => ( 5 | 8 | ); 9 | 10 | Button.propTypes = { 11 | onClick: PropTypes.func, 12 | className: PropTypes.string, 13 | children: PropTypes.node.isRequired, 14 | }; 15 | 16 | Button.defaultProps = { 17 | className: '', 18 | onClick: () => {}, 19 | }; 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /src/components/ListBooks.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import styled from 'styled-components'; 4 | import Book from './Book'; 5 | import AddBook from './AddBook'; 6 | import { getBooks } from '../redux/books/thunks'; 7 | 8 | const ListBooks = () => { 9 | const dispatch = useDispatch(); 10 | 11 | const { books, isLoading } = useSelector((store) => store.booksArr); 12 | 13 | useEffect(() => { 14 | dispatch(getBooks()); 15 | }, [dispatch]); 16 | 17 | // Convert object to an array of books 18 | const booksArray = Object.entries(books).reduce((acc, [id, bookList]) => { 19 | const booksWithId = bookList.map((book) => ({ ...book, id })); 20 | return [...acc, ...booksWithId]; 21 | }, []); 22 | 23 | return ( 24 |
25 | {isLoading ? ( 26 | 27 | 28 | 29 | ) : ( 30 | <> 31 | {booksArray.map((book) => ( 32 | 33 | ))} 34 | 35 | 36 | )} 37 |
38 | ); 39 | }; 40 | 41 | export default ListBooks; 42 | 43 | const Section = styled.section``; 44 | 45 | const LoadingSpinner = styled.div` 46 | display: flex; 47 | align-items: center; 48 | justify-content: center; 49 | height: 200px; 50 | `; 51 | 52 | const Spinner = styled.div` 53 | width: 40px; 54 | height: 40px; 55 | border: 4px solid #f3f3f3; 56 | border-top: 4px solid #3498db; 57 | border-radius: 50%; 58 | animation: spin 2s linear infinite; 59 | 60 | @keyframes spin { 61 | 0% { 62 | transform: rotate(0deg); 63 | } 64 | 100% { 65 | transform: rotate(360deg); 66 | } 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import styled from 'styled-components'; 3 | import { CgProfile } from 'react-icons/cg'; 4 | 5 | const Navbar = () => ( 6 | 20 | ); 21 | 22 | export default Navbar; 23 | 24 | const Nav = styled.nav` 25 | display: flex; 26 | align-items: center; 27 | height: 5.938rem; 28 | padding: 0 6.25rem; 29 | background-color: #fff; 30 | margin-bottom: 3.125rem; 31 | border-bottom: 1px solid #e8e8e8; 32 | 33 | .title { 34 | width: 15rem; 35 | height: 2.313rem; 36 | margin: 0.375rem 2.938rem 0.125rem 0; 37 | font-family: Montserrat; 38 | font-size: 1.875rem; 39 | font-weight: bold; 40 | font-stretch: normal; 41 | font-style: normal; 42 | line-height: normal; 43 | letter-spacing: normal; 44 | color: #0090ff; 45 | } 46 | .navigation { 47 | display: flex; 48 | gap: 2.5rem; 49 | 50 | .link { 51 | width: 3.688rem; 52 | height: 1rem; 53 | margin: 1.125rem 2.563rem 0.688rem 0; 54 | font-family: Montserrat; 55 | font-size: 0.813rem; 56 | font-weight: normal; 57 | font-stretch: normal; 58 | font-style: normal; 59 | line-height: normal; 60 | letter-spacing: 1.9px; 61 | color: #121212; 62 | } 63 | } 64 | .profile { 65 | margin-left: auto; 66 | 67 | .icon { 68 | width: auto; 69 | height: 2.313rem; 70 | color: #0090ff; 71 | } 72 | } 73 | @media (max-width: 1000px) { 74 | .navigation { 75 | display: none; 76 | } 77 | } 78 | 79 | @media (max-width: 540px) { 80 | padding: 0 1.5rem; 81 | 82 | .title { 83 | font-size: 1.5rem; 84 | } 85 | } 86 | `; 87 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, 5 | BlinkMacSystemFont, 6 | 'Segoe UI', 7 | 'Roboto', 8 | 'Oxygen', 9 | 'Ubuntu', 10 | 'Cantarell', 11 | 'Fira Sans', 12 | 'Droid Sans', 13 | 'Helvetica Neue', 14 | sans-serif; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | code { 20 | font-family: 21 | source-code-pro, 22 | Menlo, 23 | Monaco, 24 | Consolas, 25 | 'Courier New', 26 | monospace; 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root')); 9 | root.render( 10 | 11 | 12 | 13 | 14 | , 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/books/booksArrSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { getBooks, postBook, deleteBook } from './thunks'; 3 | 4 | const initialState = { 5 | books: [], 6 | isLoading: true, 7 | }; 8 | 9 | const booksArrSlice = createSlice({ 10 | name: 'booksArr', 11 | initialState, 12 | extraReducers: (builder) => { 13 | // Cases for getBooks thunk 14 | builder 15 | .addCase(getBooks.pending, (state) => { 16 | state.isLoading = true; 17 | }) 18 | .addCase(getBooks.fulfilled, (state, action) => { 19 | state.isLoading = false; 20 | state.books = action.payload; 21 | }) 22 | .addCase(getBooks.rejected, (state, action) => { 23 | console.log(action); 24 | state.isLoading = false; 25 | }); 26 | 27 | // Cases for postBook thunk 28 | builder 29 | .addCase(postBook.pending, (state) => { 30 | state.isLoading = true; 31 | }) 32 | .addCase(postBook.fulfilled, (state) => { 33 | state.isLoading = true; 34 | }) 35 | .addCase(postBook.rejected, (state, action) => { 36 | console.log(action); 37 | state.isLoading = false; 38 | }); 39 | 40 | // Cases for deleteBook thunk 41 | builder 42 | .addCase(deleteBook.pending, (state) => { 43 | state.isLoading = true; 44 | }) 45 | .addCase(deleteBook.fulfilled, (state) => { 46 | state.isLoading = true; 47 | }) 48 | .addCase(deleteBook.rejected, (state, action) => { 49 | console.log(action); 50 | state.isLoading = false; 51 | }); 52 | }, 53 | }); 54 | 55 | export default booksArrSlice.reducer; 56 | -------------------------------------------------------------------------------- /src/redux/books/thunks.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import axios from 'axios'; 4 | 5 | // Get books from API 6 | const getBooksURL = 'https://us-central1-bookstore-api-e63c8.cloudfunctions.net/bookstoreApi/apps/xqDpmLzvPxikb9A9LRQw/books'; 7 | export const getBooks = createAsyncThunk('books/getBooks', async (thunkAPI) => { 8 | try { 9 | const response = await axios(getBooksURL); 10 | return response.data; 11 | } catch (error) { 12 | console.log(error); 13 | return thunkAPI.rejectWithValue('something went wrong'); 14 | } 15 | }); 16 | 17 | // Add book to API 18 | const addBookURL = 'https://us-central1-bookstore-api-e63c8.cloudfunctions.net/bookstoreApi/apps/xqDpmLzvPxikb9A9LRQw/books'; 19 | export const postBook = createAsyncThunk('books/addBook', async (book, thunkAPI) => { 20 | const bookObj = { 21 | item_id: uuidv4(), 22 | title: book[0], 23 | author: book[1], 24 | category: 'Action', 25 | }; 26 | try { 27 | const response = await axios.post(addBookURL, bookObj); 28 | thunkAPI.dispatch(getBooks()); // Dispatch getBooks after successful post 29 | return response.data; 30 | } catch (error) { 31 | console.log(error); 32 | return thunkAPI.rejectWithValue('something went wrong'); 33 | } 34 | }); 35 | 36 | // Delete book from API 37 | const deleteBookURL = 'https://us-central1-bookstore-api-e63c8.cloudfunctions.net/bookstoreApi/apps/xqDpmLzvPxikb9A9LRQw/books'; 38 | export const deleteBook = createAsyncThunk('books/deleteBook', async (bookId, thunkAPI) => { 39 | try { 40 | const response = await axios.delete(`${deleteBookURL}/${bookId}`); 41 | thunkAPI.dispatch(getBooks()); // Dispatch getBooks after successful deletion 42 | return response.data; 43 | } catch (error) { 44 | console.log(error); 45 | return thunkAPI.rejectWithValue('something went wrong'); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/redux/categories/categoriesSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | categories: [], 5 | }; 6 | 7 | const categoriesSlice = createSlice({ 8 | name: 'categories', 9 | initialState, 10 | reducers: { 11 | setStatus: (state) => { 12 | state.categories = 'Under construction'; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setStatus } = categoriesSlice.actions; 18 | 19 | export default categoriesSlice.reducer; 20 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import booksArrSlice from './books/booksArrSlice'; 3 | import categoriesSlice from './categories/categoriesSlice'; 4 | 5 | const store = configureStore({ 6 | reducer: { 7 | booksArr: booksArrSlice, 8 | categories: categoriesSlice, 9 | }, 10 | }); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ 4 | getCLS, getFID, getFCP, getLCP, getTTFB, 5 | }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/routes/Categories.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Categories = () => ( 4 |
5 |

Under Construction

6 |
7 | ); 8 | 9 | export default Categories; 10 | 11 | const Div = styled.section` 12 | display: flex; 13 | justify-content: center; 14 | height: 100vh; 15 | 16 | h1 { 17 | font-size: 3rem; 18 | text-align: center; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/routes/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import { useRouteError } from 'react-router-dom'; 2 | 3 | export default function ErrorPage() { 4 | const error = useRouteError(); 5 | 6 | return ( 7 |
8 |

Oops!

9 |

Something went wrong.

10 |

11 | {error.statusText || error.message} 12 |

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/Home.js: -------------------------------------------------------------------------------- 1 | import ListBooks from '../components/ListBooks'; 2 | 3 | const Home = () => ; 4 | export default Home; 5 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------