This commit is contained in:
2020-06-24 18:56:24 +03:00
parent 9447ee613b
commit 5d4972145b
119 changed files with 5412 additions and 5086 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
$mainColor: #efefef
$secondaryColor: #aaaaaa
$linkHoverColor: #dddddd
$linkHoverColor: #dfdfdf
// BLACK
$primaryBackgroundColor: #121212
Regular → Executable
+13 -7
View File
@@ -1,17 +1,22 @@
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.nix13.pw/v1';
export const response = async (url: string, method: string, body?: any) => {
export const response = async (url: string, method: string, body?: string | FormData) => {
let bUrl = baseUrl;
if(baseUrl.endsWith('/')) bUrl = bUrl.slice(0, baseUrl.length-1);
if(url.startsWith('/')) url = url.slice(1, baseUrl.length);
if(typeof body === 'object') body = JSON.stringify(body);
const res = await fetch(`${bUrl}/${url}`, {
method, body,
credentials: "include"
});
const json: ApiResponse = await res.json();
let json: ApiResponse;
try {
json = await res.json();
} catch (e) {
json = { error: false };
}
if(res.status === 429) return new Promise(res => {
setTimeout(res.bind(null, response(url, method, body)), 1500);
@@ -21,10 +26,10 @@ export const response = async (url: string, method: string, body?: any) => {
return Promise.resolve(json.data);
}
const get = (url: string, data?: object) => response(url, 'GET', data);
const post = (url: string, data?: object) => response(url, 'POST', data);
const put = (url: string, data?: object) => response(url, 'PUT', data);
const del = (url: string, data?: object) => response(url, 'DELETE', data);
const get = (url: string, data?: object) => response(url, 'GET', JSON.stringify(data));
const post = (url: string, data?: object | FormData) => response(url, 'POST', data instanceof FormData ? data : JSON.stringify(data));
const put = (url: string, data?: object) => response(url, 'PUT', JSON.stringify(data));
const del = (url: string, data?: object) => response(url, 'DELETE', JSON.stringify(data));
const TranslateStatus: { [id: string]: string } = {
planned: 'Запланировано',
@@ -35,5 +40,6 @@ const TranslateStatus: { [id: string]: string } = {
};
export const getVersion = () => get('version');
export const version = '0.9';
export { get, post, put, del, TranslateStatus };
Regular → Executable
+2 -2
View File
@@ -1,3 +1,3 @@
import {post} from "./api";
import {post} from "./api";
export const restore = (token: string, new_password: string) => post('auth/restore', { token, new_password });
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+6 -4
View File
@@ -1,6 +1,8 @@
import {get} from './api';
import {get, put} from './api';
const getNews = () => get('news');
const getPost = (id: number) => get(`news/${id}`);
const fetchNews = () => get('news');
export { getNews, getPost }
const fetchPost = (id: number) => get(`news/${id}`);
const updatePost = (post: PostType) => put(`news/${post.id}`, post);
export { fetchNews, fetchPost, updatePost }
Regular → Executable
View File
Regular → Executable
+5 -1
View File
@@ -5,7 +5,7 @@ export const fetchUsers = () => get('users');
export const fetchUser = (id: number) => get(`users/${id}`);
export const updateUser = (user: UserType) => put(`users/${user.id}`, { user })
export const fetchCurrentUser = () => get('users/current');
export const fetchCurrentUser = () => get('users/me');
export const fetchUserNovels = (id: number) => get(`users/${id}/novels`);
export const registerUser = (login: string, password: string, email: string, recaptcha: string) => post('auth/register', { login, password, email, recaptcha });
@@ -15,4 +15,8 @@ export const userLogout = () => post('auth/logout');
export const activateAccount = (token: string) => post('auth/activate', { token });
export const generateActivateLink = () => post('auth/activateToken');
export const fetchUserSettings = () => get('users/me/settings');
export const updateUserSettings = (use_gravatar: boolean, avatar: string) => post('users/me/settings', { use_gravatar, avatar });
export const uploadAvatar = (avatar: FormData) => post('users/me/avatar', avatar);
export const generateRestoreLink = (email: string, recaptcha: string) => post('auth/restoreToken', { email, recaptcha });
+31
View File
@@ -0,0 +1,31 @@
export interface ObjectClassNames {
[index: string]: boolean;
}
export type ClassName = number | string | ObjectClassNames | false | null | undefined;
export default function classNames(...classNames: ClassName[]) {
let result: string[] = [];
classNames.forEach((item: ClassName): void => {
if (!item) {
return;
}
switch (typeof item) {
case 'string':
result.push(item);
break;
case 'object':
Object.keys(item).forEach((key: string) => {
if (item[key]) {
result.push(key);
}
});
break;
default:
result.push(`${item}`);
}
});
return result.join(' ');
}
-29
View File
@@ -1,29 +0,0 @@
.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
border-top: var(--link-color) 1px solid
border-bottom: var(--link-color) 1px solid
&__Footer
display: flex
justify-content: space-between
&:not(:last-child)
margin-bottom: 1rem
-49
View File
@@ -1,49 +0,0 @@
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.slice(0, 5).map(post => (
<div className={'Post'} key={post.id}>
<div className="Post__Title"><Link to={`/news/${post.id }`}>{post.title}</Link></div>
<div className="Post__Content">{post.short_post}</div>
<div className="Post__Footer">
<div>
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
</div>
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
</div>
</div>
))}
</div>
);
}
}
export default News;
-22
View File
@@ -1,22 +0,0 @@
.Modal__Overlay
display: flex
align-items: center
justify-content: center
position: fixed
top: 0
right: 0
left: 0
bottom: 0
background-color: #0f0f0fef
z-index: 9999
opacity: 1
overflow: hidden
.Modal__CloseButton
background: none
border: none
font-size: 1.5rem
position: absolute
top: 0
right: 0
margin: 1rem
@@ -1,13 +0,0 @@
.PasswordRestore
display: flex
flex-direction: column
button, input
margin: .5rem
font-size: 1.2rem
input
width: 100%
button
align-self: flex-start
Regular → Executable
+1 -1
View File
@@ -7,7 +7,7 @@ import store from "./store/store";
// import * as Sentry from '@sentry/browser';
import App from './components/App/App';
import App from './pages/App/App';
import * as serviceWorker from './serviceWorker';
+1 -1
View File
@@ -3,7 +3,7 @@ import {activateAccount} from "../api/users";
import {connect} from "react-redux";
import {addNotification} from "../store/actions";
import errors from "../api/errors";
import Loading from "./Loading";
import Loading from "../ui/Loading";
type Props = {
history: { push: (path: string) => void }
+18 -18
View File
@@ -1,19 +1,19 @@
@import "../Constants"
.AdminPanel
background-color: var(--sec-bg-color)
padding: .5rem
&__Title
font-size: 1.5rem
font-weight: 600
text-align: center
&__Router
display: flex
flex-wrap: wrap
border-bottom: 1px solid white
a
margin: 0 .2rem
@import "../../Constants"
.AdminPanel
background-color: var(--sec-bg-color)
padding: .5rem
&__Title
font-size: 1.5rem
font-weight: 600
text-align: center
&__Router
display: flex
flex-wrap: wrap
border-bottom: 1px solid white
a
margin: 0 .2rem
padding: .1rem
+62 -60
View File
@@ -1,61 +1,63 @@
import {connect} from "react-redux";
import React from "react";
import NotFoundError from "../NotFoundError";
import {hasAccessToAdminPanel} from "../../utils";
import './AdminPanel.sass';
import {Link} from "react-router-dom";
import AdminPanelUsers from "./Users/AdminPanelUsers";
import AdminPanelNovels from "./Novels/AdminPanelNovels";
import ModifyUser from "./Users/ModifyUser";
type Props = {
user: UserType
match: { path: string }
location: { pathname: string }
}
type State = {
}
class AdminPanel extends React.Component<Props, State> {
render() {
const { user, match: { path }, location: { pathname } } = this.props;
if(!user || !user.group) return <NotFoundError/>;
if(!hasAccessToAdminPanel(user)) return <NotFoundError/>;
const subPath = pathname.slice(path.length+1);
let component;
switch(subPath) {
case 'users': component = <AdminPanelUsers path={`${path}/${subPath}`}/>; break;
case 'novels': component = <AdminPanelNovels/>; break;
case 'news': component = <div>Новости</div>; break;
default: {
const users = /users\/(\d+)/;
const userMatch = subPath.match(users);
if(userMatch)
component = <ModifyUser userId={parseInt(userMatch[1])}/>;
}
}
return (
<div className={'AdminPanel'}>
<div className="AdminPanel__Title">Админ панель <span style={{ color: 'var(--error-bg-color)' }}>(в разработке)</span></div>
<div className="AdminPanel__Router">
<Link to={path}>Главная</Link>
<Link to={`${path}/users`}>Пользователи</Link>
<Link to={`${path}/novels`}>Новелы</Link>
<Link to={`${path}/news`}>Новости</Link>
</div>
{component}
</div>
);
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
})
import {connect} from "react-redux";
import React from "react";
import NotFoundError from "../NotFoundError";
import {hasAccessToAdminPanel} from "../../utils";
import './AdminPanel.sass';
import {Link} from "react-router-dom";
import AdminPanelUsers from "./Users/AdminPanelUsers";
import AdminPanelNovels from "./Novels/AdminPanelNovels";
import ModifyUser from "./Users/ModifyUser";
import APNews from "./News/APNews";
import APModifyPost from "./News/APModifyPost";
type Props = {
user: UserType
match: { path: string }
location: { pathname: string }
}
type State = {}
class AdminPanel extends React.Component<Props, State> {
render() {
const { user, match: { path }, location: { pathname } } = this.props;
if(!user || !user.group) return <NotFoundError/>;
if(!hasAccessToAdminPanel(user)) return <NotFoundError/>;
const subPath = pathname.slice(path.length+1);
let component;
switch(subPath) {
case 'users': component = <AdminPanelUsers path={`${path}/${subPath}`}/>; break;
case 'novels': component = <AdminPanelNovels/>; break;
case 'news': component = <APNews path={`${path}/${subPath}`}/>; break;
default: {
const users = /users\/(\d+)/;
const news = /news\/(\d+)/;
const userMatch = subPath.match(users);
const newsMatch = subPath.match(news);
if(userMatch) component = <ModifyUser userId={parseInt(userMatch[1])}/>;
if(newsMatch) component = <APModifyPost postId={parseInt(newsMatch[1])}/>;
}
}
return (
<div className={'AdminPanel'}>
<div className="AdminPanel__Title">Админ панель <span style={{ color: 'var(--error-bg-color)' }}>(в разработке)</span></div>
<div className="AdminPanel__Router">
<Link to={path}>Главная</Link>
<Link to={`${path}/users`}>Пользователи</Link>
<Link to={`${path}/novels`}>Новелы</Link>
<Link to={`${path}/news`}>Новости</Link>
</div>
{component}
</div>
);
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
})
)(AdminPanel);
+120
View File
@@ -0,0 +1,120 @@
import React, {ChangeEvent, FormEvent} from "react";
import Group from "../../../ui/Group/Group";
import NotFoundError from "../../NotFoundError";
import {fetchPost, updatePost} from "../../../api/news";
import './APNews.sass';
import '../../../ui/TextField/TextField.sass'
import Input from "../../../ui/Input/Input";
import Button from "../../../ui/Button/Button";
import BBEditor from "../../../ui/BBEditor/BBEditor";
// @ts-ignore
import parser from 'bbcode-to-react';
import Title from "../../../ui/Title/Title";
type Props = {
postId: number
//addNotification: (notification: React.ReactNode) => void
};
type State = {
post: PostType | null
title: string
previewShort: string
previewFull: string
};
class APModifyPost extends React.Component<Props, State> {
private shortPost = React.createRef<HTMLTextAreaElement>();
private fullPost = React.createRef<HTMLTextAreaElement>();
state: State = {
post: null, title: '',
previewShort: '', previewFull: ''
}
componentDidMount() {
const { postId } = this.props;
fetchPost(postId).then((post: PostType) => {
this.setState({ post, title: post.title });
});
}
savePost = async (event: FormEvent) => {
event.preventDefault();
const post = this.state.post;
if(!post) return;
if(!this.shortPost.current || !this.fullPost.current) return;
post.short_post = this.shortPost.current.value;
post.full_post = this.fullPost.current.value;
await updatePost(post);
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
if(prevState.post) return;
if(!this.state.post) return;
if(!this.shortPost.current || !this.fullPost.current) return;
this.shortPost.current.value = this.state.post.short_post;
this.fullPost.current.value = this.state.post.full_post;
}
previewShort = () => {
if(!this.shortPost.current) return;
this.setState({ previewShort: parser.toReact(this.shortPost.current.value) })
}
previewFull = () => {
if(!this.fullPost.current) return;
this.setState({ previewFull: parser.toReact(this.fullPost.current.value) })
}
changeTitle = (event: ChangeEvent<HTMLInputElement>) => {
const post = this.state.post;
if(!post) return;
post.title = event.target.value;
this.setState({ post });
}
render() {
const { post, title, previewShort, previewFull } = this.state;
if(!post) return <NotFoundError/>;
return (
<Group title={title}>
<form onSubmit={this.savePost} className={'APPost'}>
<Input type="text" placeholder={'Заголовок'} value={post.title} onChange={this.changeTitle}/>
<div className={'APPost__Short'}>
<Title>Короткая новость</Title>
<BBEditor inputRef={this.shortPost} placeholder={'Короткая новость'} />
{previewShort.length > 0 && (<>
<Title>Предпросмотр</Title>
<div className={'APPost__Short_Preview TextField'}>{previewShort}</div>
</>)}
<Button type={"button"} onClick={this.previewShort}>Предпросмотр</Button>
</div>
<div className={'APPost__Full'}>
<Title>Полная новость</Title>
<BBEditor inputRef={this.fullPost} placeholder={'Полная новость'} />
{previewFull.length > 0 && (<>
<Title>Предпросмотр</Title>
<div className={'APPost__Full_Preview TextField'}>{previewFull}</div>
</>)}
<Button type={"button"} onClick={this.previewFull}>Предпросмотр</Button>
</div>
<div>
<Button>Сохранить</Button>
<Button type={"button"} style={{ color: 'var(--error) '}}>Удалить</Button>
</div>
</form>
</Group>
)
}
}
export default APModifyPost;
+20
View File
@@ -0,0 +1,20 @@
.APPost
display: flex
flex-direction: column
.Input, .TextField, .Button
font-size: 1.2rem
.Input, .TextField
margin-bottom: 1rem
.TextField
width: 100%
min-height: 4rem
resize: none
&__Short, &__Full
display: flex
flex-direction: column
align-items: flex-start
margin-bottom: 1rem
+42
View File
@@ -0,0 +1,42 @@
import React from "react";
import {fetchNews} from "../../../api/news";
import Group from "../../../ui/Group/Group";
import {Link} from "react-router-dom";
type Props = {
path: string
};
type State = {
news: PostType[]
};
class APNews extends React.Component<Props, State> {
state: State = {
news: []
}
componentDidMount() {
fetchNews().then(news => this.setState({ news }));
}
render() {
const { news } = this.state;
const { path } = this.props;
return (
<Group title={'Новости'}>
{news.map(post => (
<Link to={`${path}/${post.id}`} key={post.id}>
<div style={{ display: "flex" }}>
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{post.id}</div>
<div>{post.title}</div>
</div>
</Link>
))}
</Group>
)
}
}
export default APNews;
@@ -1,24 +1,24 @@
import React from "react";
import {connect} from "react-redux";
type Props = {
};
type State = {
};
class AdminPanelNovels extends React.Component<Props, State> {
render() {
return (
<div>Novels</div>
)
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
})
import React from "react";
import {connect} from "react-redux";
type Props = {
};
type State = {
};
class AdminPanelNovels extends React.Component<Props, State> {
render() {
return (
<div>Novels</div>
)
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
})
)(AdminPanelNovels);
@@ -1,27 +1,27 @@
.APUser__Modify
&_Title
text-align: center
font-weight: 600
font-size: 1.5rem
&_Form
display: flex
flex-direction: column
button
font-size: 1.5rem
label
display: flex
flex-wrap: nowrap
white-space: nowrap
align-items: center
font-size: 1.2rem
input
margin: .5rem 0 .5rem .5rem
font-size: 1.2rem
width: 100%
&[type='checkbox']
width: 0
.APUser__Modify
&_Title
text-align: center
font-weight: 600
font-size: 1.5rem
&_Form
display: flex
flex-direction: column
button
font-size: 1.5rem
label
display: flex
flex-wrap: nowrap
white-space: nowrap
align-items: center
font-size: 1.2rem
input
margin: .5rem 0 .5rem .5rem
font-size: 1.2rem
width: 100%
&[type='checkbox']
width: 0
@@ -1,49 +1,49 @@
import React from "react";
import {connect} from "react-redux";
import {fetchUsers} from "../../../api/users";
import {Link} from "react-router-dom";
type Props = {
path: string
};
type State = {
users: UserType[]
};
class AdminPanelUsers extends React.Component<Props, State> {
state: State = {
users: []
}
componentDidMount() {
fetchUsers().then(users => this.setState({ users }))
}
render() {
const { users } = this.state;
const { path } = this.props;
return (
<div>
<div style={{ textAlign: "center", fontWeight: 600, fontSize: '1.5rem' }}>Пользователи</div>
<div>
{users.sort((a, b) => a.id - b.id).map(user => (
<Link to={`${path}/${user.id}`} key={user.id}>
<div style={{ display: "flex" }}>
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{user.id}</div>
<div>{user.username}</div>
</div>
</Link>
))}
</div>
</div>
)
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
})
import React from "react";
import {connect} from "react-redux";
import {fetchUsers} from "../../../api/users";
import {Link} from "react-router-dom";
type Props = {
path: string
};
type State = {
users: UserType[]
};
class AdminPanelUsers extends React.Component<Props, State> {
state: State = {
users: []
}
componentDidMount() {
fetchUsers().then(users => this.setState({ users }))
}
render() {
const { users } = this.state;
const { path } = this.props;
return (
<div>
<div style={{ textAlign: "center", fontWeight: 600, fontSize: '1.5rem' }}>Пользователи</div>
<div>
{users.sort((a, b) => a.id - b.id).map(user => (
<Link to={`${path}/${user.id}`} key={user.id}>
<div style={{ display: "flex" }}>
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{user.id}</div>
<div>{user.username}</div>
</div>
</Link>
))}
</div>
</div>
)
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
})
)(AdminPanelUsers);
@@ -1,92 +1,93 @@
import React, {ChangeEvent, FormEvent} from "react";
import {connect} from "react-redux";
import {fetchUser, updateUser} from "../../../api/users";
import Loading from "../../Loading";
import './APUser.sass';
import {addNotification} from "../../../store/actions";
import NotificationMessage from "../../Notifications/NotificationMessage";
type Props = {
userId: number
addNotification: (notification: React.ReactNode) => void
};
type State = {
user: UserType | null
username: string
};
class ModifyUser extends React.Component<Props, State> {
state: State = {
user: null, username: ''
}
componentDidMount() {
const { userId } = this.props;
fetchUser(userId).then((user: UserType) => {
this.setState({ user, username: user.username });
document.title = `${user.username} / Админ-панель`;
})
}
onChange = (event: ChangeEvent<HTMLInputElement>) => {
const user = Object.assign({}, this.state.user);
const target = event.target;
// @ts-ignore
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
this.setState({ user })
}
onSubmit = (event: FormEvent) => {
event.preventDefault();
const { user } = this.state;
const { addNotification } = this.props;
if(user === null) {
console.error('Пользователь - null!');
return;
}
updateUser(user).then(() => {
const notification = (
<NotificationMessage level={"success"}>
Пользователь успешно изменен!
</NotificationMessage>
)
addNotification(notification)
});
}
render() {
const { user, username } = this.state;
if(!user) return <Loading/>;
return (
<div className={'APUser__Modify'}>
<div className={'APUser__Modify_Title'}>{username}</div>
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'} value={user.username}/></label>
<label>Email: <input type='email' onChange={this.onChange} name={'email'} value={user.email}/></label>
<label>Аватар: <input type='text' onChange={this.onChange} name={'avatar'} value={user.avatar}/></label>
<label>Суперюзер: <input type='checkbox' onChange={this.onChange} name={'is_superuser'} checked={user.is_superuser}/></label>
<label>Активирован: <input type='checkbox' onChange={this.onChange} name={'is_activated'} checked={user.is_activated}/></label>
<div style={{ textAlign: "right" }}>
<button>Сохранить!</button>
</div>
</form>
</div>
)
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
}),
(dispatch) => ({
addNotification: (notification: React.ReactNode) => {
dispatch(addNotification(notification))
}
})
import React, {ChangeEvent, FormEvent} from "react";
import {connect} from "react-redux";
import {fetchUser, updateUser} from "../../../api/users";
import Loading from "../../../ui/Loading";
import './APUser.sass';
import {addNotification} from "../../../store/actions";
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
import Button from "../../../ui/Button/Button";
type Props = {
userId: number
addNotification: (notification: React.ReactNode) => void
};
type State = {
user: UserType | null
username: string
};
class ModifyUser extends React.Component<Props, State> {
state: State = {
user: null, username: ''
}
componentDidMount() {
const { userId } = this.props;
fetchUser(userId).then((user: UserType) => {
this.setState({ user, username: user.username });
document.title = `${user.username} / Админ-панель`;
})
}
onChange = (event: ChangeEvent<HTMLInputElement>) => {
const user = Object.assign({}, this.state.user);
const target = event.target;
// @ts-ignore
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
this.setState({ user })
}
onSubmit = (event: FormEvent) => {
event.preventDefault();
const { user } = this.state;
const { addNotification } = this.props;
if(user === null) {
console.error('Пользователь - null!');
return;
}
updateUser(user).then(() => {
const notification = (
<NotificationMessage level={"success"}>
Пользователь успешно изменен!
</NotificationMessage>
)
addNotification(notification)
});
}
render() {
const { user, username } = this.state;
if(!user) return <Loading/>;
return (
<div className={'APUser__Modify'}>
<div className={'APUser__Modify_Title'}>{username}</div>
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'} value={user.username}/></label>
<label>Email: <input type='email' onChange={this.onChange} name={'email'} value={user.email}/></label>
<label>Аватар: <input type='text' onChange={this.onChange} name={'avatar'} value={user.avatar}/></label>
<label>Суперюзер: <input type='checkbox' onChange={this.onChange} name={'is_superuser'} checked={user.is_superuser}/></label>
<label>Активирован: <input type='checkbox' onChange={this.onChange} name={'is_activated'} checked={user.is_activated}/></label>
<div style={{ textAlign: "right" }}>
<Button>Сохранить!</Button>
</div>
</form>
</div>
)
}
}
export default connect(
(state: StoreState) => ({
user: state.currentUser
}),
(dispatch) => ({
addNotification: (notification: React.ReactNode) => {
dispatch(addNotification(notification))
}
})
)(ModifyUser);
+6 -27
View File
@@ -1,5 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap')
@import "../Constants"
@import "../../Constants"
html, body, #root
width: 100%
@@ -75,37 +75,13 @@ a
&:hover
color: var(--link-color)
input, button, textarea
color: var(--main-color)
&:disabled
color: var(--sec-color)
button
cursor: pointer
padding: .2rem
border-radius: $borderRadius
background-color: var(--block-color)
border: 1px solid var(--main-color)
&:hover
background-color: var(--block-sec-color)
color: var(--link-color)
border: 1px solid var(--link-color)
input
background-color: var(--sec-block-color) !important
border: none
border-bottom: 1px solid var(--main-color) !important
*
scrollbar-color: #eee #222
scrollbar-width: thin
.Container
max-width: 1200px
min-height: calc(100% - 4rem)
min-height: calc(100vh - 16rem)
margin: .5rem auto
.Notifications
@@ -113,7 +89,10 @@ input
bottom: 0
left: 0
z-index: 100000
padding: 1rem
@media (min-width: 500px)
padding: 1rem
box-sizing: border-box
.Successful, .Error
border: var(--link-color) 1px solid
+25 -35
View File
@@ -9,16 +9,14 @@ import NotFoundError from "../NotFoundError";
import {fetchCurrentUser} from "../../api/users";
import {connect} from "react-redux";
import {setUser} from "../../store/actions";
import LoginModal from "../LoginModal/LoginModal";
import RegisterModal from "../LoginModal/RegisterModal";
import Character from "../Character/Character";
// @ts-ignore
import parser from 'bbcode-to-react';
import SpoilerTag from "../Spoiler/SpoilerTag";
import SpoilerTag from "../../ui/Spoiler/SpoilerTag";
import Novels from "../Novels/Novels";
import Footer from '../Footer/Footer';
import UserNovels from '../User/Novels/UserNovels';
import ColorTag from "../Tags/ColorTag";
import ColorTag from "../../ui/Tags/ColorTag";
import ActivateAccount from "../ActivateAccount";
import AllNews from "../News/AllNews";
import Post from "../News/Post";
@@ -26,30 +24,23 @@ import AdminPanel from "../AdminPanel/AdminPanel";
import PasswordRestoreGenerate from "../PasswordRestore/PasswordRestoreGenerate";
import PasswordRestore from "../PasswordRestore/PasswordRestore";
import Users from "../Users/Users";
import {getVersion} from "../../api/api";
import {getVersion, version} from "../../api/api";
import Settings from "../User/Settings/Settings";
type Props = {
notifications: React.ReactNode[]
setUser: (user: UserType) => void
modal: React.ReactNode
};
type State = {
loginModalVisible: boolean
registerModalVisible: boolean
error: string
login: string
password: string
}
type State = {}
class App extends React.Component<Props, State> {
state: State = {}
constructor(props: Props) {
super(props);
this.state = {
loginModalVisible: false, registerModalVisible: false, error: '',
login: '', password: ''
};
const { setUser } = this.props;
fetchCurrentUser().then(setUser).catch((err: ApiError) => {
if(err.code !== 3 && err.text) console.error(err.text);
@@ -62,24 +53,26 @@ class App extends React.Component<Props, State> {
parser.registerTag('spoiler', SpoilerTag);
parser.registerTag('color', ColorTag);
// In future do something normal with this version
getVersion().then(res => console.debug(`Current back-end version: ${res.version}`))
getVersion().then(res => {
if(version !== res.version) {
console.debug(`Current back-end version: ${res.version}. Clearing cache`);
caches.keys().then(names => {
names.forEach(name => caches.delete(name).catch(console.error))
});
}
});
}
showLoginModal = () => this.setState({ loginModalVisible: true, registerModalVisible: false });
hideLoginModal = () => this.setState({ loginModalVisible: false });
showRegisterModal = () => this.setState({ registerModalVisible: true, loginModalVisible: false });
hideRegisterModal = () => this.setState({ registerModalVisible: false });
render() {
const { loginModalVisible, registerModalVisible } = this.state;
const { modal } = this.props;
const notificationWidth = window.screen.width >= 500 ? 500 : window.screen.width;
return (
<>
<Navbar showLoginModal={this.showLoginModal} showRegisterModal={this.showRegisterModal}/>
<Navbar/>
<div className={'Container'}>
<div className="Notifications">
<div className="Notifications" style={{ width: notificationWidth }}>
{this.props.notifications.map((n, i) => (
<React.Fragment key={i}>
{n}
@@ -98,6 +91,7 @@ class App extends React.Component<Props, State> {
<Route exact path={'/users'} component={Users}/>
<Route exact path={'/user/:userId'} component={User}/>
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
<Route exact path={'/user/:userId/settings'} component={Settings}/>
<Route exact path={'/character/:charId'} component={Character}/>
@@ -111,12 +105,7 @@ class App extends React.Component<Props, State> {
<Route component={NotFoundError}/>
</Switch>
{loginModalVisible && (
<LoginModal hideModal={this.hideLoginModal} openRegisterModal={this.showRegisterModal}/>
)}
{registerModalVisible && (
<RegisterModal hideModal={this.hideRegisterModal}/>
)}
{modal && modal}
</div>
<Footer/>
</>
@@ -126,7 +115,8 @@ class App extends React.Component<Props, State> {
export default connect(
(state: StoreState) => ({
notifications: state.notifications
notifications: state.notifications,
modal: state.modal
}),
dispatch => ({
setUser: (userData: UserType) => {
+1 -1
View File
@@ -1,4 +1,4 @@
@import '../Constants'
@import '../../Constants'
.Character
.Character__Main
+2 -2
View File
@@ -3,8 +3,8 @@ import {fetchChar, fetchCharNovels} from "../../api/characters";
import './Character.sass'
// @ts-ignore
import parser from 'bbcode-to-react';
import NovelItem from "../NovelItem/NovelItem";
import Loading from "../Loading";
import NovelItem from "../../ui/NovelItem/NovelItem";
import Loading from "../../ui/Loading";
import NotFoundError from "../NotFoundError";
type Props = {
+45 -45
View File
@@ -1,46 +1,46 @@
@import '../Constants'
.Comments
margin-top: 1rem
padding: $primaryPadding
background-color: var(--sec-bg-color)
border-radius: $borderRadius
form
width: 100%
margin-bottom: .5rem
.Comments_BBCodes
font-size: 1.3rem
margin-bottom: .2rem
svg
cursor: pointer
svg:first-child
padding-right: .5rem
svg:last-child
padding-left: .5rem
svg:not(:first-child):not(:last-child)
padding: 0 .5rem
.Comments_TextArea
width: 100%
background-color: var(--block-color)
font-size: 1rem
padding: .25rem
border: none
border-radius: .2rem
box-sizing: border-box
min-height: 4rem
resize: none
margin-bottom: .5rem
.Comments_Submit
background-color: var(--block-color) !important
border-radius: .2rem
border: none
font-size: 1rem
@import '../../Constants'
.Comments
margin-top: 1rem
padding: $primaryPadding
background-color: var(--sec-bg-color)
border-radius: $borderRadius
form
width: 100%
margin-bottom: .5rem
.Comments_BBCodes
font-size: 1.3rem
margin-bottom: .2rem
svg
cursor: pointer
svg:first-child
padding-right: .5rem
svg:last-child
padding-left: .5rem
svg:not(:first-child):not(:last-child)
padding: 0 .5rem
.Comments_TextArea
width: 100%
background-color: var(--block-color)
font-size: 1rem
padding: .25rem
border: none
border-radius: .2rem
box-sizing: border-box
min-height: 4rem
resize: none
margin-bottom: .5rem
.Comments_Submit
background-color: var(--block-color) !important
border-radius: .2rem
border: none
font-size: 1rem
padding: .5rem
+132 -131
View File
@@ -1,132 +1,133 @@
import React from 'react';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBold, faExclamationTriangle, faItalic, faStrikethrough, faUnderline, faTint } from "@fortawesome/free-solid-svg-icons";
import Comment from './Comment';
import './Comments.sass';
import Loading from "../Loading";
import {fetchUser} from "../../api/users";
import {connect} from "react-redux";
import {addNotification} from "../../store/actions";
import NotificationMessage from "../Notifications/NotificationMessage";
type Props = {
comments: CommentType[]
hasMore: boolean
user: UserType
loadMore: () => void
sendComment: (text: string) => void
addNotification: (notification: React.ReactNode) => void
};
type State = {
commentText: string
users: Map<number, UserType>
};
class Comments extends React.Component<Props, State> {
input: HTMLTextAreaElement | null = null;
state: State = {
commentText: '', users: new Map()
};
addTagToComment = (event: React.MouseEvent, tag: string, defaultValue?: string) => {
event.preventDefault();
if(!this.input) {
console.error('Input not initialized!');
return;
}
let { commentText } = this.state;
const [start, end] = [this.input.selectionStart, this.input.selectionEnd];
const openTag = `${tag}${defaultValue ? `=${defaultValue}` : ''}`;
this.setState({ commentText: `${commentText.slice(0, start)}[${openTag}]${commentText.slice(start, end)}[/${tag}]${commentText.slice(end, commentText.length)}` });
this.input.focus();
};
sendComment = (event: React.FormEvent) => {
event.preventDefault();
const { sendComment, addNotification } = this.props;
const { commentText } = this.state;
if(commentText === '') {
const notification = (
<NotificationMessage level={"error"}>
Пустой комментарий
</NotificationMessage>
);
addNotification(notification);
} else {
sendComment(commentText);
this.setState({ commentText: '' });
}
};
componentDidUpdate(prevProps: Readonly<Props>) {
const { comments } = this.props;
const { comments: prevComments } = prevProps;
if(prevComments.length !== comments.length) {
let { users } = this.state;
comments.forEach(async comment => {
if(!users.has(comment.user_id)) {
const user = await fetchUser(comment.user_id);
users = users.set(comment.user_id, user);
this.setState({ users });
}
});
}
}
render() {
const { user, comments, hasMore, loadMore } = this.props;
const { users } = this.state;
if(!user) return <Loading/>;
return (
<div className="Comments">
{user.authorized &&
<form onSubmit={this.sendComment}>
<div className="Comments_BBCodes">
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'b')} icon={faBold}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'i')} icon={faItalic}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'u')} icon={faUnderline}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 's')} icon={faStrikethrough}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'spoiler')} icon={faExclamationTriangle}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'color', 'white')} icon={faTint}/>
</div>
<textarea
className={'Comments_TextArea'}
placeholder={'Текст комментария'}
value={this.state.commentText}
onChange={(event) => this.setState({ commentText: event.target.value })}
ref={ref => this.input = ref}
/>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div style={{ display: "flex", alignItems: "center" }}>
<button type="submit" className={'Comments_Submit'}>Написать</button>
<div style={{ marginLeft: '.5rem', color: '#bb3333' }}>Комментарии в разработке!</div>
</div>
{hasMore && <button type={"button"} onClick={loadMore}>Загрузить ещё</button>}
</div>
</form>}
{!comments && (
<Loading/>
)}
{comments && comments.length > 0 ? (
comments.map(comment => <Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)} />)
) : (
'Никто не оставил комментарий к этой новелле... :c'
)}
</div>
)
}
}
export default connect(null,
dispatch => ({
addNotification: (notification: React.ReactNode) => {
dispatch(addNotification(notification))
}
})
import React from 'react';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBold, faExclamationTriangle, faItalic, faStrikethrough, faUnderline, faTint } from "@fortawesome/free-solid-svg-icons";
import Comment from '../../ui/Comment/Comment';
import './Comments.sass';
import Loading from "../../ui/Loading";
import {fetchUser} from "../../api/users";
import {connect} from "react-redux";
import {addNotification} from "../../store/actions";
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
import Button from "../../ui/Button/Button";
type Props = {
comments: CommentType[]
hasMore: boolean
user: UserType
loadMore: () => void
sendComment: (text: string) => void
addNotification: (notification: React.ReactNode) => void
};
type State = {
commentText: string
users: Map<number, UserType>
};
class Comments extends React.Component<Props, State> {
input: HTMLTextAreaElement | null = null;
state: State = {
commentText: '', users: new Map()
};
addTagToComment = (event: React.MouseEvent, tag: string, defaultValue?: string) => {
event.preventDefault();
if(!this.input) {
console.error('Input not initialized!');
return;
}
let { commentText } = this.state;
const [start, end] = [this.input.selectionStart, this.input.selectionEnd];
const openTag = `${tag}${defaultValue ? `=${defaultValue}` : ''}`;
this.setState({ commentText: `${commentText.slice(0, start)}[${openTag}]${commentText.slice(start, end)}[/${tag}]${commentText.slice(end, commentText.length)}` });
this.input.focus();
};
sendComment = (event: React.FormEvent) => {
event.preventDefault();
const { sendComment, addNotification } = this.props;
const { commentText } = this.state;
if(commentText === '') {
const notification = (
<NotificationMessage level={"error"}>
Пустой комментарий
</NotificationMessage>
);
addNotification(notification);
} else {
sendComment(commentText);
this.setState({ commentText: '' });
}
};
componentDidUpdate(prevProps: Readonly<Props>) {
const { comments } = this.props;
const { comments: prevComments } = prevProps;
if(prevComments.length !== comments.length) {
let { users } = this.state;
comments.forEach(async comment => {
if(!users.has(comment.user_id)) {
const user = await fetchUser(comment.user_id);
users = users.set(comment.user_id, user);
this.setState({ users });
}
});
}
}
render() {
const { user, comments, hasMore, loadMore } = this.props;
const { users, commentText } = this.state;
if(!user) return <Loading/>;
return (
<div className="Comments">
{user.authorized &&
<form onSubmit={this.sendComment}>
<div className="Comments_BBCodes">
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'b')} icon={faBold}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'i')} icon={faItalic}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'u')} icon={faUnderline}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 's')} icon={faStrikethrough}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'spoiler')} icon={faExclamationTriangle}/>
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'color', 'white')} icon={faTint}/>
</div>
<textarea
className={'Comments_TextArea'}
placeholder={'Текст комментария'}
value={commentText}
onChange={(event) => this.setState({ commentText: event.target.value })}
ref={ref => this.input = ref}
/>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div style={{ display: "flex", alignItems: "center" }}>
<Button type="submit" className={'Comments_Submit'}>Написать</Button>
<div style={{ marginLeft: '.5rem', color: '#bb3333' }}>Комментарии в разработке!</div>
</div>
{hasMore && <Button onClick={loadMore}>Загрузить ещё</Button>}
</div>
</form>}
{!comments && (
<Loading/>
)}
{comments && comments.length > 0 ? (
comments.map(comment => <Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)} />)
) : (
'Никто не оставил комментарий к этой новелле... :c'
)}
</div>
)
}
}
export default connect(null,
dispatch => ({
addNotification: (notification: React.ReactNode) => {
dispatch(addNotification(notification))
}
})
)(Comments);
+58 -58
View File
@@ -1,59 +1,59 @@
.Footer
background: var(--nav-bg-color)
display: flex
flex-direction: column
position: relative
bottom: 0
padding: 1rem
.Footer__Tabs
display: flex
@media (max-width: 600px)
flex-direction: column
.Footer__Tab
font-weight: 300
display: flex
flex-direction: column
margin-right: 4rem
.Footer__Tab_Title
color: var(--main-color)
font-weight: 400
margin-bottom: 1rem
.Link
margin-bottom: 1rem
.Links
display: flex
.Link
height: 46px
width: 46px
font-size: 1rem
background-color: var(--block-color)
border-radius: 50%
margin-right: 1rem
&:hover
background-color: var(--sec-bg-color)
svg
position: relative
top: 14px
left: 14px
width: 18px
height: 18px
a
color: var(--sec-color)
&:hover
color: var(--main-color)
.Footer__Bottom
border-top: 1px solid var(--main-color)
padding: 2rem 0
.Footer
background: var(--nav-bg-color)
display: flex
flex-direction: column
position: relative
bottom: 0
padding: 1rem
.Footer__Tabs
display: flex
@media (max-width: 600px)
flex-direction: column
.Footer__Tab
font-weight: 300
display: flex
flex-direction: column
margin-right: 4rem
.Footer__Tab_Title
color: var(--main-color)
font-weight: 400
margin-bottom: 1rem
.Link
margin-bottom: 1rem
.Links
display: flex
.Link
height: 46px
width: 46px
font-size: 1rem
background-color: var(--block-color)
border-radius: 50%
margin-right: 1rem
&:hover
background-color: var(--sec-bg-color)
svg
position: relative
top: 14px
left: 14px
width: 18px
height: 18px
a
color: var(--sec-color)
&:hover
color: var(--main-color)
.Footer__Bottom
border-top: 1px solid var(--main-color)
padding: 2rem 0
text-align: center
+56 -56
View File
@@ -1,56 +1,56 @@
import React from 'react';
import './Footer.sass';
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 = () => {
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'>
<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>
</div>
);
};
export default Footer;
import React from 'react';
import './Footer.sass';
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 = () => {
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'>
<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/4CA8Cju">
<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>
</div>
);
};
export default Footer;
+11
View File
@@ -0,0 +1,11 @@
.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
View File
+43
View File
@@ -0,0 +1,43 @@
import React from "react";
import {fetchNews} from "../../api/news";
import Loading from "../../ui/Loading";
import {Link} from "react-router-dom";
import NewsPost from "../../ui/NewsPost/NewsPost";
type State = {
news: PostType[]
}
class News extends React.Component<{}, State> {
state: State = {
news: []
};
componentDidMount() {
fetchNews().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.slice(0, 3).map(post => (
<Link key={post.id} to={`/news/${post.id}`}>
<NewsPost {...post} />
</Link>
))}
</div>
);
}
}
export default News;
+1 -1
View File
@@ -1,4 +1,4 @@
@import '../Constants'
@import '../../Constants'
.LoginModal
border-radius: $borderRadius
+23 -16
View File
@@ -1,19 +1,22 @@
import React, {FormEvent, ChangeEvent} from "react";
import Modal from "../Modal/Modal";
import Modal from "../../ui/Modal/Modal";
import {login} from "../../api/users";
import {connect} from "react-redux";
import {addNotification, setUser} from "../../store/actions";
import {addNotification, hideModal, setModal, setUser} from "../../store/actions";
import './LoginModal.sass'
import {Link} from "react-router-dom";
import errors from "../../api/errors";
import NotificationMessage from "../Notifications/NotificationMessage";
import Recaptcha from '../Recaptcha/Recaptcha'
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
import Recaptcha from '../../ui/Recaptcha/Recaptcha'
import Button from "../../ui/Button/Button";
import RegisterModal from "./RegisterModal";
import Input from "../../ui/Input/Input";
type Props = {
setUser: (user: UserType) => void
hideModal: () => void
openRegisterModal: () => void
addNotification: (notification: React.ReactNode) => void
setModal: (modal: React.ReactNode | null) => void
hideModal: () => void
};
type State = {
@@ -31,8 +34,9 @@ class LoginModal extends React.Component<Props, State> {
loginAndFetchToken = (event: FormEvent) => {
event.preventDefault();
const { setUser, hideModal, addNotification } = this.props;
const { setUser, addNotification, setModal } = this.props;
const { login: log, password, recaptcha } = this.state;
login(log, password, recaptcha).then(user => {
if(!user) return;
@@ -43,7 +47,7 @@ class LoginModal extends React.Component<Props, State> {
</NotificationMessage>
);
addNotification(successful);
hideModal();
setModal(null);
})
.catch((r: ApiError) => {
this.setState({ error: errors[r.code] });
@@ -56,12 +60,13 @@ class LoginModal extends React.Component<Props, State> {
render() {
const { login, password, error, recaptchaNeeded } = this.state;
const { hideModal, setModal } = this.props;
return (
<Modal onCloseRequest={this.props.hideModal} className={'LoginModal'}>
<h1>Авторизация</h1>
<Modal className={'LoginModal'} title={'Авторизация'}>
{error && <div className="LoginModal__Error">{error}</div>}
<form className={'LoginModal__Form'} onSubmit={this.loginAndFetchToken}>
<input
<Input
type="text"
name={'login'}
placeholder={'Логин'}
@@ -69,7 +74,7 @@ class LoginModal extends React.Component<Props, State> {
onChange={this.handleLoginChange}
value={login}
/>
<input
<Input
type="password"
name={'password'}
placeholder={'Пароль'}
@@ -83,12 +88,12 @@ class LoginModal extends React.Component<Props, State> {
theme={'dark'}
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
/>}
<button className={'LoginModal__Submit'} type={"submit"}>Войти</button>
<Button className={'LoginModal__Submit'}>Войти</Button>
</form>
<div className="LoginModal__Links">
<Link to={'/restore'}>Забыли пароль?</Link>
<Link to={'/restore'} onClick={hideModal}>Забыли пароль?</Link>
<p>|</p>
<Link to={''} onClick={this.props.openRegisterModal}>Регистрация</Link>
<Link to={''} onClick={() => setModal(<RegisterModal/>)}>Регистрация</Link>
</div>
</Modal>
)
@@ -98,6 +103,8 @@ class LoginModal extends React.Component<Props, State> {
export default connect(null,
dispatch => ({
setUser: (user: UserType) => dispatch(setUser(user)),
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
hideModal: () => dispatch(hideModal())
})
)(LoginModal);
@@ -1,4 +1,4 @@
@import '../Constants'
@import '../../Constants'
.RegisterModal
border-radius: $borderRadius
+17 -16
View File
@@ -1,15 +1,17 @@
import React from "react";
import Modal from "../Modal/Modal";
import Recaptcha from '../Recaptcha/Recaptcha'
import Modal from "../../ui/Modal/Modal";
import Recaptcha from '../../ui/Recaptcha/Recaptcha'
import './RegisterModal.sass'
import {registerUser} from "../../api/users";
import {connect} from "react-redux";
import {addNotification} from "../../store/actions";
import NotificationMessage from "../Notifications/NotificationMessage";
import {addNotification, setModal} from "../../store/actions";
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
import Button from "../../ui/Button/Button";
import Input from "../../ui/Input/Input";
type Props = {
addNotification: (notification: React.ReactNode) => void
hideModal: () => void
setModal: (modal: React.ReactNode | null) => void
}
type State = {
@@ -55,23 +57,21 @@ class RegisterModal extends React.Component<Props, State> {
Проверьте почту <strong>{email}</strong>. На неё была отправлена ссылка для подтверждения аккаунта.
</NotificationMessage>
);
const { addNotification, hideModal } = this.props;
const { addNotification, setModal } = this.props;
addNotification(notification);
hideModal();
setModal(null);
}).catch(console.error);
}
};
render() {
const { hideModal } = this.props;
const { error, email, login, password1, password2 } = this.state;
return (
<Modal onCloseRequest={hideModal} className={'RegisterModal'}>
<h1>Регистрация</h1>
<Modal className={'RegisterModal'} title={'Регистрация'}>
{error && <div className="RegisterModal__Error" dangerouslySetInnerHTML={{ __html: error }}/>}
<form className="RegisterModal__Form" onSubmit={this.submitForm}>
<input
<Input
type="text"
min={4}
max={20}
@@ -81,7 +81,7 @@ class RegisterModal extends React.Component<Props, State> {
className={'RegisterModal__Field'}
onChange={this.handleLoginChange}
value={login}/>
<input
<Input
type="email"
name={'email'}
placeholder={'E-mail'}
@@ -89,14 +89,14 @@ class RegisterModal extends React.Component<Props, State> {
className={'RegisterModal__Field'}
onChange={this.handleEmailChange}
value={email}/>
<input
<Input
type="password"
name={'password1'}
placeholder={'Пароль'}
className={'RegisterModal__Field'}
onChange={this.handlePassword1Change}
value={password1}/>
<input
<Input
type="password"
name={'password2'}
placeholder={'Повтор пароля'}
@@ -110,7 +110,7 @@ class RegisterModal extends React.Component<Props, State> {
theme={'dark'}
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}/>
<button className={'RegisterModal__Submit'} type={"submit"}>Регистрация</button>
<Button className={'RegisterModal__Submit'}>Регистрация</Button>
</form>
</Modal>
)
@@ -119,6 +119,7 @@ class RegisterModal extends React.Component<Props, State> {
export default connect(null,
dispatch => ({
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal))
})
)(RegisterModal);
+2 -2
View File
@@ -75,8 +75,8 @@ $hoverColor: #f22
left: 0
height: 0
background: $hoverColor
transition: heigth .1s ease-in
-webkit-transition: height .1s ease-in
transition: heigth .1s linear
-webkit-transition: height .1s linear
&:hover
background-color: var(--main-bg-color)
+31 -23
View File
@@ -3,25 +3,27 @@ import './Navbar.sass'
import {connect} from "react-redux";
import {userLogout} from "../../api/users";
import {Link} from "react-router-dom";
import {logout} from "../../store/actions";
import {logout, setModal} from "../../store/actions";
import {hasAccessToAdminPanel} from "../../utils";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBars} from '@fortawesome/free-solid-svg-icons';
import RegisterModal from "../Modals/RegisterModal";
import LoginModal from "../Modals/LoginModal";
type Props = {
showRegisterModal: () => void
showLoginModal: () => void
setModal: SetModalType
logout: () => void
currentUser: UserType
};
type State = {
expand: boolean
width: number
}
class Navbar extends React.Component<Props, State> {
state: State = {
expand: false
expand: false, width: window.screen.width
}
logout = () => {
@@ -34,8 +36,15 @@ class Navbar extends React.Component<Props, State> {
this.setState({ expand: !this.state.expand })
}
componentDidMount() {
window.addEventListener("resize", () => {
if(this.state.width !== window.screen.width)
this.setState({ width: window.screen.width });
})
}
render() {
const { currentUser, showRegisterModal, showLoginModal } = this.props;
const { currentUser, setModal } = this.props;
const { expand } = this.state;
const isMinimized = window.screen.width <= 1000;
@@ -68,25 +77,22 @@ class Navbar extends React.Component<Props, State> {
)}
{currentUser &&
(<div className={'Header__User'}>
{currentUser.authorized && (
{currentUser.authorized ? (<>
<Link to={`/user/${currentUser.id}`} className="Header__User_Profile">
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar})` }}/>
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar}?s=40&t=${new Date().getTime()})` }}/>
<div>{this.props.currentUser.username}</div>
</Link>
)}
{currentUser.group && hasAccessToAdminPanel(currentUser) && (
<Link to={'/kawaii__neko'} className={'Header_Button'}>
Админ-панель
</Link>
)}
{!currentUser.authorized && (
<div className={'Header_Button'} onClick={showRegisterModal}>Регистрация</div>
)}
{!currentUser.authorized ? (
<div className={'Header_Button'} onClick={showLoginModal}>Войти</div>
) : (
{currentUser.group && hasAccessToAdminPanel(currentUser) && (
<Link to={'/kawaii__neko'} className={'Header_Button'}>
Админ-панель
</Link>
)}
<Link className="Header_Button" to={`/user/${currentUser.id}/settings`}>Настройки</Link>
<div className={'Header_Button'} onClick={this.logout}>Выйти</div>
)}
</>) : (<>
<div className={'Header_Button'} onClick={() => setModal(<RegisterModal/>)}>Регистрация</div>
<div className={'Header_Button'} onClick={() => setModal(<LoginModal/>)}>Войти</div>
</>)}
</div>)
}
</nav>
@@ -95,9 +101,11 @@ class Navbar extends React.Component<Props, State> {
}
export default connect(
(state: any) => ({
(state: StoreState) => ({
currentUser: state.currentUser
}),
dispatch => ({
logout: () => dispatch(logout())
}))(Navbar);
logout: () => dispatch(logout()),
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal))
})
)(Navbar);
+7 -13
View File
@@ -1,6 +1,7 @@
import React from "react";
import Loading from "../Loading";
import {getNews} from "../../api/news";
import Loading from "../../ui/Loading";
import {fetchNews} from "../../api/news";
import NewsPost from "../../ui/NewsPost/NewsPost";
import {Link} from "react-router-dom";
type State = {
@@ -14,7 +15,7 @@ class AllNews extends React.Component<{}, State> {
componentDidMount() {
document.title = 'Новости';
getNews().then((news: PostType[]) => {
fetchNews().then((news: PostType[]) => {
news.forEach(p => p.date = new Date(p.date));
this.setState({ news })
});
@@ -30,16 +31,9 @@ class AllNews extends React.Component<{}, State> {
Новости
</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>
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
</div>
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
</div>
</div>
<Link key={post.id} to={`/post/${post.id}`}>
<NewsPost {...post} />
</Link>
))}
</div>
);
+12 -12
View File
@@ -1,13 +1,13 @@
.Post
background-color: var(--sec-bg-color)
padding: .5rem
&__Title
font-size: 1.5rem
font-weight: 600
&__Content
border-top: var(--link-color) 1px solid
border-bottom: var(--link-color) 1px solid
padding: .5rem 0
.Post
background-color: var(--sec-bg-color)
padding: .5rem
&__Title
font-size: 1.5rem
font-weight: 600
&__Content
border-top: var(--link-color) 1px solid
border-bottom: var(--link-color) 1px solid
padding: .5rem 0
margin: .5rem 0
+54 -50
View File
@@ -1,51 +1,55 @@
import React from "react";
import {getPost} from "../../api/news";
import Loading from "../Loading";
import NotFoundError from "../NotFoundError";
import './Post.sass';
import {Link} from "react-router-dom";
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: PostType) => {
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 || post.short_post}
</div>
<div className="Post__Footer">
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
</div>
</div>
)
}
}
import React from "react";
import {fetchPost} from "../../api/news";
import Loading from "../../ui/Loading";
import NotFoundError from "../NotFoundError";
import './Post.sass'
import {Link} from "react-router-dom";
// @ts-ignore
import parser from 'bbcode-to-react';
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;
fetchPost(postId).then((post: PostType) => {
post.date = new Date(post.date);
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>
<pre className='Post__Content'>{parser.toReact(post.short_post)}</pre>
<div className="Post__Footer">
<div>
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
</div>
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
</div>
</div>
)
}
}
export default Post;
View File
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../Constants"
@import "../../Constants"
.Novel
.Novel__Main
+2 -2
View File
@@ -12,9 +12,9 @@ import {
fetchNovelGenres, postNovelComment, createUserNovel
} from "../../api/novels";
import {Link} from "react-router-dom";
import Loading from "../Loading";
import Loading from "../../ui/Loading";
import { TranslateStatus } from "../../api/api";
import Comments from "../Comment/Comments";
import Comments from "../Comments/Comments";
type Props = {
currentUser: UserType
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../Constants"
@import "../../Constants"
.Novels
background-color: var(--sec-bg-color)
+2 -2
View File
@@ -1,8 +1,8 @@
import './Novels.sass'
import React from "react";
import {fetchNovels} from "../../api/novels";
import NovelItem from '../NovelItem/NovelItem';
import Loading from "../Loading";
import NovelItem from '../../ui/NovelItem/NovelItem';
import Loading from "../../ui/Loading";
type State = {
novels: NovelType[] | null
+15
View File
@@ -0,0 +1,15 @@
.PasswordRestore
display: flex
flex-direction: column
padding: .5rem
.Button, input
font-size: 1.2rem
input
width: 100%
margin-bottom: .5rem
.Button
align-self: flex-start
margin-top: .5rem
@@ -1,78 +1,79 @@
import React from "react";
import {restore} from "../../api/auth";
import errors from "../../api/errors";
import './PasswordRestore.sass';
type Props = {
match: { params: { token: string } }
}
type State = {
p1: string
p2: string
error: string
message: string
}
class PasswordRestore extends React.Component<Props, State> {
state = {
p1: '', p2: '', error: '', message: ''
}
timeout: NodeJS.Timeout | null = null;
onSubmit = (event: React.FormEvent) => {
const { token } = this.props.match.params;
const { p1, p2 } = this.state;
event.preventDefault();
if(p1 !== p2) {
this.setState({ error: 'Пароли не совпадают!' })
return;
}
restore(token, p1).then(res => {
if(res === 'ok') {
this.setState({ message: 'Успешно!\nВы будете пренаправлены на главную через 3 секунды!' })
this.timeout = setTimeout(() => {
}, 3000)
}
}).catch((err: ApiError) => {
const error = errors[err.code];
if(error) this.setState({ error });
else this.setState({ error: `Произошла ошибка! Сообщите номер и текст разработчику!\nКод: ${err.code}\nТекст: ${err.text}` })
})
}
componentWillUnmount() {
if(this.timeout) clearInterval(this.timeout);
}
render() {
const { p1, p2, error, message } = this.state;
return (
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
{error && <pre className="Error">{error}</pre>}
{message && <pre className="Successful">{message}</pre>}
<input
type="password"
placeholder={'Новый пароль'}
minLength={6}
value={p1}
onChange={event => this.setState({ p1: event.target.value })}
required
/>
<input
type="password"
placeholder={'Повторите пароль'}
minLength={6}
value={p2}
onChange={event => this.setState({ p2: event.target.value })}
required
/>
<button>Сменить</button>
</form>
)
}
}
import React from "react";
import {restore} from "../../api/auth";
import errors from "../../api/errors";
import './PasswordRestore.sass';
import Button from "../../ui/Button/Button";
type Props = {
match: { params: { token: string } }
}
type State = {
p1: string
p2: string
error: string
message: string
}
class PasswordRestore extends React.Component<Props, State> {
state = {
p1: '', p2: '', error: '', message: ''
}
timeout: NodeJS.Timeout | null = null;
onSubmit = (event: React.FormEvent) => {
const { token } = this.props.match.params;
const { p1, p2 } = this.state;
event.preventDefault();
if(p1 !== p2) {
this.setState({ error: 'Пароли не совпадают!' })
return;
}
restore(token, p1).then(res => {
if(res === 'ok') {
this.setState({ message: 'Успешно!\nВы будете пренаправлены на главную через 3 секунды!' })
this.timeout = setTimeout(() => {
}, 3000)
}
}).catch((err: ApiError) => {
const error = errors[err.code];
if(error) this.setState({ error });
else this.setState({ error: `Произошла ошибка! Сообщите номер и текст разработчику!\nКод: ${err.code}\nТекст: ${err.text}` })
})
}
componentWillUnmount() {
if(this.timeout) clearInterval(this.timeout);
}
render() {
const { p1, p2, error, message } = this.state;
return (
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
{error && <pre className="Error">{error}</pre>}
{message && <pre className="Successful">{message}</pre>}
<input
type="password"
placeholder={'Новый пароль'}
minLength={6}
value={p1}
onChange={event => this.setState({ p1: event.target.value })}
required
/>
<input
type="password"
placeholder={'Повторите пароль'}
minLength={6}
value={p2}
onChange={event => this.setState({ p2: event.target.value })}
required
/>
<Button>Сменить</Button>
</form>
)
}
}
export default PasswordRestore;
@@ -1,52 +1,53 @@
import React from "react";
// @ts-ignore
// import Recaptcha from "react-recaptcha";
import Recaptcha from '../Recaptcha/Recaptcha';
import './PasswordRestore.sass';
import {generateRestoreLink} from "../../api/users";
type State = {
email: string
recaptcha: string
}
class PasswordRestoreGenerate extends React.Component<{}, State> {
private recaptcha: any;
state: State = {
email: '', recaptcha: ''
}
onSubmit = (event: React.FormEvent) => {
event.preventDefault();
const { email, recaptcha } = this.state;
generateRestoreLink(email, recaptcha).then(console.log)
}
componentWillUnmount() {
this.recaptcha.reset()
}
render() {
return (
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
<input
type="email"
onChange={event => this.setState({ email: event.target.value })}
placeholder={'E-mail'}
/>
<Recaptcha
ref={(e: any) => this.recaptcha = e}
onVerify={(res: string) => this.setState({ recaptcha: res })}
theme={'dark'}
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
/>
<button>Восстановить</button>
</form>
);
}
}
import React from "react";
// @ts-ignore
// import Recaptcha from "react-recaptcha";
import Recaptcha from '../../ui/Recaptcha/Recaptcha';
import './PasswordRestore.sass';
import {generateRestoreLink} from "../../api/users";
import Button from "../../ui/Button/Button";
type State = {
email: string
recaptcha: string
}
class PasswordRestoreGenerate extends React.Component<{}, State> {
private recaptcha: any;
state: State = {
email: '', recaptcha: ''
}
onSubmit = (event: React.FormEvent) => {
event.preventDefault();
const { email, recaptcha } = this.state;
generateRestoreLink(email, recaptcha).then(console.log)
}
componentWillUnmount() {
this.recaptcha.reset()
}
render() {
return (
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
<input
type="email"
onChange={event => this.setState({ email: event.target.value })}
placeholder={'E-mail'}
/>
<Recaptcha
ref={(e: any) => this.recaptcha = e}
onVerify={(res: string) => this.setState({ recaptcha: res })}
theme={'dark'}
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
/>
<Button>Восстановить</Button>
</form>
);
}
}
export default PasswordRestoreGenerate;
@@ -1,47 +1,47 @@
@import "../../Constants"
.UserNovels
background-color: var(--sec-bg-color)
padding: .5rem
&__Title
font-size: 1.5rem
font-weight: 600
text-align: center
.svg-inline--fa
margin: 0 .5rem
cursor: pointer
&:hover
color: var(--link-color)
&__List
display: flex
flex-direction: column
width: 100%
&:not(:last-child)
border-bottom: var(--link-color) 1px solid
margin-bottom: 1rem
padding-bottom: 1rem
&_Title
display: flex
font-size: 1.5rem
margin-right: 1rem
button
font-weight: 600
&_Novels
display: flex
justify-content: center
&.Grid
flex-direction: row
flex-wrap: wrap
&.Table
flex-direction: column
@import "../../../Constants"
.UserNovels
background-color: var(--sec-bg-color)
padding: .5rem
&__Title
font-size: 1.5rem
font-weight: 600
text-align: center
.svg-inline--fa
margin: 0 .5rem
cursor: pointer
&:hover
color: var(--link-color)
&__List
display: flex
flex-direction: column
width: 100%
&:not(:last-child)
border-bottom: var(--link-color) 1px solid
margin-bottom: 1rem
padding-bottom: 1rem
&_Title
display: flex
font-size: 1.5rem
margin-right: 1rem
button
font-weight: 600
&_Novels
display: flex
justify-content: center
&.Grid
flex-direction: row
flex-wrap: wrap
&.Table
flex-direction: column
width: 100%
+94 -94
View File
@@ -1,95 +1,95 @@
import React from 'react';
import './UserNovels.sass';
import { fetchUserNovels, fetchUser } from '../../../api/users';
import Loading from '../../Loading';
import NovelItem from '../../NovelItem/NovelItem';
import {capitalize, generateClassName, getRandomInt} from "../../../utils";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons";
type Props = {
match: { params: { userId: number } }
history: { push: (path: string) => void }
};
type State = {
novels: NovelType[]
user: UserType | null
viewType: string
};
class UserNovels extends React.Component<Props, State> {
state: State = {
novels: [], user: null, viewType: 'grid'
};
getRandomNovel = () => {
const { history } = this.props;
const { novels } = this.state;
const planned = novels.filter(n => n.status === 'planned');
const index = getRandomInt(0, planned.length-1);
history.push(`/novels/${planned[index].id}`);
};
componentDidMount() {
const { match: { params: { userId } } } = this.props;
fetchUserNovels(userId)
.then(novels => this.setState({ novels }));
fetchUser(userId)
.then(user => this.setState({ user }));
const viewType = localStorage.getItem('viewType');
if(viewType) {
if(viewType !== 'grid' && viewType !== 'table') {
console.warn('Тип был неизвестен... Сбрасываю...');
localStorage.setItem('viewType', 'grid')
} else {
this.setState({ viewType });
}
}
};
changeViewType = (type: 'grid' | 'table') => {
this.setState({ viewType: type });
localStorage.setItem('viewType', type);
}
render() {
const { novels, user, viewType } = this.state;
if(!novels || !user) return <Loading/>;
const planned = novels.filter(n => n.status === 'planned');
const completed = novels.filter(n => n.status === 'completed');
return (
<div className='UserNovels'>
<div className="UserNovels__Title">
Новеллы пользователя {user.username}
<FontAwesomeIcon icon={faThLarge} onClick={() => this.changeViewType('grid')}/>
<FontAwesomeIcon icon={faAlignJustify} onClick={() => this.changeViewType('table')}/>
</div>
{planned.length > 0 && <div className={'UserNovels__List'}>
<div className="UserNovels__List_Title">
<div style={{ marginRight: '1rem' }}>Запланированные</div>
<button onClick={this.getRandomNovel}>Рандомная новелла</button>
</div>
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
{planned.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
</div>
</div>}
{completed.length > 0 && <div className={'UserNovels__List'}>
<div className="UserNovels__List_Title">
Пройденные
</div>
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
{completed.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
</div>
</div>}
</div>
);
}
}
import React from 'react';
import './UserNovels.sass';
import { fetchUserNovels, fetchUser } from '../../../api/users';
import Loading from '../../../ui/Loading';
import NovelItem from '../../../ui/NovelItem/NovelItem';
import {capitalize, generateClassName, getRandomInt} from "../../../utils";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons";
type Props = {
match: { params: { userId: number } }
history: { push: (path: string) => void }
};
type State = {
novels: NovelType[]
user: UserType | null
viewType: string
};
class UserNovels extends React.Component<Props, State> {
state: State = {
novels: [], user: null, viewType: 'grid'
};
getRandomNovel = () => {
const { history } = this.props;
const { novels } = this.state;
const planned = novels.filter(n => n.status === 'planned');
const index = getRandomInt(0, planned.length-1);
history.push(`/novels/${planned[index].id}`);
};
componentDidMount() {
const { match: { params: { userId } } } = this.props;
fetchUserNovels(userId)
.then(novels => this.setState({ novels }));
fetchUser(userId)
.then(user => this.setState({ user }));
const viewType = localStorage.getItem('viewType');
if(viewType) {
if(viewType !== 'grid' && viewType !== 'table') {
console.warn('Тип был неизвестен... Сбрасываю...');
localStorage.setItem('viewType', 'grid')
} else {
this.setState({ viewType });
}
}
};
changeViewType = (type: 'grid' | 'table') => {
this.setState({ viewType: type });
localStorage.setItem('viewType', type);
}
render() {
const { novels, user, viewType } = this.state;
if(!novels || !user) return <Loading/>;
const planned = novels.filter(n => n.status === 'planned');
const completed = novels.filter(n => n.status === 'completed');
return (
<div className='UserNovels'>
<div className="UserNovels__Title">
Новеллы пользователя {user.username}
<FontAwesomeIcon icon={faThLarge} onClick={() => this.changeViewType('grid')}/>
<FontAwesomeIcon icon={faAlignJustify} onClick={() => this.changeViewType('table')}/>
</div>
{planned.length > 0 && <div className={'UserNovels__List'}>
<div className="UserNovels__List_Title">
<div style={{ marginRight: '1rem' }}>Запланированные</div>
<button onClick={this.getRandomNovel}>Рандомная новелла</button>
</div>
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
{planned.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} viewType={viewType}/>)}
</div>
</div>}
{completed.length > 0 && <div className={'UserNovels__List'}>
<div className="UserNovels__List_Title">
Пройденные
</div>
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
{completed.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} viewType={viewType}/>)}
</div>
</div>}
</div>
);
}
}
export default UserNovels;
+12
View File
@@ -0,0 +1,12 @@
.Group
.Settings_Avatar
form
display: flex
flex-direction: column
align-items: flex-start
input
margin: .5rem 0
.Button
flex-grow: 0
+83
View File
@@ -0,0 +1,83 @@
import React, {ChangeEvent, FormEvent} from "react";
import Group from "../../../ui/Group/Group";
import {connect} from "react-redux";
import NotFoundError from "../../NotFoundError";
import Button from "../../../ui/Button/Button";
import './Settings.sass';
import {fetchUserSettings, updateUserSettings, uploadAvatar} from "../../../api/users";
import {setUser} from "../../../store/actions";
type Props = {
match: { params: { userId: string } }
currentUser: UserType
setUser: (user: UserType) => void
}
type State = {
// Avatar
useGravatar: boolean
avatar: File | null
}
class Settings extends React.Component<Props, State> {
state: State = {
useGravatar: false, avatar: null
}
componentDidMount() {
fetchUserSettings().then(settings => {
this.setState({ useGravatar: settings.use_gravatar })
})
}
onChangeAvatar = (event: ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if(!files) return;
this.setState({ avatar: files[0] });
}
saveAvatar = (event: FormEvent) => {
event.preventDefault();
const { avatar, useGravatar } = this.state;
const { setUser } = this.props;
if(avatar) {
const formData = new FormData();
formData.append('avatar', avatar);
uploadAvatar(formData).then(avatarUrl => {
updateUserSettings(useGravatar, avatarUrl).then(setUser)
})
} else {
updateUserSettings(useGravatar, '').then(setUser)
}
}
render() {
const { currentUser, match: { params: { userId } } } = this.props;
const { useGravatar } = this.state
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
return (
<Group title={'Настройки'}>
<Group title={'Аватар'} className={'Settings_Avatar'}>
<form onSubmit={this.saveAvatar}>
<label>Использовать Gravatar <input type="checkbox" onChange={event => this.setState({ useGravatar: event.target.checked })} checked={useGravatar}/></label>
<label>Загрузить аватар <input type="file" onChange={this.onChangeAvatar} disabled={useGravatar}/></label>
<Button>Сохранить</Button>
</form>
</Group>
</Group>
);
}
}
export default connect(
(state: StoreState) => ({
currentUser: state.currentUser
}),
dispatch => ({
setUser: (user: UserType) => dispatch(setUser(user))
})
)(Settings);
+4 -1
View File
@@ -42,7 +42,10 @@
font-weight: 600
&_Group
border: 1px solid #eeeeee
border-color: #eeeeee
border-width: 1px 1px 1px 4px
border-style: solid
border-radius: 4px
padding: .2rem
text-align: center
+6 -5
View File
@@ -3,9 +3,10 @@ import {fetchUser, fetchUserNovels, generateActivateLink} from "../../api/users"
import './User.sass'
import NotFoundError from "../NotFoundError";
import {Link} from "react-router-dom";
import Loading from "../Loading";
import NovelItem from "../NovelItem/NovelItem";
import Loading from "../../ui/Loading";
import NovelItem from "../../ui/NovelItem/NovelItem";
import {connect} from "react-redux";
import Button from "../../ui/Button/Button";
type Props = {
match: { params: { userId: string } }
@@ -55,7 +56,7 @@ class User extends React.Component<Props, State> {
<h2>Ваш аккаунт не активирован!</h2>
<p>Активируйте аккаунт, для доступа ко многим функциям. Так же актвация аккаунта позволит восстановить пароль.</p>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<button onClick={this.sendActivateLink} style={{ fontSize: '1.2rem' }}>Активировать!</button>
<Button onClick={this.sendActivateLink} style={{ fontSize: '1.2rem' }}>Активировать!</Button>
</div>
</div>
)}
@@ -63,7 +64,7 @@ class User extends React.Component<Props, State> {
<div className="User__Info" style={style}>
{user.cover && <div className="User__Info_Cover" style={{ backgroundImage: `url(${user.cover})` }}/>}
<div className={'User__Info_Main'}>
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar})` }} />
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar}?s=96&t=${new Date().getTime()})` }} />
<div>
<div className='User__Info_Nick'>{user.username}</div>
<div className='User__Info_Group' style={{ borderColor: user.group.color, color: user.group.color }}>{user.group.name}</div>
@@ -91,7 +92,7 @@ class User extends React.Component<Props, State> {
</div>
<div className="User__List_Items">
{novels.length > 0 ?
novels.slice(0, 4).map((novel) => <NovelItem {...novel} key={novel.id} viewType={'grid'} />
novels.slice(0, 4).map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} viewType={'grid'} />
) : (
<div>У этого пользователя нет новелл!</div>
)}
+13 -13
View File
@@ -1,14 +1,14 @@
.Users
display: flex
flex-wrap: wrap
&__User
background-color: var(--sec-bg-color)
padding: .5rem
&_Avatar
width: 96px
height: 96px
background-size: cover
border-radius: 50%
.Users
display: flex
flex-wrap: wrap
&__User
background-color: var(--sec-bg-color)
padding: .5rem
&_Avatar
width: 96px
height: 96px
background-size: cover
border-radius: 50%
margin-bottom: .5rem
+40 -40
View File
@@ -1,41 +1,41 @@
import React from "react";
import {fetchUsers} from "../../api/users";
import Loading from "../Loading";
import './Users.sass';
import {Link} from "react-router-dom";
type Props = {
}
type State = {
users: UserType[]
}
class Users extends React.Component<Props, State> {
state: State = {
users: []
}
componentDidMount() {
fetchUsers().then(users => this.setState({ users }))
}
render() {
const { users } = this.state;
if(users.length === 0) return <Loading/>;
return (
<div className={'Users'}>
{users.map(user => (
<Link className={'Users__User'} key={user.id} to={`user/${user.id}`}>
<div className="Users__User_Avatar" style={{ backgroundImage: `url(${user.avatar})` }}/>
{user.username}
</Link>
))}
</div>
)
}
}
import React from "react";
import {fetchUsers} from "../../api/users";
import Loading from "../../ui/Loading";
import './Users.sass';
import {Link} from "react-router-dom";
type Props = {
}
type State = {
users: UserType[]
}
class Users extends React.Component<Props, State> {
state: State = {
users: []
}
componentDidMount() {
fetchUsers().then(users => this.setState({ users }))
}
render() {
const { users } = this.state;
if(users.length === 0) return <Loading/>;
return (
<div className={'Users'}>
{users.map(user => (
<Link className={'Users__User'} key={user.id} to={`user/${user.id}`}>
<div className="Users__User_Avatar" style={{ backgroundImage: `url(${user.avatar}?s=96?t=${new Date().getTime()})` }}/>
{user.username}
</Link>
))}
</div>
)
}
}
export default Users;
Vendored Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+3 -1
View File
@@ -1,7 +1,9 @@
enum ActionTypes {
SET_USER = 'SET_USER',
LOGOUT = 'LOGOUT',
ADD_NOTIFICATION = 'ADD_NOTIFICATION'
ADD_NOTIFICATION = 'ADD_NOTIFICATION',
SET_MODAL = 'SET_MODAL',
HIDE_MODAL = 'HIDE_MODAL'
}
export default ActionTypes;
Regular → Executable
+10 -1
View File
@@ -14,5 +14,14 @@ const addNotification = (notification: React.ReactNode) => ({
notification
});
const setModal = (modal: React.ReactNode | null) => ({
type: ActionTypes.SET_MODAL,
modal
});
const hideModal = () => ({
type: ActionTypes.HIDE_MODAL
})
export { setUser, logout,
addNotification };
addNotification,
setModal, hideModal };
View File
Regular → Executable
+2 -2
View File
@@ -1,6 +1,6 @@
import {combineReducers} from "redux";
import currentUser from "./currentUser";
import notifications from "./notifications";
import modal from "./modal";
export default combineReducers({ currentUser, notifications })
export default combineReducers({ currentUser, notifications, modal })
+21
View File
@@ -0,0 +1,21 @@
import React from "react";
import ActionTypes from "../actionTypes";
const initialState: React.ReactNode | null = null;
type Action = {
type: string
modal: React.ReactNode | null
}
export default (state=initialState, action: Action) => {
switch (action.type) {
case ActionTypes.SET_MODAL: {
return action.modal;
}
case ActionTypes.HIDE_MODAL: {
return null;
}
default: return state;
}
}
View File
Regular → Executable
View File
Vendored Regular → Executable
+5 -2
View File
@@ -79,5 +79,8 @@ type PostType = {
type StoreState = {
currentUser: UserType
notifications: React.Component[]
}
notifications: React.ReactNode[]
modal: React.ReactNode
}
type SetModalType = (modal: React.ReactNode | null) => void
+16
View File
@@ -0,0 +1,16 @@
.BBCodes
font-size: 1.3rem
margin-bottom: .2rem
svg
cursor: pointer
padding: 0
svg:first-child
padding-right: .5rem
svg:last-child
padding-left: .5rem
svg:not(:first-child):not(:last-child)
padding: 0 .5rem
+51
View File
@@ -0,0 +1,51 @@
import React, {useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faBold,
faExclamationTriangle,
faItalic,
faStrikethrough,
faTint,
faUnderline
} from "@fortawesome/free-solid-svg-icons";
import './BBEditor.sass';
import TextField from "../TextField/TextField";
interface Props extends React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement> {
inputRef: React.RefObject<HTMLTextAreaElement>
}
const BBEditor = ({ inputRef, ref, ...props }: Props) => {
const [text, setText] = useState('');
const addTag = (event: React.MouseEvent, tag: string, defaultValue?: string) => {
event.preventDefault();
const [start, end] = [inputRef.current!!.selectionStart, inputRef.current!!.selectionEnd];
const openTag = `${tag}${defaultValue ? `=${defaultValue}` : ''}`;
setText(`${text.slice(0, start)}[${openTag}]${text.slice(start, end)}[/${tag}]${text.slice(end, text.length)}`)
inputRef.current!!.focus();
}
return (
<div style={{ width: '100%' }}>
<div className="BBCodes">
<FontAwesomeIcon onClick={event => addTag(event, 'b')} icon={faBold}/>
<FontAwesomeIcon onClick={event => addTag(event, 'i')} icon={faItalic}/>
<FontAwesomeIcon onClick={event => addTag(event, 'u')} icon={faUnderline}/>
<FontAwesomeIcon onClick={event => addTag(event, 's')} icon={faStrikethrough}/>
<FontAwesomeIcon onClick={event => addTag(event, 'spoiler')} icon={faExclamationTriangle}/>
<FontAwesomeIcon onClick={event => addTag(event, 'color', 'white')} icon={faTint}/>
</div>
<TextField
ref={inputRef}
style={{ width: '100%' }}
value={text}
onChange={event => setText(event.target.value)}
{...props}
/>
</div>
)
}
export default BBEditor;
+17
View File
@@ -0,0 +1,17 @@
@import "../../Constants"
.Button
cursor: pointer
padding: .2rem
border-radius: $borderRadius
background-color: var(--block-color)
border: 1px solid var(--main-color)
color: var(--main-color)
&:disabled
color: var(--sec-color)
&:hover
background-color: var(--block-sec-color)
color: var(--link-color)
border: 1px solid var(--link-color)
+14
View File
@@ -0,0 +1,14 @@
import React, {ButtonHTMLAttributes} from "react";
import './Button.sass';
import classNames from "../../classNames";
const Button = (restProps: ButtonHTMLAttributes<HTMLElement>) => {
const { className, children, ...props } = restProps;
return (
<button className={classNames('Button', className)} {...props}>
{children}
</button>
)
}
export default Button;
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../Constants"
@import "../../Constants"
.Comment
display: flex
View File
+7
View File
@@ -0,0 +1,7 @@
.Buttons
display: flex
justify-content: flex-end
.Button
margin: 1rem .5rem 0
font-size: 1.3rem
+53
View File
@@ -0,0 +1,53 @@
import React from "react";
import Button from "../Button/Button";
import './ConfirmPopout.sass'
import '../Modal/Modal.sass';
import {connect} from "react-redux";
import {hideModal} from "../../store/actions";
type Props = {
hideModal: () => void
onConfirm?: () => void
onDismiss?: () => void
title?: string
children: any
}
class ConfirmPopout extends React.Component<Props> {
onConfirm = () => {
const { hideModal, onConfirm } = this.props;
if(onConfirm) onConfirm();
hideModal();
}
onDismiss = () => {
const { hideModal, onDismiss } = this.props;
if(onDismiss) onDismiss();
hideModal();
}
render() {
const { children, title } = this.props;
return (
<div className="Modal__Overlay">
<div className={"Modal"}>
{title && <div className="Modal__Header">
{title}
</div>}
{children}
<div className={'Buttons'}>
<Button onClick={this.onConfirm}>Да</Button>
<Button onClick={this.onDismiss}>Нет</Button>
</div>
</div>
</div>
)
}
}
export default connect(null,
dispatch => ({
hideModal: () => dispatch(hideModal())
})
)(ConfirmPopout);
+11
View File
@@ -0,0 +1,11 @@
@import "../../Constants"
.Group
background-color: var(--sec-bg-color)
border-radius: $borderRadius
padding: .5rem
&__Title
font-size: 1.2rem
font-weight: 600
margin-bottom: .5rem
+29
View File
@@ -0,0 +1,29 @@
import React, {HTMLAttributes} from "react";
import classNames from "../../classNames";
import './Group.sass'
interface Props extends HTMLAttributes<HTMLElement> {
title: string
direction?: 'column' | 'row'
}
const Group = (restProps: Props) => {
const { title, direction, className, children, ...props } = restProps;
let style: React.CSSProperties = {};
if(direction === 'column') {
style = {
display: 'flex',
flexDirection: 'column'
}
}
return (
<div className={classNames('Group', className)} {...props}>
<div className="Group__Title">{title}</div>
<div style={style}>
{children}
</div>
</div>
)
}
export default Group;
+8
View File
@@ -0,0 +1,8 @@
.Input
background-color: var(--sec-block-color) !important
border: none
border-bottom: 1px solid var(--main-color) !important
color: var(--main-color)
&:disabled
color: var(--sec-color)
+12
View File
@@ -0,0 +1,12 @@
import React, {InputHTMLAttributes} from "react";
import classNames from "../../classNames";
import './Input.sass';
const Input = (restProps: InputHTMLAttributes<HTMLElement>) => {
const { className, ...props } = restProps;
return (
<input className={classNames('Input', className)} {...props} />
)
}
export default Input;
+10 -10
View File
@@ -1,11 +1,11 @@
import React from 'react';
// @ts-ignore
import Loader from 'react-loader-spinner'
const Loading: React.FC = () => (
<div style={{ textAlign: 'center' }}>
<Loader color='#aaa' type='Rings' style={{ height: '100%' }} />
</div>
);
import React from 'react';
// @ts-ignore
import Loader from 'react-loader-spinner'
const Loading: React.FC = () => (
<div style={{ textAlign: 'center' }}>
<Loader color='#aaa' type='Rings' style={{ height: '100%' }} />
</div>
);
export default Loading;
+38
View File
@@ -0,0 +1,38 @@
.Modal__Overlay
display: flex
align-items: center
justify-content: center
position: fixed
top: 0
right: 0
left: 0
bottom: 0
background-color: #0f0f0fe9
z-index: 9999
opacity: 1
overflow: hidden
.Modal
background-color: var(--main-bg-color)
padding: .5rem
.Modal__Header
display: flex
justify-content: space-between
align-items: center
font-size: 1.4rem
font-weight: 700
margin-bottom: .5rem
.Modal__CloseButton
background: none
font-size: 2rem
padding: 0
height: 2.5rem
width: 2.5rem
border: none
color: var(--main-color)
cursor: pointer
&:hover
color: var(--link-color)
+23 -9
View File
@@ -1,10 +1,13 @@
import React from "react";
import './Modal.sass'
import {connect} from "react-redux";
import {setModal} from "../../store/actions";
type Props = {
onCloseRequest: any
setModal: (modal: React.ReactNode | null) => void
children: any
className?: string
title?: string
}
class Modal extends React.Component<Props> {
@@ -22,31 +25,42 @@ class Modal extends React.Component<Props> {
handleKeyUp = (e: KeyboardEvent) => {
if(e.code === 'Escape') {
const { onCloseRequest } = this.props;
const { setModal } = this.props;
e.preventDefault();
onCloseRequest();
setModal(null);
}
};
handleOutsideClick = (e: any) => {
if(this.modal && !this.modal.contains(e.target)) {
const { onCloseRequest } = this.props;
onCloseRequest();
const { setModal } = this.props;
setModal(null);
}
};
render() {
const { onCloseRequest, children } = this.props;
const { setModal, children, title } = this.props;
return (
<div className="Modal__Overlay">
<button type="button" className={"Modal__CloseButton"} onClick={onCloseRequest}>X</button>
<div className={"Modal"} ref={node => (this.modal = node)}>
<div className={this.props.className}>{children}</div>
<div className={this.props.className}>
<div className="Modal__Header">
{title && title}
<button className={"Modal__CloseButton"} onClick={() => setModal(null)}>X</button>
</div>
{children}
</div>
</div>
</div>
);
}
}
export default Modal;
export default connect(null,
dispatch => ({
setModal: (modal: React.ReactNode | null) => {
dispatch(setModal(modal))
}
})
)(Modal);
+17
View File
@@ -0,0 +1,17 @@
.Post
background-color: var(--block-color)
padding: .5rem
&__Title
font-size: 1.2rem
&__Content
border-top: var(--link-color) 1px solid
border-bottom: var(--link-color) 1px solid
&__Footer
display: flex
justify-content: space-between
&:not(:last-child)
margin-bottom: 1rem
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
// @ts-ignore
import parser from 'bbcode-to-react';
import classNames from "../../classNames";
import './NewsPost.sass';
interface Props {
title: string
short_post: string
author_id: number
author: string
date: Date
className?: string
onClick?: () => void
}
const NewsPost = (restProps: Props) => {
const { title, short_post, author, author_id, className, onClick, ...props } = restProps;
const date = new Date(restProps.date);
return (
<div className={classNames('Post', className)} {...props} onClick={onClick}>
<div className="Post__Title">{title}</div>
<pre className='Post__Content'>{parser.toReact(short_post)}</pre>
<div className="Post__Footer">
<div>
Автор: {author}
</div>
<div>Дата: {date.toLocaleDateString()}, {date.toLocaleTimeString()}</div>
</div>
</div>
)
}
export default NewsPost;
@@ -1,4 +1,4 @@
@import "../Constants"
@import "../../Constants"
.Animate-Notification-exit
opacity: 0
@@ -7,6 +7,7 @@
background-color: var(--block-color)
border: 1px solid var(--link-color)
padding: .5rem
box-sizing: border-box
max-width: 500px
width: 500px
cursor: pointer
@@ -23,5 +24,4 @@
.Progress
height: 5px
background: white
transition: width 5ms
background: white
@@ -28,7 +28,7 @@ class NotificationMessage extends React.Component<Props, State> {
};
onClick = () => {
this.timer = setInterval(() => this.interval(400), 50);
this.timer = setInterval(() => this.interval(80), 1);
this.setState({ paused: false });
};
@@ -39,7 +39,7 @@ class NotificationMessage extends React.Component<Props, State> {
};
componentDidMount(): void {
this.timer = setInterval(() => this.interval(), 50)
this.timer = setInterval(() => this.interval(10), 1)
};
componentWillUnmount(): void {
@@ -53,6 +53,7 @@ class NotificationMessage extends React.Component<Props, State> {
return null;
const styleClass = level ? `Notification--${capitalize(level)}` : '';
const width = window.screen.width >= 500 ? 500 : window.screen.width;
return (
<div
@@ -60,6 +61,7 @@ class NotificationMessage extends React.Component<Props, State> {
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
onClick={this.onClick}
style={{ width }}
>
{children}
<div className="Progress" style={{ width: `${time / 5000 * 100}%` }}/>
+57 -57
View File
@@ -1,58 +1,58 @@
@import '../Constants'
.NovelItem
background-color: var(--block-color)
padding-bottom: $blockPadding
border-radius: $borderRadius
margin: .5rem
@media (min-width: 601px)
width: $novelBlockSize
@media (max-width: 600px)
width: 100%
display: flex
&.Grid
flex-direction: column
align-items: center
&.Table
width: 100%
padding: .2rem
box-sizing: border-box
flex-direction: row
justify-content: space-between
align-items: center
&:not(:last-child)
margin: 0 0 .5rem
&:last-child
margin: 0
.NovelItem__Status
white-space: nowrap
&__Image
border-radius: $borderRadius $borderRadius 0 0
background-size: cover
background-position: center
width: 100%
height: 130px
&__Title
width: 100%
text-align: center
display: inline-block
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
padding: .2rem
box-sizing: border-box
font-weight: 400
&__Status
font-weight: 300
@import '../../Constants'
.NovelItem
background-color: var(--block-color)
padding-bottom: $blockPadding
border-radius: $borderRadius
margin: .5rem
@media (min-width: 601px)
width: $novelBlockSize
@media (max-width: 600px)
width: 100%
display: flex
&.Grid
flex-direction: column
align-items: center
&.Table
width: 100%
padding: .2rem
box-sizing: border-box
flex-direction: row
justify-content: space-between
align-items: center
&:not(:last-child)
margin: 0 0 .5rem
&:last-child
margin: 0
.NovelItem__Status
white-space: nowrap
&__Image
border-radius: $borderRadius $borderRadius 0 0
background-size: cover
background-position: center
width: 100%
height: 130px
&__Title
width: 100%
text-align: center
display: inline-block
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
padding: .2rem
box-sizing: border-box
font-weight: 400
&__Status
font-weight: 300
display: flex
+40 -40
View File
@@ -1,41 +1,41 @@
import React from 'react';
import { TranslateStatus } from '../../api/api';
import { Link } from 'react-router-dom';
import './NovelItem.sass';
import {capitalize, generateClassName} from "../../utils";
type Props = {
id: number
original_name: string
viewType: string
key?: number
localized_name?: string
image?: string
status?: string
mark?: number
}
const NovelItem = ({ id, original_name, localized_name, image, status, mark, viewType, key }: Props) => (
<Link to={`/novels/${id}`} className={generateClassName("NovelItem", capitalize(viewType) || 'Grid')}>
{viewType === 'grid' && (<>
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
<div className="NovelItem__Title">
{localized_name || original_name}
</div>
{status && <div className="NovelItem__Status">
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
</div>}
</>)}
{viewType === 'table' && (<>
<div className="NovelItem__Component">{key || ''}</div>
<div className="NovelItem__Title">
{localized_name || original_name}
</div>
{status && <div className="NovelItem__Status">
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
</div>}
</>)}
</Link>
);
import React from 'react';
import { TranslateStatus } from '../../api/api';
import { Link } from 'react-router-dom';
import './NovelItem.sass';
import {capitalize, generateClassName} from "../../utils";
type Props = {
id: number
original_name: string
viewType: string
novelId?: number
localized_name?: string
image?: string
status?: string
mark?: number
}
const NovelItem = ({ id, original_name, localized_name, image, status, mark, viewType, novelId }: Props) => (
<Link to={`/novels/${id}`} className={generateClassName("NovelItem", capitalize(viewType) || 'Grid')}>
{viewType === 'grid' && (<>
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
<div className="NovelItem__Title">
{localized_name || original_name}
</div>
{status && <div className="NovelItem__Status">
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
</div>}
</>)}
{viewType === 'table' && (<>
<div className="NovelItem__Component">{novelId || ''}</div>
<div className="NovelItem__Title">
{localized_name || original_name}
</div>
{status && <div className="NovelItem__Status">
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
</div>}
</>)}
</Link>
);
export default NovelItem;
+61 -61
View File
@@ -1,62 +1,62 @@
import React from "react";
type Props = {
sitekey: string
theme?: 'dark' | 'light'
onVerify?: (res: string) => void
onError?: (error: string) => void
onExpired?: () => void
}
type State = {
isReady: boolean
}
class Recaptcha extends React.Component<Props, State> {
private readonly interval: NodeJS.Timeout;
state: State = {
isReady: false
}
constructor(props: Props) {
super(props);
this.interval = setInterval(() => {
// @ts-ignore
if(typeof window?.grecaptcha?.render === 'function') {
clearInterval(this.interval);
this.setState({ isReady: true })
}
}, 100)
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) {
const { isReady: isReadyPrev } = prevState;
const { isReady } = this.state;
if(!isReadyPrev && isReady) {
const { sitekey, theme, onExpired, onVerify } = this.props;
// @ts-ignore
window.grecaptcha.render(document.getElementById('g-recaptcha'), {
sitekey, theme,
callback: onVerify || undefined,
'expired-callback': onExpired || undefined
})
}
}
reset = () => {
// @ts-ignore
if(typeof window?.grecaptcha?.reset === "function") window.grecaptcha.reset(document.getElementById('g-recaptcha'));
}
render() {
return (
<div
id="g-recaptcha"
/>
)
}
}
import React from "react";
type Props = {
sitekey: string
theme?: 'dark' | 'light'
onVerify?: (res: string) => void
onError?: (error: string) => void
onExpired?: () => void
}
type State = {
isReady: boolean
}
class Recaptcha extends React.Component<Props, State> {
private readonly interval: NodeJS.Timeout;
state: State = {
isReady: false
}
constructor(props: Props) {
super(props);
this.interval = setInterval(() => {
// @ts-ignore
if(typeof window?.grecaptcha?.render === 'function') {
clearInterval(this.interval);
this.setState({ isReady: true })
}
}, 100)
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) {
const { isReady: isReadyPrev } = prevState;
const { isReady } = this.state;
if(!isReadyPrev && isReady) {
const { sitekey, theme, onExpired, onVerify } = this.props;
// @ts-ignore
window.grecaptcha.render(document.getElementById('g-recaptcha'), {
sitekey, theme,
callback: onVerify || undefined,
'expired-callback': onExpired || undefined
})
}
}
reset = () => {
// @ts-ignore
if(typeof window?.grecaptcha?.reset === "function") window.grecaptcha.reset(document.getElementById('g-recaptcha'));
}
render() {
return (
<div
id="g-recaptcha"
/>
)
}
}
export default Recaptcha;
+1 -1
View File
@@ -1,4 +1,4 @@
@import "../Constants"
@import "../../Constants"
.Spoiler
cursor: pointer
View File
+15 -15
View File
@@ -1,16 +1,16 @@
// @ts-ignore
import { Tag } from 'bbcode-to-react';
import React from 'react';
class ColorTag extends Tag {
toReact() {
// @ts-ignore
const text = this.getContent(true);
// @ts-ignore
const color = this.params.color;
// @ts-ignore
return <span style={{ color }}>{text}</span>
}
}
// @ts-ignore
import { Tag } from 'bbcode-to-react';
import React from 'react';
class ColorTag extends Tag {
toReact() {
// @ts-ignore
const text = this.getContent(true);
// @ts-ignore
const color = this.params.color;
// @ts-ignore
return <span style={{ color }}>{text}</span>;
}
}
export default ColorTag;
+11
View File
@@ -0,0 +1,11 @@
.TextField
color: var(--main-color)
background-color: var(--block-color)
font-size: 1rem
padding: .25rem
border: none
border-radius: .2rem
box-sizing: border-box
&:disabled
color: var(--sec-color)
+13
View File
@@ -0,0 +1,13 @@
import React, {TextareaHTMLAttributes} from "react";
import classNames from "../../classNames";
import './TextField.sass';
const TextField = React.forwardRef(
(restProps: React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, ref: React.Ref<HTMLTextAreaElement>) => {
const { className, ...props } = restProps;
return (
<textarea className={classNames('TextField', className)} ref={ref} {...props} />
)
})
export default TextField;
+3
View File
@@ -0,0 +1,3 @@
.Title
font-size: 1.2rem
font-weight: 600
+8
View File
@@ -0,0 +1,8 @@
import React from "react";
import './Title.sass';
const Title = ({ children }: any) => (
<div className={'Title'}>{children}</div>
)
export default Title;

Some files were not shown because too many files have changed in this diff Show More