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