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';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 post = (url: string, data?: object) => response(url, 'POST', data);
|
||||
const put = (url: string, data?: object) => response(url, 'PUT', data);
|
||||
const del = (url: string, data?: object) => response(url, 'DELETE', data);
|
||||
|
||||
// const get = (url: string, data?: object) => http.get(url, data).then(r => r?.data?.data);
|
||||
// const post = (url: string, data?: object) => http.post(url, data).then(r => r?.data.data);
|
||||
// const put = (url: string, data?: object) => http.put(url, data).then(r => r?.data.data);
|
||||
// const del = (url: string, data?: object) => http.delete(url, data).then(r => r?.data.data);
|
||||
|
||||
const TranslateStatus: { [id: string]: string } = {
|
||||
planned: 'Запланировано',
|
||||
completed: 'Пройдено',
|
||||
@@ -61,4 +34,6 @@ const TranslateStatus: { [id: string]: string } = {
|
||||
deferred: 'Отложено'
|
||||
};
|
||||
|
||||
export const getVersion = () => get('version');
|
||||
|
||||
export { get, post, put, del, TranslateStatus };
|
||||
@@ -1,9 +1,4 @@
|
||||
import {http} from "./api";
|
||||
import {get} from "./api";
|
||||
|
||||
export const fetchChar = (id: number) =>
|
||||
http.get(`characters/${id}`)
|
||||
.then(res => res.data.data);
|
||||
|
||||
export const fetchCharNovels = (id: number) =>
|
||||
http.get(`characters/${id}/novels`)
|
||||
.then(res => res.data.data);
|
||||
export const fetchChar = (id: number) => get(`characters/${id}`);
|
||||
export const fetchCharNovels = (id: number) => get(`characters/${id}/novels`);
|
||||
@@ -5,6 +5,7 @@ const errors: { [key: number]: string } = {
|
||||
5: 'Аккаунт уже активирован! Вы будете перенаправлены на главную через 3 секунды.',
|
||||
6: 'Токен истек!',
|
||||
8: 'Новый и старый пароли совпадают!',
|
||||
9: 'Необходима проверка ReCaptcha',
|
||||
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 createNovel = (novel: NovelType) => post('novels', { ...novel });
|
||||
|
||||
export const fetchNovel = (id: number) => get(`novels/${id}`);
|
||||
export const updateNovel = (id: number, novel: NovelType) => put(`novels/${id}`, { ...novel });
|
||||
|
||||
export const fetchNovelCharacters = (id: number) => get(`novels/${id}/characters`);
|
||||
|
||||
export const fetchNovelGenres = (id: number) => get(`novels/${id}/genres`);
|
||||
|
||||
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 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 activateAccount = (token: string) => post('auth/activate', { token });
|
||||
|
||||
@@ -4,7 +4,7 @@ import {fetchUser, updateUser} from "../../../api/users";
|
||||
import Loading from "../../Loading";
|
||||
import './APUser.sass';
|
||||
import {addNotification} from "../../../store/actions";
|
||||
import Notification from "../../Notification/Notification";
|
||||
import NotificationMessage from "../../Notifications/NotificationMessage";
|
||||
|
||||
type Props = {
|
||||
userId: number
|
||||
@@ -48,9 +48,9 @@ class ModifyUser extends React.Component<Props, State> {
|
||||
}
|
||||
updateUser(user).then(() => {
|
||||
const notification = (
|
||||
<Notification level={"success"}>
|
||||
<NotificationMessage level={"success"}>
|
||||
Пользователь успешно изменен!
|
||||
</Notification>
|
||||
</NotificationMessage>
|
||||
)
|
||||
addNotification(notification)
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import AdminPanel from "../AdminPanel/AdminPanel";
|
||||
import PasswordRestoreGenerate from "../PasswordRestore/PasswordRestoreGenerate";
|
||||
import PasswordRestore from "../PasswordRestore/PasswordRestore";
|
||||
import Users from "../Users/Users";
|
||||
import {getVersion} from "../../api/api";
|
||||
|
||||
type Props = {
|
||||
notifications: React.ReactNode[]
|
||||
@@ -60,6 +61,8 @@ class App extends React.Component<Props, State> {
|
||||
|
||||
parser.registerTag('spoiler', SpoilerTag);
|
||||
parser.registerTag('color', ColorTag);
|
||||
|
||||
getVersion().then(res => console.debug(`Current back-end version: ${res.version}`))
|
||||
}
|
||||
|
||||
showLoginModal = () => this.setState({ loginModalVisible: true, registerModalVisible: false });
|
||||
|
||||
@@ -5,32 +5,35 @@ import './Character.sass'
|
||||
import parser from 'bbcode-to-react';
|
||||
import NovelItem from "../NovelItem/NovelItem";
|
||||
import Loading from "../Loading";
|
||||
import NotFoundError from "../NotFoundError";
|
||||
|
||||
type Props = {
|
||||
match: { params: { id: number } }
|
||||
match: { params: { charId: number } }
|
||||
}
|
||||
|
||||
type State = {
|
||||
char: CharacterType | null
|
||||
novels: NovelType[] | null
|
||||
novels: NovelType[]
|
||||
errorCode: number
|
||||
};
|
||||
|
||||
class Character extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
char: null, novels: null
|
||||
char: null, novels: [], errorCode: 0
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
const { id } = this.props.match.params;
|
||||
fetchChar(id).then(char => {
|
||||
const { charId } = this.props.match.params;
|
||||
fetchChar(charId).then(char => {
|
||||
this.setState({ char });
|
||||
document.title = char.localized_name ? `${char.original_name} / ${char.localized_name}` : char.original_name;
|
||||
});
|
||||
fetchCharNovels(id).then(novels => this.setState({ novels }));
|
||||
}).catch(r => this.setState({ errorCode: r.code }));
|
||||
fetchCharNovels(charId).then(novels => this.setState({ novels }));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { char, novels } = this.state;
|
||||
const { char, novels, errorCode } = this.state;
|
||||
if(errorCode === 100) return <NotFoundError/>;
|
||||
if(!char) return <Loading/>;
|
||||
return (
|
||||
<div className={'Character'}>
|
||||
@@ -45,9 +48,9 @@ class Character extends React.Component<Props, State> {
|
||||
<pre className="Character__Info_Description">{parser.toReact(char.description)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{novels &&
|
||||
{novels.length > 0 &&
|
||||
<div className="Character__Novels">
|
||||
{novels.map(novel => <NovelItem {...novel} key={novel.id} />)}
|
||||
{novels.map(novel => <NovelItem {...novel} key={novel.id} viewType='grid' />)}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import Loading from "../Loading";
|
||||
import {fetchUser} from "../../api/users";
|
||||
import {connect} from "react-redux";
|
||||
import {addNotification} from "../../store/actions";
|
||||
import Notification from "../Notification/Notification";
|
||||
import NotificationMessage from "../Notifications/NotificationMessage";
|
||||
|
||||
type Props = {
|
||||
comments: CommentType[]
|
||||
@@ -50,9 +50,9 @@ class Comments extends React.Component<Props, State> {
|
||||
|
||||
if(commentText === '') {
|
||||
const notification = (
|
||||
<Notification level={"error"}>
|
||||
<NotificationMessage level={"error"}>
|
||||
Пустой комментарий
|
||||
</Notification>
|
||||
</NotificationMessage>
|
||||
);
|
||||
addNotification(notification);
|
||||
} else {
|
||||
|
||||
@@ -6,9 +6,8 @@ import {addNotification, setUser} from "../../store/actions";
|
||||
import './LoginModal.sass'
|
||||
import {Link} from "react-router-dom";
|
||||
import errors from "../../api/errors";
|
||||
import Notification from "../Notification/Notification";
|
||||
// @ts-ignore
|
||||
import Recaptcha from 'react-recaptcha'
|
||||
import NotificationMessage from "../Notifications/NotificationMessage";
|
||||
import Recaptcha from '../Recaptcha/Recaptcha'
|
||||
|
||||
type Props = {
|
||||
setUser: (user: UserType) => void
|
||||
@@ -21,39 +20,46 @@ type State = {
|
||||
error: string
|
||||
login: string
|
||||
password: string
|
||||
recaptcha: string
|
||||
recaptchaNeeded: boolean
|
||||
};
|
||||
|
||||
class LoginModal extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
error: '', login: '', password: ''
|
||||
error: '', login: '', password: '', recaptcha: '', recaptchaNeeded: false
|
||||
};
|
||||
|
||||
loginAndFetchToken = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
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;
|
||||
|
||||
setUser(user);
|
||||
const successful = (
|
||||
<Notification level={"success"}>
|
||||
<NotificationMessage level={"success"}>
|
||||
Ты успешно вошел!
|
||||
</Notification>
|
||||
</NotificationMessage>
|
||||
);
|
||||
addNotification(successful);
|
||||
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 });
|
||||
handleLoginChange = (event: ChangeEvent) => this.setState({ login: (event.target as HTMLInputElement).value });
|
||||
|
||||
render() {
|
||||
const { login, password, error, recaptchaNeeded } = this.state;
|
||||
return (
|
||||
<Modal onCloseRequest={this.props.hideModal} className={'LoginModal'}>
|
||||
<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}>
|
||||
<input
|
||||
type="text"
|
||||
@@ -61,7 +67,7 @@ class LoginModal extends React.Component<Props, State> {
|
||||
placeholder={'Логин'}
|
||||
className={'LoginModal__Field'}
|
||||
onChange={this.handleLoginChange}
|
||||
value={this.state.login}
|
||||
value={login}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
@@ -69,8 +75,14 @@ class LoginModal extends React.Component<Props, State> {
|
||||
placeholder={'Пароль'}
|
||||
className={'LoginModal__Field'}
|
||||
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>
|
||||
</form>
|
||||
<div className="LoginModal__Links">
|
||||
|
||||
@@ -5,7 +5,7 @@ import './RegisterModal.sass'
|
||||
import {registerUser} from "../../api/users";
|
||||
import {connect} from "react-redux";
|
||||
import {addNotification} from "../../store/actions";
|
||||
import Notification from "../Notification/Notification";
|
||||
import NotificationMessage from "../Notifications/NotificationMessage";
|
||||
|
||||
type Props = {
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
@@ -50,10 +50,10 @@ class RegisterModal extends React.Component<Props, State> {
|
||||
else {
|
||||
registerUser(login, password1, email, recaptcha).then(() => {
|
||||
const notification = (
|
||||
<Notification level={"success"}>
|
||||
<NotificationMessage level={"success"}>
|
||||
Аккаунт успешно создан!
|
||||
Проверьте почту <strong>{email}</strong>. На неё была отправлена ссылка для подтверждения аккаунта.
|
||||
</Notification>
|
||||
</NotificationMessage>
|
||||
);
|
||||
const { addNotification, hideModal } = this.props;
|
||||
addNotification(notification);
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import './Notification.sass'
|
||||
import './NotificationMessage.sass'
|
||||
import {capitalize} from "../../utils";
|
||||
|
||||
type Props = {
|
||||
@@ -12,7 +12,7 @@ type State = {
|
||||
paused: boolean
|
||||
};
|
||||
|
||||
class Notification extends React.Component<Props, State> {
|
||||
class NotificationMessage extends React.Component<Props, State> {
|
||||
timer: NodeJS.Timeout | undefined;
|
||||
|
||||
state = {
|
||||
@@ -68,4 +68,4 @@ class Notification extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default Notification;
|
||||
export default NotificationMessage;
|
||||
@@ -12,9 +12,29 @@
|
||||
width: 100%
|
||||
|
||||
display: flex
|
||||
|
||||
&.Grid
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
|
||||
&.Table
|
||||
width: 100%
|
||||
padding: .2rem
|
||||
box-sizing: border-box
|
||||
flex-direction: row
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
|
||||
&:not(:last-child)
|
||||
margin: 0 0 .5rem
|
||||
|
||||
&:last-child
|
||||
margin: 0
|
||||
|
||||
.NovelItem__Status
|
||||
white-space: nowrap
|
||||
|
||||
|
||||
&__Image
|
||||
border-radius: $borderRadius $borderRadius 0 0
|
||||
background-size: cover
|
||||
|
||||
@@ -2,18 +2,22 @@ import React from 'react';
|
||||
import { TranslateStatus } from '../../api/api';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './NovelItem.sass';
|
||||
import {capitalize, generateClassName} from "../../utils";
|
||||
|
||||
type Props = {
|
||||
id: number
|
||||
original_name: string
|
||||
viewType: string
|
||||
key?: number
|
||||
localized_name?: string
|
||||
image?: string
|
||||
status?: string
|
||||
mark?: number
|
||||
}
|
||||
|
||||
const NovelItem = ({ id, original_name, localized_name, image, status, mark }: Props) => (
|
||||
<Link to={`/novels/${id}`} className="NovelItem">
|
||||
const NovelItem = ({ id, original_name, localized_name, image, status, mark, viewType, key }: Props) => (
|
||||
<Link to={`/novels/${id}`} className={generateClassName("NovelItem", capitalize(viewType) || 'Grid')}>
|
||||
{viewType === 'grid' && (<>
|
||||
<div className='NovelItem__Image' style={{ backgroundImage: `url(${image || '/static/img/no-image.png'})` }}/>
|
||||
<div className="NovelItem__Title">
|
||||
{localized_name || original_name}
|
||||
@@ -21,6 +25,16 @@ const NovelItem = ({ id, original_name, localized_name, image, status, mark }: P
|
||||
{status && <div className="NovelItem__Status">
|
||||
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
||||
</div>}
|
||||
</>)}
|
||||
{viewType === 'table' && (<>
|
||||
<div className="NovelItem__Component">{key || ''}</div>
|
||||
<div className="NovelItem__Title">
|
||||
{localized_name || original_name}
|
||||
</div>
|
||||
{status && <div className="NovelItem__Status">
|
||||
{TranslateStatus[status]} {mark !== undefined && mark > 0 && mark}
|
||||
</div>}
|
||||
</>)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import './Novels.sass'
|
||||
import './Novels.sass'
|
||||
import React from "react";
|
||||
import {fetchNovels} from "../../api/novels";
|
||||
import NovelItem from '../NovelItem/NovelItem';
|
||||
import Loading from "../Loading";
|
||||
import Loading from "../Loading";
|
||||
|
||||
type State = {
|
||||
novels: NovelType[] | null
|
||||
@@ -52,7 +52,7 @@ class Novels extends React.Component<{}, State> {
|
||||
return (
|
||||
<div className={'Novels'}>
|
||||
<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 className="Novels__Filters">
|
||||
<div className="Novels__Filters_Genre">
|
||||
|
||||
@@ -47,7 +47,7 @@ class Recaptcha extends React.Component<Props, State> {
|
||||
|
||||
reset = () => {
|
||||
// @ts-ignore
|
||||
if(typeof window?.grecaptcha?.reset === "function")window.grecaptcha.reset(document.getElementById('g-recaptcha'));
|
||||
if(typeof window?.grecaptcha?.reset === "function") window.grecaptcha.reset(document.getElementById('g-recaptcha'));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -9,9 +9,17 @@
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
|
||||
.svg-inline--fa
|
||||
margin: 0 .5rem
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
color: var(--link-color)
|
||||
|
||||
&__List
|
||||
display: flex
|
||||
flex-direction: column
|
||||
width: 100%
|
||||
|
||||
&:not(:last-child)
|
||||
border-bottom: var(--link-color) 1px solid
|
||||
@@ -28,6 +36,12 @@
|
||||
|
||||
&_Novels
|
||||
display: flex
|
||||
justify-content: center
|
||||
|
||||
&.Grid
|
||||
flex-direction: row
|
||||
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 Loading from '../../Loading';
|
||||
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 = {
|
||||
match: { params: { userId: number } }
|
||||
@@ -12,11 +14,12 @@ type Props = {
|
||||
type State = {
|
||||
novels: NovelType[]
|
||||
user: UserType | null
|
||||
viewType: string
|
||||
};
|
||||
|
||||
class UserNovels extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
novels: [], user: null
|
||||
novels: [], user: null, viewType: 'grid'
|
||||
};
|
||||
|
||||
getRandomNovel = () => {
|
||||
@@ -34,10 +37,25 @@ class UserNovels extends React.Component<Props, State> {
|
||||
.then(novels => this.setState({ novels }));
|
||||
fetchUser(userId)
|
||||
.then(user => this.setState({ user }));
|
||||
|
||||
const viewType = localStorage.getItem('viewType');
|
||||
if(viewType) {
|
||||
if(viewType !== 'grid' && viewType !== 'table') {
|
||||
console.warn('Тип был неизвестен... Сбрасываю...');
|
||||
localStorage.setItem('viewType', 'grid')
|
||||
} else {
|
||||
this.setState({ viewType });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
changeViewType = (type: 'grid' | 'table') => {
|
||||
this.setState({ viewType: type });
|
||||
localStorage.setItem('viewType', type);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { novels, user } = this.state;
|
||||
const { novels, user, viewType } = this.state;
|
||||
if(!novels || !user) return <Loading/>;
|
||||
|
||||
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__Title">
|
||||
Новеллы пользователя {user.username}
|
||||
<FontAwesomeIcon icon={faThLarge} onClick={() => this.changeViewType('grid')}/>
|
||||
<FontAwesomeIcon icon={faAlignJustify} onClick={() => this.changeViewType('table')}/>
|
||||
</div>
|
||||
|
||||
{planned.length > 0 &&<div className="UserNovels__List">
|
||||
{planned.length > 0 && <div className={'UserNovels__List'}>
|
||||
<div className="UserNovels__List_Title">
|
||||
<div style={{ marginRight: '1rem' }}>Запланированные</div>
|
||||
<button onClick={this.getRandomNovel}>Рандомная новелла</button>
|
||||
</div>
|
||||
<div className="UserNovels__List_Novels">
|
||||
{planned.map(novel => <NovelItem {...novel} key={novel.id}/>)}
|
||||
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||
{planned.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{completed.length > 0 &&<div className="UserNovels__List">
|
||||
{completed.length > 0 && <div className={'UserNovels__List'}>
|
||||
<div className="UserNovels__List_Title">
|
||||
Пройденные
|
||||
</div>
|
||||
<div className="UserNovels__List_Novels">
|
||||
{completed.map(novel => <NovelItem {...novel} key={novel.id}/>)}
|
||||
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||
{completed.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@ class User extends React.Component<Props, State> {
|
||||
</div>
|
||||
<div className="User__List_Items">
|
||||
{novels.length > 0 ?
|
||||
novels.slice(0, 4).map((novel) => <NovelItem {...novel} key={novel.id} />
|
||||
novels.slice(0, 4).map((novel) => <NovelItem {...novel} key={novel.id} viewType={'grid'} />
|
||||
) : (
|
||||
<div>У этого пользователя нет новелл!</div>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ const initialState: React.ReactNode[] = [];
|
||||
|
||||
type Action = {
|
||||
type: string
|
||||
notification: React.ReactNode[]
|
||||
notification: React.ReactNode
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
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