├── .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 |
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 |
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 |
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 |
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 |
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 |
10 | {view !== classname ? (
11 |
12 | ) : (
13 |
14 | )}
15 |
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 |
15 |
16 | Try again
17 |
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 |
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 | 
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 |
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 |
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 | You need to enable JavaScript to run this app.
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 |
73 | this.props.history.push(
74 | `/read/${manga.alias}/chapter/${manga.chapterNum}?q=${manga.page}`
75 | )
76 | }
77 | >
78 | RESUME
79 |
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 |
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 |
55 | Close
56 |
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 |
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 |
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 |
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 |
149 |
150 |
151 |
Webtoon
152 |
153 |
154 |
155 |
156 |
Background:
157 |
161 |
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 |
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 |
58 | Cancel
59 |
60 | {
63 | localForage
64 | .removeItem('user')
65 | .then((value) => {
66 | onClose();
67 | window.location.reload();
68 | })
69 | .catch((err) => console.log(err));
70 | }}
71 | >
72 | Log out
73 |
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 |
209 |
210 | Logout
211 |
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 |
this.props.history.push('/edit-profile')}
137 | >
138 | Edit profile
139 |
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 |
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 |
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 |
494 | {`Read Chapter ${prevChapter.chapterNum}`}
495 |
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 |
549 | {`Read Chapter ${nextChapter[0]}`}
550 |
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 |
--------------------------------------------------------------------------------