├── .env.sample ├── .vscode └── settings.json ├── .prettierrc ├── src ├── utils │ └── config.js ├── assets │ ├── logo.png │ ├── loader.gif │ ├── fallback.png │ ├── image-loader.png │ └── image-loading.gif ├── components │ ├── css │ │ ├── Hamburger.css │ │ ├── BackButton.css │ │ ├── SearchButton.css │ │ ├── CheckButton.css │ │ ├── SmallPic.css │ │ ├── Loader.css │ │ ├── MiniHeader.css │ │ ├── MangaCardList.css │ │ ├── ErrorMessage.css │ │ ├── ChapterBox.css │ │ ├── MangaCard.css │ │ ├── Header.css │ │ └── CategoryBox.css │ └── js │ │ ├── SmallPic.js │ │ ├── Loader.js │ │ ├── SearchButton.js │ │ ├── ChapterImage.js │ │ ├── MiniHeader.js │ │ ├── AppName.js │ │ ├── BackButton.js │ │ ├── Image.js │ │ ├── CheckButton.js │ │ ├── CategoryList.js │ │ ├── ErrorMessage.js │ │ ├── MangaCard.js │ │ ├── Hamburger.js │ │ ├── ChapterBox.js │ │ ├── ChapterBoxList.js │ │ ├── MangaCardList.js │ │ ├── CategoryBox.js │ │ ├── AuthDetails.js │ │ ├── Header.js │ │ └── Collapsible.js ├── containers │ ├── css │ │ ├── Categories.css │ │ ├── SearchResults.css │ │ ├── Settings.css │ │ ├── EditProfile.css │ │ ├── UserProfile.css │ │ ├── HistoryPage.css │ │ ├── App.css │ │ ├── NavBar.css │ │ ├── MangaPage.css │ │ └── ChapterPage.css │ └── js │ │ ├── LibraryUpdates.js │ │ ├── Favorites.js │ │ ├── Library.js │ │ ├── Categories.js │ │ ├── RecentUpdate.js │ │ ├── CategoryPage.js │ │ ├── ExplorePage.js │ │ ├── HistoryPage.js │ │ ├── App.js │ │ ├── SignIn.js │ │ ├── SearchResults.js │ │ ├── SignUp.js │ │ ├── Settings.js │ │ ├── NavBar.js │ │ ├── UserProfile.js │ │ ├── EditProfile.js │ │ └── ChapterPage.js ├── setupTests.js ├── index.js ├── index.css └── serviceWorker.js ├── netlify.toml ├── public ├── logo72.png ├── logo96.png ├── favicon.ico ├── logo128.png ├── logo144.png ├── logo152.png ├── logo192.png ├── logo384.png ├── logo512.png ├── ogimage.jpg ├── robots.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── logo-192-default.png ├── logo-512-default.png ├── manifest.json └── index.html ├── .gitignore ├── README.md └── package.json /.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_API_BASE_URL= -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL; 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 5 | -------------------------------------------------------------------------------- /public/logo72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo72.png -------------------------------------------------------------------------------- /public/logo96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo96.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo128.png -------------------------------------------------------------------------------- /public/logo144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo144.png -------------------------------------------------------------------------------- /public/logo152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo152.png -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo384.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/ogimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/ogimage.jpg -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/src/assets/loader.gif -------------------------------------------------------------------------------- /src/components/css/Hamburger.css: -------------------------------------------------------------------------------- 1 | .hamburger .fa-bars { 2 | width: 24px; 3 | height: 24px; 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/src/assets/fallback.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/logo-192-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo-192-default.png -------------------------------------------------------------------------------- /public/logo-512-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/public/logo-512-default.png -------------------------------------------------------------------------------- /src/assets/image-loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/src/assets/image-loader.png -------------------------------------------------------------------------------- /src/assets/image-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justsolomon/mangahaven/HEAD/src/assets/image-loading.gif -------------------------------------------------------------------------------- /src/components/css/BackButton.css: -------------------------------------------------------------------------------- 1 | .back-button .fa-arrow-left { 2 | width: 24px; 3 | height: 24px; 4 | cursor: pointer; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/css/SearchButton.css: -------------------------------------------------------------------------------- 1 | .search-button { 2 | margin-right: 1rem; 3 | } 4 | 5 | .search-button .fa-search { 6 | height: 20px; 7 | width: 20px; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/css/CheckButton.css: -------------------------------------------------------------------------------- 1 | .check-button { 2 | width: 0; 3 | padding: 0; 4 | background: none; 5 | border: none; 6 | margin-right: 1.5rem; 7 | } 8 | 9 | .check-button .fa-circle { 10 | width: 18px; 11 | height: 18px; 12 | } 13 | -------------------------------------------------------------------------------- /src/containers/css/Categories.css: -------------------------------------------------------------------------------- 1 | .category-list { 2 | width: 95%; 3 | margin: 1rem auto; 4 | } 5 | 6 | @media (min-width: 1000px) { 7 | .category-list { 8 | margin-left: 22%; 9 | width: 78%; 10 | padding-right: 1rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/css/SmallPic.css: -------------------------------------------------------------------------------- 1 | .small-profile-pic { 2 | width: 30px; 3 | height: 30px; 4 | overflow: hidden; 5 | border-radius: 50%; 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | .small-profile-pic img { 11 | width: 40px; 12 | height: 30px; 13 | } 14 | -------------------------------------------------------------------------------- /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/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/components/css/Loader.css: -------------------------------------------------------------------------------- 1 | .loader-gif { 2 | width: 100%; 3 | text-align: center; 4 | margin: 0 auto; 5 | } 6 | 7 | .loader-gif img { 8 | width: 64px; 9 | } 10 | 11 | @media (min-width: 1000px) { 12 | .loader-gif { 13 | margin-left: 22%; 14 | width: 78%; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/css/SearchResults.css: -------------------------------------------------------------------------------- 1 | .no-results { 2 | width: 90%; 3 | margin: 1.5rem auto; 4 | text-align: center; 5 | } 6 | 7 | .no-results span { 8 | font-weight: 500; 9 | } 10 | 11 | @media (min-width: 1000px) { 12 | .no-results { 13 | margin-left: 22%; 14 | width: 78%; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/js/SmallPic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Pic from '../../assets/pic.jpg'; 3 | import '../css/SmallPic.css'; 4 | 5 | const SmallPic = () => { 6 | return ( 7 |
8 | user profile pic 9 |
10 | ); 11 | }; 12 | 13 | export default SmallPic; 14 | -------------------------------------------------------------------------------- /src/components/js/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../css/Loader.css'; 3 | import LoaderGif from '../../assets/loader.gif'; 4 | 5 | const Loader = ({ background }) => { 6 | return ( 7 |
8 | loader gif 9 |
10 | ); 11 | }; 12 | 13 | export default Loader; 14 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 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 | -------------------------------------------------------------------------------- /src/components/js/SearchButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faSearch } from '@fortawesome/free-solid-svg-icons'; 4 | import '../css/SearchButton.css'; 5 | 6 | const SearchButton = ({ toggleSearch }) => { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | export default SearchButton; 15 | -------------------------------------------------------------------------------- /src/components/js/ChapterImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-image'; 3 | import ImageLoader from '../../assets/image-loading.gif'; 4 | 5 | const ChapterImage = ({ url }) => { 6 | return ( 7 | 11 | loader icon 12 | 13 | } 14 | alt='chapter page' 15 | /> 16 | ); 17 | }; 18 | 19 | export default ChapterImage; 20 | -------------------------------------------------------------------------------- /src/components/js/MiniHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BackButton from './BackButton.js'; 3 | import { useHistory } from 'react-router'; 4 | import '../css/MiniHeader.css'; 5 | 6 | const MiniHeader = ({ currentMenu }) => { 7 | const history = useHistory(); 8 | return ( 9 |
10 | history.push('/')} /> 11 |

{currentMenu}

12 |
13 | ); 14 | }; 15 | 16 | export default MiniHeader; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './containers/js/App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.register(); 13 | -------------------------------------------------------------------------------- /src/components/js/AppName.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Logo from '../../assets/logo.png'; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | const AppName = () => { 6 | const history = useHistory(); 7 | return ( 8 |
history.push('/')} 11 | style={{ cursor: 'pointer' }} 12 | > 13 | MangaHaven logo 14 |

MangaHaven

15 |
16 | ); 17 | }; 18 | 19 | export default AppName; 20 | -------------------------------------------------------------------------------- /src/components/js/BackButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; 4 | import '../css/BackButton.css'; 5 | 6 | const BackButton = ({ clickAction }) => { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | export default BackButton; 15 | 16 | // function() { window.history.back() } 17 | -------------------------------------------------------------------------------- /src/components/js/Image.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Img from 'react-image'; 3 | import ImageLoader from '../../assets/image-loading.gif'; 4 | import Fallback from '../../assets/fallback.png'; 5 | 6 | const Image = ({ url, title }) => { 7 | return ( 8 | 12 | loader icon 13 | 14 | } 15 | alt={`${title} cover illustration`} 16 | /> 17 | ); 18 | }; 19 | 20 | export default Image; 21 | -------------------------------------------------------------------------------- /src/components/css/MiniHeader.css: -------------------------------------------------------------------------------- 1 | .mini-header { 2 | height: 3rem; 3 | display: flex; 4 | align-items: center; 5 | color: #fff; 6 | background-color: rgb(86, 128, 220); 7 | box-shadow: rgba(0, 0, 0, 0.13) 0px 2px 4px 0px; 8 | padding-left: 1rem; 9 | position: sticky; 10 | z-index: 1; 11 | top: 0; 12 | } 13 | 14 | .mini-header .current-menu { 15 | margin: 0; 16 | /*padding-bottom: .1rem;*/ 17 | margin-left: 1rem; 18 | font-size: 1.3rem; 19 | font-weight: 600; 20 | } 21 | 22 | @media (min-width: 1000px) { 23 | .mini-header { 24 | margin-left: 22%; 25 | width: 78%; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/js/CheckButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faCircle as regularCircle } from '@fortawesome/free-regular-svg-icons'; 4 | import { faCircle as solidCircle } from '@fortawesome/free-solid-svg-icons'; 5 | import '../css/CheckButton.css'; 6 | 7 | const CheckButton = ({ classname, view }) => { 8 | return ( 9 | 16 | ); 17 | }; 18 | 19 | export default CheckButton; 20 | -------------------------------------------------------------------------------- /src/components/js/CategoryList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LazyLoad from 'react-lazyload'; 3 | import Loader from './Loader.js'; 4 | import CategoryBox from './CategoryBox.js'; 5 | 6 | const CategoryList = ({ categoryManga }) => { 7 | return ( 8 |
9 | {categoryManga.map((category, i) => { 10 | return ( 11 | } 17 | > 18 | 19 | 20 | ); 21 | })} 22 |
23 | ); 24 | }; 25 | 26 | export default CategoryList; 27 | -------------------------------------------------------------------------------- /src/components/css/MangaCardList.css: -------------------------------------------------------------------------------- 1 | .manga-list { 2 | display: grid; 3 | grid-template-columns: repeat(2, 1fr); 4 | grid-gap: 0.7rem 1rem; 5 | margin: 1rem auto; 6 | width: 90%; 7 | } 8 | 9 | @media (min-width: 500px) { 10 | .manga-list { 11 | width: 95%; 12 | grid-template-columns: repeat(3, 1fr); 13 | } 14 | } 15 | 16 | @media (min-width: 675px) { 17 | .manga-list { 18 | grid-template-columns: repeat(4, 1fr); 19 | grid-gap: 1rem 1.5rem; 20 | } 21 | } 22 | 23 | @media (min-width: 1000px) { 24 | .manga-list { 25 | margin-left: 22%; 26 | width: 78%; 27 | padding: 0 1rem; 28 | } 29 | } 30 | 31 | @media (min-width: 1200px) { 32 | .manga-list { 33 | grid-template-columns: repeat(5, 1fr); 34 | grid-gap: 1.5rem 1.8rem; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/js/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../css/ErrorMessage.css'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { faRedo } from '@fortawesome/free-solid-svg-icons'; 5 | 6 | const ErrorMessage = ({ renderList }) => { 7 | return ( 8 |
9 |
10 |

11 | An error occurred while loading. Please check your internet connection 12 | and try again 13 |

14 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default ErrorMessage; 24 | -------------------------------------------------------------------------------- /src/components/js/MangaCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../css/MangaCard.css'; 3 | import Image from './Image.js'; 4 | import LazyLoad from 'react-lazyload'; 5 | import { useHistory } from 'react-router-dom'; 6 | 7 | const MangaCard = ({ imageUrl, mangaTitle, alias }) => { 8 | const history = useHistory(); 9 | return ( 10 | } 13 | height={100} 14 | offset={[-50, 50]} 15 | > 16 |
22 | 23 |
24 |

{mangaTitle}

25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default MangaCard; 32 | -------------------------------------------------------------------------------- /src/components/css/ErrorMessage.css: -------------------------------------------------------------------------------- 1 | .error-div { 2 | width: 85%; 3 | margin: 2rem auto; 4 | text-align: center; 5 | } 6 | 7 | .error-div .loader-gif { 8 | display: none; 9 | } 10 | 11 | .error-message { 12 | margin: 0; 13 | padding: 0 0.5rem; 14 | font-size: 1.05rem; 15 | } 16 | 17 | .reload-button { 18 | padding: 0.5rem 1rem; 19 | border-radius: 1rem; 20 | border: 1px solid rgb(86, 128, 220); 21 | background-color: rgb(86, 128, 220); 22 | color: #fff; 23 | margin-top: 1rem; 24 | cursor: pointer; 25 | } 26 | 27 | .reload-button:hover { 28 | border: 1px solid rgb(76, 118, 210); 29 | background-color: rgb(76, 118, 210); 30 | } 31 | 32 | .reload-button span { 33 | font-size: 1rem; 34 | font-weight: 500; 35 | margin-left: 0.5rem; 36 | } 37 | 38 | .reload-button .fa-redo { 39 | width: 14px; 40 | height: 14px; 41 | } 42 | 43 | @media (min-width: 1000px) { 44 | .error-div { 45 | margin-left: 22%; 46 | width: 78%; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | background-color: #fdfffc; 9 | /*#0D1F2D*/ 10 | } 11 | 12 | /*:root { 13 | --bg-color: ; 14 | --font-color: ; 15 | }*/ 16 | 17 | body { 18 | margin: 0; 19 | width: 100%; 20 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 21 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 22 | sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | color: #100c08; 26 | } 27 | 28 | a { 29 | text-decoration: none; 30 | } 31 | 32 | button { 33 | font-family: inherit; 34 | } 35 | 36 | a:focus, 37 | button:focus { 38 | outline: none; 39 | } 40 | 41 | code { 42 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 43 | monospace; 44 | } 45 | 46 | /* to prevent scroll when navbar is open */ 47 | .prevent-scroll { 48 | height: 100vh; 49 | overflow: hidden; 50 | } 51 | 52 | /*former-header-color: #5680e9*/ 53 | -------------------------------------------------------------------------------- /src/components/js/Hamburger.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faBars } from '@fortawesome/free-solid-svg-icons'; 4 | import '../css/Hamburger.css'; 5 | 6 | class Hamburger extends React.Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | count: 0, 11 | }; 12 | } 13 | 14 | displayNavbar = () => { 15 | if (this.state.count === 1) 16 | document.querySelector('.navigation').classList.toggle('slide-out-left'); 17 | document.querySelector('.navigation-outer').classList.toggle('unhide'); 18 | document.querySelector('.navigation').classList.toggle('slide-in-left'); 19 | document.querySelector('html').classList.toggle('prevent-scroll'); 20 | this.setState({ count: 1 }); 21 | }; 22 | 23 | render() { 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } 30 | } 31 | 32 | export default Hamburger; 33 | -------------------------------------------------------------------------------- /src/components/css/ChapterBox.css: -------------------------------------------------------------------------------- 1 | .manga-chapter { 2 | border-bottom: 1px solid rgba(140, 140, 140, 0.5); 3 | transition: 0.3s; 4 | cursor: pointer; 5 | } 6 | 7 | .manga-chapter:hover { 8 | background-color: #ddd; 9 | } 10 | 11 | .chapter-completed { 12 | color: rgb(150, 150, 150) !important; 13 | } 14 | 15 | .manga-chapter p { 16 | padding: 0 1rem; 17 | margin: 0 0 0.5rem 0; 18 | } 19 | 20 | .manga-chapter .chapter-name { 21 | font-size: 0.95rem; 22 | padding-top: 0.8rem; 23 | } 24 | 25 | .manga-chapter .chapter-details { 26 | font-size: 0.85rem; 27 | margin: 0; 28 | display: flex; 29 | } 30 | 31 | .chapter-details p { 32 | margin: 0.3rem 0; 33 | } 34 | 35 | .chapter-details .release-date { 36 | width: 40%; 37 | } 38 | 39 | .chapter-details .history-page-number { 40 | color: rgb(150, 150, 150); 41 | width: 60%; 42 | } 43 | 44 | @media (min-width: 1000px) { 45 | .manga-chapter .chapter-name { 46 | font-size: 1.05rem; 47 | } 48 | .manga-chapter .chapter-details { 49 | font-size: 0.95rem; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/js/ChapterBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../css/ChapterBox.css'; 3 | 4 | const ChapterBox = ({ 5 | number, 6 | title, 7 | uploadDate, 8 | completed, 9 | page, 10 | displayChapter, 11 | continueChapter, 12 | }) => { 13 | const date = new Date(uploadDate).toLocaleDateString(); 14 | return ( 15 |
21 |

22 | {title ? `Chapter ${number} - ${title}` : `Chapter ${number}`} 23 |

24 |
25 |

{date}

26 | {page !== undefined ? ( 27 |

28 | {`Page: ${page} ${completed ? `(Completed)` : ''}`} 29 |

30 | ) : null} 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default ChapterBox; 37 | -------------------------------------------------------------------------------- /src/components/js/ChapterBoxList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChapterBox from './ChapterBox.js'; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | const ChapterBoxList = ({ allChapters, mangaName }) => { 6 | const history = useHistory(); 7 | return ( 8 |
9 | {allChapters.map((chapter, i) => { 10 | const { 11 | chapterNum, 12 | Date, 13 | ChapterName, 14 | completed, 15 | currentPage, 16 | } = chapter; 17 | return ( 18 | { 26 | history.push(`/read/${mangaName}/chapter/${chapterNum}/`); 27 | }} 28 | continueChapter={() => { 29 | history.push( 30 | `/read/${mangaName}/chapter/${chapterNum}?q=${chapter[4]}` 31 | ); 32 | }} 33 | /> 34 | ); 35 | })} 36 |
37 | ); 38 | }; 39 | 40 | export default ChapterBoxList; 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mangahaven 2 | An online manga reader for learning purposes. Try it out [here](https://mangahaven.netlify.app). 3 | 4 | ## Demo 5 | ![mangahaven](https://github.com/justsolomon/mangahaven/assets/55158465/4a84df32-0f57-4869-a429-6c4ee3b128b8) 6 | 7 | ## Features 8 | - Browse all/recently updated manga 9 | - Read the latest manga chapters 10 | - Bookmark/Favorite manga 11 | - Resume reading a manga from the History section 12 | - Search for manga 13 | 14 | ## Getting Started 15 | The following contains the steps required to get the application up and running on your local workspace. 16 | 17 | ### Prerequisites 18 | - Node v16.16.0 19 | - npm v8.11.0 20 | - Git v2.39.2 21 | 22 | ### Running locally 23 | 24 | To run the app locally, follow the steps below: 25 | 26 | 1. Clone the repository to your PC using your terminal. For more info, refer to this [article](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github/cloning-a-repository) 27 | 28 | 2. After cloning, navigate into the repo using the command: 29 | ``` 30 | cd mangahaven 31 | ``` 32 | 33 | 3. Install the dependencies in the package.json using the command: 34 | ``` 35 | npm install 36 | ``` 37 | 38 | 5. After the dependencies have been installed, run the app in your terminal using the command 39 | ``` 40 | npm start 41 | ``` 42 | -------------------------------------------------------------------------------- /src/components/js/MangaCardList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../css/MangaCardList.css'; 3 | import MangaCard from './MangaCard.js'; 4 | 5 | const MangaCardList = ({ mangaArray, genre, search, bookmark }) => { 6 | return ( 7 |
8 | {mangaArray.map((manga, id) => { 9 | let data; 10 | const { 11 | SeriesName, 12 | IndexName, 13 | imageUrl, 14 | serialName, 15 | name, 16 | alias, 17 | s, 18 | i, 19 | } = manga; 20 | if (genre) data = { imageUrl, name, alias: serialName }; 21 | else if (bookmark) data = { imageUrl, name, alias }; 22 | else if (search) 23 | data = { 24 | imageUrl: `https://temp.compsci88.com/cover/${i}.jpg`, 25 | name: s, 26 | alias: i, 27 | }; 28 | else 29 | data = { 30 | imageUrl: `https://temp.compsci88.com/cover/${IndexName}.jpg`, 31 | name: SeriesName, 32 | alias: IndexName, 33 | }; 34 | return ( 35 | 41 | ); 42 | })} 43 |
44 | ); 45 | }; 46 | 47 | export default MangaCardList; 48 | -------------------------------------------------------------------------------- /src/components/css/MangaCard.css: -------------------------------------------------------------------------------- 1 | .manga-card { 2 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); 3 | transition: 0.3s; 4 | border-radius: 0.1rem; 5 | position: relative; 6 | height: 200px; 7 | width: 100%; 8 | background-color: rgba(0, 0, 0, 0.5); 9 | cursor: pointer; 10 | } 11 | 12 | .manga-card:hover { 13 | box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2); 14 | } 15 | 16 | .manga-card img { 17 | border-radius: 0.1rem; 18 | width: 100%; 19 | height: 200px; 20 | } 21 | 22 | .manga-card .image-loader { 23 | border-radius: 0; 24 | width: 100%; 25 | margin: 50px auto; 26 | text-align: center; 27 | } 28 | 29 | .manga-card .image-loader img { 30 | width: 72px; 31 | height: 72px; 32 | } 33 | 34 | .container { 35 | position: absolute; 36 | bottom: 0; 37 | padding: 0.3rem; 38 | background: rgba(0, 0, 0, 0.3); 39 | color: rgb(250, 250, 250); 40 | width: 100%; 41 | } 42 | 43 | .container .manga-title { 44 | padding: 0; 45 | margin: 0.2rem 0; 46 | font-weight: 600; 47 | font-size: 0.95rem; 48 | overflow: hidden; 49 | white-space: nowrap; 50 | text-overflow: ellipsis; 51 | } 52 | 53 | @media (min-width: 675px) { 54 | .manga-card { 55 | width: 100%; 56 | } 57 | } 58 | 59 | @media (min-width: 1000px) { 60 | .manga-card, 61 | .manga-card img { 62 | height: 220px; 63 | } 64 | .manga-card .image-loader { 65 | margin: 60px auto; 66 | } 67 | .container { 68 | padding: 0.4rem 0.5rem; 69 | } 70 | .container .manga-title { 71 | font-size: 1.05rem; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/containers/js/LibraryUpdates.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import localForage from 'localforage'; 3 | import Header from '../../components/js/Header.js'; 4 | import NavBar from './NavBar.js'; 5 | 6 | class LibraryUpdates extends React.Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | chapterUpdates: [], 11 | noUpdate: '', 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | localForage 17 | .getItem('library-updates') 18 | .then((value) => { 19 | this.setState({ chapterUpdates: value }); 20 | }) 21 | .catch(console.log); 22 | 23 | localForage.getItem('userBookmarks').then((value) => { 24 | if (value === null || value === []) { 25 | this.setState({ 26 | chapterUpdates: null, 27 | noUpdate: 'There are no manga in your library', 28 | }); 29 | } 30 | }); 31 | localForage.getItem('offlineManga').then(console.log); 32 | } 33 | 34 | render() { 35 | // const { chapterUpdates } = this.state; 36 | return ( 37 |
38 |
39 | 40 |

Feature coming soon

41 | {/* {chapterUpdates === null ? ( 42 |
43 |

No updates

44 |
45 | ) : ( 46 |

Updates available

47 | )} */} 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default LibraryUpdates; 54 | -------------------------------------------------------------------------------- /src/containers/css/Settings.css: -------------------------------------------------------------------------------- 1 | .Collapsible p { 2 | margin: 0; 3 | } 4 | 5 | .Collapsible__trigger { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | padding: 1rem; 10 | border-bottom: 1px solid rgb(140, 140, 140, 0.5); 11 | } 12 | 13 | .Collapsible .svg-inline--fa { 14 | height: 20px; 15 | width: 20px; 16 | color: rgba(255, 255, 255, 0.95); 17 | } 18 | 19 | .Collapsible .fa-angle-up, 20 | .Collapsible .fa-angle-down { 21 | color: #100c08; 22 | } 23 | 24 | .Collapsible__contentInner { 25 | background-color: rgba(0, 0, 0, 0.6); 26 | color: #fff; 27 | padding: 1rem 1rem 0 1rem; 28 | } 29 | 30 | .Collapsible__contentInner .setting-options, 31 | .Collapsible__contentInner .check-box { 32 | display: flex; 33 | } 34 | 35 | .setting-options { 36 | padding-bottom: 1rem; 37 | } 38 | 39 | .setting-options .setting-name { 40 | width: 30%; 41 | } 42 | 43 | .setting-options .check-box { 44 | width: 35%; 45 | } 46 | 47 | .setting-options .light-theme { 48 | width: 25%; 49 | } 50 | 51 | .setting-options .dark-theme { 52 | width: 50%; 53 | } 54 | 55 | .Collapsible a { 56 | display: flex; 57 | color: #fff; 58 | align-items: center; 59 | width: 100%; 60 | justify-content: space-between; 61 | padding-bottom: 0.8rem; 62 | } 63 | 64 | .Collapsible .fa-external-link-alt { 65 | width: 16px; 66 | height: 16px; 67 | } 68 | 69 | .attribution, 70 | .feedback-report, 71 | .about-section { 72 | width: 100%; 73 | padding-bottom: 0.2rem; 74 | } 75 | 76 | @media (min-width: 1000px) { 77 | .setting-headers { 78 | margin-left: 22%; 79 | width: 78%; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "MangaHaven", 3 | "name": "MangaHaven", 4 | "icons": [ 5 | { 6 | "src": "logo-192-default.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "logo-512-default.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | }, 15 | { 16 | "purpose": "maskable", 17 | "src": "logo72.png", 18 | "type": "image/png", 19 | "sizes": "72x72" 20 | }, 21 | { 22 | "purpose": "maskable", 23 | "src": "logo96.png", 24 | "type": "image/png", 25 | "sizes": "96x96" 26 | }, 27 | { 28 | "purpose": "maskable", 29 | "src": "logo128.png", 30 | "type": "image/png", 31 | "sizes": "128x128" 32 | }, 33 | { 34 | "purpose": "maskable", 35 | "src": "logo144.png", 36 | "type": "image/png", 37 | "sizes": "144x144" 38 | }, 39 | { 40 | "purpose": "maskable", 41 | "src": "logo152.png", 42 | "type": "image/png", 43 | "sizes": "152x152" 44 | }, 45 | { 46 | "purpose": "maskable", 47 | "src": "logo192.png", 48 | "type": "image/png", 49 | "sizes": "192x192" 50 | }, 51 | { 52 | "purpose": "maskable", 53 | "src": "logo384.png", 54 | "type": "image/png", 55 | "sizes": "384x384" 56 | }, 57 | { 58 | "purpose": "maskable", 59 | "src": "logo512.png", 60 | "type": "image/png", 61 | "sizes": "512x512" 62 | } 63 | ], 64 | "start_url": "/", 65 | "display": "standalone", 66 | "scope": "/", 67 | "background_color": "#FDFFFC", 68 | "theme_color": "#FDFFFC" 69 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manga-haven", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 7 | "@fortawesome/free-brands-svg-icons": "^5.13.0", 8 | "@fortawesome/free-regular-svg-icons": "^5.13.0", 9 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 10 | "@fortawesome/react-fontawesome": "^0.1.9", 11 | "@testing-library/jest-dom": "^4.2.4", 12 | "@testing-library/react": "^9.5.0", 13 | "@testing-library/user-event": "^7.2.1", 14 | "html-entities": "^1.3.1", 15 | "localforage": "^1.7.3", 16 | "rc-progress": "^2.6.0", 17 | "react": "^16.13.1", 18 | "react-confirm-alert": "^2.6.1", 19 | "react-copy-to-clipboard": "^5.0.2", 20 | "react-dom": "^16.13.1", 21 | "react-helmet": "^6.0.0", 22 | "react-horizontal-scrolling-menu": "^0.7.7", 23 | "react-image": "^2.4.0", 24 | "react-infinite-scroller": "^1.2.4", 25 | "react-intersection-observer": "^8.26.2", 26 | "react-lazyload": "^2.6.7", 27 | "react-modal": "^3.11.2", 28 | "react-responsive-carousel": "^3.2.7", 29 | "react-router-dom": "^5.1.2", 30 | "react-scripts": "3.4.1", 31 | "react-share": "^4.1.0", 32 | "react-swipeable-views": "^0.13.9", 33 | "secure-ls": "^1.2.6" 34 | }, 35 | "scripts": { 36 | "start": "react-scripts --openssl-legacy-provider start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "eslintConfig": { 42 | "extends": "react-app" 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/containers/css/EditProfile.css: -------------------------------------------------------------------------------- 1 | .message { 2 | display: flex; 3 | padding: 0.5rem 1rem; 4 | background-color: rgb(75, 75, 75); 5 | box-shadow: rgba(0, 0, 0, 0.13) 0px 2px 4px 0px; 6 | justify-content: space-between; 7 | align-items: center; 8 | color: #fff; 9 | position: sticky; 10 | top: 3rem; 11 | z-index: 1; 12 | } 13 | 14 | .message .fa-times { 15 | width: 24px; 16 | height: 24px; 17 | } 18 | 19 | .message p { 20 | margin: 0; 21 | font-size: 1.05rem; 22 | } 23 | 24 | .App .profile-form { 25 | margin: 1rem auto 2rem auto; 26 | } 27 | 28 | .App .profile-form textarea { 29 | height: 5rem; 30 | resize: none; 31 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 32 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 33 | sans-serif; 34 | } 35 | 36 | .App .profile-form input, 37 | .App .profile-form textarea { 38 | margin-bottom: 1rem; 39 | } 40 | 41 | .profile-form .submit-button { 42 | margin-top: 1rem; 43 | font-size: 1.15rem; 44 | padding: 0.7rem 1rem; 45 | border: none; 46 | border-radius: 0.3rem; 47 | background-color: #4664c8; 48 | color: #fff; 49 | font-weight: 600; 50 | cursor: pointer; 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | .profile-form .submit-button:hover { 56 | background-color: rgb(50, 100, 200); 57 | } 58 | 59 | .profile-form .submit-button img { 60 | width: 28px; 61 | height: 28px; 62 | } 63 | 64 | .profile-form .submit-button img { 65 | margin-right: 0.5rem; 66 | } 67 | 68 | @media (min-width: 1000px) { 69 | .message { 70 | width: 78%; 71 | margin-left: 22%; 72 | } 73 | .App .profile-form { 74 | margin-left: 22%; 75 | width: 60%; 76 | padding: 0 2rem; 77 | } 78 | .profile-form .submit-button { 79 | font-size: 1.3rem; 80 | padding: 0.8rem 2rem; 81 | } 82 | .profile-form .submit-button img { 83 | width: 32px; 84 | height: 32px; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/containers/js/Favorites.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/js/Header.js'; 3 | import NavBar from './NavBar.js'; 4 | import MangaCardList from '../../components/js/MangaCardList.js'; 5 | import localForage from 'localforage'; 6 | import { Helmet } from 'react-helmet'; 7 | 8 | class Favorites extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | userFav: [], 13 | displayedManga: [], 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | //check if user favorites exists in storage 19 | localForage 20 | .getItem('userFavorites') 21 | .then((value) => { 22 | if (value !== null) value.reverse(); 23 | this.setState({ 24 | userFav: value, 25 | displayedManga: value, 26 | }); 27 | }) 28 | .catch((err) => console.log(err)); 29 | } 30 | 31 | filterManga = (keyword) => { 32 | let { userFav, displayedManga } = this.state; 33 | 34 | displayedManga = userFav.filter((manga) => { 35 | const regex = new RegExp(keyword, 'gi'); 36 | return manga.name.match(regex) || manga.alias.match(regex); 37 | }); 38 | this.setState({ displayedManga }); 39 | }; 40 | 41 | render() { 42 | const { userFav, displayedManga } = this.state; 43 | return ( 44 |
45 | 46 | Favorites - MangaHaven 47 | 48 | 49 |
54 | 55 | {userFav === null ? ( 56 |

You don't have any favorited manga yet

57 | ) : ( 58 | 59 | )} 60 |
61 | ); 62 | } 63 | } 64 | 65 | export default Favorites; 66 | -------------------------------------------------------------------------------- /src/containers/js/Library.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/js/Header.js'; 3 | import NavBar from './NavBar.js'; 4 | import MangaCardList from '../../components/js/MangaCardList.js'; 5 | import { Helmet } from 'react-helmet'; 6 | import localForage from 'localforage'; 7 | 8 | class Library extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | library: [], 13 | displayedManga: [], 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | //check if user bookmarks exists in storage 19 | localForage 20 | .getItem('userBookmarks') 21 | .then((value) => { 22 | if (value !== null) value.reverse(); 23 | this.setState({ 24 | library: value, 25 | displayedManga: value, 26 | }); 27 | }) 28 | .catch((err) => console.log(err)); 29 | } 30 | 31 | filterManga = (keyword) => { 32 | let { library, displayedManga } = this.state; 33 | 34 | displayedManga = library.filter((manga) => { 35 | const regex = new RegExp(keyword, 'gi'); 36 | return manga.name.match(regex) || manga.alias.match(regex); 37 | }); 38 | this.setState({ displayedManga }); 39 | }; 40 | 41 | render() { 42 | const { library, displayedManga } = this.state; 43 | return ( 44 |
45 | 46 | Library - MangaHaven 47 | 48 | 49 |
54 | 55 | {library === null ? ( 56 |

57 | You don't have any bookmarked manga yet 58 |

59 | ) : ( 60 | 61 | )} 62 |
63 | ); 64 | } 65 | } 66 | 67 | export default Library; 68 | -------------------------------------------------------------------------------- /src/containers/css/UserProfile.css: -------------------------------------------------------------------------------- 1 | .profile-details { 2 | margin: 1rem auto; 3 | width: 90%; 4 | } 5 | 6 | .profile-details p { 7 | margin: 0; 8 | } 9 | 10 | .profile-header { 11 | display: flex; 12 | } 13 | 14 | .profile-header .user-action { 15 | margin: 0.7rem 0 0 1rem; 16 | } 17 | 18 | .user-action .username { 19 | font-size: 1.4rem; 20 | font-weight: 600; 21 | margin-bottom: 0.5rem; 22 | } 23 | 24 | .profile-pic, 25 | .profile-pic img { 26 | width: 100px; 27 | height: 100px; 28 | border-radius: 0.5rem; 29 | } 30 | 31 | .profile-pic { 32 | background-color: rgba(0, 0, 0, 0.5); 33 | } 34 | 35 | .profile-pic .image-loader { 36 | margin: 24px auto; 37 | text-align: center; 38 | } 39 | 40 | .profile-pic .image-loader img { 41 | width: 48px; 42 | height: 48px; 43 | } 44 | 45 | .edit-button { 46 | padding: 0.5rem 1rem; 47 | border-radius: 1.5rem; 48 | border: 1px solid rgb(86, 128, 220); 49 | background-color: #fff; 50 | color: rgb(86, 128, 220); 51 | font-weight: 600; 52 | display: block; 53 | margin: 0 auto; 54 | font-size: 1rem; 55 | cursor: pointer; 56 | } 57 | 58 | .edit-button:hover, 59 | .edit-button:focus { 60 | background-color: rgb(86, 128, 220); 61 | color: #fff; 62 | } 63 | 64 | .profile-info { 65 | margin-top: 1rem; 66 | } 67 | 68 | .section-list { 69 | display: flex; 70 | flex-wrap: wrap; 71 | } 72 | 73 | .section { 74 | display: flex; 75 | color: rgb(135, 135, 135); 76 | } 77 | 78 | .section a, 79 | .section p { 80 | overflow: hidden; 81 | text-overflow: ellipsis; 82 | white-space: nowrap; 83 | } 84 | 85 | .section-list .section { 86 | width: 50%; 87 | margin-bottom: 0.5rem; 88 | } 89 | 90 | .email-section { 91 | margin: 0.5rem 0; 92 | } 93 | 94 | .section .icon { 95 | margin-right: 7px; 96 | } 97 | 98 | .section a { 99 | color: blue; 100 | } 101 | 102 | @media (min-width: 1000px) { 103 | .profile-details { 104 | margin-left: 22%; 105 | width: 78%; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/components/css/Header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 3rem; 3 | overflow: hidden; 4 | display: flex; 5 | justify-content: space-between; 6 | margin: 0; 7 | padding: 0; 8 | padding-left: 1rem; 9 | align-items: center; 10 | width: 100%; 11 | left: 0; 12 | color: #fff; 13 | background-color: rgb(86, 128, 220); 14 | box-shadow: rgba(0, 0, 0, 0.13) 0px 2px 4px 0px; 15 | position: sticky; 16 | z-index: 1; 17 | top: 0; 18 | } 19 | 20 | .header .active { 21 | display: flex; 22 | } 23 | 24 | .header .inactive { 25 | display: none; 26 | } 27 | 28 | .header .active-state { 29 | display: block; 30 | } 31 | 32 | .header #on-search-page { 33 | display: none; 34 | } 35 | 36 | .header .svg-inline--fa { 37 | cursor: pointer; 38 | } 39 | 40 | .header-title { 41 | align-items: center; 42 | } 43 | 44 | .header .current-menu { 45 | margin: 0; 46 | padding: 0; 47 | margin-left: 1rem; 48 | font-size: 1.3rem; 49 | font-weight: 600; 50 | } 51 | 52 | .clear-button { 53 | margin-right: 0.8rem; 54 | } 55 | 56 | .clear-button .fa-times { 57 | height: 24px; 58 | width: 24px; 59 | } 60 | 61 | .search-box { 62 | align-items: center; 63 | width: 100%; 64 | } 65 | 66 | .search-input { 67 | height: 3rem; 68 | margin-left: 1rem; 69 | font-size: 1.15rem; 70 | width: 80%; 71 | background-color: rgb(86, 128, 220); 72 | border: 0px; 73 | padding: 0 0.5rem; 74 | color: #fff; 75 | } 76 | 77 | .search-input::placeholder { 78 | color: rgb(220, 220, 220); 79 | } 80 | 81 | .search-input:focus { 82 | outline: 0; 83 | } 84 | 85 | @media (min-width: 1000px) { 86 | .header { 87 | margin-left: 22%; 88 | width: 78%; 89 | } 90 | .header .hamburger { 91 | display: none; 92 | } 93 | .header .current-menu { 94 | margin: 0; 95 | font-size: 1.4rem; 96 | } 97 | .header .fa-search { 98 | height: 22px; 99 | width: 22px; 100 | } 101 | .search-input { 102 | font-size: 1.25rem; 103 | letter-spacing: 0.03rem; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/containers/js/Categories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/js/Header.js'; 3 | import Loader from '../../components/js/Loader.js'; 4 | import CategoryList from '../../components/js/CategoryList.js'; 5 | import ErrorMessage from '../../components/js/ErrorMessage.js'; 6 | import NavBar from './NavBar.js'; 7 | import { Helmet } from 'react-helmet'; 8 | import { API_BASE_URL } from '../../utils/config.js'; 9 | import '../css/Categories.css'; 10 | 11 | class Categories extends React.Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | categoryManga: [], 16 | loader: true, 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | this.loadManga(); 22 | } 23 | 24 | loadManga = () => { 25 | if (this.state.categoryManga === null) { 26 | this.setState({ 27 | categoryManga: [], 28 | loader: true, 29 | }); 30 | } 31 | 32 | fetch(`${API_BASE_URL}/all-genres`) 33 | .then((res) => res.json()) 34 | .then((data) => { 35 | this.setState({ 36 | categoryManga: data, 37 | loader: false, 38 | }); 39 | }) 40 | .catch((err) => { 41 | this.setState({ 42 | categoryManga: null, 43 | loader: false, 44 | }); 45 | }); 46 | }; 47 | 48 | render() { 49 | const renderedContent = 50 | this.state.categoryManga === null ? ( 51 | 52 | ) : ( 53 | 54 | ); 55 | 56 | const loader = this.state.loader ? : null; 57 | 58 | return ( 59 |
60 | 61 | All Genres Manga - MangaHaven 62 | 63 | 64 |
65 | 66 | {loader} 67 | {renderedContent} 68 |
69 | ); 70 | } 71 | } 72 | 73 | export default Categories; 74 | -------------------------------------------------------------------------------- /src/components/css/CategoryBox.css: -------------------------------------------------------------------------------- 1 | .category { 2 | margin-bottom: 1.5rem; 3 | } 4 | 5 | .category-header { 6 | display: flex; 7 | justify-content: space-between; 8 | margin: 0 1.2rem 0.3rem 1.2rem; 9 | cursor: pointer; 10 | align-items: center; 11 | } 12 | 13 | .category-name { 14 | font-weight: 600; 15 | font-size: 1.15rem; 16 | margin: 0; 17 | } 18 | 19 | .category-header .fa-long-arrow-alt-right { 20 | width: 28px; 21 | height: 28px; 22 | color: rgb(50, 50, 50); 23 | } 24 | 25 | .scroll-menu-arrow--disabled { 26 | visibility: hidden; 27 | } 28 | 29 | .scroll-menu-arrow .svg-inline--fa { 30 | height: 20px; 31 | width: 20px; 32 | color: grey; 33 | cursor: pointer; 34 | } 35 | 36 | .scroll-menu-arrow .fa-chevron-left { 37 | margin-right: 0.1rem; 38 | } 39 | 40 | .scroll-menu-arrow .fa-chevron-right { 41 | margin-left: 0.1rem; 42 | } 43 | 44 | .menu-item { 45 | margin: 0 auto; 46 | margin-right: 0.7rem; 47 | } 48 | 49 | .menu-item .manga-card { 50 | width: 110px; 51 | height: 140px; 52 | } 53 | 54 | .menu-item .manga-card img { 55 | height: 140px; 56 | } 57 | 58 | .menu-item .manga-card .image-loader { 59 | margin: 50px auto; 60 | text-align: center; 61 | } 62 | 63 | .menu-item .manga-card .image-loader img { 64 | width: 36px; 65 | height: 36px; 66 | } 67 | 68 | .menu-item .manga-card .container { 69 | padding: 0.2rem 0.4rem; 70 | } 71 | 72 | .menu-item .manga-card .container .manga-title { 73 | margin: 0.1rem 0; 74 | font-size: 0.85rem; 75 | } 76 | 77 | @media (min-width: 1000px) { 78 | .category-name { 79 | font-size: 1.25rem; 80 | } 81 | .category-header { 82 | margin: 0 1.6rem 0.5rem 1.7rem; 83 | } 84 | .category-header .fa-long-arrow-alt-right { 85 | width: 32px; 86 | height: 32px; 87 | } 88 | .scroll-menu-arrow .svg-inline--fa { 89 | height: 28px; 90 | width: 28px; 91 | } 92 | .menu-item { 93 | margin-right: 1rem; 94 | } 95 | .menu-item .manga-card, 96 | .menu-item .manga-card img { 97 | width: 150px; 98 | height: 180px; 99 | } 100 | .menu-item .manga-card .image-loader { 101 | margin: 65px auto; 102 | } 103 | .menu-item .manga-card .image-loader img { 104 | width: 50px; 105 | height: 50px; 106 | } 107 | .menu-item .manga-card .container { 108 | padding: 0.3rem 0.5rem; 109 | } 110 | .menu-item .manga-card .container .manga-title { 111 | font-size: 1rem; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/components/js/CategoryBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MangaCard from './MangaCard.js'; 3 | import ScrollMenu from 'react-horizontal-scrolling-menu'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; 6 | import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; 7 | import { faLongArrowAltRight } from '@fortawesome/free-solid-svg-icons'; 8 | import { withRouter } from 'react-router-dom'; 9 | import '../css/CategoryBox.css'; 10 | 11 | const MenuItem = ({ text }) => { 12 | return
{text}
; 13 | }; 14 | 15 | const Menu = (category) => { 16 | return category.map((manga, i) => { 17 | const { imageUrl, name, serialName } = manga; 18 | return ( 19 | 27 | } 28 | key={i} 29 | /> 30 | ); 31 | }); 32 | }; 33 | 34 | class CategoryBox extends React.Component { 35 | constructor(props) { 36 | super(props); 37 | this.state = { 38 | count: 5, 39 | menu: Menu(this.props.category.genreManga.slice(0, 5)), 40 | }; 41 | } 42 | 43 | loadItems = () => { 44 | const { genreManga } = this.props.category; 45 | if (genreManga.length >= this.state.count) { 46 | this.setState({ 47 | count: this.state.count + 5, 48 | menu: Menu(genreManga.slice(0, this.state.count)), 49 | }); 50 | } 51 | }; 52 | 53 | render() { 54 | const { history } = this.props; 55 | const { genre } = this.props.category; 56 | return ( 57 |
58 |
history.push(`/genre/${genre}`)} 61 | > 62 |

{genre}

63 | 64 |
65 | } 68 | arrowRight={} 69 | hideArrows={true} 70 | hideSingleArrow={true} 71 | transition={0.5} 72 | alignCenter={false} 73 | onLastItemVisible={this.loadItems} 74 | /> 75 |
76 | ); 77 | } 78 | } 79 | 80 | export default withRouter(CategoryBox); 81 | -------------------------------------------------------------------------------- /src/containers/js/RecentUpdate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/js/Header.js'; 3 | import NavBar from './NavBar.js'; 4 | import MangaCardList from '../../components/js/MangaCardList.js'; 5 | import InfiniteScroll from 'react-infinite-scroller'; 6 | import Loader from '../../components/js/Loader.js'; 7 | import ErrorMessage from '../../components/js/ErrorMessage.js'; 8 | import { Helmet } from 'react-helmet'; 9 | import { API_BASE_URL } from '../../utils/config.js'; 10 | 11 | class RecentUpdate extends React.Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | allManga: [], 16 | displayedManga: [], 17 | count: 10, 18 | hasMoreItems: true, 19 | }; 20 | } 21 | 22 | componentDidMount() { 23 | this.fetchManga(); 24 | } 25 | 26 | displayManga = () => { 27 | if (this.state.allManga.length >= this.state.count) { 28 | this.setState({ 29 | count: this.state.count + 10, //increase back to 100 30 | displayedManga: this.state.allManga.slice(0, this.state.count), 31 | }); 32 | } 33 | }; 34 | 35 | fetchManga = () => { 36 | if (this.state.allManga === null) { 37 | this.setState({ 38 | allManga: [], 39 | hasMoreItems: true, 40 | }); 41 | } 42 | 43 | fetch(`${API_BASE_URL}/recent`) 44 | .then((res) => res.json()) 45 | .then((data) => { 46 | console.log(data); 47 | this.setState({ allManga: data }); 48 | this.displayManga(); 49 | }) 50 | .catch((err) => { 51 | this.setState({ 52 | allManga: null, 53 | hasMoreItems: false, 54 | }); 55 | }); 56 | }; 57 | 58 | render() { 59 | const renderedContent = 60 | this.state.allManga === null ? ( 61 | 62 | ) : ( 63 | } 68 | > 69 | 70 | 71 | ); 72 | return ( 73 |
74 | 75 | Recently Updated Manga - MangaHaven 76 | 77 | 78 |
79 | 80 | {renderedContent} 81 |
82 | ); 83 | } 84 | } 85 | 86 | export default RecentUpdate; 87 | -------------------------------------------------------------------------------- /src/containers/css/HistoryPage.css: -------------------------------------------------------------------------------- 1 | .manga-history p { 2 | margin: 0; 3 | } 4 | 5 | .manga-history .no-manga-history { 6 | text-align: center; 7 | margin-top: 1rem; 8 | } 9 | 10 | .history-card-list { 11 | margin: 1rem auto; 12 | width: 90%; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .history-card { 18 | display: flex; 19 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); 20 | width: 100%; 21 | height: 140px; 22 | margin-bottom: 0.7rem; 23 | } 24 | 25 | .history-card .card-image { 26 | width: 30%; 27 | height: 100%; 28 | cursor: pointer; 29 | } 30 | 31 | .card-image img { 32 | width: 100%; 33 | height: 100%; 34 | } 35 | 36 | .card-image .image-loader { 37 | border-radius: 0; 38 | width: 100%; 39 | margin: 35px auto; 40 | text-align: center; 41 | } 42 | 43 | .card-image .image-loader img { 44 | width: 70px; 45 | height: 70px; 46 | } 47 | 48 | .history-card .card-details { 49 | margin: 0.8rem 0 0.3rem 1rem; 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: space-between; 53 | width: 60%; 54 | } 55 | 56 | .card-details p { 57 | font-size: 0.85rem; 58 | } 59 | 60 | .card-details .title { 61 | display: -webkit-box; 62 | -webkit-box-orient: vertical; 63 | -webkit-line-clamp: 2; 64 | overflow: hidden; 65 | font-size: 1.25rem; 66 | margin-bottom: 0.2rem; 67 | } 68 | 69 | .card-details .chapter-info { 70 | margin-bottom: 0.1rem; 71 | } 72 | 73 | .card-details .date-added { 74 | color: rgb(150, 150, 150); 75 | } 76 | 77 | .card-details .date-added span { 78 | margin-right: 0.5rem; 79 | } 80 | 81 | .card-details .action-buttons .resume-button { 82 | position: relative; 83 | background-color: #fff; 84 | border: none; 85 | color: rgb(0, 80, 225); 86 | padding: 0.3rem 0.3rem 0.3rem 0; 87 | font-size: 0.9rem; 88 | cursor: pointer; 89 | } 90 | 91 | .card-details .action-buttons .resume-button:focus, 92 | .card-details .action-buttons .resume-button:hover { 93 | background-color: rgba(0, 0, 0, 0.1); 94 | } 95 | 96 | @media (min-width: 675px) { 97 | .history-card-list { 98 | display: grid; 99 | grid-template-columns: repeat(2, 1fr); 100 | grid-gap: 1rem; 101 | width: 95%; 102 | } 103 | } 104 | 105 | @media (min-width: 1000px) { 106 | .history-card-list { 107 | width: 78%; 108 | margin-left: 22%; 109 | padding: 0 1rem; 110 | } 111 | .manga-history .no-manga-history { 112 | margin-left: 22%; 113 | width: 78%; 114 | } 115 | } 116 | 117 | @media (min-width: 1200px) { 118 | .history-card-list { 119 | grid-template-columns: repeat(3, 1fr); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/containers/js/CategoryPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/js/Header.js'; 3 | import NavBar from './NavBar.js'; 4 | import MangaCardList from '../../components/js/MangaCardList.js'; 5 | import InfiniteScroll from 'react-infinite-scroller'; 6 | import ErrorMessage from '../../components/js/ErrorMessage.js'; 7 | import Loader from '../../components/js/Loader.js'; 8 | import { Helmet } from 'react-helmet'; 9 | import { withRouter } from 'react-router-dom'; 10 | import { API_BASE_URL } from '../../utils/config.js'; 11 | 12 | class CategoryPage extends React.Component { 13 | constructor() { 14 | super(); 15 | this.state = { 16 | manga: [], 17 | displayedManga: [], 18 | count: 10, 19 | hasMoreItems: true, 20 | }; 21 | } 22 | 23 | componentDidMount() { 24 | this.fetchManga(); 25 | } 26 | 27 | displayManga = () => { 28 | if (this.state.manga.length >= this.state.count) { 29 | this.setState({ 30 | count: this.state.count + 10, //increase back to 100 during production 31 | displayedManga: this.state.manga.slice(0, this.state.count), 32 | }); 33 | } 34 | }; 35 | 36 | fetchManga = () => { 37 | if (this.state.manga === null) { 38 | this.setState({ 39 | manga: [], 40 | hasMoreItems: true, 41 | }); 42 | } 43 | 44 | const genre = this.props.match.params.name; 45 | fetch(`${API_BASE_URL}/genre/${genre}`) 46 | .then((res) => res.json()) 47 | .then((data) => { 48 | this.setState({ manga: data }); 49 | this.displayManga(); 50 | }) 51 | .catch((err) => { 52 | this.setState({ 53 | manga: null, 54 | hasMoreItems: false, 55 | }); 56 | }); 57 | }; 58 | 59 | render() { 60 | const genre = this.props.match.params.name; 61 | const renderedContent = 62 | this.state.manga === null ? ( 63 | 64 | ) : ( 65 | } 70 | > 71 | 72 | 73 | ); 74 | 75 | return ( 76 |
77 | 78 | {`${genre} Manga - MangaHaven`} 79 | 80 | 81 |
82 | 83 | {renderedContent} 84 |
85 | ); 86 | } 87 | } 88 | 89 | export default withRouter(CategoryPage); 90 | -------------------------------------------------------------------------------- /src/containers/js/ExplorePage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/js/Header.js'; 3 | import NavBar from './NavBar.js'; 4 | import MangaCardList from '../../components/js/MangaCardList.js'; 5 | import InfiniteScroll from 'react-infinite-scroller'; 6 | import Loader from '../../components/js/Loader.js'; 7 | import { Helmet } from 'react-helmet'; 8 | import { API_BASE_URL } from '../../utils/config.js'; 9 | import ErrorMessage from '../../components/js/ErrorMessage.js'; 10 | 11 | class ExplorePage extends React.Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | allManga: [], 16 | displayedManga: [], 17 | count: 10, 18 | hasMoreItems: true, 19 | }; 20 | } 21 | 22 | componentDidMount() { 23 | this.setState({ hasMoreItems: true }); 24 | this.loadManga(); 25 | console.log(this.state.hasMoreItems); 26 | } 27 | 28 | displayManga = () => { 29 | if (this.state.allManga.length >= this.state.count) { 30 | this.setState({ 31 | count: this.state.count + 10, //increase back to 100 32 | displayedManga: this.state.allManga.slice(0, this.state.count), 33 | }); 34 | } else { 35 | this.setState({ hasMoreItems: false }); 36 | } 37 | }; 38 | 39 | loadManga = () => { 40 | if (this.state.allManga === null) { 41 | this.setState({ 42 | allManga: [], 43 | hasMoreItems: true, 44 | }); 45 | } 46 | 47 | fetch(`${API_BASE_URL}/hot`) 48 | .then((res) => res.json()) 49 | .then((data) => { 50 | this.setState({ 51 | allManga: data, 52 | hasMoreItems: true, 53 | }); 54 | console.log(this.state.hasMoreItems); 55 | 56 | this.displayManga(); 57 | }) 58 | .catch((err) => { 59 | this.setState({ hasMoreItems: false }); 60 | setTimeout(() => this.setState({ hasMoreItems: true }), 1000); 61 | }); 62 | }; 63 | 64 | render() { 65 | const { allManga, displayedManga, hasMoreItems } = this.state; 66 | 67 | const renderedContent = 68 | allManga === null ? ( 69 | 70 | ) : ( 71 | } 76 | > 77 | 78 | 79 | 80 | ); 81 | return ( 82 |
83 | 84 | Explore - MangaHaven 85 | 86 | 87 |
88 | 89 | {renderedContent} 90 |
91 | ); 92 | } 93 | } 94 | 95 | export default ExplorePage; 96 | -------------------------------------------------------------------------------- /src/components/js/AuthDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loader from '../../assets/image-loader.png'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { faEye } from '@fortawesome/free-solid-svg-icons'; 5 | import { faEyeSlash } from '@fortawesome/free-solid-svg-icons'; 6 | 7 | class AuthDetails extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | pwHidden: true, 12 | pwTwoHidden: true, 13 | }; 14 | } 15 | 16 | hidePw = () => this.setState({ pwHidden: true }); 17 | 18 | showPw = () => this.setState({ pwHidden: false }); 19 | 20 | hidePwTwo = () => this.setState({ pwTwoHidden: true }); 21 | 22 | showPwTwo = () => this.setState({ pwTwoHidden: false }); 23 | 24 | render() { 25 | const { 26 | value, 27 | loader, 28 | updateEmail, 29 | updatePassword, 30 | updatePasswordTwo, 31 | hideControl, 32 | hideControlTwo, 33 | } = this.props; 34 | const { pwHidden, pwTwoHidden } = this.state; 35 | return ( 36 |
37 | 47 | 64 | {value === 'Login' ? null : ( 65 | 82 | )} 83 | 89 |
90 | ); 91 | } 92 | } 93 | 94 | export default AuthDetails; 95 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 43 | MangaHaven 44 | 45 | 46 | 47 |
48 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/containers/js/HistoryPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import localForage from 'localforage'; 3 | import Header from '../../components/js/Header.js'; 4 | import NavBar from './NavBar.js'; 5 | import Image from '../../components/js/Image.js'; 6 | import { Helmet } from 'react-helmet'; 7 | import { withRouter } from 'react-router-dom'; 8 | import '../css/HistoryPage.css'; 9 | 10 | class HistoryPage extends React.Component { 11 | constructor() { 12 | super(); 13 | this.state = { 14 | mangaHistory: [], 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | localForage 20 | .getItem('readHistory') 21 | .then((manga) => { 22 | if (manga !== null) manga.sort((a, b) => b.added - a.added); 23 | console.log(manga); 24 | this.setState({ mangaHistory: manga }); 25 | }) 26 | .catch((err) => console.log(err)); 27 | } 28 | 29 | render() { 30 | const { mangaHistory } = this.state; 31 | return ( 32 |
33 | 34 | History - MangaHaven 35 | 36 | 37 |
38 | 39 | {mangaHistory === null ? ( 40 |

You have no recently read manga

41 | ) : ( 42 |
43 | {mangaHistory.map((manga, i) => { 44 | return ( 45 |
46 |
49 | this.props.history.push( 50 | `/manga/${manga.alias}/${manga.mangaId}` 51 | ) 52 | } 53 | > 54 | 55 |
56 |
57 |
58 |

{manga.title}

59 |

{`Chapter ${manga.chapterNum} - Page ${manga.page}`}

60 |

61 | 62 | {new Date(manga.added).toLocaleDateString()} 63 | 64 | 65 | {new Date(manga.added).toLocaleTimeString()} 66 | 67 |

68 |
69 |
70 | 80 |
81 |
82 |
83 | ); 84 | })} 85 |
86 | )} 87 |
88 | ); 89 | } 90 | } 91 | 92 | export default withRouter(HistoryPage); 93 | -------------------------------------------------------------------------------- /src/containers/js/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SignIn from './SignIn.js'; 3 | import SignUp from './SignUp.js'; 4 | import UserProfile from './UserProfile.js'; 5 | import ExplorePage from './ExplorePage.js'; 6 | import RecentUpdate from './RecentUpdate.js'; 7 | import Categories from './Categories.js'; 8 | import MangaPage from './MangaPage.js'; 9 | import CategoryPage from './CategoryPage.js'; 10 | import SearchResults from './SearchResults.js'; 11 | import Favorites from './Favorites.js'; 12 | import Library from './Library.js'; 13 | import LibraryUpdates from './LibraryUpdates.js'; 14 | import Settings from './Settings.js'; 15 | import ChapterPage from './ChapterPage.js'; 16 | import HistoryPage from './HistoryPage.js'; 17 | import EditProfile from './EditProfile.js'; 18 | import { Route, BrowserRouter as Router, Redirect } from 'react-router-dom'; 19 | import '../css/App.css'; 20 | import { confirmAlert } from 'react-confirm-alert'; 21 | import Logo from '../../assets/logo.png'; 22 | 23 | class App extends React.Component { 24 | // componentDidMount() { 25 | // let noticeShown = sessionStorage['notice']; 26 | 27 | // if (!noticeShown) { 28 | // this.showNotice(); 29 | // sessionStorage['notice'] = true; 30 | // } 31 | // } 32 | 33 | showNotice = () => { 34 | confirmAlert({ 35 | customUI: ({ onClose }) => { 36 | return ( 37 |
38 |
e.stopPropagation()} 41 | > 42 | MangaHaven logo 43 |

