├── .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 |
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 | You need to enable JavaScript to run this app.
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 |
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 | Comments
28 | {
31 | handleDeleteBook();
32 | }}
33 | >
34 | Remove
35 |
36 | Edit
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
60%
45 |
Completed
46 |
47 |
48 |
49 |
50 |
CURRENT CHAPTER
51 |
Chapter 13
52 |
53 | UPDATE PROGRESS
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 |
6 | {children}
7 |
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 |
7 | Bookstore CMS
8 |
9 |
10 | BOOKS
11 |
12 |
13 | Categories
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------