many new
This commit is contained in:
Executable
+172
@@ -0,0 +1,172 @@
|
||||
@import "../../Constants"
|
||||
|
||||
.Novel
|
||||
.Novel__Main
|
||||
display: flex
|
||||
padding: $primaryPadding
|
||||
background-color: var(--sec-bg-color)
|
||||
border-radius: $borderRadius
|
||||
|
||||
@media (max-width: 600px)
|
||||
flex-direction: column
|
||||
|
||||
&_Left
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.Novel__Main_Image
|
||||
max-width: 350px
|
||||
max-height: 200px
|
||||
margin-right: .5rem
|
||||
border-radius: $borderRadius
|
||||
|
||||
.Novel__Main_Status
|
||||
margin-bottom: 1rem
|
||||
|
||||
&_Mark
|
||||
margin-top: .5rem
|
||||
width: 100%
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
|
||||
.Selected
|
||||
background-color: var(--block-sec-color)
|
||||
|
||||
&_Element
|
||||
padding: .5rem
|
||||
background-color: var(--block-color)
|
||||
width: 100%
|
||||
text-align: center
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
background-color: var(--sec-bg-color)
|
||||
|
||||
&_Primary
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
background-color: var(--block-color)
|
||||
font-size: 1rem
|
||||
margin-top: .5rem
|
||||
padding: .25rem
|
||||
|
||||
&_Extended
|
||||
background-color: var(--block-color)
|
||||
font-size: 1rem
|
||||
|
||||
.Novel__Main_Status_Element
|
||||
padding: .25rem
|
||||
border-top: var(--sec-bg-color) 2px solid
|
||||
|
||||
.Novel__Main_Status_Element
|
||||
cursor: pointer
|
||||
|
||||
.Novel__Info_Title
|
||||
@media (max-width: 600px)
|
||||
margin-top: 1rem
|
||||
|
||||
font-size: 1.7rem
|
||||
font-weight: 600
|
||||
margin-bottom: 1rem
|
||||
|
||||
.Novel__Info_Description
|
||||
margin-top: .5rem
|
||||
@media (min-width: 600px)
|
||||
margin-left: 1rem
|
||||
font-size: 1rem
|
||||
|
||||
.Novel__Info_Genres
|
||||
display: flex
|
||||
align-items: center
|
||||
font-size: 1rem
|
||||
|
||||
.Genre
|
||||
margin: .2rem
|
||||
border: #992222 1px solid
|
||||
padding: .1rem
|
||||
border-radius: 3px
|
||||
font-weight: 300
|
||||
|
||||
.Novel__Comments
|
||||
margin-top: 1rem
|
||||
padding: $primaryPadding
|
||||
background-color: var(--sec-bg-color)
|
||||
border-radius: $borderRadius
|
||||
|
||||
form
|
||||
width: 100%
|
||||
margin-bottom: .5rem
|
||||
|
||||
.Novel__Comments_BBCodes
|
||||
font-size: 1.3rem
|
||||
margin-bottom: .2rem
|
||||
|
||||
svg
|
||||
cursor: pointer
|
||||
|
||||
svg:first-child
|
||||
padding-right: .5rem
|
||||
|
||||
svg:last-child
|
||||
padding-left: .5rem
|
||||
|
||||
svg:not(:first-child):not(:last-child)
|
||||
padding: 0 .5rem
|
||||
|
||||
.Novel__Comments_TextArea
|
||||
width: 100%
|
||||
background-color: var(--block-color)
|
||||
font-size: 1rem
|
||||
padding: .25rem
|
||||
border: none
|
||||
border-radius: $borderRadius
|
||||
box-sizing: border-box
|
||||
min-height: 4rem
|
||||
resize: none
|
||||
margin-bottom: .5rem
|
||||
|
||||
.Novel__Comments_Submit
|
||||
background-color: var(--block-color) !important
|
||||
border-radius: $borderRadius
|
||||
border: none
|
||||
font-size: 1rem
|
||||
padding: .5rem
|
||||
|
||||
.Novel__Characters
|
||||
margin-top: 1rem
|
||||
padding: $primaryPadding 0
|
||||
background-color: var(--sec-bg-color)
|
||||
border-radius: $borderRadius
|
||||
|
||||
.Novel__Characters_Title
|
||||
margin-left: .5rem
|
||||
font-size: 1.2rem
|
||||
font-weight: bold
|
||||
|
||||
.Novel__Characters_List
|
||||
width: 100%
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
|
||||
@media (max-width: 600px)
|
||||
justify-content: center
|
||||
|
||||
.Novel__Character
|
||||
box-sizing: border-box
|
||||
width: 150px
|
||||
height: 250px
|
||||
border-radius: $borderRadius
|
||||
background-color: var(--block-color)
|
||||
margin: 0 .5rem
|
||||
padding-bottom: .2rem
|
||||
|
||||
.Novel__Character_Name
|
||||
text-align: center
|
||||
|
||||
.Character_Image
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
width: 100%
|
||||
height: calc(100% - 1.5rem)
|
||||
background-size: cover
|
||||
background-position: center
|
||||
margin-bottom: .2rem
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
import React from "react";
|
||||
import './Novel.sass'
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import {connect} from "react-redux";
|
||||
import {
|
||||
fetchNovel,
|
||||
fetchNovelCharacters,
|
||||
fetchNovelComments,
|
||||
fetchUserNovel,
|
||||
updateUserNovel,
|
||||
deleteUserNovel,
|
||||
fetchNovelGenres, postNovelComment, createUserNovel
|
||||
} from "../../api/novels";
|
||||
import {Link} from "react-router-dom";
|
||||
import Loading from "../../ui/Loading";
|
||||
import { TranslateStatus } from "../../api/api";
|
||||
import Comments from "../Comments/Comments";
|
||||
|
||||
type Props = {
|
||||
currentUser: UserType
|
||||
match: { params: { novelId: number } }
|
||||
};
|
||||
|
||||
type State = {
|
||||
novel: NovelType | null
|
||||
characters: CharacterType[]
|
||||
genres: GenreType[]
|
||||
|
||||
errorCode: number | null
|
||||
userData: UserNovelType | null
|
||||
statusExpanded: boolean
|
||||
|
||||
comments: CommentType[]
|
||||
commentsOffset: number
|
||||
hasMoreComments: boolean
|
||||
}
|
||||
|
||||
class Novel extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
novel: null, characters: [], genres: [],
|
||||
errorCode: null, userData: null, statusExpanded: false,
|
||||
comments: [], commentsOffset: 5, hasMoreComments: true
|
||||
};
|
||||
|
||||
updateNovelStatus = (status: string) => {
|
||||
const { match: { params } , currentUser } = this.props;
|
||||
const { userData } = this.state;
|
||||
|
||||
if(userData) {
|
||||
updateUserNovel(currentUser.id, params.novelId, { status }).then(r => {
|
||||
this.setState({ userData: r, statusExpanded: false })
|
||||
}).catch(console.error);
|
||||
} else {
|
||||
createUserNovel(currentUser.id, params.novelId, status).then(r => {
|
||||
this.setState({ userData: r, statusExpanded: false })
|
||||
}).catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
deleteNovelStatus = () => {
|
||||
const { match: { params } , currentUser } = this.props;
|
||||
|
||||
deleteUserNovel(currentUser.id, params.novelId).then(() => {
|
||||
this.setState({ userData: null, statusExpanded: false });
|
||||
});
|
||||
};
|
||||
|
||||
sendComment = (text: string) => {
|
||||
const { match: { params: { novelId } } } = this.props;
|
||||
postNovelComment(novelId, text).then((comment: CommentType) => {
|
||||
const { comments } = this.state;
|
||||
this.setState({ comments: [comment, ...comments] });
|
||||
}).catch(console.error);
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
const { match: { params } } = this.props;
|
||||
let { comments, commentsOffset } = this.state;
|
||||
|
||||
fetchNovelComments(params.novelId, commentsOffset, 10).then(r => {
|
||||
const hasMoreComments = r.count <= commentsOffset;
|
||||
commentsOffset += 5;
|
||||
this.setState({ comments: [...r.comments.reverse(), ...comments], hasMoreComments, commentsOffset })
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: any) {
|
||||
const { match: { params }, currentUser } = this.props;
|
||||
const { currentUser: prevUser } = prevProps;
|
||||
|
||||
if(!prevUser.authorized && currentUser.authorized)
|
||||
fetchUserNovel(currentUser.id, params.novelId)
|
||||
.then(data => this.setState({ userData: data }))
|
||||
.catch(err => console.error(err));
|
||||
else if(prevUser.authorized && !currentUser.authorized)
|
||||
this.setState({ userData: null })
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { match: { params }, currentUser } = this.props;
|
||||
|
||||
fetchNovel(params.novelId).then((novel: NovelType) => {
|
||||
novel.release_date = new Date(novel.release_date);
|
||||
this.setState({ novel });
|
||||
document.title = novel.localized_name ? `${novel.original_name} / ${novel.localized_name}` : novel.original_name;
|
||||
|
||||
fetchNovelComments(params.novelId).then(r => {
|
||||
const hasMoreComments = r.count > 5;
|
||||
this.setState({ comments: r.comments.reverse(), hasMoreComments })
|
||||
});
|
||||
fetchNovelCharacters(params.novelId).then(characters => this.setState({ characters }));
|
||||
fetchNovelGenres(params.novelId).then(genres => this.setState({ genres }));
|
||||
}).catch((err: ApiError) => {
|
||||
this.setState({ errorCode: err.code });
|
||||
});
|
||||
|
||||
if(currentUser.authorized)
|
||||
fetchUserNovel(currentUser.id, params.novelId).then((userData: UserNovelType) => {
|
||||
this.setState({ userData });
|
||||
}).catch((err: ApiError) => {
|
||||
if(err.code !== 100) console.error(err.text);
|
||||
});
|
||||
}
|
||||
|
||||
updateNovelMark = (mark: number) => {
|
||||
const { match: { params }, currentUser } = this.props;
|
||||
const { userData } = this.state;
|
||||
if(userData?.mark === mark) return;
|
||||
updateUserNovel(currentUser.id, params.novelId, { mark }).then(res => {
|
||||
userData!!.mark = mark;
|
||||
this.setState({ userData })
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorCode, novel, comments, characters, genres, userData, statusExpanded, hasMoreComments } = this.state;
|
||||
const { currentUser } = this.props;
|
||||
|
||||
if(errorCode === 100) return <NotFoundError/>;
|
||||
if(!novel) return <Loading/>;
|
||||
return (
|
||||
<div className={'Novel'}>
|
||||
<div className="Novel__Main">
|
||||
<div className={'Novel__Main_Left'}>
|
||||
{novel.image
|
||||
? <img src={novel.image} className={"Novel__Main_Image"} alt={novel.original_name}/>
|
||||
: <img src={"/static/img/no-image.png"} className={"Novel__Main_Image"} alt=""/>}
|
||||
{currentUser.authorized &&
|
||||
<div className={'Novel__Main_Status'}>
|
||||
<div className="Novel__Main_Status_Primary">
|
||||
{!userData
|
||||
? <div className={'Novel__Main_Status_Element'} onClick={() => this.updateNovelStatus('planned')}>Добавить в список</div>
|
||||
: <div className={"Novel__Main_Status_Element"}>{TranslateStatus[userData.status]}</div>}
|
||||
{userData && (
|
||||
<div
|
||||
className={'Novel__Main_Status_Element'}
|
||||
onClick={() => this.setState({ statusExpanded: !statusExpanded })}
|
||||
style={{ paddingLeft: 5, borderLeft: '1px solid white' }}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{statusExpanded &&
|
||||
<div className="Novel__Main_Status_Extended">
|
||||
{userData && Object.entries(TranslateStatus).filter(([eng]) => eng !== userData.status).map(([eng, rus]) => (
|
||||
<div key={eng} className="Novel__Main_Status_Element" onClick={() => this.updateNovelStatus(eng)}>{rus}</div>
|
||||
))}
|
||||
<div className={"Novel__Main_Status_Element"} style={{ color: 'red' }} onClick={this.deleteNovelStatus}>Удалить из списка</div>
|
||||
</div>}
|
||||
<div className={'Novel__Main_Status_Mark'}>
|
||||
{userData && [0,1,2,3,4,5,6,7,8,9,10].map(mark => (
|
||||
<div
|
||||
key={mark}
|
||||
className={mark === userData.mark && userData.mark !== 0 ? 'Novel__Main_Status_Mark_Element Selected' : 'Novel__Main_Status_Mark_Element'}
|
||||
onClick={() => this.updateNovelMark(mark)}
|
||||
>
|
||||
{mark}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
<div className={'Novel__Info'}>
|
||||
<div className={'Novel__Info_Title'}>{novel.localized_name || novel.original_name}</div>
|
||||
{novel.localized_name && <div className="Novel__Info_OriginalName">
|
||||
<strong>Оригинальное название</strong>: {novel.original_name}
|
||||
</div>}
|
||||
{genres && genres.length > 0 &&
|
||||
<div className="Novel__Info_Genres">
|
||||
<strong>Жанры</strong>: {genres.map(genre => <div className={'Genre'} key={genre.id}>{genre.localized_name}</div>)}
|
||||
</div>}
|
||||
<div>
|
||||
<strong>Год выхода</strong>: {novel.release_date.toLocaleDateString()}
|
||||
</div>
|
||||
<div className="Novel__Info_Rating"><strong>Ср. оценка</strong>: {novel.rating.toFixed(2)}</div>
|
||||
<pre className={'Novel__Info_Description'}>{novel.description}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{characters && characters.length > 0 &&
|
||||
<div className="Novel__Characters">
|
||||
<div className="Novel__Characters_Title">Главные герои:</div>
|
||||
<div className="Novel__Characters_List">
|
||||
{this.state.characters.filter(char => char.main).map(char =>
|
||||
<div className={'Novel__Character'} key={char.id}>
|
||||
<div className='Character_Image' style={{ backgroundImage: `url(${char.image})`}}/>
|
||||
<Link to={`/character/${char.id}`}><div className={'Novel__Character_Name'}>{char.localized_name}</div></Link>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>}
|
||||
<Comments user={currentUser} comments={comments} sendComment={this.sendComment} loadMore={this.loadMoreComments} hasMore={hasMoreComments} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
currentUser: state.currentUser
|
||||
})
|
||||
)(Novel);
|
||||
Reference in New Issue
Block a user