something new
This commit is contained in:
+2
-27
@@ -1,10 +1,3 @@
|
|||||||
import axios, { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
export const http = axios.create({
|
|
||||||
baseURL: process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.nix13.pw/v1',
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.nix13.pw/v1';
|
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.nix13.pw/v1';
|
||||||
|
|
||||||
export const response = async (url: string, method: string, body?: any) => {
|
export const response = async (url: string, method: string, body?: any) => {
|
||||||
@@ -28,31 +21,11 @@ export const response = async (url: string, method: string, body?: any) => {
|
|||||||
return Promise.resolve(json.data);
|
return Promise.resolve(json.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
http.interceptors.response.use(undefined, (error: AxiosError) => {
|
|
||||||
if(error.response) {
|
|
||||||
if(error.response?.status === 429) {
|
|
||||||
return new Promise(res => {
|
|
||||||
setTimeout(res.bind(null, http.request(error.config)), 1000)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if(error.response.data.error_data)
|
|
||||||
return Promise.reject(error.response.data.error_data);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const get = (url: string, data?: object) => response(url, 'GET', data);
|
const get = (url: string, data?: object) => response(url, 'GET', data);
|
||||||
const post = (url: string, data?: object) => response(url, 'POST', data);
|
const post = (url: string, data?: object) => response(url, 'POST', data);
|
||||||
const put = (url: string, data?: object) => response(url, 'PUT', data);
|
const put = (url: string, data?: object) => response(url, 'PUT', data);
|
||||||
const del = (url: string, data?: object) => response(url, 'DELETE', 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: 'Запланировано',
|
||||||
completed: 'Пройдено',
|
completed: 'Пройдено',
|
||||||
@@ -61,4 +34,6 @@ const TranslateStatus: { [id: string]: string } = {
|
|||||||
deferred: 'Отложено'
|
deferred: 'Отложено'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getVersion = () => get('version');
|
||||||
|
|
||||||
export { get, post, put, del, TranslateStatus };
|
export { get, post, put, del, TranslateStatus };
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
import {http} from "./api";
|
import {get} from "./api";
|
||||||
|
|
||||||
export const fetchChar = (id: number) =>
|
export const fetchChar = (id: number) => get(`characters/${id}`);
|
||||||
http.get(`characters/${id}`)
|
export const fetchCharNovels = (id: number) => get(`characters/${id}/novels`);
|
||||||
.then(res => res.data.data);
|
|
||||||
|
|
||||||
export const fetchCharNovels = (id: number) =>
|
|
||||||
http.get(`characters/${id}/novels`)
|
|
||||||
.then(res => res.data.data);
|
|
||||||
@@ -5,6 +5,7 @@ const errors: { [key: number]: string } = {
|
|||||||
5: 'Аккаунт уже активирован! Вы будете перенаправлены на главную через 3 секунды.',
|
5: 'Аккаунт уже активирован! Вы будете перенаправлены на главную через 3 секунды.',
|
||||||
6: 'Токен истек!',
|
6: 'Токен истек!',
|
||||||
8: 'Новый и старый пароли совпадают!',
|
8: 'Новый и старый пароли совпадают!',
|
||||||
|
9: 'Необходима проверка ReCaptcha',
|
||||||
100: 'Не найдено!'
|
100: 'Не найдено!'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ 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 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 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`);
|
||||||
|
|
||||||
export const fetchNovelComments = (id: number, offset=0, count=5) => get(`novels/${id}/comments?offset=${offset}&count=${count}`);
|
export const fetchNovelComments = (id: number, offset=0, count=5) => get(`novels/${id}/comments?offset=${offset}&count=${count}`);
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ 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 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 login = (login: string, password: string, recaptcha: string) => post('auth/login', { login, password, recaptcha });
|
||||||
export const userLogout = () => post('auth/logout');
|
export const userLogout = () => post('auth/logout');
|
||||||
|
|
||||||
export const activateAccount = (token: string) => post('auth/activate', { token });
|
export const activateAccount = (token: string) => post('auth/activate', { token });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {fetchUser, updateUser} from "../../../api/users";
|
|||||||
import Loading from "../../Loading";
|
import Loading from "../../Loading";
|
||||||
import './APUser.sass';
|
import './APUser.sass';
|
||||||
import {addNotification} from "../../../store/actions";
|
import {addNotification} from "../../../store/actions";
|
||||||
import Notification from "../../Notification/Notification";
|
import NotificationMessage from "../../Notifications/NotificationMessage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userId: number
|
userId: number
|
||||||
@@ -48,9 +48,9 @@ class ModifyUser extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
updateUser(user).then(() => {
|
updateUser(user).then(() => {
|
||||||
const notification = (
|
const notification = (
|
||||||
<Notification level={"success"}>
|
<NotificationMessage level={"success"}>
|
||||||
Пользователь успешно изменен!
|
Пользователь успешно изменен!
|
||||||
</Notification>
|
</NotificationMessage>
|
||||||
)
|
)
|
||||||
addNotification(notification)
|
addNotification(notification)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import AdminPanel from "../AdminPanel/AdminPanel";
|
|||||||
import PasswordRestoreGenerate from "../PasswordRestore/PasswordRestoreGenerate";
|
import PasswordRestoreGenerate from "../PasswordRestore/PasswordRestoreGenerate";
|
||||||
import PasswordRestore from "../PasswordRestore/PasswordRestore";
|
import PasswordRestore from "../PasswordRestore/PasswordRestore";
|
||||||
import Users from "../Users/Users";
|
import Users from "../Users/Users";
|
||||||
|
import {getVersion} from "../../api/api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notifications: React.ReactNode[]
|
notifications: React.ReactNode[]
|
||||||
@@ -60,6 +61,8 @@ class App extends React.Component<Props, State> {
|
|||||||
|
|
||||||
parser.registerTag('spoiler', SpoilerTag);
|
parser.registerTag('spoiler', SpoilerTag);
|
||||||
parser.registerTag('color', ColorTag);
|
parser.registerTag('color', ColorTag);
|
||||||
|
|
||||||
|
getVersion().then(res => console.debug(`Current back-end version: ${res.version}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoginModal = () => this.setState({ loginModalVisible: true, registerModalVisible: false });
|
showLoginModal = () => this.setState({ loginModalVisible: true, registerModalVisible: false });
|
||||||
|
|||||||
@@ -5,32 +5,35 @@ import './Character.sass'
|
|||||||
import parser from 'bbcode-to-react';
|
import parser from 'bbcode-to-react';
|
||||||
import NovelItem from "../NovelItem/NovelItem";
|
import NovelItem from "../NovelItem/NovelItem";
|
||||||
import Loading from "../Loading";
|
import Loading from "../Loading";
|
||||||
|
import NotFoundError from "../NotFoundError";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { id: number } }
|
match: { params: { charId: number } }
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
char: CharacterType | null
|
char: CharacterType | null
|
||||||
novels: NovelType[] | null
|
novels: NovelType[]
|
||||||
|
errorCode: number
|
||||||
};
|
};
|
||||||
|
|
||||||
class Character extends React.Component<Props, State> {
|
class Character extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
char: null, novels: null
|
char: null, novels: [], errorCode: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
const { id } = this.props.match.params;
|
const { charId } = this.props.match.params;
|
||||||
fetchChar(id).then(char => {
|
fetchChar(charId).then(char => {
|
||||||
this.setState({ char });
|
this.setState({ char });
|
||||||
document.title = char.localized_name ? `${char.original_name} / ${char.localized_name}` : char.original_name;
|
document.title = char.localized_name ? `${char.original_name} / ${char.localized_name}` : char.original_name;
|
||||||
});
|
}).catch(r => this.setState({ errorCode: r.code }));
|
||||||
fetchCharNovels(id).then(novels => this.setState({ novels }));
|
fetchCharNovels(charId).then(novels => this.setState({ novels }));
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { char, novels } = this.state;
|
const { char, novels, errorCode } = this.state;
|
||||||
|
if(errorCode === 100) return <NotFoundError/>;
|
||||||
if(!char) return <Loading/>;
|
if(!char) return <Loading/>;
|
||||||
return (
|
return (
|
||||||
<div className={'Character'}>
|
<div className={'Character'}>
|
||||||
@@ -45,9 +48,9 @@ class Character extends React.Component<Props, State> {
|
|||||||
<pre className="Character__Info_Description">{parser.toReact(char.description)}</pre>
|
<pre className="Character__Info_Description">{parser.toReact(char.description)}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{novels &&
|
{novels.length > 0 &&
|
||||||
<div className="Character__Novels">
|
<div className="Character__Novels">
|
||||||
{novels.map(novel => <NovelItem {...novel} key={novel.id} />)}
|
{novels.map(novel => <NovelItem {...novel} key={novel.id} viewType='grid' />)}
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Loading from "../Loading";
|
|||||||
import {fetchUser} from "../../api/users";
|
import {fetchUser} from "../../api/users";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {addNotification} from "../../store/actions";
|
import {addNotification} from "../../store/actions";
|
||||||
import Notification from "../Notification/Notification";
|
import NotificationMessage from "../Notifications/NotificationMessage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comments: CommentType[]
|
comments: CommentType[]
|
||||||
@@ -50,9 +50,9 @@ class Comments extends React.Component<Props, State> {
|
|||||||
|
|
||||||
if(commentText === '') {
|
if(commentText === '') {
|
||||||
const notification = (
|
const notification = (
|
||||||
<Notification level={"error"}>
|
<NotificationMessage level={"error"}>
|
||||||
Пустой комментарий
|
Пустой комментарий
|
||||||
</Notification>
|
</NotificationMessage>
|
||||||
);
|
);
|
||||||
addNotification(notification);
|
addNotification(notification);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import {addNotification, setUser} from "../../store/actions";
|
|||||||
import './LoginModal.sass'
|
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 NotificationMessage from "../Notifications/NotificationMessage";
|
||||||
// @ts-ignore
|
import Recaptcha from '../Recaptcha/Recaptcha'
|
||||||
import Recaptcha from 'react-recaptcha'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setUser: (user: UserType) => void
|
setUser: (user: UserType) => void
|
||||||
@@ -21,39 +20,46 @@ type State = {
|
|||||||
error: string
|
error: string
|
||||||
login: string
|
login: string
|
||||||
password: string
|
password: string
|
||||||
|
recaptcha: string
|
||||||
|
recaptchaNeeded: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class LoginModal extends React.Component<Props, State> {
|
class LoginModal extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
error: '', login: '', password: ''
|
error: '', login: '', password: '', recaptcha: '', recaptchaNeeded: false
|
||||||
};
|
};
|
||||||
|
|
||||||
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).then(user => {
|
const { login: log, password, recaptcha } = this.state;
|
||||||
|
login(log, password, recaptcha).then(user => {
|
||||||
if(!user) return;
|
if(!user) return;
|
||||||
|
|
||||||
setUser(user);
|
setUser(user);
|
||||||
const successful = (
|
const successful = (
|
||||||
<Notification level={"success"}>
|
<NotificationMessage level={"success"}>
|
||||||
Ты успешно вошел!
|
Ты успешно вошел!
|
||||||
</Notification>
|
</NotificationMessage>
|
||||||
);
|
);
|
||||||
addNotification(successful);
|
addNotification(successful);
|
||||||
hideModal();
|
hideModal();
|
||||||
})
|
})
|
||||||
.catch((r: ApiError) => this.setState({ error: errors[r.code] }));
|
.catch((r: ApiError) => {
|
||||||
|
this.setState({ error: errors[r.code] });
|
||||||
|
if(r.code === 9) this.setState({ recaptchaNeeded: true });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePasswordChange = (event: ChangeEvent) => this.setState({ password: (event.target as HTMLInputElement).value });
|
handlePasswordChange = (event: ChangeEvent) => this.setState({ password: (event.target as HTMLInputElement).value });
|
||||||
handleLoginChange = (event: ChangeEvent) => this.setState({ login: (event.target as HTMLInputElement).value });
|
handleLoginChange = (event: ChangeEvent) => this.setState({ login: (event.target as HTMLInputElement).value });
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { login, password, error, recaptchaNeeded } = this.state;
|
||||||
return (
|
return (
|
||||||
<Modal onCloseRequest={this.props.hideModal} className={'LoginModal'}>
|
<Modal onCloseRequest={this.props.hideModal} className={'LoginModal'}>
|
||||||
<h1>Авторизация</h1>
|
<h1>Авторизация</h1>
|
||||||
{this.state.error && <div className="LoginModal__Error">{this.state.error}</div>}
|
{error && <div className="LoginModal__Error">{error}</div>}
|
||||||
<form className={'LoginModal__Form'} onSubmit={this.loginAndFetchToken}>
|
<form className={'LoginModal__Form'} onSubmit={this.loginAndFetchToken}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -61,7 +67,7 @@ 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={login}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -69,8 +75,14 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
placeholder={'Пароль'}
|
placeholder={'Пароль'}
|
||||||
className={'LoginModal__Field'}
|
className={'LoginModal__Field'}
|
||||||
onChange={this.handlePasswordChange}
|
onChange={this.handlePasswordChange}
|
||||||
value={this.state.password}
|
value={password}
|
||||||
/>
|
/>
|
||||||
|
{recaptchaNeeded &&
|
||||||
|
<Recaptcha
|
||||||
|
onVerify={(res: any) => this.setState({ recaptcha: res })}
|
||||||
|
theme={'dark'}
|
||||||
|
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
|
||||||
|
/>}
|
||||||
<button className={'LoginModal__Submit'} type={"submit"}>Войти</button>
|
<button className={'LoginModal__Submit'} type={"submit"}>Войти</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="LoginModal__Links">
|
<div className="LoginModal__Links">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import './RegisterModal.sass'
|
|||||||
import {registerUser} from "../../api/users";
|
import {registerUser} from "../../api/users";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {addNotification} from "../../store/actions";
|
import {addNotification} from "../../store/actions";
|
||||||
import Notification from "../Notification/Notification";
|
import NotificationMessage from "../Notifications/NotificationMessage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
addNotification: (notification: React.ReactNode) => void
|
addNotification: (notification: React.ReactNode) => void
|
||||||
@@ -50,10 +50,10 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
else {
|
else {
|
||||||
registerUser(login, password1, email, recaptcha).then(() => {
|
registerUser(login, password1, email, recaptcha).then(() => {
|
||||||
const notification = (
|
const notification = (
|
||||||
<Notification level={"success"}>
|
<NotificationMessage level={"success"}>
|
||||||
Аккаунт успешно создан!
|
Аккаунт успешно создан!
|
||||||
Проверьте почту <strong>{email}</strong>. На неё была отправлена ссылка для подтверждения аккаунта.
|
Проверьте почту <strong>{email}</strong>. На неё была отправлена ссылка для подтверждения аккаунта.
|
||||||
</Notification>
|
</NotificationMessage>
|
||||||
);
|
);
|
||||||
const { addNotification, hideModal } = this.props;
|
const { addNotification, hideModal } = this.props;
|
||||||
addNotification(notification);
|
addNotification(notification);
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import './Notification.sass'
|
import './NotificationMessage.sass'
|
||||||
import {capitalize} from "../../utils";
|
import {capitalize} from "../../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,7 +12,7 @@ type State = {
|
|||||||
paused: boolean
|
paused: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class Notification extends React.Component<Props, State> {
|
class NotificationMessage extends React.Component<Props, State> {
|
||||||
timer: NodeJS.Timeout | undefined;
|
timer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -68,4 +68,4 @@ class Notification extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Notification;
|
export default NotificationMessage;
|
||||||
@@ -12,9 +12,29 @@
|
|||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
|
&.Grid
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
align-items: center
|
align-items: center
|
||||||
|
|
||||||
|
&.Table
|
||||||
|
width: 100%
|
||||||
|
padding: .2rem
|
||||||
|
box-sizing: border-box
|
||||||
|
flex-direction: row
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
&:not(:last-child)
|
||||||
|
margin: 0 0 .5rem
|
||||||
|
|
||||||
|
&:last-child
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.NovelItem__Status
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
|
||||||
&__Image
|
&__Image
|
||||||
border-radius: $borderRadius $borderRadius 0 0
|
border-radius: $borderRadius $borderRadius 0 0
|
||||||
background-size: cover
|
background-size: cover
|
||||||
|
|||||||
@@ -2,18 +2,22 @@ import React from 'react';
|
|||||||
import { TranslateStatus } from '../../api/api';
|
import { TranslateStatus } from '../../api/api';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import './NovelItem.sass';
|
import './NovelItem.sass';
|
||||||
|
import {capitalize, generateClassName} from "../../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number
|
id: number
|
||||||
original_name: string
|
original_name: string
|
||||||
|
viewType: string
|
||||||
|
key?: number
|
||||||
localized_name?: string
|
localized_name?: string
|
||||||
image?: string
|
image?: string
|
||||||
status?: string
|
status?: string
|
||||||
mark?: number
|
mark?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const NovelItem = ({ id, original_name, localized_name, image, status, mark }: Props) => (
|
const NovelItem = ({ id, original_name, localized_name, image, status, mark, viewType, key }: Props) => (
|
||||||
<Link to={`/novels/${id}`} className="NovelItem">
|
<Link to={`/novels/${id}`} className={generateClassName("NovelItem", capitalize(viewType) || 'Grid')}>
|
||||||
|
{viewType === 'grid' && (<>
|
||||||
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
||||||
<div className="NovelItem__Title">
|
<div className="NovelItem__Title">
|
||||||
{localized_name || original_name}
|
{localized_name || original_name}
|
||||||
@@ -21,6 +25,16 @@ const NovelItem = ({ id, original_name, localized_name, image, status, mark }: P
|
|||||||
{status && <div className="NovelItem__Status">
|
{status && <div className="NovelItem__Status">
|
||||||
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
||||||
</div>}
|
</div>}
|
||||||
|
</>)}
|
||||||
|
{viewType === 'table' && (<>
|
||||||
|
<div className="NovelItem__Component">{key || ''}</div>
|
||||||
|
<div className="NovelItem__Title">
|
||||||
|
{localized_name || original_name}
|
||||||
|
</div>
|
||||||
|
{status && <div className="NovelItem__Status">
|
||||||
|
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
||||||
|
</div>}
|
||||||
|
</>)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class Novels extends React.Component<{}, State> {
|
|||||||
return (
|
return (
|
||||||
<div className={'Novels'}>
|
<div className={'Novels'}>
|
||||||
<div className="Novels__List">
|
<div className="Novels__List">
|
||||||
{novels.map((novel: NovelType) => <NovelItem {...novel} key={novel.id} />)}
|
{novels.map((novel: NovelType) => <NovelItem {...novel} key={novel.id} viewType={'grid'} />)}
|
||||||
</div>
|
</div>
|
||||||
{/*<div className="Novels__Filters">
|
{/*<div className="Novels__Filters">
|
||||||
<div className="Novels__Filters_Genre">
|
<div className="Novels__Filters_Genre">
|
||||||
|
|||||||
@@ -9,9 +9,17 @@
|
|||||||
font-weight: 600
|
font-weight: 600
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
|
.svg-inline--fa
|
||||||
|
margin: 0 .5rem
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: var(--link-color)
|
||||||
|
|
||||||
&__List
|
&__List
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
width: 100%
|
||||||
|
|
||||||
&:not(:last-child)
|
&:not(:last-child)
|
||||||
border-bottom: var(--link-color) 1px solid
|
border-bottom: var(--link-color) 1px solid
|
||||||
@@ -28,6 +36,12 @@
|
|||||||
|
|
||||||
&_Novels
|
&_Novels
|
||||||
display: flex
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
&.Grid
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
justify-content: center
|
|
||||||
|
&.Table
|
||||||
|
flex-direction: column
|
||||||
|
width: 100%
|
||||||
@@ -3,7 +3,9 @@ import './UserNovels.sass';
|
|||||||
import { fetchUserNovels, fetchUser } from '../../../api/users';
|
import { fetchUserNovels, fetchUser } from '../../../api/users';
|
||||||
import Loading from '../../Loading';
|
import Loading from '../../Loading';
|
||||||
import NovelItem from '../../NovelItem/NovelItem';
|
import NovelItem from '../../NovelItem/NovelItem';
|
||||||
import {getRandomInt} from "../../../utils";
|
import {capitalize, generateClassName, getRandomInt} from "../../../utils";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: number } }
|
match: { params: { userId: number } }
|
||||||
@@ -12,11 +14,12 @@ type Props = {
|
|||||||
type State = {
|
type State = {
|
||||||
novels: NovelType[]
|
novels: NovelType[]
|
||||||
user: UserType | null
|
user: UserType | null
|
||||||
|
viewType: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserNovels extends React.Component<Props, State> {
|
class UserNovels extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
novels: [], user: null
|
novels: [], user: null, viewType: 'grid'
|
||||||
};
|
};
|
||||||
|
|
||||||
getRandomNovel = () => {
|
getRandomNovel = () => {
|
||||||
@@ -34,10 +37,25 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
.then(novels => this.setState({ novels }));
|
.then(novels => this.setState({ novels }));
|
||||||
fetchUser(userId)
|
fetchUser(userId)
|
||||||
.then(user => this.setState({ user }));
|
.then(user => this.setState({ user }));
|
||||||
|
|
||||||
|
const viewType = localStorage.getItem('viewType');
|
||||||
|
if(viewType) {
|
||||||
|
if(viewType !== 'grid' && viewType !== 'table') {
|
||||||
|
console.warn('Тип был неизвестен... Сбрасываю...');
|
||||||
|
localStorage.setItem('viewType', 'grid')
|
||||||
|
} else {
|
||||||
|
this.setState({ viewType });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
changeViewType = (type: 'grid' | 'table') => {
|
||||||
|
this.setState({ viewType: type });
|
||||||
|
localStorage.setItem('viewType', type);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { novels, user } = this.state;
|
const { novels, user, viewType } = this.state;
|
||||||
if(!novels || !user) return <Loading/>;
|
if(!novels || !user) return <Loading/>;
|
||||||
|
|
||||||
const planned = novels.filter(n => n.status === 'planned');
|
const planned = novels.filter(n => n.status === 'planned');
|
||||||
@@ -47,24 +65,26 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
<div className='UserNovels'>
|
<div className='UserNovels'>
|
||||||
<div className="UserNovels__Title">
|
<div className="UserNovels__Title">
|
||||||
Новеллы пользователя {user.username}
|
Новеллы пользователя {user.username}
|
||||||
|
<FontAwesomeIcon icon={faThLarge} onClick={() => this.changeViewType('grid')}/>
|
||||||
|
<FontAwesomeIcon icon={faAlignJustify} onClick={() => this.changeViewType('table')}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{planned.length > 0 &&<div className="UserNovels__List">
|
{planned.length > 0 && <div className={'UserNovels__List'}>
|
||||||
<div className="UserNovels__List_Title">
|
<div className="UserNovels__List_Title">
|
||||||
<div style={{ marginRight: '1rem' }}>Запланированные</div>
|
<div style={{ marginRight: '1rem' }}>Запланированные</div>
|
||||||
<button onClick={this.getRandomNovel}>Рандомная новелла</button>
|
<button onClick={this.getRandomNovel}>Рандомная новелла</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="UserNovels__List_Novels">
|
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||||
{planned.map(novel => <NovelItem {...novel} key={novel.id}/>)}
|
{planned.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{completed.length > 0 &&<div className="UserNovels__List">
|
{completed.length > 0 && <div className={'UserNovels__List'}>
|
||||||
<div className="UserNovels__List_Title">
|
<div className="UserNovels__List_Title">
|
||||||
Пройденные
|
Пройденные
|
||||||
</div>
|
</div>
|
||||||
<div className="UserNovels__List_Novels">
|
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||||
{completed.map(novel => <NovelItem {...novel} key={novel.id}/>)}
|
{completed.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class User extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="User__List_Items">
|
<div className="User__List_Items">
|
||||||
{novels.length > 0 ?
|
{novels.length > 0 ?
|
||||||
novels.slice(0, 4).map((novel) => <NovelItem {...novel} key={novel.id} />
|
novels.slice(0, 4).map((novel) => <NovelItem {...novel} key={novel.id} viewType={'grid'} />
|
||||||
) : (
|
) : (
|
||||||
<div>У этого пользователя нет новелл!</div>
|
<div>У этого пользователя нет новелл!</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const initialState: React.ReactNode[] = [];
|
|||||||
|
|
||||||
type Action = {
|
type Action = {
|
||||||
type: string
|
type: string
|
||||||
notification: React.ReactNode[]
|
notification: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (state = initialState, action: Action) => {
|
export default (state = initialState, action: Action) => {
|
||||||
|
|||||||
+6
-1
@@ -15,4 +15,9 @@ const hasPermission = (user: UserType, permission: string) => {
|
|||||||
|
|
||||||
const hasAccessToAdminPanel = (user: UserType) => user.is_superuser || user.group.is_superuser || hasPermission(user, 'admin_panel')
|
const hasAccessToAdminPanel = (user: UserType) => user.is_superuser || user.group.is_superuser || hasPermission(user, 'admin_panel')
|
||||||
|
|
||||||
export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel };
|
const generateClassName = (baseClassName: string, ...classNames: string[]) => {
|
||||||
|
if(classNames.length === 0) return baseClassName;
|
||||||
|
else return `${baseClassName} ${classNames.join(" ")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel, generateClassName };
|
||||||
Reference in New Issue
Block a user