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
+51 -50
View File
@@ -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"
]
}
}
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 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}`);
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 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>
View File
View File
View File
View File
View File
View File
Executable → Regular
View File
View File
Executable → Regular
+43 -24
View File
@@ -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;
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}>
<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}/>
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 News from "./News";
import './Main.sass';
class Main extends React.Component {
render() {
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
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
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() {
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>}
View File
View File
View File
Executable → Regular
+6 -11
View File
@@ -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 = 'Новелы';
})
};
View File
View File
View File
Executable → Regular
View File
View File
View File
Executable → Regular
+17 -13
View File
@@ -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
Executable → Regular
+11 -8
View File
@@ -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>
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({
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
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[]
}
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;
};
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