Many fixes
This commit is contained in:
Generated
+44
@@ -1857,6 +1857,15 @@
|
||||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.4.tgz",
|
||||
"integrity": "sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||
@@ -4649,6 +4658,30 @@
|
||||
"utila": "~0.4"
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
|
||||
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^2.6.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
|
||||
"integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
|
||||
@@ -12456,6 +12489,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz",
|
||||
"integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"react-recaptcha": "^2.3.10",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"redux": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -26,6 +27,7 @@
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"@types/react-transition-group": "^4.2.4",
|
||||
"node-sass": "^4.13.1",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"typescript": "^3.8.2",
|
||||
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
import {get, post} from "./api";
|
||||
import {get, post, put} from "./api";
|
||||
|
||||
export const fetchUsers = () => get('/users');
|
||||
|
||||
export const fetchUser = (id: number) => get(`users/${id}`);
|
||||
export const updateUser = (user: UserType) => put(`users/${user.id}`, { user })
|
||||
|
||||
export const fetchCurrentUser = () => get('users/current');
|
||||
export const fetchUserNovels = (id: number) => get(`users/${id}/novels`);
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
@import "../Constants"
|
||||
|
||||
.AdminPanel
|
||||
background-color: var(--sec-bg-color)
|
||||
padding: .5rem
|
||||
|
||||
&__Title
|
||||
font-size: 1.5rem
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
|
||||
&__Router
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
border-bottom: 1px solid white
|
||||
|
||||
a
|
||||
margin: 0 .2rem
|
||||
padding: .1rem
|
||||
@@ -0,0 +1,61 @@
|
||||
import {connect} from "react-redux";
|
||||
import React from "react";
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import {hasAccessToAdminPanel} from "../../utils";
|
||||
import './AdminPanel.sass';
|
||||
import {Link} from "react-router-dom";
|
||||
import AdminPanelUsers from "./Users/AdminPanelUsers";
|
||||
import AdminPanelNovels from "./Novels/AdminPanelNovels";
|
||||
import ModifyUser from "./Users/ModifyUser";
|
||||
|
||||
type Props = {
|
||||
user: UserType
|
||||
match: { path: string }
|
||||
location: { pathname: string }
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
||||
}
|
||||
|
||||
class AdminPanel extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { user, match: { path }, location: { pathname } } = this.props;
|
||||
if(!user || !user.group) return <NotFoundError/>;
|
||||
if(!hasAccessToAdminPanel(user)) return <NotFoundError/>;
|
||||
const subPath = pathname.slice(path.length+1);
|
||||
let component;
|
||||
switch(subPath) {
|
||||
case 'users': component = <AdminPanelUsers path={`${path}/${subPath}`}/>; break;
|
||||
case 'novels': component = <AdminPanelNovels/>; break;
|
||||
case 'news': component = <div>Новости</div>; break;
|
||||
default: {
|
||||
const users = /users\/(\d+)/;
|
||||
|
||||
const userMatch = subPath.match(users);
|
||||
|
||||
if(userMatch)
|
||||
component = <ModifyUser userId={parseInt(userMatch[1])}/>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'AdminPanel'}>
|
||||
<div className="AdminPanel__Title">Админ панель <span style={{ color: 'var(--error-bg-color)' }}>(в разработке)</span></div>
|
||||
<div className="AdminPanel__Router">
|
||||
<Link to={path}>Главная</Link>
|
||||
<Link to={`${path}/users`}>Пользователи</Link>
|
||||
<Link to={`${path}/novels`}>Новелы</Link>
|
||||
<Link to={`${path}/news`}>Новости</Link>
|
||||
</div>
|
||||
{component}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(AdminPanel);
|
||||
@@ -1,30 +0,0 @@
|
||||
import {connect} from "react-redux";
|
||||
import React from "react";
|
||||
import NotFoundError from "../../NotFoundError";
|
||||
import {hasPermission} from "../../../utils";
|
||||
|
||||
type Props = {
|
||||
user: UserType
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
||||
}
|
||||
|
||||
class AdminPanel extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
if(!user || !user.group) return <NotFoundError/>;
|
||||
if(!user.is_superuser && !user.group.is_superuser && !hasPermission(user, 'admin_panel')) return <NotFoundError/>;
|
||||
|
||||
return (
|
||||
<div>Админ панель, ёпта</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(AdminPanel);
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
type Props = {
|
||||
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
};
|
||||
|
||||
class AdminPanelNovels extends React.Component<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<div>Novels</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(AdminPanelNovels);
|
||||
@@ -0,0 +1,27 @@
|
||||
.APUser__Modify
|
||||
&_Title
|
||||
text-align: center
|
||||
font-weight: 600
|
||||
font-size: 1.5rem
|
||||
|
||||
&_Form
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
button
|
||||
font-size: 1.5rem
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-wrap: nowrap
|
||||
white-space: nowrap
|
||||
align-items: center
|
||||
font-size: 1.2rem
|
||||
|
||||
input
|
||||
margin: .5rem 0 .5rem .5rem
|
||||
font-size: 1.2rem
|
||||
width: 100%
|
||||
|
||||
&[type='checkbox']
|
||||
width: 0
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {fetchUsers} from "../../../api/users";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
path: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
users: UserType[]
|
||||
};
|
||||
|
||||
class AdminPanelUsers extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
users: []
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetchUsers().then(users => this.setState({ users }))
|
||||
}
|
||||
|
||||
render() {
|
||||
const { users } = this.state;
|
||||
const { path } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ textAlign: "center", fontWeight: 600, fontSize: '1.5rem' }}>Пользователи</div>
|
||||
<div>
|
||||
{users.sort((a, b) => a.id - b.id).map(user => (
|
||||
<Link to={`${path}/${user.id}`} key={user.id}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{user.id}</div>
|
||||
<div>{user.username}</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(AdminPanelUsers);
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, {ChangeEvent, FormEvent} from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {fetchUser, updateUser} from "../../../api/users";
|
||||
import Loading from "../../Loading";
|
||||
import './APUser.sass';
|
||||
import {addNotification} from "../../../store/actions";
|
||||
import Notification from "../../Notification/Notification";
|
||||
|
||||
type Props = {
|
||||
userId: number
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
user: UserType | null
|
||||
username: string
|
||||
};
|
||||
|
||||
class ModifyUser extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
user: null, username: ''
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { userId } = this.props;
|
||||
fetchUser(userId).then((user: UserType) => {
|
||||
this.setState({ user, username: user.username });
|
||||
document.title = `${user.username} / Админ-панель`;
|
||||
})
|
||||
}
|
||||
|
||||
onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const user = Object.assign({}, this.state.user);
|
||||
const target = event.target;
|
||||
// @ts-ignore
|
||||
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||
this.setState({ user })
|
||||
}
|
||||
|
||||
onSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const { user } = this.state;
|
||||
const { addNotification } = this.props;
|
||||
|
||||
if(user === null) {
|
||||
console.error('Пользователь - null!');
|
||||
return;
|
||||
}
|
||||
updateUser(user).then(() => {
|
||||
const notification = (
|
||||
<Notification level={"success"}>
|
||||
Пользователь успешно изменен!
|
||||
</Notification>
|
||||
)
|
||||
addNotification(notification)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, username } = this.state;
|
||||
|
||||
if(!user) return <Loading/>;
|
||||
return (
|
||||
<div className={'APUser__Modify'}>
|
||||
<div className={'APUser__Modify_Title'}>{username}</div>
|
||||
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
|
||||
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'} value={user.username}/></label>
|
||||
<label>Email: <input type='email' onChange={this.onChange} name={'email'} value={user.email}/></label>
|
||||
<label>Аватар: <input type='text' onChange={this.onChange} name={'avatar'} value={user.avatar}/></label>
|
||||
|
||||
<label>Суперюзер: <input type='checkbox' onChange={this.onChange} name={'is_superuser'} checked={user.is_superuser}/></label>
|
||||
<label>Активирован: <input type='checkbox' onChange={this.onChange} name={'is_activated'} checked={user.is_activated}/></label>
|
||||
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<button>Сохранить!</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
}),
|
||||
(dispatch) => ({
|
||||
addNotification: (notification: React.ReactNode) => {
|
||||
dispatch(addNotification(notification))
|
||||
}
|
||||
})
|
||||
)(ModifyUser);
|
||||
@@ -5,11 +5,14 @@ html, body, #root
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
body, pre, textarea
|
||||
font-family: 'Roboto', sans-serif
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p, body, pre
|
||||
margin: 0
|
||||
|
||||
body
|
||||
margin: 0
|
||||
padding: 0
|
||||
font-family: 'Roboto', sans-serif
|
||||
|
||||
--main-color: #{$mainColor}
|
||||
--sec-color: #{$secondaryColor}
|
||||
@@ -28,8 +31,6 @@ body
|
||||
background: var(--main-bg-color)
|
||||
|
||||
pre
|
||||
font-family: 'Roboto', sans-serif
|
||||
margin: 0
|
||||
white-space: pre-wrap
|
||||
|
||||
body[theme='red']
|
||||
@@ -64,6 +65,9 @@ body[theme='light']
|
||||
--block-sec-color: #{$blockSecondaryBackgroundColorLight}
|
||||
--nav-bg-color: #{$navbarBackgroundColorLight}
|
||||
|
||||
color: var(--main-color)
|
||||
background: var(--main-bg-color)
|
||||
|
||||
a
|
||||
color: var(--main-color)
|
||||
text-decoration: none
|
||||
@@ -74,8 +78,12 @@ a
|
||||
input, button, textarea
|
||||
color: var(--main-color)
|
||||
|
||||
&:disabled
|
||||
color: var(--sec-color)
|
||||
|
||||
button
|
||||
cursor: pointer
|
||||
padding: .2rem
|
||||
border-radius: $borderRadius
|
||||
background-color: var(--block-color)
|
||||
border: 1px solid var(--main-color)
|
||||
@@ -97,7 +105,7 @@ input
|
||||
|
||||
.Container
|
||||
max-width: 1200px
|
||||
min-height: calc( 100% - 4rem )
|
||||
min-height: calc(100% - 4rem)
|
||||
margin: .5rem auto
|
||||
|
||||
.Notifications
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
import {Switch, Route} from "react-router-dom";
|
||||
import './App.sass';
|
||||
import Navbar from "../Navbar/Navbar";
|
||||
import Novel from "../Novel/Novel";
|
||||
@@ -22,7 +22,7 @@ import ColorTag from "../Tags/ColorTag";
|
||||
import ActivateAccount from "../ActivateAccount";
|
||||
import AllNews from "../News/AllNews";
|
||||
import Post from "../News/Post";
|
||||
import AdminPanel from "../AdminPanel/Main/AdminPanel";
|
||||
import AdminPanel from "../AdminPanel/AdminPanel";
|
||||
|
||||
type Props = {
|
||||
notifications: React.ReactNode[]
|
||||
@@ -47,7 +47,9 @@ class App extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
const { setUser } = this.props;
|
||||
fetchCurrentUser().then(currentUser => setUser(currentUser)).catch(console.error);
|
||||
fetchCurrentUser().then(currentUser => setUser(currentUser)).catch((err: ApiError) => {
|
||||
if(err.code !== 3) console.error(err.text);
|
||||
});
|
||||
|
||||
const theme = localStorage.getItem('theme');
|
||||
if(theme)
|
||||
@@ -91,7 +93,9 @@ class App extends React.Component<Props, State> {
|
||||
|
||||
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
||||
|
||||
<Route exact path={'/kawaii__neko'} component={AdminPanel}/>
|
||||
<Route path={'/kawaii__neko'} component={AdminPanel}/>
|
||||
{/*<Route exact path={'/kawaii__neko/users'} component={AdminPanelUsers}/>*/}
|
||||
|
||||
<Route component={NotFoundError}/>
|
||||
</Switch>
|
||||
|
||||
@@ -105,7 +109,7 @@ class App extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: { notifications: React.ReactNode[] }) => ({
|
||||
(state: StoreState) => ({
|
||||
notifications: state.notifications
|
||||
}),
|
||||
dispatch => ({
|
||||
|
||||
@@ -9,18 +9,10 @@ type Props = {
|
||||
user: UserType | undefined
|
||||
text: string
|
||||
}
|
||||
type State = {
|
||||
user: UserType | null
|
||||
}
|
||||
|
||||
class Comment extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
user: null
|
||||
};
|
||||
|
||||
class Comment extends React.Component<Props> {
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const { text } = this.props;
|
||||
const { user, text } = this.props;
|
||||
if(!user) return <Loading/>;
|
||||
|
||||
return (user &&
|
||||
|
||||
@@ -5,6 +5,9 @@ import Comment from './Comment';
|
||||
import './Comments.sass';
|
||||
import Loading from "../Loading";
|
||||
import {fetchUser} from "../../api/users";
|
||||
import {connect} from "react-redux";
|
||||
import {addNotification} from "../../store/actions";
|
||||
import Notification from "../Notification/Notification";
|
||||
|
||||
type Props = {
|
||||
comments: CommentType[]
|
||||
@@ -12,6 +15,7 @@ type Props = {
|
||||
user: UserType
|
||||
loadMore: () => void
|
||||
sendComment: (text: string) => void
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -41,35 +45,43 @@ class Comments extends React.Component<Props, State> {
|
||||
|
||||
sendComment = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const { sendComment } = this.props;
|
||||
const { sendComment, addNotification } = this.props;
|
||||
const { commentText } = this.state;
|
||||
|
||||
if(commentText === '')
|
||||
console.log('empty');
|
||||
else {
|
||||
if(commentText === '') {
|
||||
const notification = (
|
||||
<Notification level={"error"}>
|
||||
Пустой комментарий
|
||||
</Notification>
|
||||
);
|
||||
addNotification(notification);
|
||||
} else {
|
||||
sendComment(commentText);
|
||||
this.setState({ commentText: '' });
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>) {
|
||||
if(prevProps.comments.length !== this.props.comments.length) {
|
||||
let map: Map<number, UserType> = new Map();
|
||||
this.props.comments.forEach(comment => {
|
||||
if(!map.has(comment.user_id)) {
|
||||
fetchUser(comment.user_id).then(user => {
|
||||
map = map.set(comment.user_id, user)
|
||||
});
|
||||
const { comments } = this.props;
|
||||
const { comments: prevComments } = prevProps;
|
||||
|
||||
if(prevComments.length !== comments.length) {
|
||||
let { users } = this.state;
|
||||
|
||||
comments.forEach(async comment => {
|
||||
if(!users.has(comment.user_id)) {
|
||||
const user = await fetchUser(comment.user_id);
|
||||
users = users.set(comment.user_id, user);
|
||||
this.setState({ users });
|
||||
}
|
||||
});
|
||||
console.log(map)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, comments, hasMore, loadMore } = this.props;
|
||||
const { users } = this.state;
|
||||
if(!user || !comments) return <Loading/>;
|
||||
if(!user) return <Loading/>;
|
||||
|
||||
return (
|
||||
<div className="Comments">
|
||||
@@ -91,10 +103,16 @@ class Comments extends React.Component<Props, State> {
|
||||
ref={ref => this.input = ref}
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<button type="submit" className={'Comments_Submit'}>Написать</button>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<button type="submit" className={'Comments_Submit'}>Написать</button>
|
||||
<div style={{ marginLeft: '.5rem', color: '#bb3333' }}>Комментарии в разработке!</div>
|
||||
</div>
|
||||
{hasMore && <button type={"button"} onClick={loadMore}>Загрузить ещё</button>}
|
||||
</div>
|
||||
</form>}
|
||||
{!comments && (
|
||||
<Loading/>
|
||||
)}
|
||||
{comments && comments.length > 0 ? (
|
||||
comments.map(comment => <Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)} />)
|
||||
) : (
|
||||
@@ -105,4 +123,10 @@ class Comments extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default Comments;
|
||||
export default connect(null,
|
||||
dispatch => ({
|
||||
addNotification: (notification: React.ReactNode) => {
|
||||
dispatch(addNotification(notification))
|
||||
}
|
||||
})
|
||||
)(Comments);
|
||||
@@ -1,32 +1,38 @@
|
||||
@keyframes dissolve
|
||||
0%
|
||||
opacity: 1
|
||||
100%
|
||||
opacity: 0
|
||||
|
||||
$mainColor: #efefef
|
||||
$secondaryColor: #aaaaaa
|
||||
$linkHoverColor: #dddddd
|
||||
|
||||
// BLACK
|
||||
$primaryBackgroundColor: #121212
|
||||
$secondaryBackgroundColor: #212121
|
||||
$blockBackgroundColor: #191919
|
||||
$secondaryBackgroundColor: #191919
|
||||
$blockBackgroundColor: #212121
|
||||
$blockSecondaryBackgroundColor: #121212
|
||||
$navbarBackgroundColor: #0b0b0b
|
||||
|
||||
// BLUE
|
||||
$primaryBackgroundColorBlue: #121222
|
||||
$secondaryBackgroundColorBlue: #212131
|
||||
$blockBackgroundColorBlue: #191929
|
||||
$secondaryBackgroundColorBlue: #191929
|
||||
$blockBackgroundColorBlue: #212131
|
||||
$blockSecondaryBackgroundColorBlue: #121222
|
||||
$navbarBackgroundColorBlue: #0b0b1a
|
||||
|
||||
// RED
|
||||
$primaryBackgroundColorRed: #221212
|
||||
$secondaryBackgroundColorRed: #312121
|
||||
$blockBackgroundColorRed: #291919
|
||||
$secondaryBackgroundColorRed: #291919
|
||||
$blockBackgroundColorRed: #312121
|
||||
$blockSecondaryBackgroundColorRed: #221212
|
||||
$navbarBackgroundColorRed: #2a0b0b
|
||||
|
||||
// GREEN
|
||||
$primaryBackgroundColorGreen: #122212
|
||||
$secondaryBackgroundColorGreen: #213121
|
||||
$blockBackgroundColorGreen: #192919
|
||||
$secondaryBackgroundColorGreen: #192919
|
||||
$blockBackgroundColorGreen: #213121
|
||||
$blockSecondaryBackgroundColorGreen: #122212
|
||||
$navbarBackgroundColorGreen: #0b2a0b
|
||||
|
||||
@@ -35,9 +41,9 @@ $mainColorLight: #121212
|
||||
$secondaryColorLight: #444444
|
||||
$linkHoverColorLight: #333333
|
||||
|
||||
$primaryBackgroundColorLight: #efefef
|
||||
$secondaryBackgroundColorLight: #fefefe
|
||||
$blockBackgroundColorLight: #f1f1f1
|
||||
$primaryBackgroundColorLight: #e0e0e0
|
||||
$secondaryBackgroundColorLight: #f1f1f1
|
||||
$blockBackgroundColorLight: #fefefe
|
||||
$blockSecondaryBackgroundColorLight: #dfdfdf
|
||||
$navbarBackgroundColorLight: #d0d0d0
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
&__Title
|
||||
font-size: 1.2rem
|
||||
|
||||
&__Content, &__Title
|
||||
&__Content
|
||||
border-top: var(--link-color) 1px solid
|
||||
border-bottom: var(--link-color) 1px solid
|
||||
|
||||
&__Footer
|
||||
|
||||
@@ -29,9 +29,9 @@ class News extends React.Component<{}, State> {
|
||||
<div>Новости</div>
|
||||
<Link to={'/news'}>Все новости</Link>
|
||||
</div>
|
||||
{news.map(post => (
|
||||
{news.slice(0, 5).map(post => (
|
||||
<div className={'Post'} key={post.id}>
|
||||
<div className="Post__Title">{post.title}</div>
|
||||
<div className="Post__Title"><Link to={`/news/${post.id }`}>{post.title}</Link></div>
|
||||
<div className="Post__Content">{post.short_post}</div>
|
||||
<div className="Post__Footer">
|
||||
<div>Автор: {post.author}</div>
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
@import "../App/App"
|
||||
|
||||
.Minimized
|
||||
height: 3rem
|
||||
|
||||
.Expanded
|
||||
min-height: 3rem
|
||||
|
||||
&__Links
|
||||
position: relative
|
||||
width: 100%
|
||||
|
||||
.Header
|
||||
color: var(--main-color)
|
||||
background-color: var(--nav-bg-color)
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
min-height: 3rem
|
||||
align-items: flex-start
|
||||
flex-wrap: wrap
|
||||
|
||||
.Header_Links
|
||||
&__Links, &__User
|
||||
min-height: 3rem
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
|
||||
.Header_Button
|
||||
&_Button
|
||||
font-size: 1rem
|
||||
padding: .5rem
|
||||
height: 3rem
|
||||
color: var(--main-color)
|
||||
background-color: var(--nav-bg-color)
|
||||
border: none
|
||||
border-radius: 0
|
||||
|
||||
.Header_Button:hover, .Header__User_Profile:hover
|
||||
background-color: var(--main-bg-color)
|
||||
|
||||
.Header_User
|
||||
display: flex
|
||||
|
||||
.Header__User_Profile
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
font-size: 1rem
|
||||
height: 3rem
|
||||
color: var(--main-color)
|
||||
background-color: var(--nav-bg-color)
|
||||
border: none
|
||||
border-radius: 0
|
||||
cursor: pointer
|
||||
|
||||
.Header__User_Avatar
|
||||
background-size: cover
|
||||
background-position: center
|
||||
width: 40px
|
||||
height: 40px
|
||||
border-radius: 50%
|
||||
margin-right: .2rem
|
||||
&:hover
|
||||
background-color: var(--main-bg-color)
|
||||
|
||||
&__User
|
||||
justify-content: flex-end
|
||||
|
||||
&_Profile
|
||||
display: flex
|
||||
align-items: center
|
||||
font-size: 1rem
|
||||
height: 3rem
|
||||
color: var(--main-color)
|
||||
background-color: var(--nav-bg-color)
|
||||
border: none
|
||||
border-radius: 0
|
||||
padding: 0 .5rem
|
||||
|
||||
&:hover
|
||||
background-color: var(--main-bg-color)
|
||||
|
||||
&_Avatar
|
||||
background-size: cover
|
||||
background-position: center
|
||||
width: 40px
|
||||
height: 40px
|
||||
border-radius: 50%
|
||||
margin-right: .2rem
|
||||
@@ -4,6 +4,9 @@ import {connect} from "react-redux";
|
||||
import {userLogout} from "../../api/users";
|
||||
import {Link} from "react-router-dom";
|
||||
import {logout} from "../../store/actions";
|
||||
import {hasAccessToAdminPanel} from "../../utils";
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faBars} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
type Props = {
|
||||
showRegisterModal: () => void
|
||||
@@ -12,47 +15,70 @@ type Props = {
|
||||
currentUser: UserType
|
||||
};
|
||||
|
||||
class Navbar extends React.Component<Props> {
|
||||
type State = {
|
||||
expand: boolean
|
||||
}
|
||||
|
||||
class Navbar extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
expand: false
|
||||
}
|
||||
|
||||
logout = () => {
|
||||
this.props.logout();
|
||||
localStorage.removeItem('user');
|
||||
userLogout().then(console.log).catch(console.error);
|
||||
};
|
||||
|
||||
changeSize = () => {
|
||||
this.setState({ expand: !this.state.expand })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentUser } = this.props;
|
||||
const { currentUser, showRegisterModal, showLoginModal } = this.props;
|
||||
const { expand } = this.state;
|
||||
|
||||
const isMinimized = window.screen.width <= 1000;
|
||||
|
||||
const links = (
|
||||
<div className="Header__Links">
|
||||
<Link to="/" className={'Header_Button'}>
|
||||
Главная
|
||||
</Link>
|
||||
<Link to="/novels" className={'Header_Button'}>
|
||||
Новеллы
|
||||
</Link>
|
||||
{expand && isMinimized && (
|
||||
<div onClick={this.changeSize} className={'Header_Button'} style={{ position: "absolute", right: 0, margin: '.5rem' }}>X</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className={'Header'}>
|
||||
<div className="Header_Links">
|
||||
<Link to="/">
|
||||
<button className={'Header_Button'}>
|
||||
Главная
|
||||
</button>
|
||||
</Link>
|
||||
<Link to="/novels">
|
||||
<button className={'Header_Button'}>
|
||||
Новеллы
|
||||
</button>
|
||||
</Link>
|
||||
<Link to="/">
|
||||
<button className={'Header_Button'}>
|
||||
Пользователи
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
{this.props.currentUser &&
|
||||
<div className={'Header_User'}>
|
||||
{this.props.currentUser.authorized &&
|
||||
<Link to={`/user/${currentUser.id}`}>
|
||||
<button className="Header__User_Profile">
|
||||
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar})` }}/>
|
||||
<div>{this.props.currentUser.username}</div>
|
||||
</button>
|
||||
<nav className={`Header ${expand ? 'Expanded' : 'Minimized'}`}>
|
||||
{expand || !isMinimized ? links : (
|
||||
<div className="Header__Links">
|
||||
<div onClick={this.changeSize} className={'Header_Button'}>
|
||||
<FontAwesomeIcon icon={faBars}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentUser &&
|
||||
<div className={'Header__User'}>
|
||||
{currentUser.authorized &&
|
||||
<Link to={`/user/${currentUser.id}`} className="Header__User_Profile">
|
||||
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar})` }}/>
|
||||
<div>{this.props.currentUser.username}</div>
|
||||
</Link>}
|
||||
{!currentUser.authorized && <button className={'Header_Button'} onClick={this.props.showRegisterModal}>Регистрация</button>}
|
||||
{currentUser.group && hasAccessToAdminPanel(currentUser) && (
|
||||
<Link to={'/kawaii__neko'} className={'Header_Button'}>
|
||||
Админ-панель
|
||||
</Link>
|
||||
)}
|
||||
{!currentUser.authorized && <div className={'Header_Button'} onClick={showRegisterModal}>Регистрация</div>}
|
||||
{!currentUser.authorized
|
||||
? <button className={'Header_Button'} onClick={this.props.showLoginModal}>Войти</button>
|
||||
: <button className={'Header_Button'} onClick={this.logout}>Выйти</button>}
|
||||
? <div className={'Header_Button'} onClick={showLoginModal}>Войти</div>
|
||||
: <div className={'Header_Button'} onClick={this.logout}>Выйти</div>}
|
||||
</div>}
|
||||
</nav>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
import React from "react";
|
||||
import Loading from "../Loading";
|
||||
import {getNews} from "../../api/news";
|
||||
|
||||
type State = {
|
||||
|
||||
news: PostType[]
|
||||
}
|
||||
|
||||
class AllNews extends React.Component<{}, State> {
|
||||
state: State = {
|
||||
|
||||
news: []
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
document.title = 'Новости';
|
||||
getNews().then((news: PostType[]) => {
|
||||
news.forEach(p => p.date = new Date(p.date));
|
||||
this.setState({ news })
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { news } = this.state;
|
||||
if(news.length === 0) return <Loading/>;
|
||||
|
||||
return (
|
||||
<div></div>
|
||||
<div className="News">
|
||||
<div className="News__Title">
|
||||
Новости
|
||||
</div>
|
||||
{news.map(post => (
|
||||
<div className={'Post'} key={post.id}>
|
||||
<div className="Post__Title">{post.title}</div>
|
||||
<div className="Post__Content">{post.short_post}</div>
|
||||
<div className="Post__Footer">
|
||||
<div>Автор: {post.author}</div>
|
||||
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
&__Title
|
||||
font-size: 1.5rem
|
||||
font-weight: 600
|
||||
border-bottom: 1px solid var(--main-color)
|
||||
|
||||
&__Content
|
||||
margin-top: .1rem
|
||||
border-top: var(--link-color) 1px solid
|
||||
border-bottom: var(--link-color) 1px solid
|
||||
padding: .5rem 0
|
||||
margin: .5rem 0
|
||||
@@ -20,6 +20,7 @@ class Post extends React.Component<Props, State> {
|
||||
|
||||
componentDidMount(): void {
|
||||
const { match: { params: { postId }} } = this.props;
|
||||
|
||||
getPost(postId).then(post => {
|
||||
this.setState({ post })
|
||||
}).catch((error: ApiError) => {
|
||||
@@ -39,7 +40,7 @@ class Post extends React.Component<Props, State> {
|
||||
{post.full_post}
|
||||
</div>
|
||||
<div className="Post__Footer">
|
||||
{post.author}
|
||||
Автор: {post.author}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
@import "../Constants"
|
||||
|
||||
.Animate-Notification-exit
|
||||
opacity: 0
|
||||
|
||||
.Notification
|
||||
background-color: var(--block-color)
|
||||
border: 1px solid var(--link-color)
|
||||
@@ -5,6 +10,7 @@
|
||||
max-width: 500px
|
||||
width: 500px
|
||||
cursor: pointer
|
||||
margin-top: 1rem
|
||||
|
||||
&--Error
|
||||
background: var(--error-bg-color)
|
||||
@@ -18,4 +24,4 @@
|
||||
.Progress
|
||||
height: 5px
|
||||
background: white
|
||||
transition: width 100ms
|
||||
transition: width 5ms
|
||||
@@ -14,7 +14,7 @@ type State = {
|
||||
|
||||
class Notification extends React.Component<Props, State> {
|
||||
timer: NodeJS.Timeout | undefined;
|
||||
element: any;
|
||||
|
||||
state = {
|
||||
time: 5000, paused: false
|
||||
};
|
||||
@@ -34,10 +34,8 @@ class Notification extends React.Component<Props, State> {
|
||||
|
||||
interval = (step=50) => {
|
||||
const { time, paused } = this.state;
|
||||
if(time <= 0 && this.timer)
|
||||
clearInterval(this.timer);
|
||||
if(!paused)
|
||||
this.setState({ time: time-step })
|
||||
if(time <= 0 && this.timer) clearInterval(this.timer);
|
||||
if(!paused) this.setState({ time: time-step });
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -45,8 +43,7 @@ class Notification extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if(this.timer)
|
||||
clearTimeout(this.timer);
|
||||
if(this.timer) clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -58,9 +55,14 @@ class Notification extends React.Component<Props, State> {
|
||||
const styleClass = level ? `Notification--${capitalize(level)}` : '';
|
||||
|
||||
return (
|
||||
<div className={`Notification ${styleClass}`} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} onClick={this.onClick}>
|
||||
<div
|
||||
className={`Notification ${styleClass}`}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{children}
|
||||
<div className="Progress" style={{ width: `${(time / 5000 * 100)}%` }}/>
|
||||
<div className="Progress" style={{ width: `${time / 5000 * 100}%` }}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
|
||||
.Selected
|
||||
background-color: var(--block-sec-color)
|
||||
|
||||
&_Element
|
||||
padding: .5rem
|
||||
background-color: var(--block-color)
|
||||
|
||||
@@ -67,25 +67,23 @@ class Novel extends React.Component<Props, State> {
|
||||
|
||||
sendComment = (text: string) => {
|
||||
const { match: { params: { novelId } } } = this.props;
|
||||
postNovelComment(novelId, text)
|
||||
.then((comment: CommentType) => {
|
||||
const { comments } = this.state;
|
||||
console.log(Object.assign({}, comments, { comment }));
|
||||
this.setState({ comments: [comment, ...comments] });
|
||||
})
|
||||
.catch(console.error);
|
||||
postNovelComment(novelId, text).then((comment: CommentType) => {
|
||||
const { comments } = this.state;
|
||||
console.log(comment)
|
||||
console.log(Object.assign({}, comments, { comment }));
|
||||
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 })
|
||||
});
|
||||
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) {
|
||||
@@ -103,39 +101,37 @@ class Novel extends React.Component<Props, State> {
|
||||
componentDidMount() {
|
||||
const { match: { params }, currentUser } = this.props;
|
||||
|
||||
fetchNovel(params.novelId)
|
||||
.then(novel => {
|
||||
this.setState({ novel });
|
||||
document.title = novel.localized_name ? `${novel.original_name} / ${novel.localized_name}` : novel.original_name;
|
||||
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) => {
|
||||
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);
|
||||
});
|
||||
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;
|
||||
updateUserNovel(currentUser.id, params.novelId, { mark }).then(console.log);
|
||||
fetchNovel(params.novelId).then(novel => this.setState({ novel }));
|
||||
const { userData } = this.state;
|
||||
if(userData?.mark === mark) return;
|
||||
updateUserNovel(currentUser.id, params.novelId, { mark }).then(res => {
|
||||
userData!!.mark = mark;
|
||||
this.setState({ userData })
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -157,7 +153,15 @@ class Novel extends React.Component<Props, State> {
|
||||
{!userData
|
||||
? <div className={'Novel__Main_Status_Element'} onClick={() => this.updateNovelStatus('planned')}>Добавить в список</div>
|
||||
: <div className={"Novel__Main_Status_Element"}>{TranslateStatus[userData.status]}</div>}
|
||||
<div className={'Novel__Main_Status_Element'} onClick={() => this.setState({ statusExpanded: !statusExpanded })}>+</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">
|
||||
@@ -167,10 +171,10 @@ class Novel extends React.Component<Props, State> {
|
||||
<div className={"Novel__Main_Status_Element"} style={{ color: 'red' }} onClick={this.deleteNovelStatus}>Удалить из списка</div>
|
||||
</div>}
|
||||
<div className={'Novel__Main_Status_Mark'}>
|
||||
{[1,2,3,4,5,6,7,8,9,10].map(mark => (
|
||||
{userData && [0,1,2,3,4,5,6,7,8,9,10].map(mark => (
|
||||
<div
|
||||
key={mark}
|
||||
className={'Novel__Main_Status_Mark_Element'}
|
||||
className={mark === userData.mark && userData.mark !== 0 ? 'Novel__Main_Status_Mark_Element Selected' : 'Novel__Main_Status_Mark_Element'}
|
||||
onClick={() => this.updateNovelMark(mark)}
|
||||
>
|
||||
{mark}
|
||||
@@ -188,6 +192,9 @@ class Novel extends React.Component<Props, State> {
|
||||
<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>
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
@import '../Constants'
|
||||
|
||||
.NovelItem
|
||||
background-color: var(--block-color)
|
||||
padding-bottom: $blockPadding
|
||||
border-radius: $borderRadius
|
||||
margin: .5rem
|
||||
background-color: var(--block-color)
|
||||
padding-bottom: $blockPadding
|
||||
border-radius: $borderRadius
|
||||
margin: .5rem
|
||||
|
||||
@media (min-width: 601px)
|
||||
width: $novelBlockSize
|
||||
@media (max-width: 600px)
|
||||
width: 100%
|
||||
@media (min-width: 601px)
|
||||
width: $novelBlockSize
|
||||
@media (max-width: 600px)
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
|
||||
&__Image
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
background-size: cover
|
||||
background-position: center
|
||||
width: 100%
|
||||
height: 130px
|
||||
|
||||
&__Title
|
||||
width: 100%
|
||||
text-align: center
|
||||
display: inline-block
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
padding: .2rem
|
||||
box-sizing: border-box
|
||||
font-weight: 400
|
||||
|
||||
&__Status
|
||||
font-weight: 300
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
|
||||
&__Image
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
background-size: cover
|
||||
background-position: center
|
||||
width: 100%
|
||||
height: 130px
|
||||
|
||||
&__Title
|
||||
width: 100%
|
||||
text-align: center
|
||||
display: inline-block
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
padding: .2rem
|
||||
box-sizing: border-box
|
||||
font-weight: 400
|
||||
|
||||
&__Status
|
||||
font-weight: 300
|
||||
@@ -9,16 +9,23 @@ type Props = {
|
||||
localized_name?: string
|
||||
image?: string
|
||||
status?: string
|
||||
mark?: number
|
||||
}
|
||||
|
||||
const NovelItem = ({ id, original_name, localized_name, image, status }: Props) => (
|
||||
<Link to={`/novels/${id}`} className="NovelItem">
|
||||
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
||||
<div className="NovelItem__Title">
|
||||
{localized_name || original_name}
|
||||
</div>
|
||||
{status && <div className="NovelItem__Status">{TranslateStatus[status]}</div>}
|
||||
</Link>
|
||||
);
|
||||
const NovelItem = ({ id, original_name, localized_name, image, status, mark }: Props) => {
|
||||
console.log(mark !== undefined && mark > 0 && <div>{mark}</div>)
|
||||
|
||||
return (
|
||||
<Link to={`/novels/${id}`} className="NovelItem">
|
||||
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
||||
<div className="NovelItem__Title">
|
||||
{localized_name || original_name}
|
||||
</div>
|
||||
{status && <div className="NovelItem__Status">
|
||||
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
||||
</div>}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default NovelItem;
|
||||
@@ -3,3 +3,6 @@
|
||||
.Spoiler
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
color: var(--link-color)
|
||||
text-decoration: underline
|
||||
@@ -1,29 +1,33 @@
|
||||
@import "../../Constants"
|
||||
|
||||
.UserNovels
|
||||
background-color: var(--sec-bg-color)
|
||||
padding: .5rem
|
||||
background-color: var(--sec-bg-color)
|
||||
padding: .5rem
|
||||
|
||||
&__Title
|
||||
font-size: 1.5rem
|
||||
text-align: center
|
||||
&__Title
|
||||
font-size: 1.5rem
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
|
||||
&__List
|
||||
display: flex
|
||||
flex-direction: column
|
||||
&__List
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
&:not(:last-child)
|
||||
border-bottom: var(--link-color) 1px solid
|
||||
margin-bottom: 1rem
|
||||
padding-bottom: 1rem
|
||||
&:not(:last-child)
|
||||
border-bottom: var(--link-color) 1px solid
|
||||
margin-bottom: 1rem
|
||||
padding-bottom: 1rem
|
||||
|
||||
&_Title
|
||||
display: flex
|
||||
font-size: 1.5rem
|
||||
margin-right: 1rem
|
||||
&_Title
|
||||
display: flex
|
||||
font-size: 1.5rem
|
||||
margin-right: 1rem
|
||||
|
||||
&_Novels
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: wrap
|
||||
justify-content: center
|
||||
button
|
||||
font-weight: 600
|
||||
|
||||
&_Novels
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: wrap
|
||||
justify-content: center
|
||||
@@ -1,29 +1,52 @@
|
||||
@import "../App/App"
|
||||
|
||||
.User
|
||||
&__Activate
|
||||
background-color: var(--sec-bg-color)
|
||||
border-radius: $borderRadius
|
||||
padding: .5rem
|
||||
|
||||
|
||||
&__Info
|
||||
background-color: var(--sec-bg-color)
|
||||
border-radius: $borderRadius
|
||||
padding: $primaryPadding
|
||||
display: flex
|
||||
align-items: flex-end
|
||||
background-position: center
|
||||
background-size: cover
|
||||
background-repeat: no-repeat
|
||||
flex-direction: column
|
||||
|
||||
@media (max-width: 250px)
|
||||
flex-direction: column
|
||||
|
||||
&_Cover
|
||||
height: 20rem
|
||||
width: 100%
|
||||
background-position: center
|
||||
background-size: cover
|
||||
background-repeat: no-repeat
|
||||
|
||||
@media (max-width: 500px)
|
||||
height: 10rem !important
|
||||
|
||||
&_Main
|
||||
display: flex
|
||||
align-items: flex-end
|
||||
background-color: var(--sec-bg-color)
|
||||
width: 100%
|
||||
padding: 0.5rem
|
||||
border-radius: 0 0 $borderRadius $borderRadius
|
||||
box-sizing: border-box
|
||||
|
||||
&_Nick
|
||||
font-size: 1.5rem
|
||||
margin-left: .5rem
|
||||
font-weight: 600
|
||||
|
||||
&_Group
|
||||
margin-left: .5rem
|
||||
border: 1px solid #eeeeee
|
||||
border-radius: 5px
|
||||
border-radius: 4px
|
||||
padding: .1rem
|
||||
text-align: center
|
||||
font-size: 1rem
|
||||
font-weight: 300
|
||||
|
||||
&_Avatar
|
||||
background-size: cover
|
||||
@@ -31,7 +54,7 @@
|
||||
width: 96px
|
||||
height: 96px
|
||||
border-radius: 50%
|
||||
margin-right: .2rem
|
||||
margin-right: .5rem
|
||||
|
||||
&__Statistic
|
||||
background-color: var(--sec-bg-color)
|
||||
@@ -64,6 +87,7 @@
|
||||
.User__Statistic_Blocks
|
||||
display: flex
|
||||
flex-direction: column
|
||||
color: white
|
||||
|
||||
@media (max-width: 300px)
|
||||
width: 100%
|
||||
|
||||
@@ -5,9 +5,11 @@ import NotFoundError from "../NotFoundError";
|
||||
import {Link} from "react-router-dom";
|
||||
import Loading from "../Loading";
|
||||
import NovelItem from "../NovelItem/NovelItem";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
type Props = {
|
||||
match: { params: { userId: number } }
|
||||
match: { params: { userId: string } }
|
||||
currentUser: UserType
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -24,32 +26,52 @@ class User extends React.Component<Props, State> {
|
||||
componentDidMount() {
|
||||
const params = this.props.match.params;
|
||||
|
||||
fetchUser(params.userId)
|
||||
.then((user: UserType) => {
|
||||
this.setState({ user });
|
||||
document.title = `${user.username} / Unmei`;
|
||||
})
|
||||
.catch((err: ApiError) => this.setState({ errorCode: err.code }));
|
||||
fetchUser(parseInt(params.userId)).then((user: UserType) => {
|
||||
console.log(user)
|
||||
this.setState({ user });
|
||||
document.title = `${user.username} / Unmei`;
|
||||
|
||||
fetchUserNovels(params.userId).then((novels: NovelType[]) => {
|
||||
fetchUserNovels(parseInt(params.userId)).then((novels: NovelType[]) => {
|
||||
this.setState({ novels });
|
||||
}).catch((err: ApiError) => console.error(err));
|
||||
}).catch((err: ApiError) => {
|
||||
this.setState({ errorCode: err.code })
|
||||
console.log(err)
|
||||
});
|
||||
}
|
||||
|
||||
sendActivateLink = () => {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentUser } = this.props;
|
||||
const { errorCode, user, novels } = this.state;
|
||||
if(errorCode === 100) return <NotFoundError/>;
|
||||
if(!user) return <Loading />;
|
||||
|
||||
const style = user.cover ? { backgroundImage: `url(${user.cover})`, height: '20rem' } : {}
|
||||
const style = !user.is_activated ? { marginTop: '1rem' } : {}
|
||||
|
||||
return (
|
||||
<div className={'User'}>
|
||||
{!user.is_activated && user.id === currentUser.id && (
|
||||
<div className={'User__Activate'}>
|
||||
<h2>Ваш аккаунт не активирован!</h2>
|
||||
<p>Активируйте аккаунт, для доступа ко многим функциям. Так же актвация аккаунта позволит восстановить пароль.</p>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<button onClick={this.sendActivateLink} style={{ fontSize: '1.2rem' }}>Активировать!</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="User__Info" style={style}>
|
||||
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar})` }} />
|
||||
<div>
|
||||
<div className='User__Info_Nick'>{user.username}</div>
|
||||
<div className='User__Info_Group' style={{ borderColor: user.group.color, color: user.group.color }}>{user.group.name}</div>
|
||||
{user.cover && <div className="User__Info_Cover" style={{ backgroundImage: `url(${user.cover})` }}/>}
|
||||
<div className={'User__Info_Main'}>
|
||||
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar})` }} />
|
||||
<div>
|
||||
<div className='User__Info_Nick'>{user.username}</div>
|
||||
<div className='User__Info_Group' style={{ borderColor: user.group.color, color: user.group.color }}>{user.group.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,4 +127,8 @@ class User extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
currentUser: state.currentUser
|
||||
})
|
||||
)(User);
|
||||
Vendored
+2
@@ -27,10 +27,12 @@ type UserType = {
|
||||
authorized: boolean
|
||||
id: number
|
||||
username: string
|
||||
email?: string
|
||||
avatar: string
|
||||
cover: string
|
||||
group: UserGroupType
|
||||
is_superuser: boolean
|
||||
is_activated: boolean
|
||||
}
|
||||
|
||||
type UserNovelType = {
|
||||
|
||||
+4
-2
@@ -11,6 +11,8 @@ const getRandomInt: (min: number, max: number) => number = (min: number, max: nu
|
||||
const hasPermission = (user: UserType, permission: string) => {
|
||||
const permissions = user?.group?.permissions.split(',');
|
||||
return permissions?.filter(perm => perm === permission).length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
export { capitalize, getRandomInt, hasPermission };
|
||||
const hasAccessToAdminPanel = (user: UserType) => user.is_superuser || user.group.is_superuser || hasPermission(user, 'admin_panel')
|
||||
|
||||
export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel };
|
||||
Reference in New Issue
Block a user