Many fixes
This commit is contained in:
Generated
+44
@@ -1857,6 +1857,15 @@
|
|||||||
"@types/react-router": "*"
|
"@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": {
|
"@types/stack-utils": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||||
@@ -4649,6 +4658,30 @@
|
|||||||
"utila": "~0.4"
|
"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": {
|
"dom-serializer": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
|
"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": {
|
"read-pkg": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"react-recaptcha": "^2.3.10",
|
"react-recaptcha": "^2.3.10",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
|
"react-transition-group": "^4.3.0",
|
||||||
"redux": "^4.0.5"
|
"redux": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"@types/react-dom": "^16.9.5",
|
"@types/react-dom": "^16.9.5",
|
||||||
"@types/react-redux": "^7.1.7",
|
"@types/react-redux": "^7.1.7",
|
||||||
"@types/react-router-dom": "^5.1.3",
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
"@types/react-transition-group": "^4.2.4",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^4.13.1",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "^2.13.8",
|
||||||
"typescript": "^3.8.2",
|
"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 fetchUser = (id: number) => get(`users/${id}`);
|
||||||
|
export const updateUser = (user: UserType) => put(`users/${user.id}`, { user })
|
||||||
|
|
||||||
export const fetchCurrentUser = () => get('users/current');
|
export const fetchCurrentUser = () => get('users/current');
|
||||||
export const fetchUserNovels = (id: number) => get(`users/${id}/novels`);
|
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%
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
|
body, pre, textarea
|
||||||
|
font-family: 'Roboto', sans-serif
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, p, body, pre
|
||||||
|
margin: 0
|
||||||
|
|
||||||
body
|
body
|
||||||
margin: 0
|
|
||||||
padding: 0
|
padding: 0
|
||||||
font-family: 'Roboto', sans-serif
|
|
||||||
|
|
||||||
--main-color: #{$mainColor}
|
--main-color: #{$mainColor}
|
||||||
--sec-color: #{$secondaryColor}
|
--sec-color: #{$secondaryColor}
|
||||||
@@ -28,8 +31,6 @@ body
|
|||||||
background: var(--main-bg-color)
|
background: var(--main-bg-color)
|
||||||
|
|
||||||
pre
|
pre
|
||||||
font-family: 'Roboto', sans-serif
|
|
||||||
margin: 0
|
|
||||||
white-space: pre-wrap
|
white-space: pre-wrap
|
||||||
|
|
||||||
body[theme='red']
|
body[theme='red']
|
||||||
@@ -64,6 +65,9 @@ body[theme='light']
|
|||||||
--block-sec-color: #{$blockSecondaryBackgroundColorLight}
|
--block-sec-color: #{$blockSecondaryBackgroundColorLight}
|
||||||
--nav-bg-color: #{$navbarBackgroundColorLight}
|
--nav-bg-color: #{$navbarBackgroundColorLight}
|
||||||
|
|
||||||
|
color: var(--main-color)
|
||||||
|
background: var(--main-bg-color)
|
||||||
|
|
||||||
a
|
a
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
@@ -74,8 +78,12 @@ a
|
|||||||
input, button, textarea
|
input, button, textarea
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
color: var(--sec-color)
|
||||||
|
|
||||||
button
|
button
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
padding: .2rem
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
border: 1px solid var(--main-color)
|
border: 1px solid var(--main-color)
|
||||||
@@ -97,7 +105,7 @@ input
|
|||||||
|
|
||||||
.Container
|
.Container
|
||||||
max-width: 1200px
|
max-width: 1200px
|
||||||
min-height: calc( 100% - 4rem )
|
min-height: calc(100% - 4rem)
|
||||||
margin: .5rem auto
|
margin: .5rem auto
|
||||||
|
|
||||||
.Notifications
|
.Notifications
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Switch, Route } from "react-router-dom";
|
import {Switch, Route} from "react-router-dom";
|
||||||
import './App.sass';
|
import './App.sass';
|
||||||
import Navbar from "../Navbar/Navbar";
|
import Navbar from "../Navbar/Navbar";
|
||||||
import Novel from "../Novel/Novel";
|
import Novel from "../Novel/Novel";
|
||||||
@@ -22,7 +22,7 @@ import ColorTag from "../Tags/ColorTag";
|
|||||||
import ActivateAccount from "../ActivateAccount";
|
import ActivateAccount from "../ActivateAccount";
|
||||||
import AllNews from "../News/AllNews";
|
import AllNews from "../News/AllNews";
|
||||||
import Post from "../News/Post";
|
import Post from "../News/Post";
|
||||||
import AdminPanel from "../AdminPanel/Main/AdminPanel";
|
import AdminPanel from "../AdminPanel/AdminPanel";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notifications: React.ReactNode[]
|
notifications: React.ReactNode[]
|
||||||
@@ -47,7 +47,9 @@ class App extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { setUser } = this.props;
|
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');
|
const theme = localStorage.getItem('theme');
|
||||||
if(theme)
|
if(theme)
|
||||||
@@ -91,7 +93,9 @@ class App extends React.Component<Props, State> {
|
|||||||
|
|
||||||
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
<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}/>
|
<Route component={NotFoundError}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
@@ -105,7 +109,7 @@ class App extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state: { notifications: React.ReactNode[] }) => ({
|
(state: StoreState) => ({
|
||||||
notifications: state.notifications
|
notifications: state.notifications
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
|
|||||||
@@ -9,18 +9,10 @@ type Props = {
|
|||||||
user: UserType | undefined
|
user: UserType | undefined
|
||||||
text: string
|
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() {
|
render() {
|
||||||
const { user } = this.props;
|
const { user, text } = this.props;
|
||||||
const { text } = this.props;
|
|
||||||
if(!user) return <Loading/>;
|
if(!user) return <Loading/>;
|
||||||
|
|
||||||
return (user &&
|
return (user &&
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import Comment from './Comment';
|
|||||||
import './Comments.sass';
|
import './Comments.sass';
|
||||||
import Loading from "../Loading";
|
import Loading from "../Loading";
|
||||||
import {fetchUser} from "../../api/users";
|
import {fetchUser} from "../../api/users";
|
||||||
|
import {connect} from "react-redux";
|
||||||
|
import {addNotification} from "../../store/actions";
|
||||||
|
import Notification from "../Notification/Notification";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comments: CommentType[]
|
comments: CommentType[]
|
||||||
@@ -12,6 +15,7 @@ type Props = {
|
|||||||
user: UserType
|
user: UserType
|
||||||
loadMore: () => void
|
loadMore: () => void
|
||||||
sendComment: (text: string) => void
|
sendComment: (text: string) => void
|
||||||
|
addNotification: (notification: React.ReactNode) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -41,35 +45,43 @@ class Comments extends React.Component<Props, State> {
|
|||||||
|
|
||||||
sendComment = (event: React.FormEvent) => {
|
sendComment = (event: React.FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { sendComment } = this.props;
|
const { sendComment, addNotification } = this.props;
|
||||||
const { commentText } = this.state;
|
const { commentText } = this.state;
|
||||||
|
|
||||||
if(commentText === '')
|
if(commentText === '') {
|
||||||
console.log('empty');
|
const notification = (
|
||||||
else {
|
<Notification level={"error"}>
|
||||||
|
Пустой комментарий
|
||||||
|
</Notification>
|
||||||
|
);
|
||||||
|
addNotification(notification);
|
||||||
|
} else {
|
||||||
sendComment(commentText);
|
sendComment(commentText);
|
||||||
this.setState({ commentText: '' });
|
this.setState({ commentText: '' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Readonly<Props>) {
|
componentDidUpdate(prevProps: Readonly<Props>) {
|
||||||
if(prevProps.comments.length !== this.props.comments.length) {
|
const { comments } = this.props;
|
||||||
let map: Map<number, UserType> = new Map();
|
const { comments: prevComments } = prevProps;
|
||||||
this.props.comments.forEach(comment => {
|
|
||||||
if(!map.has(comment.user_id)) {
|
if(prevComments.length !== comments.length) {
|
||||||
fetchUser(comment.user_id).then(user => {
|
let { users } = this.state;
|
||||||
map = map.set(comment.user_id, user)
|
|
||||||
});
|
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() {
|
render() {
|
||||||
const { user, comments, hasMore, loadMore } = this.props;
|
const { user, comments, hasMore, loadMore } = this.props;
|
||||||
const { users } = this.state;
|
const { users } = this.state;
|
||||||
if(!user || !comments) return <Loading/>;
|
if(!user) return <Loading/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Comments">
|
<div className="Comments">
|
||||||
@@ -91,10 +103,16 @@ class Comments extends React.Component<Props, State> {
|
|||||||
ref={ref => this.input = ref}
|
ref={ref => this.input = ref}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<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>}
|
{hasMore && <button type={"button"} onClick={loadMore}>Загрузить ещё</button>}
|
||||||
</div>
|
</div>
|
||||||
</form>}
|
</form>}
|
||||||
|
{!comments && (
|
||||||
|
<Loading/>
|
||||||
|
)}
|
||||||
{comments && comments.length > 0 ? (
|
{comments && comments.length > 0 ? (
|
||||||
comments.map(comment => <Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)} />)
|
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
|
$mainColor: #efefef
|
||||||
$secondaryColor: #aaaaaa
|
$secondaryColor: #aaaaaa
|
||||||
$linkHoverColor: #dddddd
|
$linkHoverColor: #dddddd
|
||||||
|
|
||||||
// BLACK
|
// BLACK
|
||||||
$primaryBackgroundColor: #121212
|
$primaryBackgroundColor: #121212
|
||||||
$secondaryBackgroundColor: #212121
|
$secondaryBackgroundColor: #191919
|
||||||
$blockBackgroundColor: #191919
|
$blockBackgroundColor: #212121
|
||||||
$blockSecondaryBackgroundColor: #121212
|
$blockSecondaryBackgroundColor: #121212
|
||||||
$navbarBackgroundColor: #0b0b0b
|
$navbarBackgroundColor: #0b0b0b
|
||||||
|
|
||||||
// BLUE
|
// BLUE
|
||||||
$primaryBackgroundColorBlue: #121222
|
$primaryBackgroundColorBlue: #121222
|
||||||
$secondaryBackgroundColorBlue: #212131
|
$secondaryBackgroundColorBlue: #191929
|
||||||
$blockBackgroundColorBlue: #191929
|
$blockBackgroundColorBlue: #212131
|
||||||
$blockSecondaryBackgroundColorBlue: #121222
|
$blockSecondaryBackgroundColorBlue: #121222
|
||||||
$navbarBackgroundColorBlue: #0b0b1a
|
$navbarBackgroundColorBlue: #0b0b1a
|
||||||
|
|
||||||
// RED
|
// RED
|
||||||
$primaryBackgroundColorRed: #221212
|
$primaryBackgroundColorRed: #221212
|
||||||
$secondaryBackgroundColorRed: #312121
|
$secondaryBackgroundColorRed: #291919
|
||||||
$blockBackgroundColorRed: #291919
|
$blockBackgroundColorRed: #312121
|
||||||
$blockSecondaryBackgroundColorRed: #221212
|
$blockSecondaryBackgroundColorRed: #221212
|
||||||
$navbarBackgroundColorRed: #2a0b0b
|
$navbarBackgroundColorRed: #2a0b0b
|
||||||
|
|
||||||
// GREEN
|
// GREEN
|
||||||
$primaryBackgroundColorGreen: #122212
|
$primaryBackgroundColorGreen: #122212
|
||||||
$secondaryBackgroundColorGreen: #213121
|
$secondaryBackgroundColorGreen: #192919
|
||||||
$blockBackgroundColorGreen: #192919
|
$blockBackgroundColorGreen: #213121
|
||||||
$blockSecondaryBackgroundColorGreen: #122212
|
$blockSecondaryBackgroundColorGreen: #122212
|
||||||
$navbarBackgroundColorGreen: #0b2a0b
|
$navbarBackgroundColorGreen: #0b2a0b
|
||||||
|
|
||||||
@@ -35,9 +41,9 @@ $mainColorLight: #121212
|
|||||||
$secondaryColorLight: #444444
|
$secondaryColorLight: #444444
|
||||||
$linkHoverColorLight: #333333
|
$linkHoverColorLight: #333333
|
||||||
|
|
||||||
$primaryBackgroundColorLight: #efefef
|
$primaryBackgroundColorLight: #e0e0e0
|
||||||
$secondaryBackgroundColorLight: #fefefe
|
$secondaryBackgroundColorLight: #f1f1f1
|
||||||
$blockBackgroundColorLight: #f1f1f1
|
$blockBackgroundColorLight: #fefefe
|
||||||
$blockSecondaryBackgroundColorLight: #dfdfdf
|
$blockSecondaryBackgroundColorLight: #dfdfdf
|
||||||
$navbarBackgroundColorLight: #d0d0d0
|
$navbarBackgroundColorLight: #d0d0d0
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
&__Title
|
&__Title
|
||||||
font-size: 1.2rem
|
font-size: 1.2rem
|
||||||
|
|
||||||
&__Content, &__Title
|
&__Content
|
||||||
|
border-top: var(--link-color) 1px solid
|
||||||
border-bottom: var(--link-color) 1px solid
|
border-bottom: var(--link-color) 1px solid
|
||||||
|
|
||||||
&__Footer
|
&__Footer
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ class News extends React.Component<{}, State> {
|
|||||||
<div>Новости</div>
|
<div>Новости</div>
|
||||||
<Link to={'/news'}>Все новости</Link>
|
<Link to={'/news'}>Все новости</Link>
|
||||||
</div>
|
</div>
|
||||||
{news.map(post => (
|
{news.slice(0, 5).map(post => (
|
||||||
<div className={'Post'} key={post.id}>
|
<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__Content">{post.short_post}</div>
|
||||||
<div className="Post__Footer">
|
<div className="Post__Footer">
|
||||||
<div>Автор: {post.author}</div>
|
<div>Автор: {post.author}</div>
|
||||||
|
|||||||
@@ -1,46 +1,63 @@
|
|||||||
@import "../App/App"
|
@import "../App/App"
|
||||||
|
|
||||||
|
.Minimized
|
||||||
|
height: 3rem
|
||||||
|
|
||||||
|
.Expanded
|
||||||
|
min-height: 3rem
|
||||||
|
|
||||||
|
&__Links
|
||||||
|
position: relative
|
||||||
|
width: 100%
|
||||||
|
|
||||||
.Header
|
.Header
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
background-color: var(--nav-bg-color)
|
background-color: var(--nav-bg-color)
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: space-between
|
justify-content: space-between
|
||||||
min-height: 3rem
|
align-items: flex-start
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
.Header_Links
|
&__Links, &__User
|
||||||
min-height: 3rem
|
min-height: 3rem
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
|
||||||
.Header_Button
|
&_Button
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
height: 3rem
|
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
background-color: var(--nav-bg-color)
|
background-color: var(--nav-bg-color)
|
||||||
border: none
|
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
|
display: flex
|
||||||
|
justify-content: center
|
||||||
align-items: center
|
align-items: center
|
||||||
font-size: 1rem
|
cursor: pointer
|
||||||
height: 3rem
|
|
||||||
color: var(--main-color)
|
|
||||||
background-color: var(--nav-bg-color)
|
|
||||||
border: none
|
|
||||||
border-radius: 0
|
|
||||||
|
|
||||||
.Header__User_Avatar
|
&:hover
|
||||||
background-size: cover
|
background-color: var(--main-bg-color)
|
||||||
background-position: center
|
|
||||||
width: 40px
|
&__User
|
||||||
height: 40px
|
justify-content: flex-end
|
||||||
border-radius: 50%
|
|
||||||
margin-right: .2rem
|
&_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 {userLogout} from "../../api/users";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {logout} from "../../store/actions";
|
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 = {
|
type Props = {
|
||||||
showRegisterModal: () => void
|
showRegisterModal: () => void
|
||||||
@@ -12,47 +15,70 @@ type Props = {
|
|||||||
currentUser: UserType
|
currentUser: UserType
|
||||||
};
|
};
|
||||||
|
|
||||||
class Navbar extends React.Component<Props> {
|
type State = {
|
||||||
|
expand: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class Navbar extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
expand: false
|
||||||
|
}
|
||||||
|
|
||||||
logout = () => {
|
logout = () => {
|
||||||
this.props.logout();
|
this.props.logout();
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
userLogout().then(console.log).catch(console.error);
|
userLogout().then(console.log).catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
changeSize = () => {
|
||||||
|
this.setState({ expand: !this.state.expand })
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
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 (
|
return (
|
||||||
<nav className={'Header'}>
|
<nav className={`Header ${expand ? 'Expanded' : 'Minimized'}`}>
|
||||||
<div className="Header_Links">
|
{expand || !isMinimized ? links : (
|
||||||
<Link to="/">
|
<div className="Header__Links">
|
||||||
<button className={'Header_Button'}>
|
<div onClick={this.changeSize} className={'Header_Button'}>
|
||||||
Главная
|
<FontAwesomeIcon icon={faBars}/>
|
||||||
</button>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
<Link to="/novels">
|
)}
|
||||||
<button className={'Header_Button'}>
|
{currentUser &&
|
||||||
Новеллы
|
<div className={'Header__User'}>
|
||||||
</button>
|
{currentUser.authorized &&
|
||||||
</Link>
|
<Link to={`/user/${currentUser.id}`} className="Header__User_Profile">
|
||||||
<Link to="/">
|
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar})` }}/>
|
||||||
<button className={'Header_Button'}>
|
<div>{this.props.currentUser.username}</div>
|
||||||
Пользователи
|
|
||||||
</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>
|
|
||||||
</Link>}
|
</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
|
{!currentUser.authorized
|
||||||
? <button className={'Header_Button'} onClick={this.props.showLoginModal}>Войти</button>
|
? <div className={'Header_Button'} onClick={showLoginModal}>Войти</div>
|
||||||
: <button className={'Header_Button'} onClick={this.logout}>Выйти</button>}
|
: <div className={'Header_Button'} onClick={this.logout}>Выйти</div>}
|
||||||
</div>}
|
</div>}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,44 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Loading from "../Loading";
|
||||||
|
import {getNews} from "../../api/news";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
news: PostType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllNews extends React.Component<{}, State> {
|
class AllNews extends React.Component<{}, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
|
news: []
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
document.title = 'Новости';
|
||||||
|
getNews().then((news: PostType[]) => {
|
||||||
|
news.forEach(p => p.date = new Date(p.date));
|
||||||
|
this.setState({ news })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { news } = this.state;
|
||||||
|
if(news.length === 0) return <Loading/>;
|
||||||
|
|
||||||
return (
|
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
|
&__Title
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
border-bottom: 1px solid var(--main-color)
|
|
||||||
|
|
||||||
&__Content
|
&__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 {
|
componentDidMount(): void {
|
||||||
const { match: { params: { postId }} } = this.props;
|
const { match: { params: { postId }} } = this.props;
|
||||||
|
|
||||||
getPost(postId).then(post => {
|
getPost(postId).then(post => {
|
||||||
this.setState({ post })
|
this.setState({ post })
|
||||||
}).catch((error: ApiError) => {
|
}).catch((error: ApiError) => {
|
||||||
@@ -39,7 +40,7 @@ class Post extends React.Component<Props, State> {
|
|||||||
{post.full_post}
|
{post.full_post}
|
||||||
</div>
|
</div>
|
||||||
<div className="Post__Footer">
|
<div className="Post__Footer">
|
||||||
{post.author}
|
Автор: {post.author}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
@import "../Constants"
|
||||||
|
|
||||||
|
.Animate-Notification-exit
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
.Notification
|
.Notification
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
border: 1px solid var(--link-color)
|
border: 1px solid var(--link-color)
|
||||||
@@ -5,6 +10,7 @@
|
|||||||
max-width: 500px
|
max-width: 500px
|
||||||
width: 500px
|
width: 500px
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
margin-top: 1rem
|
||||||
|
|
||||||
&--Error
|
&--Error
|
||||||
background: var(--error-bg-color)
|
background: var(--error-bg-color)
|
||||||
@@ -18,4 +24,4 @@
|
|||||||
.Progress
|
.Progress
|
||||||
height: 5px
|
height: 5px
|
||||||
background: white
|
background: white
|
||||||
transition: width 100ms
|
transition: width 5ms
|
||||||
@@ -14,7 +14,7 @@ type State = {
|
|||||||
|
|
||||||
class Notification extends React.Component<Props, State> {
|
class Notification extends React.Component<Props, State> {
|
||||||
timer: NodeJS.Timeout | undefined;
|
timer: NodeJS.Timeout | undefined;
|
||||||
element: any;
|
|
||||||
state = {
|
state = {
|
||||||
time: 5000, paused: false
|
time: 5000, paused: false
|
||||||
};
|
};
|
||||||
@@ -34,10 +34,8 @@ class Notification extends React.Component<Props, State> {
|
|||||||
|
|
||||||
interval = (step=50) => {
|
interval = (step=50) => {
|
||||||
const { time, paused } = this.state;
|
const { time, paused } = this.state;
|
||||||
if(time <= 0 && this.timer)
|
if(time <= 0 && this.timer) clearInterval(this.timer);
|
||||||
clearInterval(this.timer);
|
if(!paused) this.setState({ time: time-step });
|
||||||
if(!paused)
|
|
||||||
this.setState({ time: time-step })
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
@@ -45,8 +43,7 @@ class Notification extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
if(this.timer)
|
if(this.timer) clearTimeout(this.timer);
|
||||||
clearTimeout(this.timer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -58,9 +55,14 @@ class Notification extends React.Component<Props, State> {
|
|||||||
const styleClass = level ? `Notification--${capitalize(level)}` : '';
|
const styleClass = level ? `Notification--${capitalize(level)}` : '';
|
||||||
|
|
||||||
return (
|
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}
|
{children}
|
||||||
<div className="Progress" style={{ width: `${(time / 5000 * 100)}%` }}/>
|
<div className="Progress" style={{ width: `${time / 5000 * 100}%` }}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
display: flex
|
display: flex
|
||||||
justify-content: space-between
|
justify-content: space-between
|
||||||
|
|
||||||
|
.Selected
|
||||||
|
background-color: var(--block-sec-color)
|
||||||
|
|
||||||
&_Element
|
&_Element
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
|
|||||||
@@ -67,25 +67,23 @@ class Novel extends React.Component<Props, State> {
|
|||||||
|
|
||||||
sendComment = (text: string) => {
|
sendComment = (text: string) => {
|
||||||
const { match: { params: { novelId } } } = this.props;
|
const { match: { params: { novelId } } } = this.props;
|
||||||
postNovelComment(novelId, text)
|
postNovelComment(novelId, text).then((comment: CommentType) => {
|
||||||
.then((comment: CommentType) => {
|
const { comments } = this.state;
|
||||||
const { comments } = this.state;
|
console.log(comment)
|
||||||
console.log(Object.assign({}, comments, { comment }));
|
console.log(Object.assign({}, comments, { comment }));
|
||||||
this.setState({ comments: [comment, ...comments] });
|
this.setState({ comments: [comment, ...comments] });
|
||||||
})
|
}).catch(console.error);
|
||||||
.catch(console.error);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadMoreComments = () => {
|
loadMoreComments = () => {
|
||||||
const { match: { params } } = this.props;
|
const { match: { params } } = this.props;
|
||||||
let { comments, commentsOffset } = this.state;
|
let { comments, commentsOffset } = this.state;
|
||||||
|
|
||||||
fetchNovelComments(params.novelId, commentsOffset, 10)
|
fetchNovelComments(params.novelId, commentsOffset, 10).then(r => {
|
||||||
.then(r => {
|
const hasMoreComments = r.count <= commentsOffset;
|
||||||
const hasMoreComments = r.count <= commentsOffset;
|
commentsOffset += 5;
|
||||||
commentsOffset += 5;
|
this.setState({ comments: [...r.comments.reverse(), ...comments], hasMoreComments, commentsOffset })
|
||||||
this.setState({ comments: [...r.comments.reverse(), ...comments], hasMoreComments, commentsOffset })
|
});
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: any) {
|
componentDidUpdate(prevProps: any) {
|
||||||
@@ -103,39 +101,37 @@ class Novel extends React.Component<Props, State> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match: { params }, currentUser } = this.props;
|
const { match: { params }, currentUser } = this.props;
|
||||||
|
|
||||||
fetchNovel(params.novelId)
|
fetchNovel(params.novelId).then((novel: NovelType) => {
|
||||||
.then(novel => {
|
novel.release_date = new Date(novel.release_date);
|
||||||
this.setState({ novel });
|
this.setState({ novel });
|
||||||
document.title = novel.localized_name ? `${novel.original_name} / ${novel.localized_name}` : novel.original_name;
|
document.title = novel.localized_name ? `${novel.original_name} / ${novel.localized_name}` : novel.original_name;
|
||||||
|
|
||||||
fetchNovelComments(params.novelId)
|
fetchNovelComments(params.novelId).then(r => {
|
||||||
.then(r => {
|
const hasMoreComments = r.count > 5;
|
||||||
const hasMoreComments = r.count > 5;
|
this.setState({ comments: r.comments.reverse(), hasMoreComments })
|
||||||
this.setState({ comments: r.comments.reverse(), hasMoreComments })
|
});
|
||||||
});
|
fetchNovelCharacters(params.novelId).then(characters => this.setState({ characters }));
|
||||||
fetchNovelCharacters(params.novelId)
|
fetchNovelGenres(params.novelId).then(genres => this.setState({ genres }));
|
||||||
.then(characters => this.setState({ characters }));
|
}).catch((err: ApiError) => {
|
||||||
fetchNovelGenres(params.novelId)
|
|
||||||
.then(genres => this.setState({ genres }));
|
|
||||||
})
|
|
||||||
.catch((err: ApiError) => {
|
|
||||||
this.setState({ errorCode: err.code });
|
this.setState({ errorCode: err.code });
|
||||||
});
|
});
|
||||||
|
|
||||||
if(currentUser.authorized)
|
if(currentUser.authorized)
|
||||||
fetchUserNovel(currentUser.id, params.novelId)
|
fetchUserNovel(currentUser.id, params.novelId).then((userData: UserNovelType) => {
|
||||||
.then((userData: UserNovelType) => {
|
this.setState({ userData });
|
||||||
this.setState({ userData });
|
}).catch((err: ApiError) => {
|
||||||
})
|
if(err.code !== 100) console.error(err.text);
|
||||||
.catch((err: ApiError) => {
|
});
|
||||||
if(err.code !== 100) console.error(err.text);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNovelMark = (mark: number) => {
|
updateNovelMark = (mark: number) => {
|
||||||
const { match: { params }, currentUser } = this.props;
|
const { match: { params }, currentUser } = this.props;
|
||||||
updateUserNovel(currentUser.id, params.novelId, { mark }).then(console.log);
|
const { userData } = this.state;
|
||||||
fetchNovel(params.novelId).then(novel => this.setState({ novel }));
|
if(userData?.mark === mark) return;
|
||||||
|
updateUserNovel(currentUser.id, params.novelId, { mark }).then(res => {
|
||||||
|
userData!!.mark = mark;
|
||||||
|
this.setState({ userData })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -157,7 +153,15 @@ class Novel extends React.Component<Props, State> {
|
|||||||
{!userData
|
{!userData
|
||||||
? <div className={'Novel__Main_Status_Element'} onClick={() => this.updateNovelStatus('planned')}>Добавить в список</div>
|
? <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"}>{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>
|
</div>
|
||||||
{statusExpanded &&
|
{statusExpanded &&
|
||||||
<div className="Novel__Main_Status_Extended">
|
<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 className={"Novel__Main_Status_Element"} style={{ color: 'red' }} onClick={this.deleteNovelStatus}>Удалить из списка</div>
|
||||||
</div>}
|
</div>}
|
||||||
<div className={'Novel__Main_Status_Mark'}>
|
<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
|
<div
|
||||||
key={mark}
|
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)}
|
onClick={() => this.updateNovelMark(mark)}
|
||||||
>
|
>
|
||||||
{mark}
|
{mark}
|
||||||
@@ -188,6 +192,9 @@ class Novel extends React.Component<Props, State> {
|
|||||||
<div className="Novel__Info_Genres">
|
<div className="Novel__Info_Genres">
|
||||||
<strong>Жанры</strong>: {genres.map(genre => <div className={'Genre'} key={genre.id}>{genre.localized_name}</div>)}
|
<strong>Жанры</strong>: {genres.map(genre => <div className={'Genre'} key={genre.id}>{genre.localized_name}</div>)}
|
||||||
</div>}
|
</div>}
|
||||||
|
<div>
|
||||||
|
<strong>Год выхода</strong>: {novel.release_date.toLocaleDateString()}
|
||||||
|
</div>
|
||||||
<div className="Novel__Info_Rating"><strong>Ср. оценка</strong>: {novel.rating.toFixed(2)}</div>
|
<div className="Novel__Info_Rating"><strong>Ср. оценка</strong>: {novel.rating.toFixed(2)}</div>
|
||||||
<pre className={'Novel__Info_Description'}>{novel.description}</pre>
|
<pre className={'Novel__Info_Description'}>{novel.description}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,37 +1,38 @@
|
|||||||
@import '../Constants'
|
@import '../Constants'
|
||||||
|
|
||||||
.NovelItem
|
.NovelItem
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
padding-bottom: $blockPadding
|
padding-bottom: $blockPadding
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
margin: .5rem
|
margin: .5rem
|
||||||
|
|
||||||
@media (min-width: 601px)
|
@media (min-width: 601px)
|
||||||
width: $novelBlockSize
|
width: $novelBlockSize
|
||||||
@media (max-width: 600px)
|
@media (max-width: 600px)
|
||||||
width: 100%
|
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
|
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
|
localized_name?: string
|
||||||
image?: string
|
image?: string
|
||||||
status?: string
|
status?: string
|
||||||
|
mark?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const NovelItem = ({ id, original_name, localized_name, image, status }: Props) => (
|
const NovelItem = ({ id, original_name, localized_name, image, status, mark }: Props) => {
|
||||||
<Link to={`/novels/${id}`} className="NovelItem">
|
console.log(mark !== undefined && mark > 0 && <div>{mark}</div>)
|
||||||
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
|
||||||
<div className="NovelItem__Title">
|
return (
|
||||||
{localized_name || original_name}
|
<Link to={`/novels/${id}`} className="NovelItem">
|
||||||
</div>
|
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
||||||
{status && <div className="NovelItem__Status">{TranslateStatus[status]}</div>}
|
<div className="NovelItem__Title">
|
||||||
</Link>
|
{localized_name || original_name}
|
||||||
);
|
</div>
|
||||||
|
{status && <div className="NovelItem__Status">
|
||||||
|
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
||||||
|
</div>}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default NovelItem;
|
export default NovelItem;
|
||||||
@@ -3,3 +3,6 @@
|
|||||||
.Spoiler
|
.Spoiler
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: var(--link-color)
|
||||||
|
text-decoration: underline
|
||||||
@@ -1,29 +1,33 @@
|
|||||||
@import "../../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
.UserNovels
|
.UserNovels
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
|
|
||||||
&__Title
|
&__Title
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
text-align: center
|
font-weight: 600
|
||||||
|
text-align: center
|
||||||
|
|
||||||
&__List
|
&__List
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
&:not(:last-child)
|
&:not(:last-child)
|
||||||
border-bottom: var(--link-color) 1px solid
|
border-bottom: var(--link-color) 1px solid
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1rem
|
||||||
padding-bottom: 1rem
|
padding-bottom: 1rem
|
||||||
|
|
||||||
&_Title
|
&_Title
|
||||||
display: flex
|
display: flex
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
margin-right: 1rem
|
margin-right: 1rem
|
||||||
|
|
||||||
&_Novels
|
button
|
||||||
display: flex
|
font-weight: 600
|
||||||
flex-direction: row
|
|
||||||
flex-wrap: wrap
|
&_Novels
|
||||||
justify-content: center
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: center
|
||||||
@@ -1,29 +1,52 @@
|
|||||||
@import "../App/App"
|
@import "../App/App"
|
||||||
|
|
||||||
.User
|
.User
|
||||||
|
&__Activate
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
|
border-radius: $borderRadius
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
|
||||||
&__Info
|
&__Info
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
padding: $primaryPadding
|
|
||||||
display: flex
|
display: flex
|
||||||
align-items: flex-end
|
align-items: flex-end
|
||||||
background-position: center
|
flex-direction: column
|
||||||
background-size: cover
|
|
||||||
background-repeat: no-repeat
|
|
||||||
|
|
||||||
@media (max-width: 250px)
|
@media (max-width: 250px)
|
||||||
flex-direction: column
|
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
|
&_Nick
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
margin-left: .5rem
|
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|
||||||
&_Group
|
&_Group
|
||||||
margin-left: .5rem
|
|
||||||
border: 1px solid #eeeeee
|
border: 1px solid #eeeeee
|
||||||
border-radius: 5px
|
border-radius: 4px
|
||||||
padding: .1rem
|
padding: .1rem
|
||||||
|
text-align: center
|
||||||
|
font-size: 1rem
|
||||||
|
font-weight: 300
|
||||||
|
|
||||||
&_Avatar
|
&_Avatar
|
||||||
background-size: cover
|
background-size: cover
|
||||||
@@ -31,7 +54,7 @@
|
|||||||
width: 96px
|
width: 96px
|
||||||
height: 96px
|
height: 96px
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
margin-right: .2rem
|
margin-right: .5rem
|
||||||
|
|
||||||
&__Statistic
|
&__Statistic
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
@@ -64,6 +87,7 @@
|
|||||||
.User__Statistic_Blocks
|
.User__Statistic_Blocks
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
color: white
|
||||||
|
|
||||||
@media (max-width: 300px)
|
@media (max-width: 300px)
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import NotFoundError from "../NotFoundError";
|
|||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import Loading from "../Loading";
|
import Loading from "../Loading";
|
||||||
import NovelItem from "../NovelItem/NovelItem";
|
import NovelItem from "../NovelItem/NovelItem";
|
||||||
|
import {connect} from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: number } }
|
match: { params: { userId: string } }
|
||||||
|
currentUser: UserType
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -24,32 +26,52 @@ class User extends React.Component<Props, State> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const params = this.props.match.params;
|
const params = this.props.match.params;
|
||||||
|
|
||||||
fetchUser(params.userId)
|
fetchUser(parseInt(params.userId)).then((user: UserType) => {
|
||||||
.then((user: UserType) => {
|
console.log(user)
|
||||||
this.setState({ user });
|
this.setState({ user });
|
||||||
document.title = `${user.username} / Unmei`;
|
document.title = `${user.username} / Unmei`;
|
||||||
})
|
|
||||||
.catch((err: ApiError) => this.setState({ errorCode: err.code }));
|
|
||||||
|
|
||||||
fetchUserNovels(params.userId).then((novels: NovelType[]) => {
|
fetchUserNovels(parseInt(params.userId)).then((novels: NovelType[]) => {
|
||||||
this.setState({ novels });
|
this.setState({ novels });
|
||||||
}).catch((err: ApiError) => console.error(err));
|
}).catch((err: ApiError) => console.error(err));
|
||||||
|
}).catch((err: ApiError) => {
|
||||||
|
this.setState({ errorCode: err.code })
|
||||||
|
console.log(err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendActivateLink = () => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { currentUser } = this.props;
|
||||||
const { errorCode, user, novels } = this.state;
|
const { errorCode, user, novels } = this.state;
|
||||||
if(errorCode === 100) return <NotFoundError/>;
|
if(errorCode === 100) return <NotFoundError/>;
|
||||||
if(!user) return <Loading />;
|
if(!user) return <Loading />;
|
||||||
|
|
||||||
const style = user.cover ? { backgroundImage: `url(${user.cover})`, height: '20rem' } : {}
|
const style = !user.is_activated ? { marginTop: '1rem' } : {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'User'}>
|
<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" style={style}>
|
||||||
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar})` }} />
|
{user.cover && <div className="User__Info_Cover" style={{ backgroundImage: `url(${user.cover})` }}/>}
|
||||||
<div>
|
<div className={'User__Info_Main'}>
|
||||||
<div className='User__Info_Nick'>{user.username}</div>
|
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar})` }} />
|
||||||
<div className='User__Info_Group' style={{ borderColor: user.group.color, color: user.group.color }}>{user.group.name}</div>
|
<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>
|
||||||
</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
|
authorized: boolean
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
|
email?: string
|
||||||
avatar: string
|
avatar: string
|
||||||
cover: string
|
cover: string
|
||||||
group: UserGroupType
|
group: UserGroupType
|
||||||
is_superuser: boolean
|
is_superuser: boolean
|
||||||
|
is_activated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserNovelType = {
|
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 hasPermission = (user: UserType, permission: string) => {
|
||||||
const permissions = user?.group?.permissions.split(',');
|
const permissions = user?.group?.permissions.split(',');
|
||||||
return permissions?.filter(perm => perm === permission).length > 0;
|
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