Important Notice

44 |

45 | Due to the API we were previously using being taken down, some 46 | of the site's features are currently not functioning. Please 47 | bear with us as we are working on getting a replacement up as 48 | soon as we can. 49 |

50 |
54 | 57 |
58 |
59 |
60 | ); 61 | }, 62 | }); 63 | }; 64 | 65 | render() { 66 | return ( 67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 91 | 92 |
93 | ); 94 | } 95 | } 96 | 97 | export default App; 98 | -------------------------------------------------------------------------------- /src/containers/css/App.css: -------------------------------------------------------------------------------- 1 | /* styles for forms */ 2 | .auth-form { 3 | margin: 0 auto; 4 | margin-top: 6rem; 5 | width: 85%; 6 | } 7 | 8 | .auth-form .app-name { 9 | display: flex; 10 | align-items: center; 11 | position: absolute; 12 | top: 0; 13 | margin-top: 1rem; 14 | color: rgba(70, 70, 70); 15 | } 16 | 17 | .auth-form .app-name img { 18 | width: 25px; 19 | height: 25px; 20 | } 21 | 22 | .auth-form .app-name p { 23 | font-size: 1.2rem; 24 | margin-left: 0.1rem; 25 | font-weight: 400; 26 | } 27 | 28 | .auth-form p { 29 | margin: 0.5rem 0; 30 | font-weight: 600; 31 | font-size: 1.1rem; 32 | } 33 | 34 | .auth-form .error-message { 35 | margin: 0; 36 | font-size: 0.95rem; 37 | font-weight: normal; 38 | color: red; 39 | padding: 0; 40 | } 41 | 42 | .auth-form label { 43 | width: 100%; 44 | } 45 | 46 | .auth-form label .svg-inline--fa { 47 | position: absolute; 48 | margin-top: -1.5rem; 49 | margin-left: 78%; 50 | cursor: pointer; 51 | } 52 | 53 | .auth-form input, 54 | .auth-form textarea, 55 | .auth-form select { 56 | border: 1px solid grey; 57 | display: block; 58 | height: 2.3rem; 59 | width: 100%; 60 | margin: 0; 61 | font-size: 1.05rem; 62 | padding: 0.5rem; 63 | border-radius: 0.25rem; 64 | outline: 0; 65 | } 66 | 67 | .auth-form input:focus, 68 | .auth-form textarea:focus, 69 | .auth-form select:focus { 70 | border: 2px solid grey; 71 | } 72 | 73 | .auth-button { 74 | width: 100%; 75 | display: flex; 76 | text-align: center; 77 | justify-content: center; 78 | align-items: center; 79 | border: 1px solid rgb(0, 140, 0); 80 | height: 2.5rem; 81 | margin: 1rem 0; 82 | border-radius: 0.3rem; 83 | background-color: rgb(0, 140, 0); 84 | font-size: 1.2rem; 85 | color: #fff; 86 | font-weight: 600; 87 | cursor: pointer; 88 | transition: 0.2s; 89 | } 90 | 91 | .auth-button:hover { 92 | background-color: rgb(0, 120, 0); 93 | border: 1px solid rgb(0, 120, 0); 94 | } 95 | 96 | .auth-button img { 97 | width: 28px; 98 | height: 28px; 99 | position: absolute; 100 | } 101 | 102 | .signin-form .auth-button img { 103 | margin-left: -3rem; 104 | } 105 | 106 | .signup-form .auth-button img { 107 | margin-left: -3.7rem; 108 | } 109 | 110 | .auth-form .auth-redirect { 111 | font-weight: normal; 112 | text-align: center; 113 | } 114 | 115 | .auth-form .auth-redirect a { 116 | color: blue; 117 | margin-left: 0.5rem; 118 | } 119 | 120 | @media (min-width: 675px) { 121 | .auth-form { 122 | width: 60%; 123 | } 124 | .auth-form .app-name img { 125 | height: 30px; 126 | width: 30px; 127 | } 128 | .auth-form .app-name p { 129 | font-size: 1.25rem; 130 | } 131 | .auth-form h2 { 132 | font-size: 1.5rem; 133 | } 134 | .auth-form p { 135 | font-size: 1.15rem; 136 | } 137 | .auth-form .error-message { 138 | font-size: 1rem; 139 | } 140 | .auth-form label .svg-inline--fa { 141 | margin-left: 55%; 142 | margin-top: -2.2rem; 143 | height: 24px; 144 | width: 24px; 145 | } 146 | .auth-form input, 147 | .auth-form textarea, 148 | .auth-form select { 149 | height: 2.8rem; 150 | font-size: 1.1rem; 151 | padding: 0.7rem; 152 | } 153 | .auth-form input:focus, 154 | .auth-form textarea:focus, 155 | .auth-form select:focus { 156 | border: 3px solid grey; 157 | } 158 | .auth-button { 159 | height: 2.8rem; 160 | font-size: 1.2rem; 161 | } 162 | .auth-button img { 163 | width: 32px; 164 | height: 32px; 165 | } 166 | .signin-form .auth-button img { 167 | margin-left: -3.5rem; 168 | } 169 | .signup-form .auth-button img { 170 | margin-left: -4.3rem; 171 | } 172 | } 173 | 174 | @media (min-width: 1200px) { 175 | .auth-form { 176 | width: 45%; 177 | } 178 | .auth-form .app-name img { 179 | height: 35px; 180 | width: 35px; 181 | } 182 | .auth-form .app-name p { 183 | font-size: 1.35rem; 184 | } 185 | .auth-form h2 { 186 | font-size: 1.8rem; 187 | } 188 | .auth-form p { 189 | font-size: 1.25rem; 190 | } 191 | .auth-form .error-message { 192 | font-size: 1.1rem; 193 | } 194 | .auth-form label .svg-inline--fa { 195 | margin-left: 42%; 196 | } 197 | .auth-form input, 198 | .auth-form textarea { 199 | font-size: 1.3rem; 200 | } 201 | .auth-button { 202 | font-size: 1.4rem; 203 | } 204 | } 205 | 206 | /* styles for no-bookmarks */ 207 | .no-bookmarks { 208 | text-align: center; 209 | } 210 | 211 | @media (min-width: 1000px) { 212 | .no-bookmarks { 213 | margin-left: 22%; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/containers/js/SignIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthDetails from '../../components/js/AuthDetails.js'; 3 | import AppName from '../../components/js/AppName.js'; 4 | import SecureLS from 'secure-ls'; 5 | import localForage from 'localforage'; 6 | import { Helmet } from 'react-helmet'; 7 | import { withRouter } from 'react-router-dom'; 8 | 9 | class SignIn extends React.Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | loading: false, 14 | email: '', 15 | password: '', 16 | errorMsg: '', 17 | controlHidden: true, 18 | }; 19 | } 20 | 21 | signUserIn = (e) => { 22 | const { email, password } = this.state; 23 | e.preventDefault(); 24 | this.setState({ loading: true }); 25 | 26 | //make post request to API to see if user exists 27 | fetch( 28 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/users/login', 29 | { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | body: JSON.stringify({ 35 | email, 36 | password, 37 | }), 38 | } 39 | ) 40 | .then((res) => res.json()) 41 | .then((data) => { 42 | console.log(data); 43 | if (data.success) { 44 | //store JWT securely in localstorage 45 | const ls = new SecureLS(); 46 | ls.set('userToken', data.token); 47 | 48 | //fetch user profile 49 | fetch( 50 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/profile', 51 | { 52 | method: 'GET', 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | Authorization: data.token, 56 | }, 57 | } 58 | ) 59 | .then((res) => res.json()) 60 | .then((profile) => { 61 | console.log(data); 62 | //set user details for offline access in storage 63 | localForage 64 | .setItem('user', { 65 | signedIn: true, 66 | avatar: `https:${profile.user.avatar}`, 67 | dateJoined: profile.date, 68 | email: profile.user.email, 69 | name: profile.user.name, 70 | id: profile.user._id, 71 | }) 72 | .then((value) => { 73 | //redirect to user profile 74 | this.props.history.push('/profile'); 75 | }) 76 | .catch((err) => console.log(err)); 77 | }) 78 | .catch((err) => console.log(err)); 79 | } else { 80 | let errorMsg = ''; 81 | if (data.password) 82 | errorMsg = 'The password you have entered is incorrect'; 83 | else errorMsg = data.email; 84 | this.setState({ 85 | loading: false, 86 | errorMsg, 87 | }); 88 | } 89 | }) 90 | .catch((err) => { 91 | //display error in case of network down 92 | this.setState({ 93 | loading: false, 94 | errorMsg: 95 | 'An error occurred. Please check your internet connection and try again', 96 | }); 97 | }); 98 | }; 99 | 100 | updateEmail = (e) => this.setState({ email: e.target.value, errorMsg: '' }); 101 | 102 | updatePassword = (e) => { 103 | this.setState({ password: e.target.value, errorMsg: '' }); 104 | if (e.target.value === '') this.setState({ controlHidden: true }); 105 | else this.setState({ controlHidden: false }); 106 | }; 107 | 108 | render() { 109 | return ( 110 |
111 | 112 | Login - MangaHaven 113 | 114 | 115 | 116 |

Log in to your account

117 |

{this.state.errorMsg}

118 |
119 | 126 | 127 |

128 | Don't have an account? 129 | Sign Up 130 |

131 |
132 | ); 133 | } 134 | } 135 | 136 | export default withRouter(SignIn); 137 | -------------------------------------------------------------------------------- /src/components/js/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hamburger from './Hamburger.js'; 3 | import SearchButton from './SearchButton.js'; 4 | import BackButton from './BackButton.js'; 5 | import { withRouter } from 'react-router-dom'; 6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 7 | import { faTimes } from '@fortawesome/free-solid-svg-icons'; 8 | import '../css/Header.css'; 9 | 10 | class Header extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | searchActive: false, 15 | searchInput: '', 16 | onSearchPage: false, 17 | searchManga: null, 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | this.setState({ 23 | onSearchPage: this.props.onSearchPage, 24 | searchActive: this.props.onSearchPage, 25 | searchManga: this.props.manga, 26 | }); 27 | if (this.props.onSearchPage) 28 | this.setState({ searchInput: this.props.input }); 29 | } 30 | 31 | onInputChange = (e) => { 32 | let input = e.target.value; 33 | if (this.props.onSearchPage && input.length >= 3) 34 | this.displaySearchResults(input); 35 | 36 | this.setState({ searchInput: input }); 37 | }; 38 | 39 | toggleSearch = () => { 40 | this.setState({ searchActive: !this.state.searchActive }); 41 | }; 42 | 43 | onSearchClick = () => { 44 | if (!this.state.searchActive) this.setState({ searchActive: true }); 45 | else { 46 | if (this.state.searchInput !== '') 47 | this.displaySearchResults(this.state.searchInput); 48 | } 49 | }; 50 | 51 | clearText = () => { 52 | const { localSearch, searchManga } = this.props; 53 | this.setState({ searchInput: '' }); 54 | if (localSearch) searchManga(''); 55 | }; 56 | 57 | keyEvents = (e) => { 58 | let searchInput = e.target.value; 59 | if (searchInput !== '') { 60 | //for running search function 61 | if (e.key === 'Enter') this.displaySearchResults(this.state.searchInput); 62 | } 63 | }; 64 | 65 | displaySearchResults = (input) => { 66 | if (this.props.onSearchPage) { 67 | this.props.history.push(`/search?q=${input.replace(/ /g, '+')}`); 68 | this.props.displayManga(); 69 | } else { 70 | sessionStorage['prevPath'] = window.location.pathname; 71 | this.props.history.push(`/search?q=${input.replace(/ /g, '+')}`); 72 | } 73 | }; 74 | 75 | goToPrevPath = () => { 76 | this.props.history.push(sessionStorage['prevPath']); 77 | }; 78 | 79 | render() { 80 | const { onSearchPage, searchActive, searchInput } = this.state; 81 | const { currentMenu, localSearch, searchManga } = this.props; 82 | return ( 83 |
84 |
90 | 91 |

{currentMenu}

92 |
93 |
96 | 99 | {!localSearch ? ( 100 | 108 | ) : ( 109 | { 114 | let searchInput = e.target.value; 115 | this.setState({ searchInput }); 116 | searchManga(searchInput); 117 | }} 118 | value={searchInput} 119 | /> 120 | )} 121 |
122 |
130 | 131 |
132 | {!this.props.onHistoryPage ? ( 133 | searchActive && localSearch ? null : ( 134 | 135 | ) 136 | ) : null} 137 |
138 | ); 139 | } 140 | } 141 | 142 | export default withRouter(Header); 143 | -------------------------------------------------------------------------------- /src/containers/js/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/js/Header.js'; 3 | import NavBar from './NavBar.js'; 4 | import MangaCardList from '../../components/js/MangaCardList.js'; 5 | import InfiniteScroll from 'react-infinite-scroller'; 6 | import Loader from '../../components/js/Loader.js'; 7 | import ErrorMessage from '../../components/js/ErrorMessage.js'; 8 | import { Helmet } from 'react-helmet'; 9 | import { API_BASE_URL } from '../../utils/config.js'; 10 | import '../css/SearchResults.css'; 11 | 12 | class SearchResults extends React.Component { 13 | constructor() { 14 | super(); 15 | this.state = { 16 | allManga: [], 17 | searchResults: [], 18 | displayedManga: [], 19 | hasMoreItems: true, 20 | count: 10, 21 | loader: true, 22 | noResult: false, 23 | }; 24 | } 25 | 26 | componentDidMount() { 27 | this.fetchAllManga(); 28 | } 29 | 30 | fetchAllManga = () => { 31 | fetch(`${API_BASE_URL}`) 32 | .then((res) => res.json()) 33 | .then((data) => { 34 | this.setState({ allManga: data }); 35 | this.displayManga(this.setInput()); 36 | console.log(data.manga); 37 | }) 38 | .catch((err) => { 39 | this.setState({ 40 | allManga: null, 41 | loader: false, 42 | }); 43 | }); 44 | }; 45 | 46 | reFetchManga = () => { 47 | this.setState({ 48 | allManga: [], 49 | loader: true, 50 | }); 51 | this.fetchAllManga(); 52 | }; 53 | 54 | displayManga = (input) => { 55 | let searchResults = this.searchManga(input, this.state.allManga); 56 | if (searchResults.length === 0) { 57 | this.setState({ 58 | noResult: true, 59 | loader: false, 60 | }); 61 | return; 62 | } 63 | 64 | if (searchResults.length <= 10) { 65 | this.setState({ 66 | searchResults, 67 | displayedManga: searchResults, 68 | hasMoreItems: false, 69 | loader: false, 70 | noResult: false, 71 | }); 72 | } else { 73 | this.setState({ 74 | searchResults, 75 | hasMoreItems: true, 76 | loader: false, 77 | noResult: false, 78 | }); 79 | } 80 | }; 81 | 82 | displayMore = () => { 83 | if (this.state.searchResults.length >= this.state.count) { 84 | this.setState({ 85 | count: this.state.count + 10, 86 | displayedManga: this.state.searchResults.slice(0, this.state.count), 87 | }); 88 | } else this.setState({ hasMoreItems: false }); 89 | }; 90 | 91 | searchManga = (wordToMatch, allManga) => { 92 | //filter out city objects matching search term 93 | return allManga.filter((manga) => { 94 | const regex = new RegExp(wordToMatch, 'gi'); 95 | for (let i = 0; i < manga.al.length; i++) { 96 | if (manga.al[i].match(regex)) return true; 97 | } 98 | return manga.s.match(regex) || manga.i.match(regex); 99 | }); 100 | }; 101 | 102 | newSearch = () => { 103 | if (this.state.allManga !== null) this.displayManga(this.setInput()); 104 | }; 105 | 106 | setInput = () => { 107 | const url = new URL(window.location.href); 108 | let searchParams = new URLSearchParams(url.search); 109 | return searchParams.get('q').trim(); 110 | }; 111 | 112 | render() { 113 | const contentBody = this.state.noResult ? ( 114 |

