Many changes
This commit is contained in:
+32
-4
@@ -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: 'Запланировано',
|
||||||
|
|||||||
@@ -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 } = {
|
const errors: { [key: number]: string } = {
|
||||||
1: 'Неверный логин и/или пароль!',
|
1: 'Неверный логин и/или пароль!',
|
||||||
2: 'Проверка ReCaptcha не пройдена!',
|
2: 'Проверка ReCaptcha не пройдена!',
|
||||||
|
4: 'Нет доступа!',
|
||||||
5: 'Аккаунт уже активирован! Вы будете перенаправлены на главную через 3 секунды.',
|
5: 'Аккаунт уже активирован! Вы будете перенаправлены на главную через 3 секунды.',
|
||||||
6: 'Токен истек!',
|
6: 'Токен истек!',
|
||||||
|
8: 'Новый и старый пароли совпадают!',
|
||||||
100: 'Не найдено!'
|
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 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
@@ -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 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 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
|
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)
|
||||||
|
|||||||
@@ -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/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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-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
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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) => ({
|
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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+7
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user