many new
@@ -0,0 +1 @@
|
|||||||
|
REACT_EDITOR=goland
|
||||||
@@ -3,35 +3,31 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.12.1",
|
"@fortawesome/free-brands-svg-icons": "5.13.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
"@fortawesome/free-solid-svg-icons": "5.13.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
"@fortawesome/react-fontawesome": "0.1.10",
|
||||||
"@sentry/browser": "^5.7.1",
|
"@sentry/browser": "^5.7.1",
|
||||||
"@types/react-infinite-scroller": "^1.2.1",
|
"bbcode-to-react": "0.2.9",
|
||||||
"axios": "^0.19.0",
|
"react": "16.13.1",
|
||||||
"bbcode-to-react": "^0.2.9",
|
"react-dom": "16.13.1",
|
||||||
"react": "^16.12.0",
|
"react-loader-spinner": "3.1.14",
|
||||||
"react-dom": "^16.12.0",
|
"react-redux": "7.2.0",
|
||||||
"react-infinite-scroller": "^1.2.4",
|
"react-router-dom": "5.2.0",
|
||||||
"react-loader-spinner": "^3.1.4",
|
"react-transition-group": "4.4.1",
|
||||||
"react-recaptcha": "^2.3.10",
|
"redux": "4.0.5"
|
||||||
"react-redux": "^7.1.3",
|
|
||||||
"react-router-dom": "^5.1.2",
|
|
||||||
"react-transition-group": "^4.3.0",
|
|
||||||
"redux": "^4.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "12.12.26",
|
"@types/node": "14.0.13",
|
||||||
"@types/react": "16.9.19",
|
"@types/react": "16.9.36",
|
||||||
"@types/react-dom": "^16.9.5",
|
"@types/react-dom": "16.9.8",
|
||||||
"@types/react-redux": "^7.1.7",
|
"@types/react-redux": "7.1.9",
|
||||||
"@types/react-router-dom": "^5.1.3",
|
"@types/react-router-dom": "5.1.5",
|
||||||
"@types/react-transition-group": "^4.2.4",
|
"@types/react-transition-group": "4.4.0",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "4.14.1",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "2.13.8",
|
||||||
"typescript": "^3.8.2",
|
"typescript": "3.9.5",
|
||||||
"react-scripts": "^3.3.1"
|
"react-scripts": "3.4.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
@@ -47,9 +43,7 @@
|
|||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
],
|
],
|
||||||
"development": [
|
"development": [
|
||||||
"last 1 chrome version",
|
"last 1 chrome version"
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
$mainColor: #efefef
|
$mainColor: #efefef
|
||||||
$secondaryColor: #aaaaaa
|
$secondaryColor: #aaaaaa
|
||||||
$linkHoverColor: #dddddd
|
$linkHoverColor: #dfdfdf
|
||||||
|
|
||||||
// BLACK
|
// BLACK
|
||||||
$primaryBackgroundColor: #121212
|
$primaryBackgroundColor: #121212
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
|
|
||||||
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?: string | FormData) => {
|
||||||
let bUrl = baseUrl;
|
let bUrl = baseUrl;
|
||||||
if(baseUrl.endsWith('/')) bUrl = bUrl.slice(0, baseUrl.length-1);
|
if(baseUrl.endsWith('/')) bUrl = bUrl.slice(0, baseUrl.length-1);
|
||||||
if(url.startsWith('/')) url = url.slice(1, baseUrl.length);
|
if(url.startsWith('/')) url = url.slice(1, baseUrl.length);
|
||||||
|
|
||||||
if(typeof body === 'object') body = JSON.stringify(body);
|
|
||||||
const res = await fetch(`${bUrl}/${url}`, {
|
const res = await fetch(`${bUrl}/${url}`, {
|
||||||
method, body,
|
method, body,
|
||||||
credentials: "include"
|
credentials: "include"
|
||||||
});
|
});
|
||||||
|
|
||||||
const json: ApiResponse = await res.json();
|
let json: ApiResponse;
|
||||||
|
try {
|
||||||
|
json = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
json = { error: false };
|
||||||
|
}
|
||||||
|
|
||||||
if(res.status === 429) return new Promise(res => {
|
if(res.status === 429) return new Promise(res => {
|
||||||
setTimeout(res.bind(null, response(url, method, body)), 1500);
|
setTimeout(res.bind(null, response(url, method, body)), 1500);
|
||||||
@@ -21,10 +26,10 @@ export const response = async (url: string, method: string, body?: any) => {
|
|||||||
return Promise.resolve(json.data);
|
return Promise.resolve(json.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const get = (url: string, data?: object) => response(url, 'GET', data);
|
const get = (url: string, data?: object) => response(url, 'GET', JSON.stringify(data));
|
||||||
const post = (url: string, data?: object) => response(url, 'POST', data);
|
const post = (url: string, data?: object | FormData) => response(url, 'POST', data instanceof FormData ? data : JSON.stringify(data));
|
||||||
const put = (url: string, data?: object) => response(url, 'PUT', data);
|
const put = (url: string, data?: object) => response(url, 'PUT', JSON.stringify(data));
|
||||||
const del = (url: string, data?: object) => response(url, 'DELETE', data);
|
const del = (url: string, data?: object) => response(url, 'DELETE', JSON.stringify(data));
|
||||||
|
|
||||||
const TranslateStatus: { [id: string]: string } = {
|
const TranslateStatus: { [id: string]: string } = {
|
||||||
planned: 'Запланировано',
|
planned: 'Запланировано',
|
||||||
@@ -35,5 +40,6 @@ const TranslateStatus: { [id: string]: string } = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getVersion = () => get('version');
|
export const getVersion = () => get('version');
|
||||||
|
export const version = '0.9';
|
||||||
|
|
||||||
export { get, post, put, del, TranslateStatus };
|
export { get, post, put, del, TranslateStatus };
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
import {post} from "./api";
|
import {post} from "./api";
|
||||||
|
|
||||||
export const restore = (token: string, new_password: string) => post('auth/restore', { token, new_password });
|
export const restore = (token: string, new_password: string) => post('auth/restore', { token, new_password });
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import {get} from './api';
|
import {get, put} from './api';
|
||||||
|
|
||||||
const getNews = () => get('news');
|
const fetchNews = () => get('news');
|
||||||
const getPost = (id: number) => get(`news/${id}`);
|
|
||||||
|
|
||||||
export { getNews, getPost }
|
const fetchPost = (id: number) => get(`news/${id}`);
|
||||||
|
const updatePost = (post: PostType) => put(`news/${post.id}`, post);
|
||||||
|
|
||||||
|
export { fetchNews, fetchPost, updatePost }
|
||||||
@@ -5,7 +5,7 @@ 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 })
|
||||||
|
|
||||||
export const fetchCurrentUser = () => get('users/current');
|
export const fetchCurrentUser = () => get('users/me');
|
||||||
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 });
|
||||||
@@ -15,4 +15,8 @@ export const userLogout = () => post('auth/logout');
|
|||||||
export const activateAccount = (token: string) => post('auth/activate', { token });
|
export const activateAccount = (token: string) => post('auth/activate', { token });
|
||||||
export const generateActivateLink = () => post('auth/activateToken');
|
export const generateActivateLink = () => post('auth/activateToken');
|
||||||
|
|
||||||
|
export const fetchUserSettings = () => get('users/me/settings');
|
||||||
|
export const updateUserSettings = (use_gravatar: boolean, avatar: string) => post('users/me/settings', { use_gravatar, avatar });
|
||||||
|
export const uploadAvatar = (avatar: FormData) => post('users/me/avatar', avatar);
|
||||||
|
|
||||||
export const generateRestoreLink = (email: string, recaptcha: string) => post('auth/restoreToken', { email, recaptcha });
|
export const generateRestoreLink = (email: string, recaptcha: string) => post('auth/restoreToken', { email, recaptcha });
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export interface ObjectClassNames {
|
||||||
|
[index: string]: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClassName = number | string | ObjectClassNames | false | null | undefined;
|
||||||
|
|
||||||
|
export default function classNames(...classNames: ClassName[]) {
|
||||||
|
let result: string[] = [];
|
||||||
|
|
||||||
|
classNames.forEach((item: ClassName): void => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (typeof item) {
|
||||||
|
case 'string':
|
||||||
|
result.push(item);
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
Object.keys(item).forEach((key: string) => {
|
||||||
|
if (item[key]) {
|
||||||
|
result.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result.push(`${item}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.join(' ');
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
.News
|
|
||||||
background-color: var(--sec-bg-color)
|
|
||||||
padding: .5rem
|
|
||||||
|
|
||||||
&__Title
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
justify-content: space-between
|
|
||||||
font-size: 1.5rem
|
|
||||||
font-weight: 600
|
|
||||||
margin-bottom: .5rem
|
|
||||||
|
|
||||||
.Post
|
|
||||||
background-color: var(--block-color)
|
|
||||||
padding: .5rem
|
|
||||||
|
|
||||||
&__Title
|
|
||||||
font-size: 1.2rem
|
|
||||||
|
|
||||||
&__Content
|
|
||||||
border-top: var(--link-color) 1px solid
|
|
||||||
border-bottom: var(--link-color) 1px solid
|
|
||||||
|
|
||||||
&__Footer
|
|
||||||
display: flex
|
|
||||||
justify-content: space-between
|
|
||||||
|
|
||||||
&:not(:last-child)
|
|
||||||
margin-bottom: 1rem
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {getNews} from "../../api/news";
|
|
||||||
import Loading from "../Loading";
|
|
||||||
import {Link} from "react-router-dom";
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
news: PostType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
class News extends React.Component<{}, State> {
|
|
||||||
state: State = {
|
|
||||||
news: []
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
getNews().then((news: PostType[]) => {
|
|
||||||
news.forEach(p => p.date = new Date(p.date));
|
|
||||||
this.setState({ news })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { news } = this.state;
|
|
||||||
|
|
||||||
if(news.length === 0) return <Loading/>;
|
|
||||||
return (
|
|
||||||
<div className="News">
|
|
||||||
<div className="News__Title">
|
|
||||||
<div>Новости</div>
|
|
||||||
<Link to={'/news'}>Все новости</Link>
|
|
||||||
</div>
|
|
||||||
{news.slice(0, 5).map(post => (
|
|
||||||
<div className={'Post'} key={post.id}>
|
|
||||||
<div className="Post__Title"><Link to={`/news/${post.id }`}>{post.title}</Link></div>
|
|
||||||
<div className="Post__Content">{post.short_post}</div>
|
|
||||||
<div className="Post__Footer">
|
|
||||||
<div>
|
|
||||||
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
|
|
||||||
</div>
|
|
||||||
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default News;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
.Modal__Overlay
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
justify-content: center
|
|
||||||
position: fixed
|
|
||||||
top: 0
|
|
||||||
right: 0
|
|
||||||
left: 0
|
|
||||||
bottom: 0
|
|
||||||
background-color: #0f0f0fef
|
|
||||||
z-index: 9999
|
|
||||||
opacity: 1
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
.Modal__CloseButton
|
|
||||||
background: none
|
|
||||||
border: none
|
|
||||||
font-size: 1.5rem
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
right: 0
|
|
||||||
margin: 1rem
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.PasswordRestore
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
|
|
||||||
button, input
|
|
||||||
margin: .5rem
|
|
||||||
font-size: 1.2rem
|
|
||||||
|
|
||||||
input
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
button
|
|
||||||
align-self: flex-start
|
|
||||||
@@ -7,7 +7,7 @@ import store from "./store/store";
|
|||||||
|
|
||||||
// import * as Sentry from '@sentry/browser';
|
// import * as Sentry from '@sentry/browser';
|
||||||
|
|
||||||
import App from './components/App/App';
|
import App from './pages/App/App';
|
||||||
import * as serviceWorker from './serviceWorker';
|
import * as serviceWorker from './serviceWorker';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {activateAccount} from "../api/users";
|
|||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {addNotification} from "../store/actions";
|
import {addNotification} from "../store/actions";
|
||||||
import errors from "../api/errors";
|
import errors from "../api/errors";
|
||||||
import Loading from "./Loading";
|
import Loading from "../ui/Loading";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
history: { push: (path: string) => void }
|
history: { push: (path: string) => void }
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
@import "../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
.AdminPanel
|
.AdminPanel
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
|
|
||||||
&__Title
|
&__Title
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
&__Router
|
&__Router
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
border-bottom: 1px solid white
|
border-bottom: 1px solid white
|
||||||
|
|
||||||
a
|
a
|
||||||
margin: 0 .2rem
|
margin: 0 .2rem
|
||||||
padding: .1rem
|
padding: .1rem
|
||||||
@@ -1,61 +1,63 @@
|
|||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import NotFoundError from "../NotFoundError";
|
import NotFoundError from "../NotFoundError";
|
||||||
import {hasAccessToAdminPanel} from "../../utils";
|
import {hasAccessToAdminPanel} from "../../utils";
|
||||||
import './AdminPanel.sass';
|
import './AdminPanel.sass';
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import AdminPanelUsers from "./Users/AdminPanelUsers";
|
import AdminPanelUsers from "./Users/AdminPanelUsers";
|
||||||
import AdminPanelNovels from "./Novels/AdminPanelNovels";
|
import AdminPanelNovels from "./Novels/AdminPanelNovels";
|
||||||
import ModifyUser from "./Users/ModifyUser";
|
import ModifyUser from "./Users/ModifyUser";
|
||||||
|
import APNews from "./News/APNews";
|
||||||
type Props = {
|
import APModifyPost from "./News/APModifyPost";
|
||||||
user: UserType
|
|
||||||
match: { path: string }
|
type Props = {
|
||||||
location: { pathname: string }
|
user: UserType
|
||||||
}
|
match: { path: string }
|
||||||
|
location: { pathname: string }
|
||||||
type State = {
|
}
|
||||||
|
|
||||||
}
|
type State = {}
|
||||||
|
|
||||||
class AdminPanel extends React.Component<Props, State> {
|
class AdminPanel extends React.Component<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
const { user, match: { path }, location: { pathname } } = this.props;
|
const { user, match: { path }, location: { pathname } } = this.props;
|
||||||
if(!user || !user.group) return <NotFoundError/>;
|
if(!user || !user.group) return <NotFoundError/>;
|
||||||
if(!hasAccessToAdminPanel(user)) return <NotFoundError/>;
|
if(!hasAccessToAdminPanel(user)) return <NotFoundError/>;
|
||||||
const subPath = pathname.slice(path.length+1);
|
const subPath = pathname.slice(path.length+1);
|
||||||
let component;
|
let component;
|
||||||
switch(subPath) {
|
switch(subPath) {
|
||||||
case 'users': component = <AdminPanelUsers path={`${path}/${subPath}`}/>; break;
|
case 'users': component = <AdminPanelUsers path={`${path}/${subPath}`}/>; break;
|
||||||
case 'novels': component = <AdminPanelNovels/>; break;
|
case 'novels': component = <AdminPanelNovels/>; break;
|
||||||
case 'news': component = <div>Новости</div>; break;
|
case 'news': component = <APNews path={`${path}/${subPath}`}/>; break;
|
||||||
default: {
|
default: {
|
||||||
const users = /users\/(\d+)/;
|
const users = /users\/(\d+)/;
|
||||||
|
const news = /news\/(\d+)/;
|
||||||
const userMatch = subPath.match(users);
|
|
||||||
|
const userMatch = subPath.match(users);
|
||||||
if(userMatch)
|
const newsMatch = subPath.match(news);
|
||||||
component = <ModifyUser userId={parseInt(userMatch[1])}/>;
|
|
||||||
}
|
if(userMatch) component = <ModifyUser userId={parseInt(userMatch[1])}/>;
|
||||||
}
|
if(newsMatch) component = <APModifyPost postId={parseInt(newsMatch[1])}/>;
|
||||||
|
}
|
||||||
return (
|
}
|
||||||
<div className={'AdminPanel'}>
|
|
||||||
<div className="AdminPanel__Title">Админ панель <span style={{ color: 'var(--error-bg-color)' }}>(в разработке)</span></div>
|
return (
|
||||||
<div className="AdminPanel__Router">
|
<div className={'AdminPanel'}>
|
||||||
<Link to={path}>Главная</Link>
|
<div className="AdminPanel__Title">Админ панель <span style={{ color: 'var(--error-bg-color)' }}>(в разработке)</span></div>
|
||||||
<Link to={`${path}/users`}>Пользователи</Link>
|
<div className="AdminPanel__Router">
|
||||||
<Link to={`${path}/novels`}>Новелы</Link>
|
<Link to={path}>Главная</Link>
|
||||||
<Link to={`${path}/news`}>Новости</Link>
|
<Link to={`${path}/users`}>Пользователи</Link>
|
||||||
</div>
|
<Link to={`${path}/novels`}>Новелы</Link>
|
||||||
{component}
|
<Link to={`${path}/news`}>Новости</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
{component}
|
||||||
}
|
</div>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
export default connect(
|
}
|
||||||
(state: StoreState) => ({
|
|
||||||
user: state.currentUser
|
export default connect(
|
||||||
})
|
(state: StoreState) => ({
|
||||||
|
user: state.currentUser
|
||||||
|
})
|
||||||
)(AdminPanel);
|
)(AdminPanel);
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import React, {ChangeEvent, FormEvent} from "react";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import NotFoundError from "../../NotFoundError";
|
||||||
|
import {fetchPost, updatePost} from "../../../api/news";
|
||||||
|
import './APNews.sass';
|
||||||
|
import '../../../ui/TextField/TextField.sass'
|
||||||
|
import Input from "../../../ui/Input/Input";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import BBEditor from "../../../ui/BBEditor/BBEditor";
|
||||||
|
// @ts-ignore
|
||||||
|
import parser from 'bbcode-to-react';
|
||||||
|
import Title from "../../../ui/Title/Title";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
postId: number
|
||||||
|
//addNotification: (notification: React.ReactNode) => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
post: PostType | null
|
||||||
|
title: string
|
||||||
|
|
||||||
|
previewShort: string
|
||||||
|
previewFull: string
|
||||||
|
};
|
||||||
|
|
||||||
|
class APModifyPost extends React.Component<Props, State> {
|
||||||
|
private shortPost = React.createRef<HTMLTextAreaElement>();
|
||||||
|
private fullPost = React.createRef<HTMLTextAreaElement>();
|
||||||
|
|
||||||
|
state: State = {
|
||||||
|
post: null, title: '',
|
||||||
|
previewShort: '', previewFull: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { postId } = this.props;
|
||||||
|
fetchPost(postId).then((post: PostType) => {
|
||||||
|
this.setState({ post, title: post.title });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
savePost = async (event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const post = this.state.post;
|
||||||
|
if(!post) return;
|
||||||
|
if(!this.shortPost.current || !this.fullPost.current) return;
|
||||||
|
|
||||||
|
post.short_post = this.shortPost.current.value;
|
||||||
|
post.full_post = this.fullPost.current.value;
|
||||||
|
|
||||||
|
await updatePost(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||||
|
if(prevState.post) return;
|
||||||
|
if(!this.state.post) return;
|
||||||
|
if(!this.shortPost.current || !this.fullPost.current) return;
|
||||||
|
this.shortPost.current.value = this.state.post.short_post;
|
||||||
|
this.fullPost.current.value = this.state.post.full_post;
|
||||||
|
}
|
||||||
|
|
||||||
|
previewShort = () => {
|
||||||
|
if(!this.shortPost.current) return;
|
||||||
|
this.setState({ previewShort: parser.toReact(this.shortPost.current.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
previewFull = () => {
|
||||||
|
if(!this.fullPost.current) return;
|
||||||
|
this.setState({ previewFull: parser.toReact(this.fullPost.current.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTitle = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const post = this.state.post;
|
||||||
|
if(!post) return;
|
||||||
|
post.title = event.target.value;
|
||||||
|
this.setState({ post });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { post, title, previewShort, previewFull } = this.state;
|
||||||
|
|
||||||
|
if(!post) return <NotFoundError/>;
|
||||||
|
return (
|
||||||
|
<Group title={title}>
|
||||||
|
<form onSubmit={this.savePost} className={'APPost'}>
|
||||||
|
<Input type="text" placeholder={'Заголовок'} value={post.title} onChange={this.changeTitle}/>
|
||||||
|
|
||||||
|
<div className={'APPost__Short'}>
|
||||||
|
<Title>Короткая новость</Title>
|
||||||
|
<BBEditor inputRef={this.shortPost} placeholder={'Короткая новость'} />
|
||||||
|
{previewShort.length > 0 && (<>
|
||||||
|
<Title>Предпросмотр</Title>
|
||||||
|
<div className={'APPost__Short_Preview TextField'}>{previewShort}</div>
|
||||||
|
</>)}
|
||||||
|
<Button type={"button"} onClick={this.previewShort}>Предпросмотр</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'APPost__Full'}>
|
||||||
|
<Title>Полная новость</Title>
|
||||||
|
<BBEditor inputRef={this.fullPost} placeholder={'Полная новость'} />
|
||||||
|
{previewFull.length > 0 && (<>
|
||||||
|
<Title>Предпросмотр</Title>
|
||||||
|
<div className={'APPost__Full_Preview TextField'}>{previewFull}</div>
|
||||||
|
</>)}
|
||||||
|
<Button type={"button"} onClick={this.previewFull}>Предпросмотр</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button>Сохранить</Button>
|
||||||
|
<Button type={"button"} style={{ color: 'var(--error) '}}>Удалить</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APModifyPost;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.APPost
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.Input, .TextField, .Button
|
||||||
|
font-size: 1.2rem
|
||||||
|
|
||||||
|
.Input, .TextField
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
.TextField
|
||||||
|
width: 100%
|
||||||
|
min-height: 4rem
|
||||||
|
resize: none
|
||||||
|
|
||||||
|
&__Short, &__Full
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
margin-bottom: 1rem
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {fetchNews} from "../../../api/news";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
path: string
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
news: PostType[]
|
||||||
|
};
|
||||||
|
|
||||||
|
class APNews extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
news: []
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
fetchNews().then(news => this.setState({ news }));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { news } = this.state;
|
||||||
|
const { path } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group title={'Новости'}>
|
||||||
|
{news.map(post => (
|
||||||
|
<Link to={`${path}/${post.id}`} key={post.id}>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{post.id}</div>
|
||||||
|
<div>{post.title}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APNews;
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class AdminPanelNovels extends React.Component<Props, State> {
|
class AdminPanelNovels extends React.Component<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>Novels</div>
|
<div>Novels</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state: StoreState) => ({
|
(state: StoreState) => ({
|
||||||
user: state.currentUser
|
user: state.currentUser
|
||||||
})
|
})
|
||||||
)(AdminPanelNovels);
|
)(AdminPanelNovels);
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
.APUser__Modify
|
.APUser__Modify
|
||||||
&_Title
|
&_Title
|
||||||
text-align: center
|
text-align: center
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
|
|
||||||
&_Form
|
&_Form
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
button
|
button
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
|
|
||||||
label
|
label
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: nowrap
|
flex-wrap: nowrap
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
align-items: center
|
align-items: center
|
||||||
font-size: 1.2rem
|
font-size: 1.2rem
|
||||||
|
|
||||||
input
|
input
|
||||||
margin: .5rem 0 .5rem .5rem
|
margin: .5rem 0 .5rem .5rem
|
||||||
font-size: 1.2rem
|
font-size: 1.2rem
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
&[type='checkbox']
|
&[type='checkbox']
|
||||||
width: 0
|
width: 0
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {fetchUsers} from "../../../api/users";
|
import {fetchUsers} from "../../../api/users";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
path: string
|
path: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
users: UserType[]
|
users: UserType[]
|
||||||
};
|
};
|
||||||
|
|
||||||
class AdminPanelUsers extends React.Component<Props, State> {
|
class AdminPanelUsers extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
users: []
|
users: []
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
fetchUsers().then(users => this.setState({ users }))
|
fetchUsers().then(users => this.setState({ users }))
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { users } = this.state;
|
const { users } = this.state;
|
||||||
const { path } = this.props;
|
const { path } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ textAlign: "center", fontWeight: 600, fontSize: '1.5rem' }}>Пользователи</div>
|
<div style={{ textAlign: "center", fontWeight: 600, fontSize: '1.5rem' }}>Пользователи</div>
|
||||||
<div>
|
<div>
|
||||||
{users.sort((a, b) => a.id - b.id).map(user => (
|
{users.sort((a, b) => a.id - b.id).map(user => (
|
||||||
<Link to={`${path}/${user.id}`} key={user.id}>
|
<Link to={`${path}/${user.id}`} key={user.id}>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{user.id}</div>
|
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{user.id}</div>
|
||||||
<div>{user.username}</div>
|
<div>{user.username}</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state: StoreState) => ({
|
(state: StoreState) => ({
|
||||||
user: state.currentUser
|
user: state.currentUser
|
||||||
})
|
})
|
||||||
)(AdminPanelUsers);
|
)(AdminPanelUsers);
|
||||||
@@ -1,92 +1,93 @@
|
|||||||
import React, {ChangeEvent, FormEvent} from "react";
|
import React, {ChangeEvent, FormEvent} from "react";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {fetchUser, updateUser} from "../../../api/users";
|
import {fetchUser, updateUser} from "../../../api/users";
|
||||||
import Loading from "../../Loading";
|
import Loading from "../../../ui/Loading";
|
||||||
import './APUser.sass';
|
import './APUser.sass';
|
||||||
import {addNotification} from "../../../store/actions";
|
import {addNotification} from "../../../store/actions";
|
||||||
import NotificationMessage from "../../Notifications/NotificationMessage";
|
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
type Props = {
|
|
||||||
userId: number
|
type Props = {
|
||||||
addNotification: (notification: React.ReactNode) => void
|
userId: number
|
||||||
};
|
addNotification: (notification: React.ReactNode) => void
|
||||||
|
};
|
||||||
type State = {
|
|
||||||
user: UserType | null
|
type State = {
|
||||||
username: string
|
user: UserType | null
|
||||||
};
|
username: string
|
||||||
|
};
|
||||||
class ModifyUser extends React.Component<Props, State> {
|
|
||||||
state: State = {
|
class ModifyUser extends React.Component<Props, State> {
|
||||||
user: null, username: ''
|
state: State = {
|
||||||
}
|
user: null, username: ''
|
||||||
|
}
|
||||||
componentDidMount() {
|
|
||||||
const { userId } = this.props;
|
componentDidMount() {
|
||||||
fetchUser(userId).then((user: UserType) => {
|
const { userId } = this.props;
|
||||||
this.setState({ user, username: user.username });
|
fetchUser(userId).then((user: UserType) => {
|
||||||
document.title = `${user.username} / Админ-панель`;
|
this.setState({ user, username: user.username });
|
||||||
})
|
document.title = `${user.username} / Админ-панель`;
|
||||||
}
|
})
|
||||||
|
}
|
||||||
onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const user = Object.assign({}, this.state.user);
|
onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const target = event.target;
|
const user = Object.assign({}, this.state.user);
|
||||||
// @ts-ignore
|
const target = event.target;
|
||||||
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
// @ts-ignore
|
||||||
this.setState({ user })
|
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||||
}
|
this.setState({ user })
|
||||||
|
}
|
||||||
onSubmit = (event: FormEvent) => {
|
|
||||||
event.preventDefault();
|
onSubmit = (event: FormEvent) => {
|
||||||
const { user } = this.state;
|
event.preventDefault();
|
||||||
const { addNotification } = this.props;
|
const { user } = this.state;
|
||||||
|
const { addNotification } = this.props;
|
||||||
if(user === null) {
|
|
||||||
console.error('Пользователь - null!');
|
if(user === null) {
|
||||||
return;
|
console.error('Пользователь - null!');
|
||||||
}
|
return;
|
||||||
updateUser(user).then(() => {
|
}
|
||||||
const notification = (
|
updateUser(user).then(() => {
|
||||||
<NotificationMessage level={"success"}>
|
const notification = (
|
||||||
Пользователь успешно изменен!
|
<NotificationMessage level={"success"}>
|
||||||
</NotificationMessage>
|
Пользователь успешно изменен!
|
||||||
)
|
</NotificationMessage>
|
||||||
addNotification(notification)
|
)
|
||||||
});
|
addNotification(notification)
|
||||||
}
|
});
|
||||||
|
}
|
||||||
render() {
|
|
||||||
const { user, username } = this.state;
|
render() {
|
||||||
|
const { user, username } = this.state;
|
||||||
if(!user) return <Loading/>;
|
|
||||||
return (
|
if(!user) return <Loading/>;
|
||||||
<div className={'APUser__Modify'}>
|
return (
|
||||||
<div className={'APUser__Modify_Title'}>{username}</div>
|
<div className={'APUser__Modify'}>
|
||||||
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
|
<div className={'APUser__Modify_Title'}>{username}</div>
|
||||||
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'} value={user.username}/></label>
|
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
|
||||||
<label>Email: <input type='email' onChange={this.onChange} name={'email'} value={user.email}/></label>
|
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'} value={user.username}/></label>
|
||||||
<label>Аватар: <input type='text' onChange={this.onChange} name={'avatar'} value={user.avatar}/></label>
|
<label>Email: <input type='email' onChange={this.onChange} name={'email'} value={user.email}/></label>
|
||||||
|
<label>Аватар: <input type='text' onChange={this.onChange} name={'avatar'} value={user.avatar}/></label>
|
||||||
<label>Суперюзер: <input type='checkbox' onChange={this.onChange} name={'is_superuser'} checked={user.is_superuser}/></label>
|
|
||||||
<label>Активирован: <input type='checkbox' onChange={this.onChange} name={'is_activated'} checked={user.is_activated}/></label>
|
<label>Суперюзер: <input type='checkbox' onChange={this.onChange} name={'is_superuser'} checked={user.is_superuser}/></label>
|
||||||
|
<label>Активирован: <input type='checkbox' onChange={this.onChange} name={'is_activated'} checked={user.is_activated}/></label>
|
||||||
<div style={{ textAlign: "right" }}>
|
|
||||||
<button>Сохранить!</button>
|
<div style={{ textAlign: "right" }}>
|
||||||
</div>
|
<Button>Сохранить!</Button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)
|
</div>
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export default connect(
|
|
||||||
(state: StoreState) => ({
|
export default connect(
|
||||||
user: state.currentUser
|
(state: StoreState) => ({
|
||||||
}),
|
user: state.currentUser
|
||||||
(dispatch) => ({
|
}),
|
||||||
addNotification: (notification: React.ReactNode) => {
|
(dispatch) => ({
|
||||||
dispatch(addNotification(notification))
|
addNotification: (notification: React.ReactNode) => {
|
||||||
}
|
dispatch(addNotification(notification))
|
||||||
})
|
}
|
||||||
|
})
|
||||||
)(ModifyUser);
|
)(ModifyUser);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap')
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap')
|
||||||
@import "../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
html, body, #root
|
html, body, #root
|
||||||
width: 100%
|
width: 100%
|
||||||
@@ -75,37 +75,13 @@ a
|
|||||||
&:hover
|
&:hover
|
||||||
color: var(--link-color)
|
color: var(--link-color)
|
||||||
|
|
||||||
input, button, textarea
|
|
||||||
color: var(--main-color)
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
color: var(--sec-color)
|
|
||||||
|
|
||||||
button
|
|
||||||
cursor: pointer
|
|
||||||
padding: .2rem
|
|
||||||
border-radius: $borderRadius
|
|
||||||
background-color: var(--block-color)
|
|
||||||
border: 1px solid var(--main-color)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
background-color: var(--block-sec-color)
|
|
||||||
color: var(--link-color)
|
|
||||||
border: 1px solid var(--link-color)
|
|
||||||
|
|
||||||
|
|
||||||
input
|
|
||||||
background-color: var(--sec-block-color) !important
|
|
||||||
border: none
|
|
||||||
border-bottom: 1px solid var(--main-color) !important
|
|
||||||
|
|
||||||
*
|
*
|
||||||
scrollbar-color: #eee #222
|
scrollbar-color: #eee #222
|
||||||
scrollbar-width: thin
|
scrollbar-width: thin
|
||||||
|
|
||||||
.Container
|
.Container
|
||||||
max-width: 1200px
|
max-width: 1200px
|
||||||
min-height: calc(100% - 4rem)
|
min-height: calc(100vh - 16rem)
|
||||||
margin: .5rem auto
|
margin: .5rem auto
|
||||||
|
|
||||||
.Notifications
|
.Notifications
|
||||||
@@ -113,7 +89,10 @@ input
|
|||||||
bottom: 0
|
bottom: 0
|
||||||
left: 0
|
left: 0
|
||||||
z-index: 100000
|
z-index: 100000
|
||||||
padding: 1rem
|
|
||||||
|
@media (min-width: 500px)
|
||||||
|
padding: 1rem
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
.Successful, .Error
|
.Successful, .Error
|
||||||
border: var(--link-color) 1px solid
|
border: var(--link-color) 1px solid
|
||||||
@@ -9,16 +9,14 @@ import NotFoundError from "../NotFoundError";
|
|||||||
import {fetchCurrentUser} from "../../api/users";
|
import {fetchCurrentUser} from "../../api/users";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {setUser} from "../../store/actions";
|
import {setUser} from "../../store/actions";
|
||||||
import LoginModal from "../LoginModal/LoginModal";
|
|
||||||
import RegisterModal from "../LoginModal/RegisterModal";
|
|
||||||
import Character from "../Character/Character";
|
import Character from "../Character/Character";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import parser from 'bbcode-to-react';
|
import parser from 'bbcode-to-react';
|
||||||
import SpoilerTag from "../Spoiler/SpoilerTag";
|
import SpoilerTag from "../../ui/Spoiler/SpoilerTag";
|
||||||
import Novels from "../Novels/Novels";
|
import Novels from "../Novels/Novels";
|
||||||
import Footer from '../Footer/Footer';
|
import Footer from '../Footer/Footer';
|
||||||
import UserNovels from '../User/Novels/UserNovels';
|
import UserNovels from '../User/Novels/UserNovels';
|
||||||
import ColorTag from "../Tags/ColorTag";
|
import ColorTag from "../../ui/Tags/ColorTag";
|
||||||
import ActivateAccount from "../ActivateAccount";
|
import ActivateAccount from "../ActivateAccount";
|
||||||
import AllNews from "../News/AllNews";
|
import AllNews from "../News/AllNews";
|
||||||
import Post from "../News/Post";
|
import Post from "../News/Post";
|
||||||
@@ -26,30 +24,23 @@ 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";
|
import {getVersion, version} from "../../api/api";
|
||||||
|
import Settings from "../User/Settings/Settings";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notifications: React.ReactNode[]
|
notifications: React.ReactNode[]
|
||||||
setUser: (user: UserType) => void
|
setUser: (user: UserType) => void
|
||||||
|
modal: React.ReactNode
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {}
|
||||||
loginModalVisible: boolean
|
|
||||||
registerModalVisible: boolean
|
|
||||||
error: string
|
|
||||||
login: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
class App extends React.Component<Props, State> {
|
class App extends React.Component<Props, State> {
|
||||||
|
state: State = {}
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
loginModalVisible: false, registerModalVisible: false, error: '',
|
|
||||||
login: '', password: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const { setUser } = this.props;
|
const { setUser } = this.props;
|
||||||
fetchCurrentUser().then(setUser).catch((err: ApiError) => {
|
fetchCurrentUser().then(setUser).catch((err: ApiError) => {
|
||||||
if(err.code !== 3 && err.text) console.error(err.text);
|
if(err.code !== 3 && err.text) console.error(err.text);
|
||||||
@@ -62,24 +53,26 @@ class App extends React.Component<Props, State> {
|
|||||||
parser.registerTag('spoiler', SpoilerTag);
|
parser.registerTag('spoiler', SpoilerTag);
|
||||||
parser.registerTag('color', ColorTag);
|
parser.registerTag('color', ColorTag);
|
||||||
|
|
||||||
// In future do something normal with this version
|
getVersion().then(res => {
|
||||||
getVersion().then(res => console.debug(`Current back-end version: ${res.version}`))
|
if(version !== res.version) {
|
||||||
|
console.debug(`Current back-end version: ${res.version}. Clearing cache`);
|
||||||
|
|
||||||
|
caches.keys().then(names => {
|
||||||
|
names.forEach(name => caches.delete(name).catch(console.error))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoginModal = () => this.setState({ loginModalVisible: true, registerModalVisible: false });
|
|
||||||
hideLoginModal = () => this.setState({ loginModalVisible: false });
|
|
||||||
|
|
||||||
showRegisterModal = () => this.setState({ registerModalVisible: true, loginModalVisible: false });
|
|
||||||
hideRegisterModal = () => this.setState({ registerModalVisible: false });
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loginModalVisible, registerModalVisible } = this.state;
|
const { modal } = this.props;
|
||||||
|
const notificationWidth = window.screen.width >= 500 ? 500 : window.screen.width;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar showLoginModal={this.showLoginModal} showRegisterModal={this.showRegisterModal}/>
|
<Navbar/>
|
||||||
<div className={'Container'}>
|
<div className={'Container'}>
|
||||||
<div className="Notifications">
|
<div className="Notifications" style={{ width: notificationWidth }}>
|
||||||
{this.props.notifications.map((n, i) => (
|
{this.props.notifications.map((n, i) => (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
{n}
|
{n}
|
||||||
@@ -98,6 +91,7 @@ class App extends React.Component<Props, State> {
|
|||||||
<Route exact path={'/users'} component={Users}/>
|
<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}/>
|
||||||
|
<Route exact path={'/user/:userId/settings'} component={Settings}/>
|
||||||
|
|
||||||
<Route exact path={'/character/:charId'} component={Character}/>
|
<Route exact path={'/character/:charId'} component={Character}/>
|
||||||
|
|
||||||
@@ -111,12 +105,7 @@ class App extends React.Component<Props, State> {
|
|||||||
<Route component={NotFoundError}/>
|
<Route component={NotFoundError}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
{loginModalVisible && (
|
{modal && modal}
|
||||||
<LoginModal hideModal={this.hideLoginModal} openRegisterModal={this.showRegisterModal}/>
|
|
||||||
)}
|
|
||||||
{registerModalVisible && (
|
|
||||||
<RegisterModal hideModal={this.hideRegisterModal}/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
</>
|
</>
|
||||||
@@ -126,7 +115,8 @@ class App extends React.Component<Props, State> {
|
|||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state: StoreState) => ({
|
(state: StoreState) => ({
|
||||||
notifications: state.notifications
|
notifications: state.notifications,
|
||||||
|
modal: state.modal
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
setUser: (userData: UserType) => {
|
setUser: (userData: UserType) => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../Constants'
|
@import '../../Constants'
|
||||||
|
|
||||||
.Character
|
.Character
|
||||||
.Character__Main
|
.Character__Main
|
||||||
@@ -3,8 +3,8 @@ import {fetchChar, fetchCharNovels} from "../../api/characters";
|
|||||||
import './Character.sass'
|
import './Character.sass'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import parser from 'bbcode-to-react';
|
import parser from 'bbcode-to-react';
|
||||||
import NovelItem from "../NovelItem/NovelItem";
|
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/Loading";
|
||||||
import NotFoundError from "../NotFoundError";
|
import NotFoundError from "../NotFoundError";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
@import '../Constants'
|
@import '../../Constants'
|
||||||
|
|
||||||
.Comments
|
.Comments
|
||||||
margin-top: 1rem
|
margin-top: 1rem
|
||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
|
|
||||||
form
|
form
|
||||||
width: 100%
|
width: 100%
|
||||||
margin-bottom: .5rem
|
margin-bottom: .5rem
|
||||||
|
|
||||||
.Comments_BBCodes
|
.Comments_BBCodes
|
||||||
font-size: 1.3rem
|
font-size: 1.3rem
|
||||||
margin-bottom: .2rem
|
margin-bottom: .2rem
|
||||||
|
|
||||||
svg
|
svg
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
svg:first-child
|
svg:first-child
|
||||||
padding-right: .5rem
|
padding-right: .5rem
|
||||||
|
|
||||||
svg:last-child
|
svg:last-child
|
||||||
padding-left: .5rem
|
padding-left: .5rem
|
||||||
|
|
||||||
svg:not(:first-child):not(:last-child)
|
svg:not(:first-child):not(:last-child)
|
||||||
padding: 0 .5rem
|
padding: 0 .5rem
|
||||||
|
|
||||||
.Comments_TextArea
|
.Comments_TextArea
|
||||||
width: 100%
|
width: 100%
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
padding: .25rem
|
padding: .25rem
|
||||||
border: none
|
border: none
|
||||||
border-radius: .2rem
|
border-radius: .2rem
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
min-height: 4rem
|
min-height: 4rem
|
||||||
resize: none
|
resize: none
|
||||||
margin-bottom: .5rem
|
margin-bottom: .5rem
|
||||||
|
|
||||||
.Comments_Submit
|
.Comments_Submit
|
||||||
background-color: var(--block-color) !important
|
background-color: var(--block-color) !important
|
||||||
border-radius: .2rem
|
border-radius: .2rem
|
||||||
border: none
|
border: none
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
@@ -1,132 +1,133 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faBold, faExclamationTriangle, faItalic, faStrikethrough, faUnderline, faTint } from "@fortawesome/free-solid-svg-icons";
|
import { faBold, faExclamationTriangle, faItalic, faStrikethrough, faUnderline, faTint } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Comment from './Comment';
|
import Comment from '../../ui/Comment/Comment';
|
||||||
import './Comments.sass';
|
import './Comments.sass';
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/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 NotificationMessage from "../Notifications/NotificationMessage";
|
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
||||||
|
import Button from "../../ui/Button/Button";
|
||||||
type Props = {
|
|
||||||
comments: CommentType[]
|
type Props = {
|
||||||
hasMore: boolean
|
comments: CommentType[]
|
||||||
user: UserType
|
hasMore: boolean
|
||||||
loadMore: () => void
|
user: UserType
|
||||||
sendComment: (text: string) => void
|
loadMore: () => void
|
||||||
addNotification: (notification: React.ReactNode) => void
|
sendComment: (text: string) => void
|
||||||
};
|
addNotification: (notification: React.ReactNode) => void
|
||||||
|
};
|
||||||
type State = {
|
|
||||||
commentText: string
|
type State = {
|
||||||
users: Map<number, UserType>
|
commentText: string
|
||||||
};
|
users: Map<number, UserType>
|
||||||
|
};
|
||||||
class Comments extends React.Component<Props, State> {
|
|
||||||
input: HTMLTextAreaElement | null = null;
|
class Comments extends React.Component<Props, State> {
|
||||||
state: State = {
|
input: HTMLTextAreaElement | null = null;
|
||||||
commentText: '', users: new Map()
|
state: State = {
|
||||||
};
|
commentText: '', users: new Map()
|
||||||
|
};
|
||||||
addTagToComment = (event: React.MouseEvent, tag: string, defaultValue?: string) => {
|
|
||||||
event.preventDefault();
|
addTagToComment = (event: React.MouseEvent, tag: string, defaultValue?: string) => {
|
||||||
if(!this.input) {
|
event.preventDefault();
|
||||||
console.error('Input not initialized!');
|
if(!this.input) {
|
||||||
return;
|
console.error('Input not initialized!');
|
||||||
}
|
return;
|
||||||
let { commentText } = this.state;
|
}
|
||||||
const [start, end] = [this.input.selectionStart, this.input.selectionEnd];
|
let { commentText } = this.state;
|
||||||
|
const [start, end] = [this.input.selectionStart, this.input.selectionEnd];
|
||||||
const openTag = `${tag}${defaultValue ? `=${defaultValue}` : ''}`;
|
|
||||||
this.setState({ commentText: `${commentText.slice(0, start)}[${openTag}]${commentText.slice(start, end)}[/${tag}]${commentText.slice(end, commentText.length)}` });
|
const openTag = `${tag}${defaultValue ? `=${defaultValue}` : ''}`;
|
||||||
this.input.focus();
|
this.setState({ commentText: `${commentText.slice(0, start)}[${openTag}]${commentText.slice(start, end)}[/${tag}]${commentText.slice(end, commentText.length)}` });
|
||||||
};
|
this.input.focus();
|
||||||
|
};
|
||||||
sendComment = (event: React.FormEvent) => {
|
|
||||||
event.preventDefault();
|
sendComment = (event: React.FormEvent) => {
|
||||||
const { sendComment, addNotification } = this.props;
|
event.preventDefault();
|
||||||
const { commentText } = this.state;
|
const { sendComment, addNotification } = this.props;
|
||||||
|
const { commentText } = this.state;
|
||||||
if(commentText === '') {
|
|
||||||
const notification = (
|
if(commentText === '') {
|
||||||
<NotificationMessage level={"error"}>
|
const notification = (
|
||||||
Пустой комментарий
|
<NotificationMessage level={"error"}>
|
||||||
</NotificationMessage>
|
Пустой комментарий
|
||||||
);
|
</NotificationMessage>
|
||||||
addNotification(notification);
|
);
|
||||||
} else {
|
addNotification(notification);
|
||||||
sendComment(commentText);
|
} else {
|
||||||
this.setState({ commentText: '' });
|
sendComment(commentText);
|
||||||
}
|
this.setState({ commentText: '' });
|
||||||
};
|
}
|
||||||
|
};
|
||||||
componentDidUpdate(prevProps: Readonly<Props>) {
|
|
||||||
const { comments } = this.props;
|
componentDidUpdate(prevProps: Readonly<Props>) {
|
||||||
const { comments: prevComments } = prevProps;
|
const { comments } = this.props;
|
||||||
|
const { comments: prevComments } = prevProps;
|
||||||
if(prevComments.length !== comments.length) {
|
|
||||||
let { users } = this.state;
|
if(prevComments.length !== comments.length) {
|
||||||
|
let { users } = this.state;
|
||||||
comments.forEach(async comment => {
|
|
||||||
if(!users.has(comment.user_id)) {
|
comments.forEach(async comment => {
|
||||||
const user = await fetchUser(comment.user_id);
|
if(!users.has(comment.user_id)) {
|
||||||
users = users.set(comment.user_id, user);
|
const user = await fetchUser(comment.user_id);
|
||||||
this.setState({ users });
|
users = users.set(comment.user_id, user);
|
||||||
}
|
this.setState({ users });
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
render() {
|
|
||||||
const { user, comments, hasMore, loadMore } = this.props;
|
render() {
|
||||||
const { users } = this.state;
|
const { user, comments, hasMore, loadMore } = this.props;
|
||||||
if(!user) return <Loading/>;
|
const { users, commentText } = this.state;
|
||||||
|
if(!user) return <Loading/>;
|
||||||
return (
|
|
||||||
<div className="Comments">
|
return (
|
||||||
{user.authorized &&
|
<div className="Comments">
|
||||||
<form onSubmit={this.sendComment}>
|
{user.authorized &&
|
||||||
<div className="Comments_BBCodes">
|
<form onSubmit={this.sendComment}>
|
||||||
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'b')} icon={faBold}/>
|
<div className="Comments_BBCodes">
|
||||||
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'i')} icon={faItalic}/>
|
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'b')} icon={faBold}/>
|
||||||
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'u')} icon={faUnderline}/>
|
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'i')} icon={faItalic}/>
|
||||||
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 's')} icon={faStrikethrough}/>
|
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'u')} icon={faUnderline}/>
|
||||||
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'spoiler')} icon={faExclamationTriangle}/>
|
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 's')} icon={faStrikethrough}/>
|
||||||
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'color', 'white')} icon={faTint}/>
|
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'spoiler')} icon={faExclamationTriangle}/>
|
||||||
</div>
|
<FontAwesomeIcon onClick={event => this.addTagToComment(event, 'color', 'white')} icon={faTint}/>
|
||||||
<textarea
|
</div>
|
||||||
className={'Comments_TextArea'}
|
<textarea
|
||||||
placeholder={'Текст комментария'}
|
className={'Comments_TextArea'}
|
||||||
value={this.state.commentText}
|
placeholder={'Текст комментария'}
|
||||||
onChange={(event) => this.setState({ commentText: event.target.value })}
|
value={commentText}
|
||||||
ref={ref => this.input = ref}
|
onChange={(event) => this.setState({ commentText: event.target.value })}
|
||||||
/>
|
ref={ref => this.input = ref}
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
/>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<button type="submit" className={'Comments_Submit'}>Написать</button>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<div style={{ marginLeft: '.5rem', color: '#bb3333' }}>Комментарии в разработке!</div>
|
<Button type="submit" className={'Comments_Submit'}>Написать</Button>
|
||||||
</div>
|
<div style={{ marginLeft: '.5rem', color: '#bb3333' }}>Комментарии в разработке!</div>
|
||||||
{hasMore && <button type={"button"} onClick={loadMore}>Загрузить ещё</button>}
|
</div>
|
||||||
</div>
|
{hasMore && <Button onClick={loadMore}>Загрузить ещё</Button>}
|
||||||
</form>}
|
</div>
|
||||||
{!comments && (
|
</form>}
|
||||||
<Loading/>
|
{!comments && (
|
||||||
)}
|
<Loading/>
|
||||||
{comments && comments.length > 0 ? (
|
)}
|
||||||
comments.map(comment => <Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)} />)
|
{comments && comments.length > 0 ? (
|
||||||
) : (
|
comments.map(comment => <Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)} />)
|
||||||
'Никто не оставил комментарий к этой новелле... :c'
|
) : (
|
||||||
)}
|
'Никто не оставил комментарий к этой новелле... :c'
|
||||||
</div>
|
)}
|
||||||
)
|
</div>
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export default connect(null,
|
|
||||||
dispatch => ({
|
export default connect(null,
|
||||||
addNotification: (notification: React.ReactNode) => {
|
dispatch => ({
|
||||||
dispatch(addNotification(notification))
|
addNotification: (notification: React.ReactNode) => {
|
||||||
}
|
dispatch(addNotification(notification))
|
||||||
})
|
}
|
||||||
|
})
|
||||||
)(Comments);
|
)(Comments);
|
||||||
@@ -1,59 +1,59 @@
|
|||||||
.Footer
|
.Footer
|
||||||
background: var(--nav-bg-color)
|
background: var(--nav-bg-color)
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
position: relative
|
position: relative
|
||||||
bottom: 0
|
bottom: 0
|
||||||
padding: 1rem
|
padding: 1rem
|
||||||
|
|
||||||
.Footer__Tabs
|
.Footer__Tabs
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
@media (max-width: 600px)
|
@media (max-width: 600px)
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
.Footer__Tab
|
.Footer__Tab
|
||||||
font-weight: 300
|
font-weight: 300
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
margin-right: 4rem
|
margin-right: 4rem
|
||||||
|
|
||||||
.Footer__Tab_Title
|
.Footer__Tab_Title
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
font-weight: 400
|
font-weight: 400
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1rem
|
||||||
|
|
||||||
.Link
|
.Link
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1rem
|
||||||
|
|
||||||
.Links
|
.Links
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
.Link
|
.Link
|
||||||
height: 46px
|
height: 46px
|
||||||
width: 46px
|
width: 46px
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
margin-right: 1rem
|
margin-right: 1rem
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
|
|
||||||
svg
|
svg
|
||||||
position: relative
|
position: relative
|
||||||
top: 14px
|
top: 14px
|
||||||
left: 14px
|
left: 14px
|
||||||
width: 18px
|
width: 18px
|
||||||
height: 18px
|
height: 18px
|
||||||
|
|
||||||
a
|
a
|
||||||
color: var(--sec-color)
|
color: var(--sec-color)
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
|
|
||||||
.Footer__Bottom
|
.Footer__Bottom
|
||||||
border-top: 1px solid var(--main-color)
|
border-top: 1px solid var(--main-color)
|
||||||
padding: 2rem 0
|
padding: 2rem 0
|
||||||
text-align: center
|
text-align: center
|
||||||
@@ -1,56 +1,56 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './Footer.sass';
|
import './Footer.sass';
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faDiscord, faVk, faTelegramPlane } from '@fortawesome/free-brands-svg-icons';
|
import { faDiscord, faVk, faTelegramPlane } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
const secretChangeTheme = () => {
|
const secretChangeTheme = () => {
|
||||||
const theme = localStorage.getItem('theme');
|
const theme = localStorage.getItem('theme');
|
||||||
if(theme == null) {
|
if(theme == null) {
|
||||||
localStorage.setItem('theme', 'light');
|
localStorage.setItem('theme', 'light');
|
||||||
document.body.setAttribute('theme', 'light')
|
document.body.setAttribute('theme', 'light')
|
||||||
} else if(theme === 'light') {
|
} else if(theme === 'light') {
|
||||||
localStorage.setItem('theme', 'dark');
|
localStorage.setItem('theme', 'dark');
|
||||||
document.body.setAttribute('theme', 'dark')
|
document.body.setAttribute('theme', 'dark')
|
||||||
} else if(theme === 'dark') {
|
} else if(theme === 'dark') {
|
||||||
localStorage.setItem('theme', 'light');
|
localStorage.setItem('theme', 'light');
|
||||||
document.body.setAttribute('theme', 'light')
|
document.body.setAttribute('theme', 'light')
|
||||||
}
|
}
|
||||||
if(theme !== 'light') {
|
if(theme !== 'light') {
|
||||||
console.log('Светлая тема находится в разработке! Не все может корректно отображаться.\nК тому же, эта кнопка секретная. Ты знал, на что шел ;)');
|
console.log('Светлая тема находится в разработке! Не все может корректно отображаться.\nК тому же, эта кнопка секретная. Ты знал, на что шел ;)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='Footer'>
|
<div className='Footer'>
|
||||||
<footer className='Footer__Tabs'>
|
<footer className='Footer__Tabs'>
|
||||||
<div className="Footer__Tab">
|
<div className="Footer__Tab">
|
||||||
<div className="Footer__Tab_Title" onClick={secretChangeTheme}>Команда</div>
|
<div className="Footer__Tab_Title" onClick={secretChangeTheme}>Команда</div>
|
||||||
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="Footer__Tab">
|
<div className="Footer__Tab">
|
||||||
<div className="Footer__Tab_Title">Соцсети</div>
|
<div className="Footer__Tab_Title">Соцсети</div>
|
||||||
|
|
||||||
<div className="Links">
|
<div className="Links">
|
||||||
<a href="https://vk.com/unmei_site">
|
<a href="https://vk.com/unmei_site">
|
||||||
<div className="Link"><FontAwesomeIcon icon={faVk}/></div>
|
<div className="Link"><FontAwesomeIcon icon={faVk}/></div>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/GMMymRU">
|
<a href="https://discord.gg/4CA8Cju">
|
||||||
<div className="Link"><FontAwesomeIcon icon={faDiscord}/></div>
|
<div className="Link"><FontAwesomeIcon icon={faDiscord}/></div>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://t.me/unmei_site">
|
<a href="https://t.me/unmei_site">
|
||||||
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<div className="Footer__Bottom">
|
<div className="Footer__Bottom">
|
||||||
Unmei © 2019-2020
|
Unmei © 2019-2020
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.News
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
&__Title
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
justify-content: space-between
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 600
|
||||||
|
margin-bottom: .5rem
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {fetchNews} from "../../api/news";
|
||||||
|
import Loading from "../../ui/Loading";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import NewsPost from "../../ui/NewsPost/NewsPost";
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
news: PostType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class News extends React.Component<{}, State> {
|
||||||
|
state: State = {
|
||||||
|
news: []
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
fetchNews().then((news: PostType[]) => {
|
||||||
|
news.forEach(p => p.date = new Date(p.date));
|
||||||
|
this.setState({ news })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { news } = this.state;
|
||||||
|
|
||||||
|
if(news.length === 0) return <Loading/>;
|
||||||
|
return (
|
||||||
|
<div className="News">
|
||||||
|
<div className="News__Title">
|
||||||
|
<div>Новости</div>
|
||||||
|
<Link to={'/news'}>Все новости</Link>
|
||||||
|
</div>
|
||||||
|
{news.slice(0, 3).map(post => (
|
||||||
|
<Link key={post.id} to={`/news/${post.id}`}>
|
||||||
|
<NewsPost {...post} />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default News;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../Constants'
|
@import '../../Constants'
|
||||||
|
|
||||||
.LoginModal
|
.LoginModal
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
@@ -1,19 +1,22 @@
|
|||||||
import React, {FormEvent, ChangeEvent} from "react";
|
import React, {FormEvent, ChangeEvent} from "react";
|
||||||
import Modal from "../Modal/Modal";
|
import Modal from "../../ui/Modal/Modal";
|
||||||
import {login} from "../../api/users";
|
import {login} from "../../api/users";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {addNotification, setUser} from "../../store/actions";
|
import {addNotification, hideModal, setModal, 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 NotificationMessage from "../Notifications/NotificationMessage";
|
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
||||||
import Recaptcha from '../Recaptcha/Recaptcha'
|
import Recaptcha from '../../ui/Recaptcha/Recaptcha'
|
||||||
|
import Button from "../../ui/Button/Button";
|
||||||
|
import RegisterModal from "./RegisterModal";
|
||||||
|
import Input from "../../ui/Input/Input";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setUser: (user: UserType) => void
|
setUser: (user: UserType) => void
|
||||||
hideModal: () => void
|
|
||||||
openRegisterModal: () => void
|
|
||||||
addNotification: (notification: React.ReactNode) => void
|
addNotification: (notification: React.ReactNode) => void
|
||||||
|
setModal: (modal: React.ReactNode | null) => void
|
||||||
|
hideModal: () => void
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -31,8 +34,9 @@ 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, addNotification, setModal } = this.props;
|
||||||
const { login: log, password, recaptcha } = this.state;
|
const { login: log, password, recaptcha } = this.state;
|
||||||
|
|
||||||
login(log, password, recaptcha).then(user => {
|
login(log, password, recaptcha).then(user => {
|
||||||
if(!user) return;
|
if(!user) return;
|
||||||
|
|
||||||
@@ -43,7 +47,7 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
</NotificationMessage>
|
</NotificationMessage>
|
||||||
);
|
);
|
||||||
addNotification(successful);
|
addNotification(successful);
|
||||||
hideModal();
|
setModal(null);
|
||||||
})
|
})
|
||||||
.catch((r: ApiError) => {
|
.catch((r: ApiError) => {
|
||||||
this.setState({ error: errors[r.code] });
|
this.setState({ error: errors[r.code] });
|
||||||
@@ -56,12 +60,13 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { login, password, error, recaptchaNeeded } = this.state;
|
const { login, password, error, recaptchaNeeded } = this.state;
|
||||||
|
const { hideModal, setModal } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onCloseRequest={this.props.hideModal} className={'LoginModal'}>
|
<Modal className={'LoginModal'} title={'Авторизация'}>
|
||||||
<h1>Авторизация</h1>
|
|
||||||
{error && <div className="LoginModal__Error">{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"
|
||||||
name={'login'}
|
name={'login'}
|
||||||
placeholder={'Логин'}
|
placeholder={'Логин'}
|
||||||
@@ -69,7 +74,7 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
onChange={this.handleLoginChange}
|
onChange={this.handleLoginChange}
|
||||||
value={login}
|
value={login}
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name={'password'}
|
name={'password'}
|
||||||
placeholder={'Пароль'}
|
placeholder={'Пароль'}
|
||||||
@@ -83,12 +88,12 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
theme={'dark'}
|
theme={'dark'}
|
||||||
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
|
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
|
||||||
/>}
|
/>}
|
||||||
<button className={'LoginModal__Submit'} type={"submit"}>Войти</button>
|
<Button className={'LoginModal__Submit'}>Войти</Button>
|
||||||
</form>
|
</form>
|
||||||
<div className="LoginModal__Links">
|
<div className="LoginModal__Links">
|
||||||
<Link to={'/restore'}>Забыли пароль?</Link>
|
<Link to={'/restore'} onClick={hideModal}>Забыли пароль?</Link>
|
||||||
<p>|</p>
|
<p>|</p>
|
||||||
<Link to={''} onClick={this.props.openRegisterModal}>Регистрация</Link>
|
<Link to={''} onClick={() => setModal(<RegisterModal/>)}>Регистрация</Link>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
@@ -98,6 +103,8 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
export default connect(null,
|
export default connect(null,
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
setUser: (user: UserType) => dispatch(setUser(user)),
|
setUser: (user: UserType) => dispatch(setUser(user)),
|
||||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
|
||||||
|
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||||
|
hideModal: () => dispatch(hideModal())
|
||||||
})
|
})
|
||||||
)(LoginModal);
|
)(LoginModal);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../Constants'
|
@import '../../Constants'
|
||||||
|
|
||||||
.RegisterModal
|
.RegisterModal
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Modal from "../Modal/Modal";
|
import Modal from "../../ui/Modal/Modal";
|
||||||
import Recaptcha from '../Recaptcha/Recaptcha'
|
import Recaptcha from '../../ui/Recaptcha/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";
|
||||||
import {addNotification} from "../../store/actions";
|
import {addNotification, setModal} from "../../store/actions";
|
||||||
import NotificationMessage from "../Notifications/NotificationMessage";
|
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
||||||
|
import Button from "../../ui/Button/Button";
|
||||||
|
import Input from "../../ui/Input/Input";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
addNotification: (notification: React.ReactNode) => void
|
addNotification: (notification: React.ReactNode) => void
|
||||||
hideModal: () => void
|
setModal: (modal: React.ReactNode | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -55,23 +57,21 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
Проверьте почту <strong>{email}</strong>. На неё была отправлена ссылка для подтверждения аккаунта.
|
Проверьте почту <strong>{email}</strong>. На неё была отправлена ссылка для подтверждения аккаунта.
|
||||||
</NotificationMessage>
|
</NotificationMessage>
|
||||||
);
|
);
|
||||||
const { addNotification, hideModal } = this.props;
|
const { addNotification, setModal } = this.props;
|
||||||
addNotification(notification);
|
addNotification(notification);
|
||||||
hideModal();
|
setModal(null);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { hideModal } = this.props;
|
|
||||||
const { error, email, login, password1, password2 } = this.state;
|
const { error, email, login, password1, password2 } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onCloseRequest={hideModal} className={'RegisterModal'}>
|
<Modal className={'RegisterModal'} title={'Регистрация'}>
|
||||||
<h1>Регистрация</h1>
|
|
||||||
{error && <div className="RegisterModal__Error" dangerouslySetInnerHTML={{ __html: error }}/>}
|
{error && <div className="RegisterModal__Error" dangerouslySetInnerHTML={{ __html: error }}/>}
|
||||||
<form className="RegisterModal__Form" onSubmit={this.submitForm}>
|
<form className="RegisterModal__Form" onSubmit={this.submitForm}>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
min={4}
|
min={4}
|
||||||
max={20}
|
max={20}
|
||||||
@@ -81,7 +81,7 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
className={'RegisterModal__Field'}
|
className={'RegisterModal__Field'}
|
||||||
onChange={this.handleLoginChange}
|
onChange={this.handleLoginChange}
|
||||||
value={login}/>
|
value={login}/>
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
name={'email'}
|
name={'email'}
|
||||||
placeholder={'E-mail'}
|
placeholder={'E-mail'}
|
||||||
@@ -89,14 +89,14 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
className={'RegisterModal__Field'}
|
className={'RegisterModal__Field'}
|
||||||
onChange={this.handleEmailChange}
|
onChange={this.handleEmailChange}
|
||||||
value={email}/>
|
value={email}/>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name={'password1'}
|
name={'password1'}
|
||||||
placeholder={'Пароль'}
|
placeholder={'Пароль'}
|
||||||
className={'RegisterModal__Field'}
|
className={'RegisterModal__Field'}
|
||||||
onChange={this.handlePassword1Change}
|
onChange={this.handlePassword1Change}
|
||||||
value={password1}/>
|
value={password1}/>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
name={'password2'}
|
name={'password2'}
|
||||||
placeholder={'Повтор пароля'}
|
placeholder={'Повтор пароля'}
|
||||||
@@ -110,7 +110,7 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
theme={'dark'}
|
theme={'dark'}
|
||||||
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}/>
|
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}/>
|
||||||
|
|
||||||
<button className={'RegisterModal__Submit'} type={"submit"}>Регистрация</button>
|
<Button className={'RegisterModal__Submit'}>Регистрация</Button>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
@@ -119,6 +119,7 @@ class RegisterModal extends React.Component<Props, State> {
|
|||||||
|
|
||||||
export default connect(null,
|
export default connect(null,
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
|
||||||
|
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal))
|
||||||
})
|
})
|
||||||
)(RegisterModal);
|
)(RegisterModal);
|
||||||
@@ -75,8 +75,8 @@ $hoverColor: #f22
|
|||||||
left: 0
|
left: 0
|
||||||
height: 0
|
height: 0
|
||||||
background: $hoverColor
|
background: $hoverColor
|
||||||
transition: heigth .1s ease-in
|
transition: heigth .1s linear
|
||||||
-webkit-transition: height .1s ease-in
|
-webkit-transition: height .1s linear
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: var(--main-bg-color)
|
background-color: var(--main-bg-color)
|
||||||
@@ -3,25 +3,27 @@ import './Navbar.sass'
|
|||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {userLogout} from "../../api/users";
|
import {userLogout} from "../../api/users";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {logout} from "../../store/actions";
|
import {logout, setModal} from "../../store/actions";
|
||||||
import {hasAccessToAdminPanel} from "../../utils";
|
import {hasAccessToAdminPanel} from "../../utils";
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
import {faBars} from '@fortawesome/free-solid-svg-icons';
|
import {faBars} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import RegisterModal from "../Modals/RegisterModal";
|
||||||
|
import LoginModal from "../Modals/LoginModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
showRegisterModal: () => void
|
setModal: SetModalType
|
||||||
showLoginModal: () => void
|
|
||||||
logout: () => void
|
logout: () => void
|
||||||
currentUser: UserType
|
currentUser: UserType
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
expand: boolean
|
expand: boolean
|
||||||
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
class Navbar extends React.Component<Props, State> {
|
class Navbar extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
expand: false
|
expand: false, width: window.screen.width
|
||||||
}
|
}
|
||||||
|
|
||||||
logout = () => {
|
logout = () => {
|
||||||
@@ -34,8 +36,15 @@ class Navbar extends React.Component<Props, State> {
|
|||||||
this.setState({ expand: !this.state.expand })
|
this.setState({ expand: !this.state.expand })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
if(this.state.width !== window.screen.width)
|
||||||
|
this.setState({ width: window.screen.width });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentUser, showRegisterModal, showLoginModal } = this.props;
|
const { currentUser, setModal } = this.props;
|
||||||
const { expand } = this.state;
|
const { expand } = this.state;
|
||||||
|
|
||||||
const isMinimized = window.screen.width <= 1000;
|
const isMinimized = window.screen.width <= 1000;
|
||||||
@@ -68,25 +77,22 @@ class Navbar extends React.Component<Props, State> {
|
|||||||
)}
|
)}
|
||||||
{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}?s=40&t=${new Date().getTime()})` }}/>
|
||||||
<div>{this.props.currentUser.username}</div>
|
<div>{this.props.currentUser.username}</div>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
{currentUser.group && hasAccessToAdminPanel(currentUser) && (
|
||||||
{currentUser.group && hasAccessToAdminPanel(currentUser) && (
|
<Link to={'/kawaii__neko'} className={'Header_Button'}>
|
||||||
<Link to={'/kawaii__neko'} className={'Header_Button'}>
|
Админ-панель
|
||||||
Админ-панель
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
)}
|
<Link className="Header_Button" to={`/user/${currentUser.id}/settings`}>Настройки</Link>
|
||||||
{!currentUser.authorized && (
|
|
||||||
<div className={'Header_Button'} onClick={showRegisterModal}>Регистрация</div>
|
|
||||||
)}
|
|
||||||
{!currentUser.authorized ? (
|
|
||||||
<div className={'Header_Button'} onClick={showLoginModal}>Войти</div>
|
|
||||||
) : (
|
|
||||||
<div className={'Header_Button'} onClick={this.logout}>Выйти</div>
|
<div className={'Header_Button'} onClick={this.logout}>Выйти</div>
|
||||||
)}
|
</>) : (<>
|
||||||
|
<div className={'Header_Button'} onClick={() => setModal(<RegisterModal/>)}>Регистрация</div>
|
||||||
|
<div className={'Header_Button'} onClick={() => setModal(<LoginModal/>)}>Войти</div>
|
||||||
|
</>)}
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -95,9 +101,11 @@ class Navbar extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state: any) => ({
|
(state: StoreState) => ({
|
||||||
currentUser: state.currentUser
|
currentUser: state.currentUser
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
logout: () => dispatch(logout())
|
logout: () => dispatch(logout()),
|
||||||
}))(Navbar);
|
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal))
|
||||||
|
})
|
||||||
|
)(Navbar);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/Loading";
|
||||||
import {getNews} from "../../api/news";
|
import {fetchNews} from "../../api/news";
|
||||||
|
import NewsPost from "../../ui/NewsPost/NewsPost";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -14,7 +15,7 @@ class AllNews extends React.Component<{}, State> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = 'Новости';
|
document.title = 'Новости';
|
||||||
getNews().then((news: PostType[]) => {
|
fetchNews().then((news: PostType[]) => {
|
||||||
news.forEach(p => p.date = new Date(p.date));
|
news.forEach(p => p.date = new Date(p.date));
|
||||||
this.setState({ news })
|
this.setState({ news })
|
||||||
});
|
});
|
||||||
@@ -30,16 +31,9 @@ class AllNews extends React.Component<{}, State> {
|
|||||||
Новости
|
Новости
|
||||||
</div>
|
</div>
|
||||||
{news.map(post => (
|
{news.map(post => (
|
||||||
<div className={'Post'} key={post.id}>
|
<Link key={post.id} to={`/post/${post.id}`}>
|
||||||
<div className="Post__Title">{post.title}</div>
|
<NewsPost {...post} />
|
||||||
<div className="Post__Content">{post.short_post}</div>
|
</Link>
|
||||||
<div className="Post__Footer">
|
|
||||||
<div>
|
|
||||||
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
|
|
||||||
</div>
|
|
||||||
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
.Post
|
.Post
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
|
|
||||||
&__Title
|
&__Title
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|
||||||
&__Content
|
&__Content
|
||||||
border-top: var(--link-color) 1px solid
|
border-top: var(--link-color) 1px solid
|
||||||
border-bottom: var(--link-color) 1px solid
|
border-bottom: var(--link-color) 1px solid
|
||||||
padding: .5rem 0
|
padding: .5rem 0
|
||||||
margin: .5rem 0
|
margin: .5rem 0
|
||||||
@@ -1,51 +1,55 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {getPost} from "../../api/news";
|
import {fetchPost} from "../../api/news";
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/Loading";
|
||||||
import NotFoundError from "../NotFoundError";
|
import NotFoundError from "../NotFoundError";
|
||||||
import './Post.sass';
|
import './Post.sass'
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
|
// @ts-ignore
|
||||||
type Props = {
|
import parser from 'bbcode-to-react';
|
||||||
match: { params: { postId: number } }
|
|
||||||
};
|
type Props = {
|
||||||
|
match: { params: { postId: number } }
|
||||||
type State = {
|
};
|
||||||
post: PostType | null
|
|
||||||
code: number
|
type State = {
|
||||||
};
|
post: PostType | null
|
||||||
|
code: number
|
||||||
class Post extends React.Component<Props, State> {
|
};
|
||||||
state: State = {
|
|
||||||
post: null, code: 0
|
class Post extends React.Component<Props, State> {
|
||||||
};
|
state: State = {
|
||||||
|
post: null, code: 0
|
||||||
componentDidMount(): void {
|
};
|
||||||
const { match: { params: { postId }} } = this.props;
|
|
||||||
|
componentDidMount(): void {
|
||||||
getPost(postId).then((post: PostType) => {
|
const { match: { params: { postId }} } = this.props;
|
||||||
this.setState({ post })
|
|
||||||
}).catch((error: ApiError) => {
|
fetchPost(postId).then((post: PostType) => {
|
||||||
this.setState({ code: error.code })
|
post.date = new Date(post.date);
|
||||||
});
|
this.setState({ post });
|
||||||
}
|
}).catch((error: ApiError) => {
|
||||||
|
this.setState({ code: error.code })
|
||||||
render() {
|
});
|
||||||
const { post, code } = this.state;
|
}
|
||||||
|
|
||||||
if(code === 100) return <NotFoundError/>;
|
render() {
|
||||||
if(!post) return <Loading/>;
|
const { post, code } = this.state;
|
||||||
return (
|
|
||||||
<div className={'Post'}>
|
if(code === 100) return <NotFoundError/>;
|
||||||
<div className={'Post__Title'}>{post.title}</div>
|
if(!post) return <Loading/>;
|
||||||
<div className="Post__Content">
|
return (
|
||||||
{post.full_post || post.short_post}
|
<div className={'Post'}>
|
||||||
</div>
|
<div className="Post__Title">{post.title}</div>
|
||||||
<div className="Post__Footer">
|
<pre className='Post__Content'>{parser.toReact(post.short_post)}</pre>
|
||||||
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
|
<div className="Post__Footer">
|
||||||
</div>
|
<div>
|
||||||
</div>
|
Автор: <Link to={`/user/${post.author_id}`}>{post.author}</Link>
|
||||||
)
|
</div>
|
||||||
}
|
<div>Дата: {post.date.toLocaleDateString()}, {post.date.toLocaleTimeString()}</div>
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Post;
|
export default Post;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
.Novel
|
.Novel
|
||||||
.Novel__Main
|
.Novel__Main
|
||||||
@@ -12,9 +12,9 @@ import {
|
|||||||
fetchNovelGenres, postNovelComment, createUserNovel
|
fetchNovelGenres, postNovelComment, createUserNovel
|
||||||
} from "../../api/novels";
|
} from "../../api/novels";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/Loading";
|
||||||
import { TranslateStatus } from "../../api/api";
|
import { TranslateStatus } from "../../api/api";
|
||||||
import Comments from "../Comment/Comments";
|
import Comments from "../Comments/Comments";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentUser: UserType
|
currentUser: UserType
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
.Novels
|
.Novels
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import './Novels.sass'
|
import './Novels.sass'
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {fetchNovels} from "../../api/novels";
|
import {fetchNovels} from "../../api/novels";
|
||||||
import NovelItem from '../NovelItem/NovelItem';
|
import NovelItem from '../../ui/NovelItem/NovelItem';
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/Loading";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
novels: NovelType[] | null
|
novels: NovelType[] | null
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.PasswordRestore
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
.Button, input
|
||||||
|
font-size: 1.2rem
|
||||||
|
|
||||||
|
input
|
||||||
|
width: 100%
|
||||||
|
margin-bottom: .5rem
|
||||||
|
|
||||||
|
.Button
|
||||||
|
align-self: flex-start
|
||||||
|
margin-top: .5rem
|
||||||
@@ -1,78 +1,79 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {restore} from "../../api/auth";
|
import {restore} from "../../api/auth";
|
||||||
import errors from "../../api/errors";
|
import errors from "../../api/errors";
|
||||||
import './PasswordRestore.sass';
|
import './PasswordRestore.sass';
|
||||||
|
import Button from "../../ui/Button/Button";
|
||||||
type Props = {
|
|
||||||
match: { params: { token: string } }
|
type Props = {
|
||||||
}
|
match: { params: { token: string } }
|
||||||
|
}
|
||||||
type State = {
|
|
||||||
p1: string
|
type State = {
|
||||||
p2: string
|
p1: string
|
||||||
error: string
|
p2: string
|
||||||
message: string
|
error: string
|
||||||
}
|
message: string
|
||||||
|
}
|
||||||
class PasswordRestore extends React.Component<Props, State> {
|
|
||||||
state = {
|
class PasswordRestore extends React.Component<Props, State> {
|
||||||
p1: '', p2: '', error: '', message: ''
|
state = {
|
||||||
}
|
p1: '', p2: '', error: '', message: ''
|
||||||
timeout: NodeJS.Timeout | null = null;
|
}
|
||||||
|
timeout: NodeJS.Timeout | null = null;
|
||||||
onSubmit = (event: React.FormEvent) => {
|
|
||||||
const { token } = this.props.match.params;
|
onSubmit = (event: React.FormEvent) => {
|
||||||
const { p1, p2 } = this.state;
|
const { token } = this.props.match.params;
|
||||||
event.preventDefault();
|
const { p1, p2 } = this.state;
|
||||||
if(p1 !== p2) {
|
event.preventDefault();
|
||||||
this.setState({ error: 'Пароли не совпадают!' })
|
if(p1 !== p2) {
|
||||||
return;
|
this.setState({ error: 'Пароли не совпадают!' })
|
||||||
}
|
return;
|
||||||
restore(token, p1).then(res => {
|
}
|
||||||
if(res === 'ok') {
|
restore(token, p1).then(res => {
|
||||||
this.setState({ message: 'Успешно!\nВы будете пренаправлены на главную через 3 секунды!' })
|
if(res === 'ok') {
|
||||||
this.timeout = setTimeout(() => {
|
this.setState({ message: 'Успешно!\nВы будете пренаправлены на главную через 3 секунды!' })
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
}, 3000)
|
|
||||||
}
|
}, 3000)
|
||||||
}).catch((err: ApiError) => {
|
}
|
||||||
const error = errors[err.code];
|
}).catch((err: ApiError) => {
|
||||||
if(error) this.setState({ error });
|
const error = errors[err.code];
|
||||||
else this.setState({ error: `Произошла ошибка! Сообщите номер и текст разработчику!\nКод: ${err.code}\nТекст: ${err.text}` })
|
if(error) this.setState({ error });
|
||||||
})
|
else this.setState({ error: `Произошла ошибка! Сообщите номер и текст разработчику!\nКод: ${err.code}\nТекст: ${err.text}` })
|
||||||
}
|
})
|
||||||
|
}
|
||||||
componentWillUnmount() {
|
|
||||||
if(this.timeout) clearInterval(this.timeout);
|
componentWillUnmount() {
|
||||||
}
|
if(this.timeout) clearInterval(this.timeout);
|
||||||
|
}
|
||||||
render() {
|
|
||||||
const { p1, p2, error, message } = this.state;
|
render() {
|
||||||
|
const { p1, p2, error, message } = this.state;
|
||||||
return (
|
|
||||||
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
|
return (
|
||||||
{error && <pre className="Error">{error}</pre>}
|
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
|
||||||
{message && <pre className="Successful">{message}</pre>}
|
{error && <pre className="Error">{error}</pre>}
|
||||||
<input
|
{message && <pre className="Successful">{message}</pre>}
|
||||||
type="password"
|
<input
|
||||||
placeholder={'Новый пароль'}
|
type="password"
|
||||||
minLength={6}
|
placeholder={'Новый пароль'}
|
||||||
value={p1}
|
minLength={6}
|
||||||
onChange={event => this.setState({ p1: event.target.value })}
|
value={p1}
|
||||||
required
|
onChange={event => this.setState({ p1: event.target.value })}
|
||||||
/>
|
required
|
||||||
<input
|
/>
|
||||||
type="password"
|
<input
|
||||||
placeholder={'Повторите пароль'}
|
type="password"
|
||||||
minLength={6}
|
placeholder={'Повторите пароль'}
|
||||||
value={p2}
|
minLength={6}
|
||||||
onChange={event => this.setState({ p2: event.target.value })}
|
value={p2}
|
||||||
required
|
onChange={event => this.setState({ p2: event.target.value })}
|
||||||
/>
|
required
|
||||||
<button>Сменить</button>
|
/>
|
||||||
</form>
|
<Button>Сменить</Button>
|
||||||
)
|
</form>
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default PasswordRestore;
|
export default PasswordRestore;
|
||||||
@@ -1,52 +1,53 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// import Recaptcha from "react-recaptcha";
|
// import Recaptcha from "react-recaptcha";
|
||||||
import Recaptcha from '../Recaptcha/Recaptcha';
|
import Recaptcha from '../../ui/Recaptcha/Recaptcha';
|
||||||
import './PasswordRestore.sass';
|
import './PasswordRestore.sass';
|
||||||
import {generateRestoreLink} from "../../api/users";
|
import {generateRestoreLink} from "../../api/users";
|
||||||
|
import Button from "../../ui/Button/Button";
|
||||||
type State = {
|
|
||||||
email: string
|
type State = {
|
||||||
recaptcha: string
|
email: string
|
||||||
}
|
recaptcha: string
|
||||||
|
}
|
||||||
class PasswordRestoreGenerate extends React.Component<{}, State> {
|
|
||||||
private recaptcha: any;
|
class PasswordRestoreGenerate extends React.Component<{}, State> {
|
||||||
|
private recaptcha: any;
|
||||||
state: State = {
|
|
||||||
email: '', recaptcha: ''
|
state: State = {
|
||||||
}
|
email: '', recaptcha: ''
|
||||||
|
}
|
||||||
onSubmit = (event: React.FormEvent) => {
|
|
||||||
event.preventDefault();
|
onSubmit = (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
const { email, recaptcha } = this.state;
|
|
||||||
generateRestoreLink(email, recaptcha).then(console.log)
|
const { email, recaptcha } = this.state;
|
||||||
}
|
generateRestoreLink(email, recaptcha).then(console.log)
|
||||||
|
}
|
||||||
componentWillUnmount() {
|
|
||||||
this.recaptcha.reset()
|
componentWillUnmount() {
|
||||||
}
|
this.recaptcha.reset()
|
||||||
|
}
|
||||||
render() {
|
|
||||||
return (
|
render() {
|
||||||
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
|
return (
|
||||||
<input
|
<form onSubmit={this.onSubmit} className={'PasswordRestore'}>
|
||||||
type="email"
|
<input
|
||||||
onChange={event => this.setState({ email: event.target.value })}
|
type="email"
|
||||||
placeholder={'E-mail'}
|
onChange={event => this.setState({ email: event.target.value })}
|
||||||
/>
|
placeholder={'E-mail'}
|
||||||
|
/>
|
||||||
<Recaptcha
|
|
||||||
ref={(e: any) => this.recaptcha = e}
|
<Recaptcha
|
||||||
onVerify={(res: string) => this.setState({ recaptcha: res })}
|
ref={(e: any) => this.recaptcha = e}
|
||||||
theme={'dark'}
|
onVerify={(res: string) => this.setState({ recaptcha: res })}
|
||||||
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
|
theme={'dark'}
|
||||||
/>
|
sitekey={'6LfnDsMUAAAAAEDfD5ubCdFQbNUnKxxJlMWeUMzN'}
|
||||||
<button>Восстановить</button>
|
/>
|
||||||
</form>
|
<Button>Восстановить</Button>
|
||||||
);
|
</form>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default PasswordRestoreGenerate;
|
export default PasswordRestoreGenerate;
|
||||||
@@ -1,47 +1,47 @@
|
|||||||
@import "../../Constants"
|
@import "../../../Constants"
|
||||||
|
|
||||||
.UserNovels
|
.UserNovels
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
|
|
||||||
&__Title
|
&__Title
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
.svg-inline--fa
|
.svg-inline--fa
|
||||||
margin: 0 .5rem
|
margin: 0 .5rem
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color: var(--link-color)
|
color: var(--link-color)
|
||||||
|
|
||||||
&__List
|
&__List
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
&:not(:last-child)
|
&:not(:last-child)
|
||||||
border-bottom: var(--link-color) 1px solid
|
border-bottom: var(--link-color) 1px solid
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1rem
|
||||||
padding-bottom: 1rem
|
padding-bottom: 1rem
|
||||||
|
|
||||||
&_Title
|
&_Title
|
||||||
display: flex
|
display: flex
|
||||||
font-size: 1.5rem
|
font-size: 1.5rem
|
||||||
margin-right: 1rem
|
margin-right: 1rem
|
||||||
|
|
||||||
button
|
button
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|
||||||
&_Novels
|
&_Novels
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: center
|
justify-content: center
|
||||||
|
|
||||||
&.Grid
|
&.Grid
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
|
||||||
&.Table
|
&.Table
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
width: 100%
|
width: 100%
|
||||||
@@ -1,95 +1,95 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './UserNovels.sass';
|
import './UserNovels.sass';
|
||||||
import { fetchUserNovels, fetchUser } from '../../../api/users';
|
import { fetchUserNovels, fetchUser } from '../../../api/users';
|
||||||
import Loading from '../../Loading';
|
import Loading from '../../../ui/Loading';
|
||||||
import NovelItem from '../../NovelItem/NovelItem';
|
import NovelItem from '../../../ui/NovelItem/NovelItem';
|
||||||
import {capitalize, generateClassName, getRandomInt} from "../../../utils";
|
import {capitalize, generateClassName, getRandomInt} from "../../../utils";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons";
|
import {faAlignJustify, faThLarge} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: number } }
|
match: { params: { userId: number } }
|
||||||
history: { push: (path: string) => void }
|
history: { push: (path: string) => void }
|
||||||
};
|
};
|
||||||
type State = {
|
type State = {
|
||||||
novels: NovelType[]
|
novels: NovelType[]
|
||||||
user: UserType | null
|
user: UserType | null
|
||||||
viewType: string
|
viewType: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserNovels extends React.Component<Props, State> {
|
class UserNovels extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
novels: [], user: null, viewType: 'grid'
|
novels: [], user: null, viewType: 'grid'
|
||||||
};
|
};
|
||||||
|
|
||||||
getRandomNovel = () => {
|
getRandomNovel = () => {
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
const { novels } = this.state;
|
const { novels } = this.state;
|
||||||
const planned = novels.filter(n => n.status === 'planned');
|
const planned = novels.filter(n => n.status === 'planned');
|
||||||
const index = getRandomInt(0, planned.length-1);
|
const index = getRandomInt(0, planned.length-1);
|
||||||
history.push(`/novels/${planned[index].id}`);
|
history.push(`/novels/${planned[index].id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match: { params: { userId } } } = this.props;
|
const { match: { params: { userId } } } = this.props;
|
||||||
|
|
||||||
fetchUserNovels(userId)
|
fetchUserNovels(userId)
|
||||||
.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');
|
const viewType = localStorage.getItem('viewType');
|
||||||
if(viewType) {
|
if(viewType) {
|
||||||
if(viewType !== 'grid' && viewType !== 'table') {
|
if(viewType !== 'grid' && viewType !== 'table') {
|
||||||
console.warn('Тип был неизвестен... Сбрасываю...');
|
console.warn('Тип был неизвестен... Сбрасываю...');
|
||||||
localStorage.setItem('viewType', 'grid')
|
localStorage.setItem('viewType', 'grid')
|
||||||
} else {
|
} else {
|
||||||
this.setState({ viewType });
|
this.setState({ viewType });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
changeViewType = (type: 'grid' | 'table') => {
|
changeViewType = (type: 'grid' | 'table') => {
|
||||||
this.setState({ viewType: type });
|
this.setState({ viewType: type });
|
||||||
localStorage.setItem('viewType', type);
|
localStorage.setItem('viewType', type);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { novels, user, viewType } = 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');
|
||||||
const completed = novels.filter(n => n.status === 'completed');
|
const completed = novels.filter(n => n.status === 'completed');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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={faThLarge} onClick={() => this.changeViewType('grid')}/>
|
||||||
<FontAwesomeIcon icon={faAlignJustify} onClick={() => this.changeViewType('table')}/>
|
<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={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||||
{planned.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
|
{planned.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} 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={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||||
{completed.map(novel => <NovelItem {...novel} key={novel.id} viewType={viewType}/>)}
|
{completed.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} viewType={viewType}/>)}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserNovels;
|
export default UserNovels;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.Group
|
||||||
|
.Settings_Avatar
|
||||||
|
form
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
|
||||||
|
input
|
||||||
|
margin: .5rem 0
|
||||||
|
|
||||||
|
.Button
|
||||||
|
flex-grow: 0
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React, {ChangeEvent, FormEvent} from "react";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import {connect} from "react-redux";
|
||||||
|
import NotFoundError from "../../NotFoundError";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import './Settings.sass';
|
||||||
|
import {fetchUserSettings, updateUserSettings, uploadAvatar} from "../../../api/users";
|
||||||
|
import {setUser} from "../../../store/actions";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
match: { params: { userId: string } }
|
||||||
|
currentUser: UserType
|
||||||
|
setUser: (user: UserType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
// Avatar
|
||||||
|
useGravatar: boolean
|
||||||
|
avatar: File | null
|
||||||
|
}
|
||||||
|
|
||||||
|
class Settings extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
useGravatar: false, avatar: null
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
fetchUserSettings().then(settings => {
|
||||||
|
this.setState({ useGravatar: settings.use_gravatar })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeAvatar = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = event.target.files;
|
||||||
|
if(!files) return;
|
||||||
|
this.setState({ avatar: files[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAvatar = (event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const { avatar, useGravatar } = this.state;
|
||||||
|
const { setUser } = this.props;
|
||||||
|
|
||||||
|
if(avatar) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', avatar);
|
||||||
|
|
||||||
|
uploadAvatar(formData).then(avatarUrl => {
|
||||||
|
updateUserSettings(useGravatar, avatarUrl).then(setUser)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
updateUserSettings(useGravatar, '').then(setUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { currentUser, match: { params: { userId } } } = this.props;
|
||||||
|
const { useGravatar } = this.state
|
||||||
|
|
||||||
|
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group title={'Настройки'}>
|
||||||
|
<Group title={'Аватар'} className={'Settings_Avatar'}>
|
||||||
|
<form onSubmit={this.saveAvatar}>
|
||||||
|
<label>Использовать Gravatar <input type="checkbox" onChange={event => this.setState({ useGravatar: event.target.checked })} checked={useGravatar}/></label>
|
||||||
|
<label>Загрузить аватар <input type="file" onChange={this.onChangeAvatar} disabled={useGravatar}/></label>
|
||||||
|
<Button>Сохранить</Button>
|
||||||
|
</form>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
(state: StoreState) => ({
|
||||||
|
currentUser: state.currentUser
|
||||||
|
}),
|
||||||
|
dispatch => ({
|
||||||
|
setUser: (user: UserType) => dispatch(setUser(user))
|
||||||
|
})
|
||||||
|
)(Settings);
|
||||||
@@ -42,7 +42,10 @@
|
|||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|
||||||
&_Group
|
&_Group
|
||||||
border: 1px solid #eeeeee
|
border-color: #eeeeee
|
||||||
|
border-width: 1px 1px 1px 4px
|
||||||
|
border-style: solid
|
||||||
|
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
padding: .2rem
|
padding: .2rem
|
||||||
text-align: center
|
text-align: center
|
||||||
@@ -3,9 +3,10 @@ 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";
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/Loading";
|
||||||
import NovelItem from "../NovelItem/NovelItem";
|
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
|
import Button from "../../ui/Button/Button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: string } }
|
match: { params: { userId: string } }
|
||||||
@@ -55,7 +56,7 @@ class User extends React.Component<Props, State> {
|
|||||||
<h2>Ваш аккаунт не активирован!</h2>
|
<h2>Ваш аккаунт не активирован!</h2>
|
||||||
<p>Активируйте аккаунт, для доступа ко многим функциям. Так же актвация аккаунта позволит восстановить пароль.</p>
|
<p>Активируйте аккаунт, для доступа ко многим функциям. Так же актвация аккаунта позволит восстановить пароль.</p>
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||||
<button onClick={this.sendActivateLink} style={{ fontSize: '1.2rem' }}>Активировать!</button>
|
<Button onClick={this.sendActivateLink} style={{ fontSize: '1.2rem' }}>Активировать!</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -63,7 +64,7 @@ class User extends React.Component<Props, State> {
|
|||||||
<div className="User__Info" style={style}>
|
<div className="User__Info" style={style}>
|
||||||
{user.cover && <div className="User__Info_Cover" style={{ backgroundImage: `url(${user.cover})` }}/>}
|
{user.cover && <div className="User__Info_Cover" style={{ backgroundImage: `url(${user.cover})` }}/>}
|
||||||
<div className={'User__Info_Main'}>
|
<div className={'User__Info_Main'}>
|
||||||
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar})` }} />
|
<div className="User__Info_Avatar" style={{ backgroundImage: `url(${user.avatar}?s=96&t=${new Date().getTime()})` }} />
|
||||||
<div>
|
<div>
|
||||||
<div className='User__Info_Nick'>{user.username}</div>
|
<div className='User__Info_Nick'>{user.username}</div>
|
||||||
<div className='User__Info_Group' style={{ borderColor: user.group.color, color: user.group.color }}>{user.group.name}</div>
|
<div className='User__Info_Group' style={{ borderColor: user.group.color, color: user.group.color }}>{user.group.name}</div>
|
||||||
@@ -91,7 +92,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} viewType={'grid'} />
|
novels.slice(0, 4).map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id+1} viewType={'grid'} />
|
||||||
) : (
|
) : (
|
||||||
<div>У этого пользователя нет новелл!</div>
|
<div>У этого пользователя нет новелл!</div>
|
||||||
)}
|
)}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
.Users
|
.Users
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
|
||||||
&__User
|
&__User
|
||||||
background-color: var(--sec-bg-color)
|
background-color: var(--sec-bg-color)
|
||||||
padding: .5rem
|
padding: .5rem
|
||||||
|
|
||||||
&_Avatar
|
&_Avatar
|
||||||
width: 96px
|
width: 96px
|
||||||
height: 96px
|
height: 96px
|
||||||
background-size: cover
|
background-size: cover
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
margin-bottom: .5rem
|
margin-bottom: .5rem
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {fetchUsers} from "../../api/users";
|
import {fetchUsers} from "../../api/users";
|
||||||
import Loading from "../Loading";
|
import Loading from "../../ui/Loading";
|
||||||
import './Users.sass';
|
import './Users.sass';
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
users: UserType[]
|
users: UserType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Users extends React.Component<Props, State> {
|
class Users extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
users: []
|
users: []
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
fetchUsers().then(users => this.setState({ users }))
|
fetchUsers().then(users => this.setState({ users }))
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { users } = this.state;
|
const { users } = this.state;
|
||||||
if(users.length === 0) return <Loading/>;
|
if(users.length === 0) return <Loading/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'Users'}>
|
<div className={'Users'}>
|
||||||
{users.map(user => (
|
{users.map(user => (
|
||||||
<Link className={'Users__User'} key={user.id} to={`user/${user.id}`}>
|
<Link className={'Users__User'} key={user.id} to={`user/${user.id}`}>
|
||||||
<div className="Users__User_Avatar" style={{ backgroundImage: `url(${user.avatar})` }}/>
|
<div className="Users__User_Avatar" style={{ backgroundImage: `url(${user.avatar}?s=96?t=${new Date().getTime()})` }}/>
|
||||||
{user.username}
|
{user.username}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Users;
|
export default Users;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
enum ActionTypes {
|
enum ActionTypes {
|
||||||
SET_USER = 'SET_USER',
|
SET_USER = 'SET_USER',
|
||||||
LOGOUT = 'LOGOUT',
|
LOGOUT = 'LOGOUT',
|
||||||
ADD_NOTIFICATION = 'ADD_NOTIFICATION'
|
ADD_NOTIFICATION = 'ADD_NOTIFICATION',
|
||||||
|
SET_MODAL = 'SET_MODAL',
|
||||||
|
HIDE_MODAL = 'HIDE_MODAL'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ActionTypes;
|
export default ActionTypes;
|
||||||
|
|||||||
@@ -14,5 +14,14 @@ const addNotification = (notification: React.ReactNode) => ({
|
|||||||
notification
|
notification
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setModal = (modal: React.ReactNode | null) => ({
|
||||||
|
type: ActionTypes.SET_MODAL,
|
||||||
|
modal
|
||||||
|
});
|
||||||
|
const hideModal = () => ({
|
||||||
|
type: ActionTypes.HIDE_MODAL
|
||||||
|
})
|
||||||
|
|
||||||
export { setUser, logout,
|
export { setUser, logout,
|
||||||
addNotification };
|
addNotification,
|
||||||
|
setModal, hideModal };
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {combineReducers} from "redux";
|
import {combineReducers} from "redux";
|
||||||
import currentUser from "./currentUser";
|
import currentUser from "./currentUser";
|
||||||
import notifications from "./notifications";
|
import notifications from "./notifications";
|
||||||
|
import modal from "./modal";
|
||||||
|
|
||||||
|
export default combineReducers({ currentUser, notifications, modal })
|
||||||
export default combineReducers({ currentUser, notifications })
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ActionTypes from "../actionTypes";
|
||||||
|
|
||||||
|
const initialState: React.ReactNode | null = null;
|
||||||
|
|
||||||
|
type Action = {
|
||||||
|
type: string
|
||||||
|
modal: React.ReactNode | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (state=initialState, action: Action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SET_MODAL: {
|
||||||
|
return action.modal;
|
||||||
|
}
|
||||||
|
case ActionTypes.HIDE_MODAL: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
default: return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,5 +79,8 @@ type PostType = {
|
|||||||
|
|
||||||
type StoreState = {
|
type StoreState = {
|
||||||
currentUser: UserType
|
currentUser: UserType
|
||||||
notifications: React.Component[]
|
notifications: React.ReactNode[]
|
||||||
}
|
modal: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetModalType = (modal: React.ReactNode | null) => void
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.BBCodes
|
||||||
|
font-size: 1.3rem
|
||||||
|
margin-bottom: .2rem
|
||||||
|
|
||||||
|
svg
|
||||||
|
cursor: pointer
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
svg:first-child
|
||||||
|
padding-right: .5rem
|
||||||
|
|
||||||
|
svg:last-child
|
||||||
|
padding-left: .5rem
|
||||||
|
|
||||||
|
svg:not(:first-child):not(:last-child)
|
||||||
|
padding: 0 .5rem
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, {useState} from "react";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faBold,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faItalic,
|
||||||
|
faStrikethrough,
|
||||||
|
faTint,
|
||||||
|
faUnderline
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import './BBEditor.sass';
|
||||||
|
import TextField from "../TextField/TextField";
|
||||||
|
|
||||||
|
interface Props extends React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement> {
|
||||||
|
inputRef: React.RefObject<HTMLTextAreaElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const BBEditor = ({ inputRef, ref, ...props }: Props) => {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
|
||||||
|
const addTag = (event: React.MouseEvent, tag: string, defaultValue?: string) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const [start, end] = [inputRef.current!!.selectionStart, inputRef.current!!.selectionEnd];
|
||||||
|
|
||||||
|
const openTag = `${tag}${defaultValue ? `=${defaultValue}` : ''}`;
|
||||||
|
setText(`${text.slice(0, start)}[${openTag}]${text.slice(start, end)}[/${tag}]${text.slice(end, text.length)}`)
|
||||||
|
inputRef.current!!.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<div className="BBCodes">
|
||||||
|
<FontAwesomeIcon onClick={event => addTag(event, 'b')} icon={faBold}/>
|
||||||
|
<FontAwesomeIcon onClick={event => addTag(event, 'i')} icon={faItalic}/>
|
||||||
|
<FontAwesomeIcon onClick={event => addTag(event, 'u')} icon={faUnderline}/>
|
||||||
|
<FontAwesomeIcon onClick={event => addTag(event, 's')} icon={faStrikethrough}/>
|
||||||
|
<FontAwesomeIcon onClick={event => addTag(event, 'spoiler')} icon={faExclamationTriangle}/>
|
||||||
|
<FontAwesomeIcon onClick={event => addTag(event, 'color', 'white')} icon={faTint}/>
|
||||||
|
</div>
|
||||||
|
<TextField
|
||||||
|
ref={inputRef}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={text}
|
||||||
|
onChange={event => setText(event.target.value)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BBEditor;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@import "../../Constants"
|
||||||
|
|
||||||
|
.Button
|
||||||
|
cursor: pointer
|
||||||
|
padding: .2rem
|
||||||
|
border-radius: $borderRadius
|
||||||
|
background-color: var(--block-color)
|
||||||
|
border: 1px solid var(--main-color)
|
||||||
|
color: var(--main-color)
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
color: var(--sec-color)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: var(--block-sec-color)
|
||||||
|
color: var(--link-color)
|
||||||
|
border: 1px solid var(--link-color)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React, {ButtonHTMLAttributes} from "react";
|
||||||
|
import './Button.sass';
|
||||||
|
import classNames from "../../classNames";
|
||||||
|
|
||||||
|
const Button = (restProps: ButtonHTMLAttributes<HTMLElement>) => {
|
||||||
|
const { className, children, ...props } = restProps;
|
||||||
|
return (
|
||||||
|
<button className={classNames('Button', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
.Comment
|
.Comment
|
||||||
display: flex
|
display: flex
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.Buttons
|
||||||
|
display: flex
|
||||||
|
justify-content: flex-end
|
||||||
|
|
||||||
|
.Button
|
||||||
|
margin: 1rem .5rem 0
|
||||||
|
font-size: 1.3rem
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Button from "../Button/Button";
|
||||||
|
import './ConfirmPopout.sass'
|
||||||
|
import '../Modal/Modal.sass';
|
||||||
|
import {connect} from "react-redux";
|
||||||
|
import {hideModal} from "../../store/actions";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hideModal: () => void
|
||||||
|
onConfirm?: () => void
|
||||||
|
onDismiss?: () => void
|
||||||
|
title?: string
|
||||||
|
children: any
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfirmPopout extends React.Component<Props> {
|
||||||
|
onConfirm = () => {
|
||||||
|
const { hideModal, onConfirm } = this.props;
|
||||||
|
if(onConfirm) onConfirm();
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDismiss = () => {
|
||||||
|
const { hideModal, onDismiss } = this.props;
|
||||||
|
if(onDismiss) onDismiss();
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children, title } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Modal__Overlay">
|
||||||
|
<div className={"Modal"}>
|
||||||
|
{title && <div className="Modal__Header">
|
||||||
|
{title}
|
||||||
|
</div>}
|
||||||
|
{children}
|
||||||
|
<div className={'Buttons'}>
|
||||||
|
<Button onClick={this.onConfirm}>Да</Button>
|
||||||
|
<Button onClick={this.onDismiss}>Нет</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null,
|
||||||
|
dispatch => ({
|
||||||
|
hideModal: () => dispatch(hideModal())
|
||||||
|
})
|
||||||
|
)(ConfirmPopout);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@import "../../Constants"
|
||||||
|
|
||||||
|
.Group
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
|
border-radius: $borderRadius
|
||||||
|
padding: .5rem
|
||||||
|
|
||||||
|
&__Title
|
||||||
|
font-size: 1.2rem
|
||||||
|
font-weight: 600
|
||||||
|
margin-bottom: .5rem
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React, {HTMLAttributes} from "react";
|
||||||
|
import classNames from "../../classNames";
|
||||||
|
import './Group.sass'
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLElement> {
|
||||||
|
title: string
|
||||||
|
direction?: 'column' | 'row'
|
||||||
|
}
|
||||||
|
|
||||||
|
const Group = (restProps: Props) => {
|
||||||
|
const { title, direction, className, children, ...props } = restProps;
|
||||||
|
let style: React.CSSProperties = {};
|
||||||
|
if(direction === 'column') {
|
||||||
|
style = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classNames('Group', className)} {...props}>
|
||||||
|
<div className="Group__Title">{title}</div>
|
||||||
|
<div style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Group;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.Input
|
||||||
|
background-color: var(--sec-block-color) !important
|
||||||
|
border: none
|
||||||
|
border-bottom: 1px solid var(--main-color) !important
|
||||||
|
color: var(--main-color)
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
color: var(--sec-color)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React, {InputHTMLAttributes} from "react";
|
||||||
|
import classNames from "../../classNames";
|
||||||
|
import './Input.sass';
|
||||||
|
|
||||||
|
const Input = (restProps: InputHTMLAttributes<HTMLElement>) => {
|
||||||
|
const { className, ...props } = restProps;
|
||||||
|
return (
|
||||||
|
<input className={classNames('Input', className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Loader from 'react-loader-spinner'
|
import Loader from 'react-loader-spinner'
|
||||||
|
|
||||||
const Loading: React.FC = () => (
|
const Loading: React.FC = () => (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Loader color='#aaa' type='Rings' style={{ height: '100%' }} />
|
<Loader color='#aaa' type='Rings' style={{ height: '100%' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Loading;
|
export default Loading;
|
||||||