115 | Sorry, no results were found for '{this.setInput()}' 116 |

117 | ) : ( 118 | 123 | 124 | 125 | ); 126 | 127 | const renderedContent = 128 | this.state.allManga === null ? ( 129 | 130 | ) : ( 131 | contentBody 132 | ); 133 | 134 | const loader = this.state.loader ? : null; 135 | 136 | return ( 137 |
138 | 139 | {`Search results for '${this.setInput()}' - MangaHaven`} 140 | 141 | 142 |
148 | 149 | {loader} 150 | {renderedContent} 151 |
152 | ); 153 | } 154 | } 155 | 156 | export default SearchResults; 157 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then((registration) => { 135 | registration.unregister(); 136 | }) 137 | .catch((error) => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/containers/js/SignUp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthDetails from '../../components/js/AuthDetails.js'; 3 | import AppName from '../../components/js/AppName.js'; 4 | import localForage from 'localforage'; 5 | import SecureLS from 'secure-ls'; 6 | import { Helmet } from 'react-helmet'; 7 | import { withRouter } from 'react-router-dom'; 8 | 9 | class SignUp extends React.Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | loading: false, 14 | email: '', 15 | password: '', 16 | name: '', 17 | passwordTwo: '', 18 | errorMsg: '', 19 | controlHidden: true, 20 | controlTwoHidden: true, 21 | }; 22 | } 23 | 24 | registerUser = (e) => { 25 | e.preventDefault(); 26 | const { email, password, name, passwordTwo } = this.state; 27 | 28 | //check if password matches password two 29 | if (password !== passwordTwo) { 30 | this.setState({ errorMsg: 'The passwords you entered do not match' }); 31 | return; 32 | } 33 | 34 | this.setState({ loading: true }); 35 | 36 | //post user details to server 37 | fetch( 38 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/users/register', 39 | { 40 | method: 'POST', 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | }, 44 | body: JSON.stringify({ 45 | email, 46 | password, 47 | name, 48 | passwordTwo, 49 | }), 50 | } 51 | ) 52 | .then((res) => res.json()) 53 | .then((data) => { 54 | console.log(data); 55 | if (data._id !== undefined) { 56 | //sign user in to get token 57 | this.logUserIn(email, password); 58 | 59 | //replace default avatar with retro 60 | data.avatar = data.avatar.replace('d=mm', 'd=retro'); 61 | 62 | //update user info stored in web storage 63 | localForage 64 | .getItem('user') 65 | .then((value) => { 66 | if (value !== null) { 67 | value.signedIn = true; 68 | value.avatar = `https:${data.avatar}`; 69 | value.dateJoined = data.date; 70 | value.email = data.email; 71 | value.name = data.name; 72 | value.id = data._id; 73 | } else { 74 | value = { 75 | signedIn: true, 76 | avatar: `https:${data.avatar}`, 77 | dateJoined: data.date, 78 | email: data.email, 79 | name: data.name, 80 | id: data._id, 81 | }; 82 | } 83 | localForage 84 | .setItem('user', value) 85 | .then((value) => { 86 | console.log(value); 87 | //redirect to user profile 88 | this.props.history.push('/profile'); 89 | }) 90 | .catch((err) => console.log(err)); 91 | }) 92 | .catch((err) => console.log(err)); 93 | } else { 94 | //display error response from server 95 | this.setState({ loading: false }); 96 | if (data.name) this.setState({ errorMsg: 'Username already exists' }); 97 | else if (data.email) 98 | this.setState({ errorMsg: 'Email address already exists' }); 99 | else if (data.passwordTwo) 100 | this.setState({ 101 | errorMsg: 'The passwords you entered do not match', 102 | }); 103 | } 104 | }) 105 | .catch((err) => { 106 | //display error in case of network down 107 | this.setState({ 108 | loading: false, 109 | errorMsg: 110 | 'An error occurred. Please check your internet connection and try again', 111 | }); 112 | }); 113 | }; 114 | 115 | logUserIn = (email, password) => { 116 | fetch( 117 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/users/login', 118 | { 119 | method: 'POST', 120 | headers: { 121 | 'Content-Type': 'application/json', 122 | }, 123 | body: JSON.stringify({ 124 | email, 125 | password, 126 | }), 127 | } 128 | ) 129 | .then((res) => res.json()) 130 | .then((data) => { 131 | console.log(data); 132 | if (data.success) { 133 | //store JWT securely in localstorage 134 | const ls = new SecureLS(); 135 | ls.set('userToken', data.token); 136 | 137 | //create user profile with JWT 138 | this.createProfile(data.token); 139 | } 140 | }) 141 | .catch((err) => console.log(err)); 142 | }; 143 | 144 | createProfile = (token) => { 145 | fetch( 146 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/profile', 147 | { 148 | method: 'POST', 149 | headers: { 150 | 'Content-Type': 'application/json', 151 | Authorization: token, 152 | }, 153 | } 154 | ) 155 | .then((res) => res.json()) 156 | .then(console.log) 157 | .catch(console.log); 158 | }; 159 | 160 | updateEmail = (e) => this.setState({ email: e.target.value, errorMsg: '' }); 161 | 162 | updatePassword = (e) => { 163 | this.setState({ password: e.target.value, errorMsg: '' }); 164 | if (e.target.value === '') this.setState({ controlHidden: true }); 165 | else this.setState({ controlHidden: false }); 166 | }; 167 | 168 | updateName = (e) => this.setState({ name: e.target.value, errorMsg: '' }); 169 | 170 | updatePasswordTwo = (e) => { 171 | this.setState({ passwordTwo: e.target.value, errorMsg: '' }); 172 | if (e.target.value === '') this.setState({ controlTwoHidden: true }); 173 | else this.setState({ controlTwoHidden: false }); 174 | }; 175 | 176 | render() { 177 | const { updateEmail, updatePassword, updateName, updatePasswordTwo } = this; 178 | const { controlHidden, controlTwoHidden } = this.state; 179 | return ( 180 |
181 | 182 | Sign Up - MangaHaven 183 | 184 | 185 | 186 |

Create your account

187 |

{this.state.errorMsg}

188 |
189 | 199 | 200 | 209 | 210 |

211 | Already have an account? 212 | Log in now 213 |

214 | 215 |
216 | ); 217 | } 218 | } 219 | 220 | export default withRouter(SignUp); 221 | -------------------------------------------------------------------------------- /src/containers/css/NavBar.css: -------------------------------------------------------------------------------- 1 | .navigation-outer { 2 | display: none; 3 | background-color: rgba(0, 0, 0, 0.05); 4 | width: 100%; 5 | height: 100vh; 6 | top: 0; 7 | bottom: 0; 8 | position: fixed; 9 | z-index: 1; 10 | } 11 | 12 | .navigation { 13 | display: flex; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | flex-direction: column; 18 | width: 270px; 19 | height: 100%; 20 | background-color: #fdfffc; 21 | box-shadow: rgba(0, 0, 0, 0.13) 2px 0px 4px 0px; 22 | overflow: hidden; 23 | } 24 | 25 | .navigation a, 26 | .navigation .logout-button { 27 | color: grey; 28 | } 29 | 30 | .nav-heading { 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between; 34 | height: 3rem; 35 | padding: 0 1rem; 36 | border-bottom: 1px solid rgba(140, 140, 140, 0.6); 37 | } 38 | 39 | .nav-heading .app-name { 40 | display: flex; 41 | align-items: center; 42 | font-size: 1.15rem; 43 | } 44 | 45 | .nav-heading .app-name img { 46 | height: 28px; 47 | width: 28px; 48 | margin-right: 0.5rem; 49 | } 50 | 51 | .nav-heading .fa-times { 52 | height: 25px; 53 | width: 25px; 54 | } 55 | 56 | .navbar-links { 57 | display: flex; 58 | flex-direction: column; 59 | margin-top: 0.3rem; 60 | } 61 | 62 | .navbar-links a, 63 | .navbar-links .logout-button { 64 | padding: 0.9rem 1rem; 65 | display: flex; 66 | font-size: 0.9rem; 67 | cursor: pointer; 68 | } 69 | 70 | .navbar-links .logout-button { 71 | border: none; 72 | background-color: #fff; 73 | } 74 | 75 | .navbar-links .svg-inline--fa { 76 | width: 16px; 77 | height: 16px; 78 | margin: auto 0; 79 | } 80 | 81 | .navbar-links span { 82 | display: block; 83 | margin: auto; 84 | margin-left: 1.6rem; 85 | color: #100c08; 86 | } 87 | 88 | .navbar-links .active-window, 89 | .navbar-links a:hover, 90 | .navbar-links .logout-button:hover { 91 | background-color: rgb(0, 0, 0, 0.15); 92 | color: #4664c8; 93 | } 94 | 95 | .navbar-links .active-window span, 96 | .navbar-links a:hover span, 97 | .navbar-links .logout-button:hover span { 98 | color: #4664c8; 99 | } 100 | 101 | .navbar-links .nav-breakline { 102 | display: block; 103 | position: relative; 104 | width: 150%; 105 | height: 1px; 106 | margin: 0.5rem 0 0.5rem -1rem; 107 | background-color: rgb(140, 140, 140, 0.5); 108 | } 109 | 110 | .unhide { 111 | display: block; 112 | } 113 | 114 | .confirm-logout { 115 | position: absolute; 116 | z-index: 2; 117 | top: 0; 118 | background-color: rgba(0, 0, 0, 0.7); 119 | height: 100vh; 120 | width: 100%; 121 | display: flex; 122 | align-items: center; 123 | } 124 | 125 | .confirm-logout-inner { 126 | background-color: #fff; 127 | margin: 0 auto; 128 | text-align: center; 129 | width: 80%; 130 | border-radius: 0.5rem; 131 | padding: 2rem 1rem 1.5rem 1rem; 132 | z-index: 3; 133 | } 134 | 135 | .confirm-logout img { 136 | height: 36px; 137 | width: 36px; 138 | } 139 | 140 | .confirm-logout p, 141 | .confirm-logout h3 { 142 | margin: 0; 143 | margin-bottom: 0.5rem; 144 | } 145 | 146 | .confirm-logout h3 { 147 | font-size: 1.25rem; 148 | margin-top: 0.5rem; 149 | } 150 | 151 | .confirm-logout p { 152 | font-size: 0.95rem; 153 | color: rgb(100, 100, 100); 154 | } 155 | 156 | .confirm-logout .action-buttons { 157 | display: flex; 158 | justify-content: space-between; 159 | margin-top: 1rem; 160 | } 161 | 162 | .confirm-logout button { 163 | font-size: 1rem; 164 | padding: 0.6rem 2rem; 165 | border-radius: 1.5rem; 166 | font-weight: 600; 167 | cursor: pointer; 168 | } 169 | 170 | .confirm-logout .logout-button { 171 | border: none; 172 | background-color: rgb(250, 150, 0); 173 | color: #fff; 174 | } 175 | 176 | .confirm-logout .logout-button:hover { 177 | background-color: rgb(270, 170, 0); 178 | } 179 | 180 | .confirm-logout .cancel-button { 181 | border: none; 182 | background-color: rgb(230, 230, 230); 183 | color: #100c08; 184 | } 185 | 186 | .confirm-logout .cancel-button:hover { 187 | background-color: rgb(235, 235, 235); 188 | } 189 | 190 | @media (min-width: 675px) { 191 | .confirm-logout-inner { 192 | width: 60%; 193 | padding: 3rem 2rem 2.5rem 2rem; 194 | } 195 | .confirm-logout img { 196 | height: 72px; 197 | width: 72px; 198 | } 199 | .confirm-logout p, 200 | .confirm-logout h3 { 201 | margin-bottom: 0.8rem; 202 | } 203 | .confirm-logout h3 { 204 | font-size: 1.4rem; 205 | } 206 | .confirm-logout p { 207 | font-size: 1.05rem; 208 | } 209 | .confirm-logout .action-buttons { 210 | margin-top: 1.4rem; 211 | } 212 | .confirm-logout button { 213 | font-size: 1.2rem; 214 | padding: 0.7rem 2.5rem; 215 | border-radius: 1.5rem; 216 | font-weight: 500; 217 | } 218 | } 219 | 220 | @media (min-width: 1000px) { 221 | .navigation-outer { 222 | display: block; 223 | width: 22%; 224 | background-color: unset; 225 | } 226 | .navigation { 227 | width: 100%; 228 | box-shadow: none; 229 | border-right: 1px solid rgba(140, 140, 140, 0.6); 230 | } 231 | .nav-heading, 232 | .navbar-links a { 233 | padding-left: 1rem; 234 | border: none; 235 | } 236 | .nav-heading .fa-times { 237 | display: none; 238 | } 239 | .navbar-links a, 240 | .navbar-links .logout-button { 241 | align-items: center; 242 | font-size: 1.1rem; 243 | font-weight: 600; 244 | } 245 | .navbar-links .svg-inline--fa { 246 | width: 20px; 247 | height: 20px; 248 | } 249 | .slide-in-left, 250 | .slide-out-left { 251 | -webkit-animation: none; 252 | animation: none; 253 | } 254 | .confirm-logout-inner { 255 | width: 40%; 256 | } 257 | } 258 | 259 | @media (min-width: 1200px) { 260 | .nav-heading, 261 | .navbar-links a, 262 | .navbar-links .logout-button { 263 | padding-left: 2rem; 264 | } 265 | } 266 | 267 | .slide-in-left { 268 | -webkit-animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 269 | both; 270 | animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; 271 | } 272 | 273 | .slide-out-left { 274 | -webkit-animation: slide-out-left 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) 275 | both; 276 | animation: slide-out-left 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) both; 277 | } 278 | 279 | /* ---------------------------------------------- 280 | * Generated by Animista on 2020-4-26 14:50:8 281 | * Licensed under FreeBSD License. 282 | * See http://animista.net/license for more info. 283 | * w: http://animista.net, t: @cssanimista 284 | * ---------------------------------------------- */ 285 | 286 | /** 287 | * ---------------------------------------- 288 | * animation slide-in-left 289 | * ---------------------------------------- 290 | */ 291 | @-webkit-keyframes slide-in-left { 292 | 0% { 293 | -webkit-transform: translateX(-1000px); 294 | transform: translateX(-1000px); 295 | opacity: 0; 296 | } 297 | 100% { 298 | -webkit-transform: translateX(0); 299 | transform: translateX(0); 300 | opacity: 1; 301 | } 302 | } 303 | @keyframes slide-in-left { 304 | 0% { 305 | -webkit-transform: translateX(-1000px); 306 | transform: translateX(-1000px); 307 | opacity: 0; 308 | } 309 | 100% { 310 | -webkit-transform: translateX(0); 311 | transform: translateX(0); 312 | opacity: 1; 313 | } 314 | } 315 | 316 | /** 317 | * ---------------------------------------- 318 | * animation slide-out-left 319 | * ---------------------------------------- 320 | */ 321 | @-webkit-keyframes slide-out-left { 322 | 0% { 323 | -webkit-transform: translateX(0); 324 | transform: translateX(0); 325 | opacity: 1; 326 | } 327 | 100% { 328 | -webkit-transform: translateX(-1000px); 329 | transform: translateX(-1000px); 330 | opacity: 0; 331 | } 332 | } 333 | @keyframes slide-out-left { 334 | 0% { 335 | -webkit-transform: translateX(0); 336 | transform: translateX(0); 337 | opacity: 1; 338 | } 339 | 100% { 340 | -webkit-transform: translateX(-1000px); 341 | transform: translateX(-1000px); 342 | opacity: 0; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/containers/js/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MiniHeader from '../../components/js/MiniHeader.js'; 3 | import Collapsible from '../../components/js/Collapsible.js'; 4 | import CheckButton from '../../components/js/CheckButton.js'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; 7 | import localForage from 'localforage'; 8 | import { Helmet } from 'react-helmet'; 9 | import NavBar from './NavBar.js'; 10 | import '../css/Settings.css'; 11 | 12 | class Settings extends React.Component { 13 | constructor() { 14 | super(); 15 | this.state = { 16 | theme: 'light', 17 | view: 'horizontal', 18 | readMode: 'normal', 19 | background: 'light', 20 | }; 21 | } 22 | 23 | componentDidMount() { 24 | //check if preferences have alread been set 25 | localForage 26 | .getItem('userPreferences') 27 | .then((value) => { 28 | if (value !== null) { 29 | this.setState({ 30 | view: value.readView, 31 | readMode: value.readMode, 32 | background: value.readBG, 33 | }); 34 | } 35 | console.log(value); 36 | }) 37 | .catch((err) => console.log(err)); 38 | } 39 | 40 | toggleWebtoon = () => { 41 | this.updatePref('readMode', 'webtoon'); 42 | this.setState({ readMode: 'webtoon' }); 43 | }; 44 | 45 | toggleNormal = () => { 46 | this.updatePref('readMode', 'normal'); 47 | this.setState({ readMode: 'normal' }); 48 | }; 49 | 50 | toggleHorizontal = () => { 51 | this.updatePref('readView', 'horizontal'); 52 | this.setState({ view: 'horizontal' }); 53 | }; 54 | 55 | toggleVertical = () => { 56 | this.updatePref('readView', 'vertical'); 57 | this.setState({ view: 'vertical' }); 58 | }; 59 | 60 | toggleLightBG = () => { 61 | this.updatePref('readBG', 'light'); 62 | this.setState({ background: 'light' }); 63 | }; 64 | 65 | toggleDarkBG = () => { 66 | this.updatePref('readBG', 'dark'); 67 | this.setState({ background: 'dark' }); 68 | }; 69 | 70 | updatePref = (key, value) => { 71 | localForage 72 | .getItem('userPreferences') 73 | .then((prefs) => { 74 | if (prefs !== null) { 75 | prefs[key] = value; 76 | localForage 77 | .setItem('userPreferences', prefs) 78 | .then((val) => console.log(val)) 79 | .catch((err) => console.log(err)); 80 | } else { 81 | prefs = { 82 | theme: 'light', 83 | readView: 'horizontal', 84 | readMode: 'normal', 85 | readBG: 'light', 86 | }; 87 | prefs[key] = value; 88 | localForage 89 | .setItem('userPreferences', prefs) 90 | .then((val) => console.log(val)) 91 | .catch((err) => console.log(err)); 92 | } 93 | }) 94 | .catch((err) => console.log(err)); 95 | }; 96 | 97 | render() { 98 | const { theme, view, readMode, background } = this.state; 99 | return ( 100 |
101 | 102 | Settings - MangaHaven 103 | 104 | 105 | 106 | 107 |
108 | 113 |
114 |

Theme:

115 |
116 | 117 |

Light

118 |
119 |
120 | 121 |

Dark(coming soon)

122 |
123 |
124 |
125 | 126 | 131 |
132 |

View:

133 |
134 | 135 |

Horizontal

136 |
137 |
138 | 139 |

Vertical

140 |
141 |
142 | 143 |
144 |

Mode:

145 |
146 | 147 |

Normal

148 |
149 |
150 | 151 |

Webtoon

152 |
153 |
154 | 155 |
156 |

Background:

157 |
158 | 159 |

Light

160 |
161 |
162 | 163 |

Dark

164 |
165 |
166 |
167 | 168 | 173 | 184 | 185 | 186 | 191 | 201 | 202 | 203 | 208 | 226 | 227 |
228 |
229 | ); 230 | } 231 | } 232 | 233 | export default Settings; 234 | -------------------------------------------------------------------------------- /src/containers/js/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | // import { faUser } from '@fortawesome/free-solid-svg-icons'; 4 | import { faHistory } from '@fortawesome/free-solid-svg-icons'; 5 | import { faBook } from '@fortawesome/free-solid-svg-icons'; 6 | import { faBell } from '@fortawesome/free-solid-svg-icons'; 7 | import { faColumns } from '@fortawesome/free-solid-svg-icons'; 8 | import { faBookmark } from '@fortawesome/free-solid-svg-icons'; 9 | import { faCog } from '@fortawesome/free-solid-svg-icons'; 10 | import { faHeart } from '@fortawesome/free-solid-svg-icons'; 11 | import { faTimes } from '@fortawesome/free-solid-svg-icons'; 12 | import { faBookOpen } from '@fortawesome/free-solid-svg-icons'; 13 | import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; 14 | import AppName from '../../components/js/AppName.js'; 15 | import localForage from 'localforage'; 16 | import { confirmAlert } from 'react-confirm-alert'; 17 | import Logo from '../../assets/logo.png'; 18 | import { Link, withRouter } from 'react-router-dom'; 19 | import '../css/NavBar.css'; 20 | 21 | class NavBar extends React.Component { 22 | constructor() { 23 | super(); 24 | this.state = { 25 | signedIn: false, 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | //check if user is signed in 31 | localForage 32 | .getItem('user') 33 | .then((value) => { 34 | if (value !== null) { 35 | this.setState({ signedIn: value.signedIn }); 36 | } else this.setState({ signedIn: false }); 37 | }) 38 | .catch((err) => console.log(err)); 39 | } 40 | 41 | signOut = () => { 42 | confirmAlert({ 43 | customUI: ({ onClose }) => { 44 | return ( 45 |
46 |
e.stopPropagation()} 49 | > 50 | MangaHaven logo 51 |

