v1.1.2; bugfixes; admin panel improvement; small folder re-struct
This commit is contained in:
+16
-17
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.32",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.13",
|
||||
"@sentry/react": "5.28.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.34",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@sentry/react": "6.2.0",
|
||||
"bbcode-to-react": "0.2.9",
|
||||
"eruda": "2.4.1",
|
||||
"eruda-dom": "2.0.0",
|
||||
@@ -15,23 +14,23 @@
|
||||
"eruda-memory": "2.0.0",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-loader-spinner": "3.1.14",
|
||||
"react-loader-spinner": "4.0.0",
|
||||
"react-redux": "7.2.2",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-snowfall": "^1.0.2",
|
||||
"react-snowfall": "1.0.2",
|
||||
"redux": "4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "14.14.9",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/react-redux": "7.1.11",
|
||||
"@types/react-router-dom": "5.1.6",
|
||||
"@types/react-transition-group": "4.4.0",
|
||||
"react-scripts": "3.4.4",
|
||||
"@types/node": "14.14.31",
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.1",
|
||||
"@types/react-redux": "7.1.16",
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/react-transition-group": "4.4.1",
|
||||
"react-scripts": "4.0.2",
|
||||
"redux-devtools-extension": "2.13.8",
|
||||
"sass": "1.29.0",
|
||||
"typescript": "4.1.2"
|
||||
"sass": "1.32.8",
|
||||
"typescript": "4.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.space/v1';
|
||||
|
||||
// function loadRequestFromCache<T>(url: string): T | null {
|
||||
// let cached = sessionStorage.getItem(url);
|
||||
// if(!cached) return null;
|
||||
// return JSON.parse(cached) as T;
|
||||
// }
|
||||
|
||||
async function request<T>(url: string, method: string, body?: string | FormData): Promise<T> {
|
||||
let bUrl = baseUrl;
|
||||
if(baseUrl.endsWith('/'))
|
||||
@@ -7,6 +13,12 @@ async function request<T>(url: string, method: string, body?: string | FormData)
|
||||
if(url.startsWith('/'))
|
||||
url = url.slice(1, baseUrl.length);
|
||||
|
||||
// const cached = method.toLowerCase() === 'get' ? loadRequestFromCache<T>(url) : null;
|
||||
// if(cached) {
|
||||
// console.debug(`${baseUrl}/${url} loaded from session cache!`);
|
||||
// return Promise.resolve(cached);
|
||||
// }
|
||||
|
||||
const res = await fetch(`${bUrl}/${url}`, {
|
||||
method, body,
|
||||
credentials: "include",
|
||||
@@ -28,6 +40,7 @@ async function request<T>(url: string, method: string, body?: string | FormData)
|
||||
if(json?.error) return Promise.reject(json.error_data);
|
||||
if(!res.ok) return Promise.reject(res.statusText);
|
||||
if(!json.data) return Promise.reject(null);
|
||||
sessionStorage.setItem(url, JSON.stringify(json.data));
|
||||
return Promise.resolve(json.data);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { del, get, put } from './api';
|
||||
import { del, get, post, put } from './api';
|
||||
|
||||
export const fetchNews = () => get<PostType[]>('news');
|
||||
export const createPost = (p: PostType) => post<PostType>('news', p);
|
||||
|
||||
export const fetchPost = (id: number) => get<PostType>(`news/${id}`);
|
||||
export const updatePost = (post: PostType) => put<PostType>(`news/${post.id}`, post);
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
.Footer
|
||||
background: var(--nav-bg-color)
|
||||
display: flex
|
||||
flex-direction: column
|
||||
position: relative
|
||||
bottom: 0
|
||||
padding: 1rem
|
||||
|
||||
.Footer__Tabs
|
||||
display: flex
|
||||
|
||||
@media (max-width: 600px)
|
||||
flex-direction: column
|
||||
|
||||
.Footer__Tab
|
||||
font-weight: 300
|
||||
display: flex
|
||||
flex-direction: column
|
||||
margin-right: 4rem
|
||||
|
||||
.Footer__Tab_Title
|
||||
color: var(--main-color)
|
||||
font-weight: 400
|
||||
margin-bottom: 1rem
|
||||
|
||||
.Link
|
||||
margin-bottom: 1rem
|
||||
|
||||
.Links
|
||||
display: flex
|
||||
|
||||
.Link
|
||||
height: 46px
|
||||
width: 46px
|
||||
font-size: 1rem
|
||||
background-color: var(--block-color)
|
||||
border-radius: 50%
|
||||
margin-right: 1rem
|
||||
|
||||
&:hover
|
||||
background-color: var(--sec-bg-color)
|
||||
|
||||
svg
|
||||
position: relative
|
||||
top: 14px
|
||||
left: 14px
|
||||
width: 18px
|
||||
height: 18px
|
||||
|
||||
a
|
||||
color: var(--sec-color)
|
||||
|
||||
&:hover
|
||||
color: var(--main-color)
|
||||
|
||||
.Footer__Bottom
|
||||
border-top: 1px solid var(--main-color)
|
||||
padding: 2rem 0
|
||||
.Footer
|
||||
background: var(--nav-bg-color)
|
||||
display: flex
|
||||
flex-direction: column
|
||||
position: relative
|
||||
bottom: 0
|
||||
padding: 1rem
|
||||
|
||||
.Footer__Tabs
|
||||
display: flex
|
||||
|
||||
@media (max-width: 600px)
|
||||
flex-direction: column
|
||||
|
||||
.Footer__Tab
|
||||
font-weight: 300
|
||||
display: flex
|
||||
flex-direction: column
|
||||
margin-right: 4rem
|
||||
|
||||
.Footer__Tab_Title
|
||||
color: var(--main-color)
|
||||
font-weight: 400
|
||||
margin-bottom: 1rem
|
||||
|
||||
.Link
|
||||
margin-bottom: 1rem
|
||||
|
||||
.Links
|
||||
display: flex
|
||||
|
||||
.Link
|
||||
height: 46px
|
||||
width: 46px
|
||||
font-size: 1rem
|
||||
background-color: var(--block-color)
|
||||
border-radius: 50%
|
||||
margin-right: 1rem
|
||||
|
||||
&:hover
|
||||
background-color: var(--sec-bg-color)
|
||||
|
||||
svg
|
||||
position: relative
|
||||
top: 14px
|
||||
left: 14px
|
||||
width: 18px
|
||||
height: 18px
|
||||
|
||||
a
|
||||
color: var(--sec-color)
|
||||
|
||||
&:hover
|
||||
color: var(--main-color)
|
||||
|
||||
.Footer__Bottom
|
||||
border-top: 1px solid var(--main-color)
|
||||
padding: 2rem 0
|
||||
text-align: center
|
||||
@@ -1,40 +1,40 @@
|
||||
import React from 'react';
|
||||
import './Footer.sass';
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDiscord, faTelegramPlane, faVk } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
<div className='Footer'>
|
||||
<footer className='Footer__Tabs'>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Команда</div>
|
||||
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
||||
<Link to={'/user/6'} className={'Link'}>RonFall</Link>
|
||||
</div>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Соцсети</div>
|
||||
|
||||
<div className="Links">
|
||||
<a href="https://vk.com/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faVk}/></div>
|
||||
</a>
|
||||
<a href="https://discord.gg/4CA8Cju">
|
||||
<div className="Link"><FontAwesomeIcon icon={faDiscord}/></div>
|
||||
</a>
|
||||
<a href="https://t.me/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div className="Footer__Bottom">
|
||||
Unmei © 2019-2021
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
||||
import React from 'react';
|
||||
import './Footer.sass';
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDiscord, faTelegramPlane, faVk } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
<div className='Footer'>
|
||||
<footer className='Footer__Tabs'>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Команда</div>
|
||||
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
||||
<Link to={'/user/6'} className={'Link'}>RonFall</Link>
|
||||
</div>
|
||||
<div className="Footer__Tab">
|
||||
<div className="Footer__Tab_Title">Соцсети</div>
|
||||
|
||||
<div className="Links">
|
||||
<a href="https://vk.com/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faVk}/></div>
|
||||
</a>
|
||||
<a href="https://discord.gg/4CA8Cju">
|
||||
<div className="Link"><FontAwesomeIcon icon={faDiscord}/></div>
|
||||
</a>
|
||||
<a href="https://t.me/unmei_site">
|
||||
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div className="Footer__Bottom">
|
||||
Unmei © 2019-2021
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import Loader from 'react-loader-spinner'
|
||||
|
||||
const Loading: React.FC = () => (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Loader color='#aaa' type='Rings'/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import Loader from 'react-loader-spinner'
|
||||
|
||||
const Loading: React.FC = () => (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Loader color='#aaa' type='Rings'/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
@@ -6,8 +6,8 @@ import { Link } from "react-router-dom";
|
||||
import { hasAccessToAdminPanel } from "../../utils";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBars } from '@fortawesome/free-solid-svg-icons';
|
||||
import RegisterModal from "../Modals/RegisterModal";
|
||||
import LoginModal from "../Modals/LoginModal";
|
||||
import RegisterModal from "../../pages/Modals/RegisterModal";
|
||||
import LoginModal from "../../pages/Modals/LoginModal";
|
||||
import { logout } from "../../store/ducks/currentUser";
|
||||
import { setModal } from "../../store/ducks/modal";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { activateAccount } from "../api/users";
|
||||
import { connect } from "react-redux";
|
||||
import errors from "../api/errors";
|
||||
import Loading from "../ui/Loading";
|
||||
import Loading from "../components/Loading";
|
||||
import { addNotification } from "../store/ducks/notifications";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,51 +1,28 @@
|
||||
import { connect } from "react-redux";
|
||||
import React from "react";
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import NotFoundError from "../../components/NotFoundError";
|
||||
import { hasAccessToAdminPanel } from "../../utils";
|
||||
import './AdminPanel.sass';
|
||||
import { Link } from "react-router-dom";
|
||||
import AdminPanelUsers from "./Users/AdminPanelUsers";
|
||||
import AdminPanelNovels from "./Novels/AdminPanelNovels";
|
||||
import ModifyUser from "./Users/ModifyUser";
|
||||
import { Link, Route, Switch } from "react-router-dom";
|
||||
import APNews from "./News/APNews";
|
||||
import APModifyPost from "./News/APModifyPost";
|
||||
import APAddPost from "./News/APAddPost";
|
||||
import APNovels from "./Novels/APNovels";
|
||||
import APUsers from "./Users/APUsers";
|
||||
import APModifyUser from "./Users/APModifyUser";
|
||||
|
||||
type Props = {
|
||||
user: UserType
|
||||
match: { path: string }
|
||||
location: { pathname: string }
|
||||
}
|
||||
|
||||
type State = {}
|
||||
|
||||
class AdminPanel extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { user, match: { path }, location: { pathname } } = this.props;
|
||||
const { user, match: { path } } = this.props;
|
||||
if(!user || !user.group) return <NotFoundError/>;
|
||||
if(!hasAccessToAdminPanel(user)) return <NotFoundError/>;
|
||||
const subPath = pathname.slice(path.length + 1);
|
||||
let component;
|
||||
switch(subPath) {
|
||||
case 'users':
|
||||
component = <AdminPanelUsers path={`${path}/${subPath}`}/>;
|
||||
break;
|
||||
case 'novels':
|
||||
component = <AdminPanelNovels/>;
|
||||
break;
|
||||
case 'news':
|
||||
component = <APNews path={`${path}/${subPath}`}/>;
|
||||
break;
|
||||
default: {
|
||||
const users = /users\/(\d+)/;
|
||||
const news = /news\/(\d+)/;
|
||||
|
||||
const userMatch = subPath.match(users);
|
||||
const newsMatch = subPath.match(news);
|
||||
|
||||
if(userMatch) component = <ModifyUser userId={parseInt(userMatch[1])}/>;
|
||||
if(newsMatch) component = <APModifyPost postId={parseInt(newsMatch[1])}/>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'AdminPanel'}>
|
||||
@@ -57,7 +34,20 @@ class AdminPanel extends React.Component<Props, State> {
|
||||
<Link to={`${path}/novels`}>Новелы</Link>
|
||||
<Link to={`${path}/news`}>Новости</Link>
|
||||
</div>
|
||||
{component}
|
||||
|
||||
<Switch>
|
||||
<Route exact path={`${path}/news`} component={APNews}/>
|
||||
<Route exact path={`${path}/news/new`} component={APAddPost}/>
|
||||
<Route exact path={`${path}/news/:id`} component={APModifyPost}/>
|
||||
|
||||
<Route exact path={`${path}/novels`} component={APNovels}/>
|
||||
<Route exact path={`${path}/novels/new`} component={NotFoundError}/>
|
||||
<Route exact path={`${path}/novels/:id`} component={NotFoundError}/>
|
||||
|
||||
<Route exact path={`${path}/users`} component={APUsers}/>
|
||||
<Route exact path={`${path}/users/new`} component={NotFoundError}/>
|
||||
<Route exact path={`${path}/users/:id`} component={APModifyUser}/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
import Group from "../../../ui/Group/Group";
|
||||
import { createPost } 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";
|
||||
import { connect } from "react-redux";
|
||||
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||
import { setModal } from "../../../store/ducks/modal";
|
||||
import { addNotification } from "../../../store/ducks/notifications";
|
||||
|
||||
type Props = {
|
||||
match: { path: string }
|
||||
history: { push: (path: string) => void }
|
||||
setPopout: (modal: React.ReactNode | null) => void
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
post: PostType
|
||||
title: string
|
||||
|
||||
previewShort: string
|
||||
previewFull: string
|
||||
};
|
||||
|
||||
class APModifyPost extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
post: {} as PostType, title: '',
|
||||
previewShort: '', previewFull: ''
|
||||
}
|
||||
private shortPost = React.createRef<HTMLTextAreaElement>();
|
||||
private fullPost = React.createRef<HTMLTextAreaElement>();
|
||||
|
||||
savePost = async(event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { post } = this.state;
|
||||
if(!this.shortPost.current || !this.fullPost.current)
|
||||
return;
|
||||
|
||||
post.short_post = this.shortPost.current.value;
|
||||
post.full_post = this.fullPost.current.value;
|
||||
const { addNotification } = this.props;
|
||||
|
||||
createPost(post).then(createdPost => {
|
||||
const notification = (
|
||||
<NotificationMessage level={"success"}>
|
||||
Успешно создана новость с ID {createdPost.id}!
|
||||
</NotificationMessage>
|
||||
);
|
||||
addNotification(notification);
|
||||
const path = this.props.match.path.split('/');
|
||||
this.props.history.push(`/${path[1]}/${path[2]}`);
|
||||
});
|
||||
}
|
||||
|
||||
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.title = event.target.value;
|
||||
document.title = event.target.value || 'Unmei';
|
||||
this.setState({ post });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { post, title, previewShort, previewFull } = this.state;
|
||||
|
||||
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 style={{ marginRight: '1rem' }}>Сохранить</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null,
|
||||
dispatch => ({
|
||||
setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
||||
})
|
||||
)(APModifyPost);
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
import Group from "../../../ui/Group/Group";
|
||||
import NotFoundError from "../../NotFoundError";
|
||||
import NotFoundError from "../../../components/NotFoundError";
|
||||
import { deletePost, 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";
|
||||
@@ -17,7 +16,8 @@ import { setModal } from "../../../store/ducks/modal";
|
||||
import { addNotification } from "../../../store/ducks/notifications";
|
||||
|
||||
type Props = {
|
||||
postId: number
|
||||
match: { params: { id: number }, path: string }
|
||||
history: { push: (path: string) => void }
|
||||
setPopout: (modal: React.ReactNode | null) => void
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
};
|
||||
@@ -39,8 +39,9 @@ class APModifyPost extends React.Component<Props, State> {
|
||||
private fullPost = React.createRef<HTMLTextAreaElement>();
|
||||
|
||||
componentDidMount() {
|
||||
const { postId } = this.props;
|
||||
fetchPost(postId).then((post: PostType) => {
|
||||
const { match } = this.props;
|
||||
|
||||
fetchPost(match.params.id).then((post: PostType) => {
|
||||
this.setState({ post, title: post.title });
|
||||
});
|
||||
}
|
||||
@@ -61,7 +62,7 @@ class APModifyPost extends React.Component<Props, State> {
|
||||
<NotificationMessage level={"success"}>
|
||||
Успешно!
|
||||
</NotificationMessage>
|
||||
)
|
||||
);
|
||||
addNotification(notification)
|
||||
});
|
||||
}
|
||||
@@ -88,14 +89,21 @@ class APModifyPost extends React.Component<Props, State> {
|
||||
const post = this.state.post;
|
||||
if(!post) return;
|
||||
post.title = event.target.value;
|
||||
document.title = event.target.value || 'Unmei';
|
||||
this.setState({ post });
|
||||
}
|
||||
|
||||
deletePost = () => {
|
||||
const { postId, setPopout } = this.props;
|
||||
deletePost = async (postId: number) => {
|
||||
await deletePost(postId);
|
||||
const path = this.props.match.path.split('/')
|
||||
this.props.history.push(`/${path[1]}/${path[2]}`);
|
||||
}
|
||||
|
||||
_deletePost = () => {
|
||||
const { match: { params: { id} }, setPopout } = this.props;
|
||||
|
||||
const popout = (
|
||||
<ConfirmPopout onConfirm={() => deletePost(postId)}>
|
||||
<ConfirmPopout onConfirm={() => this.deletePost(id)}>
|
||||
Вы уверены?
|
||||
</ConfirmPopout>
|
||||
);
|
||||
@@ -135,7 +143,7 @@ class APModifyPost extends React.Component<Props, State> {
|
||||
<div>
|
||||
<Button style={{ marginRight: '1rem' }}>Сохранить</Button>
|
||||
<Button type={"button"} style={{ color: 'var(--error-bg-color)' }}
|
||||
onClick={this.deletePost}>Удалить</Button>
|
||||
onClick={this._deletePost}>Удалить</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Group>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
|
||||
import Button from "../../../ui/Button/Button";
|
||||
|
||||
type Props = {
|
||||
path: string
|
||||
match: { path: string }
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -23,11 +23,13 @@ class APNews extends React.Component<Props, State> {
|
||||
|
||||
render() {
|
||||
const { news } = this.state;
|
||||
const { path } = this.props;
|
||||
const { match: { path } } = this.props;
|
||||
|
||||
return (
|
||||
<Group title={'Новости'}>
|
||||
<Button>Добавить новость</Button>
|
||||
<Link to={`${path}/new`}>
|
||||
<Button>Добавить новость</Button>
|
||||
</Link>
|
||||
{news.map(post => (
|
||||
<Link to={`${path}/${post.id}`} key={post.id}>
|
||||
<div style={{ display: "flex" }}>
|
||||
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
type Props = {};
|
||||
|
||||
type State = {};
|
||||
|
||||
class AdminPanelNovels extends React.Component<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<div>Novels</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(AdminPanelNovels);
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
type Props = {};
|
||||
|
||||
type State = {};
|
||||
|
||||
class APNovels extends React.Component<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<div>Novels</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(APNovels);
|
||||
+98
-98
@@ -1,98 +1,98 @@
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { fetchUser, updateUser } from "../../../api/users";
|
||||
import Loading from "../../../ui/Loading";
|
||||
import './APUser.sass';
|
||||
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||
import Button from "../../../ui/Button/Button";
|
||||
import { addNotification } from "../../../store/ducks/notifications";
|
||||
|
||||
type Props = {
|
||||
userId: number
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
user: UserType | null
|
||||
username: string
|
||||
};
|
||||
|
||||
class ModifyUser extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
user: null, username: ''
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { userId } = this.props;
|
||||
fetchUser(userId).then((user: UserType) => {
|
||||
this.setState({ user, username: user.username });
|
||||
document.title = `${user.username} / Админ-панель`;
|
||||
})
|
||||
}
|
||||
|
||||
onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const user = Object.assign({}, this.state.user);
|
||||
const target = event.target;
|
||||
// @ts-ignore
|
||||
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||
this.setState({ user })
|
||||
}
|
||||
|
||||
onSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const { user } = this.state;
|
||||
const { addNotification } = this.props;
|
||||
|
||||
if(user === null) {
|
||||
console.error('Пользователь - null!');
|
||||
return;
|
||||
}
|
||||
updateUser(user).then(() => {
|
||||
const notification = (
|
||||
<NotificationMessage level={"success"}>
|
||||
Пользователь успешно изменен!
|
||||
</NotificationMessage>
|
||||
)
|
||||
addNotification(notification)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, username } = this.state;
|
||||
|
||||
if(!user) return <Loading/>;
|
||||
return (
|
||||
<div className={'APUser__Modify'}>
|
||||
<div className={'APUser__Modify_Title'}>{username}</div>
|
||||
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
|
||||
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'}
|
||||
value={user.username}/></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>
|
||||
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<Button>Сохранить!</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
}),
|
||||
(dispatch) => ({
|
||||
addNotification: (notification: React.ReactNode) => {
|
||||
dispatch(addNotification(notification))
|
||||
}
|
||||
})
|
||||
)(ModifyUser);
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { fetchUser, updateUser } from "../../../api/users";
|
||||
import Loading from "../../../components/Loading";
|
||||
import './APUser.sass';
|
||||
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||
import Button from "../../../ui/Button/Button";
|
||||
import { addNotification } from "../../../store/ducks/notifications";
|
||||
|
||||
type Props = {
|
||||
match: { params: { id: number } }
|
||||
addNotification: (notification: React.ReactNode) => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
user: UserType | null
|
||||
username: string
|
||||
};
|
||||
|
||||
class APModifyUser extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
user: null, username: ''
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { match: { params: { id } } } = this.props;
|
||||
fetchUser(id).then((user: UserType) => {
|
||||
this.setState({ user, username: user.username });
|
||||
document.title = `${user.username} / Админ-панель`;
|
||||
})
|
||||
}
|
||||
|
||||
onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const user = Object.assign({}, this.state.user);
|
||||
const target = event.target;
|
||||
// @ts-ignore
|
||||
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||
this.setState({ user })
|
||||
}
|
||||
|
||||
onSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const { user } = this.state;
|
||||
const { addNotification } = this.props;
|
||||
|
||||
if(user === null) {
|
||||
console.error('Пользователь - null!');
|
||||
return;
|
||||
}
|
||||
updateUser(user).then(() => {
|
||||
const notification = (
|
||||
<NotificationMessage level={"success"}>
|
||||
Пользователь успешно изменен!
|
||||
</NotificationMessage>
|
||||
)
|
||||
addNotification(notification)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, username } = this.state;
|
||||
|
||||
if(!user) return <Loading/>;
|
||||
return (
|
||||
<div className={'APUser__Modify'}>
|
||||
<div className={'APUser__Modify_Title'}>{username}</div>
|
||||
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
|
||||
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'}
|
||||
value={user.username}/></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>
|
||||
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<Button>Сохранить!</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
}),
|
||||
(dispatch) => ({
|
||||
addNotification: (notification: React.ReactNode) => {
|
||||
dispatch(addNotification(notification))
|
||||
}
|
||||
})
|
||||
)(APModifyUser);
|
||||
@@ -24,4 +24,4 @@
|
||||
width: 100%
|
||||
|
||||
&[type='checkbox']
|
||||
width: 0
|
||||
width: 16px
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { fetchUsers } from "../../../api/users";
|
||||
import { Link } from "react-router-dom";
|
||||
import Group from "../../../ui/Group/Group";
|
||||
|
||||
type Props = {
|
||||
match: { path: string }
|
||||
};
|
||||
|
||||
type State = {
|
||||
users: UserType[]
|
||||
};
|
||||
|
||||
class APUsers extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
users: []
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetchUsers().then(users => this.setState({ users }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { users } = this.state;
|
||||
const { match: { path } } = this.props;
|
||||
|
||||
return (
|
||||
<Group title={'Пользователи'}>
|
||||
{users.sort((a, b) => a.id - b.id).map(user => (
|
||||
<Link to={`${path}/${user.id}`} key={user.id}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{user.id}</div>
|
||||
<div>{user.username}</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(APUsers);
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { fetchUsers } from "../../../api/users";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
path: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
users: UserType[]
|
||||
};
|
||||
|
||||
class AdminPanelUsers extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
users: []
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetchUsers().then(users => this.setState({ users }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { users } = this.state;
|
||||
const { path } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ textAlign: "center", fontWeight: 600, fontSize: '1.5rem' }}>Пользователи</div>
|
||||
<div>
|
||||
{users.sort((a, b) => a.id - b.id).map(user => (
|
||||
<Link to={`${path}/${user.id}`} key={user.id}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{user.id}</div>
|
||||
<div>{user.username}</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.currentUser
|
||||
})
|
||||
)(AdminPanelUsers);
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import './App.sass';
|
||||
import Navbar from "../Navbar/Navbar";
|
||||
import Navbar from "../../components/Navbar/Navbar";
|
||||
import Novel from "../Novel/Novel";
|
||||
import User from "../User/User";
|
||||
import Main from "../Main/Main"
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import NotFoundError from "../../components/NotFoundError";
|
||||
import { fetchCurrentUser, fetchUserSettings } from "../../api/users";
|
||||
import { connect } from "react-redux";
|
||||
import Character from "../Character/Character";
|
||||
import SpoilerTag from "../../ui/Spoiler/SpoilerTag";
|
||||
import Novels from "../Novels/Novels";
|
||||
import Footer from '../Footer/Footer';
|
||||
import Footer from '../../components/Footer/Footer';
|
||||
import UserNovels from '../User/Novels/UserNovels';
|
||||
import ColorTag from "../../ui/Tags/ColorTag";
|
||||
import ActivateAccount from "../ActivateAccount";
|
||||
|
||||
@@ -4,8 +4,8 @@ import './Character.sass'
|
||||
// @ts-ignore
|
||||
import parser from 'bbcode-to-react';
|
||||
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||
import Loading from "../../ui/Loading";
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import Loading from "../../components/Loading";
|
||||
import NotFoundError from "../../components/NotFoundError";
|
||||
|
||||
type Props = {
|
||||
match: { params: { charId: number } }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { getClub } from "../../api/clubs";
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
import Group from "../../ui/Group/Group";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import Comment from '../../ui/Comment/Comment';
|
||||
import './Comments.sass';
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
import { fetchUser } from "../../api/users";
|
||||
import { connect } from "react-redux";
|
||||
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
||||
@@ -71,9 +71,8 @@ class Comments extends React.Component<Props, State> {
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>) {
|
||||
const { comments } = this.props;
|
||||
const { comments: prevComments } = prevProps;
|
||||
|
||||
if(prevComments.length !== comments.length) {
|
||||
if(prevProps.comments.length !== comments.length) {
|
||||
let { users } = this.state;
|
||||
|
||||
comments.forEach(async comment => {
|
||||
@@ -89,7 +88,8 @@ class Comments extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { user, comments, hasMore, loadMore } = this.props;
|
||||
const { users, commentText } = this.state;
|
||||
if(!user) return <Loading/>;
|
||||
if(!user)
|
||||
return <Loading/>;
|
||||
|
||||
return (
|
||||
<div className="Comments">
|
||||
@@ -120,12 +120,13 @@ class Comments extends React.Component<Props, State> {
|
||||
{hasMore && <Button onClick={loadMore}>Загрузить ещё</Button>}
|
||||
</div>
|
||||
</form>}
|
||||
{!comments && (
|
||||
{comments.length === 0 && (
|
||||
<Loading/>
|
||||
)}
|
||||
{comments && comments.length > 0 ? (
|
||||
comments.map(comment => <Comment text={comment.text} key={comment.id}
|
||||
user={users.get(comment.user_id)}/>)
|
||||
{comments.length > 0 ? (
|
||||
comments.map(comment => (
|
||||
<Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)}/>
|
||||
))
|
||||
) : (
|
||||
'Никто не оставил комментарий к этой новелле... :c'
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { fetchNews } from "../../api/news";
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
import { Link } from "react-router-dom";
|
||||
import NewsPost from "../../ui/NewsPost/NewsPost";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
import { fetchNews } from "../../api/news";
|
||||
import NewsPost from "../../ui/NewsPost/NewsPost";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { fetchPost } from "../../api/news";
|
||||
import Loading from "../../ui/Loading";
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import Loading from "../../components/Loading";
|
||||
import NotFoundError from "../../components/NotFoundError";
|
||||
import './Post.sass'
|
||||
import { Link } from "react-router-dom";
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import './Novel.sass'
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import NotFoundError from "../../components/NotFoundError";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
createUserNovel,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
updateUserNovel
|
||||
} from "../../api/novels";
|
||||
import { Link } from "react-router-dom";
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
import { TranslateExitStatus, TranslatePlatform, TranslateStatus } from "../../api/api";
|
||||
import Comments from "../Comments/Comments";
|
||||
import LoadingModal from "../Modals/LoadingModal";
|
||||
|
||||
@@ -2,7 +2,7 @@ import './Novels.sass'
|
||||
import React from "react";
|
||||
import { fetchNovels } from "../../api/novels";
|
||||
import NovelItem from '../../ui/NovelItem/NovelItem';
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
|
||||
type State = {
|
||||
novels: NovelType[] | null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import './UserNovels.sass';
|
||||
import { fetchUser, fetchUserNovels } from '../../../api/users';
|
||||
import Loading from '../../../ui/Loading';
|
||||
import Loading from '../../../components/Loading';
|
||||
import NovelItem from '../../../ui/NovelItem/NovelItem';
|
||||
import { capitalize, generateClassName, getRandomInt } from "../../../utils";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import Group from "../../../ui/Group/Group";
|
||||
import { connect } from "react-redux";
|
||||
import NotFoundError from "../../NotFoundError";
|
||||
import NotFoundError from "../../../components/NotFoundError";
|
||||
import './Settings.sass';
|
||||
import Tabs from "../../../ui/Tabs/Tabs";
|
||||
import TabItem from "../../../ui/Tabs/TabItem";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { CSSProperties } from "react";
|
||||
import { fetchUser, fetchUserNovels, generateActivateLink } from "../../api/users";
|
||||
import './User.sass'
|
||||
import NotFoundError from "../NotFoundError";
|
||||
import NotFoundError from "../../components/NotFoundError";
|
||||
import { Link } from "react-router-dom";
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||
import { connect } from "react-redux";
|
||||
import Button from "../../ui/Button/Button";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { fetchUsers } from "../../api/users";
|
||||
import Loading from "../../ui/Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
import './Users.sass';
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import './Comment.sass'
|
||||
import { Link } from "react-router-dom";
|
||||
// @ts-ignore
|
||||
import parser from 'bbcode-to-react';
|
||||
import Loading from "../Loading";
|
||||
import Loading from "../../components/Loading";
|
||||
|
||||
type Props = {
|
||||
user: UserType | undefined
|
||||
|
||||
+3
-2
@@ -16,8 +16,9 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"skipLibCheck": true
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Reference in New Issue
Block a user