Many changes
This commit is contained in:
+32
-4
@@ -5,6 +5,29 @@ export const http = axios.create({
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
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) => {
|
||||
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();
|
||||
|
||||
if(res.status === 429) return new Promise(res => {
|
||||
setTimeout(res.bind(null, response(url, method, body)), 1500);
|
||||
});
|
||||
if(json?.error) return Promise.reject(json.error_data);
|
||||
if(!res.ok) return Promise.reject(res.statusText);
|
||||
return Promise.resolve(json.data);
|
||||
}
|
||||
|
||||
http.interceptors.response.use(undefined, (error: AxiosError) => {
|
||||
if(error.response) {
|
||||
if(error.response?.status === 429) {
|
||||
@@ -20,10 +43,15 @@ http.interceptors.response.use(undefined, (error: AxiosError) => {
|
||||
}
|
||||
});
|
||||
|
||||
const get = (url: string, data?: object) => http.get(url, data).then(r => r?.data?.data);
|
||||
const post = (url: string, data?: object) => http.post(url, data).then(r => r?.data.data);
|
||||
const put = (url: string, data?: object) => http.put(url, data).then(r => r?.data.data);
|
||||
const del = (url: string, data?: object) => http.delete(url, data).then(r => r?.data.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) => http.get(url, data).then(r => r?.data?.data);
|
||||
// const post = (url: string, data?: object) => http.post(url, data).then(r => r?.data.data);
|
||||
// const put = (url: string, data?: object) => http.put(url, data).then(r => r?.data.data);
|
||||
// const del = (url: string, data?: object) => http.delete(url, data).then(r => r?.data.data);
|
||||
|
||||
const TranslateStatus: { [id: string]: string } = {
|
||||
planned: 'Запланировано',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import {post} from "./api";
|
||||
|
||||
export const restore = (token: string, new_password: string) => post('auth/restore', { token, new_password });
|
||||
@@ -1,8 +1,10 @@
|
||||
const errors: { [key: number]: string } = {
|
||||
1: 'Неверный логин и/или пароль!',
|
||||
2: 'Проверка ReCaptcha не пройдена!',
|
||||
4: 'Нет доступа!',
|
||||
5: 'Аккаунт уже активирован! Вы будете перенаправлены на главную через 3 секунды.',
|
||||
6: 'Токен истек!',
|
||||
8: 'Новый и старый пароли совпадают!',
|
||||
100: 'Не найдено!'
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import { get, post, put, del } from "./api";
|
||||
|
||||
|
||||
export const fetchNovels = (orderBy?: string) => get(orderBy ? `novels?sort=${orderBy}` : 'novels');
|
||||
export const createNovel = (novel: NovelType) => post('novels', { ...novel });
|
||||
export const fetchNovel = (id: number) => get(`novels/${id}`);
|
||||
export const updateNovel = (id: number, novel: NovelType) => put(`novels/${id}`, { ...novel });
|
||||
export const fetchNovelCharacters = (id: number) => get(`novels/${id}/characters`);
|
||||
export const fetchNovelGenres = (id: number) => get(`novels/${id}/genres`);
|
||||
|
||||
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
import {get, post, put} from "./api";
|
||||
|
||||
export const fetchUsers = () => get('/users');
|
||||
export const fetchUsers = () => get('users');
|
||||
|
||||
export const fetchUser = (id: number) => get(`users/${id}`);
|
||||
export const updateUser = (user: UserType) => put(`users/${user.id}`, { user })
|
||||
@@ -8,7 +8,11 @@ export const updateUser = (user: UserType) => put(`users/${user.id}`, { user })
|
||||
export const fetchCurrentUser = () => get('users/current');
|
||||
export const fetchUserNovels = (id: number) => get(`users/${id}/novels`);
|
||||
|
||||
export const login = (login: string, password: string) => post('auth/login', { login, password });
|
||||
export const registerUser = (login: string, password: string, email: string, recaptcha: string) => post('auth/register', { login, password, email, recaptcha });
|
||||
export const activateAccount = (token: string) => post('auth/activate', { token });
|
||||
export const login = (login: string, password: string) => post('auth/login', { login, password });
|
||||
export const userLogout = () => post('auth/logout');
|
||||
|
||||
export const activateAccount = (token: string) => post('auth/activate', { token });
|
||||
export const generateActivateLink = () => post('auth/activateToken');
|
||||
|
||||
export const generateRestoreLink = (email: string, recaptcha: string) => post('auth/restoreToken', { email, recaptcha });
|
||||
@@ -115,7 +115,14 @@ input
|
||||
z-index: 100000
|
||||
padding: 1rem
|
||||
|
||||
.Error
|
||||
.Successful, .Error
|
||||
border: var(--link-color) 1px solid
|
||||
padding: 1rem
|
||||
padding: .5rem
|
||||
border-radius: 3px
|
||||
margin-bottom: 1rem
|
||||
|
||||
.Error
|
||||
background-color: var(--error-bg-color)
|
||||
|
||||
.Successful
|
||||
background-color: var(--success-bg-color)
|
||||
|
||||
@@ -23,6 +23,9 @@ import ActivateAccount from "../ActivateAccount";
|
||||
import AllNews from "../News/AllNews";
|
||||
import Post from "../News/Post";
|
||||
import AdminPanel from "../AdminPanel/AdminPanel";
|
||||
import PasswordRestoreGenerate from "../PasswordRestore/PasswordRestoreGenerate";
|
||||
import PasswordRestore from "../PasswordRestore/PasswordRestore";
|
||||
import Users from "../Users/Users";
|
||||
|
||||
type Props = {
|
||||
notifications: React.ReactNode[]
|
||||
@@ -47,8 +50,8 @@ class App extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
const { setUser } = this.props;
|
||||
fetchCurrentUser().then(currentUser => setUser(currentUser)).catch((err: ApiError) => {
|
||||
if(err.code !== 3) console.error(err.text);
|
||||
fetchCurrentUser().then(setUser).catch((err: ApiError) => {
|
||||
if(err.code !== 3 && err.text) console.error(err.text);
|
||||
});
|
||||
|
||||
const theme = localStorage.getItem('theme');
|
||||
@@ -66,6 +69,8 @@ class App extends React.Component<Props, State> {
|
||||
hideRegisterModal = () => this.setState({ registerModalVisible: false });
|
||||
|
||||
render() {
|
||||
const { loginModalVisible, registerModalVisible } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar showLoginModal={this.showLoginModal} showRegisterModal={this.showRegisterModal}/>
|
||||
@@ -86,6 +91,7 @@ class App extends React.Component<Props, State> {
|
||||
<Route exact path={'/novels'} component={Novels}/>
|
||||
<Route exact path={'/novels/:novelId'} component={Novel}/>
|
||||
|
||||
<Route exact path={'/users'} component={Users}/>
|
||||
<Route exact path={'/user/:userId'} component={User}/>
|
||||
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
|
||||
|
||||
@@ -93,14 +99,20 @@ class App extends React.Component<Props, State> {
|
||||
|
||||
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
||||
|
||||
<Route exact path={'/restore'} component={PasswordRestoreGenerate}/>
|
||||
<Route exact path={'/restore/:token'} component={PasswordRestore}/>
|
||||
|
||||
<Route path={'/kawaii__neko'} component={AdminPanel}/>
|
||||
{/*<Route exact path={'/kawaii__neko/users'} component={AdminPanelUsers}/>*/}
|
||||
|
||||
<Route component={NotFoundError}/>
|
||||
</Switch>
|
||||
|
||||
{this.state.loginModalVisible && <LoginModal hideModal={this.hideLoginModal} openRegisterModal={this.showRegisterModal}/>}
|
||||
{this.state.registerModalVisible && <RegisterModal hideModal={this.hideRegisterModal}/>}
|
||||
{loginModalVisible && (
|
||||
<LoginModal hideModal={this.hideLoginModal} openRegisterModal={this.showRegisterModal}/>
|
||||
)}
|
||||
{registerModalVisible && (
|
||||
<RegisterModal hideModal={this.hideRegisterModal}/>
|
||||
)}
|
||||
</div>
|
||||
<Footer/>
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,8 @@ import './LoginModal.sass'
|
||||
import {Link} from "react-router-dom";
|
||||
import errors from "../../api/errors";
|
||||
import Notification from "../Notification/Notification";
|
||||
// @ts-ignore
|
||||
import Recaptcha from 'react-recaptcha'
|
||||
|
||||
type Props = {
|
||||
setUser: (user: UserType) => void
|
||||
@@ -29,20 +31,19 @@ class LoginModal extends React.Component<Props, State> {
|
||||
loginAndFetchToken = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const { setUser, hideModal, addNotification } = this.props;
|
||||
login(this.state.login, this.state.password)
|
||||
.then(user => {
|
||||
if(user) {
|
||||
setUser(user);
|
||||
const successful = (
|
||||
<Notification level={"success"}>
|
||||
Ты успешно вошел!
|
||||
</Notification>
|
||||
);
|
||||
addNotification(successful);
|
||||
hideModal();
|
||||
}
|
||||
})
|
||||
.catch((r: ApiError) => this.setState({ error: errors[r.code] }));
|
||||
login(this.state.login, this.state.password).then(user => {
|
||||
if(!user) return;
|
||||
|
||||
setUser(user);
|
||||
const successful = (
|
||||
<Notification level={"success"}>
|
||||
Ты успешно вошел!
|
||||
</Notification>
|
||||
);
|
||||
addNotification(successful);
|
||||
hideModal();
|
||||
})
|
||||
.catch((r: ApiError) => this.setState({ error: errors[r.code] }));
|
||||
};
|
||||
|
||||
handlePasswordChange = (event: ChangeEvent) => this.setState({ password: (event.target as HTMLInputElement).value });
|
||||
@@ -60,18 +61,20 @@ class LoginModal extends React.Component<Props, State> {
|
||||
placeholder={'Логин'}
|
||||
className={'LoginModal__Field'}
|
||||
onChange={this.handleLoginChange}
|
||||
value={this.state.login}/>
|
||||
value={this.state.login}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name={'password'}
|
||||
placeholder={'Пароль'}
|
||||
className={'LoginModal__Field'}
|
||||
onChange={this.handlePasswordChange}
|
||||
value={this.state.password}/>
|
||||
value={this.state.password}
|
||||
/>
|
||||
<button className={'LoginModal__Submit'} type={"submit"}>Войти</button>
|
||||
</form>
|
||||
<div className="LoginModal__Links">
|
||||
<Link to={'/'}>Забыли пароль?</Link>
|
||||
<Link to={'/restore'}>Забыли пароль?</Link>
|
||||
<p>|</p>
|
||||
<Link to={''} onClick={this.props.openRegisterModal}>Регистрация</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import Modal from "../Modal/Modal";
|
||||
// @ts-ignore
|
||||
import Recaptcha from 'react-recaptcha'
|
||||
import Recaptcha from '../Recaptcha/Recaptcha'
|
||||
import './RegisterModal.sass'
|
||||
import {registerUser} from "../../api/users";
|
||||
import {connect} from "react-redux";
|
||||
@@ -34,7 +33,7 @@ class RegisterModal extends React.Component<Props, State> {
|
||||
handleEmailChange = (event: any) => this.setState({ email: event.target.value });
|
||||
|
||||
componentWillUnmount() {
|
||||
this.recaptcha.reset()
|
||||
if(this.recaptcha) this.recaptcha.reset();
|
||||
}
|
||||
|
||||
submitForm = (e: any) => {
|
||||
@@ -107,10 +106,8 @@ class RegisterModal extends React.Component<Props, State> {
|
||||
|
||||
<Recaptcha
|
||||
ref={(e: any) => this.recaptcha = e}
|
||||
className={'RegisterModal__Recaptcha'}
|
||||
verifyCallback={(res: any) => this.setState({ recaptcha: res })}
|
||||
onVerify={(res: any) => this.setState({ recaptcha: res })}
|
||||
theme={'dark'}
|
||||
badge={'inline'}
|
||||
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}/>
|
||||
|
||||
<button className={'RegisterModal__Submit'} type={"submit"}>Регистрация</button>
|
||||
|
||||
@@ -25,8 +25,5 @@
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
|
||||
*
|
||||
padding: .2rem
|
||||
|
||||
&:not(:last-child)
|
||||
margin-bottom: 1rem
|
||||
@@ -34,7 +34,9 @@ class News extends React.Component<{}, State> {
|
||||
<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>Автор: {post.author}</div>
|
||||
<div>
|
||||
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
|
||||
</div>
|
||||
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../App/App"
|
||||
$hoverColor: #f22
|
||||
|
||||
.Minimized
|
||||
height: 3rem
|
||||
@@ -33,10 +33,25 @@
|
||||
justify-content: center
|
||||
align-items: center
|
||||
cursor: pointer
|
||||
position: relative
|
||||
|
||||
&::before
|
||||
content: ""
|
||||
width: 100%
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: 0
|
||||
height: 0
|
||||
background: $hoverColor
|
||||
transition: heigth .1s ease-in
|
||||
-webkit-transition: height .1s ease-in
|
||||
|
||||
&:hover
|
||||
background-color: var(--main-bg-color)
|
||||
|
||||
&::before
|
||||
height: 5px
|
||||
|
||||
&__User
|
||||
justify-content: flex-end
|
||||
|
||||
@@ -50,10 +65,25 @@
|
||||
border: none
|
||||
border-radius: 0
|
||||
padding: 0 .5rem
|
||||
position: relative
|
||||
|
||||
&::before
|
||||
content: ""
|
||||
width: 100%
|
||||
position: absolute
|
||||
bottom: 0
|
||||
left: 0
|
||||
height: 0
|
||||
background: $hoverColor
|
||||
transition: heigth .1s ease-in
|
||||
-webkit-transition: height .1s ease-in
|
||||
|
||||
&:hover
|
||||
background-color: var(--main-bg-color)
|
||||
|
||||
&::before
|
||||
height: 5px
|
||||
|
||||
&_Avatar
|
||||
background-size: cover
|
||||
background-position: center
|
||||
|
||||
@@ -48,6 +48,9 @@ class Navbar extends React.Component<Props, State> {
|
||||
<Link to="/novels" className={'Header_Button'}>
|
||||
Новеллы
|
||||
</Link>
|
||||
<Link to="/users" className={'Header_Button'}>
|
||||
Пользователи
|
||||
</Link>
|
||||
{expand && isMinimized && (
|
||||
<div onClick={this.changeSize} className={'Header_Button'} style={{ position: "absolute", right: 0, margin: '.5rem' }}>X</div>
|
||||
)}
|
||||
@@ -64,22 +67,28 @@ class Navbar extends React.Component<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
{currentUser &&
|
||||
<div className={'Header__User'}>
|
||||
{currentUser.authorized &&
|
||||
<Link to={`/user/${currentUser.id}`} className="Header__User_Profile">
|
||||
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar})` }}/>
|
||||
<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>
|
||||
: <div className={'Header_Button'} onClick={this.logout}>Выйти</div>}
|
||||
</div>}
|
||||
(<div className={'Header__User'}>
|
||||
{currentUser.authorized && (
|
||||
<Link to={`/user/${currentUser.id}`} className="Header__User_Profile">
|
||||
<div className={'Header__User_Avatar'} style={{ backgroundImage: `url(${currentUser.avatar})` }}/>
|
||||
<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>
|
||||
) : (
|
||||
<div className={'Header_Button'} onClick={this.logout}>Выйти</div>
|
||||
)}
|
||||
</div>)
|
||||
}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import Loading from "../Loading";
|
||||
import {getNews} from "../../api/news";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
type State = {
|
||||
news: PostType[]
|
||||
@@ -33,7 +34,9 @@ class AllNews extends React.Component<{}, State> {
|
||||
<div className="Post__Title">{post.title}</div>
|
||||
<div className="Post__Content">{post.short_post}</div>
|
||||
<div className="Post__Footer">
|
||||
<div>Автор: {post.author}</div>
|
||||
<div>
|
||||
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
|
||||
</div>
|
||||
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 } }
|
||||
@@ -21,7 +22,7 @@ class Post extends React.Component<Props, State> {
|
||||
componentDidMount(): void {
|
||||
const { match: { params: { postId }} } = this.props;
|
||||
|
||||
getPost(postId).then(post => {
|
||||
getPost(postId).then((post: PostType) => {
|
||||
this.setState({ post })
|
||||
}).catch((error: ApiError) => {
|
||||
this.setState({ code: error.code })
|
||||
@@ -37,10 +38,10 @@ class Post extends React.Component<Props, State> {
|
||||
<div className={'Post'}>
|
||||
<div className={'Post__Title'}>{post.title}</div>
|
||||
<div className="Post__Content">
|
||||
{post.full_post}
|
||||
{post.full_post || post.short_post}
|
||||
</div>
|
||||
<div className="Post__Footer">
|
||||
Автор: {post.author}
|
||||
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -64,13 +64,15 @@
|
||||
.Novel__Info_Title
|
||||
@media (max-width: 600px)
|
||||
margin-top: 1rem
|
||||
|
||||
font-size: 1.7rem
|
||||
font-weight: 600
|
||||
margin-bottom: 1rem
|
||||
|
||||
.Novel__Info_Description
|
||||
margin-top: .5rem
|
||||
margin-left: 1rem
|
||||
@media (min-width: 600px)
|
||||
margin-left: 1rem
|
||||
font-size: 1rem
|
||||
|
||||
.Novel__Info_Genres
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.PasswordRestore
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
button, input
|
||||
margin: .5rem
|
||||
font-size: 1.2rem
|
||||
|
||||
input
|
||||
width: 100%
|
||||
|
||||
button
|
||||
align-self: flex-start
|
||||
@@ -0,0 +1,78 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PasswordRestore;
|
||||
@@ -0,0 +1,52 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PasswordRestoreGenerate;
|
||||
@@ -0,0 +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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Recaptcha;
|
||||
@@ -23,6 +23,7 @@
|
||||
background-position: center
|
||||
background-size: cover
|
||||
background-repeat: no-repeat
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
|
||||
@media (max-width: 500px)
|
||||
height: 10rem !important
|
||||
@@ -43,7 +44,7 @@
|
||||
&_Group
|
||||
border: 1px solid #eeeeee
|
||||
border-radius: 4px
|
||||
padding: .1rem
|
||||
padding: .2rem
|
||||
text-align: center
|
||||
font-size: 1rem
|
||||
font-weight: 300
|
||||
@@ -136,9 +137,6 @@
|
||||
justify-content: space-between
|
||||
|
||||
&_Items
|
||||
padding: $blockPadding * 3
|
||||
background-color: var(--block-sec-color)
|
||||
border-radius: $borderRadius
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: center
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {fetchUser, fetchUserNovels} from "../../api/users";
|
||||
import {fetchUser, fetchUserNovels, generateActivateLink} from "../../api/users";
|
||||
import './User.sass'
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import {Link} from "react-router-dom";
|
||||
@@ -37,7 +37,7 @@ class User extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
sendActivateLink = () => {
|
||||
|
||||
generateActivateLink().then(console.log).catch(console.error)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -0,0 +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%
|
||||
margin-bottom: .5rem
|
||||
@@ -0,0 +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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Users;
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
|
||||
const setUser = (userData: UserType) => ({
|
||||
type: ActionTypes.SET_USER,
|
||||
payload: { userData }
|
||||
userData
|
||||
});
|
||||
const logout = () => ({
|
||||
type: ActionTypes.LOGOUT
|
||||
@@ -11,7 +11,7 @@ const logout = () => ({
|
||||
|
||||
const addNotification = (notification: React.ReactNode) => ({
|
||||
type: ActionTypes.ADD_NOTIFICATION,
|
||||
payload: { notification }
|
||||
notification
|
||||
});
|
||||
|
||||
export { setUser, logout,
|
||||
|
||||
@@ -6,16 +6,14 @@ const initialState = {
|
||||
|
||||
type Action = {
|
||||
type: string
|
||||
payload: {
|
||||
userData: UserType
|
||||
}
|
||||
userData: UserType
|
||||
}
|
||||
|
||||
export default (state = initialState, action: Action) => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_USER:
|
||||
if(action.payload.userData) {
|
||||
state = Object.assign({}, state, action.payload.userData);
|
||||
if(action.userData) {
|
||||
state = Object.assign({}, state, action.userData);
|
||||
state.authorized = true;
|
||||
}
|
||||
return state;
|
||||
|
||||
@@ -5,17 +5,15 @@ const initialState: React.ReactNode[] = [];
|
||||
|
||||
type Action = {
|
||||
type: string
|
||||
payload: {
|
||||
notification: React.ReactNode[]
|
||||
}
|
||||
notification: React.ReactNode[]
|
||||
}
|
||||
|
||||
export default (state = initialState, action: Action) => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.ADD_NOTIFICATION: {
|
||||
if(state.length === 3)
|
||||
return [...state.slice(1), action.payload.notification];
|
||||
return [...state, action.payload.notification];
|
||||
return [...state.slice(1), action.notification];
|
||||
return [...state, action.notification];
|
||||
}
|
||||
default: return state;
|
||||
}
|
||||
|
||||
Vendored
+7
@@ -3,6 +3,12 @@ type ApiError = {
|
||||
text: string
|
||||
}
|
||||
|
||||
type ApiResponse = {
|
||||
error: boolean
|
||||
data?: any
|
||||
error_data?: ApiError
|
||||
}
|
||||
|
||||
type NovelType = {
|
||||
id: number
|
||||
original_name: string
|
||||
@@ -68,6 +74,7 @@ type PostType = {
|
||||
full_post: string
|
||||
date: Date
|
||||
author: string
|
||||
author_id: number
|
||||
}
|
||||
|
||||
type StoreState = {
|
||||
|
||||
Reference in New Issue
Block a user