Log out of MangaHaven?

52 |

53 | You can log back in at anytime or create a new account by 54 | heading to the profile section. 55 |

56 |
57 | 60 | 74 |
75 |
76 |
77 | ); 78 | }, 79 | }); 80 | }; 81 | 82 | render() { 83 | const { page } = this.props; 84 | return ( 85 |
{ 88 | document 89 | .querySelector('.navigation') 90 | .classList.toggle('slide-out-left'); 91 | document 92 | .querySelector('.navigation') 93 | .classList.toggle('slide-in-left'); 94 | document.querySelector('html').classList.toggle('prevent-scroll'); 95 | setTimeout(function () { 96 | document 97 | .querySelector('.navigation-outer') 98 | .classList.toggle('unhide'); 99 | }, 500); 100 | }} 101 | > 102 |
e.stopPropagation()}> 103 |
104 |
105 | 106 | { 109 | document 110 | .querySelector('.navigation') 111 | .classList.toggle('slide-out-left'); 112 | document 113 | .querySelector('.navigation') 114 | .classList.toggle('slide-in-left'); 115 | document 116 | .querySelector('html') 117 | .classList.toggle('prevent-scroll'); 118 | setTimeout(function () { 119 | document 120 | .querySelector('.navigation-outer') 121 | .classList.toggle('unhide'); 122 | }, 500); 123 | }} 124 | /> 125 |
126 |
127 | 133 | 134 | Explore 135 | 136 | 140 | 141 | Recently Updated 142 | 143 | 149 | 150 | All Genres 151 | 152 | 158 | 159 | My Library 160 | 161 | 167 | 168 | Library Updates 169 | 170 | 176 | 177 | Favorites 178 | 179 | 185 | 186 | History 187 | 188 | {/* 194 | 195 | Profile 196 | */} 197 | 198 | 204 | 205 | Settings 206 | 207 | {!this.state.signedIn ? null : ( 208 | 212 | )} 213 |
214 |
215 |
216 |
217 | ); 218 | } 219 | } 220 | 221 | export default withRouter(NavBar); 222 | -------------------------------------------------------------------------------- /src/containers/js/UserProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MiniHeader from '../../components/js/MiniHeader.js'; 3 | import '../css/UserProfile.css'; 4 | import Loader from '../../components/js/Loader.js'; 5 | import localForage from 'localforage'; 6 | import NavBar from './NavBar.js'; 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 8 | import { faCalendarAlt } from '@fortawesome/free-solid-svg-icons'; 9 | import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; 10 | import { faTwitter } from '@fortawesome/free-brands-svg-icons'; 11 | import { faBirthdayCake } from '@fortawesome/free-solid-svg-icons'; 12 | import { faFacebook } from '@fortawesome/free-brands-svg-icons'; 13 | import { faMapMarkerAlt } from '@fortawesome/free-solid-svg-icons'; 14 | import { faVenusMars } from '@fortawesome/free-solid-svg-icons'; 15 | import Image from '../../components/js/Image.js'; 16 | import { withRouter } from 'react-router-dom'; 17 | import { Helmet } from 'react-helmet'; 18 | import SecureLS from 'secure-ls'; 19 | 20 | class UserProfile extends React.Component { 21 | constructor() { 22 | super(); 23 | this.state = { 24 | redirect: true, 25 | userData: {}, 26 | social: {}, 27 | joined: '', 28 | }; 29 | } 30 | 31 | componentDidMount() { 32 | console.log(this.props); 33 | //check if user is signed in 34 | localForage 35 | .getItem('user') 36 | .then((value) => { 37 | if (value !== null) { 38 | if (value.signedIn) { 39 | //fetch user profile from db 40 | const ls = new SecureLS(); 41 | let token = ls.get('userToken'); 42 | fetch( 43 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/profile', 44 | { 45 | method: 'GET', 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | Authorization: token, 49 | }, 50 | } 51 | ) 52 | .then((res) => res.json()) 53 | .then((data) => { 54 | console.log(data); 55 | //replace default avatar with retro 56 | data.user.avatar = data.user.avatar.replace('d=mm', 'd=retro'); 57 | 58 | //replace data.social with empty object to prevent errors when destructuring 59 | if (data.social === undefined) data.social = {}; 60 | this.setState({ 61 | redirect: false, 62 | userData: { ...data, ...data.user }, 63 | social: data.social, 64 | joined: new Date(data.date), 65 | }); 66 | }) 67 | .catch((err) => { 68 | //replace value.social with empty object to prevent errors when destructuring 69 | if (value.social === undefined) value.social = {}; 70 | 71 | //replace default avatar with retro 72 | value.avatar = value.avatar.replace('d=mm', 'd=retro'); 73 | 74 | //display user info from webstorage in case of error 75 | this.setState({ 76 | redirect: false, 77 | userData: value, 78 | social: value.social, 79 | joined: new Date(value.dateJoined), 80 | }); 81 | }); 82 | } else this.props.history.push('/signin'); 83 | } else { 84 | this.props.history.push('/signin'); 85 | } 86 | }) 87 | .catch((err) => console.log(err)); 88 | } 89 | 90 | render() { 91 | const { redirect, joined } = this.state; 92 | const { 93 | name, 94 | avatar, 95 | bio, 96 | email, 97 | location, 98 | gender, 99 | date_of_birth, 100 | } = this.state.userData; 101 | const { twitter, facebook } = this.state.social; 102 | const months = [ 103 | 'January', 104 | 'February', 105 | 'March', 106 | 'April', 107 | 'May', 108 | 'June', 109 | 'July', 110 | 'August', 111 | 'September', 112 | 'October', 113 | 'November', 114 | 'December', 115 | ]; 116 | return ( 117 |
118 | 119 | {`${name} - MangaHaven`} 120 | 121 | 122 | 123 | 124 | {redirect ? ( 125 | 126 | ) : ( 127 |
128 |
129 |
130 | 131 |
132 |
133 |

{name}

134 | 140 |
141 |
142 |
143 | {bio !== undefined && bio !== '' ? ( 144 |

{bio}

145 | ) : null} 146 |
147 |
148 | 149 |
150 |

{email}

151 |
152 |
153 |
154 |
155 | 156 |
157 |

{`Joined ${ 158 | months[joined.getMonth()] 159 | } ${joined.getFullYear()}`}

160 |
161 | {location !== undefined && location !== '' ? ( 162 |
163 |
164 | 165 |
166 |

{location}

167 |
168 | ) : null} 169 | {gender !== undefined && gender !== '' ? ( 170 |
171 |
172 | 173 |
174 |

{gender}

175 |
176 | ) : null} 177 | {date_of_birth !== undefined && date_of_birth !== '' ? ( 178 |
179 |
180 | 181 |
182 |

{`Born ${ 183 | months[new Date(date_of_birth).getMonth()] 184 | } ${new Date(date_of_birth).getFullYear()}`}

185 |
186 | ) : null} 187 | {twitter !== undefined && twitter !== '' ? ( 188 | 198 | ) : null} 199 | {facebook !== undefined && facebook !== '' ? ( 200 | 210 | ) : null} 211 |
212 |
213 |
214 | )} 215 |
216 | ); 217 | } 218 | } 219 | 220 | export default withRouter(UserProfile); 221 | -------------------------------------------------------------------------------- /src/containers/js/EditProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MiniHeader from '../../components/js/MiniHeader.js'; 3 | import localForage from 'localforage'; 4 | import NavBar from './NavBar.js'; 5 | import { withRouter } from 'react-router-dom'; 6 | import { Helmet } from 'react-helmet'; 7 | import SecureLS from 'secure-ls'; 8 | import Loader from '../../assets/image-loader.png'; 9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 10 | import { faTimes } from '@fortawesome/free-solid-svg-icons'; 11 | import '../css/EditProfile.css'; 12 | 13 | class EditProfile extends React.Component { 14 | constructor() { 15 | super(); 16 | this.state = { 17 | loader: false, 18 | location: '', 19 | bio: '', 20 | date_of_birth: '', 21 | gender: '', 22 | twitter: '', 23 | facebook: '', 24 | token: '', 25 | success: false, 26 | error: false, 27 | }; 28 | } 29 | 30 | componentDidMount = () => { 31 | const ls = new SecureLS(); 32 | const token = ls.get('userToken'); 33 | this.setState({ token }); 34 | 35 | //fetch existing user profile 36 | fetch( 37 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/profile', 38 | { 39 | method: 'GET', 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | Authorization: token, 43 | }, 44 | } 45 | ) 46 | .then((res) => res.json()) 47 | .then((data) => { 48 | console.log(data); 49 | this.displayUserInfo(data); 50 | }) 51 | .catch((err) => { 52 | //display info from web storage 53 | localForage 54 | .getItem('user') 55 | .then((value) => this.displayUserInfo(value)) 56 | .catch((err) => console.log(err)); 57 | }); 58 | }; 59 | 60 | updateInput = (target, value) => { 61 | this.setState({ [target]: value }); 62 | }; 63 | 64 | submitProfile = (e) => { 65 | e.preventDefault(); 66 | this.setState({ loader: true }); 67 | let { 68 | location, 69 | bio, 70 | date_of_birth, 71 | gender, 72 | twitter, 73 | facebook, 74 | token, 75 | } = this.state; 76 | 77 | if (twitter !== '') twitter = `https://twitter.com/${twitter}`; 78 | if (facebook !== '') facebook = `https://facebook.com/${facebook}`; 79 | //post details to db 80 | fetch( 81 | 'https://mangahaven-server.netlify.app/.netlify/functions/app/profile', 82 | { 83 | method: 'POST', 84 | headers: { 85 | 'Content-Type': 'application/json', 86 | Authorization: token, 87 | }, 88 | body: JSON.stringify({ 89 | location, 90 | bio, 91 | date_of_birth, 92 | gender, 93 | twitter, 94 | facebook, 95 | }), 96 | } 97 | ) 98 | .then((res) => res.json()) 99 | .then((data) => { 100 | this.setState({ 101 | loader: false, 102 | success: true, 103 | }); 104 | console.log(data); 105 | 106 | //save new profile details to web storage 107 | this.saveUserProfile(data); 108 | }) 109 | .catch((err) => { 110 | this.setState({ 111 | loader: false, 112 | success: true, 113 | error: true, 114 | }); 115 | }); 116 | }; 117 | 118 | saveUserProfile = (user) => { 119 | localForage 120 | .getItem('user') 121 | .then((value) => { 122 | value.bio = user.bio; 123 | value.date_of_birth = user.date_of_birth; 124 | value.gender = user.gender; 125 | value.location = user.location; 126 | value.social = user.social; 127 | 128 | localForage.setItem('user', value).then(console.log).catch(console.log); 129 | }) 130 | .catch(console.log); 131 | }; 132 | 133 | displayUserInfo = (value) => { 134 | //get default values of profile social details 135 | let { twitter, facebook } = this.state; 136 | 137 | if (value.social !== undefined) { 138 | if (value.social.twitter) 139 | twitter = value.social.twitter.replace('https://twitter.com/', ''); 140 | if (value.social.facebook) 141 | facebook = value.social.facebook.replace('https://facebook.com/', ''); 142 | } 143 | 144 | let { location, bio, date_of_birth, gender } = value; 145 | 146 | //format dob to required format 147 | date_of_birth = date_of_birth.slice(0, date_of_birth.indexOf('T')); 148 | 149 | this.setState({ 150 | location, 151 | bio, 152 | date_of_birth, 153 | gender, 154 | twitter, 155 | facebook, 156 | }); 157 | }; 158 | 159 | render() { 160 | const { 161 | location, 162 | bio, 163 | date_of_birth, 164 | gender, 165 | twitter, 166 | facebook, 167 | loader, 168 | success, 169 | error, 170 | } = this.state; 171 | const { updateInput, submitProfile } = this; 172 | return ( 173 |
174 | 175 | {`Edit Profile - MangaHaven`} 176 | 177 | 178 | 179 | 180 | {success ? ( 181 | !error ? ( 182 |
183 |

Profile updated successfully

184 |
185 |

this.props.history.push('/profile')}> 186 | View Profile 187 |

188 | this.setState({ success: false })} 190 | icon={faTimes} 191 | /> 192 |
193 |
194 | ) : ( 195 |
196 |

An error occurred. Please try again

197 | this.setState({ success: false })} 199 | icon={faTimes} 200 | /> 201 |
202 | ) 203 | ) : null} 204 |
205 | 214 | 223 | 232 | 241 | 250 | 253 | 265 | 271 |
272 |
273 | ); 274 | } 275 | } 276 | 277 | export default withRouter(EditProfile); 278 | -------------------------------------------------------------------------------- /src/containers/css/MangaPage.css: -------------------------------------------------------------------------------- 1 | .manga-page .inactive { 2 | display: none; 3 | } 4 | 5 | .manga-page .active { 6 | display: block; 7 | } 8 | 9 | .manga-page .error-message, 10 | .manga-page .offline-message { 11 | display: none; 12 | color: #fff; 13 | width: 100%; 14 | background-color: rgb(50, 50, 50); 15 | height: 50px; 16 | align-items: center; 17 | font-size: 0.9rem; 18 | } 19 | 20 | .manga-page .error-message { 21 | position: fixed; 22 | justify-content: space-between; 23 | padding: 0 1.5rem; 24 | z-index: 2; 25 | bottom: 0; 26 | } 27 | 28 | .manga-page .retry, 29 | .manga-page .fa-times { 30 | color: rgb(30, 120, 255); 31 | cursor: pointer; 32 | } 33 | 34 | .manga-page .error-active { 35 | display: flex; 36 | } 37 | 38 | .error-message div, 39 | .offline-message .action-buttons div { 40 | display: flex; 41 | align-items: center; 42 | text-align: center; 43 | } 44 | 45 | .manga-page .network-load { 46 | height: 24px; 47 | width: 24px; 48 | } 49 | 50 | .manga-page .offline-message { 51 | padding: 0 1rem; 52 | background-color: rgb(50, 50, 50); 53 | justify-content: space-between; 54 | } 55 | 56 | .offline-message .action-buttons { 57 | width: 30%; 58 | display: flex; 59 | justify-content: flex-end; 60 | align-items: center; 61 | } 62 | 63 | .offline-message .retry { 64 | margin-right: 0.5rem; 65 | } 66 | 67 | .manga-header { 68 | width: 100%; 69 | left: 0; 70 | color: #fff; 71 | background-color: rgb(86, 128, 220); 72 | box-shadow: 0px 2px rgba(140, 140, 140, 0.2); 73 | position: sticky; 74 | z-index: 1; 75 | top: 0; 76 | } 77 | 78 | .manga-header p, 79 | .manga-info p { 80 | margin: 0; 81 | } 82 | 83 | .manga-header .svg-inline--fa { 84 | width: 20px; 85 | height: 20px; 86 | cursor: pointer; 87 | } 88 | 89 | .manga-header .fa-comment-dots { 90 | margin-left: 1.2rem; 91 | } 92 | 93 | .manga-header-nav { 94 | display: flex; 95 | justify-content: space-between; 96 | margin: 0 1rem 0.5rem 1rem; 97 | height: 3rem; 98 | align-items: center; 99 | } 100 | 101 | .manga-header-title { 102 | display: flex; 103 | align-items: flex-end; 104 | width: 80%; 105 | } 106 | 107 | .manga-header-title p { 108 | margin-left: 1.5rem; 109 | font-size: 1.2rem; 110 | font-weight: 500; 111 | overflow: hidden; 112 | white-space: nowrap; 113 | text-overflow: ellipsis; 114 | } 115 | 116 | .manga-header-buttons { 117 | display: flex; 118 | } 119 | 120 | .manga-details-nav { 121 | display: flex; 122 | padding-bottom: 0.5rem; 123 | } 124 | 125 | .manga-details-nav p { 126 | font-size: 0.85rem; 127 | font-weight: 500; 128 | width: 50%; 129 | text-align: center; 130 | color: rgb(235, 235, 235); 131 | cursor: pointer; 132 | } 133 | 134 | .manga-details-nav .current-menu { 135 | color: #fff; 136 | } 137 | 138 | .active-menu-line { 139 | width: 50%; 140 | height: 2px; 141 | background-color: #fff; 142 | transition: 0.3s ease-in-out; 143 | } 144 | 145 | .chapter { 146 | margin-left: 50%; 147 | } 148 | 149 | .active-lines .inactive { 150 | background-color: rgb(86, 128, 220); 151 | display: block; 152 | } 153 | 154 | .react-swipeable-view-container { 155 | height: 100vh; 156 | } 157 | 158 | .manga-details { 159 | overflow: hidden; 160 | } 161 | 162 | .manga-info { 163 | width: 90%; 164 | margin: 1rem auto; 165 | } 166 | 167 | .manga-info-header { 168 | display: flex; 169 | } 170 | 171 | .manga-image { 172 | background-color: rgba(0, 0, 0, 0.5); 173 | } 174 | 175 | .manga-image img, 176 | .manga-image { 177 | width: 110px; 178 | height: 160px; 179 | } 180 | 181 | .manga-image .image-loader { 182 | border-radius: 0; 183 | width: 100%; 184 | margin: 55px auto; 185 | text-align: center; 186 | } 187 | 188 | .manga-image .image-loader img { 189 | width: 50px; 190 | height: 50px; 191 | } 192 | 193 | .manga-info-details { 194 | margin-left: 0.8rem; 195 | } 196 | 197 | .manga-info-details p { 198 | margin-bottom: 0.4rem; 199 | font-size: 0.9rem; 200 | } 201 | 202 | .manga-info-details .alternative-names { 203 | display: none; 204 | } 205 | 206 | .manga-info-details .manga-title { 207 | font-size: 1.2rem; 208 | font-weight: 400; 209 | } 210 | 211 | .manga-info-details span { 212 | margin-left: 0.3rem; 213 | color: rgb(0, 0, 0, 0.6); 214 | } 215 | 216 | .manga-description { 217 | margin-top: 0.7rem; 218 | } 219 | 220 | .manga-description p { 221 | margin-bottom: 0.4rem; 222 | } 223 | 224 | .action-icon-buttons { 225 | display: flex; 226 | position: absolute; 227 | right: 1rem; 228 | margin-top: -1.5rem; 229 | } 230 | 231 | .action-icon-buttons button { 232 | display: flex; 233 | background-color: rgb(86, 128, 220); 234 | width: 35px; 235 | height: 35px; 236 | border-radius: 50%; 237 | border: 0px; 238 | cursor: pointer; 239 | } 240 | 241 | .action-icon-buttons button:focus { 242 | animation: shadow-pulse 1.5s; 243 | } 244 | 245 | .action-icon-buttons button:disabled { 246 | opacity: 0.5; 247 | } 248 | 249 | .bookmark-icon { 250 | margin-right: 0.4rem; 251 | } 252 | 253 | .action-icon-buttons button .svg-inline--fa { 254 | margin: auto; 255 | width: 16px; 256 | height: 16px; 257 | color: white; 258 | } 259 | 260 | @keyframes shadow-pulse { 261 | 0% { 262 | box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2); 263 | } 264 | 100% { 265 | box-shadow: 0 0 0 8px rgba(0, 0, 0, 0); 266 | } 267 | } 268 | 269 | .manga-genres { 270 | margin-bottom: 0.7rem; 271 | } 272 | 273 | .genre-links { 274 | display: flex; 275 | flex-wrap: wrap; 276 | } 277 | 278 | .genre-links a { 279 | padding-right: 0.2rem; 280 | font-size: 0.95rem; 281 | color: blue; 282 | } 283 | 284 | .manga-description-note { 285 | color: rgb(0, 0, 0, 0.6); 286 | font-size: 0.9rem; 287 | } 288 | 289 | .manga-share-box { 290 | display: none; 291 | position: fixed; 292 | top: 0; 293 | left: 0; 294 | bottom: 0; 295 | right: 0; 296 | z-index: 3; 297 | background: rgba(0, 0, 0, 0.5); 298 | } 299 | 300 | .manga-share-box-inner { 301 | display: flex; 302 | flex-wrap: wrap; 303 | position: fixed; 304 | bottom: 0; 305 | left: 0; 306 | right: 0; 307 | background: #fff; 308 | width: 90%; 309 | margin: 1.5rem auto; 310 | border-radius: 10px; 311 | padding-top: 2rem; 312 | padding-bottom: 1rem; 313 | } 314 | 315 | .manga-share-box p { 316 | margin: 0; 317 | font-size: 0.8rem; 318 | } 319 | 320 | .share-button { 321 | width: 25%; 322 | text-align: center; 323 | margin-bottom: 1rem; 324 | } 325 | 326 | .fa-copy { 327 | width: 45px !important; 328 | height: 45px; 329 | } 330 | 331 | .copy-success { 332 | display: none; 333 | width: 100%; 334 | text-align: center; 335 | position: fixed; 336 | bottom: 0; 337 | margin-bottom: 2.5rem; 338 | } 339 | 340 | .copy-success span { 341 | display: inline-block; 342 | padding: 0.5rem; 343 | background: rgb(20, 20, 20); 344 | color: #fff; 345 | border-radius: 3px; 346 | } 347 | 348 | @media (min-width: 1000px) { 349 | .manga-header, 350 | .manga-details, 351 | .manga-page .error-message { 352 | margin-left: 22%; 353 | width: 78%; 354 | } 355 | 356 | .manga-page .error-message { 357 | font-size: 1.1rem; 358 | height: 4rem; 359 | } 360 | 361 | .manga-header-title p { 362 | font-size: 1.4rem; 363 | } 364 | 365 | .manga-header .svg-inline--fa { 366 | width: 24px; 367 | height: 24px; 368 | } 369 | 370 | .manga-details-nav p { 371 | font-size: 0.95rem; 372 | } 373 | 374 | .manga-share-box-inner { 375 | margin-left: 36%; 376 | width: 50%; 377 | } 378 | 379 | .offline-message p { 380 | font-size: 1rem; 381 | } 382 | 383 | .manga-image, 384 | .manga-image img { 385 | width: 180px; 386 | height: 250px; 387 | } 388 | 389 | .manga-info-details { 390 | margin-left: 1rem; 391 | } 392 | 393 | .manga-info-details p { 394 | font-size: 1.05rem; 395 | } 396 | 397 | .manga-info-details .manga-title { 398 | font-size: 1.35rem; 399 | } 400 | 401 | .manga-info-details .alternative-names { 402 | display: -webkit-box; 403 | -webkit-box-orient: vertical; 404 | -webkit-line-clamp: 2; 405 | overflow: hidden; 406 | } 407 | 408 | .manga-info-details span { 409 | margin-left: 0.5rem; 410 | font-size: 0.98rem; 411 | } 412 | 413 | .action-icon-buttons { 414 | right: 5rem; 415 | } 416 | 417 | .action-icon-buttons button { 418 | height: 40px; 419 | width: 40px; 420 | } 421 | 422 | .action-icon-buttons button .svg-inline--fa { 423 | width: 20px; 424 | height: 20px; 425 | } 426 | 427 | .manga-genres p, 428 | .manga-description p { 429 | font-size: 1.2rem; 430 | } 431 | 432 | .manga-description-note p, 433 | .genre-links a { 434 | font-size: 1rem; 435 | word-spacing: 0.1rem; 436 | line-height: 1.2rem; 437 | } 438 | 439 | .manga-share-box p { 440 | font-size: 1.2rem; 441 | } 442 | 443 | .share-button svg { 444 | height: 75px; 445 | width: 75px; 446 | } 447 | 448 | .fa-copy { 449 | height: 60px; 450 | width: 60px !important; 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/containers/css/ChapterPage.css: -------------------------------------------------------------------------------- 1 | .chapter-page .error-div { 2 | margin: 0 auto; 3 | display: flex; 4 | align-items: center; 5 | height: 100vh; 6 | } 7 | 8 | .chapter-page .svg-inline--fa { 9 | cursor: pointer; 10 | } 11 | 12 | .chapter-page-header, 13 | .chapter-page-footer, 14 | .chapter-page-settings-inner { 15 | display: none; 16 | position: absolute; 17 | width: 100%; 18 | } 19 | 20 | .chapter-page-header, 21 | .chapter-page-footer { 22 | align-items: center; 23 | background-color: rgb(70, 70, 70); 24 | color: #fff; 25 | z-index: 3; 26 | height: 3rem; 27 | padding: 0 1rem; 28 | } 29 | 30 | .chapter-page .active { 31 | display: flex; 32 | } 33 | 34 | .chapter-page-header { 35 | top: 0; 36 | left: 0; 37 | } 38 | 39 | .chapter-page-header .back-button { 40 | width: 13%; 41 | } 42 | 43 | .header-wrapper { 44 | display: flex; 45 | justify-content: space-between; 46 | align-items: center; 47 | width: 87%; 48 | } 49 | 50 | .chapter-page-header p { 51 | margin: 0; 52 | } 53 | 54 | .chapter-page-header .svg-inline--fa { 55 | height: 24px; 56 | width: 24px; 57 | } 58 | 59 | .chapter-page-header .header-title { 60 | width: 80%; 61 | } 62 | 63 | .chapter-page-header .header-title p { 64 | overflow: hidden; 65 | white-space: nowrap; 66 | text-overflow: ellipsis; 67 | } 68 | 69 | .chapter-page-header .header-title .manga-title { 70 | font-size: 1.2rem; 71 | } 72 | 73 | .chapter-page-header .header-title .chapter-number { 74 | font-size: 0.9rem; 75 | } 76 | 77 | .chapter-page-header .fa-cog { 78 | float: right; 79 | } 80 | 81 | .chapter-page-footer { 82 | bottom: 0; 83 | } 84 | 85 | .chapter-page-footer .svg-inline--fa { 86 | height: 16px; 87 | width: 16px; 88 | } 89 | 90 | .chapter-page-footer .fa-step-backward { 91 | margin-right: 0.7rem; 92 | } 93 | 94 | .chapter-page-footer .fa-step-forward { 95 | margin-left: 0.7rem; 96 | } 97 | 98 | .chapter-page-footer .rc-progress-line { 99 | width: 100%; 100 | } 101 | 102 | .light, 103 | .light .svg-inline--fa { 104 | background-color: #fff; 105 | color: #000; 106 | } 107 | 108 | .dark, 109 | .dark .svg-inline--fa { 110 | background-color: #000; 111 | color: #fff; 112 | } 113 | 114 | .chapter-page-settings { 115 | height: 100%; 116 | width: 100%; 117 | position: fixed; 118 | background-color: rgba(0, 0, 0, 0.5); 119 | z-index: 3; 120 | top: 0; 121 | left: 0; 122 | bottom: 0; 123 | right: 0; 124 | display: none; 125 | } 126 | 127 | .block-active { 128 | display: block; 129 | } 130 | 131 | .chapter-page-settings-inner { 132 | display: flex; 133 | height: 40%; 134 | flex-direction: column; 135 | bottom: 0; 136 | z-index: 4; 137 | padding: 0 1rem; 138 | justify-content: center; 139 | } 140 | 141 | .chapter-page-settings .chapter-view, 142 | .chapter-page-settings .background-settings, 143 | .chapter-page-settings .reading-mode { 144 | display: flex; 145 | align-items: center; 146 | } 147 | 148 | .chapter-page-settings p { 149 | margin: 0; 150 | width: 30%; 151 | } 152 | 153 | .chapter-page-settings .options { 154 | display: flex; 155 | width: 70%; 156 | } 157 | 158 | .chapter-page .loader-gif { 159 | height: 100vh; 160 | display: flex; 161 | align-items: center; 162 | justify-content: center; 163 | } 164 | 165 | .chapter-page .loader-gif img { 166 | width: 96px; 167 | } 168 | 169 | .chapter-view, 170 | .background-settings { 171 | margin-bottom: 2rem; 172 | } 173 | 174 | .options div { 175 | width: 50%; 176 | display: flex; 177 | align-items: center; 178 | cursor: pointer; 179 | } 180 | 181 | .axis-vertical, 182 | .axis-vertical .slider { 183 | height: 100vh !important; 184 | } 185 | 186 | .carousel .carousel-status { 187 | text-shadow: none; 188 | } 189 | 190 | .chapter-page-dark .carousel .carousel-status { 191 | color: #fff; 192 | } 193 | 194 | .chapter-page-light .carousel .carousel-status { 195 | color: #000; 196 | } 197 | 198 | .carousel .control-arrow, 199 | .carousel.carousel-slider .control-arrow { 200 | opacity: 0; 201 | width: 100px; 202 | } 203 | 204 | .chapter-page-dark .carousel.carousel-slider .control-arrow:hover { 205 | background: rgba(255, 255, 255, 0.3); 206 | } 207 | 208 | .chapter-image { 209 | display: flex; 210 | height: 100vh; 211 | width: 100vw; 212 | margin: auto; 213 | } 214 | 215 | .chapter-image img { 216 | width: 100%; 217 | margin: auto; 218 | } 219 | 220 | .chapter-image .image-loader { 221 | border-radius: 0; 222 | width: 100%; 223 | margin: 50px auto; 224 | text-align: center; 225 | } 226 | 227 | .chapter-image .image-loader img { 228 | width: 72px; 229 | height: 72px; 230 | } 231 | 232 | .notify-chapter { 233 | margin: 0 auto; 234 | width: 100%; 235 | height: 100vh; 236 | display: flex; 237 | align-items: center; 238 | } 239 | 240 | .notify-chapter div, 241 | .notify-chapter .no-chapter { 242 | margin: 0 auto; 243 | position: relative; 244 | width: 50%; 245 | text-align: left; 246 | } 247 | 248 | .notify-chapter div p { 249 | font-weight: 600; 250 | margin: 0 auto; 251 | margin-top: 1rem; 252 | } 253 | 254 | .notify-chapter .no-chapter { 255 | text-align: center; 256 | } 257 | 258 | .notify-chapter button { 259 | display: block; 260 | margin-top: 0.6rem; 261 | background-color: rgba(175, 175, 175); 262 | border-radius: 0.3rem; 263 | border: 1px solid rgba(175, 175, 175); 264 | font-size: 0.95rem; 265 | padding: 0.5rem 0.8rem; 266 | cursor: pointer; 267 | } 268 | 269 | .notify-chapter button:hover { 270 | border: 1px solid rgba(200, 200, 200); 271 | background-color: rgba(200, 200, 200); 272 | } 273 | 274 | @media (max-height: 600px) { 275 | .chapter-image img { 276 | height: 85vh; 277 | } 278 | } 279 | 280 | @media (min-width: 675px) { 281 | .chapter-page-header, 282 | .chapter-page-footer { 283 | height: 5rem; 284 | padding: 0 1.5rem; 285 | } 286 | .chapter-page-header .svg-inline--fa { 287 | height: 36px; 288 | width: 36px; 289 | } 290 | .chapter-page-header .back-button { 291 | width: 10%; 292 | } 293 | .header-wrapper { 294 | width: 90%; 295 | } 296 | .chapter-page-header .header-title { 297 | width: 85%; 298 | } 299 | .chapter-page-header .header-title .manga-title { 300 | font-size: 1.6rem; 301 | } 302 | .chapter-page-header .header-title .chapter-number { 303 | font-size: 1.3rem; 304 | } 305 | .chapter-page-footer .svg-inline--fa { 306 | height: 28px; 307 | width: 28px; 308 | } 309 | .chapter-page-footer .fa-step-backward { 310 | margin-right: 1rem; 311 | } 312 | .chapter-page-footer .fa-step-forward { 313 | margin-left: 1rem; 314 | } 315 | .chapter-page-settings-inner { 316 | padding: 0 2rem; 317 | } 318 | .chapter-page-settings-inner .check-button { 319 | margin-right: 2rem; 320 | } 321 | .chapter-page-settings-inner .svg-inline--fa { 322 | width: 24px; 323 | height: 24px; 324 | } 325 | .chapter-page-settings p, 326 | .chapter-page-settings span { 327 | font-size: 1.5rem; 328 | } 329 | .carousel .carousel-status { 330 | font-size: 1.2rem; 331 | padding: 0 0.8rem; 332 | margin: 0.8rem 0; 333 | } 334 | .carousel .control-arrow, 335 | .carousel.carousel-slider .control-arrow { 336 | width: 180px; 337 | } 338 | .chapter-image img { 339 | height: 90vh; 340 | } 341 | .notify-chapter div p, 342 | .notify-chapter div span, 343 | .notify-chapter .no-chapter { 344 | font-size: 1.5rem; 345 | } 346 | .notify-chapter button { 347 | font-size: 1.45rem; 348 | border-radius: 0.3rem; 349 | padding: 0.8rem 1.1rem; 350 | } 351 | } 352 | 353 | @media (min-width: 1000px) { 354 | .chapter-page .error-div { 355 | width: 80%; 356 | } 357 | .chapter-page .error-div-inner { 358 | width: 100%; 359 | } 360 | .chapter-page .loader-gif { 361 | width: 100%; 362 | margin: 0 auto; 363 | } 364 | .chapter-page-header .back-button { 365 | width: 8%; 366 | } 367 | .header-wrapper { 368 | width: 92%; 369 | } 370 | .chapter-page-header .header-title { 371 | width: 90%; 372 | } 373 | .chapter-page-footer .rc-progress-line { 374 | height: 1rem; 375 | } 376 | .carousel .carousel-status { 377 | font-size: 1.2rem; 378 | padding: 0 0.8rem; 379 | margin: 0.8rem 0; 380 | } 381 | .carousel .control-arrow, 382 | .carousel.carousel-slider .control-arrow { 383 | opacity: 1; 384 | width: 15vw; 385 | } 386 | .carousel .control-arrow:before, 387 | .carousel.carousel-slider .control-arrow:before { 388 | border-top: 1.5rem solid transparent; 389 | border-bottom: 1.5rem solid transparent; 390 | } 391 | .chapter-page-dark .carousel .control-next.control-arrow:before { 392 | border-left: 1.5rem solid #fff; 393 | } 394 | .chapter-page-dark .carousel .control-prev.control-arrow:before { 395 | border-right: 1.5rem solid #fff; 396 | } 397 | .chapter-page-light .carousel .control-next.control-arrow:before { 398 | border-left: 1.5rem solid #000; 399 | } 400 | .chapter-page-light .carousel .control-prev.control-arrow:before { 401 | border-right: 1.5rem solid #000; 402 | } 403 | .carousel .slide img { 404 | height: 100vh; 405 | width: 70vw; 406 | } 407 | .chapter-image .image-loader img { 408 | width: 72px; 409 | height: 72px; 410 | } 411 | } 412 | 413 | @media (min-width: 1200px) { 414 | .chapter-page-header, 415 | .chapter-page-footer { 416 | padding: 0 2rem; 417 | } 418 | .chapter-page-header .back-button { 419 | width: 7%; 420 | } 421 | .header-wrapper { 422 | width: 93%; 423 | } 424 | .chapter-page-header .header-title { 425 | width: 90%; 426 | } 427 | .chapter-page-footer .rc-progress-line { 428 | height: 1.3rem; 429 | } 430 | .chapter-page-settings-inner { 431 | padding: 0 5rem; 432 | } 433 | .carousel .control-arrow, 434 | .carousel.carousel-slider .control-arrow { 435 | width: 29vw; 436 | } 437 | .carousel .slide img { 438 | width: 42vw; 439 | } 440 | .chapter-image .image-loader img { 441 | width: 72px; 442 | height: 72px; 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /src/components/js/Collapsible.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { faAngleUp } from '@fortawesome/free-solid-svg-icons'; 5 | import { faAngleDown } from '@fortawesome/free-solid-svg-icons'; 6 | 7 | class Collapsible extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.timeout = undefined; 12 | 13 | // Bind class methods 14 | this.handleTriggerClick = this.handleTriggerClick.bind(this); 15 | this.handleTransitionEnd = this.handleTransitionEnd.bind(this); 16 | this.continueOpenCollapsible = this.continueOpenCollapsible.bind(this); 17 | this.setInnerRef = this.setInnerRef.bind(this); 18 | 19 | // Defaults the dropdown to be closed 20 | if (props.open) { 21 | this.state = { 22 | isClosed: false, 23 | shouldSwitchAutoOnNextCycle: false, 24 | height: 'auto', 25 | transition: 'none', 26 | hasBeenOpened: true, 27 | overflow: props.overflowWhenOpen, 28 | inTransition: false, 29 | }; 30 | } else { 31 | this.state = { 32 | isClosed: true, 33 | shouldSwitchAutoOnNextCycle: false, 34 | height: 0, 35 | transition: `height ${props.transitionTime}ms ${props.easing}`, 36 | hasBeenOpened: false, 37 | overflow: 'hidden', 38 | inTransition: false, 39 | }; 40 | } 41 | } 42 | 43 | componentDidUpdate(prevProps, prevState) { 44 | if (this.state.shouldOpenOnNextCycle) { 45 | this.continueOpenCollapsible(); 46 | } 47 | 48 | if ( 49 | prevState.height === 'auto' && 50 | this.state.shouldSwitchAutoOnNextCycle === true 51 | ) { 52 | window.clearTimeout(this.timeout); 53 | this.timeout = window.setTimeout(() => { 54 | // Set small timeout to ensure a true re-render 55 | this.setState({ 56 | height: 0, 57 | overflow: 'hidden', 58 | isClosed: true, 59 | shouldSwitchAutoOnNextCycle: false, 60 | }); 61 | }, 50); 62 | } 63 | 64 | // If there has been a change in the open prop (controlled by accordion) 65 | if (prevProps.open !== this.props.open) { 66 | if (this.props.open === true) { 67 | this.openCollapsible(); 68 | this.props.onOpening(); 69 | } else { 70 | this.closeCollapsible(); 71 | this.props.onClosing(); 72 | } 73 | } 74 | } 75 | 76 | componentWillUnmount() { 77 | window.clearTimeout(this.timeout); 78 | } 79 | 80 | closeCollapsible() { 81 | this.setState({ 82 | shouldSwitchAutoOnNextCycle: true, 83 | height: this.innerRef.scrollHeight, 84 | transition: `height ${ 85 | this.props.transitionCloseTime 86 | ? this.props.transitionCloseTime 87 | : this.props.transitionTime 88 | }ms ${this.props.easing}`, 89 | inTransition: true, 90 | }); 91 | } 92 | 93 | openCollapsible() { 94 | this.setState({ 95 | inTransition: true, 96 | shouldOpenOnNextCycle: true, 97 | }); 98 | } 99 | 100 | continueOpenCollapsible() { 101 | this.setState({ 102 | height: this.innerRef.scrollHeight, 103 | transition: `height ${this.props.transitionTime}ms ${this.props.easing}`, 104 | isClosed: false, 105 | hasBeenOpened: true, 106 | inTransition: true, 107 | shouldOpenOnNextCycle: false, 108 | }); 109 | } 110 | 111 | handleTriggerClick(event) { 112 | if (this.props.triggerDisabled || this.state.inTransition) { 113 | return; 114 | } 115 | 116 | event.preventDefault(); 117 | 118 | if (this.props.handleTriggerClick) { 119 | this.props.handleTriggerClick(this.props.accordionPosition); 120 | } else { 121 | if (this.state.isClosed === true) { 122 | this.openCollapsible(); 123 | this.props.onOpening(); 124 | this.props.onTriggerOpening(); 125 | } else { 126 | this.closeCollapsible(); 127 | this.props.onClosing(); 128 | this.props.onTriggerClosing(); 129 | } 130 | } 131 | } 132 | 133 | renderNonClickableTriggerElement() { 134 | if ( 135 | this.props.triggerSibling && 136 | typeof this.props.triggerSibling === 'string' 137 | ) { 138 | return ( 139 | 140 | {this.props.triggerSibling} 141 | 142 | ); 143 | } else if ( 144 | this.props.triggerSibling && 145 | typeof this.props.triggerSibling === 'function' 146 | ) { 147 | return this.props.triggerSibling(); 148 | } else if (this.props.triggerSibling) { 149 | return ; 150 | } 151 | 152 | return null; 153 | } 154 | 155 | handleTransitionEnd(e) { 156 | // only handle transitions that origin from the container of this component 157 | if (e.target !== this.innerRef) { 158 | return; 159 | } 160 | // Switch to height auto to make the container responsive 161 | if (!this.state.isClosed) { 162 | this.setState({ 163 | height: 'auto', 164 | overflow: this.props.overflowWhenOpen, 165 | inTransition: false, 166 | }); 167 | this.props.onOpen(); 168 | } else { 169 | this.setState({ inTransition: false }); 170 | this.props.onClose(); 171 | } 172 | } 173 | 174 | setInnerRef(ref) { 175 | this.innerRef = ref; 176 | } 177 | 178 | render() { 179 | const dropdownStyle = { 180 | height: this.state.height, 181 | WebkitTransition: this.state.transition, 182 | msTransition: this.state.transition, 183 | transition: this.state.transition, 184 | overflow: this.state.overflow, 185 | }; 186 | 187 | var openClass = this.state.isClosed ? 'is-closed' : 'is-open'; 188 | var disabledClass = this.props.triggerDisabled ? 'is-disabled' : ''; 189 | 190 | //If user wants different text when tray is open 191 | var trigger = 192 | this.state.isClosed === false && this.props.triggerWhenOpen !== undefined 193 | ? this.props.triggerWhenOpen 194 | : this.props.trigger; 195 | 196 | const ContentContainerElement = this.props.contentContainerTagName; 197 | 198 | // If user wants a trigger wrapping element different than 'span' 199 | const TriggerElement = this.props.triggerTagName; 200 | 201 | // Don't render children until the first opening of the Collapsible if lazy rendering is enabled 202 | var children = 203 | this.props.lazyRender && 204 | !this.state.hasBeenOpened && 205 | this.state.isClosed && 206 | !this.state.inTransition 207 | ? null 208 | : this.props.children; 209 | 210 | // Construct CSS classes strings 211 | const triggerClassString = `${ 212 | this.props.classParentString 213 | }__trigger ${openClass} ${disabledClass} ${ 214 | this.state.isClosed 215 | ? this.props.triggerClassName 216 | : this.props.triggerOpenedClassName 217 | }`; 218 | const parentClassString = `${this.props.classParentString} ${ 219 | this.state.isClosed ? this.props.className : this.props.openedClassName 220 | }`; 221 | const outerClassString = `${this.props.classParentString}__contentOuter ${this.props.contentOuterClassName}`; 222 | const innerClassString = `${this.props.classParentString}__contentInner ${this.props.contentInnerClassName}`; 223 | 224 | return ( 225 | 229 | { 234 | const { key } = event; 235 | if ( 236 | (key === ' ' && 237 | this.props.triggerTagName.toLowerCase() !== 'button') || 238 | key === 'Enter' 239 | ) { 240 | this.handleTriggerClick(event); 241 | } 242 | }} 243 | tabIndex={this.props.tabIndex && this.props.tabIndex} 244 | {...this.props.triggerElementProps} 245 | > 246 |

{trigger}

247 | {this.state.isClosed ? ( 248 | 249 | ) : ( 250 | 251 | )} 252 |
253 | 254 | {this.renderNonClickableTriggerElement()} 255 | 256 |
262 |
{children}
263 |
264 |
265 | ); 266 | } 267 | } 268 | 269 | Collapsible.propTypes = { 270 | transitionTime: PropTypes.number, 271 | transitionCloseTime: PropTypes.number, 272 | triggerTagName: PropTypes.string, 273 | easing: PropTypes.string, 274 | open: PropTypes.bool, 275 | containerElementProps: PropTypes.object, 276 | triggerElementProps: PropTypes.object, 277 | classParentString: PropTypes.string, 278 | openedClassName: PropTypes.string, 279 | triggerStyle: PropTypes.object, 280 | triggerClassName: PropTypes.string, 281 | triggerOpenedClassName: PropTypes.string, 282 | contentOuterClassName: PropTypes.string, 283 | contentInnerClassName: PropTypes.string, 284 | accordionPosition: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 285 | handleTriggerClick: PropTypes.func, 286 | onOpen: PropTypes.func, 287 | onClose: PropTypes.func, 288 | onOpening: PropTypes.func, 289 | onClosing: PropTypes.func, 290 | onTriggerOpening: PropTypes.func, 291 | onTriggerClosing: PropTypes.func, 292 | trigger: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 293 | triggerWhenOpen: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 294 | triggerDisabled: PropTypes.bool, 295 | lazyRender: PropTypes.bool, 296 | overflowWhenOpen: PropTypes.oneOf([ 297 | 'hidden', 298 | 'visible', 299 | 'auto', 300 | 'scroll', 301 | 'inherit', 302 | 'initial', 303 | 'unset', 304 | ]), 305 | triggerSibling: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), 306 | tabIndex: PropTypes.number, 307 | contentContainerTagName: PropTypes.string, 308 | }; 309 | 310 | Collapsible.defaultProps = { 311 | transitionTime: 400, 312 | transitionCloseTime: null, 313 | triggerTagName: 'span', 314 | easing: 'linear', 315 | open: false, 316 | classParentString: 'Collapsible', 317 | triggerDisabled: false, 318 | lazyRender: false, 319 | overflowWhenOpen: 'hidden', 320 | openedClassName: '', 321 | triggerStyle: null, 322 | triggerClassName: '', 323 | triggerOpenedClassName: '', 324 | contentOuterClassName: '', 325 | contentInnerClassName: '', 326 | className: '', 327 | triggerSibling: null, 328 | onOpen: () => {}, 329 | onClose: () => {}, 330 | onOpening: () => {}, 331 | onClosing: () => {}, 332 | onTriggerOpening: () => {}, 333 | onTriggerClosing: () => {}, 334 | tabIndex: null, 335 | contentContainerTagName: 'div', 336 | }; 337 | 338 | export default Collapsible; 339 | -------------------------------------------------------------------------------- /src/containers/js/ChapterPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChapterImage from '../../components/js/ChapterImage.js'; 3 | import { Carousel } from 'react-responsive-carousel'; 4 | import { withRouter } from 'react-router-dom'; 5 | import { InView } from 'react-intersection-observer'; 6 | import Loader from '../../components/js/Loader.js'; 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 8 | import { faCog } from '@fortawesome/free-solid-svg-icons'; 9 | import { faTimes } from '@fortawesome/free-solid-svg-icons'; 10 | import { faStepBackward } from '@fortawesome/free-solid-svg-icons'; 11 | import { faStepForward } from '@fortawesome/free-solid-svg-icons'; 12 | import BackButton from '../../components/js/BackButton.js'; 13 | import CheckButton from '../../components/js/CheckButton.js'; 14 | import { Line } from 'rc-progress'; 15 | import Modal from 'react-modal'; 16 | import ErrorMessage from '../../components/js/ErrorMessage.js'; 17 | import { Helmet } from 'react-helmet'; 18 | import localForage from 'localforage'; 19 | import { API_BASE_URL } from '../../utils/config.js'; 20 | import 'react-responsive-carousel/lib/styles/carousel.min.css'; 21 | import '../css/ChapterPage.css'; 22 | 23 | class ChapterPage extends React.Component { 24 | constructor() { 25 | super(); 26 | this.state = { 27 | manga: {}, 28 | chapterIndex: 0, 29 | chapterImages: [], 30 | chapterNumber: '', 31 | defaultPage: 1, 32 | nextChapter: {}, 33 | prevChapter: {}, 34 | chapterTitle: '', 35 | mangaName: '', 36 | alias: '', 37 | progress: 0, 38 | headerActive: false, 39 | view: 'horizontal', 40 | background: 'light', 41 | readMode: 'normal', 42 | settings: false, 43 | loader: true, 44 | modalOpen: false, 45 | currentImage: [], 46 | modalColor: '#000', 47 | modalBG: 'rgba(255, 255, 255, 0.8)', 48 | networkError: false, 49 | }; 50 | } 51 | 52 | componentDidMount() { 53 | //check if default preferences have been set beforehand 54 | localForage 55 | .getItem('userPreferences') 56 | .then((value) => { 57 | if (value !== null) { 58 | this.setState({ 59 | view: value.readView, 60 | background: value.readBG, 61 | readMode: value.readMode, 62 | }); 63 | } 64 | console.log(value); 65 | }) 66 | .catch((err) => console.log(err)); 67 | this.fetchData(); 68 | } 69 | 70 | fetchData = () => { 71 | const { mangaName, chapterNum } = this.props.match.params; 72 | this.setState({ 73 | alias: mangaName, 74 | }); 75 | 76 | //check if user came from history page and parse page num from url 77 | const url = new URL(window.location.href); 78 | let pageNum = 1; 79 | if (url.search !== '') { 80 | let searchParams = new URLSearchParams(url.search); 81 | pageNum = Number(searchParams.get('q').trim()); 82 | } 83 | 84 | this.setState({ 85 | chapterNumber: chapterNum, 86 | networkError: false, 87 | defaultPage: pageNum, 88 | }); 89 | 90 | fetch(`${API_BASE_URL}/${mangaName}/chapter/${chapterNum}`) 91 | .then((res) => res.json()) 92 | .then((data) => { 93 | console.log(data); 94 | const { 95 | nextChapter, 96 | prevChapter, 97 | chapterNum, 98 | images, 99 | ChapterName, 100 | mangaName, 101 | } = data; 102 | this.setState({ 103 | chapterImages: images, 104 | currentImage: images[pageNum - 1], 105 | chapterNumber: chapterNum, 106 | nextChapter, 107 | prevChapter, 108 | mangaName, 109 | chapterTitle: ChapterName, 110 | networkError: false, 111 | }); 112 | }) 113 | .catch((err) => this.setState({ networkError: true })); 114 | 115 | fetch(`${API_BASE_URL}/manga/${mangaName}`) 116 | .then((res) => res.json()) 117 | .then((data) => { 118 | this.setState({ 119 | manga: data, 120 | loader: false, 121 | networkError: false, 122 | }); 123 | 124 | this.saveToHistory(data, pageNum); 125 | this.updateMangaCatalog(pageNum, false, data); 126 | 127 | console.log(data); 128 | }) 129 | .catch((err) => this.setState({ networkError: true })); 130 | }; 131 | 132 | calcProgress = (current, total) => { 133 | let progress = Math.round((current / total) * 100); 134 | if (progress !== this.state.progress) this.setState({ progress }); 135 | }; 136 | 137 | toggleVertical = () => this.setState({ view: 'vertical' }); 138 | 139 | toggleHorizontal = () => this.setState({ view: 'horizontal' }); 140 | 141 | toggleNormal = () => this.setState({ readMode: 'normal' }); 142 | 143 | toggleWebtoon = () => this.setState({ readMode: 'webtoon' }); 144 | 145 | toggleDark = () => { 146 | this.setState({ 147 | background: 'dark', 148 | modalColor: '#fff', 149 | modalBG: 'rgba(0, 0, 0, 0.8)', 150 | }); 151 | }; 152 | 153 | toggleLight = () => { 154 | this.setState({ 155 | background: 'light', 156 | modalColor: '#000', 157 | modalBG: 'rgba(255, 255, 255, 0.8)', 158 | }); 159 | }; 160 | 161 | displaySettings = () => this.setState({ settings: !this.state.settings }); 162 | 163 | displayNextChapter = () => { 164 | const { mangaName } = this.props.match.params; 165 | const { nextChapter } = this.state; 166 | this.props.history.push( 167 | `/read/${mangaName}/chapter/${nextChapter.chapterNum}` 168 | ); 169 | window.location.reload(); 170 | }; 171 | 172 | displayPrevChapter = () => { 173 | const { mangaName } = this.props.match.params; 174 | const { prevChapter } = this.state; 175 | this.props.history.push( 176 | `/read/${mangaName}/chapter/${prevChapter.chapterNum}` 177 | ); 178 | window.location.reload(); 179 | }; 180 | 181 | saveToHistory = (manga, pageNum) => { 182 | const { chapterNum } = this.props.match.params; 183 | //fetch history from storage 184 | localForage 185 | .getItem('readHistory') 186 | .then((value) => { 187 | if (value === null) { 188 | let recentManga = []; 189 | recentManga.push({ 190 | alias: manga.alias, 191 | image: manga.imageUrl, 192 | title: manga.name, 193 | chapterNum, 194 | page: 1, 195 | added: new Date().getTime(), 196 | }); 197 | 198 | localForage 199 | .setItem('readHistory', recentManga) 200 | .then((value) => console.log(value)) 201 | .catch((err) => console.log(err)); 202 | } else { 203 | //check if manga already exists in history 204 | let mangaPresent = false; 205 | for (let i = 0; i < value.length; i++) { 206 | if (value[i].alias === manga.alias) { 207 | value[i].chapterNum = chapterNum; 208 | value[i].added = new Date().getTime(); 209 | value[i].page = pageNum; 210 | mangaPresent = true; 211 | break; 212 | } 213 | } 214 | if (!mangaPresent) { 215 | value.push({ 216 | alias: manga.alias, 217 | image: manga.imageUrl, 218 | title: manga.name, 219 | chapterNum, 220 | page: 1, 221 | added: new Date().getTime(), 222 | }); 223 | } 224 | localForage 225 | .setItem('readHistory', value) 226 | .then((value) => console.log(value)) 227 | .catch((err) => console.log(err)); 228 | } 229 | }) 230 | .catch((err) => console.log(err)); 231 | }; 232 | 233 | updateMangaCatalog = (index, completed, manga) => { 234 | const { chapterIndex } = this.state; 235 | localForage 236 | .getItem('offlineManga') 237 | .then((allManga) => { 238 | //add new manga to catalog 239 | const addNewManga = (allManga, currentManga) => { 240 | currentManga.chapters[chapterIndex].currentPage = index; 241 | if (!currentManga.chapters[chapterIndex].completed) 242 | currentManga.chapters[chapterIndex].completed = completed; 243 | allManga.push(currentManga); 244 | localForage 245 | .setItem('offlineManga', allManga) 246 | .then((value) => console.log(value)) 247 | .catch((err) => console.log(err)); 248 | }; 249 | if (allManga !== null) { 250 | let mangaPresent = false; 251 | let mangaIndex; 252 | for (let i = 0; i < allManga.length; i++) { 253 | if (allManga[i].name === manga.name) { 254 | mangaPresent = true; 255 | mangaIndex = i; 256 | break; 257 | } 258 | } 259 | if (!mangaPresent) addNewManga(allManga, manga); 260 | else { 261 | //update manga if its already existing in catalog 262 | allManga[mangaIndex].chapters[chapterIndex].currentPage = index; 263 | if (!allManga[mangaIndex].chapters[chapterIndex].completed) 264 | allManga[mangaIndex].chapters[chapterIndex].completed = completed; 265 | localForage 266 | .setItem('offlineManga', allManga) 267 | .then((value) => console.log(value)) 268 | .catch((err) => console.log(err)); 269 | } 270 | } else { 271 | let allManga = []; 272 | addNewManga(allManga, manga); 273 | } 274 | }) 275 | .catch((err) => console.log(err)); 276 | }; 277 | 278 | updatePageNumber = (index) => { 279 | const { manga } = this.state; 280 | localForage 281 | .getItem('readHistory') 282 | .then((recentManga) => { 283 | if (recentManga !== null) { 284 | for (let i = 0; i < recentManga.length; i++) { 285 | if (recentManga[i].alias === manga.alias) { 286 | recentManga[i].page = index; 287 | } 288 | } 289 | localForage 290 | .setItem('readHistory', recentManga) 291 | .then((value) => console.log(value)) 292 | .catch((err) => console.log(err)); 293 | } 294 | }) 295 | .catch((err) => console.log(err)); 296 | }; 297 | 298 | render() { 299 | Modal.setAppElement('#root'); 300 | const { 301 | background, 302 | chapterNumber, 303 | chapterTitle, 304 | nextChapter, 305 | prevChapter, 306 | chapterImages, 307 | modalColor, 308 | modalBG, 309 | mangaName, 310 | defaultPage, 311 | alias, 312 | } = this.state; 313 | return ( 314 |
315 | 316 | {`${mangaName} - Chapter ${chapterNumber} - MangaHaven`} 317 | 321 | 322 | {!this.state.networkError ? ( 323 |
324 |
331 | this.props.history.push(`/manga/${alias}`)} 333 | /> 334 |
335 |
336 |

{mangaName}

337 |

{`${chapterNumber}${ 338 | chapterTitle !== null ? `: ${chapterTitle}` : '' 339 | }`}

340 |
341 | 342 |
343 |
344 |
351 | 355 | 360 | 364 |
365 |
373 |
e.stopPropagation()} 376 | > 377 |
378 |

View

379 |
380 |
384 | 388 | Horizontal 389 |
390 | 391 |
395 | 399 | Vertical 400 |
401 |
402 |
403 | 404 |
405 |

Background

406 |
407 |
411 | 412 | Light 413 |
414 |
415 | 416 | Dark 417 |
418 |
419 |
420 | 421 |
422 |

Mode

423 |
424 |
425 | 429 | Normal 430 |
431 |
435 | 439 | Webtoon 440 |
441 |
442 |
443 |
444 |
445 | 446 | {this.state.loader ? ( 447 | 448 | ) : ( 449 | 455 | this.setState({ headerActive: !this.state.headerActive }) 456 | } 457 | useKeyboardArrows={true} 458 | emulateTouch={true} 459 | swipeable={true} 460 | selectedItem={defaultPage} 461 | statusFormatter={(current, total) => { 462 | this.calcProgress(current, total); 463 | return `Page ${current - 1} of ${total - 2}`; 464 | }} 465 | onChange={(index) => { 466 | let completed = false; 467 | if (index === chapterImages.length) completed = true; 468 | if (index !== 0 && index !== chapterImages.length + 1) { 469 | this.setState({ currentImage: chapterImages[index - 1] }); 470 | this.updateMangaCatalog(index, completed, this.state.manga); 471 | this.updatePageNumber(index); 472 | } 473 | }} 474 | > 475 | {/* To notify if there's a previous chapter */} 476 |
477 | {this.state.prevChapter !== null ? ( 478 |
479 |

Current:

480 | {`${chapterNumber}${ 481 | chapterTitle !== null ? `: ${chapterTitle}` : '' 482 | }`} 483 | 484 |

Previous:

485 | {`${prevChapter.chapterNum}${ 486 | prevChapter.ChapterName !== null 487 | ? `: ${prevChapter.ChapterName}` 488 | : '' 489 | }`} 490 | 496 |
497 | ) : ( 498 |

There's no previous chapter

499 | )} 500 |
501 | 502 | {chapterImages.map((image, id) => { 503 | return ( 504 | 505 | {({ inView, ref, entry }) => { 506 | return ( 507 |
{ 516 | this.setState({ 517 | modalOpen: true, 518 | headerActive: false, 519 | }); 520 | }} 521 | > 522 | 523 |
524 | ); 525 | }} 526 |
527 | ); 528 | })} 529 | 530 | {/* To notify if there's a next chapter */} 531 |
532 | {this.state.nextChapter !== null ? ( 533 |
534 |

Current:

535 | {`${chapterNumber}${ 536 | chapterTitle !== null ? `: ${chapterTitle}` : '' 537 | }`} 538 | 539 |

Next:

540 | {`${nextChapter.chapterNum}${ 541 | nextChapter.ChapterName !== null 542 | ? `: ${nextChapter.ChapterName}` 543 | : '' 544 | }`} 545 | 551 |
552 | ) : ( 553 |

There's no next chapter

554 | )} 555 |
556 |
557 | )} 558 | {/* modal for displaying images to allow zoom and download */} 559 | { 577 | this.setState({ modalOpen: false }); 578 | }} 579 | > 580 | { 582 | this.setState({ modalOpen: false }); 583 | }} 584 | icon={faTimes} 585 | style={{ 586 | position: 'fixed', 587 | top: '0', 588 | right: '0', 589 | height: '28px', 590 | width: '28px', 591 | margin: '2px 5px 2px 0', 592 | cursor: 'pointer', 593 | }} 594 | /> 595 |
596 | 597 |
598 |
599 |
600 | ) : ( 601 | 602 | )} 603 |
604 | ); 605 | } 606 | } 607 | 608 | export default withRouter(ChapterPage); 609 | --------------------------------------------------------------------------------