Many new
@@ -1,52 +1,53 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.12.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
||||
"@sentry/browser": "^5.7.1",
|
||||
"@types/react-infinite-scroller": "^1.2.1",
|
||||
"axios": "^0.19.0",
|
||||
"bbcode-to-react": "^0.2.9",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-infinite-scroller": "^1.2.4",
|
||||
"react-loader-spinner": "^3.1.4",
|
||||
"react-recaptcha": "^2.3.10",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"redux": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "12.12.26",
|
||||
"@types/react": "16.9.19",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"node-sass": "^4.13.1",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"typescript": "^3.8.2",
|
||||
"react-scripts": "^3.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version"
|
||||
]
|
||||
}
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.12.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
||||
"@sentry/browser": "^5.7.1",
|
||||
"@types/react-infinite-scroller": "^1.2.1",
|
||||
"axios": "^0.19.0",
|
||||
"bbcode-to-react": "^0.2.9",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-infinite-scroller": "^1.2.4",
|
||||
"react-loader-spinner": "^3.1.4",
|
||||
"react-recaptcha": "^2.3.10",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"redux": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "12.12.26",
|
||||
"@types/react": "16.9.19",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"node-sass": "^4.13.1",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"typescript": "^3.8.2",
|
||||
"react-scripts": "^3.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome 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 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}`);
|
||||
@@ -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 ColorTag from "../Tags/ColorTag";
|
||||
import ActivateAccount from "../ActivateAccount";
|
||||
import AllNews from "../News/AllNews";
|
||||
import Post from "../News/Post";
|
||||
import AdminPanel from "../AdminPanel/Main/AdminPanel";
|
||||
|
||||
type Props = {
|
||||
notifications: React.ReactNode[]
|
||||
@@ -44,7 +47,7 @@ class App extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
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');
|
||||
if(theme)
|
||||
@@ -74,12 +77,21 @@ class App extends React.Component<Props, State> {
|
||||
</div>
|
||||
<Switch>
|
||||
<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/:novelId'} component={Novel}/>
|
||||
|
||||
<Route exact path={'/user/:userId'} component={User}/>
|
||||
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
|
||||
|
||||
<Route exact path={'/character/:charId'} component={Character}/>
|
||||
|
||||
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
||||
|
||||
<Route exact path={'/kawaii__neko'} component={AdminPanel}/>
|
||||
<Route component={NotFoundError}/>
|
||||
</Switch>
|
||||
|
||||
|
||||
@@ -4,34 +4,53 @@ import {Link} from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDiscord, faVk, faTelegramPlane } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
const Footer: React.FC = () => (
|
||||
<div className='Footer'>
|
||||
<footer className='Footer__Tabs'>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Команда</div>
|
||||
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
||||
</div>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Соцсети</div>
|
||||
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К тому же, эта кнопка секретная. Ты знал, на что шел ;)');
|
||||
}
|
||||
}
|
||||
|
||||
<div className="Links">
|
||||
<a href="https://vk.com/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faVk}/></div>
|
||||
</a>
|
||||
<a href="https://discord.gg/GMMymRU">
|
||||
<div className="Link"><FontAwesomeIcon icon={faDiscord}/></div>
|
||||
</a>
|
||||
<a href="https://t.me/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
||||
</a>
|
||||
return (
|
||||
<div className='Footer'>
|
||||
<footer className='Footer__Tabs'>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title" onClick={secretChangeTheme}>Команда</div>
|
||||
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
||||
</div>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Соцсети</div>
|
||||
|
||||
<div className="Links">
|
||||
<a href="https://vk.com/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faVk}/></div>
|
||||
</a>
|
||||
<a href="https://discord.gg/GMMymRU">
|
||||
<div className="Link"><FontAwesomeIcon icon={faDiscord}/></div>
|
||||
</a>
|
||||
<a href="https://t.me/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div className="Footer__Bottom">
|
||||
Unmei © 2019-2020
|
||||
</div>
|
||||
</footer>
|
||||
<div className="Footer__Bottom">
|
||||
Unmei © 2019-2020
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ class RegisterModal extends React.Component<Props, State> {
|
||||
<form className="RegisterModal__Form" onSubmit={this.submitForm}>
|
||||
<input
|
||||
type="text"
|
||||
min={4}
|
||||
max={20}
|
||||
pattern={'[a-zA-Z0-9_-~[]]+'}
|
||||
name={'login'}
|
||||
placeholder={'Логин'}
|
||||
className={'RegisterModal__Field'}
|
||||
@@ -83,6 +86,7 @@ class RegisterModal extends React.Component<Props, State> {
|
||||
type="email"
|
||||
name={'email'}
|
||||
placeholder={'E-mail'}
|
||||
pattern={'.+@\\w{2,6}\\.\\w{2,3}'}
|
||||
className={'RegisterModal__Field'}
|
||||
onChange={this.handleEmailChange}
|
||||
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 News from "./News";
|
||||
import './Main.sass';
|
||||
|
||||
class Main extends React.Component {
|
||||
render() {
|
||||
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
|
||||
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
|
||||
display: flex
|
||||
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() {
|
||||
const { errorCode, novel, comments, characters, genres, userData, statusExpanded, hasMoreComments } = this.state;
|
||||
const { currentUser } = this.props;
|
||||
|
||||
if(errorCode === 100) return <NotFoundError/>;
|
||||
if(!novel || !genres) return <Loading/>;
|
||||
if(!novel) return <Loading/>;
|
||||
return (
|
||||
<div className={'Novel'}>
|
||||
<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>}
|
||||
<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 className={'Novel__Info'}>
|
||||
@@ -167,7 +184,7 @@ class Novel extends React.Component<Props, State> {
|
||||
{novel.localized_name && <div className="Novel__Info_OriginalName">
|
||||
<strong>Оригинальное название</strong>: {novel.original_name}
|
||||
</div>}
|
||||
{genres.length > 0 &&
|
||||
{genres && genres.length > 0 &&
|
||||
<div className="Novel__Info_Genres">
|
||||
<strong>Жанры</strong>: {genres.map(genre => <div className={'Genre'} key={genre.id}>{genre.localized_name}</div>)}
|
||||
</div>}
|
||||
|
||||
@@ -11,23 +11,18 @@ type State = {
|
||||
}
|
||||
|
||||
class Novels extends React.Component<{}, State> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
novels: null,
|
||||
genres: [], years: []
|
||||
};
|
||||
|
||||
document.title = 'Новелы';
|
||||
}
|
||||
state: State = {
|
||||
novels: null,
|
||||
genres: [], years: []
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
fetchNovels('rating').then((novels: NovelType[]) => {
|
||||
novels?.forEach(novel => {
|
||||
novels.forEach(novel => {
|
||||
novel.release_date = new Date(novel.release_date);
|
||||
});
|
||||
this.setState({ novels });
|
||||
document.title = 'Новелы';
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
@import "../App/App"
|
||||
|
||||
.User
|
||||
.User__Info
|
||||
&__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
|
||||
|
||||
@media (max-width: 250px)
|
||||
flex-direction: column
|
||||
|
||||
.User__Info_Nick
|
||||
&_Nick
|
||||
font-size: 1.5rem
|
||||
margin-left: .5rem
|
||||
font-weight: 600
|
||||
|
||||
.User__Info_Group
|
||||
&_Group
|
||||
margin-left: .5rem
|
||||
border: 1px solid #eeeeee
|
||||
border-radius: 5px
|
||||
padding: .1rem
|
||||
|
||||
.User__Info_Avatar
|
||||
&_Avatar
|
||||
background-size: cover
|
||||
background-position: center
|
||||
width: 96px
|
||||
@@ -29,7 +33,7 @@
|
||||
border-radius: 50%
|
||||
margin-right: .2rem
|
||||
|
||||
.User__Statistic
|
||||
&__Statistic
|
||||
background-color: var(--sec-bg-color)
|
||||
border-radius: $borderRadius
|
||||
padding: $primaryPadding
|
||||
@@ -38,7 +42,7 @@
|
||||
flex-direction: row
|
||||
flex-wrap: wrap
|
||||
|
||||
.User__Statistic_Item
|
||||
&_Item
|
||||
width: calc(100% / 5 - 1rem)
|
||||
@media (max-width: 1060px)
|
||||
width: calc(100% / 4 - 1rem)
|
||||
@@ -69,11 +73,11 @@
|
||||
padding: .5rem
|
||||
height: 1.2rem !important
|
||||
|
||||
.User__Statistic_Block:first-child
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
&:first-child
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
|
||||
.User__Statistic_Block:last-child
|
||||
border-radius: 0 0 $borderRadius $borderRadius
|
||||
&:last-child
|
||||
border-radius: 0 0 $borderRadius $borderRadius
|
||||
|
||||
.Planned
|
||||
background: #339999
|
||||
@@ -95,19 +99,19 @@
|
||||
margin-left: 1rem
|
||||
height: 1rem
|
||||
|
||||
.User__List
|
||||
&__List
|
||||
background-color: var(--sec-bg-color)
|
||||
border-radius: $borderRadius
|
||||
padding: $primaryPadding
|
||||
margin-top: 1rem
|
||||
|
||||
.User__List_Title
|
||||
&_Title
|
||||
font-size: 1.2rem
|
||||
margin: .3rem 0
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
|
||||
.User__List_Items
|
||||
&_Items
|
||||
padding: $blockPadding * 3
|
||||
background-color: var(--block-sec-color)
|
||||
border-radius: $borderRadius
|
||||
|
||||
@@ -6,19 +6,20 @@ import {Link} from "react-router-dom";
|
||||
import Loading from "../Loading";
|
||||
import NovelItem from "../NovelItem/NovelItem";
|
||||
|
||||
type Props = {
|
||||
match: { params: { userId: number } }
|
||||
}
|
||||
|
||||
type State = {
|
||||
user: UserType | null
|
||||
novels: NovelType[]
|
||||
errorCode: number | null
|
||||
}
|
||||
|
||||
class User extends React.Component<any, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: null, novels: [], errorCode: null
|
||||
};
|
||||
}
|
||||
class User extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
user: null, novels: [], errorCode: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const params = this.props.match.params;
|
||||
@@ -40,9 +41,11 @@ class User extends React.Component<any, State> {
|
||||
if(errorCode === 100) return <NotFoundError/>;
|
||||
if(!user) return <Loading />;
|
||||
|
||||
const style = user.cover ? { backgroundImage: `url(${user.cover})`, height: '20rem' } : {}
|
||||
|
||||
return (
|
||||
<div className={'User'}>
|
||||
<div className="User__Info">
|
||||
<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>
|
||||
|
||||
@@ -5,4 +5,5 @@ import {composeWithDevTools} from "redux-devtools-extension";
|
||||
const composeEnhancers = composeWithDevTools({
|
||||
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
|
||||
username: string
|
||||
avatar: string
|
||||
cover: string
|
||||
group: UserGroupType
|
||||
is_superuser: boolean
|
||||
}
|
||||
|
||||
type UserNovelType = {
|
||||
@@ -40,6 +42,8 @@ type UserGroupType = {
|
||||
id: number
|
||||
name: string
|
||||
color: string
|
||||
is_superuser: boolean
|
||||
permissions: string
|
||||
}
|
||||
|
||||
type CommentType = {
|
||||
@@ -54,3 +58,17 @@ type GenreType = {
|
||||
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;
|
||||
};
|
||||
|
||||
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 };
|
||||