Many new
@@ -46,7 +46,8 @@
|
|||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version",
|
"last 1 chrome version",
|
||||||
"last 1 firefox version"
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
import {get} from './api';
|
||||||
|
|
||||||
|
const getNews = () => get('news');
|
||||||
|
const getPost = (id: number) => get(`news/${id}`);
|
||||||
|
|
||||||
|
export { getNews, getPost }
|
||||||
@@ -11,5 +11,5 @@ export const postNovelComment = (id: number, text: string) => post(`novels/${id}
|
|||||||
|
|
||||||
export const fetchUserNovel = (userId: number, novelId: number) => get(`users/${userId}/novels/${novelId}`);
|
export const fetchUserNovel = (userId: number, novelId: number) => get(`users/${userId}/novels/${novelId}`);
|
||||||
export const createUserNovel = (userId: number, novelId: number, status='planned') => post(`users/${userId}/novels`, { novel_id: Number(novelId), status });
|
export const createUserNovel = (userId: number, novelId: number, status='planned') => post(`users/${userId}/novels`, { novel_id: Number(novelId), status });
|
||||||
export const updateUserNovel = (userId: number, novelId: number, data: UserNovelType) => put(`users/${userId}/novels/${novelId}`, data);
|
export const updateUserNovel = (userId: number, novelId: number, data: { mark?: number; status?: string }) => put(`users/${userId}/novels/${novelId}`, data);
|
||||||
export const deleteUserNovel = (userId: number, novelId: number) => del(`users/${userId}/novels/${novelId}`);
|
export const deleteUserNovel = (userId: number, novelId: number) => del(`users/${userId}/novels/${novelId}`);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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);
|
||||||
@@ -20,6 +20,9 @@ import Footer from '../Footer/Footer';
|
|||||||
import UserNovels from '../User/Novels/UserNovels';
|
import UserNovels from '../User/Novels/UserNovels';
|
||||||
import ColorTag from "../Tags/ColorTag";
|
import ColorTag from "../Tags/ColorTag";
|
||||||
import ActivateAccount from "../ActivateAccount";
|
import ActivateAccount from "../ActivateAccount";
|
||||||
|
import AllNews from "../News/AllNews";
|
||||||
|
import Post from "../News/Post";
|
||||||
|
import AdminPanel from "../AdminPanel/Main/AdminPanel";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notifications: React.ReactNode[]
|
notifications: React.ReactNode[]
|
||||||
@@ -44,7 +47,7 @@ class App extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { setUser } = this.props;
|
const { setUser } = this.props;
|
||||||
fetchCurrentUser().then(currentUser => setUser(currentUser)).catch(console.log);
|
fetchCurrentUser().then(currentUser => setUser(currentUser)).catch(console.error);
|
||||||
|
|
||||||
const theme = localStorage.getItem('theme');
|
const theme = localStorage.getItem('theme');
|
||||||
if(theme)
|
if(theme)
|
||||||
@@ -74,12 +77,21 @@ class App extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={'/'} component={Main}/>
|
<Route exact path={'/'} component={Main}/>
|
||||||
|
|
||||||
|
<Route exact path={'/news'} component={AllNews}/>
|
||||||
|
<Route exact path={'/news/:postId'} component={Post}/>
|
||||||
|
|
||||||
<Route exact path={'/novels'} component={Novels}/>
|
<Route exact path={'/novels'} component={Novels}/>
|
||||||
<Route exact path={'/novels/:novelId'} component={Novel}/>
|
<Route exact path={'/novels/:novelId'} component={Novel}/>
|
||||||
|
|
||||||
<Route exact path={'/user/:userId'} component={User}/>
|
<Route exact path={'/user/:userId'} component={User}/>
|
||||||
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
|
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
|
||||||
|
|
||||||
<Route exact path={'/character/:charId'} component={Character}/>
|
<Route exact path={'/character/:charId'} component={Character}/>
|
||||||
|
|
||||||
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
||||||
|
|
||||||
|
<Route exact path={'/kawaii__neko'} component={AdminPanel}/>
|
||||||
<Route component={NotFoundError}/>
|
<Route component={NotFoundError}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,29 @@ import {Link} from "react-router-dom";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faDiscord, faVk, faTelegramPlane } from '@fortawesome/free-brands-svg-icons';
|
import { faDiscord, faVk, faTelegramPlane } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
|
||||||
const Footer: React.FC = () => (
|
const Footer: React.FC = () => {
|
||||||
|
const secretChangeTheme = () => {
|
||||||
|
const theme = localStorage.getItem('theme');
|
||||||
|
if(theme == null) {
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
document.body.setAttribute('theme', 'light')
|
||||||
|
} else if(theme === 'light') {
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
document.body.setAttribute('theme', 'dark')
|
||||||
|
} else if(theme === 'dark') {
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
document.body.setAttribute('theme', 'light')
|
||||||
|
}
|
||||||
|
if(theme !== 'light') {
|
||||||
|
console.log('Светлая тема находится в разработке! Не все может корректно отображаться.\nК тому же, эта кнопка секретная. Ты знал, на что шел ;)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className='Footer'>
|
<div className='Footer'>
|
||||||
<footer className='Footer__Tabs'>
|
<footer className='Footer__Tabs'>
|
||||||
<div className="Footer__Tab">
|
<div className="Footer__Tab">
|
||||||
<div className="Footer__Tab_Title">Команда</div>
|
<div className="Footer__Tab_Title" onClick={secretChangeTheme}>Команда</div>
|
||||||
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="Footer__Tab">
|
<div className="Footer__Tab">
|
||||||
@@ -31,7 +49,8 @@ const Footer: React.FC = () => (
|
|||||||
Unmei © 2019-2020
|
Unmei © 2019-2020
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
<form className="RegisterModal__Form" onSubmit={this.submitForm}>
|
<form className="RegisterModal__Form" onSubmit={this.submitForm}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
min={4}
|
||||||
|
max={20}
|
||||||
|
pattern={'[a-zA-Z0-9_-~[]]+'}
|
||||||
name={'login'}
|
name={'login'}
|
||||||
placeholder={'Логин'}
|
placeholder={'Логин'}
|
||||||
className={'RegisterModal__Field'}
|
className={'RegisterModal__Field'}
|
||||||
@@ -83,6 +86,7 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
type="email"
|
type="email"
|
||||||
name={'email'}
|
name={'email'}
|
||||||
placeholder={'E-mail'}
|
placeholder={'E-mail'}
|
||||||
|
pattern={'.+@\\w{2,6}\\.\\w{2,3}'}
|
||||||
className={'RegisterModal__Field'}
|
className={'RegisterModal__Field'}
|
||||||
onChange={this.handleEmailChange}
|
onChange={this.handleEmailChange}
|
||||||
value={email}/>
|
value={email}/>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.News
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
&__Title
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
justify-content: space-between
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 600
|
||||||
|
margin-bottom: .5rem
|
||||||
|
|
||||||
|
.Post
|
||||||
|
background-color: var(--block-color)
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
&__Title
|
||||||
|
font-size: 1.2rem
|
||||||
|
|
||||||
|
&__Content, &__Title
|
||||||
|
border-bottom: var(--link-color) 1px solid
|
||||||
|
|
||||||
|
&__Footer
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
|
||||||
|
*
|
||||||
|
padding: .2rem
|
||||||
|
|
||||||
|
&:not(:last-child)
|
||||||
|
margin-bottom: 1rem
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import News from "./News";
|
||||||
|
import './Main.sass';
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>В разработке!</div>
|
<div className={'Main'}>
|
||||||
|
<News/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {getNews} from "../../api/news";
|
||||||
|
import Loading from "../Loading";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
news: PostType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class News extends React.Component<{}, State> {
|
||||||
|
state: State = {
|
||||||
|
news: []
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
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 className="News">
|
||||||
|
<div className="News__Title">
|
||||||
|
<div>Новости</div>
|
||||||
|
<Link to={'/news'}>Все новости</Link>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default News;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class AllNews extends React.Component<{}, State> {
|
||||||
|
state: State = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AllNews;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.Post
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
&__Title
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 600
|
||||||
|
border-bottom: 1px solid var(--main-color)
|
||||||
|
|
||||||
|
&__Content
|
||||||
|
margin-top: .1rem
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {getPost} from "../../api/news";
|
||||||
|
import Loading from "../Loading";
|
||||||
|
import NotFoundError from "../NotFoundError";
|
||||||
|
import './Post.sass';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
match: { params: { postId: number } }
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
post: PostType | null
|
||||||
|
code: number
|
||||||
|
};
|
||||||
|
|
||||||
|
class Post extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
post: null, code: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
const { match: { params: { postId }} } = this.props;
|
||||||
|
getPost(postId).then(post => {
|
||||||
|
this.setState({ post })
|
||||||
|
}).catch((error: ApiError) => {
|
||||||
|
this.setState({ code: error.code })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { post, code } = this.state;
|
||||||
|
|
||||||
|
if(code === 100) return <NotFoundError/>;
|
||||||
|
if(!post) return <Loading/>;
|
||||||
|
return (
|
||||||
|
<div className={'Post'}>
|
||||||
|
<div className={'Post__Title'}>{post.title}</div>
|
||||||
|
<div className="Post__Content">
|
||||||
|
{post.full_post}
|
||||||
|
</div>
|
||||||
|
<div className="Post__Footer">
|
||||||
|
{post.author}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Post;
|
||||||
@@ -23,6 +23,22 @@
|
|||||||
.Novel__Main_Status
|
.Novel__Main_Status
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
&_Mark
|
||||||
|
margin-top: .5rem
|
||||||
|
width: 100%
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
|
||||||
|
&_Element
|
||||||
|
padding: .5rem
|
||||||
|
background-color: var(--block-color)
|
||||||
|
width: 100%
|
||||||
|
text-align: center
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
|
|
||||||
&_Primary
|
&_Primary
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: space-between
|
justify-content: space-between
|
||||||
|
|||||||
@@ -132,12 +132,18 @@ class Novel extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }));
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { errorCode, novel, comments, characters, genres, userData, statusExpanded, hasMoreComments } = this.state;
|
const { errorCode, novel, comments, characters, genres, userData, statusExpanded, hasMoreComments } = this.state;
|
||||||
const { currentUser } = this.props;
|
const { currentUser } = this.props;
|
||||||
|
|
||||||
if(errorCode === 100) return <NotFoundError/>;
|
if(errorCode === 100) return <NotFoundError/>;
|
||||||
if(!novel || !genres) return <Loading/>;
|
if(!novel) return <Loading/>;
|
||||||
return (
|
return (
|
||||||
<div className={'Novel'}>
|
<div className={'Novel'}>
|
||||||
<div className="Novel__Main">
|
<div className="Novel__Main">
|
||||||
@@ -160,6 +166,17 @@ 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'}>
|
||||||
|
{[1,2,3,4,5,6,7,8,9,10].map(mark => (
|
||||||
|
<div
|
||||||
|
key={mark}
|
||||||
|
className={'Novel__Main_Status_Mark_Element'}
|
||||||
|
onClick={() => this.updateNovelMark(mark)}
|
||||||
|
>
|
||||||
|
{mark}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className={'Novel__Info'}>
|
<div className={'Novel__Info'}>
|
||||||
@@ -167,7 +184,7 @@ class Novel extends React.Component<Props, State> {
|
|||||||
{novel.localized_name && <div className="Novel__Info_OriginalName">
|
{novel.localized_name && <div className="Novel__Info_OriginalName">
|
||||||
<strong>Оригинальное название</strong>: {novel.original_name}
|
<strong>Оригинальное название</strong>: {novel.original_name}
|
||||||
</div>}
|
</div>}
|
||||||
{genres.length > 0 &&
|
{genres && genres.length > 0 &&
|
||||||
<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>}
|
||||||
|
|||||||
@@ -11,23 +11,18 @@ type State = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Novels extends React.Component<{}, State> {
|
class Novels extends React.Component<{}, State> {
|
||||||
constructor(props: {}) {
|
state: State = {
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
novels: null,
|
novels: null,
|
||||||
genres: [], years: []
|
genres: [], years: []
|
||||||
};
|
};
|
||||||
|
|
||||||
document.title = 'Новелы';
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
fetchNovels('rating').then((novels: NovelType[]) => {
|
fetchNovels('rating').then((novels: NovelType[]) => {
|
||||||
novels?.forEach(novel => {
|
novels.forEach(novel => {
|
||||||
novel.release_date = new Date(novel.release_date);
|
novel.release_date = new Date(novel.release_date);
|
||||||
});
|
});
|
||||||
this.setState({ novels });
|
this.setState({ novels });
|
||||||
|
document.title = 'Новелы';
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
@import "../App/App"
|
@import "../App/App"
|
||||||
|
|
||||||
.User
|
.User
|
||||||
.User__Info
|
&__Info
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
display: flex
|
display: flex
|
||||||
align-items: flex-end
|
align-items: flex-end
|
||||||
|
background-position: center
|
||||||
|
background-size: cover
|
||||||
|
background-repeat: no-repeat
|
||||||
|
|
||||||
@media (max-width: 250px)
|
@media (max-width: 250px)
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
.User__Info_Nick
|
&_Nick
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
margin-left: .5rem
|
margin-left: .5rem
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
.User__Info_Group
|
&_Group
|
||||||
margin-left: .5rem
|
margin-left: .5rem
|
||||||
border: 1px solid #eeeeee
|
border: 1px solid #eeeeee
|
||||||
border-radius: 5px
|
border-radius: 5px
|
||||||
padding: .1rem
|
padding: .1rem
|
||||||
|
|
||||||
.User__Info_Avatar
|
&_Avatar
|
||||||
background-size: cover
|
background-size: cover
|
||||||
background-position: center
|
background-position: center
|
||||||
width: 96px
|
width: 96px
|
||||||
@@ -29,7 +33,7 @@
|
|||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
margin-right: .2rem
|
margin-right: .2rem
|
||||||
|
|
||||||
.User__Statistic
|
&__Statistic
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
@@ -38,7 +42,7 @@
|
|||||||
flex-direction: row
|
flex-direction: row
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
|
||||||
.User__Statistic_Item
|
&_Item
|
||||||
width: calc(100% / 5 - 1rem)
|
width: calc(100% / 5 - 1rem)
|
||||||
@media (max-width: 1060px)
|
@media (max-width: 1060px)
|
||||||
width: calc(100% / 4 - 1rem)
|
width: calc(100% / 4 - 1rem)
|
||||||
@@ -69,10 +73,10 @@
|
|||||||
padding: .5rem
|
padding: .5rem
|
||||||
height: 1.2rem !important
|
height: 1.2rem !important
|
||||||
|
|
||||||
.User__Statistic_Block:first-child
|
&:first-child
|
||||||
border-radius: $borderRadius $borderRadius 0 0
|
border-radius: $borderRadius $borderRadius 0 0
|
||||||
|
|
||||||
.User__Statistic_Block:last-child
|
&:last-child
|
||||||
border-radius: 0 0 $borderRadius $borderRadius
|
border-radius: 0 0 $borderRadius $borderRadius
|
||||||
|
|
||||||
.Planned
|
.Planned
|
||||||
@@ -95,19 +99,19 @@
|
|||||||
margin-left: 1rem
|
margin-left: 1rem
|
||||||
height: 1rem
|
height: 1rem
|
||||||
|
|
||||||
.User__List
|
&__List
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
margin-top: 1rem
|
margin-top: 1rem
|
||||||
|
|
||||||
.User__List_Title
|
&_Title
|
||||||
font-size: 1.2rem
|
font-size: 1.2rem
|
||||||
margin: .3rem 0
|
margin: .3rem 0
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: space-between
|
justify-content: space-between
|
||||||
|
|
||||||
.User__List_Items
|
&_Items
|
||||||
padding: $blockPadding * 3
|
padding: $blockPadding * 3
|
||||||
background-color: var(--block-sec-color)
|
background-color: var(--block-sec-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
|
|||||||
@@ -6,19 +6,20 @@ import {Link} from "react-router-dom";
|
|||||||
import Loading from "../Loading";
|
import Loading from "../Loading";
|
||||||
import NovelItem from "../NovelItem/NovelItem";
|
import NovelItem from "../NovelItem/NovelItem";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
match: { params: { userId: number } }
|
||||||
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
user: UserType | null
|
user: UserType | null
|
||||||
novels: NovelType[]
|
novels: NovelType[]
|
||||||
errorCode: number | null
|
errorCode: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
class User extends React.Component<any, State> {
|
class User extends React.Component<Props, State> {
|
||||||
constructor(props: any) {
|
state: State = {
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
user: null, novels: [], errorCode: null
|
user: null, novels: [], errorCode: null
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const params = this.props.match.params;
|
const params = this.props.match.params;
|
||||||
@@ -40,9 +41,11 @@ class User extends React.Component<any, 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' } : {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'User'}>
|
<div className={'User'}>
|
||||||
<div className="User__Info">
|
<div className="User__Info" style={style}>
|
||||||
<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>
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ import {composeWithDevTools} from "redux-devtools-extension";
|
|||||||
const composeEnhancers = composeWithDevTools({
|
const composeEnhancers = composeWithDevTools({
|
||||||
trace: true
|
trace: true
|
||||||
});
|
});
|
||||||
export default createStore(index, composeEnhancers());
|
const store = process.env.NODE_ENV === 'development' ? createStore(index, composeEnhancers()) : createStore(index);
|
||||||
|
export default store;
|
||||||
@@ -28,7 +28,9 @@ type UserType = {
|
|||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
avatar: string
|
avatar: string
|
||||||
|
cover: string
|
||||||
group: UserGroupType
|
group: UserGroupType
|
||||||
|
is_superuser: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserNovelType = {
|
type UserNovelType = {
|
||||||
@@ -40,6 +42,8 @@ type UserGroupType = {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
color: string
|
color: string
|
||||||
|
is_superuser: boolean
|
||||||
|
permissions: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommentType = {
|
type CommentType = {
|
||||||
@@ -54,3 +58,17 @@ type GenreType = {
|
|||||||
name: string
|
name: string
|
||||||
localized_name: string
|
localized_name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostType = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
short_post: string
|
||||||
|
full_post: string
|
||||||
|
date: Date
|
||||||
|
author: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreState = {
|
||||||
|
currentUser: UserType
|
||||||
|
notifications: React.Component[]
|
||||||
|
}
|
||||||
@@ -8,4 +8,9 @@ const getRandomInt: (min: number, max: number) => number = (min: number, max: nu
|
|||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { capitalize, getRandomInt };
|
const hasPermission = (user: UserType, permission: string) => {
|
||||||
|
const permissions = user?.group?.permissions.split(',');
|
||||||
|
return permissions?.filter(perm => perm === permission).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { capitalize, getRandomInt, hasPermission };
|
||||||