Many fixes

This commit is contained in:
2020-04-29 02:50:07 +03:00
parent 9beee8f833
commit 9ff4779720
34 changed files with 764 additions and 281 deletions
+44
View File
@@ -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",
+2
View File
@@ -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
View File
@@ -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`);
+19
View File
@@ -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
+61
View File
@@ -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);
+13 -5
View File
@@ -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
+9 -5
View File
@@ -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 => ({
+2 -10
View File
@@ -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 &&
+38 -14
View File
@@ -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" }}>
<div style={{ display: "flex", alignItems: "center" }}>
<button type="submit" className={'Comments_Submit'}>Написать</button> <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);
+17 -11
View File
@@ -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
+2 -1
View File
@@ -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
+2 -2
View File
@@ -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>
+27 -10
View File
@@ -1,33 +1,46 @@
@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 display: flex
justify-content: center
align-items: center
cursor: pointer
.Header_Button:hover, .Header__User_Profile:hover &:hover
background-color: var(--main-bg-color) background-color: var(--main-bg-color)
.Header_User &__User
display: flex justify-content: flex-end
.Header__User_Profile &_Profile
display: flex display: flex
align-items: center align-items: center
font-size: 1rem font-size: 1rem
@@ -36,8 +49,12 @@
background-color: var(--nav-bg-color) background-color: var(--nav-bg-color)
border: none border: none
border-radius: 0 border-radius: 0
padding: 0 .5rem
.Header__User_Avatar &:hover
background-color: var(--main-bg-color)
&_Avatar
background-size: cover background-size: cover
background-position: center background-position: center
width: 40px width: 40px
+51 -25
View File
@@ -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;
return ( const { expand } = this.state;
<nav className={'Header'}>
<div className="Header_Links"> const isMinimized = window.screen.width <= 1000;
<Link to="/">
<button className={'Header_Button'}> const links = (
<div className="Header__Links">
<Link to="/" className={'Header_Button'}>
Главная Главная
</button>
</Link> </Link>
<Link to="/novels"> <Link to="/novels" className={'Header_Button'}>
<button className={'Header_Button'}>
Новеллы Новеллы
</button>
</Link>
<Link to="/">
<button className={'Header_Button'}>
Пользователи
</button>
</Link> </Link>
{expand && isMinimized && (
<div onClick={this.changeSize} className={'Header_Button'} style={{ position: "absolute", right: 0, margin: '.5rem' }}>X</div>
)}
</div> </div>
{this.props.currentUser && );
<div className={'Header_User'}>
{this.props.currentUser.authorized && return (
<Link to={`/user/${currentUser.id}`}> <nav className={`Header ${expand ? 'Expanded' : 'Minimized'}`}>
<button className="Header__User_Profile"> {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 className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar})` }}/>
<div>{this.props.currentUser.username}</div> <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>
) )
+27 -4
View File
@@ -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>
); );
} }
} }
+4 -2
View File
@@ -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
+2 -1
View File
@@ -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
+11 -9
View File
@@ -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>
); );
} }
+3
View File
@@ -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)
+32 -25
View File
@@ -67,21 +67,19 @@ 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 })
@@ -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) fetchNovelCharacters(params.novelId).then(characters => this.setState({ characters }));
.then(characters => this.setState({ characters })); fetchNovelGenres(params.novelId).then(genres => this.setState({ genres }));
fetchNovelGenres(params.novelId) }).catch((err: ApiError) => {
.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) => {
.catch((err: ApiError) => {
if(err.code !== 100) console.error(err.text); 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
View File
@@ -35,3 +35,4 @@
&__Status &__Status
font-weight: 300 font-weight: 300
display: flex
+10 -3
View File
@@ -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) => {
console.log(mark !== undefined && mark > 0 && <div>{mark}</div>)
return (
<Link to={`/novels/${id}`} className="NovelItem"> <Link to={`/novels/${id}`} className="NovelItem">
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/> <div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
<div className="NovelItem__Title"> <div className="NovelItem__Title">
{localized_name || original_name} {localized_name || original_name}
</div> </div>
{status && <div className="NovelItem__Status">{TranslateStatus[status]}</div>} {status && <div className="NovelItem__Status">
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
</div>}
</Link> </Link>
); );
}
export default NovelItem; export default NovelItem;
+3
View File
@@ -3,3 +3,6 @@
.Spoiler .Spoiler
cursor: pointer cursor: pointer
&:hover
color: var(--link-color)
text-decoration: underline
@@ -6,6 +6,7 @@
&__Title &__Title
font-size: 1.5rem font-size: 1.5rem
font-weight: 600
text-align: center text-align: center
&__List &__List
@@ -22,6 +23,9 @@
font-size: 1.5rem font-size: 1.5rem
margin-right: 1rem margin-right: 1rem
button
font-weight: 600
&_Novels &_Novels
display: flex display: flex
flex-direction: row flex-direction: row
+32 -8
View File
@@ -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%
+34 -8
View File
@@ -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,34 +26,54 @@ 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}>
{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 className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar})` }} />
<div> <div>
<div className='User__Info_Nick'>{user.username}</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 className='User__Info_Group' style={{ borderColor: user.group.color, color: user.group.color }}>{user.group.name}</div>
</div> </div>
</div> </div>
</div>
<div className="User__Statistic"> <div className="User__Statistic">
<div className="User__Statistic_Item"> <div className="User__Statistic_Item">
@@ -105,4 +127,8 @@ class User extends React.Component<Props, State> {
} }
} }
export default User; export default connect(
(state: StoreState) => ({
currentUser: state.currentUser
})
)(User);
+2
View File
@@ -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
View File
@@ -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 };