MangaDetails component improved drastically

This commit is contained in:
Aria Moradi 2021-03-17 19:17:03 +03:30
parent 149107e749
commit 26cc2f2c96
8 changed files with 230 additions and 55 deletions

View File

@ -85,6 +85,14 @@ export default function CategorySelect(props: IProps) {
<DialogTitle>Set categories</DialogTitle> <DialogTitle>Set categories</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<FormGroup> <FormGroup>
{categoryInfos.length === 0
&& (
<span>
No categories found!
<br />
You should make some from settings.
</span>
)}
{categoryInfos.map((categoryInfo) => ( {categoryInfos.map((categoryInfo) => (
<FormControlLabel <FormControlLabel
control={( control={(

View File

@ -43,7 +43,7 @@ const useStyles = makeStyles({
}); });
interface IProps { interface IProps {
manga: IManga manga: IMangaCard
} }
const MangaCard = React.forwardRef((props: IProps, ref) => { const MangaCard = React.forwardRef((props: IProps, ref) => {
const { const {

View File

@ -2,55 +2,179 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Button, createStyles, makeStyles } from '@material-ui/core'; import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react'; import IconButton from '@material-ui/core/IconButton';
import { Theme } from '@material-ui/core/styles';
import FavoriteIcon from '@material-ui/icons/Favorite';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FilterListIcon from '@material-ui/icons/FilterList';
import PublicIcon from '@material-ui/icons/Public';
import React, { useContext, useEffect, useState } from 'react';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client'; import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import CategorySelect from './CategorySelect'; import CategorySelect from './CategorySelect';
const useStyles = makeStyles(() => createStyles({ const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
root: { root: {
width: '100%',
[theme.breakpoints.up('md')]: {
display: 'flex',
},
},
top: {
padding: '10px',
[theme.breakpoints.up('md')]: {
minWidth: '50%',
},
},
leftRight: {
display: 'flex', display: 'flex',
flexDirection: 'row-reverse', },
leftSide: {
'& img': {
borderRadius: 4,
maxWidth: '100%',
minWidth: '100%',
height: 'auto',
},
maxWidth: '50%',
[theme.breakpoints.up('md')]: {
minWidth: '100px',
},
},
rightSide: {
marginLeft: 15,
maxWidth: '100%',
'& span': {
fontWeight: '400',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
buttons: {
display: 'flex',
justifyContent: 'space-around',
'& button': { '& button': {
marginLeft: 10, color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
},
'& span': {
display: 'block',
fontSize: '0.85em',
},
'& a': {
textDecoration: 'none',
color: '#858585',
'& button': {
color: 'inherit',
},
},
},
bottom: {
paddingLeft: '10px',
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
maxWidth: '50%',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
description: {
'& h4': {
marginTop: '1em',
marginBottom: 0,
},
'& p': {
textAlign: 'justify',
textJustify: 'inter-word',
},
},
genre: {
display: 'flex',
flexWrap: 'wrap',
'& h5': {
border: '2px solid #2196f3',
borderRadius: '1.13em',
marginRight: '1em',
marginTop: 0,
marginBottom: '10px',
padding: '0.3em',
color: '#2196f3',
}, },
}, },
})); }));
interface IProps{ interface IProps{
manga: IManga manga: IManga
source: ISource
} }
function getSourceName(source: ISource) { function getSourceName(source: ISource) {
if (source.name !== null) { return source.name; } if (source.name !== null) {
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
}
return source.id; return source.id;
} }
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function MangaDetails(props: IProps) { export default function MangaDetails(props: IProps) {
const classes = useStyles(); const { setAction } = useContext(NavbarContext);
const { manga, source } = props;
const { manga } = props;
const [inLibrary, setInLibrary] = useState<string>( const [inLibrary, setInLibrary] = useState<string>(
manga.inLibrary ? 'In Library' : 'Not In Library', manga.inLibrary ? 'In Library' : 'Add to Library',
); );
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false); const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
useEffect(() => {
if (inLibrary === 'In Library') {
setAction(
<>
<IconButton
onClick={() => setCategoryDialogOpen(true)}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</>,
);
} else { setAction(<></>); }
}, [inLibrary, categoryDialogOpen]);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles(inLibrary)();
function addToLibrary() { function addToLibrary() {
setInLibrary('adding'); // setInLibrary('adding');
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => { client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('In Library'); setInLibrary('In Library');
}); });
} }
function removeFromLibrary() { function removeFromLibrary() {
setInLibrary('removing'); // setInLibrary('removing');
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => { client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('Not In Library'); setInLibrary('Add To Library');
}); });
} }
function handleButtonClick() { function handleButtonClick() {
if (inLibrary === 'Not In Library') { if (inLibrary === 'Add To Library') {
addToLibrary(); addToLibrary();
} else { } else {
removeFromLibrary(); removeFromLibrary();
@ -58,26 +182,64 @@ export default function MangaDetails(props: IProps) {
} }
return ( return (
<div> <div className={classes.root}>
<h1> <div className={classes.top}>
{manga.title} <div className={classes.leftRight}>
</h1> <div className={classes.leftSide}>
<h3> <img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
Source: </div>
{' '} <div className={classes.rightSide}>
{getSourceName(source)} <h1>
</h3> {manga.title}
<div className={classes.root}> </h1>
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button> <h3>
{inLibrary === 'In Library' Author:
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>} {' '}
<span>{getValueOrUnknown(manga.author)}</span>
</h3>
<h3>
Artist:
{' '}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>
Status:
{' '}
{manga.status}
</h3>
<h3>
Source:
{' '}
{getSourceName(manga.source)}
</h3>
</div>
</div>
<div className={classes.buttons}>
<div>
<IconButton onClick={() => handleButtonClick()}>
{inLibrary === 'In Library' && <FavoriteIcon />}
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
<span>{inLibrary}</span>
</IconButton>
</div>
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
<a href={manga.url} target="_blank">
<IconButton>
<PublicIcon />
<span>Open Site</span>
</IconButton>
</a>
</div>
</div>
<div className={classes.bottom}>
<div className={classes.description}>
<h4>About</h4>
<p>{manga.description}</p>
</div>
<div className={classes.genre}>
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
</div>
</div> </div>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</div> </div>
); );
} }

View File

@ -7,7 +7,7 @@ import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard'; import MangaCard from './MangaCard';
interface IProps{ interface IProps{
mangas: IManga[] mangas: IMangaCard[]
message?: string message?: string
hasNextPage: boolean hasNextPage: boolean
lastPageNum: number lastPageNum: number
@ -48,7 +48,7 @@ export default function MangaGrid(props: IProps) {
} }
return ( return (
<Grid container spacing={1} style={{ margin: 0, padding: '5px' }}> <Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
{mapped} {mapped}
</Grid> </Grid>
); );

View File

@ -10,13 +10,12 @@ import NavbarContext from '../context/NavbarContext';
import client from '../util/client'; import client from '../util/client';
export default function Manga() { export default function Manga() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []); useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
const { id } = useParams<{id: string}>(); const { id } = useParams<{id: string}>();
const [manga, setManga] = useState<IManga>(); const [manga, setManga] = useState<IManga>();
const [source, setSource] = useState<ISource>();
const [chapters, setChapters] = useState<IChapter[]>([]); const [chapters, setChapters] = useState<IChapter[]>([]);
useEffect(() => { useEffect(() => {
@ -28,16 +27,6 @@ export default function Manga() {
}); });
}, []); }, []);
useEffect(() => {
if (manga !== undefined) {
client.get(`/api/v1/source/${manga.sourceId}`)
.then((response) => response.data)
.then((data: ISource) => {
setSource(data);
});
}
}, [manga]);
useEffect(() => { useEffect(() => {
client.get(`/api/v1/manga/${id}/chapters`) client.get(`/api/v1/manga/${id}/chapters`)
.then((response) => response.data) .then((response) => response.data)
@ -52,7 +41,7 @@ export default function Manga() {
return ( return (
<> <>
{(manga && source) && <MangaDetails manga={manga} source={source} />} {manga && <MangaDetails manga={manga} />}
{chapterCards} {chapterCards}
</> </>
); );

View File

@ -27,7 +27,7 @@ export default function SearchSingle() {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const classes = useStyles(); const classes = useStyles();
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [searchTerm, setSearchTerm] = useState<string>(''); const [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false); const [hasNextPage, setHasNextPage] = useState<boolean>(false);

View File

@ -13,7 +13,7 @@ export default function SourceMangas(props: { popular: boolean }) {
useEffect(() => { setTitle('Source'); setAction(<></>); }, []); useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [hasNextPage, setHasNextPage] = useState<boolean>(false); const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1); const [lastPageNum, setLastPageNum] = useState<number>(1);

View File

@ -21,12 +21,28 @@ interface ISource {
history: any history: any
} }
interface IManga { interface IMangaCard {
id: number id: number
sourceId?: string
title: string title: string
thumbnailUrl: string thumbnailUrl: string
inLibrary?: boolean }
interface IManga {
id: number
sourceId: string
url: string
title: string
thumbnailUrl: string
artist: string
author: string
description: string
genre: string
status: string
inLibrary: boolean
source: ISource
} }
interface IChapter { interface IChapter {