diff --git a/src/api/api.ts b/src/api/api.ts index 0a7ff76..3354ed1 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -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 }; \ No newline at end of file diff --git a/src/api/characters.ts b/src/api/characters.ts index c37cd9e..30f0e78 100644 --- a/src/api/characters.ts +++ b/src/api/characters.ts @@ -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); \ No newline at end of file +export const fetchChar = (id: number) => get(`characters/${id}`); +export const fetchCharNovels = (id: number) => get(`characters/${id}/novels`); \ No newline at end of file diff --git a/src/api/errors.ts b/src/api/errors.ts index 767275b..6b65fcc 100644 --- a/src/api/errors.ts +++ b/src/api/errors.ts @@ -5,6 +5,7 @@ const errors: { [key: number]: string } = { 5: 'Аккаунт уже активирован! Вы будете перенаправлены на главную через 3 секунды.', 6: 'Токен истек!', 8: 'Новый и старый пароли совпадают!', + 9: 'Необходима проверка ReCaptcha', 100: 'Не найдено!' }; diff --git a/src/api/novels.ts b/src/api/novels.ts index 00e25a3..ec38a90 100644 --- a/src/api/novels.ts +++ b/src/api/novels.ts @@ -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}`); diff --git a/src/api/users.ts b/src/api/users.ts index bc0536f..bbb47e8 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -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 }); diff --git a/src/components/AdminPanel/Users/ModifyUser.tsx b/src/components/AdminPanel/Users/ModifyUser.tsx index 2f0b43e..dd7d3d5 100644 --- a/src/components/AdminPanel/Users/ModifyUser.tsx +++ b/src/components/AdminPanel/Users/ModifyUser.tsx @@ -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 { } updateUser(user).then(() => { const notification = ( - + Пользователь успешно изменен! - + ) addNotification(notification) }); diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index cc733e6..ef0fbd9 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -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 { 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 }); diff --git a/src/components/Character/Character.tsx b/src/components/Character/Character.tsx index 2ff94f7..4d10ae2 100644 --- a/src/components/Character/Character.tsx +++ b/src/components/Character/Character.tsx @@ -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 { 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 ; if(!char) return ; return (
@@ -45,9 +48,9 @@ class Character extends React.Component {
{parser.toReact(char.description)}
- {novels && + {novels.length > 0 &&
- {novels.map(novel => )} + {novels.map(novel => )}
} ); diff --git a/src/components/Comment/Comments.tsx b/src/components/Comment/Comments.tsx index 9b8c3d8..af2544f 100644 --- a/src/components/Comment/Comments.tsx +++ b/src/components/Comment/Comments.tsx @@ -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 { if(commentText === '') { const notification = ( - + Пустой комментарий - + ); addNotification(notification); } else { diff --git a/src/components/LoginModal/LoginModal.tsx b/src/components/LoginModal/LoginModal.tsx index 66ca356..a3ff5f8 100644 --- a/src/components/LoginModal/LoginModal.tsx +++ b/src/components/LoginModal/LoginModal.tsx @@ -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 { 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 = ( - + Ты успешно вошел! - + ); 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 (

Авторизация

- {this.state.error &&
{this.state.error}
} + {error &&
{error}
}
{ placeholder={'Логин'} className={'LoginModal__Field'} onChange={this.handleLoginChange} - value={this.state.login} + value={login} /> { placeholder={'Пароль'} className={'LoginModal__Field'} onChange={this.handlePasswordChange} - value={this.state.password} + value={password} /> + {recaptchaNeeded && + this.setState({ recaptcha: res })} + theme={'dark'} + sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'} + />}
diff --git a/src/components/LoginModal/RegisterModal.tsx b/src/components/LoginModal/RegisterModal.tsx index c391ddd..20a1c59 100644 --- a/src/components/LoginModal/RegisterModal.tsx +++ b/src/components/LoginModal/RegisterModal.tsx @@ -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 { else { registerUser(login, password1, email, recaptcha).then(() => { const notification = ( - + Аккаунт успешно создан! Проверьте почту {email}. На неё была отправлена ссылка для подтверждения аккаунта. - + ); const { addNotification, hideModal } = this.props; addNotification(notification); diff --git a/src/components/Notification/Notification.sass b/src/components/Notifications/NotificationMessage.sass similarity index 100% rename from src/components/Notification/Notification.sass rename to src/components/Notifications/NotificationMessage.sass diff --git a/src/components/Notification/Notification.tsx b/src/components/Notifications/NotificationMessage.tsx similarity index 92% rename from src/components/Notification/Notification.tsx rename to src/components/Notifications/NotificationMessage.tsx index dd7b8b0..ef882be 100644 --- a/src/components/Notification/Notification.tsx +++ b/src/components/Notifications/NotificationMessage.tsx @@ -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 { +class NotificationMessage extends React.Component { timer: NodeJS.Timeout | undefined; state = { @@ -68,4 +68,4 @@ class Notification extends React.Component { } } -export default Notification; \ No newline at end of file +export default NotificationMessage; \ No newline at end of file diff --git a/src/components/NovelItem/NovelItem.sass b/src/components/NovelItem/NovelItem.sass index b106a90..0597d59 100644 --- a/src/components/NovelItem/NovelItem.sass +++ b/src/components/NovelItem/NovelItem.sass @@ -12,8 +12,28 @@ width: 100% display: flex - flex-direction: column - align-items: center + + &.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 diff --git a/src/components/NovelItem/NovelItem.tsx b/src/components/NovelItem/NovelItem.tsx index 4bd927f..2c8a633 100644 --- a/src/components/NovelItem/NovelItem.tsx +++ b/src/components/NovelItem/NovelItem.tsx @@ -2,25 +2,39 @@ 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) => ( - -
-
- {localized_name || original_name} -
- {status &&
- {TranslateStatus[status]} {mark !== undefined && mark > 0 && mark} -
} +const NovelItem = ({ id, original_name, localized_name, image, status, mark, viewType, key }: Props) => ( + + {viewType === 'grid' && (<> +
+
+ {localized_name || original_name} +
+ {status &&
+ {TranslateStatus[status]} {mark !== undefined && mark > 0 && mark} +
} + )} + {viewType === 'table' && (<> +
{key || ''}
+
+ {localized_name || original_name} +
+ {status &&
+ {TranslateStatus[status]} {mark !== undefined && mark > 0 && mark} +
} + )} ); diff --git a/src/components/Novels/Novels.tsx b/src/components/Novels/Novels.tsx index ebc986f..dd24820 100644 --- a/src/components/Novels/Novels.tsx +++ b/src/components/Novels/Novels.tsx @@ -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 (
- {novels.map((novel: NovelType) => )} + {novels.map((novel: NovelType) => )}
{/*
diff --git a/src/components/Recaptcha/Recaptcha.tsx b/src/components/Recaptcha/Recaptcha.tsx index 4825061..25764fb 100644 --- a/src/components/Recaptcha/Recaptcha.tsx +++ b/src/components/Recaptcha/Recaptcha.tsx @@ -47,7 +47,7 @@ class Recaptcha extends React.Component { 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() { diff --git a/src/components/User/Novels/UserNovels.sass b/src/components/User/Novels/UserNovels.sass index 35bfc54..9fcf15c 100644 --- a/src/components/User/Novels/UserNovels.sass +++ b/src/components/User/Novels/UserNovels.sass @@ -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 - flex-direction: row - flex-wrap: wrap - justify-content: center \ No newline at end of file + justify-content: center + + &.Grid + flex-direction: row + flex-wrap: wrap + + &.Table + flex-direction: column + width: 100% \ No newline at end of file diff --git a/src/components/User/Novels/UserNovels.tsx b/src/components/User/Novels/UserNovels.tsx index afe91f0..d4fbe5a 100644 --- a/src/components/User/Novels/UserNovels.tsx +++ b/src/components/User/Novels/UserNovels.tsx @@ -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 { state: State = { - novels: [], user: null + novels: [], user: null, viewType: 'grid' }; getRandomNovel = () => { @@ -34,10 +37,25 @@ class UserNovels extends React.Component { .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 ; const planned = novels.filter(n => n.status === 'planned'); @@ -47,24 +65,26 @@ class UserNovels extends React.Component {
Новеллы пользователя {user.username} + this.changeViewType('grid')}/> + this.changeViewType('table')}/>
- {planned.length > 0 &&
+ {planned.length > 0 &&
Запланированные
-
- {planned.map(novel => )} +
+ {planned.map(novel => )}
} - {completed.length > 0 &&
+ {completed.length > 0 &&
Пройденные
-
- {completed.map(novel => )} +
+ {completed.map(novel => )}
}
diff --git a/src/components/User/User.tsx b/src/components/User/User.tsx index b42527d..47c8a6f 100644 --- a/src/components/User/User.tsx +++ b/src/components/User/User.tsx @@ -91,7 +91,7 @@ class User extends React.Component {
{novels.length > 0 ? - novels.slice(0, 4).map((novel) => + novels.slice(0, 4).map((novel) => ) : (
У этого пользователя нет новелл!
)} diff --git a/src/store/reducers/notifications.ts b/src/store/reducers/notifications.ts index ecddbf8..c2d9e74 100644 --- a/src/store/reducers/notifications.ts +++ b/src/store/reducers/notifications.ts @@ -5,7 +5,7 @@ const initialState: React.ReactNode[] = []; type Action = { type: string - notification: React.ReactNode[] + notification: React.ReactNode } export default (state = initialState, action: Action) => { diff --git a/src/utils.ts b/src/utils.ts index 6de6892..0c86836 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 }; \ No newline at end of file +const generateClassName = (baseClassName: string, ...classNames: string[]) => { + if(classNames.length === 0) return baseClassName; + else return `${baseClassName} ${classNames.join(" ")}` +} + +export { capitalize, getRandomInt, hasPermission, hasAccessToAdminPanel, generateClassName }; \ No newline at end of file