Many changes

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