This commit is contained in:
2020-04-25 18:46:23 +03:00
parent 36cff8be53
commit 9beee8f833
79 changed files with 409 additions and 113 deletions
Executable → Regular
View File
Executable → Regular
View File
Generated Executable → Regular
View File
Executable → Regular
+2 -1
View File
@@ -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"
] ]
} }
} }
Executable → Regular
View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 304 B

View File

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 402 B

Executable → Regular
View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Executable → Regular
View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
+6
View File
@@ -0,0 +1,6 @@
import {get} from './api';
const getNews = () => get('news');
const getPost = (id: number) => get(`news/${id}`);
export { getNews, getPost }
Executable → Regular
+1 -1
View File
@@ -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}`);
Executable → Regular
View File
View File
@@ -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);
Executable → Regular
View File
Executable → Regular
+13 -1
View File
@@ -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>
View File
View File
View File
View File
View File
View File
Executable → Regular
View File
View File
Executable → Regular
+22 -3
View File
@@ -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;
Executable → Regular
View File
View File
View File
View File
+4
View File
@@ -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}/>
Executable → Regular
+31
View File
@@ -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
Executable → Regular
+5 -1
View File
@@ -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>
); );
} }
} }
+47
View File
@@ -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;
Executable → Regular
View File
Executable → Regular
View File
View File
Executable → Regular
View File
+23
View File
@@ -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;
+11
View File
@@ -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
+49
View File
@@ -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;
Executable → Regular
View File
View File
View File
Executable → Regular
+16
View File
@@ -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
Executable → Regular
+19 -2
View File
@@ -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>}
View File
View File
View File
Executable → Regular
+3 -8
View File
@@ -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 = 'Новелы';
}) })
}; };
View File
View File
View File
Executable → Regular
View File
View File
View File
Executable → Regular
+15 -11
View File
@@ -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
Executable → Regular
+9 -6
View File
@@ -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>
Executable → Regular
View File
Vendored Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
View File
Executable → Regular
View File
View File
Executable → Regular
+2 -1
View File
@@ -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;
Vendored Executable → Regular
+18
View File
@@ -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[]
}
Executable → Regular
+6 -1
View File
@@ -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 };
Executable → Regular
View File